• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Подмена браузерного отпечатка: Chrome - часть 1

hipeople

RAM
Пользователь
Регистрация
13.05.2022
Сообщения
123
Реакции
103
Гарант сделки
3
Депозит
0.0022
Автор: hipeople
Источник https://xss.pro


Это вторая часть моего цикла про сокрытие отпечатков на Linux, и сегодня мы поговорим про браузеры. Как мы говорили в первой части, браузеры — это сложная штука, и чтобы подделать их отпечатки, нужно серьёзно постараться. Если на уровне системы мы можем менять MAC-адреса, TTL или разрешения экрана, то в браузерах нас поджидает другой уровень игры: здесь собираются данные, которые выдают не только железо, но и ваши привычки, плагины, настройки и даже то, как вы двигаете мышкой. В этой части мы разберём, какие отпечатки обнаруживает хром, на что это влияет, как хром это делает и, главное, как скрыть свой отпечаток, автоматизировать его смену и остаться незамеченным. Я решил разделить эту статью на две части, первая про Chrome, а вторая про Tor и FireFox. Поехали!


Сбор отпечатков в Chrome
Для начала я расскажу, какие отпечатки собирает Chrome и как он это делаею. Все знают, что Google Chrome чемпион по сбору данных, но не все до конца понимают почему. Я приведу реальные примеры, как Chrome в хроме можно собирать отпечаток, с привязкой к исходникам Chromium, и объясню, на что это влияет.

Какие отпечатки собирает Chrome?
Chrome собирает кучу данных с вашего устройства, и делает это незаметно. Отпечатки можно условно разделить на несколько категорий
Первая — статические, которые почти не меняются: тип операционной системы, архитектура процессора, количество ядер, объем оперативной памяти, разрешение экрана, язык системы, и даже версия драйверов через WebGL.
Вторая категория — динамические отпечатки, которые формируются на основе вашего поведения. Chrome может незаметно анализировать, как вы двигаете мышью, с какой скоростью печатаете, как скролите страницы, как часто переключаете вкладки. Даже как долго вы задерживаетесь в фокусе на определённой части страницы. Всё это собирается с помощью JavaScript и может передаваться либо в реальном времени, либо во время фоновых синхронизаций.
Есть и гибридные механизмы — например, Canvas и WebGL fingerprinting. Когда вы рендерите текст или изображение на холсте через JavaScript, Chrome использует системные шрифты, видеокарту, настройки сглаживания и даже драйвера. Визуально это может выглядеть одинаково, но бинарные данные изображения почти всегда уникальны. То же самое с WebGL: браузер рендерит сложные объекты, измеряет производительность и особенности отрисовки — на основе этого строится уникальный профиль графического окружения.
Chrome также генерирует и хранит уникальные идентификаторы, такие как Client ID — он создаётся при первом запуске браузера и записывается в профиле. Если вы вошли в аккаунт Google, он синхронизируется с аккаунтом и может быть использован для привязки всех ваших устройств. Кроме того, Chrome отправляет данные о вашем устройстве при обновлениях, при подключении к Google-сервисам (Gmail, YouTube и т.д.), включая хэшированные версии MAC-адресов, серийные номера, дату установки, и даже канал обновлений (stable, beta и т.д.).
На сетевом уровне Chrome может "пробить" ваш локальный IP-адрес даже через VPN — через WebRTC или специфические DNS-запросы. Также учитывается fingerprint TLS-соединений и даже уникальность использования HTTP/3 или QUIC. Если вы используете расширения, даже они могут стать частью отпечатка: многие сайты "угадывают", какие именно расширения стоят у вас в браузере, просто проверяя наличие специфичных скриптов или поведения.
Наконец, если вы не отключили сбор статистики Chrome (User Metrics Analysis), браузер отправляет отчёты о сбоях, активности, конфигурации системы, включённых настройках, посещённых сайтах, и даже времени работы браузера без перезагрузки. Всё это в совокупности — система, которой под силу распознать вас почти безошибочно.

Чем опасен сбор отпечатка?
Вы, наверное, думаете: ну собирает Chrome какие-то там отпечатки, и что с того? Подделать это всё равно либо чертовски сложно, либо вообще невозможно, а большой корпорации вроде Google я точно не мешаю, чтобы мне вредить. Но не тут-то было. Chrome строит ваш цифровой профиль не просто так. И этот профиль работает против вас, даже если вы просто хотите остаться незамеченным.
Представьте: вы под VPN постите что-то анонимно, а потом с того же ноутбука заходите в Gmail. Google видит client ID, Canvas-отпечаток, ритм набора текста — и всё, ваши сессии связаны. Тот "анонимный" пост теперь привязан к вашему ноуту. Реальные случаи показывают: киберпреступников ловили именно так — не по IP или другим штукам, а по совпадению отпечатков с их личными аккаунтами. Один заход на YouTube с того же Chrome — и маска слетела.
Рекламные сети тоже в деле. Блокировщики, инкогнито? Не спасут. Они знают, что вы смотрели, с какого устройства, как кликаете. Бывает, зайдёшь на сайт авиабилетов, а потом через VPN видишь цену выше — потому что отпечаток вас выдал. А плагины? Они вообще, как троянский конь. Установил расширение для погоды или блокировщик рекламы — а оно шлёт данные о твоих вкладках, кликах и даже шрифтах третьим лицам. Некоторые плагины прямо встраивают трекеры, которые усиливают отпечаток, добавляя инфу о версиях расширений и их настройках. Даже "безопасные" из Chrome Store могут деанонить — был случай, когда популярный адблок продавал данные пользователей рекламным сетям.
Если вы, к примеру, для рассылок или теста софта решите создать кучу Gmail-аккаунтов, Google запросит номер после парочки, глядя на отпечатки. Плюс скрытые токены, которые Chrome прячет вне профиля — в памяти или через Google Update. Вы не просто пользователь, вы — открытая книга, даже не подозревая об этом.

Примеры сбора отпечатка из исходников Chromium
Теперь я приведу примеры формирования отпечатка из реальных исходников Chromium. Поскольку исходники Google Chrome в открытом доступе отсутствуют, приходится опираться на ограниченные данные, но важно учитывать, что Google может добавлять дополнительные элементы при сборке. Сам Chromium отпечатки не собирает, но предоставляет данные, которые сайты затем объединяют. Таких данных в его исходниках — огромное количество! Я покажу несколько примеров из кода, затем добавлю пару мыслей о том, что ещё может быть скрыто, и приведу пример, как это всё собирается на JavaScript.

Chromium знает, сколько ядер в твоём процессоре, и с радостью делится этим через navigator.hardwareConcurrency. Это прямо часть спецификации, и сайты это обожают для отпечатков.

Исходник:
C-подобный:
int NavigatorConcurrentHardware::hardwareConcurrency() const {

  return base::SysInfo::NumberOfProcessors();

}
Chrome определяет число ядер через системный вызов NumberOfProcessors. Функция NumberOfProcessors() лезет в систему и смотрит, сколько у тебя ядер. На Linux, например, она может заглянуть в /proc/cpuinfo или спросить через sched_getaffinity — вроде, "Эй, система, сколько у нас рабочих лошадок?". Потом это число уходит в JavaScript, и любой сайт может его подхватить. У кого-то 4 ядра, у кого-то 16 — уже первая зацепка для отпечатка.

Пример на JS:
JavaScript:
function getHardwareConcurrency() {

  const cores = navigator.hardwareConcurrency;

  console.log(`У тебя ядер: ${cores}`);

  return cores;

}

getHardwareConcurrency();

Вывод:
photo_2025-05-14_09-23-05.jpg


Chromium ещё и про видеокарту твоего пк знает все — модель, производителя, даже версию драйверов. Это через WebGL API сайты могут получить информацию о ней.

Исходник:
C-подобный:
bool CollectGraphicsInfoGL(GPUInfo* gpu_info, gl::GLDisplay* display) {

  GPU_STARTUP_TRACE_EVENT("gpu_info_collector::CollectGraphicsInfoGL");

  DCHECK_NE(gl::GetGLImplementationParts(), gl::kGLImplementationNone);

  gl::GLDisplayEGL* egl_display = display->GetAs<gl::GLDisplayEGL>();

  scoped_refptr<gl::GLSurface> surface(InitializeGLSurface(display));

  if (!surface.get()) {
    LOG(ERROR) << "Could not create surface for info collection.";
    return false;

  }

  scoped_refptr<gl::GLContext> context(InitializeGLContext(surface.get()));

  if (!context.get()) {
    LOG(ERROR) << "Could not create context for info collection.";
    return false;

  }

  if (egl_display) {
    gpu_info->display_type =
        GetDisplayTypeString(egl_display->GetDisplayType());
  }

  gpu_info->gl_renderer = GetGLString(GL_RENDERER);
  gpu_info->gl_vendor = GetGLString(GL_VENDOR);
  gpu_info->gl_version = GetGLString(GL_VERSION);
  std::string glsl_version_string = GetGLString(GL_SHADING_LANGUAGE_VERSION);
Тут Chromium дергает OpenGL-команды, типа glGetString(GL_VENDOR), и получает, например, "NVIDIA Corporation", а через glGetString(GL_RENDERER) — "NVIDIA GeForce RTX 3080". Это нужно браузеру для рендеринга графики и диагностики, но сайты через WebGL могут это подглядеть. У каждого своя видяха, свои драйвера — вот тебе и уникальность. Код сам по себе простой: дернул данные из OpenGL и закинул их в структуру GPUInfo, а дальше они доступны через JS.

Пример на JS:
JavaScript:
function getGPUFingerprint() {

  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl');

  if (!gl) {
    console.log("WebGL не работает");
    return;

  }

  const ext = gl.getExtension('WEBGL_debug_renderer_info');

  if (ext) {
    const vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);
    const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
    console.log(`Производитель: ${vendor}`);
    console.log(`Видяха: ${renderer}`);

  }
}
getGPUFingerprint();

Вывод:
photo_2025-05-11_07-57-48.jpg

Crash-репорты — это когда браузер падает, и Chromium записывает, что пошло не так. Например, какая версия была, на каком железе всё рухнуло. А DRM — это защита контента, вроде Widevine, чтобы ты смотрел Netflix, а не пиратил его. Оба этих механизма собирают информацию о твоём девайсе.

Исходник:


C-подобный:
void SetMachineID() {
  std::string machine_id = base::SysInfo::HardwareModelName() + "-" +
                           base::SysInfo::GetUniqueMachineId();
  crash_keys::SetKey("machine_id", machine_id);

}

HardwareModelName() вытягивает модель девайса — скажем, "Lenovo ThinkPad X1". Это может быть из системных файлов, типа /sys/devices/virtual/dmi/id/product_name на Linux. А GetUniqueMachineId() пытается сделать уникальный ID, основываясь на серийном номере железа или чём-то таком — точная реализация зависит от платформы, но суть в том, что это что-то вроде отпечатка твоей машины. Потом это склеивается в строку, например, "Lenovo ThinkPad X1-abc123xyz", и сохраняется для crash-репортов или DRM. Сайты это напрямую не видят, но это показывает, как Chromium внутри себя знает все о твоем железе.

Пример вывода (гипотетический):
Код:
"machine_id": "Lenovo ThinkPad X1-abc123xyz"

Пример простого скрипта для сбора отпечатка
Теперь давай склеим это всё в один пример на js, чтобы показать, как сайт может собрать отпечаток из того, что Chromium даёт:
JavaScript:
function collectFingerprint() {
  const fingerprint = {};
  fingerprint.cores = navigator.hardwareConcurrency;
  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl');

  if (gl) {
    const ext = gl.getExtension('WEBGL_debug_renderer_info');
    if (ext) {
      fingerprint.gpuVendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);
      fingerprint.gpuRenderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);

    }
  }

  fingerprint.screen = `${window.screen.width}x${window.screen.height}`;
  fingerprint.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  fingerprint.language = navigator.language;
  console.log("Твой отпечаток:", fingerprint);
  return fingerprint;

}
collectFingerprint();

Вывод в консоли:
allfin.jpg

Вы, конечно, можете заметить, и не без оснований, что мой пример показывает не прямой сбор отпечатка устройства самим Chromium, а просто, как он генерит данные для каких-то своих задач. Но это всё равно создаёт риски, потому что сторонние сайты могут использовать эти данные, чтобы вас идентифицировать. Плюс, это лишь часть примеров, и остаётся загадкой, что ещё Google добавляет в Chrome.

Вот и всё, мы наконец-то разобрались, как и какие данные для отпечатков собирает Chromium. Примеры кода, которые я показал, — это лишь базовые варианты, а на деле сайты могут использовать гораздо более хитрые способы. Но даже с этими примерами, надеюсь, вы поняли, какие риски тут есть.


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

Инструменты для сокрытия отпечатка
Сначала познакомимся с Puppeteer
. Puppeteer это библиотека для Node.js, которая позволяет управлять Chrome через JavaScript. Чаще всего его используют для автоматизации задач вроде заполнение форм. Но кто сказал, что это всё, на что он способен? Puppeteer идеально подходит для точечной подмены отпечатков прямо на уровне JavaScript. Суть в том, что Puppeteer может выполнять произвольный код внутри страницы сразу после её загрузки, но ещё до того, как скрипты самого сайта начнут работать. Это позволяет перехватить или переопределить любые свойства JavaScript-объектов, через которые сайт собирает отпечаток браузера.

Чтобы Puppeteer работал, нужен Node.js.
Установить его можно командой:
Код:
sudo apt install -y nodejs npm

Потом создать проект и поставить Puppeteer:
Код:
npm init -y
npm install puppeteer

Второй способ подмены — флаги запуска Chrome. Это параметры, которые передаются браузеру в командной строке и позволяют изменить его поведение. Флаги хороши для быстрых изменений, но не всё можно подделать, часть параметров всё равно будет считываться с системы. Так же для постоянной работы лучше завернуть их в Bash-скрипт. Список флагов огромный, но мы будем использовать только те, что реально помогают в подмене отпечатков.

Теперь к более продвинутым инструментам — Bubblewrap. Это программа, которая запускает Chrome в изолированном пространстве. Она ограничивает, к каким системным ресурсам у браузера есть доступ: можно, например, подменить системные шрифты, заморозить доступ к реальным системным переменным или подложить кастомные версии конфигов. Работает Bubblewrap на базе пространств имён Linux и является частью системы Flatpak, хотя ставится отдельно. По сути, это как контейнер, в котором ты сам решаешь, что Chrome может видеть, а что нет. В этом окружении браузер не получит доступ ни к реальной файловой системе, ни к твоим настоящим настройкам. Всё, что он увидит — то, что ты ему разрешишь смонтировать при запуске.

Установить Bubblewrap просто:
Код:
sudo apt install bubblewrap.

Похожую задачу решает Firejail. Он тоже запускает Chrome в изолированной песочнице, используя Linux namespace и фильтрацию системных вызовов. namespace в Linux позволяют создавать такие изолированные окружения, где приложение, например Chrome, видит только заданные ресурсы, и не имеет доступа к остальным каталогам. Для этого создаётся профиль — обычный текстовый файл, где прописаны правила доступа. Chrome запускается через Firejail с этим профилем, и начинает работать в среде, которую ты ему создал. Root-доступ тут не всегда обязателен, но для глубоких ограничений может понадобиться.

Устанавливается он одной командой:
Код:
sudo apt install firejail.

И напоследок — Tampermonkey. Это расширение для Chrome, которое пускает твои скрипты на любой сайт. Хочешь подменить данные о процессоре, видеокарте или Canvas? Пишешь JavaScript, загружаешь его в Tampermonkey, и он работает на каждой странице. Он работает, перехватывая или подделывая данные ещё до того, как сайт их увидит. К примру Tampermonkey перехватывает вызовы API через Object.defineProperty или прямое переопределение методов.
Некоторые сайты используют защиту от user script-расширений, таких как Tampermonkey. Это может включать обфускацию кода, проверку целостности скриптов или блокировку подозрительных вмешательств. Такие меры могут ограничивать или полностью предотвращать работу пользовательских скриптов. Так что будьте осторожны, использование Tampermonkey для подмены данных может не всегда работать.

