Сразу скажу, этот пост копия моего поста в моем блоге (там 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 Мой взгляд приковался к строке 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 . Искал ссылки, смотрел функции которые ее принимают/отдают в параметрах, и нашел фукнцию FPakPlatformFile::GetPakEncryptionKey Бинго! это то что мне нужно! Здесь даже есть ЛОГ, значит функцию можно будет легко найти по строчке! Failed to find requested encryption key %s Что же мы видим в иде по ссылке на эту строку? Да, я уже немного расписал, но все же, что мы видим? Первое что мне бросилось в глаза - это то что функцию заинлайнило. Но сравнив псевдокод с кодом на гитхабе мы можем отчетливо увидеть стейтмент else, в котором принтится заветная строка, значит что перед ним как раз функция которая нам нужна, перед ней подготавливается контекст, и она принимает ссылку на структуру ключа в которую и записывает результат. Пора вытаскивать ключик! x64dbg Запускаем отладчик, коннектимся, и взяв RVA адресс выполнения функции, ставим бряк. При бряке нам нужно будет значение из регистра RDX, там будет хранится адресс по которому запишется AES ключ. Сделаем пару вещей, что бы затриггерить получение ключа, потыкать по гуи/подключиться к серверу. Наш бряк затриггерился, хватаем адресс из RDX: 0x308AFB50 , и переходим в дамп (HEX вид под окном дизассемблера). Выполняем шаг с обходом - и вуаля, наши заветные 32 байта ключа лежат в памяти! AES Ключ -> 0x9B5B00D0C209B704B3BE2D8E6EDB68D4189739D89724D9A9F473E5E7C607A5DD Автоматизация Что бы не сидеть и не отлавливать каждый ключ руками, можно поставить хук, ведь вдруг там больше одного ключа? Вот код который я написал пока писал статью. Он ставит хук на функцию получения ключа, записывает ключ в строку, если строка(ключ) уникальная - сохраняет ее в массив. Так же запускается поток который следит за добавлением новых ключей, все выводится в консоль. 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; } } } 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 файлов! Результат Распаковка PAK файлов Что бы распаковать и достать данные, нам пригодится утилита FModel Открываем ее и выбираем папку с PAK файлами Далее нас встречает окно обозревателя файлов, как видим все PAK архивы помечены красным индикатором, так же внизу - в логе, нас ожидает ошибка, сообщающая что PAK архивы зашифрованы - мы не можем посмотреть их содержимое. Что бы их расшифровать, в левом верхнем углу нажимаем Directory -> AES и вводим в поле ключа, найденый нами AES ключ, в таком формате: Нажимаем OK и Вуаля! Все файлы помечены зелеными индикаторами, значит можно смотреть содержимое. Я решил достать файлы звуков, игра использует FMOD, а он хранит звуки в файлах .bank Выбираем все файлы, и жмем Export Raw Data. Результат окажется в папке "Output\Exports\Путь внутри Unreal Engine" в корневой директории FModel Мы почти закончили! Распаковка BANK файлов Для распаковки BANK файлов - нам потребуется FMOD Bank Tools Открываем программу, выбираем пути входных данных (путь до папки с bank файлами) или просто закинуть все bank файлы в папку bank в корне программы. Нажимаем Extract Результат хранится в папке wav, в корневой директории FMOD Bank Tools. Заключение изи нах
Очень годный материал с замахом на авторку, жаль симп не могу поставить Еще очень интересно было бы послушать как ты действовал если бы никаких строк в этой функции не оказалось
hyperhide, а ну действительно) а допустим если у тебя нету возможности сбилдить собственный проект для анализа?
kernel_32, поставил бы хук на memcpy, вывел бы return адрес, и посмотрел бы что там происходит, первое что в голову приходит