ARMANOS

Движок

Наш браузерный движок

ARMANOS Browser это сборка Chromium, которую мы компилируем сами. Машина профиля ставится внутри движка, в C++, до того как страница выполнит первую строку скрипта. Здесь показано, где ставится каждое значение, чего движок пока не закрывает и как сборку принимают перед отгрузкой.
ARMANOS Browser 153.0.7978.0
Движок
26 файлов Chromium, применяются по порядку
Набор правок
9, каждое доказано признаком в правленом файле
Объявленных умений
macOS Apple Silicon и Intel, Windows, Linux
Сборки
25, прямо в программе
Замеров у профиля

Движок это наша сборка Chromium, а не настройка поверх чужой

ARMANOS Browser 153.0.7978.0 собирается из исходников Chromium с нашими правками, наложенными при сборке. Это не дополнение, не расширение и не ключ запуска. Правки меняют тот самый код на C++, который отвечает странице, поэтому ответ выходит из браузера уже нужным.

Версия живёт в одном месте: ARMANOS_BROWSER_VERSION в packages/shared/src/index.js. Её читает программа, её читает эта страница, и профиль называет ровно ту версию, которая его рисует. Назови он другую, сайт проверил бы это одной строкой, сверив подпись браузера с возможностями, которых у той вехи нет.

Сборок четыре, по одной на систему, и в каждом установщике движок лежит внутри. Упаковка отказывается идти, если ячейка нужной архитектуры пуста. Установщик без движка тихо отдал бы покупателю запасной движок и его настоящую машину, и на экране об этом не было бы ни слова.

Сборка под Windows правит файл BRANDING в дереве до настройки сборки, поэтому chrome.exe и chrome.dll несут имя продукта в собственных сведениях о версии. Сборка под macOS пока брендируется после компиляции, и это более слабый порядок: следующая сборка под macOS должна делать так же, как виндовая. Обе сегодня отвечают ARMANOS Browser 153.0.7978.0, и проверка читает это прямо из того двоичного файла, что лежит в ячейке.

СистемаКуда кладётся сборкаЧто программа находит при запуске
macOS, Apple Siliconapps/desktop/engines/mac-arm64/ARMANOS Browser.appContents/MacOS/ARMANOS Browser внутри пакета
macOS, Intelapps/desktop/engines/mac-x64/ARMANOS Browser.appтот же путь внутри сборки для Intel
Windows 10 и 11, 64 битаapps/desktop/engines/win/ вместе с сопутствующими файламиresources/engines/armanos-browser/armanos-browser.exe либо chrome.exe
Ubuntu 22.04 и новее, 64 битаapps/desktop/engines/linux/ вместе с сопутствующими файламиresources/engines/armanos-browser/chrome

Значения ставятся в C++, до первой строки скрипта страницы

Страница здесь не может быть первой. Когда её скрипт спрашивает navigator.platform, отвечающий геттер уже заменён внутри движка, и более раннего мгновения просто нет. В этом и вся разница между движком и скриптом.

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

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

У каждого профиля свой --user-data-dir, поэтому куки, хранилище, история и собственные данные сайта лежат в каталоге, которого не видит ни один другой профиль.

  1. 1

    Программа собирает семя

    buildFingerprint(шаблон, номер профиля, страна выхода) в packages/shared/src/index.js возвращает один объект: машина, экран, строки видеокарты, шрифты, часовой пояс, языки, число приборов, числа сети и два семени шума.

  2. 2

    Семя уходит в командную строку один раз

    forkEngine.buildArgs пишет --armanos-fingerprint=<base64 JSON>, а рядом --user-data-dir этого профиля, прокси, список языков и подпись браузера.

  3. 3

    Главный процесс разбирает его однажды

    base::Base64Decode и base::JSONReader в third_party/blink/renderer/core/armanos/armanos_fp.cc, результат живёт одним объектом на весь процесс.

  4. 4

    Каждый отрисовщик получает то же семя

    Ключ внесён в список передаваемых в content/browser/renderer_host/render_process_host_impl.cc, поэтому отрисовщик, поднятый через десять минут, несёт то же самое.

  5. 5

    Геттеры Blink отвечают из семени

    navigator_id.cc, screen.cc, font_cache.cc, timezone_controller.cc, network_information.cc и прочие возвращают засеянное там, где раньше возвращали хозяйское.

  6. 6

    Страница выполняется потом

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

