Назад в Центр знаний
Отпечатки

Таблицы шрифтов CreepJS: 51 имя и 14 вариантов возврата

CreepJS проверяет 51 имя шрифтов, чтобы вывести версию Windows или macOS. Запуск таблиц показывает, что каждый ответ требует полного базового набора.

BotBrowser Team

Документация

Нужна структурированная документация по теме Отпечатки?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

BotBrowser может воспроизвести объявленный профиль на страничной проверке шрифтов и сохранить ограниченную запись найденных семейств, но он не может сделать так, чтобы установленный набор шрифтов описывал другую операционную систему. CreepJS читает фиксированный список имён шрифтов и превращает найденные страницей имена в версию Windows или macOS. Запуск опубликованных таблиц показывает, что поиск строже, чем подсказывает форма таблиц: каждая версия, которую он может назвать, требует полного базового набора шрифтов, поэтому одно случайное семейство не решает ничего.

Три имени Android-шрифтов в этом источнике не указывают, какой именно встроенный контекст их показал. В статье «CreepJS и границы сигналов Android WebView» значения страницы отделены от заголовков запроса и необязательного моста приложения.

Посмотреть видео

В этом видео на английском рассматривается опубликованный поиск по таблицам шрифтов. В исходном коде перечислены 13 именованных версий Windows и macOS; четырнадцатый возможный возврат: undefined, если ни один полный базовый набор не совпал. Озвучка английская. Воспроизводимая запись использует синтетические наборы имён шрифтов и не заявляет об измерении шрифтов в работающем браузере.

Кратко

  • Опубликованный модуль объявляет 51 имя шрифтов в пяти таблицах: 14 Windows, 18 macOS, 9 приложений рабочего стола, 7 Linux и 3 Android.
  • Эти 51 имя уже уникальны: ни одно имя не встречается в двух таблицах, поэтому схлопывать до проверки нечего.
  • Таблица Windows делит свои 14 имён на пять ведер версий, и самое маленькое ведро содержит одно имя.
  • Таблица macOS делит свои 18 имён на восемь ведер, два из которых тоже содержат одно имя.
  • Ведро из одного имени необходимо, но никогда не достаточно: в карте Windows есть записи только для ключей, содержащих полную пару Windows 7, поэтому один Segoe Fluent Icons ничего не возвращает.
  • CreepJS использует два пути обнаружения, document.fonts.check и локальную загрузку шрифтов, и объединяет оба ответа в один отсортированный список.
  • Запуск поиска даёт 5 ответов Windows, 8 ответов macOS и один ответ об отсутствии, то есть всего 14 возможных значений.

Содержание

Первая панель перечисляет пять таблиц шрифтов с 14, 18, 9, 7 и 3 именами, всего 51 разное имя. Вторая показывает таблицу Windows, разделённую на пять ведер версий, где ведро Windows 11 содержит одно имя. Третья показывает таблицу macOS, разделённую на восемь ведер, два из которых содержат одно имя. Четвёртая показывает, что ответ Windows требует полной пары Windows 7, а ответ macOS полной пары 10.9, и что поиск может вернуть 14 значений.

Что объявляют таблицы шрифтов

Модуль шрифтов это файл данных с небольшим количеством логики сверху. Проба вычисляет каждую таблицу как данные, а не сканирует её текст, потому что метки версий это ключи в кавычках, которые текстовый скан посчитал бы именами:

source -> https://raw.githubusercontent.com/abrahamjuliot/creepjs/master/src/fonts/index.ts
source_bytes -> 12847 sha256=ce2bcefd13e41cef79bfdf63634ad17ca7f641ee2e88c0ef720c2d38938dbf11
table:WindowsFonts -> names=14 shape=buckets
table:MacOSFonts -> names=18 shape=buckets
table:DesktopAppFonts -> names=9 shape=buckets
table:LINUX_FONTS -> names=7 shape=list
table:ANDROID_FONTS -> names=3 shape=list
fontlist -> length=51 distinct=51 duplicated=0
overlap:windows_macos -> 0
overlap:linux_android -> 0

Пятьдесят одно имя, без повторов. Это вся поверхность обнаружения шрифтовой половины библиотеки. Страница, которая запускает модуль, спрашивает о 51 семействе, и каждый вывод о платформе, который модуль может сделать, происходит из того, какие из этих 51 он нашёл.

Тот же публичный источник питает группы сигналов CreepJS и их объединение, где список шрифтов лишь одна группа из нескольких, и цикл зерна canvas в CreepJS, который читает генератор модуля canvas.

Пять таблиц не являются вариантами написания одного списка. Семь имён Linux это семейства, которые несёт образ Ubuntu или Chrome OS, а три имени Android это мобильный набор, который пересекается с Chrome OS, а не с настольной системой. Каждая таблица приносит имена, которых нет у других, и модуль один раз объединяет их в алфавитный список, который проверяет страница.

Windows и macOS почти уравновешены: 14 имён против 18. Эта близость обманывает по другой причине: два поиска читают свои таблицы по разным правилам, и именно более строгое правило решает, появится ли ответ вообще.

Почему одного имени никогда не хватает

Таблицы не плоские списки. Каждая версия это ведро, и поиск спрашивает, есть ли в нём хотя бы одно имя:

bucketsizes:WindowsFonts -> 7=2 8=4 10=4 11=1 8.1=3
bucketsizes:MacOSFonts -> 12=3 13=4 10.9=2 10.10=2 10.11=1 10.12=3 10.13-10.14=1 10.15-11=2
hashkeys:windows -> 11 macos=8
answers:windows -> 5 11,10,8.1,8,7
answers:macos -> 8 Ventura,Monterey,10.15-11,10.13-10.14,Sierra,El Capitan,Yosemite,Mavericks
answers:possible_returns -> 14

Ведро Windows 11 содержит одно имя, Segoe Fluent Icons. Ведро macOS 10.11 содержит одно, и ведро 10.13-10.14 тоже. Если читать только ведра, кажется, что найденный шрифт и определяет версию. Запуск поиска говорит другое.

Каждый ключ версии вычисляется, склеивается в отсортированную строку и ищется в фиксированной карте. Все записи этой карты содержат метку Windows 7, потому что проверка Windows 7 единственная требует полного набора, а не любого отдельного имени. Отсортированная склейка ставит 7 туда, куда попадает его алфавитный порядок: позиция меняется, присутствие нет. Набор, в котором есть Segoe Fluent Icons, но нет обоих имён Windows 7, не совпадает ни с одним ключом:

replay:windows_11_bucket_only -> undefined (input=1)
replay:windows_7_both_names -> Windows 7 (input=2)
replay:windows_7_one_name -> undefined (input=1)
sweep:windows:undefined -> 5 subsets 11 10 11+10 11+8.1 11+8
sweep:windows:Windows 11 -> 1 subsets 11+10+8.1+8

Развёртка выше это все подмножества четырёх добавочных ведер, добавленных к паре Windows 7. Пять из шестнадцати комбинаций не дают ничего. Единственная комбинация, дающая Windows 11, это полный набор всех пяти ведер. Убрать целое ведро и убрать одно семейство из него это разные события, и проба их различает:

drop:Segoe Fluent Icons -> Windows 10
drop:Segoe MDL2 Assets -> Windows 11
drop:Segoe UI Emoji -> Windows 11
drop:Cambria Math -> macOS Ventura

У ведра из одного имени нет запаса: потеря Segoe Fluent Icons стоит ответа Windows 11. У ведра из нескольких имён запас есть, потому что оно остаётся истинным, пока присутствует любое из его имён: Segoe MDL2 Assets лежит в ведре Windows 10 из четырёх имён, а Segoe UI Emoji в ведре 8.1 из трёх, поэтому потеря любого из них по-прежнему даёт Windows 11. Потеря имени Windows 7 это самый резкий случай: поиск Windows не даёт ничего, и включается запасной путь macOS. На машине, где есть ещё и семейства macOS, ответ переходит в macOS Ventura; на машине только с шрифтами Windows он становится undefined. В обоих случаях ответ Windows исчезает из-за одного отсутствующего семейства.

Сторона macOS ещё строже. Все восемь записей содержат метку 10.9, и в карте нет записи для набора с более новыми ведрами, но без неё:

sweep:macos:macOS Mavericks -> 1 subsets base:10.9
sweep:macos:macOS Ventura -> 1 subsets 13+12+10.15-11+10.13-10.14+10.12+10.11+10.10
sweep:macos:undefined -> 120 subsets

Сто двадцать из 128 подмножеств не дают ничего. Каждый из восьми именованных ответов соответствует ровно одному подмножеству, и ответ это самое новое присутствующее ведро. Машина, которая сообщает только шрифты эпохи Ventura и которой не хватает Helvetica Neue и Geneva, не получает никакого ответа macOS, хотя четыре имени из ведра Ventura найдены.

Значит ведра из одного имени реальны, но узки. Segoe Fluent Icons отличает Windows 11 от Windows 10 только после того, как оба имени Windows 7 и ещё три ведра уже совпали. Записывать стоит асимметрию между формой таблицы и формой поиска: таблица выглядит набором независимых проверок OR, а карта ведёт себя как лестница, которая стартует только при полном базовом наборе.

