CHROMIUM

Chromium: ОС внутри ОС

By }Marka{


Оглавление

  1. Введение: Почему Chromium — это не просто браузер
  2. Многопроцессная архитектура: Разделяй и властвуй
  3. Межпроцессное взаимодействие: Mojo и IPC
  4. Анатомия вкладки: от URL до пикселя
  5. Сетевой стек Chromium (Network Service)
  6. Песочница (Sandbox): Браузерная тюрьма
  7. Зачем его перепрошивать? (Взгляд со стороны автоматизации)
  8. Заключение

1. Введение: Почему Chromium — это не просто браузер, а операционная система внутри операционной системы

Когда обычный пользователь говорит «браузер», он видит строку поиска, вкладки и кнопку «Обновить». Когда разработчик говорит «браузер», он видит WebView или CEF. Но когда инженер по автоматизации, хардкорный дебаггер или создатель антидетект-системы говорит «Chromium», его взгляд уходит в сторону диспетчера задач, где висят 50–60 процессов, а пульс учащается при слове «sandbox».


Миф и реальность: Чем отличается Хром от Хромиума

Прежде чем залезть в глотку этому зверю, разберемся с именами. Многие путают Chromium, Chrome, Blink и V8, думая, что это синонимы. Это смертельная ошибка для инженера.


Масштаб трагедии: Почему сборка Chromium — это боевое крещение

Перейдем к цифрам. Chromium — это не «еще один опенсорсный проект». Это монстр.

Кто-то спросит: «Зачем вообще это собирать, если можно взять готовую сборку?». А затем, что только через сборку из исходников ты получаешь доступ к настоящей кастомизации. Хочешь выпилить --no-sandbox как порождение дьявола? Хочешь подменить TLS-отпечаток на уровне сокетов? Хочешь убрать флаг navigator.webdriver навсегда, не маскируя его в рантайме? Тогда добро пожаловать в ад depot_tools, gn gen и ninja.


Главная парадигма: Безопасность, Стабильность, Скорость (через изоляцию)

Почему Chromium спроектирован так монструозно? В 2008 году, когда он только появился, все браузеры были однопроцессными. Баг в Flash (тогда он был жив) валил весь браузер. Одна кривая страница — и все 15 вкладок прощаются с жизнью.

Инженеры Google сказали: «Хватит». И родилась архитектура, которая легла в основу современного веба:

Итог: Chromium — это микроядро, окруженное полчищем изолированных процессов-слуг. Browser Process — это король (имеет доступ ко всему). Renderer Process — рабы в песочнице (не имеют ничего). А общаются они через спецсвязь под названием Mojo IPC.


2. Многопроцессная архитектура: Разделяй и властвуй

Открой диспетчер задач Windows прямо сейчас. Найди chrome.exe (или chromium.exe, если ты истинный ценитель). Сколько их? Десять? Пятнадцать? А если открыто двадцать вкладок — будет под сорок-пятьдесят. Нормальный человек видит в этом бардак и жор памяти. Инженер видит гениальную архитектуру отказоустойчивости.

Запомни главную мантру Chromium: Ни один процесс не доверяет другому процессу. Даже если этот другой процесс — его собственная вкладка.

Это не паранойя. Это единственный способ заставить браузер работать 24/7 без перезагрузок, когда внутри исполняется потенциально опасный код из интернета.


Почему раньше всё было плохо (историческая справка за 30 секунд)

В старых браузерах (Internet Explorer 6, Firefox до версии 3) всё сидело в одном потоке. Одна вкладка с бесконечным циклом while(true) — и весь браузер превращается в ледышку. Один баг в Flash-баннере — и ты потерял все открытые формы, все недоотправленные письма, все наработки. Разработчики плакали. Пользователи матерились.

Google в 2008 году сказали: «А давайте сделаем как в операционной системе». Каждая вкладка — отдельный процесс. Упал один — остальные работают. Идея гениальная в своей простоте. Но реализация… реализация превратилась в монстра.


Browser Process: Король, у которого есть все ключи

Представь себе короля. Он сидит в тронном зале (главное окно браузера). У него единственного есть доступ к королевской казне (файловая система), единственный он может принимать послов (сетевые запросы через OS) и единственный он управляет армией (железом, камерой, микрофоном).

Browser Process — это и есть этот король. В твоей системе висит ровно один Browser Process на все окна браузера (если ты не открыл отдельный профиль с флагом --user-data-dir). Он делает следующее:

И самое главное: Browser Process — единственный процесс во всей архитектуре, который имеет прямой, неограниченный доступ к операционной системе. Он может читать твои файлы, лазить в реестр Windows, открывать сокеты. Именно поэтому антивирусы так трясутся над chrome.exe — это действительно проводник между внешним миром и твоим компьютером.


Renderer Process: Раб в песочнице, который ничего не умеет

А теперь представь себе раба. Его посадили в бетонную коробку (песочница, Sandbox). У него нет ножа, нет телефона, нет окон, нет дверей. Он умеет только одно: получать приказы из маленького окошка в стене и выполнять их.

Renderer Process — это и есть такой раб. Он живет на каждый сайт (или на каждую вкладку, если отключена Site Isolation). Именно внутри него происходит всё волшебство: HTML парсится в DOM-дерево, JavaScript исполняется в V8, CSS раскладывается в Layout, и даже пиксели растрируются. Но при этом:

Если он упадет (разработчик накосячил с pointer’ом в C++, или сайт сделал Array(1e9).map(x => x) и сожрал всю память) — Browser Process просто пожмет плечами, закроет вкладку, покажет надпись «Aw, Snap!» и запустит новый Renderer Process. Остальные вкладки даже не заметят.

Это и есть та самая стабильность. Один процесс-раб умер — король нанял нового.


Почему вкладок 10, а процессов 30? (Осторожно: Site Isolation)

В ранних версиях Chromium была простая логика: одна вкладка = один рендерер. Потом пришли атаки типа Spectre и Meltdown (когда один процесс мог читать память другого процесса через спекулятивное выполнение команд). Инженеры Google сказали: «Хватит, теперь даже разные домены внутри одной вкладки должны жить в разных процессах».

Site Isolation — это когда google.com и evil.com даже внутри одной вкладки (через iframe) сидят в РАЗНЫХ Renderer Process’ах. Это дало колоссальный прирост безопасности, но убило логику «один процесс на вкладку». Теперь:

Вот и набежало 50 процессов. И да, это жрет память. Но безопасность и стабильность дороже.


GPU Process: Почему синий экран смерти из-за видео — это прошлый век

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

Теперь вся работа с графикой (в том числе отрисовка UI самого браузера, CSS-анимации, WebGL, Canvas) вынесена в отдельный GPU Process. Если видеодрайвер решит упасть — упадет только GPU Process. Browser Process заметит это, перезапустит его, попросит рендереры перерисовать всё заново, и пользователь увидит лишь микро-рывок, а не критический краш.

У GPU Process одна задача: принимать команды «нарисуй треугольник», «залей пиксель» и отправлять их драйверу видеокарты. Работает быстро, падает изолированно.


Network Process: Сетевой стек, который не доверяет никому

Это нововведение (появилось в полную силу в 2019-2020 году). Раньше сетевыми запросами занимался сам Browser Process. Потом инженеры решили: «А давайте вынесем сеть в отдельный процесс, чтобы даже если хакер взломал Browser Process, он не получил бы доступ ко всем сокетам и TLS-ключам».

Network Process живет отдельно. Он управляет:

Все остальные процессы (рендереры, браузер) говорят Network Process: «Отправь запрос на такой-то URL». Network Process отправляет, получает ответ и возвращает данные. Он никогда не доверяет данным из рендерера до конца, проверяет заголовки, фильтрует опасные схемы вроде file://.


Storage, Utility и Плагины: Свита короля

Остальные процессы решают узкие задачи.


Почему это всё работает именно так

Запомни главное: Chromium — это не набор процессов, а иерархия с чёткими границами ответственности и намеренно урезанными привилегиями.

Благодаря этой архитектуре можно открыть chrome://crash в одной вкладке (что принудительно убивает её рендерер), а остальные 149 вкладок продолжат работать как ни в чём не бывало. YouTube не прервётся, форма регистрации не сбросится, текст в textarea не пропадёт. Ты это воспринимаешь как должное, но за этим стоит десятилетие эволюции сложнейшей распределённой системы.


3. Межпроцессное взаимодействие: Как они общаются (Mojo и IPC)

И вот здесь мы подходим к самому интересному. Если процессы изолированы, у них нет общей памяти, они не могут вызвать функцию друг друга — как Renderer Process говорит Browser Process «скачай этот URL» или «запиши cookie»?

Ответ: через межпроцессное взаимодействие (IPC). И в Chromium для этого есть свой собственный, выстраданный фреймворк — Mojo.

Старый IPC, который использовался до Mojo, работал на сырых сообщениях: ты сериализовал вручную структуру данных в бинарный буфер, отправлял через канал (на Windows — через именованные пайпы, на Linux — через сокет), на другой стороне парсил и диспатчил. Это было быстро, но:

Mojo — это современная замена. Это не просто библиотека, а целый генератор кода. Ты описываешь интерфейс на специальном языке .mojom:

interface NetworkService { FetchURL(string url, FetchCallback callback); };

А Mojo генерирует:

C++ класс-клиент для отправителя (рендерера).

C++ класс-сервер для получателя (Network Process).

Весь код сериализации, маршалинга, диспатчинга.

Из рендерера ты пишешь просто:

network_service->FetchURL(“https://example.com”, callback); Выглядит как обычный вызов функции. Но под капотом Mojo упаковывает вызов в сообщение, шлёт через IPC-канал, на той стороне распаковывает, вызывает настоящую функцию в Network Process, а результат так же элегантно возвращает обратно.

Почему это важно для тех, кто хочет «перешить» браузер? Потому что все важные точки взаимодействия торчат именно через Mojo. Хочешь подменить поведение сетевого стека? Переопредели реализацию NetworkService. Хочешь заблокировать передачу геолокации? Подмени обработчик в Browser Process. Хочешь понять, какие фингерпринты утекают на сервер — следи за сообщениями Mojo между рендерером и браузером.

Именно здесь, на уровне IPC и Mojo-интерфейсов, рождается большая часть детектов автоматизации. Потому что автоматизированный браузер (Puppeteer, Playwright) использует те же Mojo-каналы, но отправляет флаги вроде –enable-automation, которые меняют поведение. И если ты работаешь через DevTools Protocol — ты уже засветился на уровне IPC.

  1. Анатомия вкладки: Путь от URL до пикселя на экране Открывай любую страницу. Введи в адресной строке https://habr.com и нажми Enter. Что произошло за те 200-300 миллисекунд между нажатием клавиши и появлением текста на экране?

Список действий займёт полстраницы. Мы пройдём по ключевым этапам: как HTML становится деревом, как CSS раскладывает элементы по координатам, как JavaScript компилируется в машинный код и как всё это превращается в картинку через GPU.

От байтов до DOM: Рождение структуры Первое, что получает Browser Process после того, как Network Process скачал ответ сервера, — это поток байтов. Чаще всего — HTML, но может быть JSON, PDF или картинка. Допустим, это HTML.

Дальше работает Blink (движок рендеринга, форк WebKit). Его задача — превратить это в живое, изменяемое дерево.

Этап 1: Байты → символы

Сетевой стек отдаёт буфер. Blink смотрит на заголовок Content-Type: text/html; charset=utf-8 (или ищет внутри) и декодирует байты в символы Юникода. На этом этапе решается проблема с BOM-последовательностями и кривыми кодировками.

Этап 2: Лексер (токенизация)

Этот поток символов пропускается через HTML-лексер. Он ничего не знает про семантику — он просто умеет распознавать паттерны: открывающий тег, закрывающий тег, атрибут, текст, комментарий, DOCTYPE. Лексер идёт по символам слева направо и выдаёт поток токенов:

StartTag: div Attribute: class=”container” EndTag: div Character: ‘П’, ‘р’, ‘и’, ‘в’, ‘е’, ‘т’ Этап 3: Парсер → DOM-дерево

А вот здесь начинается магия. HTML-парсер в Blink — это не просто рекурсивный спуск, потому что HTML прощает ошибки (нет закрывающего тега? ничего страшного). Парсер получает токены и строит DOM-дерево (Document Object Model). Это живая структура в памяти: каждый узел — C++ объект с указателями на parent, firstChild, nextSibling, плюс куча служебных полей.

Ключевой момент: парсер не ждёт полной загрузки HTML. Как только он получил достаточно байтов, чтобы распарсить открывающий тег, он уже создаёт узел. Это называется speculative parsing. Браузер начинает рисовать страницу, даже когда тело документа ещё качается из сети.

И здесь же кроется первая проблема производительности. Когда парсер встречает тег

CSSOM: Красота требует жертв Пока парсер строит DOM, Network Process точно так же качает CSS-файлы (через ) и разбирает их. CSS — это другая грамматика, но принцип тот же: байты → символы → токены (селектор div.container, свойство width: 100px, значение). Парсер CSS строит CSSOM-дерево (CSS Object Model).

Здесь есть одна важная особенность: CSSOM блокирует рендеринг. Браузер не нарисует на экране ни одного пикселя, пока не построит CSSOM для текущей страницы. Потому что иначе был бы FOUC (Flash of Unstyled Content) — сначала показался бы текст без стилей, а через мгновение он бы дёрнулся, оформившись в красивые карточки.

Но есть нюанс. Браузер не ждёт CSS, который лежит в медиа-запросах media=”print” and (min-width: 1024px)”. Такие стили помечаются как «не критичные» и не блокируют рендер.

Итог: DOM + CSSOM = RenderTree. Это не просто копия DOM. В RenderTree попадают только видимые элементы: display: none вырезается, meta-теги игнорируются, псевдоэлементы ::before добавляются. Каждый узел RenderTree знает свои стили: шрифт, цвет, размер, положение относительно родителя.

Layout: Поиск координат и размеров Теперь у нас есть дерево визуальных объектов со стилями, но мы не знаем, где они находятся. Где именно на экране рисовать эту кнопку? Какой у неё width, если написано width: 50% — 50% от чего?

Layout (или Reflow) — это алгоритм, который вычисляет геометрию каждого элемента. Для Blink это гигантский конечный автомат, который начинает с корневого <html> и рекурсивно обходит RenderTree, вычисляя:

x, y — координаты относительно вьюпорта.

width, height — фактические размеры в пикселях (с учётом процентных соотношений, calc(), min-width, max-width).

margin, border, padding — блочная модель.

Самое тяжёлое в Layout — это flexbox и grid. Для блочной модели достаточно одного прохода сверху вниз, но флексы требуют вычисления размеров flex-контейнера после того, как вычислены размеры всех flex-элементов, а это уже два прохода. Это одна из причин, почему на сложных интерфейсах тормозит ресайз окна.

Важный практический вывод: каждое изменение свойства, влияющего на геометрию (например, width, height, margin, padding, font-size), запускает Layout заново. transform и opacity — не запускают. Именно поэтому в анимациях используют transform: translateX() вместо left/top.

V8 под капотом: Как JavaScript становится быстрым

JavaScript в браузере не интерпретируется построчно, как в начале 2000-х. Это было бы слишком медленно для современных приложений. Вместо этого V8 (движок JS от Google) компилирует код на лету (JIT — Just-In-Time compilation).

Базовый сценарий для «холодного» кода:

Сначала код попадает в Ignition — байткод-интерпретатор. V8 быстро парсит JS в абстрактное синтаксическое дерево (AST), а потом генерирует компактный байткод. Ignition выполняет его, собирая при этом профилирующую информацию: какие функции вызываются часто, какие типы данных передаются в аргументы, какие ветки условий редко выполняются.

Когда код становится горячим (TurboFan):

Если Ignition замечает, что какая-то функция выполнилась несколько тысяч раз, он передаёт её в TurboFan — оптимизирующий JIT-компилятор. TurboFan берёт байткод и профилирующую информацию и генерирует высокооптимизированный машинный код под конкретную архитектуру процессора (x64, ARM, MIPS).

Что значит «оптимизированный»? TurboFan делает:

Но есть и обратная сторона. Если предположения TurboFan нарушатся (например, после 5000 вызовов с числами вдруг пришла строка), V8 делает deoptimization — откатывается обратно на байткод в Ignition, потеряв все оптимизации. Это называется «оптимизационный фантом» и иногда является причиной резких фризов в приложениях на React/Vue.


Как это связано с фингерпринтингом?

А вот как. V8 компилирует код в реальный машинный код, который исполняется прямо на процессоре. Разные модели процессоров имеют:

Сайты с антифрод-системой могут запустить специальный JS-код, который замеряет время выполнения определённых операций (например, умножение матриц или хеширование через bcrypt). Разница в микросекундах позволяет определить точную модель процессора. А если ты запускаешь браузер в виртуальной машине — тайминги будут совершенно другими. Это один из самых сложных для обхода слоёв детекта, потому что на уровне JavaScript ты с ним ничего не сделаешь — он лежит в недрах генерации машинного кода V8.


Compositing и Rasterization: Последние мили до пикселя

Итак, у нас есть Layout (геометрия) и стили. Дальше начинается работа с графикой.

Этап 1: Paint (рисование)

Blink обходит RenderTree и генерирует записи рисования (paint records). Это не пиксели, а команды вроде «нарисовать прямоугольник от (10,10) до (100,100) красным цветом» и «нарисовать текст «Привет» шрифтом Arial 16px». Всё это складывается в список.

Этап 2: Rasterization (растеризация)

Список команд отдаётся в GPU Process. Здесь он разбивается на тайлы (обычно 256x256 или 512x512 пикселей). Каждый тайл растеризуется — превращается в настоящую битмапу, заполняя цветом каждый пиксель. Если страница использует CSS-фильтры или WebGL, то растеризация идёт через шейдеры прямо на видеокарте.

Этап 3: Compositing (композитинг)

Страница может состоять из десятков слоёв (will-change: transform, <video>, <canvas>, position: fixed). Каждый слой растеризуется отдельно. Задача композитинга — взять все эти слои, наложить их друг на друга с учётом прозрачности, z-index и трансформаций и отправить на экран.

Самое крутое в композитинге — это то, что трансформации слоёв выполняются на GPU без перерисовки содержимого. Именно поэтому анимация transform: translateX() работает плавно, а анимация left дёргается — left запускает Layout (дорого), а transform только композитинг (дёшево).


Собираем всю цепочку

От ввода URL до пикселя на экране:

  1. Network Process: скачивает HTML, отдаёт Blink.
  2. HTML-парсер: байты → символы → токены → DOM-дерево (с остановками на <script> без async).
  3. CSS-парсер: байты → CSSOM (блокирует рендер).
  4. RenderTree: DOM + CSSOM = визуальное дерево со стилями.
  5. Layout: вычисление координат и размеров каждого элемента (дорого).
  6. Paint: генерация команд рисования.
  7. Rasterization: команды → пиксели (можно через GPU).
  8. Compositing: слои → финальный кадр → дисплей.

Всё это происходит за десятки-сотни миллисекунд. И перезапускается каждый раз, когда ты меняешь размер окна, скроллишь страницу или JS-скрипт добавляет новый <div> в DOM.

Именно здесь, в недрах Layout и растеризации, рождаются многие уязвимости для фингерпринтинга — от измерения времени отрисовки до определения наличия GPU через WebGL. Потому что даже если ты заблокировал все JavaScript-флаги, разница в скорости растеризации между реальным ноутбуком с RTX 3060 и виртуальной машиной с софтверным рендерингом — это десятки миллисекунд, которые можно измерить и использовать против тебя.


5. Сетевой стек Chromium (Network Service)

Открой любой сайт. Chromium отправил десяток HTTP-запросов, скачал HTML, CSS, JS, картинки. Казалось бы, какая разница — что там внутри? ОС умеет делать сокеты, curl умеет ходить по HTTP, зачем изобретать велосипед?

А затем, что системные библиотеки (WinHTTP на Windows, libcurl на Linux) — это как советский гаечный ключ: он работает, но затянуть им болт с моментом 1.2 Н·м и телеметрией по каждому повороту не получится.

Chromium тащит свой монструозный стек, потому что ему нужно:

В общем, добро пожаловать в Network Service — процесс, который живёт отдельно, имеет свои сокеты, свой DNS-кэш, свои пулы соединений и свою боль.


Почему системные библиотеки — это боль

Представь, что ты используешь libcurl или WinHTTP. Что ты получишь?

Поэтому инженеры Chromium написали свой стек. С нуля. На C++. С поддержкой всех современных протоколов. И назвали это network stack, а позже вынесли в отдельный Network Service.


Что внутри Network Service: Три кита

1. Кастомный DNS-резолвер с DoH

Твой компьютер при запросе google.com обычно стучится на DNS-сервер провайдера по UDP-порту 53 в открытую. Провайдер (и твой сисадмин, и Агентство национальной безопасности, если ты параноик) видит, куда ты ходишь.

Chromium делает иначе:

Почему это важно для автоматизации? Потому что DNS-запросы видны на уровне ОС. Если ты запускаешь браузер в Docker или виртуальной машине с дефолтным резолвером 8.8.8.8, а нормальные пользователи в этом регионе пользуются DNS провайдера — это заметно. Некоторые антифрод-системы проверяют, используешь ли ты DoH и какой именно сервер. Отклонение от «нормального» поведения — флаг.


2. Сокет-пулы и мультиплексирование

Каждый раз, когда сайт загружает картинку, CSS и JS, создавать новый TCP-сокет и устанавливать соединение — это три рукопожатия (SYN, SYN-ACK, ACK). Для HTTP/1.1 можно переиспользовать соединение через Keep-Alive, но на один домен — не больше 6 параллельных соединений (ограничение самого протокола, иначе ты забьёшь сокеты).

Chromium идёт дальше:

Практический вывод для автоматизаторов: если ты поднимаешь браузер с --disable-quic, ты уже отличаешься от нормального Chromium. Антифрод-системы типа Cloudflare проверяют поддержку QUIC на уровне TLS-отпечатков (JA3, JA4). Если её нет — это подозрительно.


3. Маршрутизация трафика: как и куда идут запросы

И вот здесь самое интересное. Каждый запрос из рендерера проходит через иерархию прокси и фильтров:

Типичный путь запроса: Renderer Process → Browser Process → Network Process → [системные прокси] → [расширения с webRequest] → сокет → интернет

Но это если всё стандартно. Chromium позволяет настраивать прокси на нескольких уровнях:

И всё это — внутри Network Service. Ты можешь подменить прокси для google.com, но не для facebook.com. Можешь добавить заголовок X-Forwarded-For для всех запросов через расширение. Можешь даже написать свой кастомный прокси-сервер на Mojo (но это уже экзотика).


Почему это важно для антидетекта?

Антифрод-системы смотрят не только на User-Agent и navigator.webdriver. Они анализируют:


Пример из практики: как Cloudflare детектит прокси

Cloudflare (и Kasada, и другие WAF) при заходе на сайт запускает JavaScript, который:

  1. Делает fetch на специальный эндпоинт, который отвечает только по HTTP/2 и HTTP/3.
  2. Замеряет, сколько времени прошло между performance.now() в JS и моментом получения ответа.
  3. Сравнивает этот RTT с RTT на уровне TCP (измеренным на их стороне).

Если браузер идёт через прокси, эти два RTT будут сильно различаться (прокси добавляет задержку). Если QUIC отключен — тоже видно, потому что запрос пойдёт по TCP и будет другой паттерн таймингов.

Или ещё жёстче: Cloudflare проверяет, совпадает ли IP, на который пришёл TLS-ClientHello, с IP, с которого пришли DNS-запросы. Если DNS-запросы шли с одного IP (например, из AWS), а TLS-хендшейк — с другого (твой прокси-сервер), это явный признак подмены.


Что это значит для кастомной автоматизации?

Если ты собираешься переписывать Chromium для обхода антифрода, сетевая часть — один из ключевых фронтов. Тебе нужно:

Именно поэтому стандартные решения (Puppeteer, Playwright, Selenium) палятся — они используют флаги, которые меняют поведение Network Service.


6. Песочница (Sandbox): Браузерная тюрьма

Открой любой сайт. На нём — JavaScript, который может попытаться прочитать твой /etc/passwd или C:\Users\Admin\Documents\passwords.txt. Или установить rootkit. Или замайнить криптовалюту на твоей видеокарте.

Почему это не работает? Потому что Chromium не доверяет сайтам. Вообще. Ни капли. Каждый Renderer Process запускается в песочнице — изолированной среде, где у процесса нет прав практически ни на что.

Sandbox — это не метафора. Это конкретный код на C++, который говорит операционной системе: «Забери у этого процесса всё. Оставь только возможность дышать и переговариваться с родителем через узкую трубу IPC».


Зачем вообще нужна песочница (история одной уязвимости)

Представь: ты нашел баг в V8 — use-after-free в JIT-компиляторе. Ты можешь выполнить произвольный код внутри Renderer Process. Что дальше?

Песочница не делает твой код безопасным. Она делает так, что даже если его взломали — ущерб минимален. Это называется принцип наименьших привилегий (least privilege).


Как это реализовано на разных ОС

Chromium — кроссплатформенный. Но песочница на Windows и Linux устроена по-разному, потому что у ОС разные механизмы изоляции.


Windows: Токены, Job Objects и Desktop

Windows — сложная система с тридцатилетней историей обратной совместимости. Но Chromium выжимает из неё максимум.

Когда Browser Process создаёт новый Renderer Process, он делает следующие шаги:

  1. Ограниченный токен доступа (Restricted Token). Обычный процесс имеет токен с кучей привилегий: SeBackupPrivilege, SeDebugPrivilege, SeShutdownPrivilege. Chromium создаёт дочерний токен, из которого вырезано почти всё:
    • Удалены все привилегии (SeChangeNotifyPrivilege остаётся только для того, чтобы процесс вообще мог запуститься).
    • Ограничены SID (Security Identifiers) — процесс не может получить доступ к объектам, которые принадлежат администраторам или системе.
    • Включены только специфичные права вроде PROCESS_TERMINATE (родитель может убить ребёнка) и PROCESS_QUERY_INFORMATION (родитель может спросить статус).
  2. Job Object. Это контейнер для группы процессов. Chromium помещает рендерер в Job Object с ограничениями:
    • Лимит на использование процессора (чтобы злой JS не загрузил все ядра на 100%).
    • Лимит на память (чтобы бесконечный new Array(1e9) не положил всю систему).
    • Запрет на создание дочерних процессов (чтобы рантайм не запустил cmd.exe).
    • При закрытии последнего окна Job Object убивает все процессы внутри (чистый выход).
  3. Десктоп-изоляция (Desktop, Window Station). Это уже экзотика, но используется для защиты от атак через GUI: рендерер не может создавать окна вне своей изолированной области.
    • ALTAS (Alternate Desktop) — дополнительный уровень для отлова атак через SendMessage.

Итог на Windows: Renderer Process — это карлик, который не может прочитать твои файлы, не может создать процесс, не может вылезти за пределы своей клетки. Единственная связь с внешним миром — именованный пайп (named pipe) для Mojo IPC с Browser Process.


Linux: Namespaces и seccomp-bpf

Линуксоиды любят говорить, что у них безопасность «из коробки». Но на деле Chromium использует те же механизмы, что и Docker.

namespaces:

seccomp-bpf (Secure Computing mode with Berkeley Packet Filters). Это фильтр системных вызовов на уровне ядра. Chromium загружает в ядро BPF-программу, которая говорит: «Разрешаю только read, write, openat (на ограниченный набор путей), futex, exit_group. Всё остальное — kill -SIGSYS». Если взломанный рендерер попытается вызвать socket() (открыть сеть) или execve() (запустить процесс), ядро просто убьёт процесс.


macOS: Sandbox.kext и Seatbelt

На macOS всё похоже на Linux, но с проприетарным привкусом. Chromium использует sandbox_init() с профилями Seatbelt (это как seccomp, но на XML-стероидах). Рендерер не имеет доступа к камере, микрофону, файлам за пределами ~/Library/Caches/Chromium. И да, заветный kAuth (запрос прав) — только через Browser Process.


Почему песочница — это головная боль для автоматизации

И вот мы подходим к главному. Ты пишешь антидетект-браузер. Ты хочешь, чтобы твой рендерер мог:

Но песочница запрещает всё это. Буквально. Renderer Process в песочнице — это овощ, который не может даже пикнуть без разрешения Browser Process.

Что делают разработчики автоматизации? Есть два пути.

Путь первый (правильный, но сложный):

Написать свой Browser Process, который кастомно обрабатывает Mojo-запросы из рендерера и даёт им расширенные права. Хочешь, чтобы рендерер открыл сокет — расширяешь NetworkService, добавляешь новый метод OpenRawSocket. Хочешь прямой доступ к файлу — добавляешь RequestFileAccess. Это требует глубокого понимания Mojo, IPC и исходников Chromium. Но песочница остаётся включённой — ты просто расширяешь белый список разрешённых действий.

Путь второй (простой, но опасный и тупой):

Запустить браузер с флагом --no-sandbox. В этом случае Renderer Process получает полные права: может открывать сокеты, читать файлы, вызывать системные команды. Удобно для отладки. Но в продакшене антидетекта это смертный грех и признак плохого тона.


Почему --no-sandbox — это зло (даже в антидетекте)

Технически --no-sandbox отключает все механизмы, которые мы описали выше. На Windows рендерер получает полный токен. На Linux — живёт без seccomp. На macOS — без Seatbelt.

Почему это плохо для антидетект-браузера?

  1. Безопасность. Твой браузер заходит на сайт. На сайте — злой JavaScript. Если у рендерера есть баг в V8, злоумышленник получает полный доступ к твоей системе. Все профили клиентов, все куки, все прокси — в руках атакующего. Ты как провайдер антидетекта становишься вектором заражения. Клиенты тебя возненавидят.

  2. Детект через проверку песочницы. Антифрод-система (Cloudflare, PerimeterX, Akamai) может проверить, работает ли рендерер в песочнице. Как?
    • Попытка открыть сокет напрямую из JS (через WebSocket или RTCPeerConnection) и анализ ошибок. В песочнице ошибка одна (NetworkError), без песочницы — другая.
    • Измерение времени доступа к системным ресурсам (например, попытка прочитать file:///etc/passwd). В песочнице такая попытка завершится мгновенной ошибкой, без песочницы — будет реальная попытка доступа, которая либо упадёт, либо (о ужас) прочитает файл, что сразу выдаст автоматизацию.
    • Анализ отпечатков системных вызовов (это уже уровень ядра, но продвинутые антифрод-системы умеют).
  3. Нестабильность. Некоторые сайты ожидают, что браузер живёт в песочнице. Без неё их поведение меняется — они могут отказаться работать или показать капчу.

  4. Флаг виден в chrome://version. Любой сайт через navigator.userAgent и chrome://version (если есть доступ к этой странице) может увидеть --no-sandbox в командной строке браузера.

Вывод: --no-sandbox допустим только при локальной отладке и разработке. В продакшене антидетект-браузера, который претендует на звание «невидимый», песочница должна быть включена, а доступ к ресурсам должен быть расширен аккуратно, через кастомные Mojo-интерфейсы, а не топорным отключением защиты.


Как обойти песочницу правильно

Правильный способ (именно так делают взрослые антидетект-системы):

  1. Собираешь Chromium из исходников.
  2. Не трогаешь флаг --no-sandbox. Песочница включена.
  3. Идёшь в //content/browser/renderer_host/render_sandbox_host_linux.cc (или Windows-аналог) и добавляешь новые разрешённые операции для рендерера.
  4. Создаёшь кастомные Mojo-интерфейсы в //services/network/public/mojom/. Например, AllowRawSocketForAutomation.
  5. Реализуешь их в //services/network/network_service.cc. Так, чтобы на запрос рендерера открыть сырой сокет Browser Process говорил: «Ок, только для доменов из белого списка и с проверкой IP».
  6. Собираешь заново.

Это сложно. Это долго. Но именно так рождаются антидетект-браузеры, которые не палятся (или палятся сильно позже). Всё остальное — поделки на коленке, которые умрут при первом заходе на Cloudflare.


Итог по песочнице

Sandbox в Chromium — это не прихоть, а системообразующий элемент безопасности. Он делает жизнь взломщика сайтов невыносимой, но и жизнь автоматизатора — тоже.

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

7. Зачем его перепрошивать? (Взгляд со стороны автоматизации)

Ты написал парсер на Puppeteer. Всё работает локально. Ты настраиваешь прокси, подменяешь User-Agent, даже navigator.webdriver = false через хуки. Запускаешь на продакшене — и через пять минут Cloudflare показывает «I’m Under Attack Mode», а аккаунт на целевом сервисе заблокирован.

Что пошло не так? Всё.

Стандартные инструменты автоматизации — Puppeteer, Playwright, Selenium — великолепны для тестирования и простых парсеров. Но они создавались для контроля качества, а не для войны с антифрод-системами. Они светятся на каждом шагу. И современные WAF (Web Application Firewall) вроде Cloudflare, Akamai, Kasada, ProtectedMedia детектят их за долю секунды.

Почему? Потому что антифрод-системы больше не смотрят только на User-Agent и количество запросов в секунду. Они анализируют десятки, сотни сигналов — от переменных в window до отпечатков TLS и таймингов JIT-компиляции. И чтобы победить, нужно лезть внутрь Chromium и переписывать его поведение на уровне C++.


Почему стандартные инструменты палятся: Лига детектов

Давай пройдёмся по основным сигнатурам, которые выдают автоматизацию.


1. Флаги командной строки

Запусти любой браузер через Puppeteer. Открой chrome://version. Что ты увидишь?

Command Line: /path/to/chrome –disable-background-networking –disable-client-side-phishing-detection –disable-default-apps –disable-hang-monitor –disable-popup-blocking –disable-prompt-on-repost –disable-sync –enable-automation –force-color-profile=srgb –no-first-run –password-store=basic –use-mock-keychain

--enable-automation — это красная тряпка для антифрода. Этот флаг добавляется автоматически при использовании DevTools Protocol (которым пользуются и Puppeteer, и Playwright). Он делает видимыми:

Даже если ты через хуки подменил navigator.webdriver, --enable-automation виден в командной строке, а из JS это можно проверить через chrome.runtime (если есть доступ) или через анализ performance.getEntries() и специфические баги в Error.stack.


2. Отпечатки DevTools Protocol

Puppeteer и Playwright общаются с браузером через DevTools Protocol (CDP). Это набор JSON-RPC-команд на WebSocket, который позволяет управлять вкладками, эмулировать клики, вводить текст.

Антифрод-система может:


3. Отпечатки фреймворка

Движки автоматизации вносят изменения в navigator и window:


4. Тайминги и поведение

Человек не идеален. Он кликает не точно в пиксель, а с небольшим офсетом. Он двигает мышь по кривой, а не по прямой. Он скроллит с переменной скоростью.

Автоматизация же:

Антифрод-системы собирают метрики мыши, клавиатуры, тач-событий (на мобилках) и прогоняют через ML-модель. Если паттерн слишком «чистый» — это бот.


5. Сетевой стек и TLS-отпечатки

Это мы уже разбирали в пятой части, но повторю ключевые моменты:


6. WebGL и Canvas Fingerprinting

Это уже из области фингерпринтинга. Антифрод-система рисует невидимый canvas с текстом и считывает пиксели. Разные браузеры, ОС, драйверы, видеокарты — дают разные оттенки из-за антиалиасинга, субпиксельного рендеринга, настроек сглаживания.

В headless-режиме (или в Docker без GPU) отпечаток canvas будет отличаться от реального браузера. Например, в headless-режиме нет аппаратного ускорения, и некоторые шейдеры падают на софтрендер с другими числовыми значениями.


7. WebGL Vendor и Renderer

Через WebGL можно получить строку GL_VENDOR и GL_RENDERER. В реальном Chrome на Windows это будет Google Inc. (NVIDIA Corporation) и NVIDIA GeForce RTX 3060/PCIe/SSE2. В Docker без GPU — VMware, Inc. или Mesa Project (софтверный рендер). Это мгновенный детект виртуалки.


8. Баги в V8 и специфические свойства

Разработчики антифрода ищут несуществующие свойства в объектах, которые есть только в автоматизации, или наоборот — отсутствие свойств, которые есть везде.

Примеры:


Почему хуки на JS не работают

«Но я же могу просто переопределить navigator.webdriver = false в начале скрипта!» — скажешь ты.

Нет. Вот почему:

  1. Порядок исполнения. Код на сайте может выполниться раньше, чем твой хук. <script> на странице стартует сразу после парсинга. Чтобы перехватить его до того, нужно использовать --eval или page.evaluateOnNewDocument, но это работает не всегда — некоторые антифрод-системы уже анализируют DOM на этапе парсинга HTML, до выполнения твоих хуков.

  2. Не все свойства можно переопределить. navigator.webdriver — не writable, не configurable. Его нельзя просто так перезаписать. Да, можно через Object.defineProperty или через Proxy, но продвинутые проверки смотрят на внутренние слоты объектов, а не только на значения свойств.

  3. Глубокие проверки. Антифрод может проверить, откуда берётся свойство. Например, проверить цепочку прототипов navigator.__proto__.__proto__, или сравнить нативную функцию с подменённой через toString. Или проверить, что произойдёт при удалении свойства (delete navigator.webdriver) — в нормальном браузере оно не удаляется, в поделке может удалиться.

  4. Сигнатуры на уровне C++. Многие флаги живут в C++ объектах, которые даже не видны из JS. --enable-automation влияет на поведение chrome.runtime, chrome.webstore, chrome.loadTimes() (deprecated, но иногда используется), на алгоритмы рандомизации в Math.random() (да, есть баги, по которым можно определить headless-режим), на поведение Element.getBoundingClientRect() в некоторых edge case.

Ты никогда не сможешь полностью эмулировать реальный браузер из JS-песочницы. Потому что слишком много низкоуровневых вещей торчит через C++-объекты, которые либо есть, либо нет, либо ведут себя иначе.


Единственный способ: перепрошить Chromium на уровне C++

Именно поэтому индустрия антидетекта (и честного парсинга) идёт по пути форка Chromium. Не просто сборка с флагами, а изменение исходного кода на C++.

Что именно меняют?

1. Выпиливание --enable-automation и navigator.webdriver

Идём в //content/common/user_agent.cc и //components/embedder_support/user_agent_utils.cc. Убираем флаг CDP_HOST и все проверки на автоматизацию. Делаем так, чтобы navigator.webdriver всегда был undefined, даже если через DevTools Protocol подключились.

2. Подмена отпечатков сетевого стека

В //net/socket/ssl_client_socket_impl.cc и //net/ssl/ssl_config_service.cc подменяем набор шифров, порядок их отправки, поддержку расширений TLS. Можно захардкодить отпечаток, идентичный реальной версии Chrome на популярной ОС.

3. Эмуляция реального окружения

4. Изменение таймингов и рандомизации

Меняем поведение performance.now() и Date() — добавляем микро-задержки, чтобы они не были идеально ровными. Меняем Math.random() на криптостойкий PRNG, но с определёнными паттернами, чтобы он не палился тестами на энтропию.

5. Патчинг DevTools Protocol

Убираем возможность определять, что к браузеру подключён CDP-клиент. Меняем поведение команд Page.navigate, Input.dispatchMouseEvent так, чтобы они эмитировали события, неотличимые от реальных (добавляем случайные офсеты в клики, случайные задержки, движение мыши по кривой).


Почему это сложно и долго

Перепрошивка Chromium — это не «скачал исходники, поправил 10 строк, собрал». Это:

Но результат того стоит. Браузер, который:

Такой браузер не отличить от реального пользователя (по крайней мере, на текущем уровне технологий). Именно на таких кастомных сборках строятся профессиональные антидетект-системы и enterprise-решения для парсинга.


Заключение

Chromium — это не браузер. Это платформа для построения браузеров. И если тебе нужно не просто открыть страницу, а остаться невидимым для антифрода, стандартные инструменты не подходят.

Puppeteer, Playwright, Selenium — это игрушки для разработчиков, а не оружие для войны с Cloudflare. Настоящая автоматизация начинается там, где ты компилируешь Chromium сам, правишь C++, пересобираешь его для трёх ОС, поддерживаешь кучу патчей и каждые две недели бьёшься с новой версией Blink.

Но когда ты это делаешь правильно, ты становишься невидимым. А это, согласись, стоит затраченных усилий.