Ultimatum — еще один форк хромиума, с претензией…

в 14:04, , рубрики: opensource, WebExtensions, браузеры

image

Добрый день! Меня зовут Тимур и я программист.

Сегодня я хочу сделать настоящий анонс своей сборки chromium — Ultimatum. Он умеет уже достаточно много что бы гордо носить свое собственное имя.

В прошлой своей статье я рассказал о том как пробросил в js прямой доступ до http кеша и объяснил для чего я это делаю. Статья завершилась со словами что я еще вернусь со своим антидетект браузером. Я вернулся и это немного больше чем антидетект браузер.

Если коротко — Ultimatum уже помножил на ноль такие техники трекинга как hsts-pinning, favicons cache и вообще использование многих других кешей в трекинге. А также! Теперь можно поставить расширение с любого сайта, не только со сторов гугля, оперы или микрософта (с них кстати тоже можно — со всех!). А еще! Можно перехватывать сетевые запросы и подменять их полностью! Ну и так далее и тому подобное.

А теперь более подробно и более спокойно.

Вот тут лежат все коммиты

Скачать бинарники можно у меня в бложике, пока только osx и винда, https://gonzazoid.com

Все api — строго для webextensions, требуют нестандартных permissions и соотв. расширения написанные с использованием этих api не могут быть выложены в сторы (chrome, opera, edge) так как с точки зрения сторов у них невалидный манифест.

Для начала повторюсь про diskCache.

diskCache

Для доступа к api в манифесте расширения нужно указать permission diskCache. Расширение с таким пермишеном после установки получает доступ к api:

await chrome.diskCache.keys(cache_name); // возвращает массив ключей в кеше
await chrome.diskCache.getEntry(cache_name, key); // возвращает указанное вхождение в кеш
await chrome.diskCache.putEntry(cache_name, entry); // пишет в указанный кеш, key указывается в entry
await chrome.diskCache.deleteEntry(cache_name, key); // удаляет указанное вхождение

При этом вхождение в кеш имеет следующий формат:

{
  key: "string",
  stream0: ArrayBuffer,
  stream1: ArrayBuffer,
  stream2: ArrayBuffer,
  ranges: Array
}
// где ranges состоит из объектов:
{
  offset: number,
  length: number,
}

Свойство ranges необязательное и указывается только для sparse entities. stream0, 1, 2 обязательны для всех но для sparse entities используется только stream0 и stream1, при этом stream1 содержит все чанки идущие друг за другом (без пустот) а ranges указывают где они (чанки) должны были располагаться. То есть длина stream1 должна совпадать с суммой всех length указанных в ranges. (Это все отображение деталей реализации disk_cache в хромиум, это не моя самодеятельность)

Про устройство disk_cache я подробнее говорил в прошлой статье, если что-то непонятно — загляните туда.

cache_name может принимать значения http, js_code, wasm_code и webui_js_code. Я работал пока только с http, если будете экспериментировать с другими кешами — не стеснятесь, делитесь результатами.

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

Какие техники трекинга мы удаляем с этого плана бытия с этим api? Из списка техник evercookie это:

  • Storing cookies in HTTP ETags (Backend server required)
  • Storing cookies in Web cache (Backend server required)

А в общем — любая техника которая основывается на проверке скачан ли уже ресурс или еще нет (за исключением favicon — там свой кеш) — на этих возможностях подскользнется и забуксует.

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

sqliteCache

Позволяет получить доступ к favicon cache и history cache (они реализованы на базе sqlite). History уже достаточно давно в трекинге не участвует, но я решил что пусть будет. Для того что бы получить доступ к api у расширения в манифесте должен быть указан permission sqlCache.

Интерфейс у api следующий:

await chrome.sqlCache.exec(storage, sql_request, bindings);