Установить его достаточно просто:
Для этого надо зайти в Chrome Web Store(https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo), и нажать "Установить":
tamper.jpg

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


Подмена отпечатков
Мы разобрали инструменты для подмены отпечатков, а теперь давайте поговорим про подмену отпечатков. Но перед разбором отмечу одну важную вещь, подмена отпечатков позволяет замаскировать устройство и повысить приватность, но требует тщательной настройки. Несогласованность параметров — например, когда User-Agent указывает на Windows, а шрифты выдают Linux может быть обнаружена антифрод-системами. Потому важна не только сама подмена отпечатка, а подход к его подмене с умом. Потому вам следует учитывать это применяя методы из моей статьи. Разговор про подмену отпечатка, пожалуй, начнём идя от простого к сложному, потому первым делом сначала разберём методы подмены User-Agent.

Смена User agent
User agent для злоумышленника весьма важная зацепка, он может показывает ему версию браузера, операционную систему, и даже устройство, как вы видите уже достаточно много информации. Подмена User-Agent — один из самых простых способов изменить то, как сайт вас видит, хотя это лишь верхушка айсберга в борьбе с отпечатками. Давайте разберём каждый метод подмены User-Agent, с примерами кода, но где брать реальные юзера агенты и как это автоматизировать для смены разных User-Agent я расскажи позже в разделе про инструменты.

Первый способ — самый простой запуск Chrome с флагом --user-agent. Это настолько просто, что даже новичок справится. Вы добавляете флаг в командную строку при запуске браузера, и Chrome начинает отправлять указанный User-Agent всем сайтам. Например, вы хотите, чтобы сайт думал, что вы сидите с iPhone.

Пример:
Bash:
#!/bin/bash

UA="Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"

google-chrome --user-agent="$UA" &

Проверю изменился ли мой юзер агент на сайте https://whatmyuseragent.com/ , как мы видим мы верно подменили юзер агент:
user.jpg

Теперь подробнее разберем что происходит в этом коде?
Скрипт сохраняет User-Agent в переменную и запускает Chrome с этим для смены юзер агента в фоновом режиме (благодаря &). Флаг --user-agent переписывает значение, которое Chrome отправляет в HTTP-заголовке User-Agent. Плюс метода в его простоте: не нужно ничего устанавливать или модифицировать. Минус — это одноразовое решение. Новый запуск Chrome без флага вернёт старый User-Agent, и автоматизировать для постоянной работы не всегда удобно. К тому же, сайты могут заподозрить неладное, если User-Agent не соответствует другим отпечаткам, например, разрешению экрана или поведению JavaScript.

Давайте усложним задачу и создадим кастомный профиль Chrome. Профиль — это папка с настройками браузера, где хранятся ваши предпочтения, расширения и кэш. Мы можем заранее настроить профиль, чтобы он всегда использовал нужный User-Agent. Один из способов — добавить расширение, которое подменяет User-Agent, но можно пойти дальше и модифицировать файл Preferences. Этот файл лежит в папке профиля (обычно ~/.config/google-chrome/Default/Preferences) и хранит настройки в формате JSON.

Вот пример Bash-скрипта, который создаёт новый профиль с нужным User-Agent:
Bash:
#!/bin/bash
PROFILE_DIR="/tmp/chrome-custom-profile"
UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"

rm -rf "$PROFILE_DIR"
mkdir -p "$PROFILE_DIR"

cat <<EOF > "$PROFILE_DIR/Preferences"

{
  "profile": {
    "name": "CustomProfile"
  },

  "webrtc": {
    "ip_handling_policy": "disable_non_proxied_udp",
    "multiple_routes_enabled": false

  }
}
EOF

google-chrome \
  --user-data-dir="$PROFILE_DIR" \
  --user-agent="$UA" \
  --disable-extensions \
  --no-first-run \
  --no-default-browser-check \
  --disable-background-networking \
  --disable-sync \
  & disown

При запуске как мы видим создаётся новый профиль:
photo_2025-05-11_07-57-48 (2).jpg

Проверяем юзер агент:
Screenshot_2025-04-14_09-12-02.png

Что делает этот код? Он создаёт новую папку для чистого профиля Chrome, удаляя старую, чтобы избежать конфликтов. Затем записывает в неё файл Preferences с базовыми настройками, такими как имя профиля и отключение WebRTC для большей приватности. User-Agent задаётся через флаг командной строки, заставляя Chrome использовать указанную строку (в данном случае Windows). Дополнительные флаги отключают расширения, фоновые запросы и начальные проверки, обеспечивая минималистичную среду. Chrome запускается с этими настройками в фоновом режиме.

Дальше — расширения. Есть готовые решения вроде "User-Agent Switcher", которые можно установить из Chrome Web Store. Они позволяют выбрать User-Agent из списка или задать свой. Это нельзя автоматизировать через bash зато можно в самом плагине устанавливать список необходимых юзер агентов.

Устанавливаем плагин:
Screenshot_2025-04-16_19-54-18.png

Далее мы можем задать опции, к примеру для каких сайтов какой юзер агент использовать, или же добавить в список юзер агентов плагина свой агент:
Screenshot_2025-04-16_19-57-35.png

Как вы видели я добавил к whatmyuseragent.com юзер агент айфона, поэтому проверяем:
Screenshot_2025-04-16_19-58-48.png

Проверяем:
Screenshot_2025-04-16_19-59-18.png

Но вообще поменять юзер агент для сайта, можно и не заходя в настройки, нужно просто нажать на плагин и выбрать тип юзер агента:
Screenshot_2025-04-16_20-06-46.png


В этом методе расширение само делает всю работу, и вы можете менять User-Agent через его интерфейс. Но увы расширениям не всегда можно доверять, плюс наличие расширений иногда обнаруживается сайтами через их поведение или следы в DOM, конечно позже я расскажу методы как их скрыть, но тем не менее наличие плагинов все равно риск .

Есть ещё один способ запустить браузер через Puppeteer, используя его можно поменять User-Agent через в код на node js.

Вот как это можно сделать:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false, // Браузер видимый
        args: ['--no-sandbox', '--disable-setuid-sandbox'],

    });

    const customUserAgent = 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1';
    const pages = await browser.pages();

    for (const page of pages) {
        await page.setUserAgent(customUserAgent);
    }

    browser.on('targetcreated', async (target) => {
        if (target.type() === 'page') {
            const newPage = await target.page();
            if (newPage) {
                await newPage.setUserAgent(customUserAgent);
                console.log('Applied custom User-Agent to new page');
            }
        }
    });

    const page = await browser.newPage();
    await page.setUserAgent(customUserAgent);
    await page.goto('https://whatmyuseragent.com/', { waitUntil: 'networkidle0' });

    console.log('Initial page User-Agent:', await page.evaluate(() => navigator.userAgent));

})();

И вот теперь добавляем наш скрипт к примеру в файл index.js и запускаем:
photo_2025-05-14_10-33-34.jpg

Что происходит? Puppeteer открывает Chrome в видимом режиме, задаёт кастомный User-Agent с помощью page.setUserAgent и заходит на сайт — в примере это whatmyuseragent.com, чтобы убедиться, что подмена сработала. Поскольку браузер не закрывается, можно дальше взаимодействовать с ним, используя этот же User-Agent.

Каждый из этих методов решает задачу подмены User-Agent, но ни один не даёт полной защиты. User-Agent — лишь часть отпечатка, и если вы подменяете только его, сайты могут заметить несоответствие с другими данными, вроде WebGL или Canvas. Потому давайте разбираться, как подменить остальную часть отпечатка

Смена Client Hints
Выше мы говорили про юзер агенты, но давай копнём чуть глубже и разберёмся, в других заготовках по которым браузер может вычислить вашу ос. Есть штуки, которые называются Client Hints, или HTTP-заголовки, которые выдают кучу дополнительной инфы о вас. Они могут выдавать подробности о вашем устройстве, такие как ОС, процессор и версия браузера. Примером таких заголовков являются Sec-CH-UA (версия браузера), Sec-CH-UA-Platform (операционка) или Sec-CH-UA-Model (модель девайса). Что ж давайте разбрёмся, как их подделать.

Можно подменять Client Hints и другие заголовки, маскируя браузер под Firefox с помощью Puppeteer и CDP, а также перехват всех HTTP-запросов с подстановкой нужных заголовков.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0';

  const overrideHeaders = {
    'User-Agent': userAgent,
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept-Encoding': 'gzip, deflate, br',
    'Sec-CH-UA': '"Firefox";v="128", "Not A Brand";v="99"',
    'Sec-CH-UA-Platform': '"Windows"',
    'Sec-CH-UA-Mobile': '?0',
    'DNT': '1',
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate',
    'Sec-Fetch-Site': 'none',
    'Sec-Fetch-User': '?1',
  };

  const applyOverrides = async (page) => {
    await page.setUserAgent(userAgent);

    await page.setExtraHTTPHeaders({
      'Accept': overrideHeaders['Accept'],
      'Accept-Language': overrideHeaders['Accept-Language'],
      'Accept-Encoding': overrideHeaders['Accept-Encoding'],
      'DNT': '1',
      'Upgrade-Insecure-Requests': '1',
      'Sec-Fetch-Dest': 'document',
      'Sec-Fetch-Mode': 'navigate',
      'Sec-Fetch-Site': 'none',
      'Sec-Fetch-User': '?1',

    });

    const client = await page.target().createCDPSession();
    await client.send('Network.setUserAgentOverride', {

      userAgent: userAgent,
      platform: 'Windows',

      userAgentMetadata: {
        brands: [
          { brand: 'Firefox', version: '128' },
          { brand: 'Not A Brand', version: '99' },
        ],

        fullVersion: '128.0',
        platform: 'Windows',
        platformVersion: '10.0',
        architecture: 'x86',
        model: '',
        mobile: false,
      },

    });

    await page.setRequestInterception(true);
    page.on('request', (request) => {
      request.continue({ headers: { ...request.headers(), ...overrideHeaders } });
    });

  };

  const browser = await puppeteer.launch({

    headless: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-blink-features=AutomationControlled',
      '--disable-web-security',
      '--disable-features=IsolateOrigins,site-per-process',
    ],
  });

  const page = await browser.newPage();
  await applyOverrides(page);

  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      try {
        const newPage = await target.page();
        if (newPage) await applyOverrides(newPage);
      } catch (err) {
        console.error('Failed to apply overrides to new tab:', err);
      }
    }
  });

  await page.goto('https://browserleaks.com/javascript');
})();

Проверяем:
photo_2025-05-14_10-57-27.jpg

Код запускает Chrome с Puppeteer и подменяет основные заголовки, включая user-agent и client hints. Это делается как через Puppeteer API, так и через CDP-команду Network.setUserAgentOverride, чтобы изменить данные, которые браузер отправляет на сервер, и те, что видны из JS.

Можно подменить заголовки браузера и отпечатки, связанные с Client Hints, даже без использования CDP, просто через Puppeteer — за счёт перехвата всех запросов и внедрения скрипта до загрузки страницы.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {

  const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0';

  const headers = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept-Encoding': 'gzip, deflate, br',
    'DNT': '1',
    'Upgrade-Insecure-Requests': '1',
  };

  const browser = await puppeteer.launch({
    headless: false,

    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-blink-features=AutomationControlled',
    ],
  });

  async function applyStealth(page) {
    await page.setUserAgent(userAgent);
    await page.setExtraHTTPHeaders(headers);
    await page.setRequestInterception(true);
    page.on('request', (req) => {
      const modifiedHeaders = {
        ...req.headers(),
        ...headers,
        'User-Agent': userAgent,
        'Sec-CH-UA': '"Firefox";v="128", "Not A Brand";v="99"',
        'Sec-CH-UA-Platform': '"Windows"',
        'Sec-CH-UA-Mobile': '?0',
      };
      req.continue({ headers: modifiedHeaders });
    });
    await page.evaluateOnNewDocument(uaOverride);
  }

  function uaOverride() {
    Object.defineProperty(navigator, 'userAgent', {
      get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0',
    });

    Object.defineProperty(navigator, 'userAgentData', {
      get: () => undefined,
    });

  }

  const page = await browser.newPage();
  await applyStealth(page);
  browser.on('targetcreated', async (target) => {

    if (target.type() === 'page') {
      const newPage = await target.page();
      if (newPage) await applyStealth(newPage);
    }
  });
  await page.goto('https://browserleaks.com/javascript');

})();
Этот код меняет user-agent, заголовки Client Hints и JavaScript-свойства navigator.userAgent и navigator.userAgentData. Все запросы проходят через перехватчик, где подставляются нужные заголовки. Также добавляется инъекция скрипта, которая подменяет userAgent внутри JS-движка, чтобы скрыть Puppeteer и обмануть fingerprint-детекторы.

Можно даже создать плагин для Хрома для подмены этих заголовков, чтобы браузер сразу выглядел как Firefox(для примера но по факту естественно можно замаскироваться под что угодно). Это удобно если нужно постоянное поведение прямо в реальном браузере.

Пример:
manifest.json:

Код:
{

  "manifest_version": 3,
  "name": "Header Spoofer",
  "version": "1.0",
  "description": "Spoofs HTTP headers and navigator properties",

  "permissions": [
    "declarativeNetRequest",
    "webNavigation",
    "activeTab"
  ],

  "host_permissions": [
    "<all_urls>"
  ],

  "background": {
    "service_worker": "background.js"
  },

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ]

}

background.js:
JavaScript:
const rules = [
  {
    id: 1,
    priority: 1,
    action: {
      type: "modifyHeaders",
      requestHeaders: [
        {
          header: "User-Agent",
          operation: "set",
          value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"
        },
        {
          header: "Accept",
          operation: "set",
          value: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
        },
        {
          header: "Accept-Language",
          operation: "set",
          value: "en-US,en;q=0.9"
        },
        {
          header: "Accept-Encoding",
          operation: "set",
          value: "gzip, deflate, br"
        },
        {
          header: "Sec-CH-UA",
          operation: "set",
          value: "\"Firefox\";v=\"128\", \"Not A Brand\";v=\"99\""
        },
        {
          header: "Sec-CH-UA-Platform",
          operation: "set",
          value: "\"Windows\""
        },
        {
          header: "Sec-CH-UA-Mobile",
          operation: "set",
          value: "?0"
        },
        {
          header: "DNT",
          operation: "set",
          value: "1"
        }
      ]
    },
    condition: {
      urlFilter: "*",
      resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest", "image", "script", "stylesheet", "font", "other"]
    }
  }
];

chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1],
  addRules: rules
}, () => {
  console.log("declarativeNetRequest rules applied for Chrome");

});

if (chrome.webRequest && navigator.userAgent.includes("Firefox")) {
  console.log("Using webRequest for Firefox");

  chrome.webRequest.onBeforeSendHeaders.addListener(
    (details) => {
      const headers = details.requestHeaders.filter(
        (header) =>
          ![
            "user-agent",
            "accept",
            "accept-language",
            "accept-encoding",
            "sec-ch-ua",
            "sec-ch-ua-platform",
            "sec-ch-ua-mobile",
          ].includes(header.name.toLowerCase())
      );

      headers.push(
        {
          name: "User-Agent",
          value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"
        },
        { name: "Accept", value: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" },
        { name: "Accept-Language", value: "en-US,en;q=0.9" },
        { name: "Accept-Encoding", value: "gzip, deflate, br" },
        { name: "Sec-CH-UA", value: "\"Firefox\";v=\"128\", \"Not A Brand\";v=\"99\"" },
        { name: "Sec-CH-UA-Platform", value: "\"Windows\"" },
        { name: "Sec-CH-UA-Mobile", value: "?0" },
        { name: "DNT", value: "1" }
      );
      return { requestHeaders: headers };
    },
    { urls: ["<all_urls>"] },
    ["blocking", "requestHeaders"]
  );
}

content.js:
JavaScript:
(function () {
  "use strict";
  Object.defineProperty(navigator, "userAgent", {
    get: () => "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
    configurable: true,
  });

  Object.defineProperty(navigator, "platform", {
    get: () => "Win32",
    configurable: true,
  });

  Object.defineProperty(navigator, "hardwareConcurrency", {
    get: () => 8,
    configurable: true,

  });

  Object.defineProperty(navigator, "userAgentData", {
    get: () => undefined,
    configurable: true,

  });

  const getParameter = WebGLRenderingContext.prototype.getParameter;
  WebGLRenderingContext.prototype.getParameter = function (parameter) {

    if (parameter === 37446) return "Mozilla";
    if (parameter === 37447) return "Firefox";
    return getParameter.apply(this, arguments);

  };

  const getImageData = HTMLCanvasElement.prototype.toDataURL;
  HTMLCanvasElement.prototype.toDataURL = function () {
    const result = getImageData.apply(this, arguments);
    return result.replace(/=+$/, "") + "==";
  };

})();

Для проверки устнанавливаем палгин:
photo_2025-05-14_10-56-11.jpg

Запускаем его:

photo_2025-05-14_10-56-47.jpg

Проверяем работает ли:
photo_2025-05-11_06-57-14.jpg

Это расширение состоит из нескольких файлов. background.js отвечает за подмену HTTP-заголовков через declarativeNetRequest. Там подставляется кастомный User-Agent, сек-хинты (Sec-CH-UA, Platform, Mobile) и прочие стандартные поля. Параллельно content.js подменяет свойства navigator, отключает userAgentData, и маскирует WebGL + Canvas (в том числе добавляя noise в toDataURL). Всё это вместе подделывает поведение браузера на уровне и сети, и JavaScript, чтобы максимально сбить трекинг и отпечатки.


Подмена шрифтов
Шрифты —
это один из главных источников уникальности браузера. Сайты через Canvas или CSS могут узнать, какие шрифты установлены в системе, и использовать это для создания отпечатка. Наша цель — ограничить доступ Chrome к шрифтам (что может выглядеть подозрительно) или подменить их, чтобы браузер выглядел максимально стандартно. Для проверки я буду использовать browserleaks.com/fonts.

Мы можем скрыть системные шрифты, используя mount --bind, чтобы подменить системный каталог шрифтов /usr/share/fonts нашей собственной папкой, содержащей только выбранный шрифт. Это заставляет Chrome видеть минимальный набор шрифтов, снижая уникальность отпечатка. Это достаточно эффективно, ведь Chrome физически не может получить доступ к системным шрифтам вне нашей папки. Однако требует прав root для монтирования, плюс без блокировки веб-шрифтов сайты могут подгрузить их самостоятельно.

Пример на Bash:
Bash:
#!/bin/bash
FAKE_FONTS_DIR="/tmp/fakes-fonts"
MOUNT_DIR="/tmp/chrome-fonts-mount"
PROFILE_DIR="/tmp/chrome-fonts-profile"
CACHE_DIR="/tmp/chrome-cache"

mkdir -p "$FAKE_FONTS_DIR"
cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf "$FAKE_FONTS_DIR/"
chmod -R a+r "$FAKE_FONTS_DIR"
rm -rf "$PROFILE_DIR" "$CACHE_DIR"

sudo mount --bind "$FAKE_FONTS_DIR" /usr/share/fonts
sudo fc-cache -f -v

google-chrome \
  --user-data-dir="$PROFILE_DIR" \
  --disk-cache-dir="$CACHE_DIR" \
  --no-first-run \
  --disable-background-networking \
  --disable-remote-fonts &

wait
sudo umount /usr/share/fonts

Проверяем работоспосбность:
photo_2025-05-14_11-06-09.jpg

Скрипт создаёт изолированное окружение для Chrome, чтобы браузер видел только один шрифт — DejaVu Sans. Для этого он готовит временную папку /tmp/fake11-fonts, куда копируется шрифт, и делает его доступным для чтения. Затем удаляются старые данные профиля и кэша, чтобы избежать конфликтов. С помощью mount --bind системный каталог /usr/share/fonts заменяется нашей папкой, а команда fc-cache обновляет кэш шрифтов, чтобы изменения сразу вступили в силу. Chrome запускается с чистым профилем, отдельным кэшем, без фоновых запросов и веб-шрифтов, что минимизирует утечки. После закрытия браузера скрипт пытается размонтировать каталог, возвращая систему в исходное состояние.
Правда иногда размонтировать /usr/share/fonts не удаётся, если каталог всё ещё используется каким-нибудь процессом (например, Chrome не до конца закрылся или другой софт держит файлы). В таком случае команда sudo umount /usr/share/fonts выдаст ошибку вроде device is busy. Чтобы это исправить, можно вручную проверить, какие процессы используют каталог, с помощью sudo lsof +D /usr/share/fonts. В выводе будут перечислены процессы — обычно это chrome или связанные с ним. Их можно завершить командой pkill chrome, после чего повторить sudo umount /usr/share/fonts.

Для еще одного метода мы моем использовать Firejail для изоляции браузера.

Пример:

Bash:
#!/bin/bash

FAKE_FONTS_DIR="/tmp/fake-fonts"

mkdir -p "$FAKE_FONTS_DIR"
cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf "$FAKE_FONTS_DIR/"

FIREJAIL_PROFILE="/tmp/chrome-fonts.profile"
cat <<EOF > "$FIREJAIL_PROFILE"

noblacklist $FAKE_FONTS_DIR
whitelist $FAKE_FONTS_DIR

blacklist /usr/share/fonts
blacklist ~/.fonts
blacklist ~/.local/share/fonts
EOF

firejail \
  --profile="$FIREJAIL_PROFILE" \
  google-chrome \
  --user-data-dir="/tmp/chrome-fonts-profile" \
  --no-first-run \
  --disable-background-networking &

Проверяем работу скрипта:
photo_2025-05-14_11-09-26.jpg

Код работает следующим образом: сначала создаётся временная папка с выбранным шрифтом, затем формируется профиль для Firejail, который разрешает доступ только к этой папке и блокирует стандартные системные каталоги шрифтов. После этого Chrome запускается в изолированной среде, где он может использовать только указанный шрифт.

Ещё для смены шрифтов можно использовать fontconfig — штуку, которая помогает настраивать шрифты в Linux, чтобы Chrome видел только те шрифты, которые мы ему подсунули. Fontconfig позволяет управлять шрифтами для программ, не трогая системные файлы.

Пример на Bash:
Bash:
#!/bin/bash
FAKE_FONTS_DIR="/tmp/fake-fonts"
mkdir -p "$FAKE_FONTS_DIR"
ln -sf /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf "$FAKE_FONTS_DIR/CustomFont.ttf"
FONTS_CONF_DIR="/tmp/fonts-conf"
mkdir -p "$FONTS_CONF_DIR"

cat <<EOF > "$FONTS_CONF_DIR/fonts.conf"
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
  <dir>$FAKE_FONTS_DIR</dir>
  <match target="pattern">
    <test name="family">
      <string>Arial</string>
    </test>
    <edit name="family" mode="assign">
      <string>CustomFont</string>
    </edit>
  </match>
  <cachedir>/tmp/font-cache</cachedir>
</fontconfig>
EOF

FONTCONFIG_PATH="$FONTS_CONF_DIR" \
google-chrome \
  --user-data-dir="/tmp/chrome-fonts-profile" \
  --no-first-run \
  --disable-background-networking &
Можно так же использовать bwrap, чтобы Chrome видел только те шрифты, которые мы ему дадим.

Пример на Bash:
Bash:
#!/bin/bash
FAKE_FONTS_DIR="/tmp/fake-fonts"
mkdir -p "$FAKE_FONTS_DIR"

cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf "$FAKE_FONTS_DIR/"

sudo bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /sbin /sbin \
  --ro-bind "$FAKE_FONTS_DIR" /usr/share/fonts \
  --bind /tmp /tmp \
  --dev /dev \
  --proc /proc \
  --unshare-all \
  --share-net \
  --die-with-parent \
  google-chrome \

  --user-data-dir="/tmp/chrome-fonts-profile" \
  --no-first-run \
  --disable-background-networking
В скрипте создаётся папка с одним шрифтом, который мы хотим показать. Bubblewrap запускает Chrome в контейнере, где системная папка шрифтов заменяется на нашу, а остальной системе дан доступ только для чтения, чтобы браузер работал нормально. Сеть оставляем открытой, а профиль браузера изолируем, чтобы настройки не мешали.


Подмена локали
Теперь затронем новую, несомненно, полезную тему, а именно про подмену местоположение в хром, первое что мы разберем это подмена локали. Локаль браузера — ещё один элемент, который сайты используют для создания отпечатка. Она раскрывается через HTTP-заголовки, JavaScript. Подмена локали это достаточно важно, чтобы браузер не орал: «Эй, он не тот, за кого себя выдаёт!». Ведь VPN маскирует IP, но без подмены локали ты всё равно можешь спалиться, потому что язык браузера кричит о твоём реальном местоположении. Для проверки будем использовать тот же browserleaks.com/javascript

Самый лёгкий способ — запустить Chrome с флагом --lang. Это меняет язык браузера и то, что видит сайт через navigator.language:

Пример:

Bash:
#!/bin/bash
PROFILE_DIR="/tmp/chrome-ja-locale"
rm -rf "$PROFILE_DIR"
mkdir -p "$PROFILE_DIR"
LANG=ja_JP.UTF-8 \
LC_ALL=ja_JP.UTF-8 \

google-chrome \
  --user-data-dir="$PROFILE_DIR" \
  --lang=ja-JP \
  --no-first-run \
  --disable-translate \
  --disable-features=TranslateUI \
  --accept-lang=ja-JP \
  --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:112.0; ja-JP) Gecko/20100101 Firefox/112.0" \
  & disown

Проверяем:
photo_2025-05-14_11-21-10.jpg

Этот скрипт запускает Google Chrome с японской локалью, чтобы браузер и сайты отображали интерфейс и контент на японском языке. Он создаёт временный профиль, очищая его перед запуском. Флаги --lang=ja-JP и --accept-lang=ja-JP задают японскую локаль и предпочтение языка для сайтов, а переменные окружения LANG и LC_ALL усиливают эффект, чтобы сайты точно видели японскую локаль. Флаг --user-agent подменяет обычный юзер агент на юзер агент с точным указвнием нужной нам локали. Опции --no-first-run, --disable-translate и --disable-features=TranslateUI отключают начальную настройку Chrome и функции перевода.

Еще локаль можно сменить спомщью Firejail, котрую мы использовали ранее для смены шифтов, но на вякий случай напомню она запускает Chrome в песочнице, где можно задать свои переменные окружения и тд. Так мы меняем локаль только для браузера, не трогая систему.

Пример:
Bash:
#!/bin/bash
PROFILE_DIR="/tmp/chrome-locale-profile"
FIREJAIL_PROFILE="/tmp/chrome-locale.profile"
rm -rf "$PROFILE_DIR"
mkdir -p "$PROFILE_DIR"

cat <<EOF > "$FIREJAIL_PROFILE"
env LANG=ja_JP.UTF-8
env LANGUAGE=ja_JP
env LC_ALL=ja_JP.UTF-8
noblacklist /tmp
EOF

firejail \
  --profile="$FIREJAIL_PROFILE" \
  google-chrome \
  --user-data-dir="$PROFILE_DIR" \
  --no-first-run \
  --disable-background-networking \
  & disown

Проверка:
photo_2025-05-14_11-32-33.jpg

Этот скрипт создаёт изолированное окружение, где Chrome видит только локаль ja_JP.

Так же для подобных целей можно использовать bwrap, как вы помните она запускает Chrome в контейнере. Давайте с помощью bwrap зададим локаль.

Пример на Bash:
Bash:
#!/bin/bash
CHROME_BIN="/opt/google/chrome/chrome"
CHROME_PROFILE_DIR="/tmp/chrome-ja-profile"

rm -rf "$CHROME_PROFILE_DIR"
mkdir -p "$CHROME_PROFILE_DIR"

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /sbin /sbin \
  --ro-bind /opt /opt \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --tmpfs /tmp \
  --dev /dev \
  --proc /proc \
  --bind "$CHROME_PROFILE_DIR" "$CHROME_PROFILE_DIR" \
  --setenv LANG ja_JP.UTF-8 \
  --setenv LANGUAGE ja_JP \
  --setenv LC_ALL ja_JP.UTF-8 \
  --setenv HOME "$CHROME_PROFILE_DIR" \
  --unshare-all \
  --share-net \
  --die-with-parent \
  "$CHROME_BIN" \
    --user-data-dir="$CHROME_PROFILE_DIR" \
    --no-first-run \
    --lang=ja-JP \
    --disable-background-networking \
    --disable-translate \
    --disable-features=TranslateUI,VizDisplayCompositor \
    --disable-gpu \
    --disable-software-rasterizer \
    --disable-dev-shm-usage \
    --no-sandbox \
    --accept-lang=ja-JP \
    --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:112.0; ja-JP) Gecko/20100101 Firefox/112.0" \
  & disown

Проверяем:
photo_2025-05-14_11-34-05.jpg

В данном скрипте Bubblewrap запускает Chrome в контейнере, где локаль установлена как en-US. Для этого он создаёт временную папку для профиля браузера, очищая её перед каждым запуском, чтобы избежать накопления данных. Скрипт монтирует только необходимые системные директории в режиме "только чтение", такие как /usr, /lib, /bin и /opt, чтобы Chrome мог использовать системные библиотеки и исполняемые файлы. Сеть остаётся доступной, позволяя браузеру подключаться к интернету, а файл /etc/resolv.conf обеспечивает корректную работу DNS. Временная файловая система /tmp создаётся для изоляции временных данных, а /dev и /proc дают доступ к устройствам и процессам, необходимым для работы браузера. Домашняя директория браузера перенаправляется в созданную временную папку профиля. Также задаётся юзер агент устанавливается язык ja-JP чтобы точно полностью подделать локаль.

Еще можно использовать Puppeteer, он управляет Chrome подменяя локаль через js и заголовки.

Пример кода для подмены локали:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--lang=ja-JP'],

  });

  const page = await browser.newPage();
  await page.setExtraHTTPHeaders({
    'Accept-Language': 'ja-JP,ja;q=0.9',
  });

  await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'language', {
      get: () => 'ja-JP',
    });

    Object.defineProperty(navigator, 'languages', {
      get: () => ['ja-JP', 'ja'],
    });
  });


  await page.evaluateOnNewDocument(() => {
    Intl.DateTimeFormat = (orig => {
      return function () {
        const obj = orig.apply(this, arguments);
        obj.resolvedOptions = () => ({ locale: 'ja-JP' });
        return obj;
      };
    })(Intl.DateTimeFormat);
  });

  await page.goto('https://browserleaks.com/javascript', { waitUntil: 'networkidle0' });

})();

Запускаем и проверяем работает ли:
photo_2025-05-14_11-36-34.jpg

Мой скрипт использует Puppeteer, чтобы запустить Chrome так, будто он работает на японском языке. Браузер открывается в видимом окне для того, чтобы его можно было дальше использовать, и настраивается с японской локалью. Скрипт задаёт HTTP-заголовок, чтобы сайты отправляли контент на японском, и меняет JavaScript-объекты в браузере, чтобы они сообщали сайтам, что язык — японский. Также он подстраивает формат даты и времени под японские стандарты. В конце скрипт заходит на тестовый сайт, который проверяет настройки браузера.

Подмена таймзоны
Что ж, мы разобрались с подменой локали, таймзона всё равно может нас выдать, так что её тоже нужно подменить. Таймзона видна через JavaScript, HTTP-заголовки или системные настройки. Наша цель — обмануть сайты, чтобы они видели нужную нам таймзону. Проверять будем, как и локаль с помощью browserleaks.com/javascript.

