Загрузка...

Разбор расшифровки PAK файлов Unreal Engine, получение AES ключей.

Тема в разделе Реверсинг / Assembler создана пользователем hyperhide 19 июн 2025. 225 просмотров

  1. hyperhide
    hyperhide Автор темы 19 июн 2025 22 17 июн 2024
    Сразу скажу, этот пост копия моего поста в моем блоге (там 0 человек примерно)
    так что я не считаю что брать оттуда свою же инфу - это что то нехорошее.

    Как я доставал AES ключи шифрования для PAK файлов из игры Will To Live Online.
    Я поставил себе задачу сделать инструмент для полноценной загрузки на карту без подключения к серверам игры, для дальнейшего тестирования и анализа механик Will To Live Online .

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

    Мной было принято решение, вытащить все данные из зашифрованных .PAK файлов, но для этого нужен был AES ключ.​
    Достаточно использовать примитивные инструменты основанные на сканировании паттернов - Подумал я.

    Но я был в корне не прав, ни один инструмент из публичного доступа не нашел нужный мне AES ключ. Дааа... придётся искать все самому.​
    1. Дизассемблер - IDA Pro Для статического анализа
    2. Отладчик - x64dbg Для динамического анализа
    3. PAK Обозреватель - FModel Для просмотра PAK файлов
    4. Распаковщик BANK файлов - FMOD Bank Tools Для извлечения аудио файлов
    5. Хуки - MinHook Супер удобный и быстро импортируется.
    Что первое может подумать человек, которому предстоит найти как расшифровывается загружаемый файл?
    Лично я - подумал о том, что бы найти место где этот файл загружается, и смотреть по ситуации, ведь логично что в первую очередь исполняемая программа будет загружать, проверять, а потом уже расшифровывать файл!

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

    В IDA я решил поискать все что угодно связанное с PAK файлами. Например:
    ⁡pak file


    [IMG]


    Мой взгляд приковался к строке
    ⁡Pak file hash does not match its index entry

    Да уж, если бы я был немного внимательнее я бы увидел строку
    ⁡Registered encryption key '%s': %d pak files mounted, %d remain pending


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

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


    Действительно! Можно же просто начать искать все что связано с расшифровкой, а к тому же поискать референсы на GitHub!
    Я полез в репозиторий движка и искал как расшифровываются PAK файлы, запросы вроде Encrypt/Decrypt и нашел структуру которая хранит AES ключ - FAES::FAESKey .

    [IMG]

    Искал ссылки, смотрел функции которые ее принимают/отдают в параметрах, и нашел фукнцию FPakPlatformFile::GetPakEncryptionKey

    [IMG]


    Бинго! это то что мне нужно! Здесь даже есть ЛОГ, значит функцию можно будет легко найти по строчке!
    ⁡Failed to find requested encryption key %s

    Что же мы видим в иде по ссылке на эту строку?

    [IMG]


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

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

    Пора вытаскивать ключик!
    Запускаем отладчик, коннектимся, и взяв RVA адресс выполнения функции, ставим бряк.

    [IMG]


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

    [IMG]


    Наш бряк затриггерился, хватаем адресс из RDX:
    ⁡0x308AFB50
    ⁡, и переходим в дамп (HEX вид под окном дизассемблера).
    Выполняем шаг с обходом - и вуаля, наши заветные 32 байта ключа лежат в памяти!

    [IMG]

    AES Ключ ->
    ⁡0x9B5B00D0C209B704B3BE2D8E6EDB68D4189739D89724D9A9F473E5E7C607A5DD
    ⁡​
    Что бы не сидеть и не отлавливать каждый ключ руками, можно поставить хук, ведь вдруг там больше одного ключа?


    Вот код который я написал пока писал статью.
    Он ставит хук на функцию получения ключа, записывает ключ в строку, если строка(ключ) уникальная - сохраняет ее в массив.
    Так же запускается поток который следит за добавлением новых ключей, все выводится в консоль.


    C
    struct aes_key_data
    {
    unsigned char key[64];
    char buffer[256]; // буффер для защиты от переполнения
    };

    std::vector<std::string> aes_keys;
    std::mutex aes_keys_mutex;

    void save_key(const aes_key_data* data) {
    std::ostringstream oss;
    for (size_t i = 0; i < sizeof(data->key) / 2; ++i) {
    oss << std::uppercase << std::setw(2) << std::setfill('0') << std::hex
    << static_cast<int>(data->key[i]);
    }
    std::lock_guard<std::mutex> lock(aes_keys_mutex); // другие потоки тоже могут вызывать функцию получение ключа, так что нужен мютекс.
    if (std::find(aes_keys.begin(), aes_keys.end(), oss.str()) == aes_keys.end()) { // игнорим дубликаты
    aes_keys.push_back(oss.str());
    }
    }

    __int64 (*o_get_pack_encryption_execute)(__int64 _this, aes_key_data* data);
    __int64 get_pack_encryption_execute_hk(__int64 _this, aes_key_data* data) {
    __int64 result = o_get_pack_encryption_execute(_this, data);
    save_key(data);
    return result;
    }

    void set_aes_hook() {
    __int64 v13 = 0;
    auto v20 = (__int64)reinterpret_cast<__int64(*)()>((__int64)GetModuleHandleA(0) + 0x1AF2470)();//Взяли из фрагмента перед вызовом функции на которой мы брякались.
    if (*(DWORD*)(v20 + 8))
    v13 = *(__int64*)v20;

    if (!v13) {
    printf("Failed to install AES Hook, Invalid content\n");
    return;
    }

    printf("PakEncryptionKeyDelegate: 0x%p\n", reinterpret_cast<void*>(v13));

    void* function_ptr = reinterpret_cast<void*>(*(__int64*)(*(__int64*)v13 + 0x48));
    printf("Execute function: 0x%p\n", reinterpret_cast<void*>(function_ptr));

    if (!function_ptr) {
    printf("Failed to install AES Hook, Invalid function\n");
    return;
    }

    MH_CreateHook(function_ptr, &get_pack_encryption_execute_hk, (void**)&o_get_pack_encryption_execute);
    MH_EnableHook(function_ptr);

    printf("AES Hook successfully installed\n");
    }

    void watchdog_thread() {
    while (true) {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    static int old_count = 0;

    std::lock_guard<std::mutex> lock(aes_keys_mutex);
    while (old_count < aes_keys.size()) {
    printf("New AES key: %s\n", aes_keys.at(old_count).c_str());
    ++old_count;
    }
    }
    }

    Что бы вытащить ВСЕ ключи с помощью этого хука, можно использовать Extreme Injector , настроив автоинжект.
    Это гарантирует что мы поставим хук до расшифровки .PAK файлов!

    Результат

    [IMG]
    Что бы распаковать и достать данные, нам пригодится утилита FModel

    Открываем ее и выбираем папку с PAK файлами

    [IMG]

    Далее нас встречает окно обозревателя файлов, как видим все PAK архивы помечены красным индикатором, так же внизу - в логе, нас ожидает ошибка, сообщающая что PAK архивы зашифрованы - мы не можем посмотреть их содержимое.

    [IMG]

    Что бы их расшифровать, в левом верхнем углу нажимаем Directory -> AES и вводим в поле ключа, найденый нами AES ключ, в таком формате:

    [IMG]


    Нажимаем OK и Вуаля!
    Все файлы помечены зелеными индикаторами, значит можно смотреть содержимое.

    [IMG]


    Я решил достать файлы звуков, игра использует FMOD, а он хранит звуки в файлах .bank

    [IMG]


    Выбираем все файлы, и жмем Export Raw Data.
    Результат окажется в папке
    ⁡"Output\Exports\Путь внутри Unreal Engine"
    ⁡ в корневой директории FModel

    [IMG]

    Мы почти закончили!
    Для распаковки BANK файлов - нам потребуется FMOD Bank Tools

    Открываем программу, выбираем пути входных данных (путь до папки с bank файлами) или просто закинуть все bank файлы в папку bank в корне программы.
    Нажимаем Extract

    [IMG]

    Результат хранится в папке wav, в корневой директории FMOD Bank Tools.

    [IMG]
    изи нах​
     
  2. kernel_32
    kernel_32 19 июн 2025 15 17 июл 2019
    Очень годный материал с замахом на авторку, жаль симп не могу поставить :thumbsup:
    Еще очень интересно было бы послушать как ты действовал если бы никаких строк в этой функции не оказалось
     
    1. kernel_32
      hyperhide, а ну действительно) а допустим если у тебя нету возможности сбилдить собственный проект для анализа?
    2. hyperhide Автор темы
      kernel_32, поставил бы хук на memcpy, вывел бы return адрес, и посмотрел бы что там происходит, первое что в голову приходит
Загрузка...
Top