ESPN DeportesCR7 pide suspender a fans por burlas a Diogo JotaRTP DesportoEuroVolley 2026. Portugal soma quarta derrota consecutiva frente à UcrâniaESPN'I'm willing to do whatever it takes': Inside the 274 days that rebuilt Patrick MahomesThe Jerusalem PostUS President Donald Trump shares his music taste with reporters in odd exchange on Air Force OnePunchNASEMA distributes relief materials to disaster, banditry victimsInquirerSenate approves on 3rd reading 5-year term of barangay, SK officials3DNewsИлон Маск помирился с Apple — но X Corp и SpaceXAI продолжат судиться с OpenAIBusiness AMDenemarken en VS streven naar diplomatieke oplossing voor geschil over GroenlandRapplerLIVE UPDATES: First BARMM parliamentary electionsAntara NewsChinese hiker injured on Indonesia's Mount Rinjani, SAR team deployedBillboardMacklemore Dropped From Ed Sheeran Tour Over His ‘Free Palestine’ CommentsCNN بالعربية"لكن أنا الذي أستحقها".. لامين يامال يسمّى منافسه الوحيد في سباق الكرة الذهبية 2026
The Daily Newsstand · Free, Always
Monday, September 14, 2026

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

Translate

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

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.