Самый лёгкий способ — запустить Chrome с флагом --timezone. Это прямо говорит браузеру, какую таймзону использовать, без лишних заморочек. Например, можно задать America/New_York.

Пример:
Bash:
#!/bin/bash
CHROME_PROFILE_DIR="/tmp/chrome-tz-profile"
rm -rf "$CHROME_PROFILE_DIR"
mkdir -p "$CHROME_PROFILE_DIR"

google-chrome \
  --user-data-dir="$CHROME_PROFILE_DIR" \
  --timezone="America/New_York" \
  --no-first-run \
  --disable-background-networking \
  --no-sandbox \
  --lang=en-US \
  --disable-gpu \f
  --disable-translate \
  --disable-features=TranslateUI \
  --accept-lang=en-US \
  --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:112.0; en-US) Gecko/20100101 Firefox/112.0" \
  & disown

Проверяем:
photo_2025-05-14_11-23-44.jpg

Скрипт запускает Chrome с таймзоной America/New_York и американской локалью. Профиль чистый, лишние функции отключены.

Ещё можно запустить Chrome в изолированном time namespace с помощью unshare. Это фича Linux, которая позволяет создавать отдельное пространство для времени, где приложение видит свою таймзону. Правда, это работает только на ядрах, которые поддерживают time namespace (обычно новые версии). Мы используем timedatectl, чтобы временно задать таймзону внутри этого пространства.

Пример:
Bash:
#!/bin/bash
export TZ=Asia/Tokyo
CHROME_PROFILE_DIR="/tmp/chrome-japan-profile"
rm -rf "$CHROME_PROFILE_DIR"
mkdir -p "$CHROME_PROFILE_DIR"
export DISPLAY=${DISPLAY:-:0}

google-chrome \
  --user-data-dir="$CHROME_PROFILE_DIR" \
  --no-first-run \
  --disable-background-networking \
  --no-default-browser-check \
  --disable-sync \
  --disable-translate

Проверяем:
photo_2025-05-14_11-42-58.jpg

Скрипт запускает Chrome в изолированном time namespace, где таймзона установлена на нужную нам. Это не трогает системную таймзону, и браузер видит только то, что мы ему задали. Профиль очищается, чтобы не было мусора, а флаги отключают лишние функции.

Если нужен программный контроль, можно использовать Puppeteer может эмулировать таймзону.

Вот пример код:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox'],
  });

  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      const page = await target.page();
      await page.emulateTimezone('America/New_York');
    }
  });

  const page = await browser.newPage();
  await page.emulateTimezone('America/New_York');
  await page.goto('https://browserleaks.com/javascript', { waitUntil: 'networkidle0' });

})();

Проверяем:
-2147483648_-211868.jpg

Этот код запускает Chrome в видимом режиме, задаёт таймзону America/New_York через метод emulateTimezone() и открывает browserleaks.com для проверки. Puppeteer делает всё автоматически, без возни с системными файлами.

Можно пойти дальше и подменить Web APIs, такие как Intl.DateTimeFormat и Date.getTimezoneOffset, через Puppeteer. Это обманывает сайты, которые лезут в JavaScript за таймзоной.

Пример:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {

  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox'],
  });

  browser.on('targetcreated', async (target) => {

    if (target.type() === 'page') {
      const page = await target.page();
      await page.evaluateOnNewDocument(() => {

        const originalDateTimeFormat = Intl.DateTimeFormat;
        Intl.DateTimeFormat = function (...args) {
          const formatter = new originalDateTimeFormat(...args);
          formatter.resolvedOptions = () => ({ timeZone: 'America/New_York' });
          return formatter;
        };
        Date.prototype.getTimezoneOffset = () => -240;
      });
    }
  });

  const page = await browser.newPage();
  await page.goto('https://browserleaks.com/javascript', { waitUntil: 'networkidle0' });

})();

Проверяем:
Без названия314_20250514182530.png

Скрипт перехватывает JavaScript-методы и подменяет таймзону на America/New_York. Сайты, которые проверяют время через API, видят фейковые данные.

Еще один из способов bwrap, чтобы подсунуть Chrome фальшивый файл /etc/localtime. Этот файл в Linux отвечает за системную таймзону, и, если его заменить, браузер будет думать, что находится в другом часовом поясе.

Пример:
Bash:
#!/bin/bash
CHROME_BIN="/opt/google/chrome/chrome"
CHROME_PROFILE_DIR="/tmp/chrome-tz-profile"
FAKE_TZ_DIR="/tmp/fake-timezone"
rm -rf "$CHROME_PROFILE_DIR"
mkdir -p "$CHROME_PROFILE_DIR"
mkdir -p "$FAKE_TZ_DIR"
cp /usr/share/zoneinfo/America/New_York "$FAKE_TZ_DIR/localtime"

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /sbin /sbin \
  --ro-bind /opt /opt \
  --ro-bind "$FAKE_TZ_DIR/localtime" /etc/localtime \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind /etc/fonts /etc/fonts \
  --ro-bind /etc/ssl /etc/ssl \
  --ro-bind /etc/mime.types /etc/mime.types \
  --ro-bind /etc/machine-id /etc/machine-id \
  --ro-bind /tmp/.X11-unix /tmp/.X11-unix \
  --bind "$CHROME_PROFILE_DIR" "$CHROME_PROFILE_DIR" \
  --tmpfs /tmp \
  --dev /dev \
  --proc /proc \
  --setenv DISPLAY "$DISPLAY" \
  --setenv LANG en_US.UTF-8 \
  --setenv HOME "$CHROME_PROFILE_DIR" \
  --setenv PATH "$CHROME_PROFILE_DIR/fakebin:/usr/bin:/bin" \
  --unshare-all \
  --share-net \
  --die-with-parent \
  "$CHROME_BIN" \
    --user-data-dir="$CHROME_PROFILE_DIR" \
    --no-first-run \
    --disable-background-networking \
    --disable-crash-reporter \
    --disable-breakpad \
    --no-sandbox \
  & disown

Проверяем:
photo_2025-05-14_11-49-33.jpg

Этот скрипт создаёт папку с файлом localtime для America/New_York, монтирует его в /etc/localtime внутри контейнера и запускает Chrome в изолированной среде. Браузер видит только эту таймзону, а системные настройки не меняются. Сеть остаётся открытой, чтобы сайты работали, а профиль браузера изолирован, чтобы ничего не сломалось.

Подмена системного времени
Мы с вами уже разобрались с локалью и таймзоной, но системное время, которое браузер охотно выдает всем желающим, может тебя спалить. Почему? Потому что оно показывает время заданное в твоей системе. И если это время не совпадает с подменённой таймзоной или, что ещё хуже, выдаёт реальное твое местоположение, это проблема.

Лучше подменять системное время в браузере через JavaScript, не трогая саму систему, потому что смена времени в системе может повредить ее работе. Потому для примеров я буду использовать уже привычный Puppeteer.

Первый метод — инъекция JavaScript через Puppeteer для перехвата Date.now() и new Date().

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });

  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      const page = await target.page();
      await page.evaluateOnNewDocument(() => {
        const fakeOffset = -5 * 60 * 60 * 1000;
        const originalDateNow = Date.now;
        const originalDate = Date;

        Date = class extends originalDate {
          constructor(...args) {
            if (args.length === 0) {
              return new originalDate(originalDateNow() + fakeOffset);
            }
            return new originalDate(...args);
          }
        };

        Date.now = () => originalDateNow() + fakeOffset;
        Date.prototype = originalDate.prototype;
      });
    }
  });

  const page = await browser.newPage();
  await page.goto('https://browserleaks.com/javascript');

})();

Скрипт запускает Chrome в видимом режиме и переопределяет объект Date, чтобы он возвращал время со смещением, например, на 5 часов назад. Оригинальные методы сохраняются, чтобы не сломать функционал. Новый класс Date выдаёт фейковое время, и вы можете дальше работать с браузером вручную.

Еще метод переопределение performance.now(). Этот метод отвечает за высокоточное время, которое сайты могут проверять для тайминга или защиты от ботов.

Пример:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const offset = 2 * 60 * 60 * 1000;

  const browser = await puppeteer.launch({ headless: false });
  browser.on('targetcreated', async (target) => {

    if (target.type() === 'page') {
      const page = await target.page();
      await page.evaluateOnNewDocument((offset) => {
        const originalDateNow = Date.now;
        const originalPerformanceNow = performance.now.bind(performance);
        const originalTimeOrigin = performance.timeOrigin;
   
     class FakeDate extends Date {
          constructor(...args) {
            if (args.length === 0) {
              super(originalDateNow() + offset);
            } else {
              super(...args);
            }
          }
          static now() {
            return originalDateNow() + offset;
          }
        }

        FakeDate.UTC = Date.UTC;
        FakeDate.parse = Date.parse;
        FakeDate.prototype = Date.prototype;
        window.Date = FakeDate;
        performance.now = () => originalPerformanceNow() + offset;
        Object.defineProperty(performance, 'timeOrigin', {
          get: () => originalTimeOrigin + offset
        });
      }, offset);
    }
  });

  const page = await browser.newPage();
  await page.goto('https://browserleaks.com/javascript');

})();

Этот метод работает так что, скрипт запускает Chrome в видимом окне и перехватывает performance.now(), добавляя смещение, например, 2 часа.

Еще можно исользовать эмуляцию времени через Chrome DevTools Protocol. Это глубокий метод, который задаёт виртуальное время для всей сессии браузера.

Пример:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  browser.on('targetcreated', async (target) => {

    if (target.type() === 'page') {
      const page = await target.page();
      const client = await page.target().createCDPSession();

      await client.send('Emulation.setVirtualTimePolicy', {
        policy: 'pauseIfNetworkFetchesPending',
        virtualTimeBase: Date.now() + 3600000 // Смещение на 1 час

      });
    }
  });

  const page = await browser.newPage();
  const client = await page.target().createCDPSession();
  await client.send('Emulation.setVirtualTimePolicy', {
    policy: 'pauseIfNetworkFetchesPending',
    virtualTimeBase: Date.now() + 3600000 // Смещение на 1 час
  });
  await page.goto('https://browserleaks.com/javascript');
})();

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

Подмена кординат
При подмене вашего местоположения, даже если вы используете фейковую таймзону, прокси или VPN, браузер Chrome, если разрешить доступ к геолокации, может определить ваши реальные координаты, поэтому крайне важно грамотно подменять их. Chrome обычно получает координаты через метод getCurrentPosition, который делает запрос к операционной системе или использует данные от Wi-Fi сетей, GPS или IP-адреса, если они доступны. Однако сайты в Chrome определяют местоположение не только через getCurrentPosition. Они могут учитывать косвенные признаки, такие как cookies, кэш. Поэтому, чтобы подмена координат была эффективной, необходимо очищать cookies, которые могут хранить информацию о вашем настоящем местоположении.

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

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,

    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',

    ],
    defaultViewport: null
  });

  async function patchPage(page) {
    const context = browser.defaultBrowserContext();
    await context.overridePermissions('https://my-location.org/', ['geolocation']);
    await page.setGeolocation({
      latitude: 54.8179,
      longitude: 30.4619,
      accuracy: 100
    });
  }

  const page = await browser.newPage();
  await patchPage(page);
  await page.goto('https://my-location.org/');

  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      try {
        const newPage = await target.page();
        await patchPage(newPage);
      } catch (err) {
        console.error('Error:', err);
      }
    }
  });

})();

Проверяем:
photo_2025-05-11_07-02-55.jpg

Мой код использует два метода Puppeteer для подмены геолокации. Сначала через browser.defaultBrowserContext().overridePermissions она разрешает доступ к геолокации для указанного сайта, обходя запросы браузера на разрешение. Затем метод page.setGeolocation задаёт фейковые координаты.

Еще мы можем написать свой плагин для подмены геолокации, используя Chrome DevTools Protocol.

Пример кода:
background.js:

JavaScript:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
    if (changeInfo.status === 'complete') {
        chrome.debugger.attach({ tabId: tabId }, "1.3", function() {
            chrome.debugger.sendCommand({ tabId: tabId }, "Page.enable", {});
            chrome.debugger.sendCommand({ tabId: tabId }, "Emulation.setGeolocationOverride", {
                latitude: 50.7558,
                longitude: 34.6173,
                accuracy: 100
            });
        });
    }

});

manifest.json:
JavaScript:
{
  "manifest_version": 3,
  "name": "Geo Faker",
  "version": "1.0",
  "permissions": ["debugger"],
  "background": {
    "service_worker": "background.js"
  }

}
Проверяем работает ли он:

Для этого создаем папку для плагина и добавляем данные в файлы.
Далее переходим в chrome://extensions/ и добавляем наш плагин:
photo_2025-05-14_12-17-36.jpg

Теперь запускаем и тестим:
photo_2025-05-14_12-16-32.jpg

Скрипт manifest.json определяет структуру расширения, запрашивая разрешение debugger и регистрируя background.js как фоновый сервис. Самое интересное происходит в background.js который работает через расширение Chrome, используя Chrome DevTools Protocol для подмены геолокации. В background.js слушатель срабатывает, когда вкладка полностью загружается. Затем метод к вкладке. После активации Page.enable применяется команда, которая задаёт фейковые координаты для всех запросов геолокации в этой вкладке.

Вернемся к Puppeteer, с помощью него можно подменять геолокацию через JavaScript-инъекцию, потому что это позволяет напрямую переписать поведение API геолокации.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
    defaultViewport: null
  });

  await browser.on('targetcreated', async (target) => {
    const page = await target.page();

    if (page) {
      await page.evaluateOnNewDocument(() => {
        navigator.geolocation.getCurrentPosition = (success) => {

          success({
            coords: {
              latitude: 54.8179,
              longitude: 30.4619,
              accuracy: 100
            }
          });
        };
      });
    }
  });

  const page = await browser.newPage();
  await page.goto('https://my-location.org/');

})();
Код использует метод page.evaluateOnNewDocument для внедрения js-кода при создании каждой новой страницы. Внедрённый код переопределяет метод navigator.geolocation.getCurrentPosition, заменяя его функцией, которая немедленно вызывает переданный success-коллбэк с фейковыми координатами. Это заставляет любой вызов API геолокации возвращать поддельные данные.

Можно еще подменять статус геолокации через Chrome DevTools Protocol в Puppeteer это даёт прямой доступ к настройкам браузера, позволяя не только задать фейковые координаты, но и управлять разрешениями геолокации, обходя запросы браузера и делая подмену максимально незаметной.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
    defaultViewport: null
  });

  browser.on('targetcreated', async (target) => {
    const page = await target.page();

    if (page) {
      const client = await page.target().createCDPSession();

      await client.send('Browser.setPermission', {
        permission: { name: 'geolocation' },
        setting: 'granted' // или 'denied' для отклонения
      });

      await client.send('Emulation.setGeolocationOverride', {
        latitude: 54.8179,
        longitude: 30.4619,
        accuracy: 100
      });
    }
  });

  const page = await browser.newPage();
  await page.goto('https://my-location.org/');

})();

При создании каждой новой вкладки он открывает CDP-сессию. Затем команда Browser.setPermission задаёт разрешение на геолокацию, автоматически позволяя сайту доступ без запросов у пользователя. После этого команда Emulation.setGeolocationOverride устанавливает фейковые координаты, которые возвращаются при запросах API геолокации.

Подмена стевого отпечатка
Теперь давайте поговорим о подмене сетевого отпечатка. Сайты могут вычислить ваш IP-адрес и другие данные через JavaScript, даже если вы используете VPN. Например, они проверяют WebRTC, HTTP-заголовки или свойства браузера вроде navigator.connection. Я расскажу о нескольких методах, чтобы скрыть сетевой отпечаток, поехали!

Можно перхватывать и модифицировать ICE-кандидаты WebRTC, заменять ими реальные IP-адреса на фейковые, чтобы скрыть локальный IP с помощью Puppeteer.

Вот пример такой замены:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const fakeIp = '192.168.123.123';
  const browser = await puppeteer.launch({
    headless: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });

  const patchWebRTC = (page, fakeIp) => {
    page.evaluateOnNewDocument(fakeIp => {
      const patchIP = (str) => str.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, fakeIp);
      const NativeRTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection;
      class FakeRTCPeerConnection extends NativeRTCPeerConnection {

        constructor(...args) {
          super(...args);
          this._fakeCandidateEmitted = false;
          this.addEventListener('icecandidate', (event) => {

            if (!this._fakeCandidateEmitted) {
              this._fakeCandidateEmitted = true;
              const fakeCandidate = {
                candidate: `candidate:842163049 1 udp 1677729535 ${fakeIp} 12345 typ host`,
                sdpMid: '0',
                sdpMLineIndex: 0

              };
              const newCandidate = new RTCIceCandidate(fakeCandidate);

              setTimeout(() => {
                const iceEvent = new Event('icecandidate');
                Object.defineProperty(iceEvent, 'candidate', {
                  value: newCandidate,
                  writable: false
                });
                this.dispatchEvent(iceEvent);
              }, 50);
            }

            if (event && event.candidate) {
              Object.defineProperty(event, 'candidate', {
                value: new RTCIceCandidate({
                  ...event.candidate,
                  candidate: patchIP(event.candidate.candidate)
                }),

                writable: false
              });
            }
          });

          const origSetLocalDescription = this.setLocalDescription;
          this.setLocalDescription = function(description) {

            if (description && description.sdp) {
              description.sdp = patchIP(description.sdp);
            }
            return origSetLocalDescription.call(this, description);
          };
        }
      }
      window.RTCPeerConnection = FakeRTCPeerConnection;
      window.webkitRTCPeerConnection = FakeRTCPeerConnection;
    }, fakeIp);
  };

  const page = await browser.newPage();
  await patchWebRTC(page, fakeIp);

  browser.on('targetcreated', async (target) => {
    const newPage = await target.page();

    if (newPage) {
      await patchWebRTC(newPage, fakeIp);
    }
  });

  await page.goto('https://browserleaks.com/webrtc');

})();
Проверяем работает ли:
Screenshot_2025-04-21_23-40-04 (copy 1).png

Скрипт подменяет IP-адреса в WebRTC. А именно он заменяет реальный локальный IP на фейковый (например, 192.168.123.123), перехватывая RTCPeerConnection и события icecandidate, а также изменяя SDP. Это помогает скрыть настоящий IP, что важно при использовании VPN. Работает в браузере и новых вкладках. Но сайты могут заметить подмену, если IP выглядит подозрительно.

Можно отключить WebRTC, с помощью палина в хром WebRTC-Leak-Prevent, это поможет в том числе не палить ip.

Пример:
Для начала нужно локально скопировать плагин:
Код:
git clone https://github.com/aghorler/WebRTC-Leak-Prevent/

Сам код:
JavaScript:
PROFILE_DIR="$HOME/.config/google-chrome/webrtc_block"
EXTENSION_DIR="$HOME/WebRTC-Leak-Prevent-master"

rm -rf "$PROFILE_DIR"
mkdir -p "$PROFILE_DIR"

google-chrome \
  --user-data-dir="$PROFILE_DIR" \
  --load-extension="$EXTENSION_DIR" \
  --no-first-run \
  --no-default-browser-check

Устанвливаем нужные настройки в палгине:
photo_2025-05-14_12-32-24.jpg

И проверяем сработало ли:
photo_2025-05-14_12-33-06.jpg

В этом скрипте используется расширение для блокировки WebRTC. Сначала создаётся новый профиль браузера, затем запускается браузер с использованием этого профиля и папки, в которой находится расширение WebRTC Leak Prevent. Это расширение отключает WebRTC, что помогает предотвратить утечку IP-адреса.

Подмена dns
Еще сайты могут заметить наш dbs.

Но с помощью bwrap мы можем запустить наш браузер в изолированной среде с фейковым DNS:
Bash:
CHROME_BIN="/opt/google/chrome/chrome"
CHROME_PROFILE_DIR="/tmp/chrome-profile"
FAKE_DNS="8.8.8.8"
rm -rf "$CHROME_PROFILE_DIR"
mkdir -p "$CHROME_PROFILE_DIR"

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /sbin /sbin \
  --ro-bind /opt /opt \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --tmpfs /tmp \
  --dev /dev \
  --proc /proc \
  --bind "$CHROME_PROFILE_DIR" "$CHROME_PROFILE_DIR" \
  --setenv HOME "$CHROME_PROFILE_DIR" \
  --setenv RESOLV_CONF "/etc/resolv.conf" \
  --unshare-all \
  --share-net \
  --setenv DNS_SERVER="$FAKE_DNS" \
  --mount /etc/resolv.conf=/dev/null \
  --bind /etc/resolv.conf <<< "nameserver $FAKE_DNS" \
  --die-with-parent \
  --unshare-net \

  "$CHROME_BIN" \
    --user-data-dir="$CHROME_PROFILE_DIR" \
    --no-first-run \
    --disable-background-networking \
    --disable-translate \
    --disable-features=TranslateUI,VizDisplayCompositor \
    --disable-gpu \
    --disable-software-rasterizer \
    --disable-dev-shm-usage \
    --no-sandbox \
  & disown
Как это работает? Скрипт создаёт чистый профиль Chrome и запускает браузер в контейнере. Он подменяет файл /etc/resolv.conf, заставляя Chrome использовать фейковый DNS, а не системный. Bubblewrap изолирует сеть с помощью --unshare-net, но оставляет базовый доступ через --share-net.

Подмена X-Forwarded-For
Еще браузеры могут узнать реальный ip из X-Forwarded-For.
Его можно подменять тоже через puppeteer.

Вот пример кода:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {

  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null,
    args: ['--start-maximized']

  });

  const page = await browser.newPage();
  await page.setExtraHTTPHeaders({
    'X-Forwarded-For': '192.168.1.100'
  });

  await browser.on('targetcreated', async (target) => {
    const newPage = await target.page();

    if (newPage) {

      await newPage.setRequestInterception(true);
      newPage.on('request', (interceptedRequest) => {
        const headers = interceptedRequest.headers();

        delete headers['Host']
 
        interceptedRequest.continue({ headers: { ...headers, 'X-Forwarded-For': '192.168.1.100' } });
      });
    }
  });

  await page.goto('https://browserleaks.com/ip');

})();

Проверяем:
photo_2025-05-11_07-52-14 (3).jpg

Код работает так: когда создаётся новая страница, Puppeteer задаёт заголовок X-Forwarded-For с фейковым. Это делается через page.setExtraHTTPHeaders, чтобы все запросы с этой страницы шли с подменённым заголовком. Но это ещё не всё. Код также перехватывает каждый сетевой запрос через setRequestInterception(true). Когда браузер отправляет запрос, Puppeteer берёт текущие заголовки, убирает заголовок Host (чтобы избежать конфликтов), и добавляет наш фейковый X-Forwarded-For.

Подмена паметров сети
Для составления сетевого отпечатка сайты часто проверяют сетевые параметры через navigator.connection
, чтобы понять, сидите ли вы с Wi-Fi, 4G или кабеля, какая у вас скорость интернета и включена ли экономия трафика. Для его уникальности можно подделать другие сетвые параметры.

Вот как это сделать с помощью js:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null,

  });

  const injectFakeConnection = async (page) => {
    await page.evaluateOnNewDocument(() => {
      const fakeConnection = {
        type: 'wifi',
        effectiveType: '4g',
        rtt: 50,
        downlink: 10,
        saveData: false
      };

      Object.defineProperty(navigator, 'connection', {
        configurable: true,
        get: () => fakeConnection
      });
    });
  };
  const pages = await browser.pages();

  for (const page of pages) {
    await injectFakeConnection(page);
  }

  browser.on('targetcreated', async (target) => {
    const page = await target.page();
    if (page) {
      await injectFakeConnection(page);
    }
  });
  const page = await browser.newPage();
  await page.goto('https://www.google.com/');

})();

Проверяем:
photo_2025-05-11_07-52-14 (2).jpg

Скрипт берёт все открытые страницы и для каждой выполняет JavaScript, который подменяет объект navigator.connection. Если этот объект есть, он оборачивает его в прокси, который возвращает фейковые значения: тип соединения — Wi-Fi, эффективный тип — 4g, задержка — 50 мс, скорость загрузки — 10 Мбит/с, а экономия трафика — выключена.


Подмена размера экрана
Мы наконец закончили всё, что касается нашей геолокации, давайте теперь поговорим о достаточно важной вещи, а именно подмене размера экрана. Наша цель — заставить Chrome выдавать нужное разрешение, чтобы оно выглядело естественно и не выделялось. В прошлой части мы подменяли размер экрана на уровне системы, но не на уровне браузера.

Сам размер окна можно менять флагом --window-size:
Bash:
#!/bin/bash
WINDOW_WIDTH=1180
WINDOW_HEIGHT=620
CHROME_PROFILE_FAKE_SCREEN="/tmp/chrome_profile_fake_screen"

[ -d "$CHROME_PROFILE_FAKE_SCREEN" ] && rm -rf "$CHROME_PROFILE_FAKE_SCREEN"
mkdir -p "$CHROME_PROFILE_FAKE_SCREEN"
google-chrome --user-data-dir="$CHROME_PROFILE_FAKE_SCREEN" --window-size=${WINDOW_WIDTH},${WINDOW_HEIGHT} --no-sandbox --disable-infobars --disable-cache --disk-cache-dir=/dev/null --media-cache-dir=/dev/null
rm -rf "$CHROME_PROFILE_FAKE_SCREEN"
Это самый простой способ задать размер окна браузера, который сайты видят через JavaScript. Скрипт использует флаг --window-size, чтобы Chrome открывался, например, с разрешением 1180x620.

Еще через параметры мы можем не только менять размеры окна, но и расширение, вот пример:
Bash:
#!/bin/bash
SCALE_FACTOR=1.5
WINDOW_WIDTH=1366
WINDOW_HEIGHT=768

CHROME_PROFILE_FAKE_SCREEN="/tmp/chrome_profile_fake_screen"
[ -d "$CHROME_PROFILE_FAKE_SCREEN" ] && rm -rf "$CHROME_PROFILE_FAKE_SCREEN"
mkdir -p "$CHROME_PROFILE_FAKE_SCREEN"
google-chrome --user-data-dir="$CHROME_PROFILE_FAKE_SCREEN" --force-device-scale-factor=$SCALE_FACTOR --window-size=${WINDOW_WIDTH},${WINDOW_HEIGHT} --no-sandbox --disable-gpu --disable-cache --disk-cache-dir=/dev/null --media-cache-dir=/dev/null
rm -rf "$CHROME_PROFILE_FAKE_SCREEN"

Проверяем работает ли:
photo_2025-05-14_12-57-19.jpg

Этот метод добавляет к размеру окна подмену плотности пикселей через --force-device-scale-factor. Например, разрешение 1366x768 с масштабом 1.5 имитирует экран бюджетного ноутбука. Переменная SCALE_FACTOR задаёт DPI, который браузер возвращает через window.devicePixelRatio. Флаг --disable-gpu скрывает данные о видеокарте, а --headless=new держит всё в фоне. Метод хорош для точной настройки, но нужно следить, чтобы размер и масштаб выглядели логично вместе, иначе сайты могут заподозрить подвох.

Мы можем подменять размер экрана для свойств объекта window.screen, а также размеры окна и устройства в целом с помощью уже знакомого нам puppeteer с плагином stealt.

Пример:
JavaScript:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());

(async () => {
  try {
    const browser = await puppeteer.launch({
      headless: false,
      args: ['--start-maximized'],
      defaultViewport: null
    });

    const [page] = await browser.pages();
    const mockScreenScript = `

      Object.defineProperty(window, 'screen', {
        value: {
          width: 1920,
          height: 1080,
          availWidth: 1920,
          availHeight: 1050,
          colorDepth: 24,
          pixelDepth: 24,
          availTop: 30,
          availLeft: 0,
          orientation: {
            type: 'landscape-primary',
            angle: 0
          }
        },
        configurable: true

      });
      Object.defineProperty(screen, 'orientation', {
        value: {
          type: 'landscape-primary',
          angle: 0,
          onchange: null
        },
        configurable: true
      });

      Object.defineProperty(window, 'outerWidth', {
        get: () => 1920
      });

      Object.defineProperty(window, 'outerHeight', {
        get: () => 1050
      });

      Object.defineProperty(window, 'innerWidth', {
        get: () => 1910
      });

      Object.defineProperty(window, 'innerHeight', {
        get: () => 970
      });

      Object.defineProperty(window, 'devicePixelRatio', {
        get: () => 1
      });

      Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
        get: function () {
          return 1895;
        }
      });

      Object.defineProperty(HTMLElement.prototype, 'clientHeight', {
        get: function () {
          return 970;
        }
      });
    `;

    browser.on('targetcreated', async (target) => {

      if (target.type() === 'page') {
        const newPage = await target.page();
        if (newPage) {
          await newPage.evaluateOnNewDocument(mockScreenScript);
        }
      }
    });
    await page.evaluateOnNewDocument(mockScreenScript);
    await page.goto('https://browserleaks.com/javascript', {
    waitUntil: 'networkidle2'

    });

    const screenData = await page.evaluate(() => ({
      screen: {
        width: screen.width,
        height: screen.height,
        availWidth: screen.availWidth,
        availHeight: screen.availHeight,
        colorDepth: screen.colorDepth,
        pixelDepth: screen.pixelDepth,

        orientation: {
          type: screen.orientation?.type,
          angle: screen.orientation?.angle
        }
      },

      windowProps: {
        innerWidth: window.innerWidth,
        innerHeight: window.innerHeight,
        outerWidth: window.outerWidth,
        outerHeight: window.outerHeight,
        devicePixelRatio: window.devicePixelRatio
      },

      clientRect: (() => {
        const div = document.createElement('div');
        document.body.appendChild(div);

        const data = {
          clientWidth: div.clientWidth,
          clientHeight: div.clientHeight
        };

        div.remove();
        return data;
      })()
    }));
    console.log(JSON.stringify(screenData, null, 2));
  } catch (error) {
    console.error('Error:', error);
  }

})();

Проверяем работает ли:
photo_2025-05-14_12-58-25.jpg

Этот код перед загрузкой сайта внедряет в каждую страницу специальный js-код, который подменяет свойства экрана. Это делается через Object.defineProperty, чтобы обмануть сайты, которые пытаются определить настоящий размер экрана и параметры устройства. Также код подменяет размеры окна и даже размеры элементов на странице, такие как clientWidth у div. Это важно, потому что многие сайты проверяют не только свойства screen, но и реальные размеры элементов.

Также можно использовать xrandr и cvt для изменения разрешения экрана, это уже было в прошлой статье, но можно это доработать чисто под хром.

Пример:
Bash:
#!/bin/bash
WIDTH=1240
HEIGHT=600
REFRESH=60
MODE_NAME="${WIDTH}x${HEIGHT}_${REFRESH}.00"
DISPLAY_NAME=$(xrandr | grep " connected" | awk '{print $1}' | head -n 1)

if ! xrandr | grep -q "$MODE_NAME"; then
    echo "Режим $MODE_NAME не существует. Создаём..."
    MODELINE=$(cvt $WIDTH $HEIGHT $REFRESH | grep Modeline | sed 's/Modeline //' | sed 's/"//g')

    if [ -z "$MODELINE" ]; then
        echo "Ошибка генерации modeline!"
        exit 1
    fi
    xrandr --newmode $MODELINE
    xrandr --addmode "$DISPLAY_NAME" "$MODE_NAME"
fi

echo "Устанавливаем разрешение $MODE_NAME на $DISPLAY_NAME"

xrandr --output "$DISPLAY_NAME" --mode "$MODE_NAME"

CHROME_PROFILE_FAKE_SCREEN="/tmp/chrome_profile_fake_screen"
mkdir -p "$CHROME_PROFILE_FAKE_SCREEN"
CHROME_PATH=$(which google-chrome || which chromium-browser)

"$CHROME_PATH" --user-data-dir="$CHROME_PROFILE_FAKE_SCREEN" \
    --window-size=${WIDTH},${HEIGHT} \
    --no-sandbox \
    --disable-gpu \
    --disable-cache \
    --disk-cache-dir=/dev/null \
    --media-cache-dir=/dev/null &
wait

xrandr --output "$DISPLAY_NAME" --auto

Проверяем работает ли:
photo_2025-05-14_12-57-54.jpg

Этот скрипт создаёт и применяет кастомное разрешение экрана, запускает браузер в этом разрешении и возвращает исходные настройки. Он задаёт ширину, высоту и частоту, формируя имя режима, например "1240x600_60.00". Находит подключённый дисплей через xrandr. Если режим отсутствует, генерирует его с помощью cvt, добавляет в xrandr и устанавливает. После завершения браузера восстанавливает стандартное разрешение.

Подмена плагинов
Мы с вами дошли до одной из самых интересных тем, а именно сокрытие плагинов! Сайты, да и разного рода умельцы, обожают собирать отпечатки браузера, выуживая данные об установленных плагинах, чтобы вычислить, кто вы и составить отпечаток. Коротко о том, как собирают: сайты используют JavaScript, чтобы заглянуть в объект navigator.plugins. Он выдаёт список всех установленных расширений — их имена, версии и даже описания. Ещё могут проверять, добавляют ли плагины свои скрипты или стили в DOM, или отслеживать специфические API, которые активируют расширения. Теперь к делу — как это обойти!

Первый метод — подменить ID плагинов, которые вы импортируете через manifest.json, чтобы сайты не могли определить, какие именно расширения у вас установлены.

Пример кода:
Bash:
#!/bin/bash
EXT_DIR="$HOME/.config/google-chrome/Default/Extensions"
CLONE_DIR="$HOME/cloned_extensions"

rm -rf "$CLONE_DIR"
mkdir -p "$CLONE_DIR"

find "$EXT_DIR" -name "manifest.json" | while read -r manifest; do
    plugin_dir=$(dirname "$manifest")
    plugin_id=$(basename "$(dirname "$plugin_dir")")
    plugin_ver=$(basename "$plugin_dir")
    new_id=$(cat /dev/urandom | tr -dc 'a-p' | fold -w 32 | head -n 1)
    new_plugin_path="$CLONE_DIR/$new_id"
    echo "[*] Клонируем $plugin_id ($plugin_ver) → $new_id"
    mkdir -p "$new_plugin_path"
    cp -r "$plugin_dir"/* "$new_plugin_path"
    manifest_path="$new_plugin_path/manifest.json"

    jq 'del(.key)
        | .name = "PDF Viewer"
        | .description = "View PDF files"' "$manifest_path" > "$manifest_path.tmp" && \
        mv "$manifest_path.tmp" "$manifest_path"
done

echo "Запустим Chrome с новыми плагинами"
google-chrome \
  --disable-extensions-except=$(find "$CLONE_DIR" -mindepth 1 -maxdepth 1 | paste -sd ',') \
  --load-extension=$(find "$CLONE_DIR" -mindepth 1 -maxdepth 1 | paste -sd ',')

Проверяем работает ли этот метод:
photo_2025-05-14_13-17-29.jpg

Скрипт собирает все плагины из профиля Chrome, копирует их в отдельную папку, подставляет рандомный ID и вырезает поле key из манифеста. Имя и описание плагина тоже меняются — на что-то безобидное и общее, чтобы ни один сайт не смог определить, что это за расширение. Всё выглядит как, к примеру PDF Viewer, но на самом деле это маскированный плагин.

Если вам не важен профиль, можно просто использовать чистый запуск без куков, истории и главное плагинов — запускай Chrome с временным профилем. Всё, что ты делаешь, исчезает после закрытия окна. Это хоть и банально, но действенно.

Пример:
Bash:
#!/bin/bash
TEMP_PROFILE="/tmp/chrome_temp_profile_$(date +%s)"
mkdir -p "$TEMP_PROFILE"
google-chrome --user-data-dir="$TEMP_PROFILE" --incognito &
trap 'rm -rf "$TEMP_PROFILE"' EXIT

Здесь создаётся изолированный профиль, включается инкогнито. После выхода — профиль удаляется.

Если надо полностью избавиться от плагинов, но не трогать основную сессию, то можно запустить свой профиль, но без загрузки плагинов.

Пример:
Код:
google-chrome  --disable-extensions &
Благодаря параметру в хром расширения не подгружаются вообще, даже системные.

Еще можно удалять визуальные и API-следы расширений из DOM и JS-контекста. Это полезно при использовании Puppeteer, чтобы сайты не палили runtime и плагины.

Для примера, я сделаю несколько файлов, для подмены отпечатка и иньекции в запущенный браузер:
inject.js

JavaScript:
const puppeteer = require('puppeteer');
const fs = require('fs');

(async () => {
    const contentScript = fs.readFileSync('./content.js', 'utf8');
    const browser = await puppeteer.launch({
        headless: false,
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox'
        ]
    });

    const applyStealth = async (page) => {
        await page.evaluateOnNewDocument(contentScript);
    };

    const page = await browser.newPage();
    await applyStealth(page);

    browser.on('targetcreated', async (target) => {
        if (target.type() === 'page') {
            const newPage = await target.page();
            await applyStealth(newPage);
        }
    });

})();

content.js
Этот скрипт будет запускаться внутри браузера и маскировать следы плагинов:

JavaScript:
const removeExtensionTraces = () => {
    const selectors = [
        '#some-adblock-div',
        '[data-extension="proxy"]',
        'div[id*="extension"]',
        'script[src*="extension"]'
    ];

    for (const sel of selectors) {
        document.querySelectorAll(sel).forEach(el => el.remove());
    }
};

removeExtensionTraces();
new MutationObserver(removeExtensionTraces).observe(document, { childList: true, subtree: true });
Object.defineProperty(window.chrome, 'runtime', {

    value: {
        sendMessage: () => { throw new Error('Extension not found'); },
        connect: () => { throw new Error('Extension not found'); }

    },
    writable: false
});

Object.defineProperty(navigator, 'plugins', {
    get: () => [],
    configurable: false
});

package.json
Код:
{
  "name": "my-puppeteer-project",
  "version": "1.0.0",
  "type": "module",
  "main": "inject.js",
  "scripts": {
    "start": "node inject.js"
  },
  "dependencies": {
    "puppeteer": "^21.3.8"
  }
}

Это можно запустить через:
Код:
npm install; npm start
Мой код работает так что патчит window.chrome.runtime, вырезает плагины и чистит DOM от подозрительных следов. Работает даже при динамической подгрузке — через MutationObserver.

Есть ещё один интерсный метод, можно загрузить плагин в puppeteer, а дальше внедрить content.js, и патчить fetch/XHR, чтобы заблокировать любые обращения к chrome-extension://.

Пример:
JavaScript:
const puppeteer = require('puppeteer');
const fs = require('fs');

(async () => {
    const extensionPath = '/home/.config/google-chrome/Default/Extensions/id/version/';
    const contentScript = fs.readFileSync('content.js', 'utf8');
    const browser = await puppeteer.launch({
        headless: false,
        args: [
            `--disable-extensions-except=${extensionPath}`,
            `--load-extension=${extensionPath}`,
            '--no-sandbox',
            '--disable-setuid-sandbox'
        ],
        defaultViewport: null
    });
    const pages = await browser.pages();

    for (const page of pages) {
        await page.evaluateOnNewDocument(contentScript);
        await page.evaluateOnNewDocument(() => {
            const originalFetch = window.fetch;
            window.fetch = async (...args) => {
                if (args[0].startsWith('chrome-extension://')) {
                    throw new Error('Blocked chrome-extension request');
                }
                return originalFetch.apply(window, args);

            };
            const originalXhrOpen = XMLHttpRequest.prototype.open;

            XMLHttpRequest.prototype.open = function (...args) {
                if (args[1].startsWith('chrome-extension://')) {
                    throw new Error('Blocked chrome-extension request');
                }
                return originalXhrOpen.apply(this, args);
            };
        });
    }

    browser.on('targetcreated', async (target) => {
        if (target.type() === 'page') {
            const newPage = await target.page();
            await newPage.evaluateOnNewDocument(contentScript);
            await newPage.evaluateOnNewDocument(() => {
                const originalFetch = window.fetch;
                window.fetch = async (...args) => {
                    if (args[0].startsWith('chrome-extension://')) {
                        throw new Error('Blocked chrome-extension request');
                    }
                    return originalFetch.apply(window, args);
                };
                const originalXhrOpen = XMLHttpRequest.prototype.open;

                XMLHttpRequest.prototype.open = function (...args) {
                    if (args[1].startsWith('chrome-extension://')) {
                        throw new Error('Blocked chrome-extension request');
                    }
                    return originalXhrOpen.apply(this, args);
                };
            });
        }
    });

})();
Мой скрипт внедряет скрипт маскировки content.js во все страницы, и патчит браузерные API (fetch, XMLHttpRequest), чтобы заблокировать любые обращения к chrome-extension://


Подмена javascript свойств указывающих на браузер
Еще одна тема, это подмена косвенных параметров в хром, которые могут выдать ваши реальные данные. Сайты проверяют, в каком вы браузере, не только по user-agent, но и по другим параметрам, связанным с JavaScript, которые выдают конкретный браузер. Например, они могут определить, что у вас Chrome, через свойства вроде window.chrome, или понять, что это Firefox, через window.InstallTrigger. Также некоторые сайты смотрят на объект window.external или другие специфические особенности. Это полезно подделывать в связке с юзер агентом, но об этом я расскажу позже, когда мы будем писать полноценный скрипт для подделки отпечтка. А пока я расскажу, как можно подменять эти параметры.

Для начала покажу, как это работает с Tampermonkey, а именно напишу, скрипт чтобы для наглядности замаскировать хром под firefox.
Первым делом нужно создать скрипт в Tampermonkey, для этого нажимаем на плагин и выбираем: «Создать новый скрипт»:
photo_2025-05-14_13-39-34.jpg


Теперь пишем скрипт и сохраняем его(Нажав на File->Save).

Пример скрипта:
JavaScript:
// ==UserScript==
// @name         Spoof Firefox Objects in Chrome
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Подделка window.chrome, window.InstallTrigger и window.external для имитации Firefox
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    if (window.chrome) {
        Object.defineProperty(window, 'chrome', {
            value: undefined,
            writable: false,
            configurable: false
        });
    }

    Object.defineProperty(window, 'InstallTrigger', {
        value: {
            enabled: function() { return true; },
            updateEnabled: function() { return true; },
            install: function() { console.log('Fake InstallTrigger called'); }

        },
        writable: false,
        configurable: false
    });

    Object.defineProperty(window, 'external', {
        value: {},
        writable: false,
        configurable: false
    });

})();

Запускаем скрипт:
Screenshot_2025-05-14_13-44-15.png

Этот скрипт для Tampermonkey маскирует браузер Chrome под Firefox, подменяя объекты JavaScript в глобальном объекте window. Он проверяет наличие window.chrome, характерного для Chromium-браузеров, и, если объект существует, переопределяет его как undefined. Далее создаётся объект window.InstallTrigger, специфичный для Firefox, с методами enabled и updateEnabled, возвращающими true, и install, выводящим сообщение в консоль, что имитирует функциональность дополнений Firefox. Затем window.external переопределяется как пустой объект {}, соответствующий типичному состоянию в Firefox, с аналогичными ограничениями на изменение.

Так же подделать это можно и через puppeteer, тоже покажу, как подделать отпечаток под firfox.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false,
        args: ['--no-sandbox', '--disable-setuid-sandbox'],
        executablePath: '/usr/bin/google-chrome'
    });

    async function patchPage(page) {
        await page.evaluateOnNewDocument(() => {
            Object.defineProperty(window, 'chrome', { value: undefined, writable: false, configurable: false });
            Object.defineProperty(window, 'InstallTrigger', {
                value: { enabled: () => true, updateEnabled: () => true, install: () => console.log('Fake InstallTrigger') },
                writable: false,
                configurable: false
            });
            Object.defineProperty(window, 'external', { value: {}, writable: false, configurable: false });
        });
    }

    const page = await browser.newPage();
    await patchPage(page);
    await page.goto('https://google.com');

    browser.on('targetcreated', async (target) => {
        if (target.type() === 'page') {
            try {
                const newPage = await target.page();
                await patchPage(newPage);
            } catch (err) {
                console.error('Error:', err);
            }
        }
    });
})();

Этот скрипт также маскирует браузер, но в контексте автоматизированного управления браузером. Он подменяет те же объекты (window.chrome, window.InstallTrigger, window.external), чтобы сайты, проверяющие браузерное окружение, считали, что используется Firefox.

Подмена системных паметров
Теперь поговорим про более низкоуровневый системный отпечаток! Если подмена локали, часового пояса, плагинов и других базовых настроек помогает запутать сайты, то есть еще один уровень отслеживания, который работает глубже. Сайты, могут собирать низкоуровневые данные о вашем компьютере, к примеру используя параметры hardwareConcurrency и deviceMemory. Эти параметры раскрывают, сколько ядер у вашего процессора и сколько оперативной памяти доступно. Скрыть эти данные важно, чтобы защитить вашу приватность и усложнить сайтам задачу идентификации. Потому далее я расскажу способы подделать данные о ядрах процессора и объеме памяти.

Можно использовать CDP для инъекции JavaScript. CDP даёт низкоуровневый контроль над браузером через WebSocket или библиотеки вроде pychrome или chrome-remote-interface.

Вот пример кода, где браузер с CDP открываеться puppeteer:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--remote-debugging-port=9222']
  });

  const injectSpoof = async (page) => {
    await page.evaluateOnNewDocument(() => {
      Object.defineProperty(navigator, 'hardwareConcurrency', {
        get: () => 6
      });

      Object.defineProperty(navigator, 'deviceMemory', {
        get: () => 12
      });
    });
  };
  const pages = await browser.pages();

  for (const page of pages) {
    await injectSpoof(page);
  }

  browser.on('targetcreated', async target => {
    if (target.type() === 'page') {
      const page = await target.page();
      await injectSpoof(page);
    }
  });

  const [page] = pages;
  await page.goto('https://browserleaks.com/javascript');

})();

Проверяем:
photo_2025-05-11_07-02-51.jpg

Скрипт открывает Chrome с портом для отладки, и мы используем Runtime.evaluate чтобы подменить navigator.hardwareConcurrency на 6 и navigator.deviceMemory на 12. А именно внедряет JavaScript до загрузки страницы, переопределяя navigator.

Ещё один метод — использование Tampermonkey для подмены JavaScript на странице браузера.

Пример скрипта:
JavaScript:
// ==UserScript==
// @name         Spoof Hardware
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Подмена hardwareConcurrency и deviceMemory
// @match        *://*/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    Object.defineProperty(window.navigator, 'hardwareConcurrency', {
        get: () => 6,
        configurable: false,
        enumerable: true
    });

    Object.defineProperty(window.navigator, 'deviceMemory', {
        get: () => 12,
        configurable: false,
        enumerable: true
    });
})();

Теперь запускаем скрипт:
Screenshot_2025-04-23_21-51-54.png


Переходим на нужную страницу и активируем плагин:
Screenshot_2025-04-23_22-01-13.png


В результате наш скрипт подменяет JavaScript-объекты, отвечающие за получение информации о процессоре и памяти, сразу при загрузке страницы.

