Автор: hipeople
Источник https://xss.pro
Это третья часть моего цикла(первая часть и вторая часть про Chrome) про сокрытие отпечатков на Linux. В первой части и второй я показал, как Chrome собирает данные и как его можно перехитрить, а теперь настала очередь Firefox — браузера, который считается чуть ли не эталоном приватности. Но так ли это на самом деле? Как сайты могут использовать Firefox для создания отпечатка? Давай разберёмся, какие данные он отдаёт, как это происходит, и как с этим бороться. Сначала я расскажу, что именно собирают сайты, с примерами из исходников и JavaScript. Потом покажу, как подменять эти данные, какие инструменты для этого использовать и как автоматизировать процесс. Сам Firefox хоть и довольно гибок в изменении отпечатка, но имеет свои косяки и нюансы, которые необходимо знать, если ты хочешь сохранить свою приватность. Не будем затягивать, поехали!
Сбор данных
Firefox не такой "жадный" до данных, как Chrome, но сайты всё равно могут вытянуть из него кучу информации для фингерпринтинга. Эти данные делятся на статические (те, что браузер отдаёт напрямую через API) и динамические (поведенческие, основанные на твоих действиях). Firefox сам по себе не собирает их для себя, но его API и настройки дают сайтам возможность строить твой профиль. Давай разберём, что именно они берут.
Какие отпечатки собирает Firefox?
Статические отпечатки - это то, что сайты получают через JavaScript, словно снимая моментальный снимок твоего устройства. Например, navigator.userAgent выдаёт строку вроде "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0", которая подсказывает, что ты используешь Linux, а не Windows, как большинство. Через window.screen сайты узнают разрешение экрана, скажем, 1920x1080, и глубину цвета. navigator.hardwareConcurrency может показать, сколько ядер у твоего процессора — 16 ядер встречаются реже, и это делает тебя заметнее. Если у тебя подключён геймпад, navigator.getGamepads() раскроет его модель, что тоже добавляет уникальности. WebGL через gl.getParameter(gl.RENDERER) может выдать информацию о видеокарте, например, "NVIDIA GeForce RTX 3080". А рендеринг через создаёт отпечаток на основе шрифтов, графических настроек и даже драйверов. Даже такие мелочи, как язык браузера через navigator.language (например, "ru-RU") или список шрифтов через document.fonts, помогают сайтам выделить тебя из толпы.
Динамические отпечатки - это твоё поведение, и тут всё становится ещё интереснее. Сайты могут отслеживать, как быстро ты печатаешь, двигаешь мышкой или скролишь страницу, используя события вроде onmousemove или onkeydown. Таймеры, такие как performance.now(), показывают, с какой скоростью твой компьютер выполняет скрипты — если страница рендерится за 10 мс у тебя и за 15 мс у другого, это уже различие. Даже порядок загрузки ресурсов или реакции на события могут выдать твоё устройство.
Телеметрия в Firefox есть, но она скромнее, чем в Chrome. Браузер отправляет Mozilla отчёты о сбоях, статистику использования (например, количество вкладок) и версию через toolkit.telemetry. У каждой установки есть client ID — уникальный идентификатор, который может связать твои сессии, если телеметрия включена. Это выключаеться в настройках (about:preferences#privacy, раздел "Firefox Data Collection") или через toolkit.telemetry.enabled в about:config. Без настроек сайты через JavaScript всё равно берут данные. Например, canvas и WebGL могут связать твои сессии, если добираются до шрифтов или таймингов.
В целом, Firefox по умолчанию оставляет некоторые функции, которые могут использоваться для фингерпринтинга, но даже без дополнительных настроек браузер предлагает защитные механизмы. Например, Enhanced Tracking Protection блокирует трекеры, известные Mozilla, включая те, что собирают данные для создания уникального отпечатка устройства. Однако это не полностью отключает API, такие как WebGL или canvas, а лишь ограничивает их использование отдельными трекерами. Чтобы лучше понять, что именно браузер может раскрыть, стоит заглянуть в его исходники — там можно увидеть, какие данные Firefox вообще способен передать. Давай разберёмся, что там к чему. Погнали!
Примеры сбора отпечатка из исходников Firefox
Я начну с того, что покажу как формируется отпечаток браузера на основе исходного кода Firefox, написанного в основном на C++, но активно переписываемого на Rust для повышения безопасности. Исходники вы можете найти на https://searchfox.org/mozilla-central/source/. В отличие от Chrome, чьи закрытые исходники ограничивают анализ, Firefox предоставляет открытый код, который можно изучить, хотя Mozilla может вносить изменения в финальных сборках. Сам Firefox не занимается сбором отпечатков, но поставляет данные, которые сайты используют для их создания. Этих данных в коде предостаточно. Далее я разберу несколько фрагментов кода, покажу, и объясню, как эти данные собираются с помощью JavaScript для создания уникального отпечатка браузера.
Firefox собирает разрешение экрана и позиция окна. Он делает это через методы вроде GetScreen и GetScreenXY в nsGlobalWindowOuter.cpp, чтобы сайты могли получать эту информацию через JavaScript (например, window.screen или window.screenX)
Исходник:
Есть несколько функций, которые дают информацию о экране и окне. Например, GetScreen() возвращает объект nsScreen, через который можно взять ширину (screen.width), высоту (screen.height) и глубину цвета экрана. Дальше, GetScreenXY() вычисляет, где окно находится на экране — его координаты x и y. Но если включена защита resistFingerprinting, функция всегда возвращает возвращаются нулевые координаты, чтобы затруднить сбор отпечатка. Без защиты оно берёт настоящие координаты из системы через GetTreeOwnerWindow() и преобразует их в CSS-единицы. Ещё есть GetScreenXOuter(), который просто вызывает GetScreenXY() и отдаёт только x-координату. А GetInnerScreenRect() считает размер внутренней части окна (без рамок и панелей) в специальных единицах приложения — это нужно для точного рендеринга страниц. Если что-то идёт не так (например, нет данных от mDocShell или presShell), функции возвращают пустые значения, чтобы ничего не сломалось.
Вот как это собрать данные об экране на js:
Вывод:
Как это работает? window.screen.width и height берут данные из GetScreen(), а screenX/screenY — из GetScreenXY(). Если защита включена, screenX и screenY будут 0, а размеры могут подмениться на стандартные, но по дифолту, защиты нет, так что вы сможете собрать реальные размеры экранов у многих юзеров.
Ещё одна штука — Battery Status API. Firefox поддерживает его (хотя в новых версиях Firefox убрал его поддежку). На старых браузерах, сайты всё ещё, могут узнать уровень заряда батареи, время до разрядки и даже статус (заряжается или нет). Это даёт уникальный отпечаток, особенно если ты на ноутбуке.
Исходник:
https://searchfox.org/mozilla-central/source/dom/battery/BatteryManager.cpp
Что делает код?
Этот код решает, что показать, когда сайт спрашивает про заряд, время зарядки или разрядки. Функция Charging() проверяет, заряжается ли батарея, и возвращает true или false. Если в настройках Firefox включена защита privacy.resistFingerprinting, браузер всегда говорит сайтам, что батарея заряжается, чтобы тебя было сложнее отследить. Без этой защиты он выдаёт правду — заряжается твоя батарея или нет. Level() показывает уровень заряда в виде числа от 0 до 1, например, 0.73 для 73%. С включённой защитой Firefox выдает, что батарея всегда на 100%, чтобы твой отпечаток был как у всех. DischargingTime() выдаёт, сколько секунд осталось до разрядки батареи, но если она заряжается или данных нет (например, на ПК), возвращает огромное число. ChargingTime() говорит, сколько секунд нужно для полной зарядки, а если батарея не заряжается или данных нет, тоже возвращает бесконечность.
Пример кода для сбора Battery Fingerprint на старых версиях Firefox:
Как это работает?
Код запрашивает данные о батарее и возвращает их в виде объекта. Например, если у тебя 73% заряда и батарея заряжается, это будет { level: 0.73, charging: true, dischargingTime: Infinity, chargingTime: 3600 }. Эти данные обновляются в реальном времени, так что сайты могут отслеживать изменения, связывая их с твоей сессией.
Так же в Firefox может собираться количество ядер процессора если не включина защита от этого.
Исходник:
Сначала код проверяет, есть ли сервис RuntimeService, который знает, сколько ядер у твоего компа. Если сервиса нет, возвращает 1, чтобы не было ошибок. Если сервис есть, то ClampedHardwareConcurrency решает, что показать. Когда resistFingerprinting включён (это проверяется через ShouldResistFingerprinting с целью NavigatorHWConcurrency), число ядер ограничивается до 2 — это защита, чтобы все выглядели одинаково. Без защиты Firefox отдаёт реальное количество ядер, например, 4 или 8, в зависимости от твоего процессора.
Вот как собирать количество ядер на js:
Проверяем:
Как это работает? navigator.hardwareConcurrency вызывает HardwareConcurrency() из исходников и получает количество ядер.
И наконец timing, с помощью него firefox возвращает точное время в миллисекундах, которое сайты могут использовать для замеров скорости твоего компа.
Исходник:
Что он делает: Cначала функция берёт "сырое" время с помощью NowUnclamped(), а потом решает, что с ним делать. Если запрос идёт от системного уровня (например, не от сайта, а от самого Firefox), отдаётся точное время без изменений. Для обычных сайтов, если включён resistFingerprinting, время округляется до 100 миллисекунд через ReduceTimePrecisionAsMSecs — это мешает сайтам делать точные тайминговые атаки, где они угадывают твоё железо по скорости операций. Без защиты ты получаешь полную точность, например, 76.345 мс.
Вот как собрать тайминги для отпечатка:
Проверяем:
Как это работает? performance.now() использует Now() из исходников и получает время задежки.
Сбор отпечатков в Firefox не ограничивается этими примерами. Браузер также собирает данные о user agent, видеокарте и многом другом. Но думаю, этих примеров достаточно для понимания того как поисходит сбор отпечатков в Firefox.
Пример кода для сбора отпечатка
Для большей ясности я приведу пример кода, который можно использовать для сбора простого отпечатка в Firefox. Он дополняет то, что я уже показал, и собирает дополнительные данные.
Пример скрипта:
Пример вывода кода:
Методы скорытия отпечатка
Теперь, когда мы разобрались, какие данные Firefox отдаёт сайтам, пора перейти к методам защиты. Мы начнём с настроек about:config, рассмотрим плагины и Tampermonkey, а в конце я расскажу, как собрать единую конфигурацию Firefox для максимальной маскировки.
Firefox этот браузер из коробки уже неплохо заботится о твоей приватности, в отличие от того же Chrome, который очень охотно выдает все твои данные. Firefox сразу предлагает всякие штуки, чтобы сайтам было сложнее за тобой следить. Например, есть настройка privacy.resistFingerprinting в about:config — включаешь, и вуаля: ядра процессора показываются как два, экран становится 1920x1080, шрифты только самые базовые, типа Arial. Ещё тайминги событий округляются, чтобы сайты не могли вычислить, как ты мышкой водишь. Плюс строгий режим Enhanced Tracking Protection убирает трекеры и подозрительные API. Но, знаешь, даже с этим всем сайты всё равно могут что-то обнаружить, если не поднастроить защиту как следует.
Инструменты для скорытия отпечатка
На Linux есть множество инструментов и подходов, чтобы подменить отпечатки - от конфигурации браузера до автоматизации и изоляции окружения. Давай разберем, как это сделать.
Первое что приходит в голову это about:config, в нем можно настроить Firefox под себя. Настройки в about:config Firefox позволяют минимизировать сбор отпечатков, ограничивая данные, которые браузер передаёт сайтам. Если заглянуть в исходники браузера, всё завязано на условные операторы и флаги, вроде Preferences::GetBool, которые решают, какие данные возвращать. Хочешь, чтобы сайт получил пустышку вместо твоей инфы? Настраиваешь параметр, и браузер либо скрывает данные, либо подсовывает что-то не своё. Например, можно подкрутить WebGL, чтобы он выдавал слегка искажённые характеристики железа — графика работает, а отпечаток уже не твой. Или взять WebRTC: выключаешь его через настройку, и сайты теряют доступ к твоей сети, не могут подсмотреть твои локальные адреса. Но не всё так просто. Даже с идеальными настройками в about:config полная анонимность — это миф, если не прикрыть другие лазейки которые нужно закрывать другими инструментами.
Ещё можно использовать Firejail: он засовывает Firefox в песочницу, где можно отрезать лишние сетевые подключения или подменить переменную $DISPLAY, чтобы браузер думал, что у тебя другой экран. С параметром --net=none утечки вообще сводятся к минимуму.
Для тех, кто любит заморочиться, есть вариант собрать Firefox из исходников. В файле mozconfig можно вырубить телеметрию, WebRTC и всё, что считаешь лишним, просто добавив пару строчек вроде --disable-telemetry. Это не для новичков, но зато контроль полный — можешь даже патчить функции, чтобы, например, координаты экрана всегда были фиксированными. Времени, правда, на это уйдёт прилично. Я не буду показывать этот метод в статье, тем не менее я не мог не упомянуть его для полноты картины.
Ещё отпечаток можно подменять на уровне плагинов. В Firefox есть расширения, которые подменяют отпечатки прямо на уровне JavaScript, и их сложнее вычислить, чем в Chrome. Правда, выбор таких плагинов поменьше, но, если настроить правильно, они могут помочь в подмене отпечатка. Так же можно самостоятельно написать плагин для подмены отпечатка.
Шпаргалка: как устанавливать основные инструменты из статьи.
Перед тем как разбирать конкретные методы подмены (шрифты, WebGL, локали и т.д.), опишу как устанавливать основные инструменты, которые будем использовать: плагины для Firefox и пользовательские скрипты через Tampermonkey. Здесь вы найдёте инструкции по их установке и базовой настройке.
Как загружать плагины?
Плагины могут помочь в подмене параметров браузера, таких как шрифты, WebGL или User-Agent, на уровне JavaScript. Мы будем использовать временные плагины, которые не требуют публикации в Mozilla Add-ons.
Для создания тестового плагина в Firefox создайте папку, например test_plugin, и положите в неё два файла: manifest.json и content.js. В manifest.json задаётся структура и права плагина, а content.js содержит JavaScript-код для подмены данных.
Вот пример manifest.json для test_plugin:
Поле name отвечает за название плагина, permissions указывает права (здесь доступ ко всем сайтам), content_scripts подключает content.js, который запускается на всех сайтах на этапе document_start, до загрузки страницы.
Вот пример content.js для test_plugin с заглушкой:
Чтобы установить плагин, откройте Firefox, введите about:debugging#/runtime/this-firefox в адресной строке. После нажмите Load Temporary Add-on и выберите manifest.json из папки:
Установка скриптов Tampermonkey
Еще я очень часто буду использовать Tampermonkey — расширение для запуска JavaScript-кода на сайтах. Установите его через https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/.
После установки кликните на иконку Tampermonkey в панели расширений, выберите Create a new script:
Вот пример скрипта test_script:
Вставьте код и сохраните.
Активруйте скрипт:
Подмена отпечтаков
На этом, хватит теории, давай проверим, как это работает на практике.
С самого начала скажу тебе важную заметку на будующие: не перестарайся с рандомизацией. Если твои параметры будут слишком уж хаотичными, ты можешь выделиться среди толпы, а это хуже, чем стандартный отпечаток. Так что подменяй с умом, чтобы всё выглядело естественно. Для тестов я буду использовать https://browserleaks.com/ и https://amiunique.org/fingerprint.
Смена User agent
Когда речь заходит о сокрытии отпечатков в Firefox, одним из первых шагов становится подмена User-Agent — строки, которую браузер отправляет сайтам, сообщая о своей версии, операционной системе и других характеристиках. User-Agent может выдать, что ты используешь Firefox на Linux, а это уже зацепка для трекеров.
Начнем с самого простого и доступного способа - настройки в about:config. Это интерфейс Firefox, где можно менять скрытые параметры браузера. За User-Agent отвечает параметр general.useragent.override. По умолчанию он не задан, и Firefox отправляет стандартный User-Agent, например, Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0. Чтобы подменить его, нужно создать или изменить этот параметр вручную.
Для этого сначала в Firefox и в адресной строке введи about:config. Нажми «Принять риск и продолжить», чтобы попасть в панель настроек.
Далее в строке поиска введи general.useragent.override:
Если параметр уже существует, он будет отображаться, но чаще всего его нет, и значение параметра задано как boolean. В этом случае нужно создать его. Выбери тип String и нажми на кнопку с плюсом (+) справа, чтобы добавить новую настройку далее.
После этого в поле значения введи нужный тебе User-Agent:
После этого нажми на галочку. Теперь Firefox будет отправлять сайтам именно этот User-Agent.
После сохранения настройки стоит проверить, сработала ли подмена. Для этого я использовал browserleaks.com:
Всё работает, правда, другие параметры выдаёт в нас Firefox, но их мы подменим чуть позже.
Есть более удобный и автоматизированный способ — использование файла user.js, который находится в профиле Firefox. Для него не обязательно каждый раз лезть в about:config и вручную править настройки. Этот файл позволяет задавать настройки, которые будут применяться автоматически при каждом запуске браузера.
Профиль Firefox обычно лежит в директории ~/.mozilla/firefox/, и его имя заканчивается на .default-release, .default-esr или что-то похожее, например, abcd1234.default-release. Чтобы найти свой профиль, открой терминал и перейди в ~/.mozilla/firefox/. Команда ls покажет папки профилей. Если у тебя несколько профилей, выбери нужный, проверив их в Firefox через about:profiles. Внутри папки профиля и будет наш файл user.js.
Если файла user.js нет, его нужно создать. Открой терминал, перейди в папку профиля и создай файл:
Затем открой его в текстовом редакторе, например, nano user.js.
Чтобы подменить User-Agent, добавь в файл следующую строку:
После добавления строки перезапусти Firefox. Настройки из user.js подтягиваются автоматически при запуске и переписывают соответствующие параметры в about:config.
Также user agent можно менять с помощью плагинов. Я расскажу про User-Agent Switcher: в нем можно выбрать разные юзер-агенты — от Windows до Mac или iPhone.
Для начала установим User-Agent Switcher. Перейдите на официальную страницу дополнений Firefox по ссылке: https://addons.mozilla.org/en-US/firefox/addon/uaswitcher/.
На странице нажмите кнопку Add to Firefox, чтобы начать установку:
После нажатия на Add to Firefox браузер покажет всплывающее окно с запросом на добавление расширения. Нажмите Add, чтобы подтвердить установку.
Теперь, когда плагин установлен, ты можешь выбрать нужный User-Agent. Щёлкни по иконке User-Agent Switcher на панели инструментов браузера (или в меню расширений). В выпадающем списке доступны различные варианты: Windows, macOS, Linux, iOS, Android и тд.
Для примера я выбрал User-Agent для Windows:
Ну и проверяем сработала ли подмена юзер агента:
Смена шрифтов
Теперь поговрим про смену шрифтов в firefox. Firefox, в отличие от Chrome, в плане шрифтов намного гибче. Можно подменять шрифты через плагины или просто через в настройки. В отличие от Chrome где это можно нормально сделать только на уровне системы.
Сначала поговорим про самый простой способ — смену шрифтов через настройки Firefox. Это, конечно, не полноценная подмена, и автоматизировать процесс сложновато, но зато можно легко настроить шрифты так, чтобы они отличались от стандартных для твоей системы. Хочешь, чтобы сайт думал, что ты используешь шрифты, нехарактерные для твоей ОС? Без проблем!
Открой Firefox и в адресной строке введи about:preferences#general. Прокрути вниз до раздела Fonts или просто вбей в поиске настроек слово Fonts:
Нажми на кнопку Advance — и вуаля! Перед тобой откроется окно, где можно выбрать любые шрифты для разных категорий текста:
Выбирай шрифты, которые тебе нравятся, или те, что хочешь использовать для подмены. Например, можешь поставить дефолтные шрифты.
Еще шрифты можно менять в about:config. Если вручную это делать не охота, можно задать свои шрифты прямо в user.js, который лежит в профиле браузера. Например, можно указать Georgia для serif, Helvetica для sans-serif, задать размер шрифта побольше и минимальный порог, чтоб текст не мельчил.
А чтобы не копаться в папках вручную, я написал легкий скрипт на Bash, который всё делает сам:
Проверяем работу скрипта:
Скрипт сначала ищет папку профиля Firefox, потом создаёт user.js с нужными настройками шрифтов. Заодно подправляет .desktop-файл Firefox, чтобы DPI шрифтов в Qt-приложениях тоже был в норме — это полезно, если шрифты выглядят не так, как надо. После запуска скрипта всё готово, и Firefox стартует с твоими шрифтами.
Еще чтобы поменять шрифты в Firefox, можно создать свой плагин. Это проще, чем кажется, и даёт полный контроль над тем, как страницы отображают текст. Я покажу, как собрать такой плагин, который подменяет шрифты через CSS и JavaScript.
Тут есть два варианта: попроще, с использованием только CSS, и посложнее, с добавлением JavaScript для перехвата шрифтов.
Оба плагина подменяют шрифты на стандартные, например Arial для текста и Courier New для кода, и подключаются через about:debugging в Firefox. Я разберу оба подхода, покажу код и объясню, как всё работает, чтобы ты мог выбрать тот, что больше подходит.
Сначала расскажу про вариант один. Плагин использует CSS, чтобы заставить все сайты отображать текст в Arial, а моноширинные элементы (код, поля ввода) — в Courier New.
Вот пример:
manifest.json:
Файл manifest.json описывает плагин: он называется Font Spoofer, работает на всех сайтах (<all_urls>) и подключает styles.css. Параметр run_at: "document_start" запускает стили сразу при загрузке страницы, чтобы шрифты подменились до рендеринга.
styles.css:
В styles.css всё просто: селектор * задаёт Arial для всех элементов с !important, чтобы перебить стили сайта. Для моноширинных текстов (код, поля ввода, текстовые области) используется Courier New.
Чтобы установить плагин, создаёшь папку с двумя файлами: manifest.json и styles.css. Далее устанавливаешь плагин, и он сразу подключается и начинает работать. Он появиться в плагинах.
Проверяем его работу на https://browserleaks.com/fonts:
Еще есть второй плагин: подмена шрифтов через CSS и JavaScript. Этот вариант сложнее. Он не только применяет стили, как первый плагин, но и перехватывает шрифты, которые сайты пытаются подгрузить через FontFace. Это полезно, если сайт использует кастомные шрифты, которые могут выдать твой браузер.
Создай файл manifest.json:
Этот manifest.json почти такой же, как в первом плагине, но добавляет content.js в content_scripts, чтобы подключить JavaScript. Всё так же работает на всех сайтах и запускается в начале загрузки страницы.
content.js:
Скрипт content.js — главная фишка второго плагина. Он перехватывает создание объектов FontFace, которые сайты используют для загрузки кастомных шрифтов. Если шрифт не Arial, Times New Roman или Courier New, он заменяется на Arial. Также скрипт добавляет CSS-правила прямо в страницы через тег <style>, дублируя styles.css для надёжности.
styles.css:
Файл styles.css идентичен первому плагину: Arial для всех элементов, Courier New для моноширинных. Он работает как основная подмена шрифтов через стили.
После установки проверяй результат на browserleaks.com/fonts. Должен отображаться только Arial для текста и Courier New для моноширинных элементов. Если сайты всё ещё видят другие шрифты, убедись, что плагин запускается на этапе document_start, и проверь, не перебиваются ли стили сайта через !important.
Если не хочется писать плагин для подмены шрифтов с нуля, можно взять готовое решение, например, расширение Web Font Changer из магазина Firefox: https://addons.mozilla.org/en-US/firefox/addon/web-font-changer/. Это удобный инструмент, который позволяет менять шрифты на сайтах прямо на лету, без необходимости копаться в коде или настройках браузера.
Установить расширение проще простого. Открываешь Firefox, переходишь по ссылке https://addons.mozilla.org/en-US/firefox/addon/web-font-changer/, и на странице расширения жмёшь кнопку "Add to Firefox".
Браузер покажет всплывающее окно с запросом на добавление — подтверждаешь, нажав "Add". После этого Web Font Changer появится в списке твоих расширений, и его иконка добавится на панель инструментов Firefox.
Далее перейдите на нужный вам сайт и используйте его, выбрав нужные вам шрифты:
Я заглянул в код этого плагина, раскопав его в /.mozilla/firefox/8994vyhd.default-esr/extensions/ в xpi-архиве. И вот как он работает так: через CSS он задаёт стандартные шрифты для всех элементов страницы, перебивая стили сайта, а с помощью JavaScript он перехватывает объект FontFace, так что любые кастомные шрифты, которые сайт пытается подгрузить, превращаются в разрешённые: Arial, Times New Roman или Courier New. Настройки шрифтов (какой шрифт, стиль, для каких тегов) плагин сохраняет в browser.storage.local, и ты можешь выбрать, применять их везде или только на конкретном сайте — есть опция sameDomainOnly. Если захочешь чего-то необычного, вроде Roboto, он подтянет шрифт из Google Fonts через <link>.
Еще можно подменять шрифты и запускает Firefox с помощью Firejail. Для этого нужно создать временную конфигурацию fontconfig, которая подменяет системные шрифты, и запустить браузер с ней через Firejail, ограничив его доступ к системе.
Для автоматизации данного процесса я решил написать bash-скрипт:
Проверяем работу скрипта:
Скрипт начинается с проверки доступности шрифтов. Он использует fc-list, чтобы узнать, установлен ли Liberation Sans. Если да, задаёт Liberation Sans, Serif и Mono как основные шрифты. Если нет, пытается установить пакет fonts-liberation через apt, а в случае неудачи переключается на DejaVu Sans, Serif и Mono. Если и DejaVu не нашёл, устанавливает общие семейства sans-serif, serif и monospace как запасной вариант. Это гарантирует, что система всегда найдёт подходящий шрифт. После этого скрипт чистит кэш fontconfig, удаляя старые данные, и обновляет его через fc-cache, чтобы всё работало с актуальными настройками.
Дальше создаётся временная папка для конфигурации шрифтов. В неё записывается файл fonts.conf, который задаёт правила fontconfig: отключает шрифты Norasi и DejaVu Math TeX Gyre, включает директории с Liberation и DejaVu, активирует сглаживание, хинтинг и автокоррекцию, а также подменяет общие семейства шрифтов на выбранные. Этот файл становится временной конфигурацией, и fc-cache обновляет кэш для неё. Скрипт проверяет, правильно ли настроен sans-serif шрифт через fc-match, и, если что-то пошло не так, выдаёт ошибку и завершает работу.
Затем создаётся временный профиль для Firejail. Он наследует настройки из firefox-common.profile, включает сетевой фильтр, разрешает доступ к кэшу fontconfig, временной конфигурации шрифтов и папкам с Liberation и DejaVu, а также ограничивает сетевые протоколы unix, inet и inet6. Это изолирует Firefox, минимизируя риски. Скрипт запускает браузер через Firejail с этим профилем и переменной FONTCONFIG_PATH, указывающей на временную конфигурацию. Если запуск не удался, выводится ошибка, и всё чистится. В конце скрипт удаляет временные файлы, чтобы не оставлять мусора.
Смена локалей и таймзон
Давайте разберём тему смены локалей и таймзон. Локаль определяет, какой язык и региональные настройки браузер передаёт сайтам, а таймзона напрямую указывает на ваше географическое положение. Эти данные легко считываются через JavaScript (например, navigator.languages или Intl.DateTimeFormat) или HTTP-заголовки вроде Accept-Language.
Локаль можно изменить в about:config или файл user.js.
Вот как это сделать:
Это влияет на заголовок Accept-Language, который отправляется в HTTP-запросах, и частично на JavaScript-свойство navigator.languages.
К сожалению, подменить таймзону через user.js или about:config напрямую не получится — Firefox берёт её из системных настроек, и в стандартных настройках нет опции для её изменения. Однако подмена только локали без таймзоны, может выглядеть подозрительно, а нам это не надо. Потому расскажу, как можно подделать и таймзону и локаль, а именно через js.
К примеру, для нормальной смены локали и таймзоны можно написать плагин на Firefox. Моё расширение состоит из двух файлов: manifest.json и content.js.
Пример плагина:
manifest.json:
content.js:
content.js - этот скрипт делает всю работу, подменяя таймзону и локаль на уровне JavaScript. Он создаёт новый элемент script и вставляет его в документ, чтобы код выполнился сразу. Внутри я перехватываю метод Intl.DateTimeFormat.prototype.resolvedOptions, чтобы он возвращал поддельную таймзону, например, “Asia/Karachi”, и локаль, вроде “en-PK”. Также подменяю Date.prototype.getTimezoneOffset, задавая фиксированное смещение, скажем, -300 минут, чтобы соответствовать выбранной таймзоне. Плюс я переопределяю свойства navigator.language и navigator.languages, чтобы они возвращали поддельную локаль, делая подмену максимально убедительной.
Устанвливаем и проверяем как он работает:
Если не хочется писать своё расширение, таймзону в Firefox можно подменить с помощью готового плагина, например, Timezone Editor, который доступен по ссылке https://addons.mozilla.org/en-US/firefox/addon/timezoneeditor/.
Сначала устанавливаешь плагин через магазин дополнений Firefox. После установки находишь иконку Timezone Editor в панели инструментов браузера и кликаешь по ней:
Дальше открывается меню, где ты выбираешь нужную таймзону из списка, например, “Asia/Karachi” или любую другую, которая тебе подходит.
Плагин сразу подменяет таймзону, и сайты, которые проверяют твои настройки, будут видеть выбранное значение.
Для тех, кто хочет подменить таймзону и локаль в браузере без конкретного плагина для этого, я написал скрипт для Tampermonkey.
Пример скрипта:
Логика скрипта проста, функция в нём, создаёт элемент script и вставляет в него код injectedCode, чтобы подмена таймзоны и локали произошла на уровне страницы. Я задал поддельные значения: таймзона “Asia/Karachi”, смещение -300 минут (UTC+5) и локаль “en-PK”. Метод Date.prototype.getTimezoneOffset перехватывается, чтобы возвращать заданное смещение, а Intl.DateTimeFormat.prototype.resolvedOptions подменяется, выдавая фейковую таймзону и локаль. Это заставляет сайты видеть твои поддельные настройки времени и языка.
Подменять таймзоны и локали можно и на уровне системы перед запуском Firefox. Для этого написал простой bash-скрипт. Он позволяет временно переключить настройки на, скажем, японские, чтобы браузер и сайты видели тебя в другом регионе, а после завершения работы возвращает всё как было.
Вот сам код:
Проверяем как он работает:
Скрипт начинается с сохранения текущих системных настроек — таймзоны (через timedatectl) и локали (из переменной LANG), чтобы потом вернуть их на место. По умолчанию он берёт TZ из системы или, если не задано, запрашивает через timedatectl, а LANG ставит en_US.UTF-8. Затем я задаю желаемые значения: таймзону “Asia/Tokyo” и локаль “ja_JP.UTF-8” для японского региона.
Дальше скрипт проверяет, доступна ли нужная локаль через locale -a. Если её нет, он заглядывает в /etc/locale.gen, и, если там не прописана ja_JP.UTF-8, добавляет строку “ja_JP.UTF-8 UTF-8” и запускает locale-gen для генерации локали. Если что-то пошло не так, например, генерация не удалась, скрипт падает обратно на en_US.UTF-8, чтобы не сломать систему.
Когда локаль готова, код устанавливает переменные окружения TZ, LANG и LC_ALL на желаемые значения, чтобы Firefox подхватил их при запуске. Затем он вызывает Firefox с переданными аргументами, чтобы ты мог запустить браузер как обычно, но уже с подменёнными настройками. После завершения Firefox скрипт проверяет, успешно ли он отработал, и выводит сообщение об успехе или ошибке.
В финале всё возвращается на круги своя: TZ, LANG и LC_ALL восстанавливаются в исходные значения, а LC_ALL сбрасывается, чтобы не путать другие программы.
Подмена таймзоны через JavaScript (плагины или Tampermonkey) удобна, так как не требует изменения системных настроек и работает на уровне браузера. Однако сайты могут обнаружить несоответствие между JavaScript-таймзоной и системной (например, через HTTP-заголовки). Системная подмена через
Смена системного времени
Далее поговорим про подмену системного времени. Это можно нормально сделать через JavaScript или на уровне Linux, потому что в браузере(в about:config) таких функций нет. Тем не менее, системное время — это очень важный параметр, который может выдать твое реальное время. Поэтому его в любом случае необходимо подменять, чтобы не выдать твое местоположение.
Первым делом расскажу про смену системного времени на уровне js, а именно с помощью плагина.
manifest.json:
content.js:
Этот плагин подменяя объект Date в браузере. Он создаёт новый элемент script и вставляет в него код, который выполняется на уровне страницы. Внутри я сохраняю оригинальный объект Date в переменную OriginalDate, чтобы не сломать его функционал, и определяю новый FakeDate. Когда ты создаёшь новый объект Date без аргументов, FakeDate берёт текущее время, добавляет к нему 6 часов через setHours и возвращает новый объект на основе OriginalDate. Метод Date.now тоже перехватывается: он возвращает время с тем же сдвигом в 6 часов, чтобы window.Date.now() показывал поддельное значение. При этом Date.UTC и Date.parse остаются нетронутыми, чтобы не ломать парсинг дат и работу с UTC. После вставки скрипт добавляется в head или documentElement.
Проверяем работает ли он:
Еще для смены системного времени на уровне js можно использовать tampermonkey. Я написал небольшой скрипт для Tampermonkey, который добавляет 6 часов к текущему времени.
Сам код:
В скрипте я сохраняю оригинальный объект window.Date в переменную OriginalDate, чтобы не потерять его методы, и создаю новый FakeDate. Когда Date вызывается без аргументов, FakeDate берёт текущее время, добавляет 6 часов через setHours и возвращает новый объект на основе OriginalDate. Если переданы аргументы, например, строка или число, он передаёт их в OriginalDate без изменений, чтобы не сломать парсинг дат на сайтах. Метод FakeDate.now перехватывается, чтобы Date.now() возвращал время со сдвигом в 6 часов, сохраняя точность в миллисекундах. Методы Date.UTC и Date.parse остаются нетронутыми, чтобы не нарушать работу с UTC или строками дат. В конце window.Date заменяется на FakeDate, и скрипт выводит в консоль новое фейковое время для проверки.
Давайте проверим о смене системного времени средствами Linux. Когда речь заходит о подмене системного времени на уровне Linux, на помощь приходит утилита faketime — она позволяет обмануть программы, заставляя их видеть другое время, без возни с системными часами. Этот инструмент перехватывает системные вызовы, связанные с временем, и подсовывает фейковые значения, оставляя реальное время нетронутым. Как это работает? Faketime использует библиотеку libfaketime, которая внедряется в процесс программы и подменяет функции вроде time() или gettimeofday(), возвращая заданное тобой время. Это значит, что только выбранная программа, например, Firefox, видит поддельное время, а система продолжает работать как ни в чём не бывало.
Установить утилиту проще простого: достаточно выполнить команду:
Я написал лаконичный bash-скрипт, который сдвигает время на 8 часов вперёд и запускает Firefox в этом фейковом временном пузыре.
Вот как это выглядит:
Скрипт делает ровно то, что нужно: сначала он вычисляет новое время, прибавляя 8 часов к текущему, с помощью команды date -d "+8 hours", которая выдаёт результат в формате “год-месяц-день часы:минуты:секунды”. Это значение сохраняется в переменную NEW_TIME. Затем faketime берёт это время и запускает Firefox, заставляя браузер думать, что сейчас именно оно. Всё гениальное — просто!
Если ты хочешь подменить системное время на уровне Linux, но не желаешь устанавливать дополнительные утилиты вроде faketime, есть способ обойтись системными средствами. Можно просто задать переменную среды и с её помощью заставить Firefox, видеть нужное тебе время. Это работает через библиотеку libfaketime, которая уже есть в большинстве дистрибутивов, и я написал небольшой bash-скрипт, чтобы показать, как это работает.
Вот как выглядит код:
Скрипт использует переменную среды FAKETIME и LD_PRELOAD, чтобы внедрить библиотеку libfaketime и подменить время для Firefox. Библиотека перехватывает системные вызовы, связанные с временем, и возвращает фейковые значения только для запущенной программы, не трогая системные часы. Сначала скрипт он вычисляет новое время, прибавляя 4 часа к текущему, с помощью команды date -d "+4 hours", которая выдаёт дату в формате “год-месяц-день часы:минуты:секунды”. Это значение сохраняется в переменную NEW_TIME. Затем переменная FAKETIME задаёт поддельное время в формате @YYYY-MM-DD HH:MM:SS, а LD_PRELOAD указывает на библиотеку libfaketime.so.1, обычно лежащую в /usr/lib/x86_64-linux-gnu/. Когда Firefox запускается, libfaketime подменяет вызовы вроде time() или gettimeofday(), и браузер видит только фейковое время.
Смена координат
Теперь, наверное, ты, думаешь, что будет раздел про подмену координат в Firefox? Но его не будет. Ладно, шучу. Но некоторые нюансы в Firefox по поводу координат все же есть. Такое дело что в Firefox нельзя просто взять и получить координаты. И дело не кривых руках тех кто хочет это сделать, а в том, как Firefox устроен, особенно на Linux. Но давай разберемся почему это не работает?
Первым делом стоит заглянуть в about:config, где по умолчанию параметр geo.enabled стоит в false. Это он, отвечает за включение геолокации, и пока не переключишь его в true, браузер даже не подумает запрашивать твои координаты. Но вот загвоздка: даже когда я для теста установил geo.enabled в true, координаты всё равно не пришли.
Чтобы проверить, в чём дело, я написал простенький тестовый код, который должен был вытащить широту и долготу:
И что ты думаешь? Вместо координат я получил в консоли undefined с ошибкой “Unknown error acquiring position”. Ты можешь подумать, что я не разрешил доступ к геолокации, но нет — я честно нажал “Allow” в запросе браузера, так что дело не в этом.
Вот скриншот с сайта https://my-location.org/, где чётко видно, что я дал добро на сбор координат:
Так в чём же проблема? Я подозреваю, что на Linux это связано с особенностями сборки Firefox или с тем, как он взаимодействует с системой. В отличие от Chrome, который берет геолокацию через системные сервисы или API, Firefox, похоже, либо урезал эту функцию на уровне кода, либо требует каких-то дополнительных танцев с бубном. К примеру я читал что нужно установить параметр privacy.resistFingerprinting в false, ибо он может мешать получению координат, но даже после его установки я получал undefined вместо координат.
Возможно дело в том, что в некоторых сборках Firefox на Linux геолокация зависит от сервиса Mozilla Location Service (MLS), который может быть отключён или подобное. Плюс, Firefox вроде как находиться в песочнице, которая ограничивает доступ к системным API, включая геолокацию (но это больше актально для snap-версии).
Чтобы окончательно убедиться, я копнул в баг-трекер Mozilla и нашёл, что ошибка “Unknown error acquiring position” связана с кодом POSITION_UNAVAILABLE, который Firefox выдаёт, когда не может получить данные от бэкэнда геолокации. Причины? Либо подходящий провайдер геолокации отсутствует, либо сборка браузера имеет внутренние ограничения. На Linux, особенно в дистрибутивах, заточенных под приватность, вроде Fedora или Debian, дело осложняют дополнительные слои безопасности, такие как AppArmor или SELinux, которые могут блокировать доступ Firefox к нужным API. А вот в Chrome, лол, всё работает как по маслу, без таких проблем.
Но вот что интересно: когда я скачал Firefox из tar-архива с сайта Mozilla, геолокация вдруг заработала, как ни в чём не бывало! Это меня озадачило. Похоже, стандартная версия, устан
Как ни странно, когда я скачал Firefox прямо из tar-архива с сайта Mozilla, геолокация заработала как по часам, что меня, честно, удивило. Но вот парадокс: если взять стандартный Firefox, установленный через пакетный менеджер, например, apt в Debian, координаты не получить. Если тебе нужна геолокация, качай Firefox с официального сайта и распаковывай из tar-архива — это работает. А вот версия из apt, скорее всего, будет упираться из-за ограничений сборки или песочницы вроде snap. Так как получение координат все же где-то работает, я решил всё дать пару рекомендаций как это обойти.
Первым делом я решил написать плагин для подмены геолокации в Firefox, чтобы сайты видели фиксированные координаты, которые я сам задаю.
Вот сам плагин:
manifest.json:
content.js:
Проверяем:
На самом деле это было не так просто, ибо простая инъекция JavaScript не сработала — Firefox упорно не хотел подменять геолокацию. Заработало только прямое внедрение через DOM, и вот как это устроено. В content.js всё начинается с создания объекта fakePosition, где я задал фиксированные координаты — широту 58.2472 и долготу 8.0336, а также параметры вроде accuracy: 50 и timestamp: Date.now(), чтобы подмена выглядела правдоподобно. Затем я перехватываю методы navigator.geolocation.getCurrentPosition и navigator.geolocation.watchPosition. Первый просто вызывает переданную функцию success с фейковыми координатами, а второй ещё возвращает случайный ID, чтобы эмулировать отслеживание. Код оборачивается в строку scriptContent и вставляется в DOM через созданный элемент script, который добавляется в head или documentElement и сразу удаляется, чтобы не оставлять следов. Это позволяет подменить геолокацию на всех сайтах ещё до их загрузки, благодаря настройке run_at: document_start в manifest.json.
Если не хочешь возиться с написанием своего плагина для подмены геолокации, можно взять готовое решение, и из рабочих я нашёл Location Guard — отличный вариант, доступный по ссылке https://addons.mozilla.org/en-US/firefox/addon/location-guard/. Этот плагин позволяет легко подменить координаты, чтобы сайты видели тебя там, где ты хочешь.
После установки Location Guard геолокация не меняется мгновенно — у меня ничего не сдвинулось, пока я не залез в настройки плагина. Для этого кликаешь на иконку расширения, выбираешь Options и в разделе Default level ставишь Use fixed location.
Там же можно указать конкретные координаты, например, через карту, или выбрать готовую точку — по умолчанию это Гринвич, но ты вольёшь задать что угодно:
Чтобы проверить, сработала ли подмена, открываешь, к примеру https://www.gps-coordinates.net/my-location, который запрашивает геолокацию, и смотришь, какие координаты он показывает:
У меня всё заработало как надо после настройки, и сайты начали видеть фейковое местоположение.
На последок я решил показать, как написать скрипт для Tampermonkey, который подменяет геолокацию в Firefox, чтобы сайты видели фейковые координаты вместо твоих настоящих.
Сам код:
Проверяем работает ли скрипт:
Сначала в коде, Я задаю объект fakeCoords с фиксированными координатами: широта 58.2472, долгота 8.0336 и точность 100 метров, чтобы подмена выглядела правдоподобно. Затем через Object.defineProperty я переопределяю свойство navigator.geolocation, делая его неизменяемым (writable: false). Новый объект geolocation подменяет три метода: getCurrentPosition, watchPosition и clearWatch.
Метод getCurrentPosition вызывает переданную функцию callback, возвращая объект с фейковыми координатами и текущей временной меткой через Date.now(). Метод watchPosition делает то же самое, но возвращает фиксированный ID (1), эмулируя отслеживание. Метод clearWatch пустой, так как в нашей подмене нет реального отслеживания, но он нужен для совместимости. Это заставляет сайты, запрашивающие геолокацию, видеть заданные координаты, будто ты реально там находишься.
Скрипт работает, потому что он вмешивается в объект navigator.geolocation на уровне JavaScript, перехватывая запросы сайтов до того, как браузер обратится к системным API.
Но честно, я так и не понял, зачем в приватном браузере вроде Firefox вообще нужен доступ к геолокации - это же как оставить дверь нараспашку для трекеров. Поэтому проще всего заблокировать эту функцию раз и навсегда через user.js или about:config и жить спокойно, не парясь о том, что кто-то вычислит твоё местоположение. Всё, что нужно, создать или отредактировать файл user.js в профиле Firefox и прописать пару строк, которые вырубят геолокацию наглухо.
Вот содержимое моего user.js:
Что тут происходит? Параметр geo.enabled в false отключает доступ к геолокации, так что браузер даже не будет пытаться запрашивать координаты. geo.provider.network.url я оставил пустым, чтобы Firefox не обращался к сервисам вроде Mozilla Location Service, которые могут собирать данные о твоих Wi-Fi сетях. А privacy.resistFingerprinting в true включает защиту от фингерпринтинга, которая дополнительно искажает данные, чтобы сайтам было сложнее создать твой отпечаток.
Но если хочешь ещё больше спокойствия, мой совет — ставь Firefox через пакетный менеджер вроде apt, а не из tar-архива с сайта Mozilla. Почему? Пакетные версии, особенно в дистрибутивах вроде Ubuntu, часто идут через snap, который добавляет песочницу и может ломать геолокацию из-за своих ограничений. Это как бонус: трекеры точно не доберутся до твоих координат, даже если что-то пойдёт не по плану.
Смена локального IP
Когда мы разобрались со сменой местоположения. Давай разберёмся, как обстоят дела с подменой локального IP в Firefox, и почему это не так просто, как кажется. Если хочешь, чтобы сайты не получили твой реальный IP через ICE-кандидаты WebRTC, то тут Firefox тебя приятно удивит - по умолчанию он блокирует такие попытки, и я подозреваю, что это связано с его настройками приватности или особенностями сборки. А вот заголовок X-Forwarded-For, который иногда используют для определения IP, всё же можно подменить, и я расскажу, как это сделать.
Сначала почему не работает получение локального IP и WebRTC? ICE-кандидаты — это часть протокола WebRTC, который отвечает за установку соединений между устройствами. Они могут выдавать твой локальный IP, даже если ты сидишь за NAT или VPN, что для трекеров — как находка. Но в Firefox получить твой IP через WebRTC — задача не из лёгких. По умолчанию настройка media.peerconnection.enabled в about:config стоит в true, но Firefox активно защищает приватность. Например, с версии 68 он заменяет локальные IP в ICE-кандидатах на mDNS-имена, вроде 1f4712db-ea17-4bcf-a596-105139dfd8bf.local, что делает их бесполезными для сайтов. Плюс, если включён параметр privacy.resistFingerprinting (а в строгом режиме он активен), WebRTC вообще может быть ограничен или отключён.
Начнём говорить про подмену заголовка X-Forwarded-For, и для этого я написал плагин для Firefox, который заменяет его на случайный локальный IP. Вот как это устроено!
manifest.json:
content.js:
Это работает так: плагин состоит из двух файлов. В manifest.json я указал версию протокола 2, название “X-Forwarded-For Spoofer” и права на изменение веб-запросов через webRequest и webRequestBlocking для всех сайтов. Скрипт content.js работает как фоновый, чтобы перехватывать HTTP-запросы до их отправки.
В content.js функция generateRandomLocalIP создаёт случайный IP из диапазонов частных сетей: 192.168.0.0–255, 10.0.0.0–255 или 172.16.0.0–255, выбирая случайный диапазон и добавляя к нему число для последней части, например, 192.168.0.123. Обработчик browser.webRequest.onBeforeSendHeaders проверяет заголовки запроса: если X-Forwarded-For уже есть, он заменяет его значение на новый IP, если нет — добавляет заголовок с фейковым IP. Параметр "blocking" позволяет менять запросы до отправки, а "requestHeaders" даёт доступ к заголовкам.
Провяремя работет ли плагин на https://browserleaks.com/ip:
Если не хочешь писать свой плагин для подмены заголовков, можно взять готовое решение вроде Header Editor, доступного по ссылке https://addons.mozilla.org/en-US/firefox/addon/header-editor/. Этот плагин — не только для X-Forwarded-For, он позволяет менять любые HTTP-заголовки.
Header Editor работает как редактор правил для HTTP-запросов и ответов. После установки ты создаёшь правило, указываешь тип заголовка, его значение и условия, при которых подмена срабатывает. Плагин перехватывает запросы браузера до их отправки и применяет заданные изменения, позволяя подставить любой IP в X-Forwarded-For или даже создать новый заголовок. Всё настраивается через удобный интерфейс, без копания в коде.
Скачай плагин и кликни на его иконку в панели Firefox, затем выбери Manage.
Дальше нажми на “+”, чтобы создать новое правило для подмены заголовка:
Теперь время настроить конфигурацию плагина. В разделе Rule type выбери Modify request header — это нужно, чтобы создать или изменить заголовок запроса. В Match type поставь all, чтобы подмена работала для всех доменов. В Execute type выбери normal, чтобы правило применялось стандартно.
Затем в поле имени заголовка укажи x-forwarded-for (регистр не важен) и введи нужное значение, например, 192.168.1.100 или любой другой IP, который хочешь подставить:
После сохранения правила Header Editor начнёт подменять X-Forwarded-For для всех исходящих запросов. Это работает, потому что плагин использует API webRequest Firefox, которое позволяет перехватывать и модифицировать заголовки до их отправки на сервер. Но учти: если сайт проверяет IP другими способами, например, через серверные заголовки, подмена может не сработать.
Честно, я так и не решил, насколько хорошая идея постоянно подменять заголовок X-Forwarded-For. Сайты далеко не всегда его запрашивают, и, если ты везде пихаешь фейковый IP, это может выглядеть подозрительно — как будто ты слишком уж стараешься спрятаться. Но в некоторых случаях, когда нужно запутать трекеры или обойти проверки, такая подмена реально выручает. Главное — не злоупотребляй, комбинируй с другими мерами, и думай, где это уместно, чтобы не светиться лишний раз!
Подмена параметорв соединения
Дальше мог бы быть раздел про скрытие navigator.connection, но Mozilla опередила всех и просто не добавила этот API в Firefox, сославшись на соображения приватности и безопасности. Обсуждение этого решения можно найти в баг-трекере по ссылке https://bugzilla.mozilla.org/show_bug.cgi?id=960426, и я, честно, этому только рад. Firefox в очередной раз доказывает, что в плане защиты данных он на шаг впереди того же Chrome. Даже если в about:config включить параметр dom.netinfo.enabled в true, чтобы попытаться получить параметры сети, браузер всё равно выдаёт минимум информации.
Например, если запустить Firefox, установленный через пакетный менеджер в Linux, ты получишь что-то вроде:
А если взять версию Firefox прямо с сайта Mozilla, результат будет:
В обоих случаях никаких полезных данных о сети — ни скорости, ни типа подключения. Это значит, что параметры твоей сети остаются неизвестными для создания отпечатка, и трекеры не смогут получить из браузера ничего ценного.
Смена размера экрана
Теперь, когда мы разобрались с подменой сетевых данных, давай обсудим с тобой подмену размера экрана. К сожалению или к счастью, Firefox по умолчанию не скрывает эту информацию, и сайты могут легко получить параметры вроде screen.width и screen.height через JavaScript.
Сначала разберу, как скрыть размер экрана в Firefox с помощью about:config, а точнее, через более удобный вариант — файл user.js. Для начала создай или отредактируй файл user.js в профиле Firefox и пропиши туда несколько важных настроек.
Вот пример, который ты можешь использовать, или те же параметры можно выставить прямо в about:config:
Что тут происходит? Самое важное — privacy.resistFingerprinting в true. Эта настройка включает защиту от фингерпринтинга, которая не только скрывает точное разрешение экрана, но и округляет его до ближайших стандартных значений, вроде 1000x1000, чтобы твой отпечаток был менее уникальным. Параметры privacy.window.maxInnerWidth и privacy.window.maxInnerHeight задают максимальные размеры внутреннего окна браузера — я выбрал 900x600, чтобы имитировать небольшой экран, как у старого ноутбука. Это ограничивает, что сайты видят через window.innerWidth и window.innerHeight. А layout.css.devPixelsPerPx в -1.0 заставляет браузер использовать системную плотность пикселей, что помогает избежать утечек через CSS-метрики.
Так же можно написать плагин для подмены размера экрана в Firefox, чтобы сайты видели фейковые параметры разрешения вместо настоящих.
Пример плагина:
manifest.json:
content-script.js:
Плагин состоит из двух файлов: manifest.json и content-script.js, в manifest.json конфигурация, в content-script.js функции. Скрипт работает так: в content-script.js я создаю строку scriptContent, которая содержит код для подмены. Она превращает fakeScreen в Proxy, чтобы перехватывать обращения к свойствам объекта screen и возвращать только заданные значения, а для метода toString выдавать “[object Screen]” для правдоподобности. Затем через Object.defineProperty я заменяю window.screen на этот Proxy, а также фиксирую window.innerWidth, window.innerHeight и window.devicePixelRatio значениями 1920, 1080 и 1 соответственно, делая их неизменяемыми (writable: false). Код внедряется в страницу через элемент script, который добавляется в documentElement и сразу удаляется, чтобы не оставлять следов. Это работает, потому что подмена происходит на уровне JavaScript до загрузки страницы, и сайты видят фейковые параметры экрана, например, 1920x1080, вместо реальных.
Ещё можно создать скрипт для Tampermonkey как альтернативу плагину, чтобы подменить размер экрана в Firefoxк. Это подходит как способ обойтись без написания полноценного плагина, и при этом подменить размер экрана.
Пример скрипта:
Скрипт работает похоже на плагин: В заголовке указано, что скрипт запускается на всех URL (@match :///*) на этапе document-start, чтобы подмена произошла до загрузки страницы. В самом скрипте я создаю объект fakeScreen с фейковыми параметрами: разрешение 1920x1080, доступные размеры те же, глубина цвета 24 бита, нулевые отступы и ориентация landscape-primary, плюс методы mozLockOrientation и mozUnlockOrientation для совместимости с Firefox.
Объект fakeScreen оборачивается в Proxy, который перехватывает обращения к свойствам и возвращает заданные значения, а для toString выдаёт “[object Screen]” для правдоподобности. Затем через Object.defineProperty я заменяю window.screen на этот Proxy, а также фиксирую window.innerWidth, window.innerHeight и window.devicePixelRatio значениями 1920, 1080 и 1, делая их неизменяемыми (writable: false). Всё это обёрнуто в try-catch, чтобы поймать ошибки, если Firefox блокирует подмену, с выводом предупреждений в консоль. После применения подмены скрипт логирует результат. Это работает, потому что код вмешивается в JavaScript страницы, подменяя свойства экрана до их запроса сайтом.
Если ты хочешь отойти от JavaScript и подменить размер экрана в Firefox средствами Linux, покажу как с помщью bash скрипта запускать браузер с заданными параметрами окна, чтобы сайты видели фейковый размер вместо реального.
Пример кода:
Этот код просто запускает Firefox с флагами, задающими нужный размер окна. Сначала скрипт определяет переменную FIREFOX_BIN, указывая путь к исполняемому файлу браузера (обычно просто “firefox”). Затем я задаю ширину (WIDTH=800) и высоту (HEIGHT=600) окна, чтобы имитировать небольшой экран (но вы можете написать любой).
Дальше код ищет профиль Firefox с суффиксом .default-esr в папке ~/.mozilla/firefox, используя команду find с ограничением на первый уровень вложенности (-maxdepth 1). Если профиль не найден, скрипт выводит ошибку и завершается. Если всё ок, он печатает путь к профилю и запускает Firefox с несколькими флагами: --no-remote предотвращает подключение к уже открытому экземпляру браузера, --new-instance создаёт новый экземпляр, --width и --height задают размеры окна, а --profile указывает на найденный профиль, чтобы сохранить настройки.
Это работает, потому что Firefox позволяет управлять размерами окна через командную строку, и сайты, запрашивающие window.innerWidth и window.innerHeight, увидят заданные 800x600 вместо реальных значений. Но учти: это влияет только на размер окна, а не на window.screen.width/height, так что для полной подмены экрана комбинируй скрипт с настройками privacy.resistFingerprinting в about:config или плагином.
Чтобы полноценно подменить размер экрана в Firefox на Linux, я решил пойти дальше и запустить браузер в фейковом окне с помощью утилиты Weston.
Пример кода:
Проверяем:
Этот код работает с помощью утилиты Weston, которая нужна для создания виртуального дисплея в Linux. Weston — это композитор Wayland, но в данном случае он используется с X11-бэкендом (x11-backend.so), чтобы эмулировать дисплей с заданным разрешением. Это позволяет запустить Firefox в изолированном окружении, где сайты видят фейковый экран, например, 1024x576, вместо реального разрешения твоего монитора.
Сначала скрипт задаёт ширину (WIDTH=1024) и высоту (HEIGHT=576) фейкового дисплея. Затем он ищет профиль Firefox с суффиксом .default-esr в папке ~/.mozilla/firefox через команду find, ограниченную первым уровнем вложенности. Если профиль не найден, скрипт выводит ошибку и завершается. Если всё ок, он печатает путь к профилю.
Дальше запускается Weston с параметрами: --width и --height задают размер дисплея, --socket=wayland-fake создаёт уникальный сокет для Wayland, --backend=x11-backend.so указывает на X11-бэкенд, а --no-config отключает конфигурационный файл, чтобы использовать только командные параметры. Процесс Weston запускается в фоновом режиме (&), и его PID сохраняется в переменную WESTON_PID для последующего завершения. Пауза в 2 секунды через sleep даёт Westo времени инициализироваться.
Затем скрипт устанавливает переменную WAYLAND_DISPLAY=wayland-fake, чтобы Firefox подключился к фейковому дисплею. Переменная MOZ_ENABLE_WAYLAND=1 включает поддержку Wayland в Firefox, что необходимо для работы с Weston. Браузер запускается с флагом --no-remote, чтобы не цепляться к уже открытому экземпляру, и --profile, указывающим на найденный профиль. Запуск в фоновом режиме (&) позволяет скрипту дождаться завершения Firefox через wait. После этого процесс Weston завершается через kill.
Подделка плагинов
Чтож, пришло время рассказать про подделку плагинов в Firefox (ну, точнее, не совсем подделку, а скорее маскировку)
История с обнаружением плагинов сайтами в Firefox довольно занятная. В отличие от Chrome, где у плагинов есть фиксированные ID, в Firefox плагины не имеют постоянного id, по которому их легко вычислить. Есть объект navigator.plugins, но он выдаёт фейковые данные, вроде встроенного pdfviewer, который не несёт реальной информации. Раньше, до 2017–2019 годов, navigator.plugins показывал реальные плагины, работавшие через NPAPI. Но всё изменилось, сначала с версии Firefox 52(март 2017) Mozilla убрала поддержку NPAPI, кроме Flash, и navigator.plugins стал почти беспользным. С версии 85 (январь 2021) Flash тоже ушёл, и теперь объект содержит только встроенные модули, такие как PDF Viewer (PDF.js), Widevine (для Netflix) и иногда OpenH264 (кодек H.264). Эти "плагины" — фиктивные, для совместимости.
Ты, наверное, спросишь: как тогда сайты находят, например, AdBlock? А тут самое интересное — они делают это по косвенным признакам. Плагины часто оставляют следы, такие как библиотеки (например, blockADBlock.js для AdBlock) или изменения в DOM. К примеру, AdBlock вычисляют, проверяя наличие рекламных классов вроде .adsbox на странице. Если эти классы отсутствуют или скрыты, сайт делает вывод, что стоит блокировщик. А Tampermonkey могут засечь по вмешательствам в DOM, хотя это не всегда срабатывает из-за разных скриптов.
Но как же это скрыть? Для своего плагина можно ограничить доступ к ресурсам расширения, убрав их из web_accessible_resources в manifest.json.
Вот пример:
Либо, минимизируй следы в DOM: избегай компрометирующих названий файлов или стандартных путей, вроде content.js, и не используй очевидные библиотеки. Но, честно, полностью скрыть расширение вряд ли выйдет — сайты всё равно могут заметить косвенные изменения, вроде отсутствия рекламы или модификаций скриптов.
Ниже я покажу, как подменить объект navigator.plugins в JavaScript, но, если честно, не думаю, что это полезно(по крайней мере для новых версий Firefox). Такая подмена скорее сделает твой отпечаток более уникальным, чем наоборот, ведь сайты редко полагаются на этот объект в Firefox. Лучше сосредоточься на минимизации следов и настройках приватности. Например, включи privacy.resistFingerprinting, ведь он делает navigator.plugins одинаковым для всех, чтобы твой отпечаток был менее уникальным.
Первым делом расскажу про подмену объекта navigator.plugins с помощью Tampermonkey.
Пример кода:
Проверяем:
Этот код работает так: он запускается через Tampermonkey так чтобы подмена произошла до любых проверок сайтов. В самом скрипте я создаю массив fakePlugins с двумя фейковыми плагинами: “Fake Plugin” и “Another Fake Plugin”. У каждого есть свойства name, filename, description, version и методы item и namedItem, чтобы они выглядели как настоящие объекты Plugin. Затем я создаю fakePluginArray, который имитирует PluginArray с полями length, item, namedItem и пустым методом refresh. Через цикл плагины добавляются в fakePluginArray, чтобы их можно было получать по индексу, как в настоящем массиве.
В конце я использую Object.defineProperty, чтобы заменить navigator.plugins на fakePluginArray и сделать его неизменяемым (writable: false). Это работает, потому что подмена происходит до того, как сайты запрашивают данные, и они видят фейковый список вместо встроенных модулей, таких как PDF Viewer.
Ещё можно подменять объект navigator.plugins с помощью собственного плагина. Я написал плагин, которое делает чтобы показать, как работает подмена на его уровне. Погнали разбираться, как оно работает!
Вот пример плагина manifest.json:
content.js:
Весь функционал в content.js — там происходит подмена на уровне DOM. Первым делом я создаю строку spoofScript, которая содержит код для подмены. Она формирует массив fakePlugins с двумя фейковыми плагинами: “Fake Plugin” и “Another Fake Plugin”, каждый с полями name, filename, description, version и методами item и namedItem, чтобы имитировать объект Plugin. Затем создаётся fakePluginArray, эмулирующий PluginArray с полями length, item, namedItem и пустым refresh. Плагины добавляются в fakePluginArray через цикл для поддержки индексированного доступа.
Кроме того, я подменяю navigator.mimeTypes, создавая fakeMimeTypes с типами “application/x-fake-plugin” и “application/x-another-fake-plugin”, связанными с фейковыми плагинами. Объект fakeMimeTypeArray имитирует MimeTypeArray с полями length, item и namedItem. Через Object.defineProperty свойства navigator.plugins и navigator.mimeTypes заменяются на фейковые объекты, с writable: false для защиты от изменений. Код внедряется в DOM через элемент script, который добавляется в head или documentElement и сразу удаляется.
Это работает, потому что подмена происходит до запросов сайтов, и они видят фейковый список плагинов и MIME-типов вместо настоящих.
Подмена косвенных параметров Firefox
Теперь давай разберёмся с подменой косвенных параметров в Firefox, а именно navigator.buildID, navigator.product и InstallTrigger. Эти штуки могут выдать, что ты сидишь в Firefox, а я хочу, чтобы сайты думали, будто ты используешь Chrome.
В Firefox navigator.buildID возвращает идентификатор сборки браузера, например, дату вроде “20250625000000”, что сразу кричит: “Я Firefox!”. В Chrome этого свойства вообще нет, так что его наличие — красный флаг для трекеров. Параметр navigator.product обычно возвращает пустое знчение в Firefox, а в Chrome — “Gecko”. А InstallTrigger — это объект, уникальный для Firefox, который позволяет устанавливать расширения через скрипты, и в Chrome его тоже нет. Если сайт проверяет эти параметры, он легко вычислит твой браузер.
Чтобы подделать эти параметры под Chrome, я написал скрипт для Tampermonkey, который перехватывает их через JavaScript.
Вот как это выглядит:
Проверяем:
Скрипт использует Object.defineProperty для переопределения трёх параметров. Сначала navigator.buildID задаётся как undefined, поскольку в Chrome этого свойства нет, а в Firefox оно выдаёт дату сборки, вроде “20250625000000”. Затем navigator.product меняется на “Gecko”, чтобы имитировать Chrome. Наконец, window.InstallTrigger, уникальный объект Firefox для установки расширений, устанавливается в false, чтобы сайты не видели его — в Chrome такого свойства нет. Все свойства делаются неизменяемыми (writable: false), чтобы предотвратить их модификацию сайтом. Это работает, потому что скрипт вмешивается в JavaScript страницы, подменяя параметры до их запроса.
Еще эти параметры можно подменять на уровне плагинов. Я решил написать расширение для Firefox, которое подменяет параметры navigator.buildID, navigator.product, navigator.vendor и InstallTrigger.
Вот сам плагин:
manifest.json:
content.js:
Плагин состоит из двух файлов: manifest.json и content.js. В manifest.json я указал версию протокола 2, название “Spoof Chrome in Firefox” и права на доступ к активной вкладке и всем сайтам (<all_urls>).
Скрипт content.js запускается на этапе document_start. Код ы нем пытается заменить window.navigator на Proxy через Object.defineProperty, делая его неизменяемым (writable: false). Если это не срабатывает, он подменяет каждое свойство по отдельности. Затем window.InstallTrigger устанавливается в false, чтобы убрать следы Firefox, а window.chrome добавляется как пустой объект с полями runtime и webstore для большей правдоподобности. Наконец, window.Navigator переопределяется, чтобы возвращать spoofedNavigator. Всё обёрнуто в try-catch, чтобы избежать сбоев, если Firefox заблокирует подмену. Скрипт внедряется через элемент script, который добавляется в head или documentElement и сразу удаляется.
Ещё один вариант - скачать Firefox с https://ftp.mozilla.org/pub/firefox/releases/, и ходит слух, что в таких версиях navigator.buildID якобы отсутствует, но это не совсем правда.
Скачивая Firefox с официального FTP-архива Mozilla, ты можешь взять старую версию, и в некоторых из них buildID действительно не возвращался через navigator.buildID — просто undefined, как в Chrome. Это могло помочь маскировать браузер, ведь buildID в Firefox выдаёт дату сборки, например, “20250625000000”, что сразу обноруживает, что ты не на Chrome. Но вот подвох: в новых версиях Firefox, даже если качаешь с FTP, buildID всё равно есть, и подмена требует дополнительных телодвижений, вроде тех что я описал выше.
Подмена системных параметров
Чтож, давай поговорим про подмену параметров не самого браузера, а нашей системы, которые Firefox выдаёт через объект navigator. Речь о navigator.oscpu, который показывает платформу, hardwareConcurrency, раскрывающий количество ядер процессора, platform, тоже указывающий на систему, и deviceMemory, который в Firefox по умолчанию заблокирован. Но вот парадокс: отсутствие deviceMemory само по себе может выдать, что ты сидишь в Firefox, что для трекеров как красная тряпка. Поэтому я считаю, что лучше подделать все эти параметры, чтобы они выглядели, как будто ты на стандартной Windows-машине, — так твой отпечаток станет менее уникальным.
Первым делом расскажу про то, как подменить параметры системы в Firefox через about:config - да, не удивляйся, такая возможность есть, и в этом плане Firefox реально молодец. В about:config можно подправить всё, что касается информации об ОС, а именно navigator.oscpu и navigator.platform, чтобы сайты видели фейковые данные. Для настройки я использую файл user.js в профиле Firefox, который позволяет задать параметры автоматически.
Вот пример содержимого user.js:
Тут я меняю параметры так, чтобы они указывали на Windows. Настройка general.oscpu.override задаёт значение для navigator.oscpu, подменяя его на “Windows NT 10.0; Win64; x64”, что имитирует современную 64-битную Windows 10 или 11. Параметр general.platform.override меняет navigator.platform на “Win32”, как это часто выглядит в Windows-системах. Это работает на уровне исходного кода Firefox: браузер перехватывает запросы к этим свойствам и возвращает заданные значения вместо реальных данных о твоей ОС, например, “Linux x86_64”.
Ну и, конечно, стандартная подмена через about:config не даёт полной маскировки, так что решил я показать как написать плагин для Firefox, который подменяет системные параметры navigator, чтобы сайты видели фейковые данные вместо настоящих.
Пример плагина:
manifest.json:
content.js:
Проверяем:
Этот плагин работает, заменяя значения navigator на фейковые, чтобы сайты видели тебя как пользователя Windows с типичными характеристиками. В manifest.json я указал версию протокола 2, название “Navigator Spoofer” и права на доступ к активной вкладке и всем сайтам (<all_urls>). Скрипт content.js запускается на этапе document_start, чтобы подмена произошла до любых запросов со стороны сайта.
В content.js я создаю элемент script и внедряю в него код, который выполняется на уровне страницы. Он использует Object.defineProperty для переопределения четырёх свойств navigator: oscpu задаётся как “Windows NT 10.0; Win64; x64”, чтобы имитировать 64-битную Windows, hardwareConcurrency возвращает 6, как у среднего процессора, platform устанавливается в “Win32” для соответствия Windows, а deviceMemory выдаёт 8 (гигабайт), как в Chrome, чтобы скрыть отсутствие этого параметра в Firefox. Все свойства задаются через геттеры с configurable: true, чтобы их можно было подменить, но без возможности изменения сайтом (хотя writable не указан, так как это геттеры).
Другой способ подменить системные параметры без написания полноценного плагина — использовать Tampermonkey. Это удобный метод, если ты хочешь быстро подделать данные вроде navigator.oscpu, hardwareConcurrency, platform и deviceMemory.
Пример кoдa:
Сам код в цикле подменяет параметры navigator, чтобы сайты видели фейковые данные, будто ты на Windows. Скрипт работает через Tampermonkey и используй базовую структуру скрипта из раздела "Инструменты для подмены отпечатков". Внутри я определил функцию spoofProperty, которая подменяет одно свойство объекта navigator. Она проверяет, не заморожен ли объект, удаляет старое свойство, если оно есть, и задаёт новое через Object.defineProperty с геттером, возвращающим фейковое значение. Функция возвращает true при успехе или false, если подмена не удалась. Объект spoofs содержит фейковые значения, цикл проходит по этим свойствам, вызывая spoofProperty для каждого.
Затем я создаю Proxy для navigator, который перехватывает обращения к свойствам и возвращает фейковые значения из spoofs, если они есть, или оригинальные через Reflect.get. Прокси задаётся как window.navigator через Object.defineProperty, чтобы заменить весь объект, но это обёрнуто в try-catch на случай, если Firefox заблокирует подмену.
Подмена данных о видеокарте
Теперь я расскажу, как подделать WebGL Vendor и Renderer в Firefox — параметры, которые выдают информацию о твоей видеокарте и её драйвере, буквально крича сайтам, какое у тебя железо. Это чуть ли не 100% уникальный отпечаток, так что их подмена — важный шаг, чтобы запутать трекеры.
Тут Firefox со мной согласен, что параметры WebGL Vendor и Renderer — это не то, что стоит оставлять без изменения, ведь они выдают данные о твоей видеокарте, такие как производитель и модель. Поэтому в about:config Mozilla добавила настройки для их подмены.
Вот пример подмены в user.js:
Тут я меняю параметры на NVIDIA, чтобы сайты видели фейковую видеокарту. Настройка webgl.override-unmasked-vendor задаёт WebGL Vendor как “NVIDIA Corporation”, а webgl.override-unmasked-renderer подменяет Renderer на “GeForce GTX 1060/PCIe/SSE2”, имитируя популярную видеокарту. Это работает на уровне исходного кода Firefox: когда сайт запрашивает данные через WEBGL_debug_renderer_info, браузер возвращает эти фейковые значения вместо реальных. Однако многие сайты используют gl.getParameter(gl.VENDOR) и gl.getParameter(gl.RENDERER), которые требуют подмены через плагин или Tampermonkey, как описано ниже.
Ещё есть нюанс: даже с подменой WebGL трекеры могут пытаться собирать другие данные, например, через рендеринг. Поэтому для полной защиты можно вообще вырубить WebGL через webgl.disabled, если графика на сайтах тебе не нужна.
Ещё можно написать плагин для подмены параметров WebGL Vendor и Renderer, чтобы сайты не могли вычислить твою видеокарту и драйвер. Я написал плагин для примера, погнали разбираться, как мой код это делает!
Сам плагин:
manifest.json:
content.js:
Проверяем:
Плагин работает так, что подменяет WebGL Vendor и Renderer, чтобы сайты видели фейковые данные вместо реальных характеристик видеокарты. Скрипт content.js запускается до загрузки страницы, чтобы подмена произошла до любых WebGL-запросов. В content.js я сначала проверяю, поддерживается ли WebGLRenderingContext, и, если нет, скрипт завершается с предупреждением. Функция createSpoofingScript создаёт элемент script и внедряет код, который выполняется на уровне страницы. Внутри я определяю функцию spoofWebGL, которая подменяет методы getParameter и getExtension для WebGLRenderingContext и WebGL2RenderingContext. Для getParameter я перехватываю параметры 0x9245 (GL_VENDOR) и 0x9246 (GL_RENDERER), возвращая “Spoofed 111” для обоих, а остальные запросы передаю оригинальному методу. Для getExtension я подменяю WEBGL_debug_renderer_info, возвращая объект с фейковыми значениями для Vendor и Renderer. Также я перехватываю HTMLCanvasElement.prototype.getContext, чтобы подменять WebGL-контексты (webgl и webgl2) при их создании. Код внедряется в head или documentElement, а если DOM ещё не готов, ждёт события DOMContentLoaded.
Подобный скрипт можно написать без плагина, используя Tampermonkey — это проще и не требует лишних импортов, а результат почти тот же.
Пример кода:
Этот код работает почти аналогично плагину, но есть некоторые различия. Скрипт запускается через Tampermonkey на всех сайта на этапе document-start, чтобы подмена произошла до любых WebGL-запросов.
Сначала скрипт проверяет наличие WebGLRenderingContext, и если WebGL не поддерживается, завершается с предупреждением. Функция spoofWebGL подменяет методы getParameter и getExtension для WebGLRenderingContext и WebGL2RenderingContext. Для getParameter код перехватывает параметры 0x9245 (UNMASKED_VENDOR_WEBGL) и 0x9246 (UNMASKED_RENDERER_WEBGL), возвращая “Spoofed Vendor” и “Spoofed Renderer”, а остальные запросы передаёт оригинальному методу. Если параметр невалиден, возвращается безопасное значение. Для getExtension скрипт подменяет WEBGL_debug_renderer_info, создавая объект с фейковыми значениями Vendor и Renderer.
Также я перехватываю HTMLCanvasElement.prototype.getContext, чтобы подменять WebGL-контексты (webgl и webgl2) при их создании. Все действия обёрнуты в try-catch, чтобы ловить ошибки и выводить их в консоль. В отличие от плагина, здесь не используется внедрение через DOM-элемент script, а подмена применяется напрямую к прототипам, что делает код компактнее, но менее изолированным. Это работает, потому что скрипт вмешивается в WebGL API до запросов сайтов, возвращая фейковые данные видеокарты.
Подмена производителей
Дальше разберу, как подменить параметры navigator.productSub, navigator.vendorSub и navigator.vendor в Firefox, чтобы сайты не могли вычислить, что ты используешь именно этот браузер. Эти свойства выдают информацию о производителе и версии браузера.
Эти данные можно подменить через about:config, ну точнее не все, а конкретно navigator.productSub и navigator.vendorSub, чтобы сайты не вычислили, что ты сидишь в Firefox. Эти параметры выдают информацию о версии и сборке браузера, что трекеры используют для твоего цифрового отпечатка. Погнали разбираться, как их подделать через user.js!
Пример в user.js:
Тут я меняю параметры, чтобы запутать трекеры. Настройка general.productSub.override задаёт navigator.productSub значение “20200101”, имитируя старую версию браузера, как в некоторых сборках Chrome. Параметр general.vendorSub.override я оставил пустым (“”), что соответствует поведению Chrome, где vendorSub обычно не возвращает ничего. К сожалению, navigator.vendor через about:config подменить нельзя, так что для него нужен скрипт или плагин.
Как пример, вот как написать плагин для подмены всех параметров navigator — productSub, vendorSub и vendor — прямо перед загрузкой страницы в DOM, чтобы сайты видели фейковые данные вместо настоящих.
Пример плагина:
manifest.json:
content.js:
Проверяем:
Этот плагин загружается до загрузки сайтов и меняет объекты JavaScript параметров. В content.js я создаю элемент script и внедряю в него код, который выполняется на уровне страницы. Он использует Object.defineProperty для подмены трёх свойств navigator: productSub задаётся как “20200101”, имитируя старую версию браузера, как в некоторых сборках Chrome, vendorSub устанавливается в пустую строку, как в Chrome, а vendor меняется на “Custom Vendor” для примера. Свойства делаются неизменяемыми (writable: false, configurable: false), чтобы сайты не могли их подправить.
Последний скрипт, который я покажу, — это пример для Tampermonkey, который подменяет параметры navigator.productSub, navigator.vendorSub и navigator.vendor.
Пример скрипта:
Этот код меняет параметры navigator, чтобы сайты видели фейковые данные. Внутри функции я использую Object.defineProperty для подмены трёх свойств navigator. Параметр productSub задаётся как “20200101”. Свойство vendorSub устанавливается в пустую строк. А vendor меняется на “Google Inc”, как в Chrome, вместо “” в Firefox. Все свойства делаются неизменяемыми (writable: false), чтобы сайты не могли их подправить. Это работает, потому что скрипт вмешивается в JavaScript страницы до её загрузки, подменяя значения navigator.
Подмена медиа-устройств
Теперь давай поговорим про подмену медиа-устройств - названий камер, микрофонов и прочих девайсов, которые сайты могут получить через API вроде navigator.mediaDevices.enumerateDevices(). Эти данные легко становятся частью твоего отпечатка, ведь названия устройств, такие как “Logitech HD Webcam” или “Realtek Audio”, часто уникальны.
Первым я покажу, как написать плагин для подмены названий медиа-устройств, таких как камеры и микрофоны, через подмену navigator.mediaDevices в Firefox.
Сам пример плагна:
manifest.json:
content.js:
Код работает через загрузку в DOM и подмену значений. В content.js функция createSpoofingScript создаёт элемент script и внедряет код, который перехватывает метод navigator.mediaDevices.enumerateDevices. Он вызывает оригинальный метод, получает список устройств и подменяет их label: для видеоустройств — “Fake Camera”, для аудиовходов — “Fake Microphone”, для аудиовыходов — “Fake Speaker”. Остальные свойства, вроде deviceId и groupId, остаются нетронутыми, а метод toJSON обеспечивает корректный вывод при сериализации.Функция waitForDOM проверяет, готов ли DOM (head или documentElement), и внедряет скрипт, иначе ждёт с интервалом 10 мс.
Но ещё проще подменять названия медиа-устройств, таких как камеры и микрофоны, можно через Tampermonkey — это более лёгкий способ без написания полноценного плагина.
Пример кода:
Проверяем:
Этот код работает через перехват метода navigator.mediaDevices.enumerateDevices. Внутри функции я сохраняю оригинальный метод enumerateDevices, чтобы вызвать его для получения списка устройств. Затем метод подменяется асинхронной функцией, которая берёт реальные устройства, но меняет их label: для видеоустройств — “Spoofed Camera”, для аудиовходов — “Spoofed Mic”, для аудиовыходов — “Spoofed Speaker”. Остальные свойства, вроде deviceId и groupId, остаются нетронутыми, а метод toJSON обеспечивает корректный вывод при сериализации.
Но вообще самое надёжное решение, чтобы не возиться с подменой медиа-устройств, - это отключить WebRTC и доступ к камерам и микрофонам прямо в about:config.
Самый жёсткий вариант — вырубить WebRTC полностью через настройку media.peerconnection.enabled, установив её в false, а также проверьте media.peerconnection.ice.enabled и media.peerconnection.ice.default_address_only, установив их в false и true соответственно. Это отключает весь функционал WebRTC, включая ICE-кандидаты, которые могут выдать твой локальный IP, и доступ к камерам и микрофонам через этот протокол. Если не хочешь так радикально или не можешь отключить WebRTC из-за каких-то нужд, можно просто заблокировать доступ к медиа-устройствам через media.navigator.enabled, поставив в false. Это выключает API navigator.mediaDevices, и сайты не смогут запросить список твоих камер, микрофонов или колонок.
Ещё один вариант — отключить доступ к аппаратным ключам через media.hardwaremediakeys.enabled, установив в false. Это ограничивает взаимодействие с медиа-устройствами на уровне аппаратных интерфейсов, что дополнительно снижает риск утечки данных.
Подмена статуса батареи и геймпадов
Напоследок расскажу про подделку статуса батареи и списка геймпадов в Firefox, чтобы сайты не могли использовать эти данные для твоего отпечатка.
Сначала про батарею. Если в Firefox выполнить console.log(navigator.getBattery()), ты получишь ошибку: Uncaught TypeError: navigator.getBattery is not a function, а console.log(typeof navigator.getBattery) выдаст undefined. Это потому, что Battery Status API, который позволял сайтам видеть уровень заряда и время работы батареи, был удалён из Firefox начиная с версии 52 (март 2017). Причина — приватность: этот API давал трекерам слишком много данных для идентификации пользователей, так как уровень заряда батареи мог быть уникальным маркером. В Chrome, однако, Battery Status API всё ещё доступен, и сайты могут запрашивать эти данные. Отсутствие navigator.getBattery в Firefox может выдать твой браузер, так что подделать его — дело полезное. А так же в некоторых кастомных или старых сборках (например, Firefox ESR) API может быть включён через настройки, так что стоит проверить это в about:config (dom.battery.enabled) если у вас такая версия.
С геймпадами тоже не всё гладко. В Firefox их можно достать через navigator.getGamepads(), и это странно: батарею посчитали “нарушением приватности”, а уникальные геймпады — “ну ок, это совсем не нарушает приватность”. Но сайты могут использовать их для фингерпринтинга, ведь модель геймпада — штука довольно уникальная.
Чтобы этого избежать, я написал скрипт для Tampermonkey, который подменяет и Battery Status API, и данные геймпадов.
Пример кода:
Этот код работает так: Сначала я создаю фейковый геймпад с id “Fake Gamepad (Vendor: 1234 Product: 5678)”, стандартным маппингом, четырьмя нулевыми осями и 17 кнопками в выключенном состоянии. Метод navigator.getGamepads перехватывается через Object.defineProperty и возвращает массив с этим геймпадом и тремя null, как в типичном Chrome. Я также создаю событие gamepadconnected, которое отправляется через window.dispatchEvent через 100 мс, чтобы имитировать подключение геймпада.
Для батареи я задаю фейковый объект fakeBattery: батарея заряжается (charging: true), уровень заряда 75% (level: 0.75), время зарядки 3600 секунд, разрядки — 7200 секунд. Метод navigator.getBattery подменяется, чтобы возвращать Promise с этим объектом, как в Chrome. Пустые методы addEventListener, removeEventListener и dispatchEvent добавлены для совместимости. Через setTimeout я вывожу в консоль фейковые данные для отладки.
Ещё можно написать плагин для подмены статуса батареи и геймпадов. Я покажу, как это сделать, чтобы всё выглядело, как будто ты на Chrome.
Пример плагина:
manifest.json:
content.js:
Проверяем:
Этот плагин работает на этапе DOM, до загрузки страницы. Первым делом в content.js я создаю элемент script и внедряю в него код для подмены. Функция spoofProperty использует Object.defineProperty для безопасной замены свойств navigator. Для геймпадов создаётся объект fakeGamepad с id “Fake Gamepad (Vendor: 1234 Product: 5678)”, стандартным маппингом, четырьмя нулевыми осями и 17 кнопками в выключенном состоянии. Метод getGamepads подменяется, возвращая массив с одним фейковым геймпадом и тремя null, как в Chrome. Событие gamepadconnected отправляется через 100 мс, чтобы имитировать подключение.
Для батареи создаётся объект fakeBattery: батарея заряжается (charging: true), уровень 75% (level: 0.75), время зарядки 3600 секунд, разрядки — 7200 секунд. Метод getBattery подменяется, возвращая Promise с этим объектом. Если getBattery уже есть, он перезаписывается; если нет, добавляется с предупреждением. Код внедряется в head или documentElement и удаляется после выполнения.
Практические инструменты для смены отпечатка
Вот я и закончил рассказывать про методы подделки отпечатка, и теперь пора собрать всё воедино. Этот раздел будет практическим и разделится на две части. Сперва я покажу полную конфигурацию about:config, которую я собрал на основе всех примеров, что мы обсуждали, чтобы замаскировать твой браузер под типичный Chrome на Windows. После я расскажу как написать скрипт для Tampermonkey для поддлеки отпчатка на уровне js.
Смена отпечатка в user.js
Настройки user.js работают на уровне исходного кода Firefox, напрямую меняя поведение движка браузера. Они применяются автоматически при запуске, переписывая внутренние параметры, и сайты не могут их обойти через JavaScript или другие уловки.
Вот полная конфигурация для user.js, которую я написал для примера:
Тут подделывается всё, что можно. Настройка privacy.resistFingerprinting включена, чтобы сделать параметры вроде списка плагинов, экрана, часового пояса и количества ядер такими же, как у миллионов других. Для защиты canvas включены autoDeclineNoUserInputCanvasPrompts, чтобы блокировать автоматические запросы, и randomDataOnCanvasExtract, чтобы мешать трекерам шумом в данных рендеринга.
Система притворяется Windows 10 через general.oscpu.override и general.platform.override, а через general.useragent.override подделывается Chrome 130, чтобы сайты думали, что ты используешь популярный браузер. Параметры general.productSub.override, general.vendorSub.override и general.buildID.override убирают уникальные метки Firefox, делая его похожим на старую версию Chrome. Язык установлен en-US через intl.accept_languages и javascript.use_us_english_locale, чтобы соответствовать обычной локали Windows.
WebGL подменяется на NVIDIA GTX 1060 через webgl.override-unmasked-vendor и webgl.override-unmasked-renderer, а WebGL2 отключён через webgl.enable-webgl2, чтобы меньше данных о видеокарте уходило. Поля webgl.renderer-string-override и webgl.vendor-string-override пустые, чтобы не выдавать лишнего. Шрифты установлены стандартные для Windows — Georgia, Helvetica, Courier New, с фиксированными размерами 18 и минимум 12 через font.*, чтобы твой набор шрифтов не выделялся.
Сбор данных Mozilla полностью отключён через toolkit.telemetry., datareporting. и breakpad.reportURL, чтобы браузер ничего не отправлял. Камеры и микрофоны блокируются через media.devices.enumerate.blocked и media.navigator.device.enabled, чтобы сайты их не видели. WebRTC закомментирован через media.peerconnection.*, так как его отключение может выглядеть странно и ломать некотрые другие поддлеки данных через WebRTC, а так же данные сайтов. Батарея и геймпады отключены через dom.battery.enabled и dom.gamepad.enabled, чтобы их не видели. Количество ядер процессора и память подменяются на 2 ядра и 4 ГБ через dom.maxHardwareConcurrency и dom.maxPhysicalMemoryMB, чтобы не выдавать мощное оборудование.
Таймеры ограничены до 100 мс через privacy.reduceTimerPrecision и reduceTimerPrecision.jitter, а точные таймеры выключены через dom.event.highres-timestamp.enabled, чтобы сайты не могли отслеживать время работы скриптов. Web Audio заблокирован через dom.webaudio.enabled, а canvas дополнительно защищён через privacy.canvas.poisondata. Защита от трекинга работает через privacy.trackingprotection.*, а куки ограничены через network.cookie.cookieBehavior, чтобы принимать только нужные для работы сайта.
Сетевые утечки блокируются через network.proxy.socks_remote_dns, network.trr.mode (отключение DoH), network.dns.disablePrefetch, network.prefetch-next и network.predictor.enabled, чтобы сайты не видели DNS-запросы и не предугадывали твои действия. Разрешение экрана фиксируется на 1000x1000 через privacy.window.maxInnerWidth и maxInnerHeight, а DPI и пиксели установлены стандартными через layout.css.dpi и layout.css.devPixelsPerPx. Плагины блокируются через plugin.scan.plid.all, plugin.state.flash и plugins.enumerable_names, чтобы список плагинов ничего не выдавал.
Также отключены буфер обмена, локальное хранилище, push-уведомления, геолокация, VR, вибрация, сенсоры и веб-уведомления через dom.* и geo.enabled, чтобы сайты до них не добрались. Кэш на диске выключен через browser.cache.disk.enable, автозаполнение форм и паролей — через browser.formfill.enable и signon.rememberSignons, а Pocket и безопасный просмотр отключены через extensions.pocket.enabled и browser.safebrowsing.enabled. Поисковые подсказки и IDN-пуникод тоже выключены, чтобы не оставлять следов.
Как по мне получился неплохой пример конфигурации about:config , хотя возможности user.js и не могут подделать абсолютно всё. Он они закрывает кучу дыр, через которые сайты могут тебя отслеживать: юзер агент, платформу, шрифты, видеокарту, блокирует камеры, микрофоны и всякие лишние данные. Но будь осторожен: слишком много подмен может выделить тебя, если сайты проверяют редкие комбинаци.
Автоматизация копирования user.js
Возможно, вам может быть неудобно копировать user.js в каждый профиль Firefox вручную, особенно если их несколько. Поэтому я написал простенький bash-скрипт, который автоматически копирует user.js во все профили Firefox текущего пользователя.
Мой код:
Проверяем:
Как видно, всё копируется нормально. Скрипт работает следующим образом: он определяет текущего пользователя с помощью команды whoami, затем ищет все профили Firefox в директории /home/$USER/.mozilla/firefox/, фильтруя папки по слову "default" в их названии. Найденные профили обрабатываются в цикле, где файл user.js из текущей директории копируется в каждый профиль. После каждой попытки копирования скрипт проверяет успешность операции и выводит соответствующее сообщение.
Инструмент для Tampermonkey
Просто менять настройки Firefox часто не хватает. Некоторые вещи, вроде часового пояса, системного времени или размера экрана, можно нормально подделать только через JavaScript. Поэтому я создал инструмент, который делает подделку под Chrome Windows. Для этого я скрипт в Tampermonkey, в котором вверху находятся все настройки с фейковыми параметрами, а ниже — модули, каждый из которых отвечает за подделку конкретной части отпечатка. Давайте разберём, как устроен этот скрипт, какие методы подделки я использовал и как они работаю!
Архитектура скрипта построена вокруг модульной системы: объект CONFIG хранит все фейковые значения, а отдельные модули (FontSpoofer, TimezoneSpoofer, AudioSpoofer, WebGLSpoofer, CanvasSpoofer, DeviceSpoofer) отвечают за подмену конкретных данных. Основные методы подделки я взял из прошлых наработок, но добавил новые, подходы для WebGL и canvas. Главынй модуль скрипта это, safeSpoofPropert, она подменяет свойства объектов через Object.defineProperty, проверяя, не заморожен ли объект, и обрабатывает ошибки. Но для понятности давайте разберём мой код по частям.
Скрипт начинается с объекта CONFIG, который задаёт фейковые значения для всего:
Все значения в конфиге выбраны максимально распространёнными, чтобы Firefox выглядел как типичный Chrome на Windows. Модули используют safeSpoofProperty, которая перехватывает свойства объектов, делая их неизменяемыми, но с возможностью конфигурации, чтобы не ломать функционал сайтов. Инициализация происходит через функцию init(), которая вызывает все модули в нужном порядке.
Теперь расскажу про модули моего скрипта.
Первым я расскажу про модуль подделки шрифтов:
Модуль FontSpoofer подменяет шрифты, чтобы сайты видели только популярные, вроде Arial или Times New Roman. Метод applyStyles внедряет CSS, принудительно задавая шрифты для всех элементов и моноширинных полей, таких как код или текстовые поля. Метод spoofFontFace перехватывает объект FontFace, заменяя любые шрифты на Arial, если они не из списка популярных. Метод spoofFontCheck подменяет document.fonts.check, чтобы сайты видели только разрешённые шрифты.
Следующим идёт модуль для подмены времени:
Модуль TimezoneSpoofer подделывает таймзону и язык. Он перехватывает Intl.DateTimeFormat.prototype.resolvedOptions, возвращая стандартные значения, и корректирует время в Date, чтобы соответствовать фейковой таймзоне.
Так же я написал модуль для подделки отпечатка на основе звука:
Модуль AudioSpoofer подменяет параметры AudioContext, задавая стандартные значения для обработки звука, чтобы сайты не могли создать отпечаток на основе аудио-рендеринга.
Далее я написал модуль для подделки графики, он в целом работает почти так же, как в примерах выше:
Модуль WebGLSpoofer перехватывает методы getParameter и getExtension в WebGL-контекстах, включая WebGL2, возвращая фейковые значения для видеокарты, чтобы сайты видели только распространённую графику.
Из относительно нового я добавил модуль для подделки Canvas, методы из которого я не описывал в статье выше, чтобы не затягивать повествование:
Этот модуль добавляет случайный шум в данные canvas через методы getImageData и toDataURL, изменяя значения пикселей (красный, зелёный, синий) на ±10. Это делает отпечаток canvas нестабильным при каждом запросе, ломая попытки сайтов создать постоянный идентификатор на основе рендеринга. Если подробнее, работа модуля начинается с метода init, который перехватывает вызов getContext для всех canvas-элементов на странице, применяя модификации к контекстам 2D, WebGL и WebGL2. Метод spoofCanvasContext оборачивает оригинальные методы getImageData и toDataURL, добавляя шум к данным пикселей через функцию applyNoise. Для getImageData шум применяется напрямую к данным изображения, а для toDataURL создаётся временный canvas, куда копируется и модифицируется изображение.
И последним я расскажу про несколько общих небольших модулей объединённых в один DeviceSpoofer:
Модули в DeviceSpoofer подменяет параметры navigator, экран, медиа-устройства, геймпады и батарею. Метод spoofNavigator подменяет юзер-агент и другие свойства navigator, такие как oscpu и hardwareConcurrency, чтобы имитировать Chrome на Windows. Метод spoofScreen задаёт стандартное разрешение экрана и связанные параметры, такие как innerWidth и devicePixelRatio, чтобы окно браузера выглядело типичным. Метод spoofMediaDevices переименовывает камеры и микрофоны в "Spoofed Camera" и "Spoofed Mic", чтобы скрыть реальные устройства. Метод spoofGamepads возвращает фейковый геймпад, а spoofBattery блокирует доступ к батарее, выдавая ошибку, потому что при тестах на https://amiunique.org/fingerprint я выяснил, что это более распространённое поведение для батареи, так как на стандартных ПК она обычно так и показывает.
Проверка работы инструмента
В целом, думаю, описание скрипта получилось достаточно понятным, полный код я приложу к статье. А сейчас покажу наглядно, как он работает на https://amiunique.org/fingerprint.
Тут значения агента, платформы, таймзоны и Canvas и тд:
Вы можете заметить, что значения юзер-агента и Canvas не самые распространённые, но, поверьте, это максимально общие значения, которых получилось добиться. Другие юзер-агенты или методики с Canvas давали худшие результаты, но вы можете самостоятельно поэкспериментировать и подобрать значения.
Теперь значения браузера, вроде отсутствия buildID, вендора, памяти и ядер - как вы видите, они помечены зелёным и весьма распространённые:
Значения экрана, вроде размера и отступов, тоже весьма распространённые, хоть размеры и помечены жёлтым - думаю, это достаточно приемлемо для отпечатка:
Кстати, занятный факт: отступы, равные 0, я сделал именно потому, что на стандартной Windows так и бывает.
История с WebGL вышла средней: значения не прямо популярные, но и не редкие, и это пока лучший результат из комбинаций, которого я добился:
По поводу медиа-устройств подделка почти всего вышла достаточно гладкой, кроме AudioContext — там значения немного уникальны, ибо я указал, что поддержки нет. Но когда я указывал иные значения, уникальность вообще показывала от 0% до максимум 0.26%, поэтому я решил, что 1.96% не так уж плохо. Но вы можете поэкспериментировать самостоятельно.
Мой результат:
Вывод
Итак, мы завершили разбор подделки отпечатков в Firefox, и пора подвести итоги. На мой взгляд, Firefox выигрывает у Chrome в плане гибкости, когда речь заходит о маскировке отпечатков. Удивительно, как легко в нём можно подменять параметры через JavaScript или плагины. Взять хотя бы шрифты: в Chrome их подделка требует системных манипуляций, а в Firefox достаточно пары строк кода на JS, чтобы перехватить или изменить данные, которые сайты используют для идентификации. Ещё один плюс Firefox — в его коде изначально вырезано множество параметров, которые могли бы стать частью отпечатка. Это делает его отличной платформой для тех, кто хочет минимизировать утечку данных без сложных настроек.
Но, как и в любой истории, есть свои минусы.
Firefox оказался не слишком дружелюбным к автоматизации. Когда я пытался управлять им через Puppeteer, наткнулся на ограничения: поддерживаются только старые версии браузера, что явно не плюс. Другие инструменты на Node.js, такие как Playwright, вообще не могли нормально подменять значения. Хорошо работал только Selenium, который, хоть и запускается, но не позволяет полноценно подделывать JavaScript. (Изначально я хотел добавить раздел про инструменты автоматизации в Firefox, но подумал, что разбор, почему они не работают, будет слишком скучным. Но, если вам интересно, напишите в комментариях, и я сделаю отдельную небольшую статью об этом).
Потому для этого приходится полагаться на плагины. Так что, если ваша цель — антидетект, Firefox прекрасен для ручной настройки и плагинов, но для автоматизации он в разы уступает Chrome.
На этом я не прощаюсь, ведь впереди нас ждёт следующая часть - разбор подделки отпечатков в Tor Browser. Там я сосредоточусь на его внутренней работе и сборке из исходников. Tor — это совсем другая история, с уникальными вызовами и подходами, так что будет интересно. Спасибо за внимание, до встречи в следующей статье!
Источник https://xss.pro
Это третья часть моего цикла(первая часть и вторая часть про Chrome) про сокрытие отпечатков на Linux. В первой части и второй я показал, как Chrome собирает данные и как его можно перехитрить, а теперь настала очередь Firefox — браузера, который считается чуть ли не эталоном приватности. Но так ли это на самом деле? Как сайты могут использовать Firefox для создания отпечатка? Давай разберёмся, какие данные он отдаёт, как это происходит, и как с этим бороться. Сначала я расскажу, что именно собирают сайты, с примерами из исходников и JavaScript. Потом покажу, как подменять эти данные, какие инструменты для этого использовать и как автоматизировать процесс. Сам Firefox хоть и довольно гибок в изменении отпечатка, но имеет свои косяки и нюансы, которые необходимо знать, если ты хочешь сохранить свою приватность. Не будем затягивать, поехали!
Сбор данных
Firefox не такой "жадный" до данных, как Chrome, но сайты всё равно могут вытянуть из него кучу информации для фингерпринтинга. Эти данные делятся на статические (те, что браузер отдаёт напрямую через API) и динамические (поведенческие, основанные на твоих действиях). Firefox сам по себе не собирает их для себя, но его API и настройки дают сайтам возможность строить твой профиль. Давай разберём, что именно они берут.
Какие отпечатки собирает Firefox?
Статические отпечатки - это то, что сайты получают через JavaScript, словно снимая моментальный снимок твоего устройства. Например, navigator.userAgent выдаёт строку вроде "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0", которая подсказывает, что ты используешь Linux, а не Windows, как большинство. Через window.screen сайты узнают разрешение экрана, скажем, 1920x1080, и глубину цвета. navigator.hardwareConcurrency может показать, сколько ядер у твоего процессора — 16 ядер встречаются реже, и это делает тебя заметнее. Если у тебя подключён геймпад, navigator.getGamepads() раскроет его модель, что тоже добавляет уникальности. WebGL через gl.getParameter(gl.RENDERER) может выдать информацию о видеокарте, например, "NVIDIA GeForce RTX 3080". А рендеринг через создаёт отпечаток на основе шрифтов, графических настроек и даже драйверов. Даже такие мелочи, как язык браузера через navigator.language (например, "ru-RU") или список шрифтов через document.fonts, помогают сайтам выделить тебя из толпы.
Динамические отпечатки - это твоё поведение, и тут всё становится ещё интереснее. Сайты могут отслеживать, как быстро ты печатаешь, двигаешь мышкой или скролишь страницу, используя события вроде onmousemove или onkeydown. Таймеры, такие как performance.now(), показывают, с какой скоростью твой компьютер выполняет скрипты — если страница рендерится за 10 мс у тебя и за 15 мс у другого, это уже различие. Даже порядок загрузки ресурсов или реакции на события могут выдать твоё устройство.
Телеметрия в Firefox есть, но она скромнее, чем в Chrome. Браузер отправляет Mozilla отчёты о сбоях, статистику использования (например, количество вкладок) и версию через toolkit.telemetry. У каждой установки есть client ID — уникальный идентификатор, который может связать твои сессии, если телеметрия включена. Это выключаеться в настройках (about:preferences#privacy, раздел "Firefox Data Collection") или через toolkit.telemetry.enabled в about:config. Без настроек сайты через JavaScript всё равно берут данные. Например, canvas и WebGL могут связать твои сессии, если добираются до шрифтов или таймингов.
В целом, Firefox по умолчанию оставляет некоторые функции, которые могут использоваться для фингерпринтинга, но даже без дополнительных настроек браузер предлагает защитные механизмы. Например, Enhanced Tracking Protection блокирует трекеры, известные Mozilla, включая те, что собирают данные для создания уникального отпечатка устройства. Однако это не полностью отключает API, такие как WebGL или canvas, а лишь ограничивает их использование отдельными трекерами. Чтобы лучше понять, что именно браузер может раскрыть, стоит заглянуть в его исходники — там можно увидеть, какие данные Firefox вообще способен передать. Давай разберёмся, что там к чему. Погнали!
Примеры сбора отпечатка из исходников Firefox
Я начну с того, что покажу как формируется отпечаток браузера на основе исходного кода Firefox, написанного в основном на C++, но активно переписываемого на Rust для повышения безопасности. Исходники вы можете найти на https://searchfox.org/mozilla-central/source/. В отличие от Chrome, чьи закрытые исходники ограничивают анализ, Firefox предоставляет открытый код, который можно изучить, хотя Mozilla может вносить изменения в финальных сборках. Сам Firefox не занимается сбором отпечатков, но поставляет данные, которые сайты используют для их создания. Этих данных в коде предостаточно. Далее я разберу несколько фрагментов кода, покажу, и объясню, как эти данные собираются с помощью JavaScript для создания уникального отпечатка браузера.
Firefox собирает разрешение экрана и позиция окна. Он делает это через методы вроде GetScreen и GetScreenXY в nsGlobalWindowOuter.cpp, чтобы сайты могли получать эту информацию через JavaScript (например, window.screen или window.screenX)
Исходник:
C++:
nsScreen* nsGlobalWindowOuter::GetScreen() {
FORWARD_TO_INNER(Screen, (), nullptr);
}
CSSIntPoint nsGlobalWindowOuter::GetScreenXY(CallerType aCallerType, ErrorResult& aError) {
if (nsIGlobalObject::ShouldResistFingerprinting(aCallerType, RFPTarget::WindowScreenXY)) {
return CSSIntPoint(0, 0);
}
nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
if (!treeOwnerAsWin) {
aError.Throw(NS_ERROR_FAILURE);
return CSSIntPoint(0, 0);
}
LayoutDeviceIntPoint windowPos;
aError = treeOwnerAsWin->GetPosition(&windowPos.x.value, &windowPos.y.value);
return CSSIntPoint::FromAppUnitsRounded(windowPosAppUnits);
}
int32_t nsGlobalWindowOuter::GetScreenXOuter(CallerType aCallerType, ErrorResult& aError) {
return GetScreenXY(aCallerType, aError).x;
}
nsRect nsGlobalWindowOuter::GetInnerScreenRect() {
if (!mDocShell) return nsRect();
EnsureSizeAndPositionUpToDate();
PresShell* presShell = mDocShell->GetPresShell();
if (!presShell) return nsRect();
nsIFrame* rootFrame = presShell->GetRootFrame();
if (!rootFrame) return nsRect();
return rootFrame->GetScreenRectInAppUnits();
}
Вот как это собрать данные об экране на js:
JavaScript:
const screenFingerprint = {
width: window.screen.width,
height: window.screen.height,
colorDepth: window.screen.colorDepth,
screenX: window.screenX,
screenY: window.screenY
};
console.log('Screen Fingerprint:', screenFingerprint);
Вывод:
Как это работает? window.screen.width и height берут данные из GetScreen(), а screenX/screenY — из GetScreenXY(). Если защита включена, screenX и screenY будут 0, а размеры могут подмениться на стандартные, но по дифолту, защиты нет, так что вы сможете собрать реальные размеры экранов у многих юзеров.
Ещё одна штука — Battery Status API. Firefox поддерживает его (хотя в новых версиях Firefox убрал его поддежку). На старых браузерах, сайты всё ещё, могут узнать уровень заряда батареи, время до разрядки и даже статус (заряжается или нет). Это даёт уникальный отпечаток, особенно если ты на ноутбуке.
Исходник:
https://searchfox.org/mozilla-central/source/dom/battery/BatteryManager.cpp
C++:
bool BatteryManager::Charging() const {
MOZ_ASSERT(NS_IsMainThread());
// For testing, unable to report the battery status information
if (Preferences::GetBool("dom.battery.test.default", false)) {
return true;
}
if (Preferences::GetBool("dom.battery.test.charging", false)) {
return true;
}
if (Preferences::GetBool("dom.battery.test.discharging", false)) {
return false;
}
return mCharging;
}
double BatteryManager::DischargingTime() const {
MOZ_ASSERT(NS_IsMainThread());
// For testing, unable to report the battery status information
if (Preferences::GetBool("dom.battery.test.default", false)) {
return std::numeric_limits<double>::infinity();
}
if (Preferences::GetBool("dom.battery.test.discharging", false)) {
return 42.0;
}
if (Charging() || mRemainingTime == kUnknownRemainingTime) {
return std::numeric_limits<double>::infinity();
}
return mRemainingTime;
}
double BatteryManager::ChargingTime() const {
MOZ_ASSERT(NS_IsMainThread());
// For testing, unable to report the battery status information
if (Preferences::GetBool("dom.battery.test.default", false)) {
return 0.0;
}
if (Preferences::GetBool("dom.battery.test.charging", false)) {
return 42.0;
}
if (!Charging() || mRemainingTime == kUnknownRemainingTime) {
return std::numeric_limits<double>::infinity();
}
return mRemainingTime;
}
double BatteryManager::Level() const {
MOZ_ASSERT(NS_IsMainThread());
// For testing, unable to report the battery status information
if (Preferences::GetBool("dom.battery.test.default")) {
return 1.0;
}
return mLevel;
}
Этот код решает, что показать, когда сайт спрашивает про заряд, время зарядки или разрядки. Функция Charging() проверяет, заряжается ли батарея, и возвращает true или false. Если в настройках Firefox включена защита privacy.resistFingerprinting, браузер всегда говорит сайтам, что батарея заряжается, чтобы тебя было сложнее отследить. Без этой защиты он выдаёт правду — заряжается твоя батарея или нет. Level() показывает уровень заряда в виде числа от 0 до 1, например, 0.73 для 73%. С включённой защитой Firefox выдает, что батарея всегда на 100%, чтобы твой отпечаток был как у всех. DischargingTime() выдаёт, сколько секунд осталось до разрядки батареи, но если она заряжается или данных нет (например, на ПК), возвращает огромное число. ChargingTime() говорит, сколько секунд нужно для полной зарядки, а если батарея не заряжается или данных нет, тоже возвращает бесконечность.
Пример кода для сбора Battery Fingerprint на старых версиях Firefox:
JavaScript:
async function getBatteryFingerprint() {
try {
const battery = await navigator.getBattery();
return {
level: battery.level,
charging: battery.charging,
dischargingTime: battery.dischargingTime,
chargingTime: battery.chargingTime
};
} catch (e) {
return 'Battery API not supported';
}
}
getBatteryFingerprint().then(battery => {
console.log('Battery Fingerprint:', battery);
});
Код запрашивает данные о батарее и возвращает их в виде объекта. Например, если у тебя 73% заряда и батарея заряжается, это будет { level: 0.73, charging: true, dischargingTime: Infinity, chargingTime: 3600 }. Эти данные обновляются в реальном времени, так что сайты могут отслеживать изменения, связывая их с твоей сессией.
Так же в Firefox может собираться количество ядер процессора если не включина защита от этого.
Исходник:
C++:
uint64_t Navigator::HardwareConcurrency() {
workerinternals::RuntimeService* rts =
workerinternals::RuntimeService::GetOrCreateService();
if (!rts) {
return 1;
}
return rts->ClampedHardwareConcurrency(
nsGlobalWindowInner::Cast(mWindow)->ShouldResistFingerprinting(
RFPTarget::NavigatorHWConcurrency));
}
Вот как собирать количество ядер на js:
JavaScript:
const cpuCores = navigator.hardwareConcurrency;
console.log('CPU Cores:', cpuCores);
Код:
Проверяем:
Как это работает? navigator.hardwareConcurrency вызывает HardwareConcurrency() из исходников и получает количество ядер.
И наконец timing, с помощью него firefox возвращает точное время в миллисекундах, которое сайты могут использовать для замеров скорости твоего компа.
Исходник:
C++:
DOMHighResTimeStamp Performance::Now() {
DOMHighResTimeStamp rawTime = NowUnclamped();
if (mRTPCallerType == RTPCallerType::SystemPrincipal) {
return rawTime;
}
return nsRFPService::ReduceTimePrecisionAsMSecs(
rawTime, GetRandomTimelineSeed(), mRTPCallerType);
}
Вот как собрать тайминги для отпечатка:
JavaScript:
function getTimingFingerprint() {
const start = performance.now();
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
const end = performance.now();
return end - start;
}
const timing = getTimingFingerprint();
console.log('Timing:', timing);
Проверяем:
Как это работает? performance.now() использует Now() из исходников и получает время задежки.
Сбор отпечатков в Firefox не ограничивается этими примерами. Браузер также собирает данные о user agent, видеокарте и многом другом. Но думаю, этих примеров достаточно для понимания того как поисходит сбор отпечатков в Firefox.
Пример кода для сбора отпечатка
Для большей ясности я приведу пример кода, который можно использовать для сбора простого отпечатка в Firefox. Он дополняет то, что я уже показал, и собирает дополнительные данные.
Пример скрипта:
JavaScript:
const fingerprint = {};
fingerprint.userAgent = navigator.userAgent;
fingerprint.screen = {
width: window.screen.width,
height: window.screen.height,
colorDepth: window.screen.colorDepth,
availWidth: window.screen.availWidth,
availHeight: window.screen.availHeight
};
function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 200;
canvas.height = 50;
ctx.fillStyle = 'rgb(255, 100, 150)';
ctx.fillRect(0, 0, 200, 50);
ctx.font = '18px Arial';
ctx.fillStyle = 'black';
ctx.fillText('Fingerprint Test', 10, 30);
ctx.beginPath();
ctx.arc(100, 25, 20, 0, Math.PI * 2, true);
ctx.stroke();
const imageData = ctx.getImageData(0, 0, 200, 50).data;
let hash = 0;
for (let i = 0; i < imageData.length; i += 4) {
hash = (hash << 5) - hash + imageData[i]; // Простой хэш
hash = hash & hash;
}
return hash.toString(16);
}
fingerprint.canvas = getCanvasFingerprint();
function getWebGLFingerprint() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return 'WebGL not supported';
return {
vendor: gl.getParameter(gl.VENDOR),
renderer: gl.getParameter(gl.RENDERER),
version: gl.getParameter(gl.VERSION)
};
}
fingerprint.webGL = getWebGLFingerprint();
function getFontsFingerprint() {
const fonts = [
'Arial', 'Times New Roman', 'Courier New', 'Georgia', 'Verdana',
'Comic Sans MS', 'Impact', 'Trebuchet MS', 'Helvetica', 'Calibri'
];
const baseFont = 'monospace';
const testString = 'abcdef';
const span = document.createElement('span');
span.style.fontSize = '72px';
span.style.position = 'absolute';
span.style.visibility = 'hidden';
span.innerHTML = testString;
document.body.appendChild(span);
span.style.fontFamily = baseFont;
const baseWidth = span.offsetWidth;
const baseHeight = span.offsetHeight;
let fontHash = 0;
fonts.forEach((font, index) => {
span.style.fontFamily = `${font}, ${baseFont}`;
const width = span.offsetWidth;
const height = span.offsetHeight;
if (width !== baseWidth || height !== baseHeight) {
fontHash += 1 << index;
}
});
document.body.removeChild(span);
return fontHash.toString(16);
}
fingerprint.fonts = getFontsFingerprint();
fingerprint.cpuCores = navigator.hardwareConcurrency || 'unknown';
function getTimingFingerprint() {
const start = performance.now();
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += Math.sin(i);
}
const end = performance.now();
return Number((end - start).toFixed(2));
fingerprint.timing = getTimingFingerprint();
function getWebRTCFingerprint() {
return new Promise((resolve) => {
const rtc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
rtc.createDataChannel('test');
rtc.onicecandidate = (event) => {
if (event.candidate) {
const ip = event.candidate.candidate.match(/(\d+\.\d+\.\d+\.\d+)/)?.[0];
resolve(ip || 'no IP found');
rtc.close();
} else {
resolve('no IP found');
}
};
rtc.createOffer().then(offer => rtc.setLocalDescription(offer)).catch(() => resolve('no IP found'));
});
}
getWebRTCFingerprint().then(ip => {
fingerprint.webRTC = ip;
console.log('Fingerprint:', fingerprint);
});
Пример вывода кода:
Методы скорытия отпечатка
Теперь, когда мы разобрались, какие данные Firefox отдаёт сайтам, пора перейти к методам защиты. Мы начнём с настроек about:config, рассмотрим плагины и Tampermonkey, а в конце я расскажу, как собрать единую конфигурацию Firefox для максимальной маскировки.
Firefox этот браузер из коробки уже неплохо заботится о твоей приватности, в отличие от того же Chrome, который очень охотно выдает все твои данные. Firefox сразу предлагает всякие штуки, чтобы сайтам было сложнее за тобой следить. Например, есть настройка privacy.resistFingerprinting в about:config — включаешь, и вуаля: ядра процессора показываются как два, экран становится 1920x1080, шрифты только самые базовые, типа Arial. Ещё тайминги событий округляются, чтобы сайты не могли вычислить, как ты мышкой водишь. Плюс строгий режим Enhanced Tracking Protection убирает трекеры и подозрительные API. Но, знаешь, даже с этим всем сайты всё равно могут что-то обнаружить, если не поднастроить защиту как следует.
Инструменты для скорытия отпечатка
На Linux есть множество инструментов и подходов, чтобы подменить отпечатки - от конфигурации браузера до автоматизации и изоляции окружения. Давай разберем, как это сделать.
Первое что приходит в голову это about:config, в нем можно настроить Firefox под себя. Настройки в about:config Firefox позволяют минимизировать сбор отпечатков, ограничивая данные, которые браузер передаёт сайтам. Если заглянуть в исходники браузера, всё завязано на условные операторы и флаги, вроде Preferences::GetBool, которые решают, какие данные возвращать. Хочешь, чтобы сайт получил пустышку вместо твоей инфы? Настраиваешь параметр, и браузер либо скрывает данные, либо подсовывает что-то не своё. Например, можно подкрутить WebGL, чтобы он выдавал слегка искажённые характеристики железа — графика работает, а отпечаток уже не твой. Или взять WebRTC: выключаешь его через настройку, и сайты теряют доступ к твоей сети, не могут подсмотреть твои локальные адреса. Но не всё так просто. Даже с идеальными настройками в about:config полная анонимность — это миф, если не прикрыть другие лазейки которые нужно закрывать другими инструментами.
Ещё можно использовать Firejail: он засовывает Firefox в песочницу, где можно отрезать лишние сетевые подключения или подменить переменную $DISPLAY, чтобы браузер думал, что у тебя другой экран. С параметром --net=none утечки вообще сводятся к минимуму.
Для тех, кто любит заморочиться, есть вариант собрать Firefox из исходников. В файле mozconfig можно вырубить телеметрию, WebRTC и всё, что считаешь лишним, просто добавив пару строчек вроде --disable-telemetry. Это не для новичков, но зато контроль полный — можешь даже патчить функции, чтобы, например, координаты экрана всегда были фиксированными. Времени, правда, на это уйдёт прилично. Я не буду показывать этот метод в статье, тем не менее я не мог не упомянуть его для полноты картины.
Ещё отпечаток можно подменять на уровне плагинов. В Firefox есть расширения, которые подменяют отпечатки прямо на уровне JavaScript, и их сложнее вычислить, чем в Chrome. Правда, выбор таких плагинов поменьше, но, если настроить правильно, они могут помочь в подмене отпечатка. Так же можно самостоятельно написать плагин для подмены отпечатка.
Шпаргалка: как устанавливать основные инструменты из статьи.
Перед тем как разбирать конкретные методы подмены (шрифты, WebGL, локали и т.д.), опишу как устанавливать основные инструменты, которые будем использовать: плагины для Firefox и пользовательские скрипты через Tampermonkey. Здесь вы найдёте инструкции по их установке и базовой настройке.
Как загружать плагины?
Плагины могут помочь в подмене параметров браузера, таких как шрифты, WebGL или User-Agent, на уровне JavaScript. Мы будем использовать временные плагины, которые не требуют публикации в Mozilla Add-ons.
Для создания тестового плагина в Firefox создайте папку, например test_plugin, и положите в неё два файла: manifest.json и content.js. В manifest.json задаётся структура и права плагина, а content.js содержит JavaScript-код для подмены данных.
Вот пример manifest.json для test_plugin:
JSON:
{
"manifest_version": 2,
"name": "test_plugin",
"version": "1.0",
"description": "Тестовый плагин для подмены отпечатков",
"permissions": ["<all_urls>", "activeTab"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
Вот пример content.js для test_plugin с заглушкой:
JavaScript:
(function() {
'use strict';
// пока ничего нет
})();
Чтобы установить плагин, откройте Firefox, введите about:debugging#/runtime/this-firefox в адресной строке. После нажмите Load Temporary Add-on и выберите manifest.json из папки:
Установка скриптов Tampermonkey
Еще я очень часто буду использовать Tampermonkey — расширение для запуска JavaScript-кода на сайтах. Установите его через https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/.
После установки кликните на иконку Tampermonkey в панели расширений, выберите Create a new script:
Вот пример скрипта test_script:
JavaScript:
// ==UserScript==
// @name test_script
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Тестовый скрипт
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// пусто
})();
Активруйте скрипт:
Подмена отпечтаков
На этом, хватит теории, давай проверим, как это работает на практике.
С самого начала скажу тебе важную заметку на будующие: не перестарайся с рандомизацией. Если твои параметры будут слишком уж хаотичными, ты можешь выделиться среди толпы, а это хуже, чем стандартный отпечаток. Так что подменяй с умом, чтобы всё выглядело естественно. Для тестов я буду использовать https://browserleaks.com/ и https://amiunique.org/fingerprint.
Смена User agent
Когда речь заходит о сокрытии отпечатков в Firefox, одним из первых шагов становится подмена User-Agent — строки, которую браузер отправляет сайтам, сообщая о своей версии, операционной системе и других характеристиках. User-Agent может выдать, что ты используешь Firefox на Linux, а это уже зацепка для трекеров.
Начнем с самого простого и доступного способа - настройки в about:config. Это интерфейс Firefox, где можно менять скрытые параметры браузера. За User-Agent отвечает параметр general.useragent.override. По умолчанию он не задан, и Firefox отправляет стандартный User-Agent, например, Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0. Чтобы подменить его, нужно создать или изменить этот параметр вручную.
Для этого сначала в Firefox и в адресной строке введи about:config. Нажми «Принять риск и продолжить», чтобы попасть в панель настроек.
Далее в строке поиска введи general.useragent.override:
Если параметр уже существует, он будет отображаться, но чаще всего его нет, и значение параметра задано как boolean. В этом случае нужно создать его. Выбери тип String и нажми на кнопку с плюсом (+) справа, чтобы добавить новую настройку далее.
После этого в поле значения введи нужный тебе User-Agent:
После этого нажми на галочку. Теперь Firefox будет отправлять сайтам именно этот User-Agent.
После сохранения настройки стоит проверить, сработала ли подмена. Для этого я использовал browserleaks.com:
Всё работает, правда, другие параметры выдаёт в нас Firefox, но их мы подменим чуть позже.
Есть более удобный и автоматизированный способ — использование файла user.js, который находится в профиле Firefox. Для него не обязательно каждый раз лезть в about:config и вручную править настройки. Этот файл позволяет задавать настройки, которые будут применяться автоматически при каждом запуске браузера.
Профиль Firefox обычно лежит в директории ~/.mozilla/firefox/, и его имя заканчивается на .default-release, .default-esr или что-то похожее, например, abcd1234.default-release. Чтобы найти свой профиль, открой терминал и перейди в ~/.mozilla/firefox/. Команда ls покажет папки профилей. Если у тебя несколько профилей, выбери нужный, проверив их в Firefox через about:profiles. Внутри папки профиля и будет наш файл user.js.
Если файла user.js нет, его нужно создать. Открой терминал, перейди в папку профиля и создай файл:
Затем открой его в текстовом редакторе, например, nano user.js.
Чтобы подменить User-Agent, добавь в файл следующую строку:
Код:
user_pref("general.useragent.override", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36");
Также user agent можно менять с помощью плагинов. Я расскажу про User-Agent Switcher: в нем можно выбрать разные юзер-агенты — от Windows до Mac или iPhone.
Для начала установим User-Agent Switcher. Перейдите на официальную страницу дополнений Firefox по ссылке: https://addons.mozilla.org/en-US/firefox/addon/uaswitcher/.
На странице нажмите кнопку Add to Firefox, чтобы начать установку:
После нажатия на Add to Firefox браузер покажет всплывающее окно с запросом на добавление расширения. Нажмите Add, чтобы подтвердить установку.
Теперь, когда плагин установлен, ты можешь выбрать нужный User-Agent. Щёлкни по иконке User-Agent Switcher на панели инструментов браузера (или в меню расширений). В выпадающем списке доступны различные варианты: Windows, macOS, Linux, iOS, Android и тд.
Для примера я выбрал User-Agent для Windows:
Ну и проверяем сработала ли подмена юзер агента:
Смена шрифтов
Теперь поговрим про смену шрифтов в firefox. Firefox, в отличие от Chrome, в плане шрифтов намного гибче. Можно подменять шрифты через плагины или просто через в настройки. В отличие от Chrome где это можно нормально сделать только на уровне системы.
Сначала поговорим про самый простой способ — смену шрифтов через настройки Firefox. Это, конечно, не полноценная подмена, и автоматизировать процесс сложновато, но зато можно легко настроить шрифты так, чтобы они отличались от стандартных для твоей системы. Хочешь, чтобы сайт думал, что ты используешь шрифты, нехарактерные для твоей ОС? Без проблем!
Открой Firefox и в адресной строке введи about:preferences#general. Прокрути вниз до раздела Fonts или просто вбей в поиске настроек слово Fonts:
Нажми на кнопку Advance — и вуаля! Перед тобой откроется окно, где можно выбрать любые шрифты для разных категорий текста:
Выбирай шрифты, которые тебе нравятся, или те, что хочешь использовать для подмены. Например, можешь поставить дефолтные шрифты.
Еще шрифты можно менять в about:config. Если вручную это делать не охота, можно задать свои шрифты прямо в user.js, который лежит в профиле браузера. Например, можно указать Georgia для serif, Helvetica для sans-serif, задать размер шрифта побольше и минимальный порог, чтоб текст не мельчил.
А чтобы не копаться в папках вручную, я написал легкий скрипт на Bash, который всё делает сам:
Bash:
#!/bin/bash
PROFILE_DIR=$(find ~/.mozilla/firefox -maxdepth 1 -type d -name "*.default-esr")
USER_JS="$PROFILE_DIR/user.js"
cat <<EOT > "$USER_JS"
user_pref("font.name.serif.x-western", "Georgia");
user_pref("font.name.sans-serif.x-western", "Helvetica");
user_pref("font.size.variable.x-western", 18);
user_pref("font.minimum-size.x-western", 12);
EOT
DESKTOP_FILE="$HOME/.local/share/applications/firefox.desktop"
if [ ! -f "$DESKTOP_FILE" ]; then
cp /usr/share/applications/firefox.desktop "$DESKTOP_FILE"
fi
sed -i 's|^Exec=firefox|Exec=env QT_FONT_DPI=96 firefox|' "$DESKTOP_FILE"
echo "Font settings applied to $USER_JS and $DESKTOP_FILE"
Проверяем работу скрипта:
Скрипт сначала ищет папку профиля Firefox, потом создаёт user.js с нужными настройками шрифтов. Заодно подправляет .desktop-файл Firefox, чтобы DPI шрифтов в Qt-приложениях тоже был в норме — это полезно, если шрифты выглядят не так, как надо. После запуска скрипта всё готово, и Firefox стартует с твоими шрифтами.
Еще чтобы поменять шрифты в Firefox, можно создать свой плагин. Это проще, чем кажется, и даёт полный контроль над тем, как страницы отображают текст. Я покажу, как собрать такой плагин, который подменяет шрифты через CSS и JavaScript.
Тут есть два варианта: попроще, с использованием только CSS, и посложнее, с добавлением JavaScript для перехвата шрифтов.
Оба плагина подменяют шрифты на стандартные, например Arial для текста и Courier New для кода, и подключаются через about:debugging в Firefox. Я разберу оба подхода, покажу код и объясню, как всё работает, чтобы ты мог выбрать тот, что больше подходит.
Сначала расскажу про вариант один. Плагин использует CSS, чтобы заставить все сайты отображать текст в Arial, а моноширинные элементы (код, поля ввода) — в Courier New.
Вот пример:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Font Spoofer",
"version": "1.0",
"description": "Заменяет шрифты на стандартные в Firefox",
"permissions": [
"activeTab",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["styles.css"],
"run_at": "document_start"
}
],
"browser_specific_settings": {
"gecko": {
"id": "font-spoofer@example.com"
}
}
}
styles.css:
CSS:
* {
font-family: Arial, sans-serif !important;
font-style: normal !important;
font-weight: normal !important;
}
pre, code, textarea, input[type="text"], input[type="password"] {
font-family: "Courier New", monospace !important;
}
Чтобы установить плагин, создаёшь папку с двумя файлами: manifest.json и styles.css. Далее устанавливаешь плагин, и он сразу подключается и начинает работать. Он появиться в плагинах.
Проверяем его работу на https://browserleaks.com/fonts:
Еще есть второй плагин: подмена шрифтов через CSS и JavaScript. Этот вариант сложнее. Он не только применяет стили, как первый плагин, но и перехватывает шрифты, которые сайты пытаются подгрузить через FontFace. Это полезно, если сайт использует кастомные шрифты, которые могут выдать твой браузер.
Создай файл manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Font Spoofer",
"version": "1.0",
"description": "Заменяет шрифты на стандартные в Firefox",
"permissions": [
"activeTab",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["styles.css"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"browser_specific_settings": {
"gecko": {
"id": "font-spoofer@example.com"
}
}
}
content.js:
JavaScript:
Object.defineProperty(window, 'FontFace', {
value: function (family, source) {
if (!family.match(/Arial|Times New Roman|Courier New/)) {
family = 'Arial';
}
return new window._FontFace(family, source);
},
writable: true
});
window._FontFace = window.FontFace;
const style = document.createElement('style');
style.textContent = `
* {
font-family: Arial, sans-serif !important;
font-style: normal !important;
font-weight: normal !important;
}
pre, code, textarea, input[type="text"], input[type="password"] {
font-family: "Courier New", monospace !important;
}
`;
document.head.appendChild(style);
styles.css:
CSS:
* {
font-family: Arial, sans-serif !important;
font-style: normal !important;
font-weight: normal !important;
}
pre, code, textarea, input[type="text"], input[type="password"] {
font-family: "Courier New", monospace !important;
}
После установки проверяй результат на browserleaks.com/fonts. Должен отображаться только Arial для текста и Courier New для моноширинных элементов. Если сайты всё ещё видят другие шрифты, убедись, что плагин запускается на этапе document_start, и проверь, не перебиваются ли стили сайта через !important.
Если не хочется писать плагин для подмены шрифтов с нуля, можно взять готовое решение, например, расширение Web Font Changer из магазина Firefox: https://addons.mozilla.org/en-US/firefox/addon/web-font-changer/. Это удобный инструмент, который позволяет менять шрифты на сайтах прямо на лету, без необходимости копаться в коде или настройках браузера.
Установить расширение проще простого. Открываешь Firefox, переходишь по ссылке https://addons.mozilla.org/en-US/firefox/addon/web-font-changer/, и на странице расширения жмёшь кнопку "Add to Firefox".
Браузер покажет всплывающее окно с запросом на добавление — подтверждаешь, нажав "Add". После этого Web Font Changer появится в списке твоих расширений, и его иконка добавится на панель инструментов Firefox.
Далее перейдите на нужный вам сайт и используйте его, выбрав нужные вам шрифты:
Я заглянул в код этого плагина, раскопав его в /.mozilla/firefox/8994vyhd.default-esr/extensions/ в xpi-архиве. И вот как он работает так: через CSS он задаёт стандартные шрифты для всех элементов страницы, перебивая стили сайта, а с помощью JavaScript он перехватывает объект FontFace, так что любые кастомные шрифты, которые сайт пытается подгрузить, превращаются в разрешённые: Arial, Times New Roman или Courier New. Настройки шрифтов (какой шрифт, стиль, для каких тегов) плагин сохраняет в browser.storage.local, и ты можешь выбрать, применять их везде или только на конкретном сайте — есть опция sameDomainOnly. Если захочешь чего-то необычного, вроде Roboto, он подтянет шрифт из Google Fonts через <link>.
Еще можно подменять шрифты и запускает Firefox с помощью Firejail. Для этого нужно создать временную конфигурацию fontconfig, которая подменяет системные шрифты, и запустить браузер с ней через Firejail, ограничив его доступ к системе.
Для автоматизации данного процесса я решил написать bash-скрипт:
Bash:
#!/bin/bash
if ! fc-list | grep -qi "Liberation Sans"; then
sudo apt update && sudo apt install -y fonts-liberation || {
FONT_SANS="DejaVu Sans"
FONT_SERIF="DejaVu Serif"
FONT_MONO="DejaVu Sans Mono"
}
else
FONT_SANS="Liberation Sans"
FONT_SERIF="Liberation Serif"
FONT_MONO="Liberation Mono"
fi
if ! fc-list | grep -qi "DejaVu Sans" && [ "$FONT_SANS" != "Liberation Sans" ]; then
sudo apt install -y fonts-dejavu || {
FONT_SANS="sans-serif"
FONT_SERIF="serif"
FONT_MONO="monospace"
}
fi
rm -rf ~/.cache/fontconfig/* /var/cache/fontconfig/*
sudo fc-cache -fv
TEMP_FONT_CONF=$(mktemp -d)
cat << EOF > "$TEMP_FONT_CONF/fonts.conf"
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<selectfont>
<rejectfont>
<pattern><patelt name="family"><string>Norasi</string></patelt></pattern>
<pattern><patelt name="family"><string>DejaVu Math TeX Gyre</string></patelt></pattern>
</rejectfont>
</selectfont>
<dir>/usr/share/fonts/truetype/liberation</dir>
<dir>/usr/share/fonts/truetype/dejavu</dir>
<match target="font">
<edit name="antialias" mode="assign"><bool>true</bool></edit>
<edit name="hinting" mode="assign"><bool>true</bool></edit>
<edit name="hintstyle" mode="assign"><const>hintfull</const></edit>
<edit name="rgba" mode="assign"><const>rgb</const></edit>
<edit name="autohint" mode="assign"><bool>true</bool></edit>
</match>
<match target="pattern">
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="assign"><string>$FONT_SANS</string></edit>
</match>
<match target="pattern">
<test name="family"><string>serif</string></test>
<edit name="family" mode="assign"><string>$FONT_SERIF</string></edit>
</match>
<match target="pattern">
<test name="family"><string>monospace</string></test>
<edit name="family" mode="assign"><string>$FONT_MONO</string></edit>
</match>
</fontconfig>
EOF
FONTCONFIG_PATH="$TEMP_FONT_CONF" fc-cache -fv
FONT_CHECK=$(FONTCONFIG_PATH="$TEMP_FONT_CONF" fc-match sans-serif)
if [[ "$FONT_CHECK" != *"Liberation Sans"* && "$FONT_CHECK" != *"DejaVu Sans"* && "$FONT_CHECK" != *"sans-serif"* ]]; then
echo "Ошибка: Подходящий шрифт не найден"
rm -rf "$TEMP_FONT_CONF"
exit 1
fi
TEMP_PROFILE=$(mktemp)
cat << EOF > "$TEMP_PROFILE"
include /etc/firejail/firefox-common.profile
netfilter
whitelist ${HOME}/.cache/fontconfig
whitelist $TEMP_FONT_CONF
whitelist /usr/share/fonts/truetype/liberation
whitelist /usr/share/fonts/truetype/dejavu
protocol unix,inet,inet6
EOF
FONTCONFIG_PATH="$TEMP_FONT_CONF" firejail --profile="$TEMP_PROFILE" firefox || {
echo "Ошибка: Не удалось запустить Firefox"
rm -rf "$TEMP_FONT_CONF" "$TEMP_PROFILE"
exit 1
}
rm -rf "$TEMP_FONT_CONF" "$TEMP_PROFILE"
exit 0
Проверяем работу скрипта:
Скрипт начинается с проверки доступности шрифтов. Он использует fc-list, чтобы узнать, установлен ли Liberation Sans. Если да, задаёт Liberation Sans, Serif и Mono как основные шрифты. Если нет, пытается установить пакет fonts-liberation через apt, а в случае неудачи переключается на DejaVu Sans, Serif и Mono. Если и DejaVu не нашёл, устанавливает общие семейства sans-serif, serif и monospace как запасной вариант. Это гарантирует, что система всегда найдёт подходящий шрифт. После этого скрипт чистит кэш fontconfig, удаляя старые данные, и обновляет его через fc-cache, чтобы всё работало с актуальными настройками.
Дальше создаётся временная папка для конфигурации шрифтов. В неё записывается файл fonts.conf, который задаёт правила fontconfig: отключает шрифты Norasi и DejaVu Math TeX Gyre, включает директории с Liberation и DejaVu, активирует сглаживание, хинтинг и автокоррекцию, а также подменяет общие семейства шрифтов на выбранные. Этот файл становится временной конфигурацией, и fc-cache обновляет кэш для неё. Скрипт проверяет, правильно ли настроен sans-serif шрифт через fc-match, и, если что-то пошло не так, выдаёт ошибку и завершает работу.
Затем создаётся временный профиль для Firejail. Он наследует настройки из firefox-common.profile, включает сетевой фильтр, разрешает доступ к кэшу fontconfig, временной конфигурации шрифтов и папкам с Liberation и DejaVu, а также ограничивает сетевые протоколы unix, inet и inet6. Это изолирует Firefox, минимизируя риски. Скрипт запускает браузер через Firejail с этим профилем и переменной FONTCONFIG_PATH, указывающей на временную конфигурацию. Если запуск не удался, выводится ошибка, и всё чистится. В конце скрипт удаляет временные файлы, чтобы не оставлять мусора.
Смена локалей и таймзон
Давайте разберём тему смены локалей и таймзон. Локаль определяет, какой язык и региональные настройки браузер передаёт сайтам, а таймзона напрямую указывает на ваше географическое положение. Эти данные легко считываются через JavaScript (например, navigator.languages или Intl.DateTimeFormat) или HTTP-заголовки вроде Accept-Language.
Локаль можно изменить в about:config или файл user.js.
Вот как это сделать:
JavaScript:
user_pref("intl.accept_languages", "en-PK, en-US, en");
К сожалению, подменить таймзону через user.js или about:config напрямую не получится — Firefox берёт её из системных настроек, и в стандартных настройках нет опции для её изменения. Однако подмена только локали без таймзоны, может выглядеть подозрительно, а нам это не надо. Потому расскажу, как можно подделать и таймзону и локаль, а именно через js.
К примеру, для нормальной смены локали и таймзоны можно написать плагин на Firefox. Моё расширение состоит из двух файлов: manifest.json и content.js.
Пример плагина:
manifest.json:
JavaScript:
{
"manifest_version": 2,
"name": "Fake Timezone",
"version": "1.0",
"description": "Overrides browser timezone via JS.",
"permissions": ["<all_urls>", "tabs"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
(function() {
const script = document.createElement('script');
script.textContent = `
(() => {
const fakeTimezone = "Asia/Karachi";
const fakeOffset = -300;
const fakeLocale = "en-PK";
const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
Intl.DateTimeFormat.prototype.resolvedOptions = function() {
const options = originalResolvedOptions.call(this);
options.timeZone = fakeTimezone;
options.locale = fakeLocale;
return options;
};
const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset;
Date.prototype.getTimezoneOffset = function () {
return fakeOffset;
};
Object.defineProperty(navigator, "language", {
get: () => fakeLocale,
configurable: true
});
Object.defineProperty(navigator, "languages", {
get: () => [fakeLocale],
configurable: true
});
})();
`;
document.documentElement.appendChild(script);
})();
Устанвливаем и проверяем как он работает:
Если не хочется писать своё расширение, таймзону в Firefox можно подменить с помощью готового плагина, например, Timezone Editor, который доступен по ссылке https://addons.mozilla.org/en-US/firefox/addon/timezoneeditor/.
Сначала устанавливаешь плагин через магазин дополнений Firefox. После установки находишь иконку Timezone Editor в панели инструментов браузера и кликаешь по ней:
Дальше открывается меню, где ты выбираешь нужную таймзону из списка, например, “Asia/Karachi” или любую другую, которая тебе подходит.
Плагин сразу подменяет таймзону, и сайты, которые проверяют твои настройки, будут видеть выбранное значение.
Для тех, кто хочет подменить таймзону и локаль в браузере без конкретного плагина для этого, я написал скрипт для Tampermonkey.
Пример скрипта:
JavaScript:
// ==UserScript==
// @name Fake Timezone & Locale Only
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Подделка таймзоны и локали
// @author hipeople
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const injectedCode = `
(() => {
const fakeTimezone = "Asia/Karachi";
const fakeOffset = -300;
const fakeLocale = "en-PK";
// Подмена getTimezoneOffset()
Date.prototype.getTimezoneOffset = function () {
return fakeOffset;
};
const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
Intl.DateTimeFormat.prototype.resolvedOptions = function () {
const options = originalResolvedOptions.call(this);
options.timeZone = fakeTimezone;
options.locale = fakeLocale;
return options;
};
})();
`;
const script = document.createElement('script');
script.textContent = injectedCode;
document.documentElement.appendChild(script);
})();
Подменять таймзоны и локали можно и на уровне системы перед запуском Firefox. Для этого написал простой bash-скрипт. Он позволяет временно переключить настройки на, скажем, японские, чтобы браузер и сайты видели тебя в другом регионе, а после завершения работы возвращает всё как было.
Вот сам код:
Bash:
#!/bin/bash
ORIGINAL_TZ="${TZ:-$(timedatectl show --value --property=Timezone)}"
ORIGINAL_LANG="${LANG:-en_US.UTF-8}"
DESIRED_TZ="Asia/Tokyo"
DESIRED_LANG="ja_JP.UTF-8"
if ! locale -a | grep -q "$DESIRED_LANG"; then
echo "Локаль $DESIRED_LANG не найдена. Проверяем /etc/locale.gen..."
if ! grep -q "^$DESIRED_LANG" /etc/locale.gen; then
echo "Добавляем $DESIRED_LANG в /etc/locale.gen..."
echo "$DESIRED_LANG UTF-8" | sudo tee -a /etc/locale.gen
fi
echo "Пытаемся сгенерировать локаль..."
sudo locale-gen "$DESIRED_LANG" 2>/dev/null || {
echo "Ошибка: Не удалось сгенерировать локаль $DESIRED_LANG. Используем en_US.UTF-8."
DESIRED_LANG="en_US.UTF-8"
}
fi
export TZ="$DESIRED_TZ"
export LANG="$DESIRED_LANG"
export LC_ALL="$DESIRED_LANG"
echo "Запускаем Firefox с TZ=$TZ и LANG=$LANG..."
firefox "$@"
if [ $? -eq 0 ]; then
echo "Firefox завершен."
else
echo "Ошибка при запуске Firefox."
fi
export TZ="$ORIGINAL_TZ"
export LANG="$ORIGINAL_LANG"
export LC_ALL="$ORIGINAL_LANG"
unset LC_ALL
Проверяем как он работает:
Скрипт начинается с сохранения текущих системных настроек — таймзоны (через timedatectl) и локали (из переменной LANG), чтобы потом вернуть их на место. По умолчанию он берёт TZ из системы или, если не задано, запрашивает через timedatectl, а LANG ставит en_US.UTF-8. Затем я задаю желаемые значения: таймзону “Asia/Tokyo” и локаль “ja_JP.UTF-8” для японского региона.
Дальше скрипт проверяет, доступна ли нужная локаль через locale -a. Если её нет, он заглядывает в /etc/locale.gen, и, если там не прописана ja_JP.UTF-8, добавляет строку “ja_JP.UTF-8 UTF-8” и запускает locale-gen для генерации локали. Если что-то пошло не так, например, генерация не удалась, скрипт падает обратно на en_US.UTF-8, чтобы не сломать систему.
Когда локаль готова, код устанавливает переменные окружения TZ, LANG и LC_ALL на желаемые значения, чтобы Firefox подхватил их при запуске. Затем он вызывает Firefox с переданными аргументами, чтобы ты мог запустить браузер как обычно, но уже с подменёнными настройками. После завершения Firefox скрипт проверяет, успешно ли он отработал, и выводит сообщение об успехе или ошибке.
В финале всё возвращается на круги своя: TZ, LANG и LC_ALL восстанавливаются в исходные значения, а LC_ALL сбрасывается, чтобы не путать другие программы.
Подмена таймзоны через JavaScript (плагины или Tampermonkey) удобна, так как не требует изменения системных настроек и работает на уровне браузера. Однако сайты могут обнаружить несоответствие между JavaScript-таймзоной и системной (например, через HTTP-заголовки). Системная подмена через
TZ более надёжна, но требует прав администратора и может повлиять на другие приложения.Смена системного времени
Далее поговорим про подмену системного времени. Это можно нормально сделать через JavaScript или на уровне Linux, потому что в браузере(в about:config) таких функций нет. Тем не менее, системное время — это очень важный параметр, который может выдать твое реальное время. Поэтому его в любом случае необходимо подменять, чтобы не выдать твое местоположение.
Первым делом расскажу про смену системного времени на уровне js, а именно с помощью плагина.
manifest.json:
JSON:
{
"manifest_version": 3,
"name": "Fake Time Extension",
"version": "1.0",
"permissions": [
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
(function() {
const script = document.createElement('script');
script.textContent = `
(function() {
const OriginalDate = window.Date;
const FakeDate = function(...args) {
let realDate;
if (args.length === 0) {
realDate = new OriginalDate();
realDate.setHours(realDate.getHours() + 6);
} else if (args.length === 1) {
realDate = new OriginalDate(args[0]);
} else {
realDate = new OriginalDate(...args);
}
return new OriginalDate(realDate.getTime());
};
FakeDate.now = () => {
const realTime = new OriginalDate();
realTime.setHours(realTime.getHours() + 6);
return realTime.getTime();
};
FakeDate.UTC = OriginalDate.UTC;
FakeDate.parse = OriginalDate.parse;
Object.setPrototypeOf(FakeDate.prototype, OriginalDate.prototype);
Object.getOwnPropertyNames(OriginalDate).forEach(prop => {
if (!FakeDate[prop] && prop !== 'prototype') {
Object.defineProperty(FakeDate, prop, {
value: OriginalDate[prop],
writable: true,
configurable: true
});
}
});
window.Date = FakeDate;
console.log('Time faked to:', new Date());
})();
`;
(document.head || document.documentElement).appendChild(script);
})();
Проверяем работает ли он:
Еще для смены системного времени на уровне js можно использовать tampermonkey. Я написал небольшой скрипт для Tampermonkey, который добавляет 6 часов к текущему времени.
Сам код:
JavaScript:
// ==UserScript==
// @name Fake System Time (+6 Hours)
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Overrides system time to add 6 hours
// @author hipeople
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const OriginalDate = window.Date;
function FakeDate(...args) {
if (args.length === 0) {
// No arguments: return current time + 6 hours
const realTime = new OriginalDate();
realTime.setHours(realTime.getHours() + 6);
return new OriginalDate(realTime);
}
return new OriginalDate(...args);
}
FakeDate.now = () => {
const realTime = new OriginalDate();
realTime.setHours(realTime.getHours() + 6);
return realTime.getTime();
};
FakeDate.UTC = OriginalDate.UTC;
FakeDate.parse = OriginalDate.parse;
window.Date = FakeDate;
console.log('Time faked to:', new Date());
})();
В скрипте я сохраняю оригинальный объект window.Date в переменную OriginalDate, чтобы не потерять его методы, и создаю новый FakeDate. Когда Date вызывается без аргументов, FakeDate берёт текущее время, добавляет 6 часов через setHours и возвращает новый объект на основе OriginalDate. Если переданы аргументы, например, строка или число, он передаёт их в OriginalDate без изменений, чтобы не сломать парсинг дат на сайтах. Метод FakeDate.now перехватывается, чтобы Date.now() возвращал время со сдвигом в 6 часов, сохраняя точность в миллисекундах. Методы Date.UTC и Date.parse остаются нетронутыми, чтобы не нарушать работу с UTC или строками дат. В конце window.Date заменяется на FakeDate, и скрипт выводит в консоль новое фейковое время для проверки.
Давайте проверим о смене системного времени средствами Linux. Когда речь заходит о подмене системного времени на уровне Linux, на помощь приходит утилита faketime — она позволяет обмануть программы, заставляя их видеть другое время, без возни с системными часами. Этот инструмент перехватывает системные вызовы, связанные с временем, и подсовывает фейковые значения, оставляя реальное время нетронутым. Как это работает? Faketime использует библиотеку libfaketime, которая внедряется в процесс программы и подменяет функции вроде time() или gettimeofday(), возвращая заданное тобой время. Это значит, что только выбранная программа, например, Firefox, видит поддельное время, а система продолжает работать как ни в чём не бывало.
Установить утилиту проще простого: достаточно выполнить команду:
Код:
sudo apt install faketime
Я написал лаконичный bash-скрипт, который сдвигает время на 8 часов вперёд и запускает Firefox в этом фейковом временном пузыре.
Вот как это выглядит:
Bash:
#!/bin/bash
NEW_TIME=$(date -d "+8 hours" '+%Y-%m-%d %H:%M:%S')
faketime "$NEW_TIME" firefox
Если ты хочешь подменить системное время на уровне Linux, но не желаешь устанавливать дополнительные утилиты вроде faketime, есть способ обойтись системными средствами. Можно просто задать переменную среды и с её помощью заставить Firefox, видеть нужное тебе время. Это работает через библиотеку libfaketime, которая уже есть в большинстве дистрибутивов, и я написал небольшой bash-скрипт, чтобы показать, как это работает.
Вот как выглядит код:
Bash:
#!/bin/bash
NEW_TIME=$(date -d "+4 hours" '+%Y-%m-%d %H:%M:%S')
FAKETIME="@$NEW_TIME" \
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1 \
firefox
Смена координат
Теперь, наверное, ты, думаешь, что будет раздел про подмену координат в Firefox? Но его не будет. Ладно, шучу. Но некоторые нюансы в Firefox по поводу координат все же есть. Такое дело что в Firefox нельзя просто взять и получить координаты. И дело не кривых руках тех кто хочет это сделать, а в том, как Firefox устроен, особенно на Linux. Но давай разберемся почему это не работает?
Первым делом стоит заглянуть в about:config, где по умолчанию параметр geo.enabled стоит в false. Это он, отвечает за включение геолокации, и пока не переключишь его в true, браузер даже не подумает запрашивать твои координаты. Но вот загвоздка: даже когда я для теста установил geo.enabled в true, координаты всё равно не пришли.
Чтобы проверить, в чём дело, я написал простенький тестовый код, который должен был вытащить широту и долготу:
JavaScript:
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
pos => console.log("Широта:", pos.coords.latitude, "Долгота:", pos.coords.longitude),
err => console.error("Ошибка:", err.message)
);
}
И что ты думаешь? Вместо координат я получил в консоли undefined с ошибкой “Unknown error acquiring position”. Ты можешь подумать, что я не разрешил доступ к геолокации, но нет — я честно нажал “Allow” в запросе браузера, так что дело не в этом.
Вот скриншот с сайта https://my-location.org/, где чётко видно, что я дал добро на сбор координат:
Так в чём же проблема? Я подозреваю, что на Linux это связано с особенностями сборки Firefox или с тем, как он взаимодействует с системой. В отличие от Chrome, который берет геолокацию через системные сервисы или API, Firefox, похоже, либо урезал эту функцию на уровне кода, либо требует каких-то дополнительных танцев с бубном. К примеру я читал что нужно установить параметр privacy.resistFingerprinting в false, ибо он может мешать получению координат, но даже после его установки я получал undefined вместо координат.
Возможно дело в том, что в некоторых сборках Firefox на Linux геолокация зависит от сервиса Mozilla Location Service (MLS), который может быть отключён или подобное. Плюс, Firefox вроде как находиться в песочнице, которая ограничивает доступ к системным API, включая геолокацию (но это больше актально для snap-версии).
Чтобы окончательно убедиться, я копнул в баг-трекер Mozilla и нашёл, что ошибка “Unknown error acquiring position” связана с кодом POSITION_UNAVAILABLE, который Firefox выдаёт, когда не может получить данные от бэкэнда геолокации. Причины? Либо подходящий провайдер геолокации отсутствует, либо сборка браузера имеет внутренние ограничения. На Linux, особенно в дистрибутивах, заточенных под приватность, вроде Fedora или Debian, дело осложняют дополнительные слои безопасности, такие как AppArmor или SELinux, которые могут блокировать доступ Firefox к нужным API. А вот в Chrome, лол, всё работает как по маслу, без таких проблем.
Но вот что интересно: когда я скачал Firefox из tar-архива с сайта Mozilla, геолокация вдруг заработала, как ни в чём не бывало! Это меня озадачило. Похоже, стандартная версия, устан
Как ни странно, когда я скачал Firefox прямо из tar-архива с сайта Mozilla, геолокация заработала как по часам, что меня, честно, удивило. Но вот парадокс: если взять стандартный Firefox, установленный через пакетный менеджер, например, apt в Debian, координаты не получить. Если тебе нужна геолокация, качай Firefox с официального сайта и распаковывай из tar-архива — это работает. А вот версия из apt, скорее всего, будет упираться из-за ограничений сборки или песочницы вроде snap. Так как получение координат все же где-то работает, я решил всё дать пару рекомендаций как это обойти.
Первым делом я решил написать плагин для подмены геолокации в Firefox, чтобы сайты видели фиксированные координаты, которые я сам задаю.
Вот сам плагин:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Geolocation Spoofer",
"version": "1.0",
"description": "Spoof geolocation to fixed coordinates.",
"permissions": ["<all_urls>", "geolocation"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
const scriptContent = `
(function() {
const latitude = 58.2472;
const longitude = 8.0336;
const fakePosition = {
coords: {
latitude: latitude,
longitude: longitude,
altitude: null,
accuracy: 50,
altitudeAccuracy: null,
heading: null,
speed: null
},
timestamp: Date.now()
};
navigator.geolocation.getCurrentPosition = function(success, error) {
if (typeof success === 'function') {
success(fakePosition);
}
};
navigator.geolocation.watchPosition = function(success, error) {
const id = Math.floor(Math.random() * 10000);
if (typeof success === 'function') {
success(fakePosition);
}
return id;
};
})();
`;
const script = document.createElement('script');
script.textContent = scriptContent;
(document.head || document.documentElement).appendChild(script);
script.remove();
Проверяем:
На самом деле это было не так просто, ибо простая инъекция JavaScript не сработала — Firefox упорно не хотел подменять геолокацию. Заработало только прямое внедрение через DOM, и вот как это устроено. В content.js всё начинается с создания объекта fakePosition, где я задал фиксированные координаты — широту 58.2472 и долготу 8.0336, а также параметры вроде accuracy: 50 и timestamp: Date.now(), чтобы подмена выглядела правдоподобно. Затем я перехватываю методы navigator.geolocation.getCurrentPosition и navigator.geolocation.watchPosition. Первый просто вызывает переданную функцию success с фейковыми координатами, а второй ещё возвращает случайный ID, чтобы эмулировать отслеживание. Код оборачивается в строку scriptContent и вставляется в DOM через созданный элемент script, который добавляется в head или documentElement и сразу удаляется, чтобы не оставлять следов. Это позволяет подменить геолокацию на всех сайтах ещё до их загрузки, благодаря настройке run_at: document_start в manifest.json.
Если не хочешь возиться с написанием своего плагина для подмены геолокации, можно взять готовое решение, и из рабочих я нашёл Location Guard — отличный вариант, доступный по ссылке https://addons.mozilla.org/en-US/firefox/addon/location-guard/. Этот плагин позволяет легко подменить координаты, чтобы сайты видели тебя там, где ты хочешь.
После установки Location Guard геолокация не меняется мгновенно — у меня ничего не сдвинулось, пока я не залез в настройки плагина. Для этого кликаешь на иконку расширения, выбираешь Options и в разделе Default level ставишь Use fixed location.
Там же можно указать конкретные координаты, например, через карту, или выбрать готовую точку — по умолчанию это Гринвич, но ты вольёшь задать что угодно:
Чтобы проверить, сработала ли подмена, открываешь, к примеру https://www.gps-coordinates.net/my-location, который запрашивает геолокацию, и смотришь, какие координаты он показывает:
На последок я решил показать, как написать скрипт для Tampermonkey, который подменяет геолокацию в Firefox, чтобы сайты видели фейковые координаты вместо твоих настоящих.
Сам код:
JavaScript:
// ==UserScript==
// @name Fake Geolocation
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Spoof geolocation in Firefox
// @author hipeople
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const fakeCoords = {
latitude: 58.2472,
longitude: 8.0336,
accuracy: 100
};
Object.defineProperty(navigator, 'geolocation', {
value: {
getCurrentPosition: function(callback, error, options) {
callback({
coords: fakeCoords,
timestamp: Date.now()
});
},
watchPosition: function(callback, error, options) {
callback({
coords: fakeCoords,
timestamp: Date.now()
});
return 1;
},
clearWatch: function(id) {
}
},
writable: false
});
})();
Проверяем работает ли скрипт:
Сначала в коде, Я задаю объект fakeCoords с фиксированными координатами: широта 58.2472, долгота 8.0336 и точность 100 метров, чтобы подмена выглядела правдоподобно. Затем через Object.defineProperty я переопределяю свойство navigator.geolocation, делая его неизменяемым (writable: false). Новый объект geolocation подменяет три метода: getCurrentPosition, watchPosition и clearWatch.
Метод getCurrentPosition вызывает переданную функцию callback, возвращая объект с фейковыми координатами и текущей временной меткой через Date.now(). Метод watchPosition делает то же самое, но возвращает фиксированный ID (1), эмулируя отслеживание. Метод clearWatch пустой, так как в нашей подмене нет реального отслеживания, но он нужен для совместимости. Это заставляет сайты, запрашивающие геолокацию, видеть заданные координаты, будто ты реально там находишься.
Скрипт работает, потому что он вмешивается в объект navigator.geolocation на уровне JavaScript, перехватывая запросы сайтов до того, как браузер обратится к системным API.
Но честно, я так и не понял, зачем в приватном браузере вроде Firefox вообще нужен доступ к геолокации - это же как оставить дверь нараспашку для трекеров. Поэтому проще всего заблокировать эту функцию раз и навсегда через user.js или about:config и жить спокойно, не парясь о том, что кто-то вычислит твоё местоположение. Всё, что нужно, создать или отредактировать файл user.js в профиле Firefox и прописать пару строк, которые вырубят геолокацию наглухо.
Вот содержимое моего user.js:
JavaScript:
user_pref("geo.enabled", false);
user_pref("geo.provider.network.url", "");
user_pref("privacy.resistFingerprinting", true);
Но если хочешь ещё больше спокойствия, мой совет — ставь Firefox через пакетный менеджер вроде apt, а не из tar-архива с сайта Mozilla. Почему? Пакетные версии, особенно в дистрибутивах вроде Ubuntu, часто идут через snap, который добавляет песочницу и может ломать геолокацию из-за своих ограничений. Это как бонус: трекеры точно не доберутся до твоих координат, даже если что-то пойдёт не по плану.
Смена локального IP
Когда мы разобрались со сменой местоположения. Давай разберёмся, как обстоят дела с подменой локального IP в Firefox, и почему это не так просто, как кажется. Если хочешь, чтобы сайты не получили твой реальный IP через ICE-кандидаты WebRTC, то тут Firefox тебя приятно удивит - по умолчанию он блокирует такие попытки, и я подозреваю, что это связано с его настройками приватности или особенностями сборки. А вот заголовок X-Forwarded-For, который иногда используют для определения IP, всё же можно подменить, и я расскажу, как это сделать.
Сначала почему не работает получение локального IP и WebRTC? ICE-кандидаты — это часть протокола WebRTC, который отвечает за установку соединений между устройствами. Они могут выдавать твой локальный IP, даже если ты сидишь за NAT или VPN, что для трекеров — как находка. Но в Firefox получить твой IP через WebRTC — задача не из лёгких. По умолчанию настройка media.peerconnection.enabled в about:config стоит в true, но Firefox активно защищает приватность. Например, с версии 68 он заменяет локальные IP в ICE-кандидатах на mDNS-имена, вроде 1f4712db-ea17-4bcf-a596-105139dfd8bf.local, что делает их бесполезными для сайтов. Плюс, если включён параметр privacy.resistFingerprinting (а в строгом режиме он активен), WebRTC вообще может быть ограничен или отключён.
Начнём говорить про подмену заголовка X-Forwarded-For, и для этого я написал плагин для Firefox, который заменяет его на случайный локальный IP. Вот как это устроено!
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "X-Forwarded-For Spoofer",
"version": "1.0",
"description": "Changes the X-Forwarded-For header to a random local IP address.",
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"background": {
"scripts": ["content.js"]
}
}
content.js:
JavaScript:
function generateRandomLocalIP() {
const localRanges = [
{ start: "192.168.0.", end: 255 },
{ start: "10.0.0.", end: 255 },
{ start: "172.16.0.", end: 255 }
];
const range = localRanges[Math.floor(Math.random() * localRanges.length)];
return range.start + Math.floor(Math.random() * range.end);
}
browser.webRequest.onBeforeSendHeaders.addListener(
function(details) {
let headers = details.requestHeaders;
let hasXForwardedFor = false;
for (let header of headers) {
if (header.name.toLowerCase() === "x-forwarded-for") {
header.value = generateRandomLocalIP();
hasXForwardedFor = true;
break;
}
}
if (!hasXForwardedFor) {
headers.push({
name: "X-Forwarded-For",
value: generateRandomLocalIP()
});
}
return { requestHeaders: headers };
},
{ urls: ["<all_urls>"] },
["blocking", "requestHeaders"]
);
В content.js функция generateRandomLocalIP создаёт случайный IP из диапазонов частных сетей: 192.168.0.0–255, 10.0.0.0–255 или 172.16.0.0–255, выбирая случайный диапазон и добавляя к нему число для последней части, например, 192.168.0.123. Обработчик browser.webRequest.onBeforeSendHeaders проверяет заголовки запроса: если X-Forwarded-For уже есть, он заменяет его значение на новый IP, если нет — добавляет заголовок с фейковым IP. Параметр "blocking" позволяет менять запросы до отправки, а "requestHeaders" даёт доступ к заголовкам.
Провяремя работет ли плагин на https://browserleaks.com/ip:
Если не хочешь писать свой плагин для подмены заголовков, можно взять готовое решение вроде Header Editor, доступного по ссылке https://addons.mozilla.org/en-US/firefox/addon/header-editor/. Этот плагин — не только для X-Forwarded-For, он позволяет менять любые HTTP-заголовки.
Header Editor работает как редактор правил для HTTP-запросов и ответов. После установки ты создаёшь правило, указываешь тип заголовка, его значение и условия, при которых подмена срабатывает. Плагин перехватывает запросы браузера до их отправки и применяет заданные изменения, позволяя подставить любой IP в X-Forwarded-For или даже создать новый заголовок. Всё настраивается через удобный интерфейс, без копания в коде.
Скачай плагин и кликни на его иконку в панели Firefox, затем выбери Manage.
Дальше нажми на “+”, чтобы создать новое правило для подмены заголовка:
Теперь время настроить конфигурацию плагина. В разделе Rule type выбери Modify request header — это нужно, чтобы создать или изменить заголовок запроса. В Match type поставь all, чтобы подмена работала для всех доменов. В Execute type выбери normal, чтобы правило применялось стандартно.
Затем в поле имени заголовка укажи x-forwarded-for (регистр не важен) и введи нужное значение, например, 192.168.1.100 или любой другой IP, который хочешь подставить:
После сохранения правила Header Editor начнёт подменять X-Forwarded-For для всех исходящих запросов. Это работает, потому что плагин использует API webRequest Firefox, которое позволяет перехватывать и модифицировать заголовки до их отправки на сервер. Но учти: если сайт проверяет IP другими способами, например, через серверные заголовки, подмена может не сработать.
Честно, я так и не решил, насколько хорошая идея постоянно подменять заголовок X-Forwarded-For. Сайты далеко не всегда его запрашивают, и, если ты везде пихаешь фейковый IP, это может выглядеть подозрительно — как будто ты слишком уж стараешься спрятаться. Но в некоторых случаях, когда нужно запутать трекеры или обойти проверки, такая подмена реально выручает. Главное — не злоупотребляй, комбинируй с другими мерами, и думай, где это уместно, чтобы не светиться лишний раз!
Подмена параметорв соединения
Дальше мог бы быть раздел про скрытие navigator.connection, но Mozilla опередила всех и просто не добавила этот API в Firefox, сославшись на соображения приватности и безопасности. Обсуждение этого решения можно найти в баг-трекере по ссылке https://bugzilla.mozilla.org/show_bug.cgi?id=960426, и я, честно, этому только рад. Firefox в очередной раз доказывает, что в плане защиты данных он на шаг впереди того же Chrome. Даже если в about:config включить параметр dom.netinfo.enabled в true, чтобы попытаться получить параметры сети, браузер всё равно выдаёт минимум информации.
Например, если запустить Firefox, установленный через пакетный менеджер в Linux, ты получишь что-то вроде:
Код:
NetworkInformation { type: "none", ontypechange: null }
А если взять версию Firefox прямо с сайта Mozilla, результат будет:
Код:
NetworkInformation { type: "unknown", ontypechange: null }
В обоих случаях никаких полезных данных о сети — ни скорости, ни типа подключения. Это значит, что параметры твоей сети остаются неизвестными для создания отпечатка, и трекеры не смогут получить из браузера ничего ценного.
Смена размера экрана
Теперь, когда мы разобрались с подменой сетевых данных, давай обсудим с тобой подмену размера экрана. К сожалению или к счастью, Firefox по умолчанию не скрывает эту информацию, и сайты могут легко получить параметры вроде screen.width и screen.height через JavaScript.
Сначала разберу, как скрыть размер экрана в Firefox с помощью about:config, а точнее, через более удобный вариант — файл user.js. Для начала создай или отредактируй файл user.js в профиле Firefox и пропиши туда несколько важных настроек.
Вот пример, который ты можешь использовать, или те же параметры можно выставить прямо в about:config:
JavaScript:
user_pref("privacy.resistFingerprinting", true);
user_pref("privacy.window.maxInnerWidth", 900);
user_pref("privacy.window.maxInnerHeight", 600);
user_pref("layout.css.devPixelsPerPx", "-1.0");
Так же можно написать плагин для подмены размера экрана в Firefox, чтобы сайты видели фейковые параметры разрешения вместо настоящих.
Пример плагина:
manifest.json:
JSON:
{
"manifest_version": 3,
"name": "Fake Screen Size",
"version": "1.0",
"description": "Spoofs screen size in Firefox",
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"run_at": "document_start"
}
]
}
content-script.js:
JavaScript:
(function() {
const fakeScreen = {
width: 1920,
height: 1080,
availWidth: 1920,
availHeight: 1080,
colorDepth: 24,
pixelDepth: 24,
top: 0,
left: 0,
availTop: 0,
availLeft: 0,
orientation: {
type: 'landscape-primary',
angle: 0,
onchange: null
},
mozOrientation: 'landscape-primary',
mozLockOrientation: () => true,
mozUnlockOrientation: () => true
};
const scriptContent = `
(() => {
const fakeScreen = ${JSON.stringify(fakeScreen)};
const screenProxy = new Proxy(fakeScreen, {
get(target, prop) {
if (prop in target) return target[prop];
if (prop === 'toString') return () => '[object Screen]';
return undefined;
}
});
Object.defineProperty(window, 'screen', {
value: screenProxy,
writable: false,
configurable: true
});
Object.defineProperty(window, 'innerWidth', {
value: 1920,
writable: false,
configurable: true
});
Object.defineProperty(window, 'innerHeight', {
value: 1080,
writable: false,
configurable: true
});
Object.defineProperty(window, 'devicePixelRatio', {
value: 1,
writable: false,
configurable: true
});
console.log('Screen spoofing applied');
})();
`;
const script = document.createElement('script');
script.textContent = scriptContent;
document.documentElement.appendChild(script);
script.remove();
})();
Плагин состоит из двух файлов: manifest.json и content-script.js, в manifest.json конфигурация, в content-script.js функции. Скрипт работает так: в content-script.js я создаю строку scriptContent, которая содержит код для подмены. Она превращает fakeScreen в Proxy, чтобы перехватывать обращения к свойствам объекта screen и возвращать только заданные значения, а для метода toString выдавать “[object Screen]” для правдоподобности. Затем через Object.defineProperty я заменяю window.screen на этот Proxy, а также фиксирую window.innerWidth, window.innerHeight и window.devicePixelRatio значениями 1920, 1080 и 1 соответственно, делая их неизменяемыми (writable: false). Код внедряется в страницу через элемент script, который добавляется в documentElement и сразу удаляется, чтобы не оставлять следов. Это работает, потому что подмена происходит на уровне JavaScript до загрузки страницы, и сайты видят фейковые параметры экрана, например, 1920x1080, вместо реальных.
Ещё можно создать скрипт для Tampermonkey как альтернативу плагину, чтобы подменить размер экрана в Firefoxк. Это подходит как способ обойтись без написания полноценного плагина, и при этом подменить размер экрана.
Пример скрипта:
JavaScript:
// ==UserScript==
// @name Screen Size Spoofer
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Spoofs screen size in Firefox
// @author hieople
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
const fakeScreen = {
width: 1920,
height: 1080,
availWidth: 1920,
availHeight: 1080,
colorDepth: 24,
pixelDepth: 24,
top: 0,
left: 0,
availTop: 0,
availLeft: 0,
orientation: {
type: 'landscape-primary',
angle: 0,
onchange: null
},
mozOrientation: 'landscape-primary',
mozLockOrientation: () => true,
mozUnlockOrientation: () => true
};
const screenProxy = new Proxy(fakeScreen, {
get(target, prop) {
if (prop in target) {
return target[prop];
}
if (prop === 'toString') {
return () => '[object Screen]';
}
return undefined;
}
});
try {
Object.defineProperty(window, 'screen', {
value: screenProxy,
writable: false,
configurable: true
});
} catch (e) {
console.warn('Failed to redefine window.screen:', e);
}
try {
Object.defineProperty(window, 'innerWidth', {
value: 1920,
writable: false,
configurable: true
});
Object.defineProperty(window, 'innerHeight', {
value: 1080,
writable: false,
configurable: true
});
Object.defineProperty(window, 'devicePixelRatio', {
value: 1,
writable: false,
configurable: true
});
} catch (e) {
console.warn('Failed to redefine window properties:', e);
}
console.log('Screen spoofing applied:', window.screen);
})();
Скрипт работает похоже на плагин: В заголовке указано, что скрипт запускается на всех URL (@match :///*) на этапе document-start, чтобы подмена произошла до загрузки страницы. В самом скрипте я создаю объект fakeScreen с фейковыми параметрами: разрешение 1920x1080, доступные размеры те же, глубина цвета 24 бита, нулевые отступы и ориентация landscape-primary, плюс методы mozLockOrientation и mozUnlockOrientation для совместимости с Firefox.
Объект fakeScreen оборачивается в Proxy, который перехватывает обращения к свойствам и возвращает заданные значения, а для toString выдаёт “[object Screen]” для правдоподобности. Затем через Object.defineProperty я заменяю window.screen на этот Proxy, а также фиксирую window.innerWidth, window.innerHeight и window.devicePixelRatio значениями 1920, 1080 и 1, делая их неизменяемыми (writable: false). Всё это обёрнуто в try-catch, чтобы поймать ошибки, если Firefox блокирует подмену, с выводом предупреждений в консоль. После применения подмены скрипт логирует результат. Это работает, потому что код вмешивается в JavaScript страницы, подменяя свойства экрана до их запроса сайтом.
Если ты хочешь отойти от JavaScript и подменить размер экрана в Firefox средствами Linux, покажу как с помщью bash скрипта запускать браузер с заданными параметрами окна, чтобы сайты видели фейковый размер вместо реального.
Пример кода:
Bash:
#!/bin/bash
FIREFOX_BIN="firefox"
WIDTH=800
HEIGHT=600
PROFILE_PATH=$(find "$HOME/.mozilla/firefox" -maxdepth 1 -type d -name "*.default-esr" | head -n 1)
if [ -z "$PROFILE_PATH" ]; then
echo "Профиль с .default-esr не найден в ~/.mozilla/firefox/"
exit 1
fi
echo "Найден профиль: $PROFILE_PATH"
"$FIREFOX_BIN" \
--no-remote \
--new-instance \
--width "$WIDTH" \
--height "$HEIGHT" \
--profile "$PROFILE_PATH"
Этот код просто запускает Firefox с флагами, задающими нужный размер окна. Сначала скрипт определяет переменную FIREFOX_BIN, указывая путь к исполняемому файлу браузера (обычно просто “firefox”). Затем я задаю ширину (WIDTH=800) и высоту (HEIGHT=600) окна, чтобы имитировать небольшой экран (но вы можете написать любой).
Дальше код ищет профиль Firefox с суффиксом .default-esr в папке ~/.mozilla/firefox, используя команду find с ограничением на первый уровень вложенности (-maxdepth 1). Если профиль не найден, скрипт выводит ошибку и завершается. Если всё ок, он печатает путь к профилю и запускает Firefox с несколькими флагами: --no-remote предотвращает подключение к уже открытому экземпляру браузера, --new-instance создаёт новый экземпляр, --width и --height задают размеры окна, а --profile указывает на найденный профиль, чтобы сохранить настройки.
Это работает, потому что Firefox позволяет управлять размерами окна через командную строку, и сайты, запрашивающие window.innerWidth и window.innerHeight, увидят заданные 800x600 вместо реальных значений. Но учти: это влияет только на размер окна, а не на window.screen.width/height, так что для полной подмены экрана комбинируй скрипт с настройками privacy.resistFingerprinting в about:config или плагином.
Чтобы полноценно подменить размер экрана в Firefox на Linux, я решил пойти дальше и запустить браузер в фейковом окне с помощью утилиты Weston.
Пример кода:
Bash:
#!/bin/bash
WIDTH=1024
HEIGHT=576
PROFILE_PATH=$(find "$HOME/.mozilla/firefox" -maxdepth 1 -type d -name "*.default-esr" | head -n 1)
if [ -z "$PROFILE_PATH" ]; then
echo "Профиль не найден"
exit 1
fi
echo "Профиль: $PROFILE_PATH"
weston --width=$WIDTH --height=$HEIGHT --socket=wayland-fake --backend=x11-backend.so --no-config &
WESTON_PID=$!
sleep 2
export WAYLAND_DISPLAY=wayland-fake
MOZ_ENABLE_WAYLAND=1 firefox --no-remote --profile "$PROFILE_PATH" &
wait
kill "$WESTON_PID"
Проверяем:
Этот код работает с помощью утилиты Weston, которая нужна для создания виртуального дисплея в Linux. Weston — это композитор Wayland, но в данном случае он используется с X11-бэкендом (x11-backend.so), чтобы эмулировать дисплей с заданным разрешением. Это позволяет запустить Firefox в изолированном окружении, где сайты видят фейковый экран, например, 1024x576, вместо реального разрешения твоего монитора.
Сначала скрипт задаёт ширину (WIDTH=1024) и высоту (HEIGHT=576) фейкового дисплея. Затем он ищет профиль Firefox с суффиксом .default-esr в папке ~/.mozilla/firefox через команду find, ограниченную первым уровнем вложенности. Если профиль не найден, скрипт выводит ошибку и завершается. Если всё ок, он печатает путь к профилю.
Дальше запускается Weston с параметрами: --width и --height задают размер дисплея, --socket=wayland-fake создаёт уникальный сокет для Wayland, --backend=x11-backend.so указывает на X11-бэкенд, а --no-config отключает конфигурационный файл, чтобы использовать только командные параметры. Процесс Weston запускается в фоновом режиме (&), и его PID сохраняется в переменную WESTON_PID для последующего завершения. Пауза в 2 секунды через sleep даёт Westo времени инициализироваться.
Затем скрипт устанавливает переменную WAYLAND_DISPLAY=wayland-fake, чтобы Firefox подключился к фейковому дисплею. Переменная MOZ_ENABLE_WAYLAND=1 включает поддержку Wayland в Firefox, что необходимо для работы с Weston. Браузер запускается с флагом --no-remote, чтобы не цепляться к уже открытому экземпляру, и --profile, указывающим на найденный профиль. Запуск в фоновом режиме (&) позволяет скрипту дождаться завершения Firefox через wait. После этого процесс Weston завершается через kill.
Подделка плагинов
Чтож, пришло время рассказать про подделку плагинов в Firefox (ну, точнее, не совсем подделку, а скорее маскировку)
История с обнаружением плагинов сайтами в Firefox довольно занятная. В отличие от Chrome, где у плагинов есть фиксированные ID, в Firefox плагины не имеют постоянного id, по которому их легко вычислить. Есть объект navigator.plugins, но он выдаёт фейковые данные, вроде встроенного pdfviewer, который не несёт реальной информации. Раньше, до 2017–2019 годов, navigator.plugins показывал реальные плагины, работавшие через NPAPI. Но всё изменилось, сначала с версии Firefox 52(март 2017) Mozilla убрала поддержку NPAPI, кроме Flash, и navigator.plugins стал почти беспользным. С версии 85 (январь 2021) Flash тоже ушёл, и теперь объект содержит только встроенные модули, такие как PDF Viewer (PDF.js), Widevine (для Netflix) и иногда OpenH264 (кодек H.264). Эти "плагины" — фиктивные, для совместимости.
Ты, наверное, спросишь: как тогда сайты находят, например, AdBlock? А тут самое интересное — они делают это по косвенным признакам. Плагины часто оставляют следы, такие как библиотеки (например, blockADBlock.js для AdBlock) или изменения в DOM. К примеру, AdBlock вычисляют, проверяя наличие рекламных классов вроде .adsbox на странице. Если эти классы отсутствуют или скрыты, сайт делает вывод, что стоит блокировщик. А Tampermonkey могут засечь по вмешательствам в DOM, хотя это не всегда срабатывает из-за разных скриптов.
Но как же это скрыть? Для своего плагина можно ограничить доступ к ресурсам расширения, убрав их из web_accessible_resources в manifest.json.
Вот пример:
JSON:
{
"manifest_version": 2,
"name": "My Extension",
"version": "1.0",
"web_accessible_resources": []
}
Либо, минимизируй следы в DOM: избегай компрометирующих названий файлов или стандартных путей, вроде content.js, и не используй очевидные библиотеки. Но, честно, полностью скрыть расширение вряд ли выйдет — сайты всё равно могут заметить косвенные изменения, вроде отсутствия рекламы или модификаций скриптов.
Ниже я покажу, как подменить объект navigator.plugins в JavaScript, но, если честно, не думаю, что это полезно(по крайней мере для новых версий Firefox). Такая подмена скорее сделает твой отпечаток более уникальным, чем наоборот, ведь сайты редко полагаются на этот объект в Firefox. Лучше сосредоточься на минимизации следов и настройках приватности. Например, включи privacy.resistFingerprinting, ведь он делает navigator.plugins одинаковым для всех, чтобы твой отпечаток был менее уникальным.
Первым делом расскажу про подмену объекта navigator.plugins с помощью Tampermonkey.
Пример кода:
JavaScript:
// ==UserScript==
// @name Fake Navigator Plugins
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Fake navigator.plugins in Firefox
// @author hipeople
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const fakePlugins = [
{
name: "Fake Plugin",
filename: "fakeplugin.dll",
description: "A fake plugin for testing",
version: "1.0.0",
length: 1,
item: function(index) { return this; },
namedItem: function(name) { return this; }
},
{
name: "Another Fake Plugin",
filename: "anotherfakeplugin.so",
description: "Another fake plugin",
version: "2.0.0",
length: 1,
item: function(index) { return this; },
namedItem: function(name) { return this; }
}
];
const fakePluginArray = {
length: fakePlugins.length,
item: function(index) { return fakePlugins[index]; },
namedItem: function(name) {
return fakePlugins.find(plugin => plugin.name === name);
},
refresh: function() {}
};
fakePlugins.forEach((plugin, index) => {
fakePluginArray[index] = plugin;
});
Object.defineProperty(window.navigator, 'plugins', {
value: fakePluginArray,
writable: false,
configurable: true
});
})();
Проверяем:
Этот код работает так: он запускается через Tampermonkey так чтобы подмена произошла до любых проверок сайтов. В самом скрипте я создаю массив fakePlugins с двумя фейковыми плагинами: “Fake Plugin” и “Another Fake Plugin”. У каждого есть свойства name, filename, description, version и методы item и namedItem, чтобы они выглядели как настоящие объекты Plugin. Затем я создаю fakePluginArray, который имитирует PluginArray с полями length, item, namedItem и пустым методом refresh. Через цикл плагины добавляются в fakePluginArray, чтобы их можно было получать по индексу, как в настоящем массиве.
В конце я использую Object.defineProperty, чтобы заменить navigator.plugins на fakePluginArray и сделать его неизменяемым (writable: false). Это работает, потому что подмена происходит до того, как сайты запрашивают данные, и они видят фейковый список вместо встроенных модулей, таких как PDF Viewer.
Ещё можно подменять объект navigator.plugins с помощью собственного плагина. Я написал плагин, которое делает чтобы показать, как работает подмена на его уровне. Погнали разбираться, как оно работает!
Вот пример плагина manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Fake Plugins Extension",
"version": "1.0",
"description": "Fakes navigator.plugins in Firefox",
"permissions": [
"activeTab",
"webNavigation",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
(function() {
'use strict';
const spoofScript = `
(function() {
const fakePlugins = [
{
name: "Fake Plugin",
filename: "fakeplugin.dll",
description: "A fake plugin for testing",
version: "1.0.0",
length: 1,
item(index) {
return this;
},
namedItem(name) {
return this;
}
},
{
name: "Another Fake Plugin",
filename: "anotherfakeplugin.so",
description: "Another fake plugin",
version: "2.0.0",
length: 1,
item(index) {
return this;
},
namedItem(name) {
return this;
}
}
];
const fakePluginArray = {
length: fakePlugins.length,
item(index) {
return fakePlugins[index];
},
namedItem(name) {
return fakePlugins.find(plugin => plugin.name === name);
},
refresh() {}
};
fakePlugins.forEach((plugin, index) => {
fakePluginArray[index] = plugin;
});
const fakeMimeTypes = [
{ type: "application/x-fake-plugin", enabledPlugin: fakePlugins[0] },
{ type: "application/x-another-fake-plugin", enabledPlugin: fakePlugins[1] }
];
const fakeMimeTypeArray = {
length: fakeMimeTypes.length,
item(index) {
return fakeMimeTypes[index];
},
namedItem(name) {
return fakeMimeTypes.find(mime => mime.type === name);
}
};
Object.defineProperty(window.navigator, 'plugins', {
value: fakePluginArray,
writable: false,
configurable: true
});
Object.defineProperty(window.navigator, 'mimeTypes', {
value: fakeMimeTypeArray,
writable: false,
configurable: true
});
})();
`;
const scriptElement = document.createElement('script');
scriptElement.textContent = spoofScript;
(document.head || document.documentElement).appendChild(scriptElement);
scriptElement.remove();
})();
Весь функционал в content.js — там происходит подмена на уровне DOM. Первым делом я создаю строку spoofScript, которая содержит код для подмены. Она формирует массив fakePlugins с двумя фейковыми плагинами: “Fake Plugin” и “Another Fake Plugin”, каждый с полями name, filename, description, version и методами item и namedItem, чтобы имитировать объект Plugin. Затем создаётся fakePluginArray, эмулирующий PluginArray с полями length, item, namedItem и пустым refresh. Плагины добавляются в fakePluginArray через цикл для поддержки индексированного доступа.
Кроме того, я подменяю navigator.mimeTypes, создавая fakeMimeTypes с типами “application/x-fake-plugin” и “application/x-another-fake-plugin”, связанными с фейковыми плагинами. Объект fakeMimeTypeArray имитирует MimeTypeArray с полями length, item и namedItem. Через Object.defineProperty свойства navigator.plugins и navigator.mimeTypes заменяются на фейковые объекты, с writable: false для защиты от изменений. Код внедряется в DOM через элемент script, который добавляется в head или documentElement и сразу удаляется.
Это работает, потому что подмена происходит до запросов сайтов, и они видят фейковый список плагинов и MIME-типов вместо настоящих.
Подмена косвенных параметров Firefox
Теперь давай разберёмся с подменой косвенных параметров в Firefox, а именно navigator.buildID, navigator.product и InstallTrigger. Эти штуки могут выдать, что ты сидишь в Firefox, а я хочу, чтобы сайты думали, будто ты используешь Chrome.
В Firefox navigator.buildID возвращает идентификатор сборки браузера, например, дату вроде “20250625000000”, что сразу кричит: “Я Firefox!”. В Chrome этого свойства вообще нет, так что его наличие — красный флаг для трекеров. Параметр navigator.product обычно возвращает пустое знчение в Firefox, а в Chrome — “Gecko”. А InstallTrigger — это объект, уникальный для Firefox, который позволяет устанавливать расширения через скрипты, и в Chrome его тоже нет. Если сайт проверяет эти параметры, он легко вычислит твой браузер.
Чтобы подделать эти параметры под Chrome, я написал скрипт для Tampermonkey, который перехватывает их через JavaScript.
Вот как это выглядит:
JavaScript:
// ==UserScript==
// @name Spoof Chrome in Firefox
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Spoof navigator.buildID, navigator.product and InstallTrigger to mimic Chrome
// @author hipeople
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
Object.defineProperty(navigator, 'buildID', {
value: undefined,
writable: false,
configurable: true
});
Object.defineProperty(navigator, 'product', {
value: 'Gecko',
writable: false,
configurable: true
});
Object.defineProperty(window, 'InstallTrigger', {
value: false,
writable: false,
configurable: true
});
})();
Проверяем:
Скрипт использует Object.defineProperty для переопределения трёх параметров. Сначала navigator.buildID задаётся как undefined, поскольку в Chrome этого свойства нет, а в Firefox оно выдаёт дату сборки, вроде “20250625000000”. Затем navigator.product меняется на “Gecko”, чтобы имитировать Chrome. Наконец, window.InstallTrigger, уникальный объект Firefox для установки расширений, устанавливается в false, чтобы сайты не видели его — в Chrome такого свойства нет. Все свойства делаются неизменяемыми (writable: false), чтобы предотвратить их модификацию сайтом. Это работает, потому что скрипт вмешивается в JavaScript страницы, подменяя параметры до их запроса.
Еще эти параметры можно подменять на уровне плагинов. Я решил написать расширение для Firefox, которое подменяет параметры navigator.buildID, navigator.product, navigator.vendor и InstallTrigger.
Вот сам плагин:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Spoof Chrome in Firefox",
"version": "1.0",
"description": "Spoofs navigator.buildID, navigator.product, and InstallTrigger to mimic Chrome",
"permissions": [
"activeTab",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
}
]
}
content.js:
JavaScript:
(function() {
'use strict';
const spoofScript = `
(function() {
const spoofedValues = {
buildID: undefined,
product: 'Gecko',
vendor: 'Google Inc.',
mozGetUserMedia: undefined
};
const spoofedNavigator = new Proxy(navigator, {
get(target, prop) {
if (prop in spoofedValues) {
return spoofedValues[prop];
}
return target[prop];
}
});
try {
Object.defineProperty(window, 'navigator', {
value: spoofedNavigator,
writable: false,
configurable: true
});
} catch (e) {
for (const [prop, value] of Object.entries(spoofedValues)) {
try {
Object.defineProperty(navigator, prop, {
value,
writable: false,
configurable: true
});
} catch (err) {}
}
}
try {
Object.defineProperty(window, 'InstallTrigger', {
value: false,
writable: false,
configurable: true
});
} catch (e) {}
try {
window.chrome = { runtime: {}, webstore: {} };
} catch (e) {}
try {
Object.defineProperty(window, 'Navigator', {
value: function() { return spoofedNavigator; },
writable: false,
configurable: true
});
} catch (e) {}
})();
`;
try {
const script = document.createElement('script');
script.textContent = spoofScript;
(document.head || document.documentElement).appendChild(script);
script.remove();
} catch (e) {}
})();
Плагин состоит из двух файлов: manifest.json и content.js. В manifest.json я указал версию протокола 2, название “Spoof Chrome in Firefox” и права на доступ к активной вкладке и всем сайтам (<all_urls>).
Скрипт content.js запускается на этапе document_start. Код ы нем пытается заменить window.navigator на Proxy через Object.defineProperty, делая его неизменяемым (writable: false). Если это не срабатывает, он подменяет каждое свойство по отдельности. Затем window.InstallTrigger устанавливается в false, чтобы убрать следы Firefox, а window.chrome добавляется как пустой объект с полями runtime и webstore для большей правдоподобности. Наконец, window.Navigator переопределяется, чтобы возвращать spoofedNavigator. Всё обёрнуто в try-catch, чтобы избежать сбоев, если Firefox заблокирует подмену. Скрипт внедряется через элемент script, который добавляется в head или documentElement и сразу удаляется.
Ещё один вариант - скачать Firefox с https://ftp.mozilla.org/pub/firefox/releases/, и ходит слух, что в таких версиях navigator.buildID якобы отсутствует, но это не совсем правда.
Скачивая Firefox с официального FTP-архива Mozilla, ты можешь взять старую версию, и в некоторых из них buildID действительно не возвращался через navigator.buildID — просто undefined, как в Chrome. Это могло помочь маскировать браузер, ведь buildID в Firefox выдаёт дату сборки, например, “20250625000000”, что сразу обноруживает, что ты не на Chrome. Но вот подвох: в новых версиях Firefox, даже если качаешь с FTP, buildID всё равно есть, и подмена требует дополнительных телодвижений, вроде тех что я описал выше.
Подмена системных параметров
Чтож, давай поговорим про подмену параметров не самого браузера, а нашей системы, которые Firefox выдаёт через объект navigator. Речь о navigator.oscpu, который показывает платформу, hardwareConcurrency, раскрывающий количество ядер процессора, platform, тоже указывающий на систему, и deviceMemory, который в Firefox по умолчанию заблокирован. Но вот парадокс: отсутствие deviceMemory само по себе может выдать, что ты сидишь в Firefox, что для трекеров как красная тряпка. Поэтому я считаю, что лучше подделать все эти параметры, чтобы они выглядели, как будто ты на стандартной Windows-машине, — так твой отпечаток станет менее уникальным.
Первым делом расскажу про то, как подменить параметры системы в Firefox через about:config - да, не удивляйся, такая возможность есть, и в этом плане Firefox реально молодец. В about:config можно подправить всё, что касается информации об ОС, а именно navigator.oscpu и navigator.platform, чтобы сайты видели фейковые данные. Для настройки я использую файл user.js в профиле Firefox, который позволяет задать параметры автоматически.
Вот пример содержимого user.js:
JavaScript:
user_pref("general.oscpu.override", "Windows NT 10.0; Win64; x64");
user_pref("general.platform.override", "Win32");
Ну и, конечно, стандартная подмена через about:config не даёт полной маскировки, так что решил я показать как написать плагин для Firefox, который подменяет системные параметры navigator, чтобы сайты видели фейковые данные вместо настоящих.
Пример плагина:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Navigator Spoofer",
"version": "1.0",
"description": "Spoofs navigator properties for privacy",
"permissions": [
"activeTab",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
(function() {
'use strict';
const script = document.createElement('script');
script.textContent = `
(function() {
'use strict';
Object.defineProperty(window.navigator, 'oscpu', {
configurable: true,
get: function() {
return 'Windows NT 10.0; Win64; x64';
}
});
Object.defineProperty(window.navigator, 'hardwareConcurrency', {
configurable: true,
get: function() {
return 6;
}
});
Object.defineProperty(window.navigator, 'platform', {
configurable: true,
get: function() {
return 'Win32';
}
});
Object.defineProperty(window.navigator, 'deviceMemory', {
configurable: true,
get: function() {
return 8;
}
});
})();
`;
(document.head || document.documentElement).appendChild(script);
})();
Проверяем:
Этот плагин работает, заменяя значения navigator на фейковые, чтобы сайты видели тебя как пользователя Windows с типичными характеристиками. В manifest.json я указал версию протокола 2, название “Navigator Spoofer” и права на доступ к активной вкладке и всем сайтам (<all_urls>). Скрипт content.js запускается на этапе document_start, чтобы подмена произошла до любых запросов со стороны сайта.
В content.js я создаю элемент script и внедряю в него код, который выполняется на уровне страницы. Он использует Object.defineProperty для переопределения четырёх свойств navigator: oscpu задаётся как “Windows NT 10.0; Win64; x64”, чтобы имитировать 64-битную Windows, hardwareConcurrency возвращает 6, как у среднего процессора, platform устанавливается в “Win32” для соответствия Windows, а deviceMemory выдаёт 8 (гигабайт), как в Chrome, чтобы скрыть отсутствие этого параметра в Firefox. Все свойства задаются через геттеры с configurable: true, чтобы их можно было подменить, но без возможности изменения сайтом (хотя writable не указан, так как это геттеры).
Другой способ подменить системные параметры без написания полноценного плагина — использовать Tampermonkey. Это удобный метод, если ты хочешь быстро подделать данные вроде navigator.oscpu, hardwareConcurrency, platform и deviceMemory.
Пример кoдa:
JavaScript:
// ==UserScript==
// @name Spoof oscpu, hardwareConcurrency, and deviceMemory
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Spoof navigator properties in Firefox
// @author hipeople
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
function spoofProperty(obj, prop, value) {
try {
if (Object.isFrozen(obj) || Object.isSealed(obj)) {
return false;
}
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
delete obj[prop];
}
Object.defineProperty(obj, prop, {
configurable: true,
get: function() {
return value;
}
});
return true;
} catch (e) {
return false;
}
}
const spoofs = {
oscpu: 'Windows NT 10.0; Win64; x64',
hardwareConcurrency: 6,
platform: 'Win32',
deviceMemory: 8
};
for (const [prop, value] of Object.entries(spoofs)) {
spoofProperty(navigator, prop, value);
}
const navigatorProxy = new Proxy(navigator, {
get(target, prop) {
if (prop in spoofs) {
return spoofs[prop];
}
return Reflect.get(target, prop);
}
});
try {
Object.defineProperty(window, 'navigator', {
configurable: true,
get: function() {
return navigatorProxy;
}
});
} catch (e) {}
})();
Затем я создаю Proxy для navigator, который перехватывает обращения к свойствам и возвращает фейковые значения из spoofs, если они есть, или оригинальные через Reflect.get. Прокси задаётся как window.navigator через Object.defineProperty, чтобы заменить весь объект, но это обёрнуто в try-catch на случай, если Firefox заблокирует подмену.
Подмена данных о видеокарте
Теперь я расскажу, как подделать WebGL Vendor и Renderer в Firefox — параметры, которые выдают информацию о твоей видеокарте и её драйвере, буквально крича сайтам, какое у тебя железо. Это чуть ли не 100% уникальный отпечаток, так что их подмена — важный шаг, чтобы запутать трекеры.
Тут Firefox со мной согласен, что параметры WebGL Vendor и Renderer — это не то, что стоит оставлять без изменения, ведь они выдают данные о твоей видеокарте, такие как производитель и модель. Поэтому в about:config Mozilla добавила настройки для их подмены.
Вот пример подмены в user.js:
JavaScript:
user_pref("webgl.override-unmasked-vendor", "NVIDIA Corporation");
user_pref("webgl.override-unmasked-renderer", "GeForce GTX 1060/PCIe/SSE2");
Тут я меняю параметры на NVIDIA, чтобы сайты видели фейковую видеокарту. Настройка webgl.override-unmasked-vendor задаёт WebGL Vendor как “NVIDIA Corporation”, а webgl.override-unmasked-renderer подменяет Renderer на “GeForce GTX 1060/PCIe/SSE2”, имитируя популярную видеокарту. Это работает на уровне исходного кода Firefox: когда сайт запрашивает данные через WEBGL_debug_renderer_info, браузер возвращает эти фейковые значения вместо реальных. Однако многие сайты используют gl.getParameter(gl.VENDOR) и gl.getParameter(gl.RENDERER), которые требуют подмены через плагин или Tampermonkey, как описано ниже.
Ещё есть нюанс: даже с подменой WebGL трекеры могут пытаться собирать другие данные, например, через рендеринг. Поэтому для полной защиты можно вообще вырубить WebGL через webgl.disabled, если графика на сайтах тебе не нужна.
Ещё можно написать плагин для подмены параметров WebGL Vendor и Renderer, чтобы сайты не могли вычислить твою видеокарту и драйвер. Я написал плагин для примера, погнали разбираться, как мой код это делает!
Сам плагин:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "WebGL Spoofer",
"version": "1.0",
"description": "Spoofs WebGL Vendor and Renderer",
"permissions": [
"activeTab",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
(function() {
'use strict';
if (!window.WebGLRenderingContext) {
console.warn('WebGL not supported in this browser.');
return;
}
function createSpoofingScript() {
const script = document.createElement('script');
script.textContent = `
(function() {
'use strict';
function spoofWebGL(context) {
if (!context || !context.getParameter) {
console.warn('Invalid WebGL context.');
return;
}
const originalGetParameter = context.getParameter;
context.getParameter = function(parameter) {
try {
if (parameter === undefined || parameter === null) {
console.warn('Invalid WebGL parameter:', parameter);
return null;
}
switch (parameter) {
case 0x9245:
return 'Spoofed 111';
case 0x9246:
return 'Spoofed 111';
default:
return originalGetParameter.call(this, parameter);
}
} catch (e) {
console.error('Error in getParameter spoof:', e);
return null;
}
};
const originalGetExtension = context.getExtension;
context.getExtension = function(name) {
try {
if (name === 'WEBGL_debug_renderer_info') {
return {
UNMASKED_VENDOR_WEBGL: 0x9245,
UNMASKED_RENDERER_WEBGL: 0x9246,
getParameter: function(parameter) {
try {
if (parameter === undefined || parameter === null) {
console.warn('Invalid debug info parameter:', parameter);
return null;
}
switch (parameter) {
case 0x9245:
return 'Spoofed 111';
case 0x9246:
return 'Spoofed 111';
default:
return null;
}
} catch (e) {
console.error('Error in debug getParameter:', e);
return null;
}
}
};
}
return originalGetExtension.call(this, name);
} catch (e) {
console.error('Error in getExtension spoof:', e);
return null;
}
};
}
[WebGLRenderingContext, WebGL2RenderingContext].forEach(context => {
if (context && context.prototype) {
spoofWebGL(context.prototype);
}
});
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, ...args) {
try {
const ctx = originalGetContext.call(this, type, ...args);
if (ctx && (type === 'webgl' || type === 'webgl2')) {
spoofWebGL(ctx.__proto__);
}
return ctx;
} catch (e) {
console.error('Error in getContext spoof:', e);
return null;
}
};
})();
`;
(document.head || document.documentElement).appendChild(script);
script.remove();
}
if (document.head || document.documentElement) {
createSpoofingScript();
} else {
window.addEventListener('DOMContentLoaded', createSpoofingScript, { once: true });
}
})();
Проверяем:
Плагин работает так, что подменяет WebGL Vendor и Renderer, чтобы сайты видели фейковые данные вместо реальных характеристик видеокарты. Скрипт content.js запускается до загрузки страницы, чтобы подмена произошла до любых WebGL-запросов. В content.js я сначала проверяю, поддерживается ли WebGLRenderingContext, и, если нет, скрипт завершается с предупреждением. Функция createSpoofingScript создаёт элемент script и внедряет код, который выполняется на уровне страницы. Внутри я определяю функцию spoofWebGL, которая подменяет методы getParameter и getExtension для WebGLRenderingContext и WebGL2RenderingContext. Для getParameter я перехватываю параметры 0x9245 (GL_VENDOR) и 0x9246 (GL_RENDERER), возвращая “Spoofed 111” для обоих, а остальные запросы передаю оригинальному методу. Для getExtension я подменяю WEBGL_debug_renderer_info, возвращая объект с фейковыми значениями для Vendor и Renderer. Также я перехватываю HTMLCanvasElement.prototype.getContext, чтобы подменять WebGL-контексты (webgl и webgl2) при их создании. Код внедряется в head или documentElement, а если DOM ещё не готов, ждёт события DOMContentLoaded.
Подобный скрипт можно написать без плагина, используя Tampermonkey — это проще и не требует лишних импортов, а результат почти тот же.
Пример кода:
JavaScript:
// ==UserScript==
// @name Spoof WebGL Vendor and Renderer
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Spoof WebGL Vendor and Renderer in Firefox and other browsers
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
if (!window.WebGLRenderingContext) {
console.warn('WebGL not supported in this browser.');
return;
}
function spoofWebGL(context) {
if (!context || !context.getParameter) {
console.warn('Invalid WebGL context, skipping spoofing.');
return;
}
const originalGetParameter = context.getParameter;
context.getParameter = function(parameter) {
try {
// Проверяем валидность параметра
if (parameter === undefined || parameter === null) {
console.warn('Invalid WebGL parameter:', parameter);
return originalGetParameter.call(this, 0);
}
switch (parameter) {
case 0x9245: // UNMASKED_VENDOR_WEBGL
return 'Spoofed Vendor';
case 0x9246: // UNMASKED_RENDERER_WEBGL
return 'Spoofed Renderer';
default:
return originalGetParameter.call(this, parameter);
}
} catch (e) {
console.error('Error in getParameter spoof:', e);
return null;
}
};
const originalGetExtension = context.getExtension;
context.getExtension = function(name) {
try {
if (name === 'WEBGL_debug_renderer_info') {
return {
UNMASKED_VENDOR_WEBGL: 0x9245,
UNMASKED_RENDERER_WEBGL: 0x9246,
getParameter: function(parameter) {
try {
if (parameter === undefined || parameter === null) {
console.warn('Invalid debug info parameter:', parameter);
return null;
}
switch (parameter) {
case 0x9245:
return 'Spoofed Vendor';
case 0x9246:
return 'Spoofed Renderer';
default:
return null;
}
} catch (e) {
console.error('Error in debug getParameter:', e);
return null;
}
}
};
}
return originalGetExtension.call(this, name);
} catch (e) {
console.error('Error in getExtension spoof:', e);
return null;
}
};
}
[WebGLRenderingContext, WebGL2RenderingContext].forEach(context => {
if (context && context.prototype) {
spoofWebGL(context.prototype);
}
});
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, ...args) {
try {
const ctx = originalGetContext.call(this, type, ...args);
if (ctx && (type === 'webgl' || type === 'webgl2')) {
spoofWebGL(ctx.__proto__);
}
return ctx;
} catch (e) {
console.error('Error in getContext spoof:', e);
return null;
}
};
})();
Этот код работает почти аналогично плагину, но есть некоторые различия. Скрипт запускается через Tampermonkey на всех сайта на этапе document-start, чтобы подмена произошла до любых WebGL-запросов.
Сначала скрипт проверяет наличие WebGLRenderingContext, и если WebGL не поддерживается, завершается с предупреждением. Функция spoofWebGL подменяет методы getParameter и getExtension для WebGLRenderingContext и WebGL2RenderingContext. Для getParameter код перехватывает параметры 0x9245 (UNMASKED_VENDOR_WEBGL) и 0x9246 (UNMASKED_RENDERER_WEBGL), возвращая “Spoofed Vendor” и “Spoofed Renderer”, а остальные запросы передаёт оригинальному методу. Если параметр невалиден, возвращается безопасное значение. Для getExtension скрипт подменяет WEBGL_debug_renderer_info, создавая объект с фейковыми значениями Vendor и Renderer.
Также я перехватываю HTMLCanvasElement.prototype.getContext, чтобы подменять WebGL-контексты (webgl и webgl2) при их создании. Все действия обёрнуты в try-catch, чтобы ловить ошибки и выводить их в консоль. В отличие от плагина, здесь не используется внедрение через DOM-элемент script, а подмена применяется напрямую к прототипам, что делает код компактнее, но менее изолированным. Это работает, потому что скрипт вмешивается в WebGL API до запросов сайтов, возвращая фейковые данные видеокарты.
Подмена производителей
Дальше разберу, как подменить параметры navigator.productSub, navigator.vendorSub и navigator.vendor в Firefox, чтобы сайты не могли вычислить, что ты используешь именно этот браузер. Эти свойства выдают информацию о производителе и версии браузера.
Эти данные можно подменить через about:config, ну точнее не все, а конкретно navigator.productSub и navigator.vendorSub, чтобы сайты не вычислили, что ты сидишь в Firefox. Эти параметры выдают информацию о версии и сборке браузера, что трекеры используют для твоего цифрового отпечатка. Погнали разбираться, как их подделать через user.js!
Пример в user.js:
JavaScript:
user_pref("general.productSub.override", "20200101");
user_pref("general.vendorSub.override", "");
Как пример, вот как написать плагин для подмены всех параметров navigator — productSub, vendorSub и vendor — прямо перед загрузкой страницы в DOM, чтобы сайты видели фейковые данные вместо настоящих.
Пример плагина:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Navigator Override",
"version": "1.0",
"description": "Overrides navigator properties in Firefox",
"permissions": [
"activeTab",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
(function() {
'use strict';
const script = document.createElement('script');
script.textContent = `
Object.defineProperty(navigator, 'productSub', {
value: '20200102',
writable: false,
configurable: false
});
Object.defineProperty(navigator, 'vendorSub', {
value: '',
writable: false,
configurable: false
});
Object.defineProperty(navigator, 'vendor', {
value: 'Custom Vendor',
writable: false,
configurable: false
});
`;
document.documentElement.appendChild(script);
})();
Проверяем:
Этот плагин загружается до загрузки сайтов и меняет объекты JavaScript параметров. В content.js я создаю элемент script и внедряю в него код, который выполняется на уровне страницы. Он использует Object.defineProperty для подмены трёх свойств navigator: productSub задаётся как “20200101”, имитируя старую версию браузера, как в некоторых сборках Chrome, vendorSub устанавливается в пустую строку, как в Chrome, а vendor меняется на “Custom Vendor” для примера. Свойства делаются неизменяемыми (writable: false, configurable: false), чтобы сайты не могли их подправить.
Последний скрипт, который я покажу, — это пример для Tampermonkey, который подменяет параметры navigator.productSub, navigator.vendorSub и navigator.vendor.
Пример скрипта:
JavaScript:
// ==UserScript==
// @name Navigator Override
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Overrides navigator properties
// @author hipeope
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
Object.defineProperty(navigator, 'productSub', {
value: '20200101',
writable: false
});
Object.defineProperty(navigator, 'vendorSub', {
value: '',
writable: false
});
Object.defineProperty(navigator, 'vendor', {
value: 'Google Inc',
writable: false
});
})();
Этот код меняет параметры navigator, чтобы сайты видели фейковые данные. Внутри функции я использую Object.defineProperty для подмены трёх свойств navigator. Параметр productSub задаётся как “20200101”. Свойство vendorSub устанавливается в пустую строк. А vendor меняется на “Google Inc”, как в Chrome, вместо “” в Firefox. Все свойства делаются неизменяемыми (writable: false), чтобы сайты не могли их подправить. Это работает, потому что скрипт вмешивается в JavaScript страницы до её загрузки, подменяя значения navigator.
Подмена медиа-устройств
Теперь давай поговорим про подмену медиа-устройств - названий камер, микрофонов и прочих девайсов, которые сайты могут получить через API вроде navigator.mediaDevices.enumerateDevices(). Эти данные легко становятся частью твоего отпечатка, ведь названия устройств, такие как “Logitech HD Webcam” или “Realtek Audio”, часто уникальны.
Первым я покажу, как написать плагин для подмены названий медиа-устройств, таких как камеры и микрофоны, через подмену navigator.mediaDevices в Firefox.
Сам пример плагна:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Device Spoofer",
"version": "1.0",
"permissions": [
"<all_urls>",
"webRequest",
"webRequestBlocking",
"activeTab"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
content.js:
JavaScript:
(function() {
'use strict';
function createSpoofingScript() {
const script = document.createElement('script');
script.textContent = `
(function() {
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
navigator.mediaDevices.enumerateDevices = async function() {
const devices = await originalEnumerateDevices.call(navigator.mediaDevices);
return devices.map(device => ({
deviceId: device.deviceId,
kind: device.kind,
label: device.kind === 'videoinput' ? 'Fake Camera' :
device.kind === 'audioinput' ? 'Fake Microphone' :
device.kind === 'audiooutput' ? 'Fake Speaker' : device.label,
groupId: device.groupId,
toJSON: () => ({
deviceId: device.deviceId,
kind: device.kind,
label: device.kind === 'videoinput' ? 'Fake Camera' :
device.kind === 'audioinput' ? 'Fake Microphone' :
device.kind === 'audiooutput' ? 'Fake Speaker' : device.label,
groupId: device.groupId
})
}));
};
})();
`;
(document.head || document.documentElement).appendChild(script);
}
function waitForDOM() {
if (document.head || document.documentElement) {
createSpoofingScript();
} else {
setTimeout(waitForDOM, 10);
}
}
waitForDOM();
})();
Код работает через загрузку в DOM и подмену значений. В content.js функция createSpoofingScript создаёт элемент script и внедряет код, который перехватывает метод navigator.mediaDevices.enumerateDevices. Он вызывает оригинальный метод, получает список устройств и подменяет их label: для видеоустройств — “Fake Camera”, для аудиовходов — “Fake Microphone”, для аудиовыходов — “Fake Speaker”. Остальные свойства, вроде deviceId и groupId, остаются нетронутыми, а метод toJSON обеспечивает корректный вывод при сериализации.Функция waitForDOM проверяет, готов ли DOM (head или documentElement), и внедряет скрипт, иначе ждёт с интервалом 10 мс.
Но ещё проще подменять названия медиа-устройств, таких как камеры и микрофоны, можно через Tampermonkey — это более лёгкий способ без написания полноценного плагина.
Пример кода:
JavaScript:
// ==UserScript==
// @name Spoof Media Devices
// @namespace http://tampermonkey.net/
// @version 0.2
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
navigator.mediaDevices.enumerateDevices = async function() {
const devices = await originalEnumerateDevices.call(navigator.mediaDevices);
return devices.map(device => {
return {
deviceId: device.deviceId,
kind: device.kind,
label: device.kind === 'videoinput' ? 'Spoofed Camera' :
device.kind === 'audioinput' ? 'Spoofed Mic' :
device.kind === 'audiooutput' ? 'Spoofed Speaker' : device.label,
groupId: device.groupId,
toJSON: () => ({
deviceId: device.deviceId,
kind: device.kind,
label: device.kind === 'videoinput' ? 'Spoofed Camera' :
device.kind === 'audioinput' ? 'Spoofed Mic' :
device.kind === 'audiooutput' ? 'Spoofed Speaker' : device.label,
groupId: device.groupId
})
};
});
};
})();
Проверяем:
Этот код работает через перехват метода navigator.mediaDevices.enumerateDevices. Внутри функции я сохраняю оригинальный метод enumerateDevices, чтобы вызвать его для получения списка устройств. Затем метод подменяется асинхронной функцией, которая берёт реальные устройства, но меняет их label: для видеоустройств — “Spoofed Camera”, для аудиовходов — “Spoofed Mic”, для аудиовыходов — “Spoofed Speaker”. Остальные свойства, вроде deviceId и groupId, остаются нетронутыми, а метод toJSON обеспечивает корректный вывод при сериализации.
Но вообще самое надёжное решение, чтобы не возиться с подменой медиа-устройств, - это отключить WebRTC и доступ к камерам и микрофонам прямо в about:config.
Самый жёсткий вариант — вырубить WebRTC полностью через настройку media.peerconnection.enabled, установив её в false, а также проверьте media.peerconnection.ice.enabled и media.peerconnection.ice.default_address_only, установив их в false и true соответственно. Это отключает весь функционал WebRTC, включая ICE-кандидаты, которые могут выдать твой локальный IP, и доступ к камерам и микрофонам через этот протокол. Если не хочешь так радикально или не можешь отключить WebRTC из-за каких-то нужд, можно просто заблокировать доступ к медиа-устройствам через media.navigator.enabled, поставив в false. Это выключает API navigator.mediaDevices, и сайты не смогут запросить список твоих камер, микрофонов или колонок.
Ещё один вариант — отключить доступ к аппаратным ключам через media.hardwaremediakeys.enabled, установив в false. Это ограничивает взаимодействие с медиа-устройствами на уровне аппаратных интерфейсов, что дополнительно снижает риск утечки данных.
Подмена статуса батареи и геймпадов
Напоследок расскажу про подделку статуса батареи и списка геймпадов в Firefox, чтобы сайты не могли использовать эти данные для твоего отпечатка.
Сначала про батарею. Если в Firefox выполнить console.log(navigator.getBattery()), ты получишь ошибку: Uncaught TypeError: navigator.getBattery is not a function, а console.log(typeof navigator.getBattery) выдаст undefined. Это потому, что Battery Status API, который позволял сайтам видеть уровень заряда и время работы батареи, был удалён из Firefox начиная с версии 52 (март 2017). Причина — приватность: этот API давал трекерам слишком много данных для идентификации пользователей, так как уровень заряда батареи мог быть уникальным маркером. В Chrome, однако, Battery Status API всё ещё доступен, и сайты могут запрашивать эти данные. Отсутствие navigator.getBattery в Firefox может выдать твой браузер, так что подделать его — дело полезное. А так же в некоторых кастомных или старых сборках (например, Firefox ESR) API может быть включён через настройки, так что стоит проверить это в about:config (dom.battery.enabled) если у вас такая версия.
С геймпадами тоже не всё гладко. В Firefox их можно достать через navigator.getGamepads(), и это странно: батарею посчитали “нарушением приватности”, а уникальные геймпады — “ну ок, это совсем не нарушает приватность”. Но сайты могут использовать их для фингерпринтинга, ведь модель геймпада — штука довольно уникальная.
Чтобы этого избежать, я написал скрипт для Tampermonkey, который подменяет и Battery Status API, и данные геймпадов.
Пример кода:
JavaScript:
// ==UserScript==
// @name Fake Gamepad and Battery API
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Fake Gamepad and Battery APIs in Firefox
// @author hipeople
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const fakeGamepad = {
id: "Fake Gamepad (Vendor: 1234 Product: 5678)",
index: 0,
connected: true,
timestamp: Date.now(),
mapping: "standard",
axes: [0, 0, 0, 0],
buttons: Array(17).fill().map(() => ({ pressed: false, touched: false, value: 0 })),
};
Object.defineProperty(navigator, 'getGamepads', {
value: function() {
const gamepadList = [fakeGamepad, null, null, null];
gamepadList.__proto__ = Object.create(Array.prototype);
return gamepadList;
},
writable: false,
configurable: false
});
const gamepadConnectedEvent = new CustomEvent('gamepadconnected', {
bubbles: true,
cancelable: true,
detail: { gamepad: fakeGamepad }
});
const fakeBattery = {
charging: true,
chargingTime: 3600,
dischargingTime: 7200,
level: 0.75,
addEventListener: function(event, callback) {
},
removeEventListener: function() {},
dispatchEvent: function() {}
};
Object.defineProperty(navigator, 'getBattery', {
value: function() {
return Promise.resolve(fakeBattery);
},
writable: false,
configurable: false
});
setTimeout(() => {
console.log('Fake Gamepad API initialized');
console.log('Fake gamepad connected:', navigator.getGamepads());
window.dispatchEvent(gamepadConnectedEvent);
console.log('Fake Battery API initialized');
navigator.getBattery().then(battery => {
console.log('Fake battery status:', {
charging: battery.charging,
level: battery.level,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime
});
});
}, 100);
})();
Этот код работает так: Сначала я создаю фейковый геймпад с id “Fake Gamepad (Vendor: 1234 Product: 5678)”, стандартным маппингом, четырьмя нулевыми осями и 17 кнопками в выключенном состоянии. Метод navigator.getGamepads перехватывается через Object.defineProperty и возвращает массив с этим геймпадом и тремя null, как в типичном Chrome. Я также создаю событие gamepadconnected, которое отправляется через window.dispatchEvent через 100 мс, чтобы имитировать подключение геймпада.
Для батареи я задаю фейковый объект fakeBattery: батарея заряжается (charging: true), уровень заряда 75% (level: 0.75), время зарядки 3600 секунд, разрядки — 7200 секунд. Метод navigator.getBattery подменяется, чтобы возвращать Promise с этим объектом, как в Chrome. Пустые методы addEventListener, removeEventListener и dispatchEvent добавлены для совместимости. Через setTimeout я вывожу в консоль фейковые данные для отладки.
Ещё можно написать плагин для подмены статуса батареи и геймпадов. Я покажу, как это сделать, чтобы всё выглядело, как будто ты на Chrome.
Пример плагина:
manifest.json:
JSON:
{
"manifest_version": 2,
"name": "Fake Gamepad and Battery Spoofer",
"version": "1.0",
"description": "A Firefox extension to spoof Gamepad and Battery APIs",
"permissions": [
"activeTab",
"tabs",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"background": {
"scripts": ["background.js"]
}
}
content.js:
JavaScript:
(function() {
'use strict';
const script = document.createElement('script');
script.textContent = `
(function() {
'use strict';
function spoofProperty(obj, prop, value) {
try {
Object.defineProperty(obj, prop, {
value: value,
writable: false,
configurable: false
});
} catch (e) {
console.error('Failed to spoof ' + prop + ':', e);
}
}
const fakeGamepad = {
id: "Fake Gamepad (Vendor: 1234 Product: 5678)",
index: 0,
connected: true,
timestamp: Date.now(),
mapping: "standard",
axes: [0, 0, 0, 0],
buttons: Array(17).fill().map(() => ({ pressed: false, touched: false, value: 0 }))
};
const spoofGamepads = function() {
const gamepadList = [fakeGamepad, null, null, null];
gamepadList.__proto__ = Array.prototype;
return gamepadList;
};
spoofProperty(window.navigator, 'getGamepads', spoofGamepads);
const gamepadConnectedEvent = new CustomEvent('gamepadconnected', {
bubbles: true,
cancelable: true,
detail: { gamepad: fakeGamepad }
});
const fakeBattery = {
charging: true,
chargingTime: 3600,
dischargingTime: 7200,
level: 0.75,
addEventListener: function(event, callback) {},
removeEventListener: function() {},
dispatchEvent: function() {}
};
const spoofBattery = function() {
return Promise.resolve(fakeBattery);
};
if ('getBattery' in window.navigator) {
spoofProperty(window.navigator, 'getBattery', spoofBattery);
} else {
spoofProperty(window.navigator, 'getBattery', function() {
console.warn('Battery API not supported, using fake battery');
return Promise.resolve(fakeBattery);
});
}
setTimeout(() => {
console.log('Fake Gamepad API initialized');
console.log('Fake gamepad connected:', window.navigator.getGamepads());
window.dispatchEvent(gamepadConnectedEvent);
console.log('Fake Battery API initialized');
window.navigator.getBattery().then(battery => {
console.log('Fake battery status:', {
charging: battery.charging,
level: battery.level,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime
});
}).catch(err => {
console.error('Error accessing fake battery:', err);
});
}, 100);
})();
`;
(document.head || document.documentElement).appendChild(script);
script.remove();
})();
Проверяем:
Этот плагин работает на этапе DOM, до загрузки страницы. Первым делом в content.js я создаю элемент script и внедряю в него код для подмены. Функция spoofProperty использует Object.defineProperty для безопасной замены свойств navigator. Для геймпадов создаётся объект fakeGamepad с id “Fake Gamepad (Vendor: 1234 Product: 5678)”, стандартным маппингом, четырьмя нулевыми осями и 17 кнопками в выключенном состоянии. Метод getGamepads подменяется, возвращая массив с одним фейковым геймпадом и тремя null, как в Chrome. Событие gamepadconnected отправляется через 100 мс, чтобы имитировать подключение.
Для батареи создаётся объект fakeBattery: батарея заряжается (charging: true), уровень 75% (level: 0.75), время зарядки 3600 секунд, разрядки — 7200 секунд. Метод getBattery подменяется, возвращая Promise с этим объектом. Если getBattery уже есть, он перезаписывается; если нет, добавляется с предупреждением. Код внедряется в head или documentElement и удаляется после выполнения.
Практические инструменты для смены отпечатка
Вот я и закончил рассказывать про методы подделки отпечатка, и теперь пора собрать всё воедино. Этот раздел будет практическим и разделится на две части. Сперва я покажу полную конфигурацию about:config, которую я собрал на основе всех примеров, что мы обсуждали, чтобы замаскировать твой браузер под типичный Chrome на Windows. После я расскажу как написать скрипт для Tampermonkey для поддлеки отпчатка на уровне js.
Смена отпечатка в user.js
Настройки user.js работают на уровне исходного кода Firefox, напрямую меняя поведение движка браузера. Они применяются автоматически при запуске, переписывая внутренние параметры, и сайты не могут их обойти через JavaScript или другие уловки.
Вот полная конфигурация для user.js, которую я написал для примера:
JavaScript:
user_pref("privacy.resistFingerprinting", true);
user_pref("privacy.resistFingerprinting.letterboxing", true);
user_pref("privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts", true);
user_pref("privacy.resistFingerprinting.randomDataOnCanvasExtract", true);
user_pref("general.oscpu.override", "Windows NT 10.0; Win64; x64");
user_pref("general.platform.override", "Win32");
user_pref("general.productSub.override", "20200101");
user_pref("general.vendorSub.override", "");
user_pref("general.buildID.override", "");
user_pref("general.useragent.override", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36");
user_pref("intl.accept_languages", "en-US, en");
user_pref("javascript.use_us_english_locale", true);
user_pref("webgl.override-unmasked-vendor", "NVIDIA Corporation");
user_pref("webgl.override-unmasked-renderer", "GeForce GTX 1060/PCIe/SSE2");
user_pref("webgl.renderer-string-override", " ");
user_pref("webgl.vendor-string-override", " ");
user_pref("webgl.enable-webgl2", false);
user_pref("font.name.serif.x-western", "Georgia");
user_pref("font.name.sans-serif.x-western", "Helvetica");
user_pref("font.name.monospace.x-western", "Courier New");
user_pref("font.size.variable.x-western", 18);
user_pref("font.minimum-size.x-western", 12);
user_pref("toolkit.telemetry.enabled", false);
user_pref("toolkit.telemetry.unified", false);
user_pref("toolkit.telemetry.archive.enabled", false);
user_pref("toolkit.telemetry.newProfilePing.enabled", false);
user_pref("toolkit.telemetry.shutdownPingSender.enabled", false);
user_pref("toolkit.telemetry.updatePing.enabled", false);
user_pref("datareporting.healthreport.uploadEnabled", false);
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("breakpad.reportURL", "");
user_pref("browser.tabs.crashReporting.sendReport", false);
// Можно ещё отключить WebRTC, но это может быть подозрительно
//user_pref("media.peerconnection.enabled", false);
//user_pref("media.peerconnection.ice.default_address_only", true);
//user_pref("media.peerconnection.ice.no_host", true);
//user_pref("media.navigator.enabled", false);
//user_pref("media.hardwaremediakeys.enabled", false);
user_pref("media.devices.enumerate.blocked", true);
user_pref("media.navigator.device.enabled", false);
user_pref("dom.battery.enabled", false);
user_pref("dom.maxHardwareConcurrency", 2);
user_pref("dom.maxPhysicalMemoryMB", 4096);
user_pref("dom.gamepad.enabled", false);
user_pref("privacy.reduceTimerPrecision", true);
user_pref("privacy.reduceTimerPrecision.jitter", true);
user_pref("privacy.resistFingerprinting.reduceTimerPrecision.microseconds", 100000);
user_pref("dom.event.highres-timestamp.enabled", false);
user_pref("dom.webaudio.enabled", false);
user_pref("privacy.canvas.poisondata", true);
user_pref("privacy.trackingprotection.enabled", true);
user_pref("privacy.trackingprotection.socialtracking.enabled", true);
user_pref("privacy.trackingprotection.fingerprinting.enabled", true);
user_pref("network.cookie.cookieBehavior", 1);
user_pref("network.proxy.socks_remote_dns", true);
user_pref("network.trr.mode", 5);
user_pref("network.dns.disablePrefetch", true);
user_pref("network.prefetch-next", false);
user_pref("network.predictor.enabled", false);
user_pref("network.http.speculative-parallel-limit", 0);
user_pref("layout.css.dpi", 96);
user_pref("layout.css.devPixelsPerPx", 1.0);
user_pref("privacy.window.maxInnerWidth", 1000);
user_pref("privacy.window.maxInnerHeight", 1000);
user_pref("dom.event.clipboardevents.enabled", false);
user_pref("dom.storage.enabled", false);
user_pref("dom.netinfo.enabled", false);
user_pref("dom.push.enabled", false);
user_pref("dom.vr.enabled", false);
user_pref("dom.vibration.enabled", false);
user_pref("geo.enabled", false);
user_pref("dom.webnotifications.enabled", false);
user_pref("beacon.enabled", false);
user_pref("device.sensors.enabled", false);
user_pref("plugin.scan.plid.all", false);
user_pref("plugin.state.flash", 0);
user_pref("plugins.enumerable_names", "");
user_pref("browser.startup.homepage_override.mstone", "ignore");
user_pref("browser.aboutwelcome.enabled", false);
user_pref("browser.sessionstore.privacy_level", 2);
user_pref("browser.cache.disk.enable", false);
user_pref("browser.cache.memory.enable", true);
user_pref("browser.formfill.enable", false);
user_pref("signon.rememberSignons", false);
user_pref("extensions.pocket.enabled", false);
user_pref("browser.safebrowsing.enabled", false);
user_pref("browser.search.suggest.enabled", false);
user_pref("network.IDN_show_punycode", true);
Тут подделывается всё, что можно. Настройка privacy.resistFingerprinting включена, чтобы сделать параметры вроде списка плагинов, экрана, часового пояса и количества ядер такими же, как у миллионов других. Для защиты canvas включены autoDeclineNoUserInputCanvasPrompts, чтобы блокировать автоматические запросы, и randomDataOnCanvasExtract, чтобы мешать трекерам шумом в данных рендеринга.
Система притворяется Windows 10 через general.oscpu.override и general.platform.override, а через general.useragent.override подделывается Chrome 130, чтобы сайты думали, что ты используешь популярный браузер. Параметры general.productSub.override, general.vendorSub.override и general.buildID.override убирают уникальные метки Firefox, делая его похожим на старую версию Chrome. Язык установлен en-US через intl.accept_languages и javascript.use_us_english_locale, чтобы соответствовать обычной локали Windows.
WebGL подменяется на NVIDIA GTX 1060 через webgl.override-unmasked-vendor и webgl.override-unmasked-renderer, а WebGL2 отключён через webgl.enable-webgl2, чтобы меньше данных о видеокарте уходило. Поля webgl.renderer-string-override и webgl.vendor-string-override пустые, чтобы не выдавать лишнего. Шрифты установлены стандартные для Windows — Georgia, Helvetica, Courier New, с фиксированными размерами 18 и минимум 12 через font.*, чтобы твой набор шрифтов не выделялся.
Сбор данных Mozilla полностью отключён через toolkit.telemetry., datareporting. и breakpad.reportURL, чтобы браузер ничего не отправлял. Камеры и микрофоны блокируются через media.devices.enumerate.blocked и media.navigator.device.enabled, чтобы сайты их не видели. WebRTC закомментирован через media.peerconnection.*, так как его отключение может выглядеть странно и ломать некотрые другие поддлеки данных через WebRTC, а так же данные сайтов. Батарея и геймпады отключены через dom.battery.enabled и dom.gamepad.enabled, чтобы их не видели. Количество ядер процессора и память подменяются на 2 ядра и 4 ГБ через dom.maxHardwareConcurrency и dom.maxPhysicalMemoryMB, чтобы не выдавать мощное оборудование.
Таймеры ограничены до 100 мс через privacy.reduceTimerPrecision и reduceTimerPrecision.jitter, а точные таймеры выключены через dom.event.highres-timestamp.enabled, чтобы сайты не могли отслеживать время работы скриптов. Web Audio заблокирован через dom.webaudio.enabled, а canvas дополнительно защищён через privacy.canvas.poisondata. Защита от трекинга работает через privacy.trackingprotection.*, а куки ограничены через network.cookie.cookieBehavior, чтобы принимать только нужные для работы сайта.
Сетевые утечки блокируются через network.proxy.socks_remote_dns, network.trr.mode (отключение DoH), network.dns.disablePrefetch, network.prefetch-next и network.predictor.enabled, чтобы сайты не видели DNS-запросы и не предугадывали твои действия. Разрешение экрана фиксируется на 1000x1000 через privacy.window.maxInnerWidth и maxInnerHeight, а DPI и пиксели установлены стандартными через layout.css.dpi и layout.css.devPixelsPerPx. Плагины блокируются через plugin.scan.plid.all, plugin.state.flash и plugins.enumerable_names, чтобы список плагинов ничего не выдавал.
Также отключены буфер обмена, локальное хранилище, push-уведомления, геолокация, VR, вибрация, сенсоры и веб-уведомления через dom.* и geo.enabled, чтобы сайты до них не добрались. Кэш на диске выключен через browser.cache.disk.enable, автозаполнение форм и паролей — через browser.formfill.enable и signon.rememberSignons, а Pocket и безопасный просмотр отключены через extensions.pocket.enabled и browser.safebrowsing.enabled. Поисковые подсказки и IDN-пуникод тоже выключены, чтобы не оставлять следов.
Как по мне получился неплохой пример конфигурации about:config , хотя возможности user.js и не могут подделать абсолютно всё. Он они закрывает кучу дыр, через которые сайты могут тебя отслеживать: юзер агент, платформу, шрифты, видеокарту, блокирует камеры, микрофоны и всякие лишние данные. Но будь осторожен: слишком много подмен может выделить тебя, если сайты проверяют редкие комбинаци.
Автоматизация копирования user.js
Возможно, вам может быть неудобно копировать user.js в каждый профиль Firefox вручную, особенно если их несколько. Поэтому я написал простенький bash-скрипт, который автоматически копирует user.js во все профили Firefox текущего пользователя.
Мой код:
Bash:
#!/bin/bash
USER=$(whoami)
FIREFOX_DIR="/home/$USER/.mozilla/firefox/"
FOUND_PROFILES=$(find "$FIREFOX_DIR" -maxdepth 1 -type d -name "*default*")
while IFS= read -r PROFILE_DIR; do
cp ./user.js "$PROFILE_DIR/"
if [ $? -eq 0 ]; then
echo "Файл user.js успешно скопирован в $PROFILE_DIR"
else
echo "Ошибка при копировании файла user.js в $PROFILE_DIR"
fi
done <<< "$FOUND_PROFILES"
Проверяем:
Как видно, всё копируется нормально. Скрипт работает следующим образом: он определяет текущего пользователя с помощью команды whoami, затем ищет все профили Firefox в директории /home/$USER/.mozilla/firefox/, фильтруя папки по слову "default" в их названии. Найденные профили обрабатываются в цикле, где файл user.js из текущей директории копируется в каждый профиль. После каждой попытки копирования скрипт проверяет успешность операции и выводит соответствующее сообщение.
Инструмент для Tampermonkey
Просто менять настройки Firefox часто не хватает. Некоторые вещи, вроде часового пояса, системного времени или размера экрана, можно нормально подделать только через JavaScript. Поэтому я создал инструмент, который делает подделку под Chrome Windows. Для этого я скрипт в Tampermonkey, в котором вверху находятся все настройки с фейковыми параметрами, а ниже — модули, каждый из которых отвечает за подделку конкретной части отпечатка. Давайте разберём, как устроен этот скрипт, какие методы подделки я использовал и как они работаю!
Архитектура скрипта построена вокруг модульной системы: объект CONFIG хранит все фейковые значения, а отдельные модули (FontSpoofer, TimezoneSpoofer, AudioSpoofer, WebGLSpoofer, CanvasSpoofer, DeviceSpoofer) отвечают за подмену конкретных данных. Основные методы подделки я взял из прошлых наработок, но добавил новые, подходы для WebGL и canvas. Главынй модуль скрипта это, safeSpoofPropert, она подменяет свойства объектов через Object.defineProperty, проверяя, не заморожен ли объект, и обрабатывает ошибки. Но для понятности давайте разберём мой код по частям.
Скрипт начинается с объекта CONFIG, который задаёт фейковые значения для всего:
JavaScript:
const CONFIG = {
USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
TIMEZONE: 'America/New_York',
TIMEZONE_OFFSET: 300,
LOCALE: 'en-US',
POPULAR_FONTS: ['Arial', 'Times New Roman', 'Courier New', 'Calibri', 'Segoe UI', 'Verdana', 'Tahoma', 'Georgia', 'Trebuchet MS', 'Comic Sans MS'],
SCREEN: {
width: 1920,
height: 1080,
availWidth: 1920,
availHeight: 1040,
availTop: 0,
devicePixelRatio: 1
},
NAVIGATOR: {
oscpu: 'Windows NT 10.0; Win64; x64',
hardwareConcurrency: 8,
platform: 'Win32',
deviceMemory: 8,
buildID: undefined,
product: 'Gecko',
productSub: '20030107',
vendorSub: '',
vendor: 'Google Inc.'
},
AUDIO: {
sampleRate: 44100,
fftSize: 2048,
frequencyBinCount: 1024,
smoothingTimeConstant: 0.8
},
WEBGL: {
vendor: 'Intel',
renderer: 'Intel(R) UHD Graphics 630'
},
GAMEPAD: {
id: 'Fake Gamepad (Vendor: 1234 Product: 5678)',
index: 0,
connected: true,
mapping: 'standard',
axes: [0, 0, 0, 0],
buttons: Array(17).fill().map(() => ({ pressed: false, touched: false, value: 0 }))
}
};
function safeSpoofProperty(obj, prop, value) {
try {
if (!obj || Object.isFrozen(obj) || Object.isSealed(obj)) {
return false;
}
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
delete obj[prop];
}
Object.defineProperty(obj, prop, {
configurable: true,
get: () => value
});
return true;
} catch (error) {
console.warn(`Failed to spoof property ${prop}:`, error.message);
return false;
}
}
Теперь расскажу про модули моего скрипта.
Первым я расскажу про модуль подделки шрифтов:
JavaScript:
const FontSpoofer = {
applyStyles() {
const style = document.createElement('style');
style.textContent = `
* {
font-family: ${CONFIG.POPULAR_FONTS.slice(0, 4).join(', ')} !important;
font-style: normal !important;
font-weight: normal !important;
}
pre, code, textarea, input[type="text"], input[type="password"] {
font-family: "Courier New", monospace !important;
}
`;
(document.head || document.documentElement).appendChild(style);
},
spoofFontFace() {
const OriginalFontFace = window.FontFace;
window._FontFace = OriginalFontFace;
Object.defineProperty(window, 'FontFace', {
value: function(family, source) {
const isPopularFont = CONFIG.POPULAR_FONTS.some(font => family.includes(font));
return new window._FontFace(isPopularFont ? family : 'Arial', source);
},
writable: true
});
},
spoofFontCheck() {
if (!document.fonts?.check) return;
const originalFontsCheck = document.fonts.check;
document.fonts.check = function(font) {
try {
return CONFIG.POPULAR_FONTS.some(f => font.includes(f));
} catch {
return false;
}
};
},
init() {
this.applyStyles();
this.spoofFontFace();
this.spoofFontCheck();
}
};
Следующим идёт модуль для подмены времени:
JavaScript:
const TimezoneSpoofer = {
spoofDateTimeFormat() {
const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
Intl.DateTimeFormat.prototype.resolvedOptions = function() {
const options = originalResolvedOptions.call(this);
return {
...options,
timeZone: CONFIG.TIMEZONE,
locale: CONFIG.LOCALE
};
};
},
spoofDate() {
const OriginalDate = window.Date;
function FakeDate(...args) {
if (args.length === 0) {
const realTime = new OriginalDate();
realTime.setHours(realTime.getHours() + 6);
return new OriginalDate(realTime);
}
return new OriginalDate(...args);
}
FakeDate.now = () => {
const realTime = new OriginalDate();
realTime.setHours(realTime.getHours() + 6);
return realTime.getTime();
};
FakeDate.UTC = OriginalDate.UTC;
FakeDate.parse = OriginalDate.parse;
window.Date = FakeDate;
safeSpoofProperty(Date.prototype, 'getTimezoneOffset', () => CONFIG.TIMEZONE_OFFSET);
},
init() {
safeSpoofProperty(navigator, 'language', CONFIG.LOCALE);
safeSpoofProperty(navigator, 'languages', [CONFIG.LOCALE]);
this.spoofDateTimeFormat();
this.spoofDate();
}
};
Так же я написал модуль для подделки отпечатка на основе звука:
JavaScript:
const AudioSpoofer = {
spoofAudioContext() {
const OriginalAudioContext = window.AudioContext || window.webkitAudioContext;
if (!OriginalAudioContext) {
safeSpoofProperty(window, 'AudioContext', undefined);
safeSpoofProperty(window, 'webkitAudioContext', undefined);
return;
}
window.AudioContext = function() {
const ctx = new OriginalAudioContext();
safeSpoofProperty(ctx, 'sampleRate', CONFIG.AUDIO.sampleRate);
const originalCreateAnalyser = ctx.createAnalyser;
ctx.createAnalyser = function() {
const analyser = originalCreateAnalyser.call(this);
safeSpoofProperty(analyser, 'fftSize', CONFIG.AUDIO.fftSize);
safeSpoofProperty(analyser, 'frequencyBinCount', CONFIG.AUDIO.frequencyBinCount);
safeSpoofProperty(analyser, 'smoothingTimeConstant', CONFIG.AUDIO.smoothingTimeConstant);
return analyser;
};
return ctx;
};
window.webkitAudioContext = window.AudioContext;
},
init() {
this.spoofAudioContext();
}
};
Далее я написал модуль для подделки графики, он в целом работает почти так же, как в примерах выше:
JavaScript:
const WebGLSpoofer = {
spoofWebGLContext(context) {
if (!context?.getParameter) return;
const originalGetParameter = context.getParameter;
context.getParameter = function(parameter) {
try {
if (!parameter) return null;
switch (parameter) {
case 0x9245: return CONFIG.WEBGL.vendor;
case 0x9246: return CONFIG.WEBGL.renderer;
default: return originalGetParameter.call(this, parameter);
}
} catch {
return null;
}
};
const originalGetExtension = context.getExtension;
context.getExtension = function(name) {
try {
if (name === 'WEBGL_debug_renderer_info') {
return {
UNMASKED_VENDOR_WEBGL: 0x9245,
UNMASKED_RENDERER_WEBGL: 0x9246,
getParameter: parameter => {
try {
if (!parameter) return null;
switch (parameter) {
case 0x9245: return CONFIG.WEBGL.vendor;
case 0x9246: return CONFIG.WEBGL.renderer;
default: return null;
}
} catch {
return null;
}
}
};
}
return originalGetExtension.call(this, name);
} catch {
return null;
}
};
},
init() {
if (!window.WebGLRenderingContext) return;
const contexts = [WebGLRenderingContext];
if (typeof WebGL2RenderingContext !== 'undefined') {
contexts.push(WebGL2RenderingContext);
}
contexts.forEach(context => {
if (context?.prototype) {
this.spoofWebGLContext(context.prototype);
}
});
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, ...args) {
try {
const ctx = originalGetContext.call(this, type, ...args);
if (ctx && (type === 'webgl' || type === 'webgl2')) {
this.spoofWebGLContext(ctx.__proto__);
}
return ctx;
} catch {
return null;
}
};
}
};
Из относительно нового я добавил модуль для подделки Canvas, методы из которого я не описывал в статье выше, чтобы не затягивать повествование:
JavaScript:
const CanvasSpoofer = {
applyNoise(data) {
for (let i = 0; i < data.length; i += 4) {
data ^= Math.random() * 10 | 0; // Red
data[i + 1] ^= Math.random() * 10 | 0; // Green
data[i + 2] ^= Math.random() * 10 | 0; // Blue
}
},
spoofCanvasContext(ctx) {
if (!ctx) return;
const originalGetImageData = ctx.getImageData;
ctx.getImageData = function(...args) {
const imageData = originalGetImageData.apply(this, args);
this.applyNoise(imageData.data);
return imageData;
}.bind(this);
const originalToDataURL = ctx.toDataURL;
ctx.toDataURL = function(...args) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.width;
tempCanvas.height = this.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(this, 0, 0);
const imageData = tempCtx.getImageData(0, 0, this.width, this.height);
this.applyNoise(imageData.data);
tempCtx.putImageData(imageData, 0, 0);
return tempCtx.toDataURL(...args);
}.bind(this);
},
init() {
const originalCanvasGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, ...args) {
const ctx = originalCanvasGetContext.call(this, type, ...args);
if (ctx && (type === '2d' || type === 'webgl' || type === 'webgl2')) {
this.spoofCanvasContext(ctx);
}
return ctx;
}.bind(this);
}
};
Этот модуль добавляет случайный шум в данные canvas через методы getImageData и toDataURL, изменяя значения пикселей (красный, зелёный, синий) на ±10. Это делает отпечаток canvas нестабильным при каждом запросе, ломая попытки сайтов создать постоянный идентификатор на основе рендеринга. Если подробнее, работа модуля начинается с метода init, который перехватывает вызов getContext для всех canvas-элементов на странице, применяя модификации к контекстам 2D, WebGL и WebGL2. Метод spoofCanvasContext оборачивает оригинальные методы getImageData и toDataURL, добавляя шум к данным пикселей через функцию applyNoise. Для getImageData шум применяется напрямую к данным изображения, а для toDataURL создаётся временный canvas, куда копируется и модифицируется изображение.
И последним я расскажу про несколько общих небольших модулей объединённых в один DeviceSpoofer:
JavaScript:
const DeviceSpoofer = {
spoofNavigator() {
safeSpoofProperty(navigator, 'userAgent', CONFIG.USER_AGENT);
Object.entries(CONFIG.NAVIGATOR).forEach(([prop, value]) => {
safeSpoofProperty(navigator, prop, value);
});
},
spoofScreen() {
Object.entries(CONFIG.SCREEN).forEach(([prop, value]) => {
safeSpoofProperty(screen, prop, value);
});
safeSpoofProperty(window, 'innerWidth', CONFIG.SCREEN.width);
safeSpoofProperty(window, 'innerHeight', CONFIG.SCREEN.availHeight);
safeSpoofProperty(window, 'outerWidth', CONFIG.SCREEN.width);
safeSpoofProperty(window, 'outerHeight', CONFIG.SCREEN.height);
safeSpoofProperty(document.documentElement, 'clientWidth', CONFIG.SCREEN.width);
safeSpoofProperty(document.documentElement, 'clientHeight', CONFIG.SCREEN.availHeight);
safeSpoofProperty(window, 'devicePixelRatio', CONFIG.SCREEN.devicePixelRatio);
},
spoofMediaDevices() {
if (navigator.mediaDevices?.enumerateDevices) {
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
navigator.mediaDevices.enumerateDevices = async function() {
try {
const devices = await originalEnumerateDevices.call(navigator.mediaDevices);
return devices.map(device => ({
...device,
label: device.kind === 'videoinput' ? 'Spoofed Camera' :
device.kind === 'audioinput' ? 'Spoofed Mic' :
device.kind === 'audiooutput' ? 'Spoofed Speaker' : device.label,
toJSON: () => ({
deviceId: device.deviceId,
kind: device.kind,
label: device.kind === 'videoinput' ? 'Spoofed Camera' :
device.kind === 'audioinput' ? 'Spoofed Mic' :
device.kind === 'audiooutput' ? 'Spoofed Speaker' : device.label,
groupId: device.groupId
})
}));
} catch {
return [];
}
};
} else {
safeSpoofProperty(navigator.mediaDevices || navigator, 'enumerateDevices', async () => []);
}
},
spoofGamepads() {
safeSpoofProperty(navigator, 'getGamepads', () => {
const gamepadList = [CONFIG.GAMEPAD, null, null, null];
gamepadList.__proto__ = Array.prototype;
return gamepadList;
});
},
spoofBattery() {
safeSpoofProperty(navigator, 'getBattery', () => Promise.reject(new Error('Battery API not supported')));
},
init() {
safeSpoofProperty(window, 'InstallTrigger', false);
this.spoofNavigator();
this.spoofScreen();
this.spoofMediaDevices();
this.spoofGamepads();
this.spoofBattery();
}
};
Модули в DeviceSpoofer подменяет параметры navigator, экран, медиа-устройства, геймпады и батарею. Метод spoofNavigator подменяет юзер-агент и другие свойства navigator, такие как oscpu и hardwareConcurrency, чтобы имитировать Chrome на Windows. Метод spoofScreen задаёт стандартное разрешение экрана и связанные параметры, такие как innerWidth и devicePixelRatio, чтобы окно браузера выглядело типичным. Метод spoofMediaDevices переименовывает камеры и микрофоны в "Spoofed Camera" и "Spoofed Mic", чтобы скрыть реальные устройства. Метод spoofGamepads возвращает фейковый геймпад, а spoofBattery блокирует доступ к батарее, выдавая ошибку, потому что при тестах на https://amiunique.org/fingerprint я выяснил, что это более распространённое поведение для батареи, так как на стандартных ПК она обычно так и показывает.
Проверка работы инструмента
В целом, думаю, описание скрипта получилось достаточно понятным, полный код я приложу к статье. А сейчас покажу наглядно, как он работает на https://amiunique.org/fingerprint.
Тут значения агента, платформы, таймзоны и Canvas и тд:
Вы можете заметить, что значения юзер-агента и Canvas не самые распространённые, но, поверьте, это максимально общие значения, которых получилось добиться. Другие юзер-агенты или методики с Canvas давали худшие результаты, но вы можете самостоятельно поэкспериментировать и подобрать значения.
Теперь значения браузера, вроде отсутствия buildID, вендора, памяти и ядер - как вы видите, они помечены зелёным и весьма распространённые:
Значения экрана, вроде размера и отступов, тоже весьма распространённые, хоть размеры и помечены жёлтым - думаю, это достаточно приемлемо для отпечатка:
Кстати, занятный факт: отступы, равные 0, я сделал именно потому, что на стандартной Windows так и бывает.
История с WebGL вышла средней: значения не прямо популярные, но и не редкие, и это пока лучший результат из комбинаций, которого я добился:
По поводу медиа-устройств подделка почти всего вышла достаточно гладкой, кроме AudioContext — там значения немного уникальны, ибо я указал, что поддержки нет. Но когда я указывал иные значения, уникальность вообще показывала от 0% до максимум 0.26%, поэтому я решил, что 1.96% не так уж плохо. Но вы можете поэкспериментировать самостоятельно.
Мой результат:
Вывод
Итак, мы завершили разбор подделки отпечатков в Firefox, и пора подвести итоги. На мой взгляд, Firefox выигрывает у Chrome в плане гибкости, когда речь заходит о маскировке отпечатков. Удивительно, как легко в нём можно подменять параметры через JavaScript или плагины. Взять хотя бы шрифты: в Chrome их подделка требует системных манипуляций, а в Firefox достаточно пары строк кода на JS, чтобы перехватить или изменить данные, которые сайты используют для идентификации. Ещё один плюс Firefox — в его коде изначально вырезано множество параметров, которые могли бы стать частью отпечатка. Это делает его отличной платформой для тех, кто хочет минимизировать утечку данных без сложных настроек.
Но, как и в любой истории, есть свои минусы.
Firefox оказался не слишком дружелюбным к автоматизации. Когда я пытался управлять им через Puppeteer, наткнулся на ограничения: поддерживаются только старые версии браузера, что явно не плюс. Другие инструменты на Node.js, такие как Playwright, вообще не могли нормально подменять значения. Хорошо работал только Selenium, который, хоть и запускается, но не позволяет полноценно подделывать JavaScript. (Изначально я хотел добавить раздел про инструменты автоматизации в Firefox, но подумал, что разбор, почему они не работают, будет слишком скучным. Но, если вам интересно, напишите в комментариях, и я сделаю отдельную небольшую статью об этом).
Потому для этого приходится полагаться на плагины. Так что, если ваша цель — антидетект, Firefox прекрасен для ручной настройки и плагинов, но для автоматизации он в разы уступает Chrome.
На этом я не прощаюсь, ведь впереди нас ждёт следующая часть - разбор подделки отпечатков в Tor Browser. Там я сосредоточусь на его внутренней работе и сборке из исходников. Tor — это совсем другая история, с уникальными вызовами и подходами, так что будет интересно. Спасибо за внимание, до встречи в следующей статье!
Вложения
Последнее редактирование модератором: