Антидетект‑браузеры — инструмент, которым агентства ведут десятки рекламных кабинетов и аккаунтов маркетплейсов с одной машины: у каждого профиля свой отпечаток, свой прокси, свои 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 на той же машине:

Вектор

Состояние

canvas.toDataURL()

идентичен чистому Chrome

getClientRects()

идентичен

WebGL readPixels()

идентичен — сменена только строка renderer, не пиксели

Метрики шрифтов

идентичны

WebGPU limits (36 значений)

идентичны

enumerateDevices()

идентичен

Главное здесь — canvas. Это самый проверяемый вектор в индустрии, у него лучший сигнал среди пассивных, и на этом профиле он совпадает с отпечатком голой машины. Практическое следствие: все профили этого продукта на одной машине связываются между собой и с личным Chrome пользователя по одному хешу. А интерфейс при этом показывает «защищён».

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

Вторая находка тоньше. WebGL renderer подменён на M1 Pro, а readPixels() возвращает пиксели, отрендеренные M5. WebGPU architecture подменён (metal-3 → common-3), а 36 limits остались от настоящего чипа. Каждое подменённое значение стоит рядом с неподменённым, которое ему противоречит. Сайту не нужно знать «правильный» отпечаток — достаточно заметить, что заявленное и измеренное не сходятся.

Почему так получается: подмена в JS

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

Тест детектора

Что видит при JS‑подмене

Function.prototype.toString.call(getter)

[native code] подделать можно, но Function.prototype.toString.toString() — уже сложнее, всплывает прокси

Object.getOwnPropertyDescriptor(Navigator.prototype, 'platform')

Дескриптор переопределён, get не совпадает по идентичности с оригиналом

new Worker(...) → navigator.hardwareConcurrency

Реальное значение. Инжект в воркеры почти никто не делает корректно

iframe с src=about:blank → contentWindow.navigator

Свежий realm без патчей, если инжект не покрыл document-start во всех фреймах

Ошибка внутри подменённого геттера

Stack trace содержит имя скрипта или <anonymous> не на той глубине

Canvas: toDataURL() против WebGL‑текстуры с тем же содержимым

Шум применён к одному пути, но не к другому

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