где:

  • storage — строка, указывает к какой базе идет запрос. Может принимать значения faviconCache или historyCache. Если знаете какие либо sqlite базы в недрах chromium-а в которые хотелось бы заглянуть — говорите, обсудим.
  • sql_request — строка, собственно сам sqlite запрос.
  • bindings Тут интересно. В самом запросе конретные значения не указываются, вместо этого указывается символ подмены ?. А в bindings мы указываем что там на самом деле должно быть подставлено. То есть bindings это массив элементов каждый из которых может быть (js->c++):
    • string (литерал, не объект) — трансформируется в sql::ColumnType::kText
    • number — транформируется в sql::ColumnType::kFloat (в js числа являются float а не integer, мы же помним?)
    • объект с полями {
      type: "int",
      value: "string, decimal"
      } транформируется в sql::ColumnType::kInteger. такие сложности с integer связаны с тем что sqlite поддерживает int до 64 разрядов и во первых float (в js) не поддерживает такую точность, а во вторых если мы начнем js-овский float (который number) использовать для kInteger то нам еще придется отличать его от использования для kFloat. Можно было бы приспособить для этого js-овский BigInt но на самом деле это ничего не облегчает поэтому оставил так.
    • ArrayBuffer — транформируется в sql::ColumnType::kBlob
    • null — транформируется в sql::ColumnType::kNull

Это покрывает все типы sqlite, подробности можно посмотреть на их сайте, документация у них вполне себе приличная.

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

[
  [ /* первая строка результата */ "строка", 3.14, { type: "int", value: "73" } ],
  [ /* вторая строка результата */ "еще одна строка", 2.718, { type: "int", value: "43" } ],
  ...
]

Зачем понадобилось делать отдельное api для favicons если есть http cache? Дело в том что chrome/chromium работают с favicon "странно". Для них есть отдельный кеш, не http (в сети очень много статей в которых упоминается о том что этот кеш не сбрасывается но это уже не так, при удалении browsing data он удаляется, не могу точно сказать с какой версии chromium, в 129-ой точно). Этот кеш достаточно активно используется что бы трекать пользователей, например в довольно известных supercookie. На хабре был перевод, сам код supercookie можно тут посмотреть а описание того как работает техника — нескромно, но в моей предыдущей статье есть наиболее общее обьяснение, все что мне попадалось на просторах интернета — гораздо более частные случаи, если это не так — пишите, добавлю в статью ссылок.

О том как устроены favicon cache и history cache я расскажу отдельной статьей, здесь пока просто обзор api.

hstsCache

На данный момент hsts pinning — самая непробиваемая техника трекинга (из известных мне), так что необходимость помножить ее на ноль была очевидна. Хромиум предоставляет довольно убогий интерфейс для работы с hsts, доступный по адресу chrome://net-internals/#hsts и причины этой убогости стали ясны когда я распотрошил код, об этом ниже расписано.

Сама техника трекинга описана много где, есть paper по теме HSTS Supports Targeted Surveillance (если погуглить можно найти pdf в свободном доступе), на русском языке на хабре есть статья где упоминается эта техника, в общем при желании разобраться не долго.

Так вот, проблема в том что хромиум не предоставляет никаких инструментов что бы посмотреть какие домены что там записали в hsts cache. То есть посмотреть можно только зная домен, а вот список доменов вы не получите никак. Дело в том что хромиум не хранит сами домены, ключом к записи правил является хэш от домена. Я еще пока только думаю над тем стоит ли это исправлять, а пока что просто пробросил стандартный интерфейс для доступа. Api у нас выглядит так (доступно для расширений с permission hstsCache):

await chrome.hstsCache.keys(); // возвращает все доступные ключи в hsts cache. Каждый ключ является ArrayBuffer-ом
await chrome.hstsCache.getEntry(key); // возвращает вхождение в hstsCache с указанным ключом
await chrome.hstsCache.putEntry(entry); // записывает вхождение в кеш
await chrome.hstsCache.deleteEntry(key); // удаляет вхождение в кеш с указанным ключом

При этом вхождение имеет вид:

{
  key, // ArrayBuffer(32),
  upgradeMode, // number,
  includeSubdomains, // boolean,
  expiry, // number-timestamp like Date.now()
  lastObserved, // number-timestamp like Date.now()
}

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

localStorages

Расширение с таким пермишеном получает доступ ко всем записям в localStorage независимо от origin и прочего. То есть мы можем читать/писать/удалять любую запись любого localStorage. Api выглядит так:

await chrome.localStorages.keys(); // возвращает массив ключей, каждый ключ - arrayBuffer
await chrome.localStorages.getEntry(key); // возвращает запись соответствующую ключу, результат - arrayBuffer
await chrome.localStorages.putEntry(key, entry); // если запись существует - меняем ее, если нет - создаем
await chrome.localStorages.deleteEntry(key); // удаляем запись
await  chrome.localStorages.flush(); // объяснение ниже
await  chrome.localStorages.purgeMemory(); // объяснение ниже

Ключ является буфером, если мы переведем его в строку то получим значения такого плана:

[
  "META:chrome://settings",
  "META:devtools://devtools",
  "META:https://habr.com",
  "METAACCESS:chrome://settings",
  "METAACCESS:devtools://devtools",
  "METAACCESS:https://habr.com",
  "VERSION",
  "_chrome://settingsu0000u0001privacy-guide-promo-count",
  "_devtools://devtoolsu0000u0001console-history",
  "_devtools://devtoolsu0000u0001experiments",
  "_devtools://devtoolsu0000u0001localInspectorVersion",
  "_devtools://devtoolsu0000u0001previously-viewed-files",
  "_https://habr.comu0000u0001rom-session-start",
  "_https://www.google.com/^0https://stackoverflow.comu0000u0001rc::h",
  "_https://www.youtube.com/^0https://habr.comu0000u0001ytidb::LAST_RESULT_ENTRY_KEY"
]

Нас интересуют ключи с префиксом _http — именно они имеют отношение к web-у, но как видим у нас есть тут доступ и к другим интересным вещам. Я это еще особо не исследовал, если кто поковыряет и найдет что-то интересное — дайте знать.

Первые 4 функции достаточно понятны, тут особо ничего нового нет, давайте посмотрим на flush и purgeMemory. Для начала — вот кусочек из соотв. mojom файла:

components/services/storage/public/mojom/local_storage_control.mojom

  // Tells the service to immediately commit any pending operations to disk.
  Flush();

  // Purges any in-memory caches to free up as much memory as possible. The
  // next access to the StorageArea will reload data from the backing database.
  PurgeMemory();

Так вот, как это работает? Есть некая общая база которая где то там лежит, неважно где и неважно как. В процессе серфинга при отображении табов и фреймов с этой базы идет выборка и вытягиваются все ключи для соотв. origins (там чуть сложнее на самом деле, берется origin текущего фрейма и то ли main frame то ли parent frame, по памяти точно не скажу, надо смотреть код) Далее все фреймы которым нужны эти записи работают с их копиями в памяти. И это нормально с точки зрения производительности. Но! Когда мы хотим прочитать записи из базы — мы не знаем насколько они актуальные. Поэтому мы делаем flush() и заставляем все изменения закоммитить в базу. После этого мы можем ЧИТАТЬ и быть уверенными что работаем с актуальными данными. При этом все кешированные данные так же остаются в своих кешах и никакой просадки по производительности табы и фреймы не получают.

Далее. Мы прочитали данные, приняли какие то решения и решили что-то изменить. Пишем эти изменения в базу. Но при этом мы помним что уже открытые табы/фреймы сидят со своими кешами и этих изменений они не увидят. Вот для этого мы делаем purgeMemory(). Кеши сбрасываются и при следующем запросе к localStorage домена произойдет выборка записей с базы — да, вместе с нашими изменениями если эти изменения касались этого домена. То есть purgeMemory() мы делаем после ЗАПИСИ в базу и тут какая то просадка по производительности неизбежна.

urlRequest

Тут прям интересно. Помните как в 2019 гугль заявил что api webRequest нехорошее и поддержка его (в части блокирующих или точнее блокируемых запросов) будет прекращаться? А потом не стал переводить это api на третий манифест. И народ бурлил, и отвалились ad блокеры. И гугль выкатил свои decalrative network requests и народ бурлил еще больше. А потом опера (и вроде как микрософт, но это не точно) заявили что будут поддерживать webrequests до последнего но они так и остались во втором манифесте. Помните, да? Я вот тоже помню. И честно говоря до сих пор не очень хорошо понимаю всех этих бурлений. В том смысле что ну хочет себе гугль стрелять в ногу — так это его нога, имеет право. Нас то он в свои ноги стрелять не заставляет. В итоге я просто скопировал код webRequest api, переименовал его в urlRequest, поднял до третьего манифеста и убрал весь код (на пока) связанный с событиями, оставив только onBeforeRequest. Но вот его я немного подрихтовал что бы он выглядел посимпатичнее, а именно:

  • перехватываются все запросы, никаких защищенных доменов нет (в хромиуме есть домены гугля, запросы к которым не перехватывались)
  • помимо cancel и redirect можно вернуть объект Response либо промис который разрезолвится в Response и в этом случае запроса в сеть не будет вообще — инициатору запроса отдадутся данные Response.
  • все запросы которые попадают под requestFilter всегда блокируются, если выясняется что запрос нам не интересен можно вернуть пустой ответ либо cancel: false, в этом случае запрос пойдет в сеть без какого либо вмешательства.

Как это работает в коде? У расширения должен быть пермишен urlRequest, вот так выглядит навешивание листенера:

chrome.urlRequest.onBeforeRequest.addListener((evt) => {
    console.log(evt);
    if (evt.url !== "https://pikabu.ru/") return { cancel: false };

    return {
      response: new Promise((resolve) => {
        resolve(
          new Response(
            "<html>haha</html>",
           { statusText: "OK", headers: { "Content-Type": "text/html; charset=utf-8"}}
          )
        );
    })
  };
}, {urls: ["https://pikabu.ru/*"]})

То есть теперь можно писать расширения которые по сути могут в том числе выполнять функции web-сервера. Там есть пока еще ограничения по хедерам (они вшиты в код Response) — это я менять не буду но возможность создавать любой ответ без ограничений — добавлю.

Далее. Я намерен вернуть все остальные события из api webRequest, убирая по пути ограничение там где это разумно, пробросить tcp и udp модули в js — и это в связке с перехватом запросов дает возможность регистрировать и реализовывать поддержку любых протоколов, на уровне js расширений. С учетом того что доступ к http кешу у нас уже есть — значительная часть обслуживающая сетевые запросы может переехать в js — это снизит объем c++ кода в проекте и даст бОльшие возможности для реализации новых протоколов, мне кажется это многим командам может быть интересно.

Итого

Собственно это пока все по api, еще одно напоминание — Ultimatum помимо описанных выше возможностей умеет схемы hash://, signed:// и related://, что (на мой взгляд) является базисом для web3.0, интересующиеся темой могут посмотреть мои предыдущие статьи, там я это описывал немного подробнее, повторяться не буду, а по web3.0 будет отдельная статья, там будет интересно.

Напоследок напомню — расширения, реализующие новые возможности могут быть включены в сборку (там есть такая тема как internal extension, как например реализован pdf reader) либо установлены по дефолту — это может быть интересно electron based проектам.

Так же напомню — эта сборка умеет ставить расширения с любого источника, достаточно отдать crx с правильным заголовком ("Content-Type": "application/x-chrome-extension"). Никакого урона безопасности не наносится, он все так же предупреждает пользователя что происходит и пользователь имеет возможность подтвердить действие или отказаться. Но при этом разработчики расширений получают возможность раздавать их со своих сайтов не мучаясь с долгими ревью в сторах и не считаясь с ограничениями китов индустрии. Я пока еще не наладил механизм апдейта, но это все будет, и тут я очень рассчитываю на поддержку — сборка дает больше возможностей разработчикам, владельцы расширений предлагают такую возможность пользователям, Ultimatum получает рекламу. В общем если есть идеи/предложения — я на связи и готов обсуждать.

Код открытый, лицензия MIT, берите и пользуйтесь кому надо и как надо.

На этом на сегодня все, как использовать эти api и какие планы развития проекта — в следующих статьях.

А новостей на сегодня больше нет, с Вами был Тимур, хорошего настроения!

Автор: gonzazoid

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js