Поверхность за поверхностью: что читает сайт

Набор правок трогает 26 файлов дерева Chromium плюс архив новых файлов с нашим собственным кодом. Ниже то, что отвечает каждая поверхность сегодня. Здесь нет разрядов: в каждой строке назван файл, где лежит правка.

Через всё это проходят два правила. Первое: шум постоянен у профиля и никогда не свежий на каждый вызов, потому что значение, изменившееся между двумя чтениями за один сеанс, само по себе примета. Второе: числа выводятся по правилам самого браузера, а не по правдоподобию. Предел кучи кладётся в одну из ста ступеней; задержка огрубляется до кратного 50 мс, а скорость режется потолком 10 Мбит/с, ровно как в network_state_notifier.cc; вид связи выводится из этих чисел по порогам из network_quality_estimator_params.h, а не тянется жребием.

Приборов никогда не ноль. Движок умеет спрятать камеру, которая на машине есть, но отнять её не умеет: профиль, заявивший ноль камер, ответил бы «камеры нет» на enumerateDevices и тут же выдал бы её на getUserMedia, а два ответа подряд, противоречащих друг другу, заметнее любого числа.

ПоверхностьГде лежит правкаЧто получает страница
Чтение холстаarmanos_canvas_noise.cc, base_rendering_context_2d.cc, html_canvas_element.cc, canvas_async_blob_creator.ccпостоянная поправка на getImageData, toDataURL и toBlob, привязанная к координате, поэтому два чтения одного прямоугольника совпадают
Звукaudio_buffer.cc, realtime_analyser.ccпостоянная поправка на буфер и на спектр; повторные чтения её не накапливают
Строки WebGLwebgl_rendering_context_base.ccвидеокарта профиля, например Google Inc. (AMD) и строка ANGLE с картой, которая существует
Пиксели WebGLтот же файл, сразу после ReadPixelsHelperreadPixels получает ту же поправку, что и холст, с перевёрнутым порядком строк, поэтому две дороги чтения сходятся
WebGPUgpu_buffer.cc, gpu_command_encoder.cc, gpu_queue.ccшум ложится только на буферы, в которые снимали картинку; обычные вычисления не трогаются
Шрифтыarmanos_font_gate.cc, font_cache.ccотзываются только семейства профиля; остальное подменяется запасным, как будто его не установлено
Часовой поясtimezone_controller.ccпояс страны, в которой выходит прокси; без страны выхода ничего не навязывается
Опознание navigatornavigator_id.cc, navigator_base.cc, navigator_concurrent_hardware.cc, navigator_device_memory.ccсистема, подпись браузера, поставщик, число ядер, память, число касаний
Подсказки клиентаuser_agent_utils.cc, navigator_ua_data.ccзаголовки Sec-CH-UA и getHighEntropyValues() берутся из того же семени, что и подпись, поэтому заголовок и скрипт сходятся
Экранscreen.cc, local_dom_window.ccразмер, рабочая область, глубина цвета и плотность точек, согласованные между собой
Приборы записи и выводаmedia_devices.ccчисло микрофонов, выходов звука и камер у этого профиля
Голоса синтеза речиspeech_synthesis.ccсписок голосов урезается до языка профиля, поэтому бразильский профиль не отдаёт японские голоса
Сведения о сетиnetwork_information.ccскорость, задержка и вид связи, огрублённые так же, как их огрубляет Chromium
Предел памятиmemory_info.ccпредел кучи приводится к заявленной памяти, а когда они и так сходятся, значение не меняется вовсе

Скрипт так не умеет, и разрыв между ответами ищут первым делом

Проверялка отпечатка не просто читает значения. Она сличает заявленное с измеренным и показывает расстояние между ними. Это расстояние и создаёт подмена на уровне страницы.

Возьмите шрифты. Скрипт может переписать measureText и document.fonts.check, но не может изменить, какие очертания система рисует на самом деле, и не дотягивается ни до разбора CSS, ни до рабочего потока. Возьмите холст. Каждую дорогу чтения приходится закрывать отдельно, и одна пропущенная вернёт настоящую картинку, пока остальные возвращают изменённую: это доказательство подмены, а не сокрытие. Возьмите рабочий поток: его скрипт живёт по собственному адресу, который страница переписать не может, поэтому обёртка накроет только источники Blob и data.

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

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