Продолжим тему про js, расскажу еще метод с использование Puppeteer.

Пример кода:

JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const injectSpoof = async (page) => {
    await page.evaluateOnNewDocument(() => {
      const overrideProps = {
        hardwareConcurrency: {
          get: () => 6,
          configurable: true
        },
        deviceMemory: {
          get: () => 12,
          configurable: true
        },
        webdriver: {
          get: () => false,
          configurable: true

        },
      };
      Object.defineProperties(Navigator.prototype, overrideProps);
    });
  };

  const pages = await browser.pages();
  for (const page of pages) {
    await injectSpoof(page);
  }

  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      const newPage = await target.page();
      await injectSpoof(newPage);
    }
  });
  const [firstPage] = await browser.pages();
  await firstPage.goto('https://browserleaks.com/javascript');
})();

Проверяем:
photo_2025-05-15_20-46-25.jpg

Скрипт запускает браузер и подменяет свойства navigator.hardwareConcurrency и navigator.deviceMemory и navigator.webdriver на false. Подмена выполняется через Object.defineProperties на прототипе Navigator, чтобы эмулировать правдоподобное окружение, и применяется к каждой странице при её создании или открытии.

Последний метод котрый я опишу, но тоже с использованием Puppeteer с перехватом объектов window для глубокой подмены.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const injectNavigatorProxy = async (page) => {

    await page.evaluateOnNewDocument(() => {
      const fakeNavigator = new Proxy(navigator, {
        get(target, prop) {
          if (prop === 'hardwareConcurrency') return 6;
          if (prop === 'deviceMemory') return 12;
          return target[prop];
        }
      });

      Object.defineProperty(window, 'navigator', {
        get: () => fakeNavigator,
        configurable: true
      });
    });
  };
  const pages = await browser.pages();

  for (const page of pages) {
    await injectNavigatorProxy(page);
  }

  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      const newPage = await target.page();
      await injectNavigatorProxy(newPage);
    }
  });

  const [firstPage] = await browser.pages();
  await firstPage.goto('https://browserleaks.com/javascript');

})();
Скрипт создаёт прокси для window.navigator, перехватывая обращения к hardwareConcurrency и deviceMemory. Прокси маскирует подмену, даже если сайт копает глубже.

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

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

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox', '--use-gl=desktop']
  });

  const overridePageProperties = async (page) => {
    await page.evaluateOnNewDocument(() => {

      const fakeBattery = {
        charging: true,
        chargingTime: 0,
        dischargingTime: Infinity,
        level: 1.0,
        addEventListener: () => {}
      };

      navigator.getBattery = async () => fakeBattery;

      Object.defineProperty(navigator, 'platform', {
        get: () => 'FakeOS/1.0'
      });

      Object.defineProperty(navigator, 'vendor', {
        get: () => 'FakeVendor Inc.'
      });

      Object.defineProperty(navigator, 'productSub', {
        get: () => 'FakeProductSub'
      });

      Object.defineProperty(navigator, 'vendorSub', {
        get: () => 'FakeVendorSub'
      });

      Object.defineProperty(navigator, 'buildID', {
        get: () => 'FakeBuildID'
      });

      const getParameter = WebGLRenderingContext.prototype.getParameter;
      WebGLRenderingContext.prototype.getParameter = function(parameter) {

        console.log('WebGL parameter requested:', parameter);
        if (parameter === 7936 || parameter === 37446 || parameter === 0x9245) return 'FakeGPU Vendor';
        if (parameter === 7937 || parameter === 37447 || parameter === 0x9246) return 'FakeGPU Renderer';
        if (parameter === 7938) return 'WebGL 1.0 (Fake)'; // gl.VERSION
        if (parameter === 35724) return 'WebGL GLSL ES 1.0 (Fake)'; // gl.SHADING_LANGUAGE_VERSION
        return getParameter.apply(this, arguments);

      };

      if (typeof WebGL2RenderingContext !== 'undefined') {
        const getParameter2 = WebGL2RenderingContext.prototype.getParameter;

        WebGL2RenderingContext.prototype.getParameter = function(parameter) {
          console.log('WebGL2 parameter requested:', parameter);
          if (parameter === 7936 || parameter === 37446 || parameter === 0x9245) return 'FakeGPU Vendor';
          if (parameter === 7937 || parameter === 37447 || parameter === 0x9246) return 'FakeGPU Renderer';
          if (parameter === 7938) return 'WebGL 2.0 (Fake)'; // gl.VERSION
          if (parameter === 35724) return 'WebGL GLSL ES 3.0 (Fake)'; // gl.SHADING_LANGUAGE_VERSION
          return getParameter2.apply(this, arguments);
        };
      }
      const originalGetExtension = WebGLRenderingContext.prototype.getExtension;

      WebGLRenderingContext.prototype.getExtension = function(name) {
        if (name === 'WEBGL_debug_renderer_info') {
          console.log('WEBGL_debug_renderer_info requested');

          return {
            getParameter: (parameter) => {
              console.log('WEBGL_debug_renderer_info parameter requested:', parameter);
              if (parameter === 37446 || parameter === 0x9245) return 'FakeGPU Vendor';
              if (parameter === 37447 || parameter === 0x9246) return 'FakeGPU Renderer';
              return null;
            }
          };
        }
        return originalGetExtension.apply(this, arguments);
      };

      const originalGetSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions;
      WebGLRenderingContext.prototype.getSupportedExtensions = function() {
        console.log('Supported extensions requested');
        return ['GL_FAKE_EXTENSION', 'WEBGL_fake_extension'];
      };
    });
  };
  const pages = await browser.pages();

  for (const page of pages) {
    await overridePageProperties(page);
  }
  browser.on('targetcreated', async (target) => {

    if (target.type() === 'page') {
      const newPage = await target.page();
      await overridePageProperties(newPage);
    }
  });

  const page = await browser.newPage();
  await page.goto('https://browserleaks.com/javascript');

})();

Функция использует метод page.evaluateOnNewDocument, чтобы внедрить JavaScript-код прямо в момент загрузки страницы. Код перехватывает данные тремя способами: сначала переписывает navigator.getBattery, возвращая фейковый объект батареи через прямое присваивание, затем с помощью Object.defineProperty подменяет свойства navigator, задавая геттеры с фейковыми значениями, наконец, переопределяет методы WebGLRenderingContext.prototype.getParameter и WebGL2RenderingContext.prototype.getParameter, возвращая фейковые данные видеокарты для ряда параметров, а также подменяет версии WebGL и GLSL. Дополнительно перехватывает getExtension для подмены WEBGL_debug_renderer_info и getSupportedExtensions, выдавая фейковый список расширений.

Можно написать скрипт для Tampermonkey, который работает прямо в браузере и меняет данные на лету.

Пример кода:
JavaScript:
// ==UserScript==
// @name         Spoof Platform, Battery, and GPU
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Spoofs navigator properties, Battery API, and WebGL GPU information to fake system, battery, and GPU data in Chrome.
// @author       You
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    Object.defineProperty(navigator, 'platform', {
        get: () => 'FakeOS/2.0'
    });

    Object.defineProperty(navigator, 'vendor', {
        get: () => 'FakeVendor Inc.'
    });

    Object.defineProperty(navigator, 'productSub', {
        get: () => 'FakeProductSub'
    });

    Object.defineProperty(navigator, 'vendorSub', {
        get: () => 'FakeVendorSub'
    });

    Object.defineProperty(navigator, 'buildID', {
        get: () => 'FakeBuildID'
    });

    const fakeBattery = {
        charging: false,
        chargingTime: Infinity,
        dischargingTime: 3600,
        level: 0.75,
        addEventListener: () => {}
    };

    navigator.getBattery = async () => fakeBattery;
    const originalGetParameter = WebGLRenderingContext.prototype.getParameter;

    WebGLRenderingContext.prototype.getParameter = function(parameter) {
        console.log('WebGL parameter requested:', parameter);
        if (parameter === 7936 || parameter === 37446 || parameter === 0x9245) return 'SpoofedVendor';
        if (parameter === 7937 || parameter === 37447 || parameter === 0x9246) return 'SpoofedRenderer';
        if (parameter === 7938) return 'WebGL 1.0 (Spoofed)'; // gl.VERSION
        if (parameter === 35724) return 'WebGL GLSL ES 1.0 (Spoofed)'; // gl.SHADING_LANGUAGE_VERSION

        return originalGetParameter.apply(this, arguments);
    };

    if (typeof WebGL2RenderingContext !== 'undefined') {
        const originalGetParameter2 = WebGL2RenderingContext.prototype.getParameter;

        WebGL2RenderingContext.prototype.getParameter = function(parameter) {
            console.log('WebGL2 parameter requested:', parameter);
            if (parameter === 7936 || parameter === 37446 || parameter === 0x9245) return 'SpoofedVendor';
            if (parameter === 7937 || parameter === 37447 || parameter === 0x9246) return 'SpoofedRenderer';
            if (parameter === 7938) return 'WebGL 2.0 (Spoofed)'; // gl.VERSION
            if (parameter === 35724) return 'WebGL GLSL ES 3.0 (Spoofed)'; // gl.SHADING_LANGUAGE_VERSION
            return originalGetParameter2.apply(this, arguments);

        };
    }

    const originalGetExtension = WebGLRenderingContext.prototype.getExtension;
    WebGLRenderingContext.prototype.getExtension = function(name) {
        if (name === 'WEBGL_debug_renderer_info') {
            console.log('WEBGL_debug_renderer_info requested');

            return {
                getParameter: (parameter) => {
                    console.log('WEBGL_debug_renderer_info parameter requested:', parameter);
                    if (parameter === 37446 || parameter === 0x9245) return 'SpoofedVendor';
                    if (parameter === 37447 || parameter === 0x9246) return 'SpoofedRenderer';
                    return null;
                }
            };
        }
        return originalGetExtension.apply(this, arguments);
    };

    const originalGetSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions;

    WebGLRenderingContext.prototype.getSupportedExtensions = function() {
        console.log('Supported extensions requested');
        return ['GL_SPOOFED_EXTENSION', 'WEBGL_spoofed_extension'];

    };

    (function checkSpoofedData() {
        // Вывод navigator свойств
        console.log('Navigator Info:', {
            platform: navigator.platform,
            vendor: navigator.vendor,
            productSub: navigator.productSub,
            vendorSub: navigator.vendorSub,
            buildID: navigator.buildID
        });

        navigator.getBattery().then(battery => {
            console.log('Battery:', battery);
        }).catch(err => {
            console.error('Battery info not available:', err);

        });

        const canvas = document.createElement('canvas');
        const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

        if (!gl) {
            console.error('WebGL context not created');
        } else {
            console.log('GPU Info:', {
                vendor: gl.getParameter(gl.VENDOR),
                renderer: gl.getParameter(gl.RENDERER)

            });
            const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');

            if (debugInfo) {
                console.log('Debug Info:', {
                    vendor: debugInfo.getParameter(gl.UNMASKED_VENDOR_WEBGL),
                    renderer: debugInfo.getParameter(gl.UNMASKED_RENDERER_WEBGL)
                });
            }
            console.log('Supported Extensions:', gl.getSupportedExtensions());
        }
    })();
})();

Запускаем скрипт:
photo_2025-05-15_21-05-59.jpg

Проверяем работает ли он заходя в консоль:
photo_2025-05-15_21-06-38.jpg

Он подменяет navigator.platform, чтобы вместо реальной операционной системы сайт видел что-то вроде FakeOS/2.0. Battery API тоже подделывается — сайты думают, что у вас батарея на 75% и она не заряжается. А для WebGL скрипт перехватывает данные о видеокарте, выдавая фейковые SpoofedVendor и SpoofedRenderer. Всё это делается через аккуратные перехваты объектов и методов, а в консоли можно увидеть логи, что подмена сработала.

Теперь давайте копнём глубже с Chrome DevTools Protocol и Puppeteer. Это уже серьёзный инструмент, который позволяет управлять браузером на низком уровне.

Пример кода с CDP:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--use-gl=swiftshader',
      '--enable-webgl',
      '--ignore-gpu-blocklist',
      '--disable-gpu=false'
    ]
  });

  const applyOverrides = async (page) => {
    const client = await page.target().createCDPSession();
    await client.send('Page.enable');
    await client.send('Runtime.enable');
    await client.send('Network.setUserAgentOverride', {
      userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      platform: 'FakeOS/3.0'
    });

    await client.send('Runtime.evaluate', {
      expression: `

        (() => {
          const fakeBattery = {
            charging: true,
            chargingTime: 0,
            dischargingTime: Infinity,
            level: 1.0,
            addEventListener: () => {},
            removeEventListener: () => {}
          };

          Object.defineProperty(navigator, 'getBattery', {
            value: () => Promise.resolve(fakeBattery),
            configurable: false
          });

          Object.defineProperty(navigator, 'platform', {
            get: () => 'FakeOS/3.0',
            configurable: false
          });

          if (window.WebGLRenderingContext) {
            const getParameter = WebGLRenderingContext.prototype.getParameter;
            Object.defineProperty(WebGLRenderingContext.prototype, 'getParameter', {

              value: function(parameter) {
                if (parameter === 37446) return 'FakeVendorCDP';
                if (parameter === 37447) return 'FakeRendererCDP';
                return getParameter.apply(this, arguments);
              },
              configurable: false
            });
          }

          Object.defineProperty(navigator, 'vendor', {
            get: () => 'FakeVendor Inc.',
            configurable: false
          });

          Object.defineProperty(navigator, 'vendorSub', {
            get: () => 'fakeSub',
            configurable: false
          });

          Object.defineProperty(navigator, 'productSub', {
            get: () => '99999999',
            configurable: false
          });
        })();
      `,
      awaitPromise: true
    });
  };

  const pages = await browser.pages();
  for (const page of pages) {
    await applyOverrides(page);
  }
  browser.on('targetcreated', async (target) => {

    if (target.type() === 'page') {
      const newPage = await target.page();
      await applyOverrides(newPage);
    }

  });
  const page = await browser.newPage();
  await page.goto('https://browserleaks.com/javascript', { waitUntil: 'domcontentloaded' });

})();

Проверяем:
photo_2025-05-15_21-03-35.jpg

Код создаёт CDP-сессию и через команду Network.setUserAgentOverride подменяет платформу, задавая фейковую ОС. Затем с помощью Runtime.evaluate внедряет JavaScript, который выполняет три подмены: во-первых, через Object.defineProperty переопределяет navigator.getBattery, возвращая фейковый объект батареи с фиксированными значениями, защищённый от изменений во-вторых, тем же Object.defineProperty подменяет navigator.platform, задавая геттер для "FakeOS/3.0, в-третьих, переписывает метод getParameter у WebGLRenderingContext.prototype, подсовывая фейковые данные видеокарты.

Подмена отпечатка Canvas API
Дальше идём к Canvas API, который сайты используют для создания уникальных отпечатков. Напишу скрипт, работающий через Tampermonkey, который подменяет данные, которые сайты получают через Canvas API и JavaScript, чтобы запутать их трекеры.

Вот код:
JavaScript:
// ==UserScript==
// @name         Spoof Canvas and Platform
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Add noise to Canvas API and spoof platform
// @author       You
// @match        *://*/*
// @grant        none
// ==/UserScript==
(function() {
    'use strict';

    const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
    CanvasRenderingContext2D.prototype.getImageData = function(...args) {
        const data = originalGetImageData.apply(this, args);

        for (let i = 0; i < data.data.length; i += 4) {
            data.data += Math.random() * 2 - 1;
            data.data[i+1] += Math.random() * 2 - 1;   // Green
            data.data[i+2] += Math.random() * 2 - 1;   // Blue

        }
        return data;

    };
    const spoofedPlatform = "Win32";

    Object.defineProperty(navigator, "platform", {
        get: () => spoofedPlatform,
        configurable: true
    });
})();
Скрипт добавляет случайный шум в пиксели canvas и переписывает системные свойства, используя прямое вмешательство в прототипы и геттеры. Шум мешает трекерам создавать стабильный отпечаток, и это здорово сбивает их с толку. Скрипт перехватывает метод getImageData у CanvasRenderingContext2D.prototype, сохраняя оригинальный метод и заменяя его новой функцией. Новая функция вызывает оригинал, но перед возвратом данных добавляет случайный шум (от -1 до 1) к красному, зелёному и синему каналам каждого пикселя через цикл по массиву data.data. Это делается с помощью Math.random(), что меняет отпечаток canvas при каждом вызове. Кроме того, скрипт использует Object.defineProperty, чтобы подменить свойство navigator.platform, задавая геттер, который всегда возвращает "Win32" как фейковую платформу.

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