Комментарии (10)


  1. MxMaks
    14.09.2026 10:26

    Подобные браузеры с нуля с рабочей эффективностью на базе open-source Chromium + модифицированная Playwright обвязка для клиентского софта уже вайбкодятся за несколько часов. Частично на Fable, частично на Opus, где детектор Fable отказывается продолжать работу видя, чтото нежелательное. Сборка 1ч 45 минут на 270K plus. Ранее те самые коммерческие решения работали в основном под винду, теперь с нуля и под Linux работает.


    1. i_mrbogdan Автор
      14.09.2026 10:26

      Согласен, обвязка над Playwright + CDP-инъекции собирается быстро, и с моделями — ещё быстрее. Fury тоже написан так, я этого не скрываю. Разница не в том, кто писал код, а в том, где живёт подмена.

      Playwright-обёртка подменяет через JavaScript: Object.defineProperty на navigator, прокси над getContext, скрипт через addInitScript. Это видно: в worker'е и в iframe с srcdoc скрипт не отработал — и там navigator.platform уже настоящий. Три строки на сравнение контекстов, и профиль отдаёт хост. А canvas и WebGL при таком подходе либо не трогают вообще (у одного платного антидетекта в статье canvas совпадал с чистой машиной байт в байт — это как раз такой случай), либо шумят случайно, и тогда хеш меняется между вызовами, что палится ещё проще.

      У Fury подмена в C++ ядра, 29 патчей на Chromium 153, ноль инжектированного JS. Один и тот же ответ в main frame, worker'е и трёх видах iframe. Шум детерминированный — один профиль всегда даёт один хеш canvas. Это можно проверить самому: tools/detect-suite рядом в репозитории, там же baseline'ы с настоящего Chrome 150/153 и с чистого Chromium, чтобы было с чем сравнивать.

      Про «1ч 45 минут»: сборка ядра Chromium у меня — 2 ч 42 мин на M5 (10 ядер, 16 ГБ, замерено 30.07), 57 528 таргетов под Windows. За 1:45 собирается обёртка, ядро — нет. Так что, скорее всего, у вас в браузере стоковый Chromium плюс JS сверху — и тогда см. первый абзац.

      И самое дорогое там — вообще не код. Это данные: каталог персон с реальных машин (сейчас 27, последняя пришла через issue-форму вчера), чтобы подменённый профиль был существующей конфигурацией, а не «RTX 4060 + 4 ядра + экран 1366×768», которой в природе нет. Это модель не сгенерирует, это только собирать.

      Если у вас реально работает по-другому — прогоните вашу сборку через detect-suite и покажите json, мне интересно. Пока что каждая «за несколько часов», которую я мерил, ломалась на worker'е.


  1. Anselm_nn
    14.09.2026 10:26

    интересно было бы посмотреть сравнение с brvae


    1. i_mrbogdan Автор
      14.09.2026 10:26

      Хороший вопрос, и признаюсь сразу: Brave через стенд не гонял, так что цифр пока нет.

      Но задача у Brave обратная. Его farbling подмешивает в canvas, audio и WebGL случайный шум, свой на каждый сеанс и на каждый сайт. Цель в том, чтобы вас нельзя было узнать между визитами. Поэтому один и тот же браузер выдаёт разные хеши, и сайт это видит: конфигурация как будто меняется на ходу. Для приватности это нормально, для антидетекта это провал, потому что антидетект должен выглядеть как одна конкретная реальная машина, стабильно, в каждом контексте и в каждом вызове. Плюс Brave не трогает то, что как раз палит: navigator.platform, WebGL renderer, Client Hints, экран. Он остаётся Brave на маке, просто с шумным canvas.

      Замерю на неделе и добавлю json в tools/detect-suite/baselines рядом с чистым Chrome, тогда можно будет сравнить не на словах. Если есть конкретные вектора, которые интересуют в первую очередь, напишите, начну с них.


      1. Anselm_nn
        14.09.2026 10:26

        он не меняет фингерпринет внутри работы, так что вряд ли для сайта выглядит как постоянно меняющаяся конфигурация. а вот в разных профилях отдает разные, но сохраняющиеся внутри профиля, фп


        1. i_mrbogdan Автор
          14.09.2026 10:26

          Вы правы, я ошибся. Поставил Brave и прогнал через свой стенд, вот что вышло.

          Отпечаток стабилен внутри сеанса, переживает перезапуск браузера (тот же профиль, закрыл и открыл: ноль отличий) и разный у разных профилей (36 полей). Плюс он разный у разных сайтов в одном профиле: тот же профиль на 127.0.0.1 и на localhost даёт разный canvas, audio, число ядер и память. То есть «постоянно меняющаяся конфигурация» было неверно, беру слова назад.

          Теперь про сравнение. Brave шумит canvas, audio, WebGL2-параметры (сдвигает на единицу: 120 стало 119, 4096 стало 4095), ядра (у меня 10, Brave отдал 9 в одном профиле и 4 в другом), память (16 стало 4), screen (подставляет размер окна), plugins, голоса синтеза речи.

          И не трогает userAgent, platform (MacIntel как был), WebGL renderer (ANGLE Metal Renderer: Apple M5, как в Chrome), таймзону, шрифты. А в Client Hints brands пишет «Brave/153». Поведение тоже отличается от Chrome в 12 местах: Widevine отключён, Network Information API нет, keyboard layout API нет, акселерометр и гироскоп denied. По этому Brave узнаётся без всяких хешей.

          Итого сайт видит: Brave на Apple M5 в ru-RU с такой-то таймзоной, с canvas, который не совпадает ни с одним другим Brave. Это ровно то, что Brave и обещает: вас нельзя связать с вами же на другом сайте. Но он не пытается быть кем-то другим.

          Так что сравнение получается не «кто лучше», а «разные задачи». Brave прячет вас от трекинга между сайтами: вы один, сайтов много, и они не должны сложить вас в одну картинку. Fury для другого: когда команда ведёт десятки аккаунтов на одной площадке, и площадка не должна сложить их в одну картинку. Арбитраж, клиентские рекламные кабинеты, маркетплейсы, тест своего антифрода. Там каждый профиль должен быть отдельной правдоподобной машиной, во всех контекстах и во всех API одинаково, и оставаться ею месяцами.

          Отсюда и разница в том, что вообще есть в продукте. У Brave один профиль на человека и всё. В Fury профиль это единица, которую передают: у него владелец, проект, права доступа, лок (двое не откроют один профиль одновременно и не затрут друг другу куки), аудит, кто и когда его открывал. Бандл профиля шифруется на клиенте и уезжает на ваш же сервер, который его прочитать не может, ключ у организации. Из-за этого сотрудник уходит, а аккаунты остаются; профиль открывается с другой машины и выглядит той же самой машиной. Ни одного из этих слов в Brave нет, потому что ему они не нужны.

          Поэтому Brave как антидетект отсеется на первом же чтении navigator.platform: он честно отдаёт мак с M5 и подписывается Brave в Client Hints. А Fury как приватный браузер для одного человека избыточен, для этого хватит Brave.

          Замер лежит в tools/detect-suite, команда fury-detect diff --mode identity, кто хочет, повторит.


  1. moooV
    14.09.2026 10:26

    В докере безголово можно запустить на линукс сервере где нет десктопа?


    1. i_mrbogdan Автор
      14.09.2026 10:26

      Сейчас нет. Ядро собирается под macOS и Windows, gn-конфигурации под Linux нет, и в планах её тоже нет. В докере на линукс-сервере живёт только командный сервер (он и есть docker compose up, один бинарник плюс Postgres), а браузеры запускаются на машинах людей.

      Почему так, а не «пока не дошли руки». Первое, Linux как третья платформа, которую никто из нас не тестирует, это заявление, а не порт, а я обещал в README писать только то, что проверено. Второе, безголовый браузер на сервере это отдельный отпечаток: нет GPU, WebGL отдаёт SwiftShader, нет реального экрана и окна, и это видно проще, чем любая подмена canvas. Даже прогрев куков в Fury сделан не headless, потому что куки должны быть от того браузера, который потом залогинится.

      Что есть вместо этого: локальный API (шесть эндпоинтов, bearer-токен) и рядом примеры на Playwright и Puppeteer, в том числе «прогнать задачу по всем профилям по одному». Это работает на Windows-VPS с обычным рабочим столом, туда же ставится агент, и оттуда профили открываются с сервера команды. Не docker, но серверный сценарий закрывает.

      Если Linux нужен именно вам, самое полезное, что можно сделать, это собрать ядро и прогнать tools/detect-suite: gn-файлы под мак и винду лежат в core/args, патчи применяются к обычному Chromium. Приму, если стенд пройдёт.


  1. moooV
    14.09.2026 10:26

    Ну и ещё вопрос: на инстаграме тестили?


    1. i_mrbogdan Автор
      14.09.2026 10:26

      Нет. И про конкретные площадки я вообще ничего обещать не буду, ни про инстаграм, ни про другие. Сегодня прошло, завтра они обновили антифрод, и что стоила та проверка. Плюс бан зависит от прокси, истории, поведения, отпечаток там одна переменная из многих.

      Что могу показать, это замеры: все контексты отдают одно и то же, отпечаток не меняется между запусками, поведение совпадает с настоящим Chrome, TLS не патчится. Всё в tools/detect-suite.

      Прогон CreepJS 20 из 20 в планах и пока не сделан, это записано.

      Если есть аккаунты, которые не жалко, лучший тест ваш, а не мой. Поймаете, принесите дамп из detect-suite, разберу.