Подмена в страницеARMANOS Browser
Откуда берётся значениеиз подменённого геттера прототипаиз самого геттера
Рабочий поток со своим адресомнастоящая машиназасеянная машина
Кадр, созданный и прочитанный в один тактнастоящая машиназасеянная машина
Разрешение шрифтапереписан замер, отрисовка нетхранилище шрифтов отдаёт только семейства профиля
Две дороги чтения одного рисункакаждую надо закрывать руками, пропущенная расходится с прочимиобе идут из одного места в движке

Движок сам говорит, что закрывает, и слой в странице отходит

Не всё пока в C++. Что движок не закрывает, программа закрывает из страницы, до первого чужого скрипта. Это честная мера, и она записана, но в ней ловушка: в день, когда новый движок доедет до человека, оба слоя легли бы друг на друга. Движок добавил бы шум чтению пикселей, страница добавила бы свой поверх, и тот же рисунок через обычный холст пришёл бы другим. Расхождение двух дорог чтения это ровно та примета, ради устранения которой всё и делалось, только заведённая нами самими.

Поэтому сборка кладёт рядом с движком опись умений, armanos-engine.json, а программа читает её при запуске и гасит ровно те подмены в странице, которые движок теперь делает сам. Спрашивать у двоичного файла версию бесполезно: форк называет ровно ту же версию, что обычный Chromium той же вехи.

