- REVERSE (реверс)
- FORENSIC (криминалистика)
- WEB (вэб)
- OSINT (Open Source Intelligence, разведка на основе открытых источников)
- MISC (остальные)
Задача:
Reverse 500pts / Bridge repair
![]()
A.U.R.O.R.A.: Lieutenant, watch your step! There is a pit infested with worms down the road. There is a bridge over the pit but it’s in ruins and you can restore it only in the same way it was destroyed. I’ve got one worm in the quarantine, go for him and repair the bridge. Hurry up, we have to save our pilot!
file
Первые ощущения от задания:
Что мы имеем в наличии:
- Reverse500.exe - это консольная утилита, шифрующая по некоторому алгоритму файл
- Bridge.txt - файл, зашифрованный этой утилитой.
На первый взгляд, это задание похоже на задание ZeroNights HackQuest Task 1: ZEROCRYPT. Посмотрим, как оно на самом деле.
Решение:
[Шаг 1 | Шаг 2 | Шаг 3 | Шаг 4]Шаг 1.
Наблюдаем за работой утилиты.cmd> Reverse500.exe you must specify the file for encryption. cmd> Reverse500.exe no-such-file The file specified could't be open. cmd> Reverse500.exe 0-byte-file.txt Done cmd> Reverse500.exe 100-byte-file.txt Done
Наблюдения:
- Сначала исходный файл считывается целиком в память
- Затем файл шифруется, и некий ключ шифрования в два этапа записывается на место исходного файла (сначала 7 байт, затем 8 байт)
- После чего в файл записываются шифрованные данные
- Ключ шифрования и шифрованные данные всегда разные, даже если запускать утилиту много раз
Шаг 2.
Исследуем Reverse500.exe в IDA.Из callstack'ов Process Monitor'а и с помощью поиска по строке L"The file specified could't be open." мы находим функцию 0x00401F60, в которой происходит всё самое интересное. После некоторого времени, проведённого в отладчике и в IDA эта функция выглядит так:
BOOLEAN __usercall encrypt@<al>(LPCWSTR lpFileName@<ecx>, int a2@<eax>, int a3@<ebp>)
{
SEncoderDecoderVtbl *encoderVtbl; // eax@3
SEncoderDecoder encoder; // [sp-Ch] [bp-B4h]@1
char arKeysData[8]; // [sp+78h] [bp-30h]@3
...
SEncoderDecoder::Init(&encoder, v13, v14);
hFile = CreateFileW(v3, 0xC0000000, 0, 0, 3u, 0x80u, 0);
if ( hFile == (HANDLE)INVALID_HANDLE_VALUE )
{
v6 = GetStdHandle(STD_OUTPUT_HANDLE);
WriteConsoleW(v6, L"The file specified could't be open.", 0x24u, 0, 0);
bResult = FALSE;
}
else
{
nFileSize = GetFileSize(hFile, 0);
pBuff = (char *)malloc_(nFileSize);
// Чтение файла в память
ReadFile(hFile_, pBuff, nFileSize, &nFileSize, 0);
keyHolder.pVtbl_4346EC = (SKeyHolderVtbl *)&off_4346EC;
keyHolder.pKeysData = arKeysData;
keyHolder.pVtbl2_4346DC = (int)&off_4346DC;
encoderVtbl = encoder.pVtbl_434344;
// Генерация 8-байтного ключа, который сохраняется в arKeysData
encoderVtbl->gen_key(&encoder, &keyHolder, &dword_43C480, 8, 0);// SEncoderDecoder::gen_key
// Шифрование данных файла
pEncodedDataBuff = maybe_encode2(arKeysData, nFileSize, pBuff, &v34, arKeysData, v9);
free___(pBuff);
if ( pEncodedDataBuff ) {
// Запись ключ и зашифрованных данных в исходный файл
bResult = WriteKeysAndDataToTheFile(hFile_, pEncodedDataBuff, nFileSize, arKeysData, v10);
free___(pEncodedDataBuff);
CloseHandle(hFile_);
}
else {
bResult = FALSE;
}
}
...
return bResult;
}
А функция записи ключа и данных в файл выглядит так:
BOOLEAN __fastcall WriteDataToTheFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, char *lpKeysData, int a5)
{
...
NumberOfBytesWritten = 0;
lpDataBuffer = lpBuffer;
SetFilePointer(hFile, 0, 0, 0);
// В файл записываются первые 7 байт 8-ми байтного ключа
WriteFile(hFile, lpKeysData, 7u, &NumberOfBytesWritten, 0);
// Далее на основе 8-байтного ключа генерируются ещё 8 байт
someStruct2.nTotalNum_32_InitItem1 = 32;
someStruct2.nNumberOfDwords_Eq8_InitItem0 = 8;
SSomeStruct2::Preinit(&someStruct2);
SSomeStruct2::Preinit(&someStruct2); // Ничего не меняет
SSomeStruct2::AddKeysData(&someStruct2, lpKeysData, v7);
sub_401390(&someStruct2);
lpKey2Data = get_key2_data(&someStruct2);
// Эти дополнительные 8 байт записываются в файл
WriteFile(hFile, lpKey2Data, 8u, &NumberOfBytesWritten, 0);
// И теперь в файл записываются зашифрованный контент исходного файла
WriteFile(hFile, lpDataBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0);
return TRUE;
}
Из исходников мы выяснили, что:
- Файл шифруется с помощью некоторого 8-байтного ключа key1
- 7 из 8 байт этого ключа key1 записываются в файл
- На основе ключа key1 генерируется ключ key2, значение которого записывается в файл после key1
- В конец файла записываются зашифрованные данные исходного файла
0000000000: 47 08 8F E7 C4 C0 E9 71 42 38 2C E2 2D DE D7 1A
^^^^^^^ key1 ^^^^^^^ ^^^^^^^^^ key2 ^^^^^^^^ ++
0000000010: D7 F8 07 D2 14 1D 18 61 7C A4 54 C8 9E 3B B1 F0
+++++++++++++++ encrypted data ++++++++++++++++
Что мы из этого имеем:
- Мы можем вытащить из файла Bridge.txt 7 из 8 байт ключа шифрования key1: 47,08,8F,E7,C4,C0,E9,??)
- Последний байт ключа key1 можно подобрать, используя key2 (нужно перебрать последний байт key1[7] = [0x00..0xFF], пока из key1 не получим правильный key2)
- Как только мы найдём правильный ключ key1, мы сможем подобрать файл
Шаг 3.
Подбираем последний байт ключа key1.Легче всего будет подобрать последний байт key1, если мы немного модифицируем Reverse500.exe. Для модификации нам понадобиться найти участок кода, который не используется при шифровании файла и который можно безопасно поменять, прописав там необходимый нам патч. Для этого ищем в IDA функцию, на которую нет ссылок (помечается другим цветом), и заменяем (в HIEW) в этом месте байты на 0xCC (int 3). Если EXE-шник продолжает корректно работать, значит функция действительно не использовалась.
Итак, подходящий нам участок кода нашёлся по адресу 0x0041B8A1 и можно писать патч. В патче мы:
- подменим сгенерированный ключ key1 на нужный нам (первые 7 байт которого мы взяли из Bridge.txt).
- установим "хук" на вызов функции maybe_encode2 (0x401CC0).
; перехват вызова функции maybe_encode2 .0040205D: E840980100 call .00041B8A2 ; вызываем наш патч вместо функции maybe_encode2 ; код нашего патча (при вызове в EAX хранится указатель на key1) .0041B8A2: C70047088FE7 mov d,[eax],0E78F0847 ; меняем первые 4 байта key1 на свои .0041B8A8: C74004C4C0E900 mov d,[eax][04],000E9C0C4 ; меняем последние 4 байта key1 на свои .0041B8AF: E90C64FEFF jmp .000401CC0 ; прыжок на оригинальную функцию maybe_encode2
Запускаем пропатченный Reverse500.exe, и что мы имеем? Утилита шифрует все файлы с правильным key1, но неправильным key2. Это значит, последний байт ключа key1 сейчас задан неверно.
С помощью perl-скрипта сгенерируем на основе пропатченного Reverse500.exe ещё 256 утилит Reverse500.<num>.exe, каждая из которых будет задавать свой последний байт key1:
open(F,"<Reverse500.exe");
binmode(F);
read(F, $data, 1000000);
close(F);
$patch_ofs = 0x01ACAE;
$data1 = substr($data, 0, $patch_ofs);
$data2 = substr($data, $patch_ofs + 1, length($data));
for($pos =0; $pos < 256; ++$pos)
{
open(F,">Reverse500.$pos.exe");
binmode(F);
print F $data1 . chr($pos) . $data2;
close(F);
}
С помощью каждой из сгенерированных утилит зашифруем тестовый файл и найдём, что утилита Reverse500.171.exe дала правильный key2. Значит, мы нашли правильное значение ключа key1: 47,08,8F,E7,C4,C0,E9,AB.Шаг 4.
Расшифровываем Bridge.txt.Расшифровать файл Bridge.txt с помощью утилиты Reverse500.171.exe оказалось очень просто. Достаточно:
- Сгенерировать файл из 1272 нулей (1272 = размер(Bridge.txt) - 15 = 1287 - 15) (например, с помощью perl -e "print chr(0) x 1272" >zero_file.txt)
- Зашифровать его утилитой Reverse500.171.exe
- Поксорить зашифрованный zero_file.txt и bridge.txt с помощью Python-скрипта:
import sys
src_data = bytearray(open("Bridge.txt", 'rb').read())
xor_data = bytearray(open("zero.file.txt", 'rb').read())
for i in range(len(src_data)):
src_data[i] ^= xor_data[i % len(xor_data)]
sys.stdout.buffer.write(src_data)
Готово!

Итоги и выводы:
- Задание успешно решено, жалко только, что через 20 минут после окончания CTF'а.
- Основная сложность в этом задании - это наличие виртуальных функций (ругаю себя за то, что до сих пор не пользуюсь автоматическими средствами, помогающими в реверсе такого кода типа HexRaysPyTools - Plugin assists in creation classes/structures and detection virtual tables)
- Почему я долго решал это задание? Я слишком поздно понял, что ключ key1 8-ми байтный, а не 7-ми байтный, и не понимал, почему key2 получается каждый раз разный с одним и тем же 7-ми байтныйм key1.
