понедельник, 7 ноября 2016 г.

ZeroNights HackQuest Task 1: ZEROCRYPT

Это последняя статья из серии райтапов, в которых описывается решение задач конкурса ZeroNights HackQuest 2016 (см. решение задачи 5 и задачи 6).
В этой статье описывается решение задачи 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: ZN2016
This 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 (чтобы посмотреть, что и где ищет шифровальщик)
  • Делаем снэпшот на виртуалке перед запуском (чтобы каждый раз начинать исследование "с чистого листа")
Приготовления завершены, запускаем ManBeCareful.exe.

Далее слушаем шуршание SSD и через некоторое время видим на экране текст вымогателя:

Вымогатель пугает RSA 1024 и хочет за дешифровку 10 BTC.
Немало так: 700$ по текущему курсу.
Ладно, что там нам покажет Process Monitor? Фильтруем вывод по процессу "ManBeCareful.exe" и оставляем только запись в файл (WriteFile) и реестр (RegSetValue).


Тут мы видим, что:
  • шифровальщик скопировал себя в папку "%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:

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. Там мы наблюдаем вот такой код:

Раз v11 - это адрес функции MoveFileW, значит функция sub_401980 - это некое подобие функции GetProcAddress, только вместо имени функции ей передаётся некий четырёхбайтный номер-идентификатор.

Проводим некоторое время в отладчике, запоминая, какие значения даёт функция 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?={ @•

Полезные ссылки: