суббота, 5 ноября 2016 г.

ZeroNights HackQuest Task 5: STRONGBOX

У компании Digital Security есть замечательная традиция: перед проведением конференции ZeroNights устраивать конкурс HackQuest. И в этом году организаторы совместно с R0 Crew, School CTF и RuCTFE очередной раз порадовали новыми интересными заданиями. Решение одного из них описано ниже.

Задача:

Day 5 / StrongBox


EN: Your friend wrote an algorithm for generating licenses and asked you to test it. He promised a beer and pizza if you will find a way to crack it. Prove to him that his algorithm is no good.

RU: Твой друг по общаге написал алгоритм генерации лицензий и попросил тебя протестировать его. Взамен обещал пиво и пиццу, если у тебя получится подобрать ключ. Докажи ему, что его алгоритм никуда не годится.
Binary - StrongBox.zip 
This task was prepared by reverse4you team

Решение: [Шаг 1 | Шаг 2 | Шаг 3 | Шаг 4 | Шаг 5 | Шаг 6 | Шаг 7 | Шаг 8]

Шаг 1. Загружаем бинарник StrongBox.exe в IDA. И видим по названию секции (UPX0 / UPX1), что он упакован UPX'ом.


- Распаковываем UPX (например, с помощью PE Explorer, у которого есть плагин 'unupx').
- Для того, чтобы легче было отлаживать EXE-шник, можно сделать, чтобы он всегда загружался по одинаковым адресам, как раз по тем адресам, что мы видим в IDA. Для этого убираем из него Relocation Table (это тоже умеет делать PE Explorer). Для этого нужно удалить секцию .reloc и выставить флаг IMAGE_FILE_RELOCS_STRIPPED (0x0001) в поле Characteristics заголовка PE (см. MSDN) .

Шаг 2. Открываем модифицированный бинарник в IDA и с помощью поиска по строке 'Hey! Forget about the pizza...', находим место, где производится проверка лицензии.



Используя Hex-Rays, получаем более компактный код:

Шаг 3. Ставим точку останова, чтобы разобраться в том, какие параметры передаются в функцию check_license (0x405EB0).

Так как в коде, рядом с проверкой лицензии есть фраза 'Stay away from me!', есть подозрение, что используются некоторые анти-отладочные приёмы. Самый простой способ их обойти -поставить точку останова, не пользуясь отладчиком. Для этого я обычно патчу исполняемый файл так, чтобы программа просто "зависала" в интересующем меня месте, а только потом подключаюсь отладчиком.

Патчим с помощью HIEW (переходим на адрес .405278 и заменяем E8 33 на EB FE) :
До:

После:


После чего запускаем наш пропатченный EXE-шник на виртуалке, после нажатия на кнопку "Check" он зависает и мы спокойно можем приаттачится к нему с помощью отладчика WinDBG в интересующем нас месте.
Для того, чтобы убрать точку останова, достаточно выбрать висящий поток и выполнить команду, восстанавливающую код, который был до патча:
windbg> eb eip E8 33

Дампим параметры функции check_license:
windbg> db poi(poi(@esp+0*4))
016717cc  73 74 61 73 2e 7a 6e 40-70 6f 76 6f 6c 6f 74 73  stas.zn@povolots
016717dc  6b 79 2e 69 6e 66 6f 00-61 18 67 01 b0 04 02 00  ky.info.a.g.....
windbg> db poi(poi(@esp+1*4))
016aab0c  61 62 63 64 65 66 67 68-69 6a 6b 6c 6d 6e 6f 70  abcdefghijklmnop
016aab1c  71 72 73 74 75 76 77 78-79 7a 41 42 43 44 45 46  qrstuvwxyzABCDEF
И видим, что это как раз те самые значения полей 'Email' а и 'Password', которые мы заполнили в программе.


Теперь сохраним снэпшот на виртуалке, на которой мы осуществляем отладку. Это позволит вызывать функцию check_license много раз, задавая ей разные параметры и наблюдая за её работой)

Шаг 4. С параметрами функции разобрались, теперь разбираемся с самой функцией. Без HexRays'а понять функцию было бы не просто, поэтому используем HexRays (даже с ним функция check_license получается не маленькая: 1500 строк).

Итак, проводим некоторое время, давая нормальные названия параметрам, локальным переменным, вложенным функциям.