Вот код:
Bash:
#!/bin/bash
export DISPLAY=:0
export XAUTHORITY=$HOME/.Xauthority
xhost +SI:localuser:root

sudo unshare --uts bash -c '
  export DISPLAY=:0
  export XAUTHORITY=/home/monika/.Xauthority

  hostname fake-host

  echo "Hostname внутри namespace: $(hostname)"

  uname -a

  google-chrome \
  --no-sandbox \
  --disable-gpu \
  --disable-dev-shm-usage \
  --disable-software-rasterizer \
  --disable-extensions \
  --disable-background-networking \
  --disable-sync \
  --metrics-recording-only \
  --disable-default-apps \
  --mute-audio \
  --no-first-run \
  --disable-features=TranslateUI &
  wait
'
xhost -SI:localuser:root

Скрипт использует команду xhost +SI:localuser:root, чтобы временно разрешить root-доступ к X-серверу, обеспечивая запуск графического Chrome. Затем он, создает изолированное пространство имён UTS, где подменяет имя хоста на "fake-host". Внутри этого пространства скрипт задаёт переменные DISPLAY и XAUTHORITY для корректной работы графического интерфейса. Chrome запускается с множеством флагов, которые минимизируют утечку данных и отключают лишние функции, такие как синхронизация, расширения и звук. После завершения работы Chrome скрипт восстанавливает ограничения доступа к X-серверу.

Подмена медиа-устройств
А как насчёт медиа-устройств? Да-да, Chrome может обнаружить и их, если ты не позаботился о сокрытии своего отпечатка! Браузер умеет собирать данные о подключённых аудио- и видеоустройствах — например, микрофонах, веб-камерах или даже геймпадов. Это происходит через WebRTC и API MediaDevices, которые запрашивают доступ к твоим устройствам, чтобы, скажем, устроить видеозвонок или записать голос. Chrome может получить список устройств, их названия, типы и даже уникальные идентификаторы.

Простейший способ подменить веб-камеру и микрофон на фейковые — это скрывать реальные медиаустройства через параметры запуска Chrome.

Пример кода:
JavaScript:
#!/bin/bash
CHROME_PATH="/usr/bin/google-chrome"
DEVICE_ARGUMENTS="--use-fake-device-for-media-stream --use-fake-ui-for-media-stream --no-sandbox"
FAKE_CAMERA="--use-fake-video-capture-device"
FAKE_MICROPHONE="--use-fake-audio-capture-device"
$CHROME_PATH $DEVICE_ARGUMENTS $FAKE_CAMERA $FAKE_MICROPHONE --disable-extensions

Скрипт запускает Chrome с набором аргументов, которые подменяют медиаустройства. Я выставляю флаги --use-fake-device-for-media-stream, заставляющий браузер использовать фейковые потоки для видео и аудио, и --use-fake-ui-for-media-stream, отключающий запросы на доступ к устройствам, а так же флаг --use-fake-video-capture-device который добавляет поддельное видеоустройство, и(--use-fake-audio-capture-device — фейковый микрофон.

Можно подменять медиаустройства, и тд. через Puppeteer, для этого можно использовать параметры через JavaScript и CDP, а также использовать прокси или другие настройки для полной маскировки.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,

    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--use-fake-device-for-media-stream',
      '--enable-webrtc',
    ],
  });

  const injectionScript = `
    const navigatorProxy = new Proxy(navigator, {
      get(target, prop) {
        if (prop === 'maxTouchPoints') {
          return 5;
        }

        if (prop === 'mediaDevices') {
          const mediaDevices = target.mediaDevices || {};

          return {
            ...mediaDevices,
            enumerateDevices: async () => [
              {
                deviceId: 'fake1',
                kind: 'videoinput',
                label: 'Fake Camera',
                groupId: 'fakeGroup1',
              },
              {
                deviceId: 'fake2',
                kind: 'audioinput',
                label: 'Fake Microphone',
                groupId: 'fakeGroup2',
              },
            ],
          };
        }

        if (prop === 'getGamepads') {
          return () => {
            const fakeGamepad = {
              id: 'Fake Gamepad',
              index: 0,
              connected: true,
              timestamp: Date.now(),
              mapping: 'standard',
              axes: [0, 0, 0, 0],
              buttons: [{ pressed: false, value: 0 }],
            };
            return [fakeGamepad];
          };
        }
        return Reflect.get(target, prop);
      },
    });

    Object.defineProperty(window, 'navigator', {
      value: navigatorProxy,
      writable: false,
      configurable: true,
    });
  `;
  const applyToPage = async (page) => {
    const client = await page.target().createCDPSession();
    await client.send('Emulation.setDeviceMetricsOverride', {
      width: 375,
      height: 667,
      deviceScaleFactor: 2,
      mobile: true,
      dontSetVisibleSize: true,
    });
    await page.evaluateOnNewDocument(injectionScript);
  };
  const page = await browser.newPage();
  await applyToPage(page);
  await page.goto('https://www.google.com/', { waitUntil: 'networkidle0' });
  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      try {
        const newPage = await target.page();

        if (newPage) {
          await applyToPage(newPage);
        }

      } catch (e) {
        console.error('Ошибка при подмене в новой вкладке:', e)
      }
    }
  });
})();

Код через CDP с командой Emulation.setDeviceMetricsOverride эмулирует мобильное устройство с разрешением 375x667, масштабом 2 и мобильным режимом, подменяя метрики экрана. А также он внедряет метод injectionScript, который создаёт Proxy для объекта navigator. Этот прокси перехватывает запросы к maxTouchPoint, mediaDevices.enumerateDevices и getGamepads возвращая фейковые данные.

Конечно, можно использовать и Puppeteer с прямой JavaScript-инъекцией.

Вот пример кода:

JavaScript:
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--use-fake-device-for-media-stream',
      '--enable-webrtc',
    ],
  });

  const injectSpoofScript = async (page) => {
    await page.evaluateOnNewDocument(() => {
      Object.defineProperty(navigator, 'maxTouchPoints', { value: 5, writable: false });
      navigator.mediaDevices = navigator.mediaDevices || {};
      navigator.mediaDevices.enumerateDevices = async () => [
        { deviceId: 'fake1', kind: 'videoinput', label: 'Fake Camera', groupId: 'fakeGroup1' },
        { deviceId: 'fake2', kind: 'audioinput', label: 'Fake Microphone', groupId: 'fakeGroup2' },
      ];

      navigator.getGamepads = () => {
        const fakeGamepad = {
          id: 'Fake Gamepad',
          index: 0,
          connected: true,
          timestamp: Date.now(),
          mapping: 'standard',
          axes: [0, 0, 0, 0],
          buttons: [{ pressed: false, value: 0 }],
        };
        return [fakeGamepad, null, null, null];
      };
    });
  };

  const page = await browser.newPage();
  await injectSpoofScript(page);
  await page.goto('https://example.com', { waitUntil: 'networkidle0' });
  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      try {
        const newPage = await target.page();
        await injectSpoofScript(newPage);

      } catch (err) {
        console.warn('Error injecting spoof script to new page:', err.message);
    }
  });
})();
Скрипт использует подмену через page.evaluateOnNewDocument внедряет JavaScript, который переписывает три аспекта: задаёт navigator.maxTouchPoints на 5 с защитой от записи и navigator.mediaDevices.enumerateDevices переопределяется для возврата фейковых камеры и микрофона, а еще navigator.getGamepads заменяется функцией, возвращающей фейковый геймпад с заданными параметрами.


Сокрытие автоматизации
Чтобы подделывать отпечатки браузера, мы часто используем Puppeteer, но браузеры могут легко обнаружить его через свойство navigator.webdriver, которое выдает автоматизацию. Это происходит, потому что Puppeteer по умолчанию работает в режиме, где это свойство явно указывает на использование WebDriver. Но не переживайте, есть способы это обойти, и сейчас мы разберем их.

Можно использовать puppeteer-extra-plugin-stealth, потому что этот плагин автоматически применяет множество техник маскировки, включая подмену User-Agent, удаление специфичных следов Puppeteer, таких как window.chrome.webdriver.

Пример кода с ним:
JavaScript:
const puppeteer = require('puppeteer-extra');

const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  async function patchPage(page) {
    await page.evaluateOnNewDocument(() => {
      Object.defineProperty(navigator, 'webdriver', {
        get: () => false,

      });
    });
  }

  const page = await browser.newPage();
  await patchPage(page);
  await page.goto('https://browserleaks.com/javascript');
  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      try {
        const newPage = await target.page();
        await patchPage(newPage);
      } catch (err) {
        console.error('Ошибка при патче новой страницы:', err);
      }
    }
  });
})();
Этот скрипт использует Puppeteer с плагином puppeteer-extra-plugin-stealth, чтобы скрыть признаки автоматизации браузера. Основной метод подмены — использование StealthPlugin, который автоматически применяет набор техник для маскировки автоматизации. Дополнительно функция patchPage через page.evaluateOnNewDocument внедряет JavaScript, который с помощью Object.defineProperty переопределяет свойство navigator.webdriver, задавая геттер, возвращающий false, чтобы сайты не заподозрили автоматизацию.

Ещё один метод — прямое переопределение navigator.webdriver, чтобы избежать подозрений со стороны строгих проверок.

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  async function patchPage(page) {
    await page.evaluateOnNewDocument(() => {
      Object.defineProperty(navigator, 'webdriver', {
        get: () => false,
        configurable: true
      });
    });
  }
  const page = await browser.newPage();
  await patchPage(page);
  await page.goto('https://browserleaks.com/javascript');
  browser.on('targetcreated', async (target) => {
    if (target.type() === 'page') {
      try {
        const newPage = await target.page();
        await patchPage(newPage);
      } catch (err) {
        console.error('Error:', err);
      }
    }
  });
})();
Этот скрипт использует Puppeteer для подмены данных, чтобы сайты не могли обнаружить автоматизацию через свойство navigator.webdriver. Основной метод — прямое вмешательство в JavaScript с помощью page.evaluateOnNewDocument, где функция patchPage внедряет код для переопределения navigator.webdriver. Геттер этого свойства настроен на возврат false, что маскирует использование WebDriver. Свойство остаётся настраиваемым (configurable: true), чтобы не вызывать подозрений у строгих проверок.

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

Пример кода:
JavaScript:
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-blink-features=AutomationControlled'
    ],
    ignoreDefaultArgs: ['--enable-automation']

  });

  const page = await browser.newPage();
  await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'webdriver', {

      get: () => false
    });
  });
  await page.goto('https://browserleaks.com/javascript');

})();

Код маскирует автоматизацию Puppeteer, подменяя данные, которые сайты используют для выявления ботов, с помощью комбинации аргументов запуска браузера и js. Он отключает признаки автоматизации, добавляя флаг --disable-blink-features=AutomationControlled в аргументы запуска, чтобы убрать специфичные для WebDriver поведения движка Blink. Также через ignoreDefaultArgs удаляет флаг --enable-automation, который Puppeteer добавляет по умолчанию. Дополнительно с помощью page.evaluateOnNewDocument внедряется JavaScript, который переопределяет свойство navigator.webdriver, задавая геттер, возвращающий false, скрывая факт использования автоматизации.


Вывод
На этом первую часть можно считать завершённой. Мы с вами разобрали разные методы подделки отпечатков в Chrome — от подмены User-Agent и Client Hints до изоляции окружения через Bubblewrap, Firejail и настройку локали, шрифтов, времени и координат. Это всё может пригодиться не только тем, кто хочет повысить свою приватность, но и разработчикам, которые создают собственные инструменты — будь то для обхода антифрод-систем, автоматизации серфинга.
Но это ещё не конец - Во второй части я подробно расскажу, как я написал два полноценных инструмента для подмены отпечатков браузера — разберу их архитектуру, подходы к автоматизации, логику подстановки данных и, конечно, где брать реалистичные отпечатки, чтобы выглядеть как настоящий пользователь. Так что не прощаемся — продолжение уже на подходе.
 
Последнее редактирование:
Небезызвестный Vektor T13 в своих статьях пишет, что продвинутые системы антидетекта и борьбы с мультами используют вычисления на GPU. То есть, заставляют браузер вычислять что-то сложное и многопотоковое и в зависимости от прогресса и результата делают выводы об уникальности клиента.

Интересно твоё мнение о продуктах Вектора и его методике.

Также интересно, как обстоят дела у других браузеров типа TOR Browser или Brave или какие там ещё есть браузеры с акцентом на приватность/анонимность.
 
Небезызвестный Vektor T13 в своих статьях пишет, что продвинутые системы антидетекта и борьбы с мультами используют вычисления на GPU. То есть, заставляют браузер вычислять что-то сложное и многопотоковое и в зависимости от прогресса и результата делают выводы об уникальности клиента.

Интересно твоё мнение о продуктах Вектора и его методике.

Также интересно, как обстоят дела у других браузеров типа TOR Browser или Brave или какие там ещё есть браузеры с акцентом на приватность/анонимность.
Добрый день, он прав, даже Tor при стандартных настройках не блокирует это(в статье про tor будет про это =) ) . Обычно запускается тяжёлая задача на WebGL и заменяется сколько времени она заняла, а далее по этому времени вычисляется примерная мощность gpu.

По поводу Vektor T13, в целом положительно, у него есть достаточно много дельных мыслей, по поводу антидетекта. К примеру он хорошо рассказывал про обход обнаружения виртуалок.
 
К примеру он хорошо рассказывал про обход обнаружения виртуалок.
Может есть ссылка на чтиво об этом? Было бы интересно ознакомиться...
 
Годно.
Canvas тушится Hosts файлом кстати ещё. Но тут зависит от целей так сказать...
 
статья про анонимность, но не про спуфинг. А то ща вбиверы (ебать что за слово зумерское) ломануться палку бить по мануалам)))))
вскользь рассказано о квик протоколе, а он то уже по тихому несёт горе и утрату))))
 
отличная статья, спасибо! ждём такое же про Firefox сотоварищи.
 
При обращении к гугловским сервисам используется абсолютно всегда QUIC.
Кроме того передается X-Variations-Ids, внутри protobuf и набор "чисел", на самом деле это variation_id и формируются они от seed сервера при установке (посмотреть подобный можно на GitHub brave)
Дальше эта "телеметрия" позволяет хотя-бы минимально защищать/собирать/идентифицировать, какой нибудь BotGuard на YouTube на самом деле не делал это.

Внутри указываются данные вплоть до пиратской версии windows.

Дополнительно добавлю про цифровой отпечаток не браузера, а соединения, QUIC в этом плане более лоялен чем HTTP/2 со слов одного из разработчиков.
 
Sorry op, but you wasted a lot of time. Website admins if they want, they can detect very easily if you spoofing/modifying things, for example like prototypes. If serious obfuscation is involved, you will may never know if someone is doing any time of detection / are you triggering alarms or not. That is why there is no place for javascript if you are serious about what you are doing. My advice is search for ungoogled chromium, shape things you want/need using cpp only and leave js alone or use in conjunction with cpp where for example, your prototypes wont report that they are modified. The only problem is that you will need to apply patches on top, but this pretty quickly solvable problem.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх