June 26

[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, на гитхабе существует отдельная ветка специально для этой версии винды).

Попробуем достать файлы из папки Documents
windows.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

README.txt:

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`.

c:\users\anon\desktop\test_crypt\cryptor.exe

В этой же папке были найдены файлы 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 картинке, а все картинки всегда начинаются с

89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

Первые 8 байт - это PNG сигнатура, дальше должен идти чанк

Чанки могут быть разного размера, но всегда первые 8 байт чанка занимает размер чанка и его тип (4 байта на размер и 4 байта на тип).

В нашем случае в PNG сразу после сигнатуры идет стандартный чанк IHDR, который содержит в себе длину и ширину картинки, а также флаги компрессии, цвета, фильтры и бит глубины, этот чанк всегда имеет размер 13 байт и тип IHDR.
Получается последние 8 байт ключа это размер чанка (00 00 00 0D) и тип чанка (49 48 44 52)

Получается, что у нас есть всё для восстановления исходной картинки, кроме самого шифртекста. Его найти просто: шифруем любую PNG таким же образом и смотрим на первые 16 байт —  они совпадают для одинакового ключа.

Шифруем и ищем в памяти 

E0 23 F2 B1 5C 8C 22 0D 34 5D 56 C1 A8 FD 2D C3

Расшифровать картинку с флагом можно так:

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)")