В этой статье описывается решение задачи 1, с которой начинался конкурс. Мне удалось решить эту задачу вовремя, но недостаточно быстро, чтобы обогнать таких монстров реверса, как sysenter и vos ;) (в таблице победителей я тут помечен в категории "Also solved" просто как "stas.zn")
Задача:
Day 1 / ZeroCrypt
![]()
EN: Your boss catched crypto locker. Sysadmins were able to restore all files from backup - except one (which is very important for him). Your goal is to restore this file.
Warning: run this binary only under VM! We are not responsible for any loss or damage
RU: К твоему боссу на компьютер попал криптолокер. Админы смогли восстановить все его файлы из бекапов, кроме одного, очень важного для него. Воcстанови файл и помоги своему боссу (или накажи за все его грехи, ведь файл, похоже, относится к черной бухгалтерии).
Важно: запускайте бинарный файл только под виртуальной машиной! Мы не несем ответственности за любой возможный ущерб
Link: hackquest.zeronights.org/downloads/2016/1_zerocrypt.zip. Password: ZN2016This task was prepared by RET (Reverse4you Education Team) team
Первые ощущения от задания:
Никогда ещё не имел дела с настоящим криптером. А по красным ворнингам и паролю на архиве, можно подумать, что там именно настоящий! (конечно же не буду запускать его без виртуалки)
Решение: [Шаг 1 | Шаг 2 | Шаг 3 | Шаг 4]
Шаг 1. Распаковывыем 1_zerocrypt.zip. Defender сразу ругается, приходится его отключить.
![]() |
| Trojan:Win32/Skeeyah.A!rfn |
Только перед этим:
- На всякий случай отключим сеть
- Дадим шифровальщику пищу (создадим на рабочем столе документ test.doc (10 байт), картинку test.jpg (10 байт))
- Запустим Process Monitor (чтобы посмотреть, что и где ищет шифровальщик)
- Делаем снэпшот на виртуалке перед запуском (чтобы каждый раз начинать исследование "с чистого листа")
Далее слушаем шуршание SSD и через некоторое время видим на экране текст вымогателя:
![]() |
| Вымогатель пугает RSA 1024 и хочет за дешифровку 10 BTC. Немало так: 700$ по текущему курсу. |
- шифровальщик скопировал себя в папку "%userprofile%\AppData\Local\ZeroCrypt\"
- прописал себя в автозапуск
- зашифровал файл test.doc и превратил его в test_1b5451545ab556b5.zn2016
- записал текст вымогателя в ZEROCRYPT_RECOVER_INFO.txt
- не трогал картинку test.jpg (значит, у него есть список расширений файлов, которые он шифрует)
- подставляя файлы test.doc разного размера, мы можем выяснить, что зашифрованный файл всегда увеличивается на 128 байт. Значит, размер исходного файла BlackAccounting_1b54475454b547b5.zn2016 до того, как его зашифровали, был
239 - 128 = 111 байт - судя по последовательности записи зашифрованного файла (сначала WriteFile(10 байт), потом WriteFile(128 байт)) получается, что сначала пишется зашифрованный текст, а потом пишется некий ключ для его расшифровки
- если некоторое время экспериментировать с шифрованием файлов, можно заметить, что файлы с одинаковыми расширениями шифруются всегда в файлы с одинаковыми суффиксами (например, расширение '.doc' всегда превращается в суффикс '_1b5451545ab556b5.zn2016'). Это значит, что если мы подберём правильное расширение, которое заменится на суффикс '_1b54475454b547b5.zn2016', мы узнаем, какое было расширение у зашифрованного файла из задания (BlackAccounting_1b54475454b547b5.zn2016)
strings.exe ManBeCareful.ex_ | findstr "^\.[a-z]*$" | sort .text .rsrc .PjRW .zC .as .asm .asp .aspx .aut .bas .bbc .cc .class .cpp .cs .csx .cxx .dpr .erl .fla .fsscript .fsx .go .gvy .has .hh .hpp .hrl .hxx .hydra .ino .jav .java .jic .js .jsfl .jsh .json .jsp .lisp .lsp .lua .pas .pb .php .pl .plc .pli .pm .pod .py .pyd .qml .rb .tcc .tcl .tpu .vhd .gitattributes .gitignore .wl .key .csv .doc .docx .docm .rtf .xml .xls .xlsx .xlsm .pdf .djvu .zip .rar
На самом деле, список поддерживаемых расширений чуть больше, чем сдампила команда strings, но и этого достаточно для наших целей.
Далее создадим кучу файлов test.<ext> со всеми поддерживаемыми расширениями и запустим шифровальщик ещё раз.
После этого мы получили файл test_1b54475454b547b5.zn2016. Посмотрев в Process Monitor'е из какого файла получился файл test_1b54475454b547b5.zn2016, мы узнали, что он получился из файла test.rar. Значит, зашифрованный в задании файл BlackAccounting_1b54475454b547b5.zn2016 был изначально архивом с именем BlackAccounting.rar.
Также Process Monitor может нам подсказать место вызова каждой файловой операции в коде. Для этого достаточно посмотреть callstack. Например, операция переименования файла была с таким callstack'ом:
Мы видим, что функция MoveFileW вызывалась с помощью инструкций, находящихся рядом с относительным адресом ManBeCareful.exe + 0x46E1.
Для того, чтобы Process Monitor показывал в callstack'е тот же самый абсолютный адрес, который мы будем наблюдать в дизассемблере (IDA), можно убрать из EXE-шника Relocation Table (секцию .reloc) (например, с помощью PE Explorer) и выставить флаг IMAGE_FILE_RELOCS_STRIPPED (0x0001) в поле Characteristics заголовка PE (см. MSDN).
Тогда мы увидим в callstack'е правильный абсолютный адрес 0x4046e1 (как на скриншоте выше).
Шаг 2. Открываем ManBeCareful.exe в IDA, переходим по интересующему нас адресу 0x4046e1 и открываем функцию в Hex-Rays. Там мы наблюдаем вот такой код:
Проводим некоторое время в отладчике, запоминая, какие значения даёт функция sub_401980 (GetAPIFn) в зависимости от параметров. Переименовываем названия переменных/функций, чтобы код читался легче. Теперь этот кусок кода выглядит гораздо понятнее.
А функция шифрования (main_file_crypter_fn @ 0x404400) схематично выглядит так:
int __fastcall main_file_crypter_fn(int a1, wchar_t *pwsStr)
{
...
// Даём исходному файлу новое имя
pFNHash = (int)&fnHash__MoveFileW;
pfnMoveFileW = GetAPIFn(&pFNHash);
v15 = ((int (__stdcall *)(void *, void *))*pfnMoveFileW)(wszSourceName, wszNewName) != 0;
...
// Проверяем, что успешно переименовался
pFNHash = (int)&fnHash_shlwapi_PathFileExistsW;
pfnPathFileExistsW = GetAPIFn(&pFNHash);
if ( ((int (__stdcall *)(void *))*pfnPathFileExistsW)(v17) )
{
// Открываем переименованный файл
memset(&objFileWrap, 0, 0xC0u);
Obj2_OpenConstructor(&objFileWrap, (int)&v67, v19, v20, v21); // OpenFile(renamed)
// Определяем его размер
CBigFileWrap::Seek(&objFileWrap, v22, v23, SEEK_END);
pFileSize = CBigFileWrap::Tellg(&objFileWrap, (int)&v53);
nFileSize = *(_DWORD *)pFileSize + *((_DWORD *)pFileSize + 2);
CBigFileWrap::Seek(&objFileWrap, v26, v27, SEEK_SET);
// Генерируем "ключ шифрования"
memset(&encCtx, 0, 0x3Cu);
pFNHash = (int)&valHash_TickCountAtStart;
nRandSeed = (int)*GetAPIFn(&pFNHash);
encCtx.nSomeData_val7 = 7;
encCtx.nSomeData_val0 = 0;
encCtx.nSomeWord_val0[0] = 0;
encCtx.rand_seed = nRandSeed;
PrepareRandBytesBySeed(&encCtx);
TransformRandBytes(&encCtx);
// Читаем файл в память
pFileDataBuff = (char *)std_allocator_fn(0x400000u);
nFileSize_ = 0x400000;
CBigFileWrap::read(&objFileWrap, pFileDataBuff, v31, v32);// ReadFile (all data)
...
// Шифруем данные файла
EncodeFileData1(&encCtx, pFileDataBuff, nFileSize_);
...
// Далее записываем контент шифрованного файла и зашифрованный ключ
...
}
Шаг 3. Ищем ключ.
В листинге функции main_file_crypter_fn мы заметили блок кода "генерация ключа шифрования", по которому видно, что этот ключ генерируется только на основе значения GetTickCount, которое было при старте шифровальщика.
Это замечательно! Так как TickCount хранит время работы компьютера с последней загрузки (в мс), то обычно его значение достаточно небольшое и его легко будет перебрать.
Также мы знаем, что исходный файл был 111-байтным архивом BlackAccounting.rar. Ну что же, сделаем файлик BlackAccounting.txt со случайными данными - такой, чтобы после архивирования, размер архива получился 111 байт.
0000000000: 52 61 72 21 1A 07 00 CF │ 90 73 00 00 0D 00 00 00 Rar!→• П?s ♪
0000000010: 00 00 00 00 A0 41 74 20 │ 90 38 00 1C 00 00 00 2C At ?8 ∟ ,
0000000020: 00 00 00 02 2A FF 90 57 │ 77 10 61 49 1D 33 13 00 ☻*я?Ww►aI↔3‼
0000000030: 20 00 00 00 42 6C 61 63 │ 6B 41 63 63 6F 75 6E 74 BlackAccount
0000000040: 69 6E 67 2E 74 78 74 00 │ B0 90 9F 64 00 C1 08 FE ing.txt °??d Б◘ю
0000000050: 0C 10 94 BD 4A 65 F0 C1 │ FE A6 12 22 0A 46 E0 1D ♀►"?JeрБю│↕"◙Fа↔
0000000060: 3C 5F 34 56 78 9A BC D4 │ C4 3D 7B 00 40 07 00 <_4Vx??ФД={ @•
Наш 111-байтный архив готов, и мы теперь знаем какие первые байты должны быть в расшифрованном файле.Теперь чтобы найти ключ достаточно:
- перебрать все значения TickCount'ов начиная с 0
- сгенерировать на их основе ключи шифрования
- шифровать наш тестовый BlackAccounting.rar сгенерированными ключами, пока не получим в начале шифрованного текста байты 0x70,0xB9,0xA2,0x21 (первые 4 байта файла BlackAccounting_1b54475454b547b5.zn2016)
Так как функции шифрования файла и генерации ключа достаточно сложные, чтобы перенести их в свой код, будем использовать готовые функции из ManBeCareful.noreloc.exe (это оригинальный ManBeCareful.ex_, из которого убрана Relocation Table). Для этого соберём свою библиотеку l.dll, которую будем загружать в процесс ManBeCareful.noreloc.exe с помощью специального лоадера из WinDbg:
// "Лоадер" eb eip 6A 6C 54 FF 15 A0 31 44 00 54 50 FF 15 00 30 44 00 83 C4 04 FF D0 // Код инжектора: 6A6C push 06C ; Это L"l" (для загрузки "l.dll") 54 push esp FF15A0314400 call d,[004431A0] ; LoadLibraryW 54 push esp ; Импортируем функцию L"l" 50 push eax ; модуля "l.dll" FF1500304400 call d,[00443000] ; GetProcAddress(hMod_l, "l") 83C404 add esp,004 FFD0 call eax ; Call l.dll!l() // Полезные импортируемые функции в ManBeCareful.noreloc.exe 004431A0 off_4431A0 dd offset kernel32_LoadLibraryW 00443000 off_443000 dd offset kernel32_GetProcAddress
Итак, реализуем в l.dll брутфорсер TickCount'ов:
// Переносим из IDA описание структуры, содержащей контекст шифрования (ключ)
#pragma pack(push, 1)
struct CClass1
{
public:
__int16 nSomeWord_val0[2];
int nZero2;
int nZero3;
int nZero4;
int nSomeData_val0;
int nSomeData_val7;
int rand_seed;
char *pRandom16BytesStr;
SExtendedRandData *pExtendedRandMap;
int nZero5;
int nSomeFixedVal1;
int nSomeFixedVal2;
int nByteValChangedInEncode1;
int nByteValChangedInEncode2;
int nTotalEncodedBytes;
public:
// Добавляем функции-члены класса
int EncodeFileData1(char *pData, size_t nSize);
int PrepareRandBytesBySeed();
int TransformRandBytes();
int CleanupEncCtx();
};
#pragma pack(pop)
// Реализация функций - это просто перенаправление функции на аналогичную функцию из ManBeCareful.noreloc.exe
int CClass1::EncodeFileData1(char *pData, size_t nSize)
{
typedef decltype(&CClass1::EncodeFileData1) Pfn_t;
union
{
DWORD_PTR pfnAddr;
Pfn_t pfn;
} pfn = { 0x0040EC60 };
return (this->*pfn.pfn)(pData, nSize);
}
int CClass1::PrepareRandBytesBySeed()
{
typedef decltype(&CClass1::PrepareRandBytesBySeed) Pfn_t;
union
{
DWORD_PTR pfnAddr;
Pfn_t pfn;
} pfn = { 0x40F700 };
return (this->*pfn.pfn)();
}
// Код брутфорсера
extern "C" void __stdcall l()
{
const char arReqPattern[] = {
0x70, 0xB9, 0xA2, 0x21
};
for (DWORD dwTickTickCounter = 0; dwTickTickCounter < 0xFFFFFFFF; ++dwTickTickCounter)
{
// Код генерации ключа (скопирован из IDA, только вместо TickCount'а используем собственный счётчик dwTickTickCounter):
CClass1 encCtx;
memset(&encCtx, 0, 0x3Cu);
encCtx.nSomeData_val7 = 7;
encCtx.nSomeData_val0 = 0;
encCtx.nSomeWord_val0[0] = 0;
int nRandSeed = dwTickTickCounter;
encCtx.rand_seed = nRandSeed;
encCtx.PrepareRandBytesBySeed();
encCtx.TransformRandBytes();
char curData[sizeof(g_testFile1)];
memcpy(curData, g_testFile1, sizeof(curData));
auto pFileDataBuff = curData;
auto nFileSize_ = sizeof(curData);
// Шифруем наш тестовый BlackAccounting.rar:
encCtx.EncodeFileData1(pFileDataBuff, nFileSize_);
// Сравниваем первые 4 байта зашифрованного только что файла с первыми 4-мя байтами
// файла BlackAccounting_1b54475454b547b5.zn2016
if (0 == memcmp(pFileDataBuff, arReqPattern, sizeof(arReqPattern)))
{
// Ключ найден
...
}
}
}
Компилируем l.dll и запускаем наш брутфорсер. Для запуска брутфорсера:- ставим в WinDbg точку останова по адресу 0x404400 в процессе ManBeCareful.noreloc.exe, а затем используем лоадер (eb eip 6A 6C 54 FF 15 A0 31 44 00 54 50 FF 15 00 30 44 00 83 C4 04 FF D0)
- или сразу прописываем лоадер в ManBeCareful.noreloc.exe по адресу 0x404400 и запускаем его.
Ждём некоторое время и... урра! у нас есть ключ шифрования 0xde2bc3 (точнее Seed, с помощью которого этот ключ генерируется).
Шаг 4. Подбираем файл.
Экспериментальным путём можно выяснить, что при шифровании файла с одним и тем же ключом, изменение одного байта исходного текста влияет на один байт шифрованного текста в той же позиции. Поэтому подобрать исходный файл, зная ключ и зашифрованный файл, можно побайтно. То есть используем следующий алгоритм:
Берём 111 нулевых байт и для каждого байта по порядку:
1. берём байт в позции n (n = [0..110])
2. шифруем все 111 байт
3. проверяем шифрованный байт в позиции n: если он равен соответствующему байту в зашифрованном файле BlackAccounting_1b54475454b547b5.zn2016, значит, мы нашли правильный исходный байт в позиции n и можно переходить к следующему байту (n = n + 1), иначе меняем значение байта в позиции n на следующее и переходим к п.1
Код брутфорсера выглядит так:
extern "C" void __stdcall l()
{
const char arEncData[] = {
/*0000000000:*/0x70,0xB9,0xA2,0x21,0x4C,0x48,0xA9,0xEE,0x17,0x0D,0xE4,0xCA,0x99,0xEC,0x49,0xFC,
...
/*0000000060:*/0xE0,0x7F,0xFA,0x7D,0x88,0xEB,0xA3,0xC6,0x1C,0x87,0xDB,0x35,0x18,0x2B,0x04};
char curBruteData[sizeof(arEncData)];
size_t nPos = 0;
DWORD dwTickCounterKey = 0xde2bc3; // Key
while (nPos < sizeof(curBruteData))
{
for (unsigned int val = 0; val <= 0xFF; ++val)
{
// Задаём текущий байт в массиве curBruteData
curBruteData[nPos] = val;
// Генерируем ключ
CClass1 encCtx;
memset(&encCtx, 0, 0x3Cu);
encCtx.nSomeData_val7 = 7;
encCtx.nSomeData_val0 = 0;
encCtx.nSomeWord_val0[0] = 0;
encCtx.rand_seed = dwTickCounterKey;
encCtx.PrepareRandBytesBySeed();
encCtx.TransformRandBytes();
char curData[sizeof(curBruteData)];
memcpy(curData, curBruteData, sizeof(curData));
// Шифруем curData (копию curBruteData)
encCtx.EncodeFileData1(curData, sizeof(curData));
encCtx.CleanupEncCtx();
// Проверяем, угадали ли мы байт?
if (curData[nPos] == arEncData[nPos])
{
// Байт в позиции nPos угадали, переходим к следующей позиции...
// Но сначала проверим, может файл уже расшифрован целиком?
if (0 == memcmp(arEncData, curData, sizeof(curData))) {
WriteFileContent(L"DecFile.bin", curBruteData, sizeof(curBruteData));
MessageBoxW(0, L"FiniSH!!!", L"OK", MB_OK);
}
break;
}
}
nPos++;
}
}
Компилируем, запускаем, ждём..... Готово! Мы получили оригинальный файл с флагом.
0000000000: 52 61 72 21 1A 07 00 CF │ 90 73 00 00 0D 00 00 00 Rar!→• ?>s ♪
0000000010: 00 00 00 00 D3 86 74 20 │ 90 2D 00 27 00 00 00 27 ?>t ?- ' '
0000000020: 00 00 00 02 1B 96 76 EF │ E7 B8 23 49 1D 30 08 00 ☻←?v???#I↔0◘
0000000030: 20 00 00 00 46 6C 61 67 │ 2E 74 78 74 00 B0 45 82 Flag.txt ?E?
0000000040: 0F 5A 4E 32 30 31 36 5F │ 33 33 37 65 32 62 61 36 ☼ZN2016_337e2ba6
0000000050: 30 64 36 32 61 35 34 35 │ 66 65 63 31 38 36 39 36 0d62a545fec18696
0000000060: 62 32 36 36 33 30 30 35 │ C4 3D 7B 00 40 07 00 b2663005?={ @•
Полезные ссылки:
- Исходники моей библиотеки L.dll доступны тут: https://github.com/Stanislav-Povolotsky/ReverseTasks/tree/master/20161104/hackquest.zeronights.org/task1
- Бинарник задания можно скачать отсюда
- Скрипт (от авторов задания) для декодирования всех файлов, зашифрованных шифровальщиком можно скачать отсюда