У этой лестницы есть второе следствие, которое счёт ведер скрывает. Поскольку каждый ключ это отсортированная склейка всех истинных записей, набор с лишним ведром, которого автор не предполагал, даёт строку, отсутствующую в карте. Поиск не откатывается к ближайшему совпадению, он возвращает ничего. Опубликованные таблицы замкнуты относительно своих имён, поэтому любой реальный набор Windows 11 со всеми 14 именами, включая четыре семейства Windows 8, по-прежнему возвращает Windows 11; риск в изменении таблицы, которое добавит ведро без добавления его ключа.

Таблица настольных приложений проверяется ещё по одному правилу, и разницу стоит назвать прямо, потому что это единственное место, где полное совпадение требуется намеренно. getDesktopApps сравнивает набор шрифтов каждого приложения с найденным списком и возвращает ключ только когда присутствуют все имена. Четыре приложения поэтому дают не больше четырёх результатов, а частичное совпадение не даёт ни одного.

Что добавляют два пути обнаружения

Модуль не полагается на один способ спросить, есть ли шрифт:

detection_paths -> 2 document.fonts.check and local font load
union -> fontsChecked then fontsLoaded, deduplicated

Путь document.fonts.check спрашивает набор шрифтов напрямую. Он сообщает, доступно ли семейство документу, и модуль проходит по всем 51 имени. Путь local font load создаёт объект на каждое имя и ждёт результатов загрузки, оставляя успешные.

Два пути нужны потому, что они могут расходиться. Браузер может сообщить, что шрифт доступен через один API, и не загрузить его через другой, и страница, проверившая только один, пропустит расхождение. Объединение двух ответов это итоговый список модуля, отсортированный и без повторов, поэтому результат устойчив к порядку, в котором вернулись пути.

Отчёт, который хранит два списка путей отдельно, может ещё сказать, систематически ли один API слабее на машине. Если путь запроса не нашёл ничего, а путь загрузки нашёл имена, скорее всего сработала защита, и это другое наблюдение, чем машина, где шрифтов действительно нет.

Перед обходом списка модуль проверяет намеренно случайное имя семейства. Когда эта проверка сообщает, что случайное имя доступно, модуль пропускает запрос по именам и полагается на путь загрузки. Эта защита не даёт доверять набору, который заявляет, что содержит все имена.

Эту защиту легко пропустить при чтении отчёта, потому что пропущенный запрос и неудачный запрос одинаково оставляют проверенный список пустым. Эти два состояния различимы только если отчёт говорит, вернула ли проверка случайного имени истину. Кто видит пустой проверенный список и полный загруженный, должен предполагать срабатывание защиты, а не то, что все семейства не прошли проверку доступности.

Имя, слитое дедупликацией, выглядит так же, как имя, найденное один раз, поэтому отчёт должен хранить два списка путей отдельно. Поиск независим от порядка по другой причине: проверки ведер используют вхождение и сравнение длины, а ключ строится сортировкой меток ведер, а не списка шрифтов. Проба подтверждает это, подавая те же 32 имени в обратном порядке:

reorder:reversed_names -> Windows 11

Два пути и отказывают по-разному. Запрос, который бросает исключение, уводит весь модуль в ветку перехвата и возвращает пустой список, а одна неудачная загрузка убирает только это имя из загруженного списка. Раздельное хранение списков и позволяет отличить отказ всего модуля от одного семейства, которое не загрузилось.

Где вывод заканчивается

Порядок двух поисков платформы тоже решает, что сообщит машина со шрифтами из обеих таблиц. getPlatformVersion сначала пробует поиск Windows и переходит к macOS только когда Windows не даёт ничего, поэтому Mac, случайно удовлетворяющий всем ведрам Windows, будет назван Windows. Проба делает случай конкретным: набор со всеми 14 именами Windows и всеми 18 именами macOS даёт Windows 11, а не macOS Ventura.

Таблицы WindowsFonts и MacOSFonts это ещё и фиксированный снимок платформ, которые интересовали автора. Новая версия системы, которая несёт шрифты, не названные в таблице, невидима для модуля до обновления таблицы, поэтому отсутствие совпадения не доказывает, что платформа новая.

Таблица macOS единственная с двузначными метками версий, и метки это диапазоны, а не точные значения: 10.13-10.14 это одно ведро, 10.15-11 другое. Совпадение поэтому определяет диапазон, а не точечную версию, и отчёт должен сохранять диапазон, а не округлять его до одной версии.