После чего видим по коду первые манипуляции с серийным номером (или Password'ом в терминах программы):
  SMaybeStdString::Init1(&strKey);
  SMaybeStdString::Reserve(&strKey, 0, 0);
  SMaybeStdString::Assign(&strKey, (char *)sKey_);
  ++v191;
  base64decode(&vecKeyUnbase64, &strKey);
...
  if ( vecKeyUnbase64.pData )
    nVecLen = vecKeyUnbase64.pDataEndPos - vecKeyUnbase64.pData;
  else
    nVecLen = 0;
  if ( nVecLen == 25 )
  {
    // Основной код проверки
Тут мы получаем первое ограничение к серийнику - это base64-строка, которая после base64-декодирования должна быть размером ровно 25 байт.

Шаг 5. Ищем код, который делает так, чтобы функция check_license возвращала true (для этого смотрим все места, где записывается значение переменной, которую возвращает функция check_license, то есть смотрим "Cross reference to <varname>").

Такой код нашёлся:
    bValidLicense = 0;
    if ( len1 == SStdDWORDVector::GetLength(&pVec) )// len1 == 20
    {
      SDataWrap::GetVecBegin(&itBegin2, &pVec);
      SDataWrap::GetVecEnd(&itEnd, &v200);
      SDataWrap::GetVecBegin(&itBegin, &v200);
      if ( SDataWrap::AreEqualDWORDVectors(itBegin, v75, itEnd, v77, itBegin2) )
        bValidLicense = 1;
    }
И видим тут сравнение двух векторов, каждый из которых размером 20*4 байт (то есть 20 DWORD'ов). Ну что же, попробуем дойти до этого сравнения и посмотреть, что там с чем сравнивается. Для этого:
- задаём серийник, проходящий базовые проверки: "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNA==" (base64encode("0123456789012345678901234"))
- ставим точку останова на вызов функции сравнения SDataWrap::AreEqualDWORDVectors (WinDBG: bp 0x408DBD)
- продолжаем исполнение программы (WinDBG: g)

Дампим параметры функции SDataWrap::AreEqualDWORDVectors:
// Первый вектор:
windbg> dd poi(@esp+8) L0n20
016456d0  00000057 00000030 00000075 00000031
016456e0  00000064 00000079 0000004f 00000075
016456f0  00000031 00000069 0000006b 00000033
01645700  00000034 00000070 00000031 0000007a
01645710  0000007a 00000034 00000020 0000003f
// Второй вектор:
windbg> dd poi(@esp+0x28) L0n20
01645728  000000fd 000000f9 000000c4 000000d8
01645738  000000a8 00000030 00000033 0000007f
01645748  0000009c 000000dc 00000020 00000064
01645758  000000d2 0000007a 000000a2 00000051
01645768  00000057 0000006e 0000004a 000000e0

Далее, немного меняя разные байты входного серийного номера, замечаем, что есть некоторые байты серийного номера, которые напрямую влияют только на один элемент вектора 2. Это приводит к мысли о том, что можно подобрать такой серийный номер, чтобы вектор 2 совпал с вектором 1 (то есть нужен bruteforce).

Шаг 6. Чтобы сделать bruteforce, нужно
- или разобраться и переписать с нуля код функции проверки лицензии
- или полностью декомпилировать функцию и вставить её к себе в код
- или внедриться (заинжектиться) внутрь EXE-шника и пользоваться его функциями как своими.

Последний вариант, конечно же, самый простой. Инжектить свою DLL-ку будем прямо в отладчике с помощью маленького простого "лоадера":
windbg> eb eip 6A 6C 54 B8 84 B4 6E 00 FF D0 54 50 B8 6A B3 6E 00 FF D0 83 C4 04 FF D0
который использует функции:
.text:006EB484 LoadLibraryW    proc near
.text:006EB36A GetProcAddress  proc near
Расшифровка кода лоадера:
6A6C         push   06C           ; 'l\0\0\0'
54           push   esp           ; L"l"
B884b46E00   mov    eax, 6EB484   ; LoadLibraryW
FFD0         call   eax           ; LoadLibraryW(L"l")
54           push   esp           ; L"l"
50           push   eax           ; HINSTANCE for l.dll
B86AB36E00   mov    eax, 6EB36A   ; GetProcAddress
FFD0         call   eax           ; GetProcAddress(hMod_l_dll, L"l")
83C404       add    esp,004       ; Clean stack
FFD0         call   eax           ; call l.dll!l()
То есть лоадер просто загружает нашу DLL'ку "l.dll" и вызывает у неё экспортируемую функцию "l".

Пример простой DLL'ки, которая самостоятельно проверяет "валидность" серийника:

bool check_license(const char *sEmail, const char *sKey)
{
    typedef bool (*pfn_check_license_t)(const char **sEmail, const char **sKey);
    union
    {
        DWORD_PTR pfnAddr;
        pfn_check_license_t pfn;
    } pfn = { 0x405EB0 };
    return (*pfn.pfn)(&sEmail, &sKey);
}

// Main function
extern "C" void __stdcall l()
{
    bool bResult = check_license("stas.zn@povolotsky.info", "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNA==");
    if (bResult) {
        MessageBoxW(0, L"Correct!", L"Correct!", MB_OK);
    }
}

Шаг 7. Собираем статистику, какой байт серийника на какие байты вектора 2 влияет. Для этого нам нужно:
- много раз вызвать функцию check_license, меняя каждый раз по одному байту серийника
- смотреть, какой получается вектор 2.

Для реализации этой функциональности у нас всё есть, кроме возможности получать значение вектора 2. Самый простой способ получить его значение - это поставить "хук" на вызов функции SDataWrap::AreEqualDWORDVectors. Такой хук установить можно установить с помощью следующей функции:
void SetCallHook(void** pCallInstructionAddr, void* pNew)
{
    DWORD* dwAddr = (DWORD*)((char*)pCallInstructionAddr + 1);
    DWORD dwRelativeOffs = (DWORD_PTR)((char*)pNew - ((char*)(pCallInstructionAddr)+(1 + sizeof(DWORD))));
    //*dwAddr = dwRelativeOffs;
    DWORD_PTR dwWritten = 0;
    WriteProcessMemory(GetCurrentProcess(), dwAddr, &dwRelativeOffs, sizeof(dwRelativeOffs), &dwWritten);
}

SetCallHook((void**)(ULONG_PTR)0x408DBD, (void*)&SDataWrap_AreEqualDWORDVectors_My);
Теперь при выполнении функции check_license вызовется наша функция SDataWrap_AreEqualDWORDVectors_My, которая запомнит, какое значение имел вектор 2.

Получаем следующие зависимости <байта серийника> на список <элементов вектора>:
0:  7
1:  0 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19
2:  3
3:  6 9
4:  6
5:  12
6:  10 18
7:  0 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19
8:  13 14 15
9:  8 16
10: 4 12
11: 1 10 18
12: 13
13: 0 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19
14: 10
15: 0 4 11 12
16: 14
17: 11
18: 2 5
19: 19
20: 17
21: 0 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19
22: 5
23: 16
24: 0 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19
То есть:
- байт 3 серийника влияет на элемент вектора: 7
- байт 3 серийника влияет на элементы вектора: 6 и 9
- байты 1,7,13,21,24 влияют почти на все байты вектора.

Также получаем обратную таблицу: <элемент вектора> на список <байт серийника>, которые влияют на этот элемент
0:  1 7 13 15 21 24 
1:  1 7 11 13 21 24 
2:  1 7 13 18 21 24 
3:  1 2 7 13 21 24 
4:  1 7 10 13 15 21 24 
5:  1 7 13 18 21 22 24 
6:  3 4 
7:  0 1 7 13 21 24 
8:  1 7 9 13 21 24 
9:  1 3 7 13 21 24 
10: 1 6 7 11 13 14 21 24 
11: 1 7 13 15 17 21 24 
12: 1 5 7 10 13 15 21 24 
13: 1 7 8 12 13 21 24 
14: 1 7 8 13 16 21 24 
15: 1 7 8 13 21 24 
16: 1 7 9 13 21 23 24 
17: 1 7 13 20 21 24 
18: 1 6 7 11 13 21 24 
19: 1 7 13 19 21 24
То есть:
- элемент 6 вектора можно подобрать с помощью байта 3 и 4 серийника.

Далее выбираем для каждого элемента вектора лучший номер байта серийника, с помощью которого мы будем его подбирать (лучший байт - тот, который влияет на меньшее количество элементов вектора).
0:  15 (4)
1:  11 (3)
2:  18 (2)
3:  2 (1)
4:  10 (2)
5:  22 (1)
6:  4 (1)
7:  0 (1)
8:  9 (2)
9:  3 (2)
10: 14 (1)
11: 17 (1)
12: 5 (1)
13: 12 (1)
14: 16 (1)
15: 8 (3)
16: 23 (1)
17: 20 (1)
18: 6 (2)
19: 19 (1)
То есть:
- элемент вектора 6 будем брутить с помощью байта 4 серийника (который влияет только на один элемент вектора)
- элемент вектора 1 будем брутить с помощью байта 11 серийника (который влияет на 3 элемента вектора)

И последний этап: выбираем, в каком порядке будем подбирать элементы вектора (сначала элементы, которые меняются одновременно с бОльшим количеством других элементов):
Order: 0 1 15 4 8 9 2 18 3 5 10 11 12 13 14 6 16 17 7 19

Шаг 8. Запускаем брутфорс:
- подбираем элемент 0 (с помощью 15 байта серийника)
- подбираем элемент 1 (с помощью 11 байта серийника)
- подбираем элемент 15 (с помощью 8 байта серийника)
- подбираем элемент 4 (с помощью 10 байта серийника)
...
- подбираем элемент 7
- подбираем элемент 19

Готово! Теперь мы знаем, какие байты серийника нужны, чтобы получить правильный вектор. А эти подобранные байты, переведённые в base64-строку, и есть правильный ответ.


Итоги и выводы:
  • Задание успешно решено, хотя алгоритм, по которому работает StrongBox.exe, так и остался загадкой.
  • Иногда для решения задачи не обязательно понимать алгоритм целиком, а достаточно воспользоваться методом "чёрного ящика" и разобраться, что имеется на входе/выходе алгоритма, после чего найти закономерность: как входные данные влияют на выходные данные.

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