Подменённые нативные функции и что видит список toString
Один зонд по подменённым API: что ловит список нативных форм, какие дескрипторы всё равно выдают правку и почему Proxy или связанная функция проходят как нативные.
BotBrowser Team
Нужна структурированная документация по теме Отпечатки?
Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.
BotBrowser позволяет воспроизвести авторизованный фикстур того же источника под объявленным профилем и сохранить ограниченную запись того, какие API прочитала страница, но он не может спрятать подмену, сделанную на уровне JavaScript, от детектора подмены, не может заставить обёртку Proxy печатать код обёрнутой функции и не может изменить то, что Function.prototype.toString сообщает для функции, которую движок всё ещё считает нативной. Разбор воспроизводит набор проверок открытой библиотеки отпечатков, применяет его к десяти фиксированным случаям и фиксирует, какие подмены ловит белый список, какие дескрипторы всё равно выдают правку, и две обёртки, которые выходят чистыми, хотя страница изменила значение, получаемое читателем.
Кратко
- Нетронутая нативная проходит все проверки, поэтому зонд печатает
control:Math.random -> cleanи то же дляArray.prototype.mapиFunction.prototype.toString. - Замена
Math.randomобычной функцией оставляет одну метку:naive:replaced Math.random -> failed toString, потому что исходный текст больше не имеет вида нативного кода. - Поддельный
toStringна этой функции ничего не исправляет: зонд печатаетfailed toString | failed toString toString | extra own keys: toString, то есть правка теперь выдаёт себя тремя способами вместо одного. - Функция, возвращающая
0.42, всё равно показывает своё тело, когда детектор вызывает настоящийtoStringчерез связанную ссылку, поэтомуclone:fnToString-callпечатает код страницы, и случай клона остаётся помеченным. Proxy, перехватывающийapply, проходит всю батарею, поэтомуproxy:Math.random -> clean, хотя значение, получаемое страницей, приходит из ловушки.- Связанная функция тоже проходит, потому что зонд печатает
bind:fnToString -> function () { [native code] }иbind:checks -> clean. - Чистая нативная и подменённая различаются ровно одной меткой на дешёвом пути, поэтому
separation -> cleanFlags=0 patchedFlags=1это весь запас, который есть у детекта. - Сам хост может дать метку, которая не является подменой страницы: зонд печатает
getter:checks -> failed toString hostShape=trueдля нативного аксессора, чей код это тело реализации.
Содержание
- Что принимает белый список
- Что оставляет простая замена
- Что всё ещё выдаёт поддельный toString
- Две обёртки, которых список не видит
- Запись одного показания детекта подмены
Что принимает белый список
Самая дешёвая проверка целостности в мире отпечатков читает исходный текст каждой функции API и сравнивает его с фиксированным набором форм, которые может произвести движок. Открытая библиотека отпечатков публикует ровно этот список в своём модуле детекта подмены, и формы достаточно коротки, чтобы выписать их как есть:
// три формы, которые принимает список, NAME это имя самого API
`function ${name}() { [native code] }``function get ${name}() { [native code] }`;
('function () { [native code] }');
Проверка, читающая исходный текст, не видит значение, которое вычисляет обёртка, и источник случайности crypto-js и энтропия библиотеки фиксирует тот же предел для функции библиотеки, у которой читается вывод, а не код. Каждая из этих трёх встречается также в разбитом на строки виде, с переводами строк и четырьмя пробелами перед [native code], что печатают некоторые движки. Всё, что вне этого набора, записывается как подмена против имени API, и отчёт группирует записи по API, к которому они относятся.
Проверка выполняет настоящий Function.prototype.toString на функции, а затем второй раз на toString самого значения. Второй вызов это та часть, которая важна, потому что страница, заменяющая toString у функции, должна заменить его и у toString замены, и вся цепочка должна согласоваться. Страница, которая правит только внешний уровень, ловится внутренней проверкой.
Контрольные случаи зонда подтверждают, что белый список не срабатывает на нетронутых нативных. control:Math.random -> clean, control:Array.prototype.map -> clean и control:Function.prototype.toString -> clean выходят без меток. Это важно для чтения: детектор, который сработал бы на контроле, был бы бесполезен, поэтому три контрольные строки и есть доказательство, что остальные метки что-то значат.
Контроль задаёт и то, что измеряет белый список. Он не измеряет, правильно ли ведёт себя функция, и не сравнивает поведение с эталоном. Он читает одну строку на функцию и сравнивает её с фиксированным набором. Страница, чья подменённая функция возвращала бы ровно то же, что вернула бы нативная, всё равно провалит проверку, потому что исходный текст другой; страница, чья обёртка возвращает нечто совсем иное, может пройти, как показывает последний раздел, если только обёртка это форма, которую печатает сам движок.
Что оставляет простая замена
Первая правка, которую пишет страница, самая простая: присвоить новую функцию на свойство. Зонд делает именно это с Math.random и запускает батарею:
naive:replaced Math.random -> failed toString
separation -> cleanFlags=0 patchedFlags=1
Одна метка, и это проверка исходного текста. Код замены это function random() { return 0.42; }, что не совпадает ни с одной из шести принятых форм, поэтому записывается. Все остальные проверки проходят, потому что замена это обычная функция с привычными length и name и без лишних ключей. Весь детект держится на одном сравнении строк, и последняя строка зонда говорит об этом прямо: чистая нативная даёт ноль меток, подменённая даёт одну.
У этого запаса в одну метку есть практическое следствие для того, кто читает отчёт. Запись random: failed toString говорит одну конкретную вещь: исходный текст не имел вида движка. Она не говорит, что теперь делает функция, когда её поправили и изменила ли правка какое-либо значение. Отчёт, который печатает только счётчик подмен, скрывает, какая проверка сработала, а именно проверка и несёт доказательство.
Запас делает порядок проверок значимым для читателя. Если бы проверка исходного текста была единственной, страница могла бы её очистить, поправив toString у функции, и это следующий случай. Поскольку детектор читает ещё набор дескрипторов и собственные ключи, вторая правка стоит странице больше меток, чем экономит. Батарея устроена так, чтобы самая дешёвая правка делала отчёт больше, а не меньше.
Есть законный случай, который простая проверка исходного текста читает неверно, и зонд находит его в самом хосте, а не на странице. Одна строка читает нативный геттер и печатает getter:checks -> failed toString hostShape=true, потому что код аксессора это тело реализации с приватными полями вместо формы get native() { [native code] } браузера. Эта метка описывает хост, на котором шёл зонд, а не правку страницы. Детектор, который считал бы каждую метку доказательством манипуляции, обвинил бы хост, который ничего не правил, и поэтому отчёт обязан указывать среду, произведшую метки.
Что всё ещё выдаёт поддельный toString
Очевидный ответ на проверку исходного текста это дать замене toString, возвращающий принятую форму. Зонд делает это и получает три метки вместо одной:
fake-tostring:Math.random -> failed toString | failed toString toString | extra own keys: toString
fake-tostring:inner -> failed toString
Первая метка остаётся failed toString, и за причиной стоит проследить. Детектор вызывает Function.prototype.toString на функции, а не toString самой функции, поэтому читает настоящий исходный текст независимо от того, что говорит свойство. Страница, определяющая toString на функции, меняет то, что возвращает fn.toString(), но не то, что возвращает Function.prototype.toString.call(fn), а детектор использует вторую форму.
Вторая метка это внутренняя проверка. Детектор читает ещё код собственного toString функции, а подделка страницы для этого это другая функция страницы, чей код тоже не имеет принятой формы. Значит правка, которая должна была заглушить первую метку, создаёт вторую метку на установленном значении.
Третья метка это проверка собственных ключей. Настоящая нативная функция выставляет ровно length и name как собственные ключи, а иногда prototype. Добавление свойства toString делает видимым третий собственный ключ, и зонд печатает extra own keys: toString за него. Значит правка видна из списка свойств даже читателю, который никогда не смотрит на исходный текст.
Три метки вместе объясняют, почему дешёвая правка это плохой обмен. Страница начинает с одной метки, правит toString, чтобы её убрать, и получает три. Детектор, который публиковал бы имена своих проверок, дал бы странице увидеть, что правка ухудшает отчёт, и следующим шагом страницы было бы перестать править и изменить свойство иначе, а это как раз то место, с которого начинается следующий раздел.
Случай клона в зонде пробует более аккуратную версию той же идеи: привязывает настоящий Function.prototype.toString к настоящей функции и ставит связанную ссылку как toString клона. Строка clone:fnToString-call печатает тело клона, function random() { return 0.42; }, то есть привязка не переносит исходный текст цели в вызов. Клон всё равно помечается failed toString и снова extra own keys: toString, та же пара, что дала простая правка, и лишняя работа ничего не покупает.
Две обёртки, которых список не видит
Две формы проходят всю батарею, продолжая менять значение, получаемое страницей, и зонд фиксирует обе.
Первая это Proxy, перехватывающий apply. Зонд оборачивает настоящий Math.random в прокси, чья ловушка возвращает 0.42, присваивает прокси и запускает проверки:
proxy:Math.random -> clean
Ни одной метки. Вызов Function.prototype.toString у прокси перенаправляется на цель, которая нетронутая нативная, поэтому исходный текст имеет принятую форму. У прокси нет своего toString, поэтому внутренняя проверка тоже читает нативную форму. А собственные ключи цели прокси это ключи нативной, поэтому проверка собственных ключей проходит. Каждый сигнал, который читает батарея, описывает цель, тогда как значение, получаемое страницей, приходит из ловушки.
Дескрипторы, которые действительно меняются, когда страница переопределяет свойство, это те, о которых говорит порядок ключей объекта. Обратите внимание, что это прокси поверх вызываемой нативной, и предыдущий разбор ловушки Proxy и инварианты описывает, что движок требует для такой формы. Инварианты ограничивают то, что прокси может сообщить о свойствах цели, и ни один не ограничивает значение, возвращаемое ловушкой apply. Поэтому прокси невидим для проверки кода и дескрипторов: нечему быть несогласованным, потому что прокси это законная и согласованная обёртка вокруг нативной.
Вторая форма это связанная функция. Зонд связывает Math.random без аргументов и запускает проверки:
bind:fnToString -> function () { [native code] }
bind:checks -> clean
Связанную функцию создаёт сам движок, поэтому её исходный текст имеет принятую форму function () { [native code] }, и она несёт набор собственных ключей нативной. Значит страница может взять любую нативную, связать её и получить значение, проходящее проверку целостности, которая читает только код и дескрипторы. Меняет ли связывание поведение, зависит от цели: связывание Math.random с неким this ничего не делает, но связывание метода, читающего this, с прокси или с подготовленным получателем меняет то, что видит вызывающий код.
Поведение привязки зависит от того, что читает связанная функция, и ловушки Proxy и инварианты показывают, что движок проверяет, когда получатель это прокси. У обеих слепых зон общая причина: батарея читает статические факты об объекте функции, а страница управляет тем, что реально получает вызывающий код. Проверка, которая хочет закрыть обе, должна сравнивать возвращаемое значение с эталоном, недоступным странице, а не только читать текст функции. Это разница между статической проверкой целостности и поведенческой, и отчёт, который прогоняет только статическую половину, говорит об этом лишь если называет выполненную проверку.
Запись одного показания детекта подмены
Показание детекта подмены подтверждает узкое утверждение: этот детектор, эти проверки, эта среда дали эти метки. Четыре поля держат его проверяемым:
- Имена сработавших проверок, а не только счёт, потому что
failed toStringиextra own keysописывают разные правки, и только имя говорит какие. - Принятые формы кода, потому что белый список это фиксированный набор, и отчёт без него не переиграть на другом движке, печатающем форму, которую набор опускает.
- Сравнивалось ли значение с эталоном, потому что статическая проверка пропускает
Proxyи связанную функцию, а поведенческая может не пропустить. - Хост, произведший метки, потому что нативный аксессор может напечатать тело реализации, похожее на правку страницы, и показание без хоста не различит их.
Публичные источники
- MDN: Function.prototype.toString()
- ECMAScript: Function.prototype.toString
- ECMAScript: OwnPropertyKeys
- MDN: Proxy
- MDN: Function.prototype.bind()
- BotBrowser: справочник согласованности автоматизации
Практический вывод
Читайте отчёт по именам проверок, потому что один счёт не отличает простую замену от поддельного toString или от третьего собственного ключа. Ожидайте, что Proxy и связанная функция пройдут проверку кода и дескрипторов, так как обе это созданные движком формы вокруг нетронутой нативной, и трактуйте чистый результат этой проверки как «исходный текст выглядит нативным», а не как «значение это значение нативной». Два предыдущих разбора, ловушки Proxy и инварианты и порядок ключей объекта, описывают механику, от которой зависит проверка ключей, и запись должна всегда называть выполненные проверки и хост, произведший метки, потому что нативный аксессор может напечатать тело, которое выглядит ровно как правка.
Эта статья входит в руководство: Что такое отпечаток браузера?
Похожие статьи
Источник случайности в crypto-js: ветка сборки
ОтпечаткиНаблюдаемость криптографической случайности и отпечатки браузера
ОтпечаткиBotD: какие детекторы действительно выполняются
ОтпечаткиФингерпринтинг Network Information API: проверка политики
ОтпечаткиWeakMap и WeakSet: что они держат и что теряют
ОтпечаткиClient Hints Fingerprinting: HTTP-заголовки и идентичность
Переведите BotBrowser из исследований в продакшн
Используйте эти руководства, чтобы понять модель, а затем перейти к кроссплатформенной валидации, изолированным контекстам и масштабируемому браузерному развертыванию.