Поиск выполняется после построения списка, поэтому сбой на этапе обнаружения даёт пустой список и, вместе с ним, никакого ответа о платформе. Это важно, потому что ответ об отсутствии это одно значение, а не два: страница, которая читает пустую версию, должна проверить, был ли список пустым, прежде чем заключать, что машина не распознана, потому что пустой список и непустой список без совпавшего ключа дают одинаковое молчание.

Опубликованный модуль также даёт несколько наборов шрифтов приложений, и каждый сравнивается с найденным списком как группа:

bucketsizes:DesktopAppFonts -> Microsoft Outlook=1 Adobe Acrobat=1 LibreOffice=4 OpenOffice=3

Таблица маленькая, девять имён, а набор каждого приложения ещё меньше. Наличие шрифтов приложения не то же самое, что запущенное приложение, потому что шрифты можно копировать или поставлять отдельно. Модуль считает полный набор шрифтов приложения свидетельством этого приложения, и отчёт должен подавать это так, а не как доказательство установки программы.

Страница, которая вызывает модуль дважды в одной сессии, может получить два списка, потому что между вызовами может загрузиться шрифт. Модуль ничего не кэширует между запусками и каждый раз строит список заново, поэтому второе чтение это новое наблюдение, а не обновлённый вид первого.

Объединённый список это то, что страница действительно проверяет, поэтому имя из таблицы одной платформы спрашивается на всех. Машину Windows тоже спрашивают, есть ли у неё имена macOS, и ответ почти всегда нет. Так таблицы и остаются независимыми: страница спрашивает объединение, а поиски платформ читают ту часть объединения, которая им нужна.

Список шрифтов может меняться и без смены браузера: установка шрифта для одного пользователя меняет то, что сообщает его браузер, и не трогает список другого, а обновление пакетов может добавить имена, которых раньше не было. Ответ о платформе следует за списком, поэтому он движется с установкой, а не с версией системы.

14 возможных возвратов тоже задают минимальный полезный отчёт. Одно поле версии может назвать ответ, но не причину, а тому, кто хочет проверить вывод, нужен набор совпавших ведер, а не только названная версия. Отчёт должен хранить дайджест source и список шрифтов в одной записи, потому что список без ревизии не проверить, а ревизия без списка не покажет, что нашёл браузер.

14 возвратов задают и то, что может утверждать сравнение двух машин. Два отчёта с Windows 11 совпадают только если совпадают сохранённые наборы ведер, потому что один и тот же ответ может прийти из чуть разного найденного списка. Сравнение сохранённых списков отделяет настоящую разницу в установленных шрифтах от одних и тех же шрифтов, прочитанных дважды.

Записать чтение шрифтов

Чтение шрифтов подтверждает узкое утверждение: эта ревизия источника и этот набор таблиц дали такой список. Четыре поля сохраняют его проверяемым:

  1. Дайджест источника, потому что изменение таблицы меняет имена, о которых спрашивает страница, и смысл ответа о версии.
  2. Найденные имена, а не только версию, потому что ответ это функция всего найденного списка и его нельзя вывести заново из одного названия версии.
  3. Какой путь обнаружения нашёл каждое имя, потому что это разные наблюдения, а отсортированное объединение скрывает различие.
  4. Размеры ведер, потому что ответ о версии с одним именем и с четырьмя это не одно и то же доказательство.

Открытые источники

Практический вывод

Запись, которая хранит совпавшее ведро Windows 11 и оба списка путей, можно повторить коротким скриптом; запись, где есть только версия, без браузера не проверить. Храните найденные имена вместе с версией, потому что ответ зависит от полного базового набора и его нельзя вывести заново из одного названия версии. Читайте два пути обнаружения как отдельные наблюдения, потому что отсортированное объединение скрывает, какой API нашёл имя. Считайте 14 возможных возвратов размером поиска, а не его точностью, потому что ведро из одного имени различает только после совпадения всех остальных ведер карты. О том, как библиотека группирует шрифтовой сигнал с группами canvas и audio, читайте группы сигналов CreepJS и их объединение; о генераторе canvas того же модуля читайте цикл зерна canvas в CreepJS.

#CreepJS#Отпечаток Шрифтов#Версия Платформы#Анализ Сигналов#Приватность

Эта статья входит в руководство: Что такое отпечаток браузера?

Переведите BotBrowser из исследований в продакшн

Используйте эти руководства, чтобы понять модель, а затем перейти к кроссплатформенной валидации, изолированным контекстам и масштабируемому браузерному развертыванию.