Задача:
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.zipThis 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 много раз, задавая ей разные параметры и наблюдая за её работой)
Итак, проводим некоторое время, давая нормальные названия параметрам, локальным переменным, вложенным функциям.
После чего видим по коду первые манипуляции с серийным номером (или 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, так и остался загадкой.
- Иногда для решения задачи не обязательно понимать алгоритм целиком, а достаточно воспользоваться методом "чёрного ящика" и разобраться, что имеется на входе/выходе алгоритма, после чего найти закономерность: как входные данные влияют на выходные данные.
Полезные ссылки:
- Исходники моей библиотеки L.dll доступны тут:
https://github.com/Stanislav-Povolotsky/ReverseTasks/tree/master/20161104/hackquest.zeronights.org/task5 - Бинарник задания можно скачать отсюда
