AntiDetect
Возможности Технология Цены Для кого
Документация Блог FAQ
Язык
EnglishItalianoDeutschEspañolРусский
Скачать

Шум на canvas — сам по себе отпечаток

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

Защита, которая оборачивается против вас

Фингерпринтинг canvas работает потому, что растеризация на GPU не стандартизирована на уровне пикселей. Попросите две машины нарисовать один и тот же текст тем же шрифтом того же размера — и сглаживание будет отличаться на волосок. Возьмите хеш пикселей и получите значение, стабильное на одном устройстве и другое на следующем.

Интуитивная контрмера — добавить шум: изменить несколько пикселей до вычисления хеша, чтобы значение менялось и не годилось для слежки. Такую опцию предлагает почти каждый антидетект-браузер, обычно под названием «canvas: noise».

Загвоздка в том, что у настоящего браузера хеш canvas не меняется. Та же машина, тот же браузер, та же страница — тот же результат: сегодня, завтра и после перезагрузки. Именно эта стабильность и делает сигнал ценным для сбора.

Значит, значение, меняющееся при каждом чтении, не стало анонимным. Оно стало аномальным. Это ошибка того же класса, что и выставление navigator.webdriver = false: честный ответ вас опознаёт, а нечестный опознаёт как того, кто лжёт.

Как на самом деле работает обнаружение

Чтобы заметить рандомизацию, не нужно ничего хитрого. Четыре способа, примерно в порядке частоты встречаемости:

Нарисовать одно и то же дважды

Дважды отрисовать одинаковый canvas в пределах одной загрузки страницы и взять оба хеша. Настоящий браузер: одинаковые. Шум на каждый вызов: разные. Это две строки кода, и они есть в любом серьёзном скрипте обнаружения.

Усреднить шум

Если шум случайный и с нулевым средним, то n-кратная отрисовка с усреднением подтягивает результат к истинному значению со скоростью около √n. Двадцати отрисовок обычно хватает, чтобы восстановить исходный хеш. Рандомизация стоила трекеру нескольких миллисекунд, а вам — анонимности, да ещё и пометила вас как того, кто рандомизирует.

Проверить, уместен ли шум

Рандомизированный canvas сам по себе не подозрителен. Brave включает его по умолчанию — его «farbling» выводит шум из сида, своего для сессии и источника. Tor Browser вместо этого спрашивает пользователя. Важно другое: соответствует ли шум браузеру, за который вы себя выдаёте. Chrome canvas не рандомизирует. Клиент, у которого user agent сообщает Chrome 147, а canvas меняется на каждом вызове, сам себе противоречит — и находкой становится противоречие, а не шум.

Менять путь отрисовки

Наивные реализации перехватывают toDataURL() и getImageData(). Есть и другие выходы: toBlob(), createImageBitmap(), readPixels() в WebGL, OffscreenCanvas внутри worker. Если на одном пути шум есть, а на другом нет, они расходятся — и это расхождение весомее любого из значений по отдельности.

Диаграмма рассеяния отклонения хеша canvas, сходящегося к истинному значению по мере усреднения отрисовок
Шум с нулевым средним сам себя гасит. Ошибка падает как 1/√n, так что двадцати отрисовок обычно достаточно, чтобы вернуть значение, которое рандомизация скрывала.

Отдельно о пути через worker

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

OffscreenCanvas можно передать в Web Worker и отрисовать там. У worker свой глобальный контекст и своя копия canvas-API, и внедрение скрипта на уровне страницы туда не достаёт. Если ваш шум накладывается внедрённым JavaScript, canvas из worker выйдет чистым.

const off = new OffscreenCanvas(280, 60);
const w = new Worker(url);
w.postMessage({ canvas: off }, [off]);
// the worker draws and hashes — untouched by any page-level hook

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

Как выглядит корректная реализация

Цель не в том, чтобы «каждый раз было другое значение». Цель — значение, отличное от чужих, и каждый раз одно и то же для меня. В такой формулировке требования вытекают сами:

СвойствоТребование
В пределах одной загрузки страницыОдинаково при каждой отрисовке
Между сессиямиОдинаково для одного профиля, всегда
Между профилямиРазличается, и не на постоянную величину
Между путями отрисовки2D, WebGL, OffscreenCanvas и worker согласуются
ПравдоподобиеСоответствует видеокарте, заявленной профилем

Механизм, удовлетворяющий всем пяти условиям, — это сид. Зафиксируйте случайный сид при создании профиля, сохраните его и детерминированно выводите из него изменение пикселей. Любая отрисовка этого профиля даст одинаковый результат, потому что сид один и тот же. Два профиля различаются, потому что различаются их сиды. А поскольку вывод происходит там, где пиксели и появляются — в растеризаторе, а не в перехваченном методе JavaScript, — одинаковую обработку получает каждый путь, включая worker.

Именно последний пункт объясняет, почему это трудно сделать как следует без изменённого движка. Нет поддерживаемого способа дотянуться из контент-скрипта до растеризатора внутри worker.

Доводы за то, чтобы ничего не трогать

Есть серьёзный довод, который стоит изложить честно: canvas можно вообще не трогать.

Если у вас один профиль на одну физическую машину, ваш подлинный хеш canvas — совершенно обычное значение совершенно обычного компьютера. Он стабилен, согласован по всем путям и соответствует вашей видеокарте, потому что он и есть ваша видеокарта. Ничто в нём не заставляет присмотреться.

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

Значит, выбор не между «шум или не шум». Он такой:

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

Для этого не нужен поставщик систем обнаружения. Откройте консоль браузера на любой странице и выполните:

function h() {
  const c = document.createElement('canvas');
  const x = c.getContext('2d');
  x.textBaseline = 'top';
  x.font = '14px Arial';
  x.fillText('P8 canvas probe \u{1F511}', 2, 2);
  return c.toDataURL().slice(-32);
}
console.log(h(), h(), h());

Три одинаковые строки — это то, что даст настоящий браузер. Три разные означают шум на каждый вызов, а значит, любой сайт, выполнивший те же три строки, об этом знает.

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

Стабильно внутри профиля, по-разному между профилями

P8 выводит canvas каждого профиля из сида, зафиксированного при создании. Один и тот же профиль всегда отрисовывается одинаково; два профиля — никогда.

Начать бесплатно
Все статьи Далее: фингерпринтинг TLS и HTTP/2