Мы измерили платный антидетект‑браузер: canvas совпадал с чистой машиной байт в байт. Потом сделали свой, открытый

Антидетект‑браузеры — инструмент, которым агентства ведут десятки рекламных кабинетов и аккаунтов маркетплейсов с одной машины: у каждого профиля свой отпечаток, свой прокси, свои cookies. Стоят они $30–150 в месяц на команду, и все до одного закрытые. В интерфейсе — зелёные галочки «canvas: защищён», «WebGL: защищён». Что на самом деле уходит сайту — не знает никто, включая, как выяснилось, пользователей.
Мы взяли один из популярных продуктов, поставили рядом с настоящим Chrome на одном Mac и сравнили ~250 значений отпечатка. Результат стал причиной написать свой. Ниже — методика, которую можно повторить на любом антидетекте за десять минут, что она показала, и как в итоге устроен Fury — открытый форк Chromium, где подмена делается в C++ у источника значения, а не инжектом JS.
Продукт я называть не буду. Не потому что боюсь, а потому что это не главное: методика ниже воспроизводима, и каждый может прогнать её сам против того, за что платит.
Как измеряли
Инструмент написан до первого патча Chromium — сначала линейка, потом работа. Иначе через полгода обнаруживаешь, что патчил не то.
probe.js — скрипт, который снимает ~250 значений: navigator, screen, canvas toDataURL(), WebGL getParameter() и readPixels(), WebGPU limits, AudioContext, getClientRects(), метрики шрифтов, enumerateDevices(), голоса speechSynthesis, таймзона, Intl, permissions. Потом он перечитывает те же значения из Worker и трёх видов iframe и записывает, где ответы разошлись.
capture-chrome.sh запускает любой браузер на чистом временном профиле, указывая на probe.html?auto=<имя>, и складывает дамп в baselines/. Без кликов. Чистый профиль важен: эталон должен описывать браузер, а не то, во что его превратили расширения.
fury-detect diff сравнивает два дампа. Наивное сравнение даёт 400 различий и ничего не говорит; вопрос всегда в том, какого рода различие:
Значения — хеш canvas, строка GPU, User‑Agent, список шрифтов. Между настоящим Chrome и антидетектом они должны отличаться. Это и есть работающая подмена.
Поведение — API, которое есть в одном и нет в другом; геттер, переставший быть нативным; кодек, который перестал поддерживаться; значение, которое отличается между главным фреймом и Worker. Вот это отличаться не должно никогда: оно позволяет сайту различить два браузера по тому, что они умеют, а не по тому, что они заявляют.
Отсюда два режима:
# Один браузер, два прогона. Ничего не должно отличаться:# нестабильный шум — сигнал сильнее, чем отсутствие шума.fury-detect diff --mode identity run-a.json run-b.json
# Настоящий Chrome против кандидата. Значения могут отличаться, поведение — нет.fury-detect diff --mode spoof baselines/chrome-153-macos-arm.json candidate.json# нестабильный шум — сигнал сильнее, чем отсутствие шума.
fury-detect diff --mode identity run-a.json run-b.json
# Настоящий Chrome против кандидата. Значения могут отличаться, поведение — нет.
fury-detect diff --mode spoof baselines/chrome-153-macos-arm.json candidate.json
Что показало измерение
Один продукт, один профиль с настройками по умолчанию, версия от июля 2026, тот же Mac, что и эталонный Chrome.
Что продукт подменяет: строку WebGL renderer (M5 → M1 Pro), audio‑хеш, список голосов speechSynthesis, hardwareConcurrency (10 → 8), deviceMemory (16 → 8), таймзону.
Что оставляет нетронутым — байт в байт как у чистого Chrome на той же машине:
Вектор | Состояние |
|---|---|
| идентичен чистому Chrome |
| идентичен |
WebGL | идентичен — сменена только строка renderer, не пиксели |
Метрики шрифтов | идентичны |
WebGPU | идентичны |
| идентичен |
Главное здесь — canvas. Это самый проверяемый вектор в индустрии, у него лучший сигнал среди пассивных, и на этом профиле он совпадает с отпечатком голой машины. Практическое следствие: все профили этого продукта на одной машине связываются между собой и с личным Chrome пользователя по одному хешу. А интерфейс при этом показывает «защищён».
Оговорка, которую я обязан сделать: настройки задаются на профиль, и измерен один. Возможно, это настройка «реальный canvas» в этом конкретном профиле, а не дефолт продукта. Но пользователь не знает, что работает без защиты, — и это уже свойство продукта, а не профиля.
Вторая находка тоньше. WebGL renderer подменён на M1 Pro, а readPixels() возвращает пиксели, отрендеренные M5. WebGPU architecture подменён (metal-3 → common-3), а 36 limits остались от настоящего чипа. Каждое подменённое значение стоит рядом с неподменённым, которое ему противоречит. Сайту не нужно знать «правильный» отпечаток — достаточно заметить, что заявленное и измеренное не сходятся.
Почему так получается: подмена в JS
Почти все коммерческие антидетекты — Chromium плюс скрипт, который инжектится в каждую страницу и переопределяет геттеры. Это быстро, не требует пересборки браузера, и ровно поэтому оставляет следы, которые детекторы проверяют первым делом:
Тест детектора | Что видит при JS‑подмене |
|---|---|
|
|
| Дескриптор переопределён, |
| Реальное значение. Инжект в воркеры почти никто не делает корректно |
| Свежий realm без патчей, если инжект не покрыл |
Ошибка внутри подменённого геттера | Stack trace содержит имя скрипта или |
Canvas: | Шум применён к одному пути, но не к другому |
JA3 / HTTP/2 SETTINGS | Недоступно из JS вообще |
Последняя строка — приговор для подхода в целом: Cloudflare, Akamai, DataDome сверяют заявленный User‑Agent с сетевым отпечатком до того, как выполнится первый байт JS.
Единственный способ, при котором значение «родное» во всех контекстах — Worker, iframe, worklet, заголовки Client Hints — это менять его там, где Chromium его вычисляет. То есть патчить ядро.
Как это сделано у нас
Fury — форк Chromium 153.0.8010.37. Патчей 27, каждый — отдельный файл в репозитории с комментарием в series, зачем он и почему именно там. Несколько принципов, которые вышли из измерений, а не из головы:
Шум применяется при чтении, а не при рисовании. Canvas продолжает рендерить ровно то, что попросила страница; возмущение добавляется в getImageData() и toDataURL()/toBlob(). Эти два пути в Chromium идут через разный код, но у нас вызывают одну функцию с теми же абсолютными координатами, поэтому пиксель, прочитанный одним способом, совпадает с прочитанным другим.
Шум — чистая функция от (seed профиля, x, y). Никакого счётчика, никакой случайности. Детектор читает canvas дважды и сравнивает; нестабильный шум — сигнал сильнее, чем его отсутствие. Это тот самый режим identity в fury-detect: два прогона одного профиля обязаны совпасть до байта.
Перехват на входе, а не в каждой ветке. getParameter() в WebGL — это ~80 ветвей switch. Патчить их по одной — гарантия, что при следующем ребейзе часть потеряется, а одно неподменённое значение противоречит всем подменённым вокруг. Перехват стоит один раз наверху. WebGPU limits идут через тот же X‑macro, что и геттеры, поэтому лимит, который Chromium добавит в следующей версии, покрыт в момент появления в списке.
Только сужать, никогда не расширять. Список расширений WebGPU, шрифты, медиаустройства — из них можно убрать, но нельзя добавить. Убрать честно: requestDevice() тогда падает ровно так, как на железе, где этой возможности нет. Добавить — значит заявить возможность, которую нельзя предъявить.
Таймзона и локаль доезжают до воркеров. Патч оповещает каждый изолят и каждый поток Worker. Локаль, выставленная только на главном потоке, — противоречие, которое страница находит внутри Worker одной строкой.
TLS не трогаем — и это тоже результат измерения. Проверено по исходникам: Chrome перемешивает порядок TLS‑расширений на каждом соединении (SSL_set_permute_extensions), GREASE включён, свой список шифров Chromium не задаёт. JA3 настоящего Chrome непостоянен даже внутри одной сессии. Наша сборка — тот же BoringSSL с тем же кодом, значит сетевой отпечаток совпадает с Chrome по построению. Патч здесь сделал бы хуже.
Конфиг не проходит через argv. Persona попадает в браузер как унаследованный файловый дескриптор (на Windows — HANDLE), а не в командной строке. Командная строка процесса видна любому другому процессу на машине, и мы это проверяем скриптом, который обходит дерево процессов и требует, чтобы ни в одном argv не было ни одного значения персоны.
Вокруг ядра — агент на Rust, который запускает профили и держит прокси‑релей, оболочка на Tauri, и опциональный командный сервер (один бинарник + PostgreSQL), где бандлы профилей шифруются на клиенте и сервер их прочитать не может. Соло‑режим работает без аккаунта и без сервера.
Чего нет — честно
Об этом в README есть таблица, и она длиннее, чем хотелось бы. Коротко:
macOS (Apple Silicon) и Windows. Linux — не цель, и это решение, а не пробел: две платформы, которые тестируются, стоят больше трёх, из которых одна — догадка.
Не подписано. Apple Developer оплачен и ждёт верификации Apple. До тех пор macOS говорит «повреждён» (снимается одной командой
xattr), Windows показывает SmartScreen. В docs/15 расписано.CDP по таймингу не спрятать, и теперь это известно, а не просто не сделано. Разложили на две части: подключение ничего не стоит,
Runtime.enableстоит фиксированные 2.7× плюс рост с размером логируемого объекта. Патч убирает вторую половину и не может убрать первую. Настоящий Chrome измеряется так же. Рабочий контроль —cdp: false, и это дефолт.Widevine на машине без Chrome — нет. Агент берёт CDM из установленного Chrome, потому что распространять его нельзя. Машина без Chrome получает браузер без DRM, и это детектируемо.
Автообновлений нет намеренно. Апдейтер — это канал, который по расписанию лезет в антидетект‑браузер снаружи с адреса, который не является прокси профиля. Обновляешься, когда решишь сам.
Сборка ядра: 2 ч 42 мин на M5, ~39 ГБ на диске. Это измерено, не оценено.
ccacheне помогает — сборка с-fmodules, промахи на всём.
Как проверить самому
Каждый релиз кладёт рядом REPORT.md — отчёт по 13 проверкам с хешами ядра, дампа и серии патчей. Это утверждение о конкретном бинарнике, и хеши делают его проверяемым, а не принимаемым на веру:
shasum -a 256 <ядро> # должен совпасть с core sha256 в REPORT.mdtools/detect-suite/capture-chrome.sh mine <ядро>cargo run -p fury-detect -- report tools/detect-suite/baselines/mine.json --core <ядро>tools/detect-suite/capture-chrome.sh mine <ядро>
cargo run -p fury-detect -- report tools/detect-suite/baselines/mine.json --core <ядро>
Другой вердикт при том же хеше ядра — баг, который стоит завести. Другой вердикт при другом хеше — другой браузер.
Ту же методику можно применить к любому антидетекту: capture-chrome.sh <имя> <путь к его бинарнику>, потом diff --mode spoof против эталонного Chrome. Если в колонке «поведение» не пусто — вы платите за то, что не работает.
Что дальше и чем помочь
Самое полезное, что можно прислать, — персона со своей машины. В каталоге 26 машин, и каждая — толпа, в которой кому‑то прятаться. Есть hosted‑страница: открываешь в обычном Chrome, две кнопки, файл уходит в issue‑форму, workflow конвертирует и открывает PR под твоим именем. Публичный адрес вырезается до того, как файл покинет страницу.
Второе — сайт, который поймал профиль, с описанием, что именно он проверил. Отчёты вида «вектор X течёт в контексте Y» — ровно то, что проекту нужно.
Лицензии: AGPL-3.0 для агента, сервера и оболочки; патчи Chromium — BSD-3, как upstream, чтобы браузер, собранный из серии, не тянул copyleft; схемы — Apache-2.0, чтобы любой мог написать совместимый клиент.
Про границы применения написано отдельно в ACCEPTABLE_USE.md. Это dual‑use инструмент, как VPN или менеджер паролей. Запросы «как обойти проверку платформы N» закрываются; отчёты об утечках векторов — приветствуются без оговорок.
Репозиторий: https://github.com/furyteamtop/fury‑antidetect‑browser
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.