Опись выводится из дерева, а не переписывается со списка. Каждое умение доказывается признаком, который обязан найтись в правленом файле, например armanos::ShouldHideSystemFont( в font_cache.cc. Раньше сборка объявляла все умения, какие перечислены в коде, независимо от того, легли ли заплаты: это заявление, а не измерение. Цена такой лжи вполне осязаема: три поля связи Android программа открывает только движку, объявившему net-mobile-fields, и на движке без подмены значений эти поля отдали бы хозяйское «wifi» и бесконечную предельную скорость.

  • webgl-readpixels

    шум чтения пикселей WebGL внутри движка

  • intl

    часовой пояс, язык, календарь и разделители

  • fonts

    список шрифтов по профилю, а не по машине

  • devices

    число приборов записи и вывода

  • voices

    голоса синтеза речи по заявленному языку

  • net

    скорость, задержка и вид связи

  • memory

    предел кучи сходится с заявленной памятью

  • webgpu

    шум снимка картинки через WebGPU

  • net-mobile-fields

    три поля связи Android: type, downlinkMax, ontypechange

Встроенный движок это честный запасной, и он слабее

В программе есть второй движок: настоящий Chromium через Electron, сейчас веха 130, с защитой отпечатка, внедрённой скриптом. Он работает на любой системе, и на него профиль откатывается, когда ARMANOS Browser не смог подняться. Поскольку теперь движок лежит внутри каждого установщика, свежая установка сразу работает на ARMANOS Browser.

Откат никогда не бывает молчаливым. Если форк не поднялся, профиль всё равно откроется, но уведомление назовёт профиль и причину. Для такого продукта тихая смена движка, несущего личность, это единственный откат, о котором обязательно надо сказать: на экране движка по-прежнему стояло бы ARMANOS Browser, а профиль работал бы на подмене в странице и на другой версии Chromium.

Одну разницу надо назвать прямо, потому что она выглядит работающей и не работает. Блокирующее расширение из каталога на встроенном движке загружается, числится включённым и не блокирует ничего: Electron не исполняет правила declarativeNetRequest. Мы измерили это одним расширением на обоих движках, правилом против страницы, которая сама запрашивает /blockme. На встроенном движке сервер запрос увидел. На ARMANOS Browser не увидел. Пока это так, в разделе расширений стоит предупреждение, а проверка потребует его убрать, если Electron однажды это починит.

Наша собственная блокировка трекеров и чистка ссылок на встроенном движке работают, потому что правила исполняет сама программа, а не браузер. RPA Plus и Синхронизатор работают на обоих движках.

Встроенный движок, Chromium 130ARMANOS Browser, Chromium 153
Где ставится отпечатокв странице, внедрённым скриптомв C++, до первого скрипта
Рабочий поток совпадает с главнымДа
Кадр совпадает с родителемДа
Блокирующее расширение из каталога действительно блокируетДа
Наша блокировка трекеров и чистка ссылокДаДа
RPA Plus и СинхронизаторДаДа
Есть на любой системеДаДа
Экран выбора браузерного движка: два движка, ARMANOS Browser отмечен как установленный и работающий, ниже путь к найденному файлу
Экран движка называет оба движка и их версии, показывает найденный двоичный файл и тут же объясняет правило отката.

Что едет вместе с движком

Форк это отдельный процесс Chromium без сеанса Electron и без предзагрузки, поэтому защиты, написанные под Electron, сами по себе туда не переезжают. Их доставляет небольшое расширение, которое собирается для каждого профиля при запуске и грузится ключом --load-extension вместе с --disable-extensions-except, чтобы рядом не загрузилось ничего постороннего.

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

  • Блокировка трекеров

    Три набора правил: список узлов, собственная слежка известных площадок и метки в ссылках. Служебный поток расширения сообщает о срабатываниях, и из этого складывается экран защиты.

  • Чистка ссылок

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

  • WebRTC через прокси

    --force-webrtc-ip-handling-policy=disable_non_proxied_udp, поэтому местные кандидаты не предлагаются в обход прокси.

  • Весь трафик на прокси

    --proxy-server покрывает все схемы, а не только https. Неизвестный вид прокси приводит к отказу в запуске, а не к тихому выходу напрямую.

  • Пароль прокси мимо командной строки

    Chromium отвергает пароль внутри --proxy-server, да и в списке процессов он был бы виден всем. Пароль идёт через мост на петле, а браузер предъявляет мосту пропуск из расширения.

  • Автоматизация без приметы

    Порт отладки слушает петлю, а номер выбирает система. Его открытие ставит navigator.webdriver, поэтому признак автоматизации гасится ключом запуска; через этот же порт работают RPA Plus и Синхронизатор.

  • Свой ключ шифрования

    --use-mock-keychain и --password-store=basic держат браузер вне хранилища паролей системы, а заодно оставляют профиль переносимым между машинами.

Сборка не уезжает, пока её не приняли стенды

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

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

Исходов три, а не два. «Принято» значит, что прошли все стенды. «Принято с оговоркой» значит, что не провален ни один, но проверено не всё: обычно на машине нет настоящего Chrome той же вехи. «Не принято» значит, что провален хотя бы один стенд, и такая сборка не выкладывается.

Сам набор правок стережёт отдельная проверка, и появилась она из настоящей беды: десять пронумерованных файлов в папке заплат выглядели готовой серией и все были заглушками. Сборщик пропускал каждую с предупреждением и выдавал обычный Chromium, пока настоящая правка лежала рядом и в список не входила. Теперь проверка требует, чтобы в списке не было заглушек, чтобы каждая названная правка действительно меняла исходники, чтобы у шума WebGL был вызов после чтения пикселей с передачей высоты буфера и чтобы каждое объявленное умение подтверждалось своим признаком.

  1. 1

    Засеянные значения доезжают

    accept-engine.js запускает движок со страной выхода и читает то, что видит страница, а затем сличает с тем же движком без семени.

  2. 2

    Инварианты держатся на нескольких семенах

    engine-invariants.js гоняет по умолчанию четыре семени. Дефект холста зависит от семени, и одно удачное пропустило бы негодную сборку.

  3. 3

    Форма нашего движка снимается в файл

    compare-shape.js записывает всю поверхность движка в файл, а не печатает строку, которую никто не сличает.

  4. 4

    Форма настоящего Chrome снимается тоже

    тот же сценарий идёт по обычному Chrome, без семени, во второй файл.

  5. 5

    Две формы сличаются

    сравнить-форму.js их сравнивает. Код 3 значит «сличено частично», код 4 значит «нашли лишнее и объяснить нечем», и это отказ: такую разницу сайт находит одной строкой.

Как проверить самому

Каждый профиль открывается на стартовой странице, где перечислены проверялки, которыми эта работа и так пользуется: BrowserScan, BrowserLeaks, CreepJS, Pixelscan и определение адреса. Страницу отдаёт сервер на петле внутри программы, поэтому об открытии профиля не узнаёт никто посторонний. Единственный запрос наружу это определение адреса, и оно идёт через прокси самого профиля, так что вы видите тот адрес, который увидит сайт.

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

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

Ниже значения одного профиля, полученные той же функцией, которую отдают движку. Для своего профиля вы получите такой же вид на стартовой странице, ещё не открыв ни одного сайта.

navigator.platform
Win32
navigator.userAgent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
Ядра и память
12 логических ядер, 8 ГБ
Экран
1920 x 1080, рабочая высота 1040, плотность 1
Видеокарта, поставщик
Google Inc. (AMD)
Видеокарта, отрисовщик
ANGLE (AMD, AMD Radeon RX 6600 (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)
Часовой пояс
America/New_York
Языки
en-US, en
Шрифты
25 семейств, все виндовые
Приборы
1 микрофон, 3 выхода звука, 1 камера
Сеть
4g, 10 Мбит/с, задержка 200 мс
Голоса синтеза речи
только английские
maxTouchPoints
0

Один профиль на шаблоне windows-chrome, прокси выходит в США. Все значения выводятся из номера профиля, поэтому при следующем запуске и на другом компьютере они те же. Число приборов это потолок: движок умеет спрятать прибор, который на машине есть, но не умеет выдумать тот, которого нет.

Чего это не делает

  • Чтение пикселей WebGL закрыто для восьмибитного RGBA, той дороги, которой ходят проверялки. Три дороги остаются открытыми, и они честно названы открытыми в engine/patches/PENDING.md: RGB по три байта на пиксель, вещественные типы float и half-float, и чтение через буфер упаковки.
  • Настоящая опись шрифтов машины по-прежнему утекает через запрос local(). Замер урезан списком профиля, но поиск по уникальному имени отдаёт то, что на машине есть, и закрыть это можно только правкой в local_font_face_source.cc.
  • На macOS язык ICU следует за хозяйской машиной, поэтому Intl может форматировать числа и даты на языке хозяина, пока профиль заявляет другой. Обычный Chromium с теми же ключами ведёт себя так же. Пока движок не ставит язык ICU из семени, пояс и язык закрепляет слой в странице.
  • Профилю, чей шаблон не совпадает с системой хозяина, дописываются хозяйские базовые шрифты, иначе текст не рисуется вовсе. Эти имена странице видны, и починить это можно только в движке, разделив список на отрисовку и на ответы.
  • Признак автоматизации гасится открытым ключом Chromium, а не правкой. Ключ работает и сторожится проверкой, которая поднимает настоящий движок и читает значение с настоящей страницы, но сам ключ виден в командной строке процесса любому, кто на этой машине умеет её прочесть.
  • Мы продаём разделение и постоянную машину, а не невидимость. Сайт по-прежнему видит ваш адрес, ваше поведение и историю того аккаунта, в который вы входите, и движок об этом ничего не решает.

Как проверить

За каждым пунктом файл, который можно открыть, и стенд, который валит сборку, когда обещание перестаёт быть правдой.

Отпечаток подменяется в исходниках Chromium, правки применяются при сборке
engine/patches/series · apps/desktop/test/engine-patches-real.js
Шум чтения пикселей WebGL действительно вызывается, а не лежит в заплате мёртвым кодом
engine/patches/armanos-fp.cumulative.diff · apps/desktop/test/engine-patches-real.js
Каждое объявленное умение доказано признаком в правленом файле, а не списком
packages/shared/src/index.js · engine/scripts/опись-умений.js
Слой в странице отходит ровно там, где движок берёт работу на себя
apps/desktop/src/lib/дыры-страницы.js · apps/desktop/test/страница-отходит-когда-движок-умеет.js
Сборку принимают стенды, и неполная проверка не выдаётся за полную
engine/scripts/приёмка.sh · apps/desktop/test/сборочные-скрипты.js
Установщик не уезжает без движка и с движком чужой версии
apps/desktop/build/before-pack.js · apps/desktop/test/установщик-без-движка.js

Запустите на своей машине

Поставьте ARMANOS, откройте профиль и прочитайте стартовую страницу до того, как открывать сайты. Движок едет внутри программы.