[DFIR/Kernel] Yet Another Shitty Dump (Bimbo Accident) - IX Кубок CTF России
Сложность: Hard | Категории: Forensics, Reverse, Kernel | solved by: @mixinspace @pigadoor @ogurez @crowdyara | Автор: @revker
Котик, я тут пока поизучала всякого…
Случайно шифровальщик у себя запустила… компик не работает. Купишь новый? 🥺🙈🩷
Зашифровала себе сумочку биркин, она у меня была в папке Documents под именем MyDream.png 🩷🩷🩷
Там же и шифровальщик лежит, только он почему-то сам себя зашифровал… 🙈
Ещё там какие-то тюбики хотят выкуп, информация о нём в той же папке в файле README.txt 😓😓
Шаг 1. Первичное исследование
Таск встречает нас дампом памяти на 2 гб.Для анализа будем пользоваться volatility3.
python3 vol.py -f mem.raw windows.info Major/Minor 15.19041 NtSystemRoot C:\WindowsNt MajorVersion 10
Этот дамп был снят с Windows 10 19041 (Что удобно, так как для любителей volatility2, на гитхабе существует отдельная ветка специально для этой версии винды).
Попробуем достать файлы из папки Documentswindows.filescan
0xcd04ff6851f0 \Users\Snezhana\Documents\desktop.ini 0xcd0501a392d0 \Users\Snezhana\Documents\README.txt
Из упомянутых Снежаной файлов здесь только 1 - README.txt
python3 vol.py -f mem.raw windows.dumpfiles --virtaddr 0xcd0501a392d0
Send 1 BTC to ZmFrZV9hZGRyZXNfanVzdF9zb2x2ZV90aGVfdGFzaw to get back your Birking bag!
Your encryption id: WJA^LEJEIEHREVARE
Похоже что этот файл был оставлен малварем шифрофщиком.
На всякий проверили строчку кошелька (это Base64)
ZmFrZV9hZGRyZXNfanVzdF9zb2x2ZV90aGVfdGFzaw > fake_addres_just_solve_the_task
Осознали и пошли решать дальше.
Если есть малварь, и он зашифровал файлы, то он явно должен был запускаться.Список процессов с помощью windows.pslist и windows.psscan не выявил ничего подозрительного, в windows.dlllist и windows.handles тоже ничего не нашел.
Шаг 2. WinCrypt.sys
Внезапно вспомнил, что у таска была категория kernel и решил просмотреть список подгруженных модулей с помощью windows.modules
python3 vol.py -f mem.raw windows.modules ...0xcd04fb87dc00 0xf8050dd80000 0x12000 WdNisDrv.sys \SystemRoot\system32\Drivers\WdNisDrv.sys Disabled0 xcd0500cf3510 0xf8050dda0000 0x8000 WinCrypt.sys \?? \C:\Windows\System32\drivers\WinCrypt.sys Disabled 0xcd0500245920 0xcb58ac560000 0x48000 cdd.dll \SystemRoot\System32\cdd.dll Disabled
Среди стандартных драйверов винды внимание привлек WinCrypt.sys
Выкачаем его используя pedump, специально предназначенный для извлечения бинарей:
python3 vol.py -f mem.raw windows.pedump --base 0xf8050dda0000 --kernel-module
Дизассемблируем и изучим исходный код драйвера:
Драйвер создает устройство "\Device\WinCrypt" и обработчик команд IOCTL. Обработчик ожидает pid процесса и одну из двух команд.
//sub_F8050DDA1040NTSTATUS WinCryptDeviceControl(PDEVICE_OBJECT dev, PIRP irp) {
ULONG pid = *(ULONG*)irp->AssociatedIrp.SystemBuffer;
switch (code) {
case 0x222000:
st = sub_F8050DDA1440(pid); // sub_F8050DDA1440
break;
case 0x222004:
st = sub_F8050DDA15A4(pid); // sub_F8050DDA15A4
break;
default:
st = STATUS_INVALID_DEVICE_REQUEST;
break;
}Функция sub_F8050DDA1440 находит структуру _EPROCESS для данного pid, в которой хранятся вся информация о процессе, зануляет адрес по смещению 0x5a8 и что-то делает с памятью по адресу 0x448.
NTSTATUS HideProcessByPid(ULONG pid) {
PUCHAR eproc = NULL;
FindEProcessByPid(pid, &eproc); //sub_F8050DDA1340
...
v3 = eproc + 0x448;
RtlFillMemory((PVOID)(eproc + 0x5A8), 0x0, 0xF);
v6 = (_QWORD *)v3[1];
v5 = (_QWORD *)*v3;
if ( v6 && v5 )
{
*v6 = v5;
v5[1] = v6;
*v3 = v3;
v3[1] = v3;
return 0;
}Оффсеты 0x448 и 0x5A8 взяты из структуры _EPROC, спецификацию которой можно прочитать на сайте vergiliusproject - https://www.vergiliusproject.com/kernels/x64/windows-10/2004/_EPROCESS, где описаны все kernel структуры для каждой версии windows.
//0xa40 bytes (sizeof)
struct _EPROCESS{
struct _KPROCESS Pcb; //0x0
...
VOID* UniqueProcessId; //0x440
struct _LIST_ENTRY ActiveProcessLinks; //0x448
...
struct _FILE_OBJECT* ImageFilePointer; //0x5a0
UCHAR ImageFileName[15]; //0x5a8
...
};//0x10 bytes (sizeof)
struct _LIST_ENTRY
{
struct _LIST_ENTRY* Flink; //0x0
struct _LIST_ENTRY* Blink; //0x8
}Получается, что эта операция зануляет имя процесса, и замыкает процесс сам на себя, выводя его из общего двусвязного списка процессов.
PLIST_ENTRY prev = v3->Blink; PLIST_ENTRY next = v3->Flink; prev->Flink = next; next->Blink = prev; v3->Flink = v3->Blink = v3;
Вторая операция возвращает процесс в общий список, но оставляет имя пустым.
PLIST_ENTRY head = g_PsActiveProcessHead;
node->Flink = first;
node->Blink = head;
head->Flink = node;
first->Blink = node;Шаг 2.5. Интерлюдия
Поняв что делает драйвер, но не поняв что с этим делать я пошел вручную читать BimboIncident.raw в hex редакторе. Было интересно найти следы присутствия других файлов из описания задания.
Поиск по "Documents" выявил наличие в папке следующих файлов:
MyDream.png
Cryptor.exe
Dropper.exe
out.exe
Flag.png
Flag.png.dll
README.txt
1.exe
1.exe.txt
desktop.ini
Хотелось найти содержание Cryptor.exe, поэтому искал по названию. На удивление, слово cryptor встречался в файле 331 раз. Перейдя на первое значение, я был уверен что нашел то что надо, так как передо мной были артефакты от какого-то ransomware
You have been encrypted with Rush Ransomware\DECRYPT_YOUR_FILES.HTML\Sanction Ransomware\Project Encryptor\hidden-tearTrojan:
Обрадовавшись, я почти пошел его гуглить, но решил посмотреть и другие вхождения cryptor.
...
Kraken Cryptor
...
FileCryptor
...
/ScriptCryptor_HSTR1
...
encryptor_raas
...
Ransom:MSIL/Godcrypt
...
И еще много следов других криптеров, енкриптеров, рансомов и дропперов. Немного огорчившись, я все еще верил, что файлы были зашифрованы одним из них и попытался найти следы нашего README.txt в этих артефактах, но это ни к чему не привело.
Одним из вхождений Сryptor.exe действительно был наш малварь, но не в папке Снежаны, а на рабочем столе пользователя `anon` в папке `test_crypt`.
В этой же папке были найдены файлы README.txt и Flag.png. Похоже что автор задания от этого пользователя тестил этот и другие вирусы, а потом уже запустил на снежане.
Также в папке загрузки у анона нашел файлы
WinCrypt.inf
WinCrypt.sys
osrloader.exe
DriverView.exe
dbgview64.exe
Cryptor.exe
И смешной артефакт с оригинальным названием таска)
\ctfcup\tasks\forensics\YetAnotherShittyDump\dev\Driver\WinCrypt\x64\Release
Очевидно, что автор очень хотел правильно загрузить драйвер и проверить его работоспособность. Тут то меня и осенило, что драйвер зачем-то подгружали, он где-то точно использовался.
Шаг 3. Поиск скрытого процесса
Volatility2 и 3 ни одним стандартным плагином процессов, скрытых манипуляциями с DKOM, поэтому пришлось написать плагин для этого самому, который находит каждый `_EPROCESS` и проверяет что он замкнут на себя.
python vol.py -f mem.raw windows.malware.hidden_dkom_list Volatility 3 Framework 2.27.0 Progress: 100.00 PDB scanning finished PID PPID ImageFileName ImagePath EPROCESS_Offset(V) 4440 1208 C:\Windows\system32\ctfmon.exe 0xcd0501a08340
Нашелся процесс с pid 4440 с пустым именем, это инстанс ctfmon.exe. Через стандартные плагины волатилити вытащить дамп памяти не получилось, поэтому пришлось написать второй плагин для выкачивания таких процессов.
Получили дамп памяти процесса на 525мб, почти в самом его начале присутствует сигнатура MZER какого-то .exe файла. После обрезки файла до начала MZER и декомпилирования кода, предположение подтвердилось - мы нашли Cryptor.exe.
Шаг 4 - Cryptor.exe
Открываем бинарь, прыгаем в функцию main и начинаем исследовать его функциональность. В крипторе все строки зашифрованы, и следующий код мы будем видеть везде, где они расшифровываются:
strcpy(FileName, "QQ#QZdcN\x7Ft}y");
do
++v5;
while ( FileName[v5] );
if ( v5 )
{
v6 = 0;
if ( v5 < 64 )
goto LABEL_17;
si128 = _mm_load_si128((const __m128i *)&xmmword_14000AAC0);
do
{
*(__m128i *)&FileName[v6] = _mm_xor_si128(si128, _mm_loadu_si128((const __m128i *)&FileName[v6]));
*(__int128 *)((char *)&v13 + v6) = (__int128)_mm_xor_si128(
_mm_loadu_si128((const __m128i *)((char *)&v13 + v6)),
si128);
*(__m128i *)((char *)&retaddr + v6) = _mm_xor_si128(
_mm_loadu_si128((const __m128i *)((char *)&retaddr + v6)),
si128);
*(__int128 *)((char *)&v15 + v6) = (__int128)_mm_xor_si128(
si128,
_mm_loadu_si128((const __m128i *)((char *)&v15 + v6)));
v6 += 64LL;
}
while ( v6 < (v5 & 0xFFFFFFFFFFFFFFC0uLL) );
if ( v6 < v5 )
{
LABEL_17:
do
FileName[v6++] ^= 0xDu;
while ( v6 < v5 );
}
}Здесь зашифрованная строка копируется в память, затем расшифровывается операцией XOR с 0xD (для хвоста; для кратных 64 байтам блоков используется SSE XOR).
Следующим шагом происходит открытие файла и если он существует, то программа выполняет функцию `sub_1400033A0`, к ней и перейдём.
В первой части данной функции происходит расшифровка всех необходимых строк, затем — рекурсивный перебор всех файлов в директории (это видно по вызову sub_140002A90("recursive_directory_iterator::recursive_directory_iterator", v18, Block);).
Так как это шифратор, то должна быть функция, которая принимает путь до файла и шифрует его. В дампе строк видны стандартные WinCrypt‑API: CryptCreateHash, CryptHashData и т. д. Они используются в `sub_1400043A0` (та принимает два аргумента).
Теперь необходимо понять, как именно происходит шифрование файлов. Ключевой фрагмент:
pbData[0] = *a2 ^ *v12;
pbData[1] = a2[1] ^ v12[1];
pbData[2] = a2[2] ^ v12[2];
pbData[3] = a2[3] ^ v12[3];
pbData[4] = a2[4] ^ v12[4];
pbData[5] = a2[5] ^ v12[5];
pbData[6] = a2[6] ^ v12[6];
pbData[7] = a2[7] ^ v12[7];
pbData[8] = a2[8] ^ v12[8];
pbData[9] = a2[9] ^ v12[9];
pbData[10] = a2[10] ^ v12[10];
pbData[11] = a2[11] ^ v12[11];
pbData[12] = a2[12] ^ v12[12];
pbData[13] = a2[13] ^ v12[13];
pbData[14] = a2[14] ^ v12[14];
pbData[15] = a2[15] ^ v12[15];
phProv = 0;
phKey = 0;
phHash = 0;
if ( CryptAcquireContextW(&phProv, 0, 0, 0x18u, 0xF0000000) )
{
if ( CryptCreateHash(phProv, 0x800Cu, 0, 0, &phHash) )
{
if ( CryptHashData(phHash, pbData, 0x10u, 0) && CryptDeriveKey(phProv, 0x6610u, phHash, 0, &phKey) )
{
if ( (v7 & 0xF) != 0 )
LODWORD(v7) = 16 - (v7 & 0xF) + v7;
LODWORD(Size) = v7;
v15 = operator new((unsigned int)v7);
memset(v15, 0, (unsigned int)Size);
memcpy(v15, v12, v13 - v12);
if ( CryptEncrypt(phKey, 0, 0, 0, (BYTE *)v15, (DWORD *)&Size, Size) )
{...}
}
}
}Из него можно понять, что шифрование идет с помощью AES 256, а ключ для него генерируется на основании двух переменных - входное значение (a2) и первые 16 байт самого файла (v12), а значит, чтобы дешифровать файл, нам нужно знать входное значение (шифруется через XOR с 0xD и является строкой SNEZHANAMALVAREVA), а также первые 16 байт исходного файла. К счастью, флаг у нас в png картинке, а все картинки всегда начинаются с
Первые 8 байт - это PNG сигнатура, дальше должен идти чанк
Чанки могут быть разного размера, но всегда первые 8 байт чанка занимает размер чанка и его тип (4 байта на размер и 4 байта на тип).
В нашем случае в PNG сразу после сигнатуры идет стандартный чанк IHDR, который содержит в себе длину и ширину картинки, а также флаги компрессии, цвета, фильтры и бит глубины, этот чанк всегда имеет размер 13 байт и тип IHDR.
Получается последние 8 байт ключа это размер чанка (00 00 00 0D) и тип чанка (49 48 44 52)
Получается, что у нас есть всё для восстановления исходной картинки, кроме самого шифртекста. Его найти просто: шифруем любую PNG таким же образом и смотрим на первые 16 байт — они совпадают для одинакового ключа.
Расшифровать картинку с флагом можно так:
import os
p0 = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52] # PNG header
a2 = [0x53, 0x4e, 0x45, 0x5a, 0x48, 0x41, 0x4e, 0x41, 0x4d, 0x41, 0x4c, 0x56, 0x41, 0x52, 0x45, 0x56] # SNEZHANAMALVAREVA
key = [p0[i] ^ a2[i] for i in range(len(p0))]
with open("key_f.bin", "wb") as f:
f.write(bytes(key))
os.popen("openssl enc -d -aes-256-cbc -in hidden.pid.4440.dmp -out flag.png -K $(cat key_f.bin | sha256sum) -iv $(xxd -p -c 16 iv_f.bin)")