Появилась возможность немного пореверсить, разбор таска Artifact.
Win32 PE, не упакован. В строках есть "Base must be binary (MR_ALWAYS_BINARY defined in mirdef.h ?)", а в импорте "QueueUserAPC". Первое говорит о прилинкованной библиотеке Miracl. Второе - об асинхронных процедурах.
Делаем/применяем сигнатуры IDA для Miracl. В моем случае сигнатуры не сработали.
IDA находит _main по адресу 0x00403700, ее псевдокод:
hFile = CreateFileA("ctf-pass-file.txt", GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
ReadFile(hFile, g_pFileBuffer, 0x10u, &NumberOfBytesRead, 0);
...
SleepEx(1u, TRUE);
if (g_pDecisionFunc(g_pFileBuffer))
{
printf("fail!\n");
}
else
{
printf("you did it!\n");
...
szHash = GetHash();
printf("%s\n", szHash);
}
Создаем файл со строкой 16 символов, патчим переход по адресу 0x00403799, чтобы всегда сваливаться на ""you did it!". Запускаем, получаем какой-то хэш, как ответ не подходит.
Ставим брейк на 0x00403791 на g_pDecisionFunc(g_pFileBuffer). Запускам под отладчиком, Olly падает.
Комбинация SleepEx(xxx, TRUE) и QueueUserAPC в импорте как бы намекает. Брейк на QueueUserAPC, попадаем ___tmainCRTStartup (0x00414A34) -> j_SetupUserAPC (0x00418660) -> SetupUserAPC (0x0041C340) -> QueueUserAPC. По адресу 0x4014A0 находится наш APCProc.
.text:004014A0 push ebp ... .text:004014AD push offset sub_42D1E8 .text:004014B2 push offset unk_421000 .text:004014B7 call GetDataFromMiracl .text:004014BC add esp, 8 .text:004014BF push 3 .text:004014C1 call GetSyscallIndex ; ZwProtectVirtualMemory .text:004014C6 add esp, 4 .text:004014C9 mov [ebp+syscall], eax .text:004014CC push 3 .text:004014CE call GetNumberOfParam .text:004014D3 add esp, 4 .text:004014D6 mov [ebp+syscall_param_count], eax .text:004014D9 mov eax, [ebp+var_1C] .text:004014DC xor eax, 0FFFFFFFFh .text:004014DF mov [ebp+a4], eax .text:004014E2 mov [ebp+a5], 2000h .text:004014E9 lea ecx, [ebp+a7] .text:004014EC push ecx ; a7 .text:004014ED push 40h ; a6 .text:004014EF lea edx, [ebp+a5] .text:004014F2 push edx ; a5 .text:004014F3 lea eax, [ebp+a4] .text:004014F6 push eax ; a4 .text:004014F7 push 0FFFFFFFFh ; param1 .text:004014F9 mov ecx, [ebp+syscall_param_count] .text:004014FC push ecx ; param_count .text:004014FD mov edx, [ebp+syscall] .text:00401500 push edx ; syscall .text:00401501 call DoSyscall .text:00401506 add esp, 1Ch ...
Первый вызов по адресу 0x004014B7 расшифровывает/декодирует кусок кода, используя Miracl.
Следующие 3 вызова по адресам 0x004014C1, 0x004014CE и 0x00401501 соответственно подготавливают номер системного вызова, количество параметров и делает системный вызов.
Поиск системного вызова - 0x00405640 GetSyscallIndex(int nIndex). Внутри функции есть массив хэшей
Hashes[0] = 0x1232B8E2; Hashes[1] = 0xF70D3051; Hashes[2] = 0xEB7FEB5E; Hashes[3] = 0x2E8DB3FA; Hashes[4] = 0xD02ED5BC; Hashes[5] = 0x7659BF62; Hashes[6] = 0xF6A0F55F; Hashes[7] = 0x2C4E4F12; Hashes[8] = 0x64FACEBB;
и по адресу 0x00405697 вызов функции FindNtdllFuncByName, которая находит адрес функции по переданному хэшу. Внутри все просто, через PEB находится ntdll.dll, затем в директории экспорта перебираются все имена экспортируемых функций, вычисляется хеш, при совпадении возвращается адрес кода найденной функции.
Функция вычисления хеша:
int __cdecl GetHashOfFuncName(char *szFuncName)
{
int hash;
hash = 0;
while (*szFuncName)
hash = 51 * hash + (unsigned __int8)*szFuncName++;
return hash;
}
Пишем скрипт, который посчитает хэши всех функций ntdll.dll. Получить список экспортируемых функций можно, например, с помощью dumpbin.exe.
0 ==> 0x1232b8e2 ==> ZwAllocateVirtualMemory 1 ==> 0xf70d3051 ==> ZwReadVirtualMemory 2 ==> 0xeb7feb5e ==> ZwWriteVirtualMemory 3 ==> 0x2e8db3fa ==> ZwProtectVirtualMemory 4 ==> 0xd02ed5bc ==> ZwOpenProcess 5 ==> 0x7659bf62 ==> ZwQuerySystemInformation 6 ==> 0xf6a0f55f ==> ZwCreateThread 7 ==> 0x2c4e4f12 ==> ZwQueryInformationProcess 8 ==> 0x64facebb ==> ZwClose
После того, как адрес Zw-функции найден, GetSyscallIndex добавляет к адресу 1 и возвращает DWORD по полученному адресу - а это и есть номер системного вызова. Например, для ZwAllocateVirtualMemory, под W7 номер 0x13:
772E52D8 > B8 13000000 MOV EAX,13 772E52DD BA 0003FE7F MOV EDX,7FFE0300 772E52E2 FF12 CALL DWORD PTR DS:[EDX] 772E52E4 C2 1800 RETN 18
Но вернемся к APCProc. Далее происходит копирование расшифрованного/декодированного буфера на адрес 0x00401580 и передача туда управления. После выполнения расшифрованного кода, он затирается memset'ом.
.text:00401509 push 12EBh ; size_t .text:0040150E push offset sub_42D1E8 ; void * .text:00401513 mov eax, [ebp+var_1C] .text:00401516 xor eax, 0FFFFFFFFh .text:00401519 push eax ; void * .text:0040151A call _memmove ; dest = 00401580 .text:0040151A ; srd = 0042D1E8 .text:0040151A ; size = 000012EB .text:0040151F add esp, 0Ch .text:00401522 mov ecx, [ebp+var_1C] .text:00401525 xor ecx, 0FFFFFFFFh .text:00401528 mov [ebp+a4], ecx .text:0040152B mov [ebp+a5], 2000h .text:00401532 mov edx, [ebp+var_1C] .text:00401535 xor edx, 0FFFFFFFFh .text:00401538 mov [ebp+var_4], edx .text:0040153B push 1 .text:0040153D call [ebp+var_4] ; 00401580 .text:00401540 push 12EBh ; size_t .text:00401545 push 0 ; int .text:00401547 mov eax, [ebp+var_1C] .text:0040154A xor eax, 0FFFFFFFFh .text:0040154D push eax ; void * .text:0040154E call _memset
Вся основная работа происходит в расшифрованной функции 0x00401580. Здесь получается список процессов через ZwQuerySystemInformation, затем каждый процесс открывается ZwOpenProcess, в нем выделяется страница памяти через ZwAllocateVirtualMemory, декодируется перехватчик ZwReadVirtualMemory и пишется в выделенную память. Затем сплайсится ZwReadVirtualMemory.
Код хука (после последней инструкции записан PID защищаемого процесса, номера вызовов ZwQueryInformationProcess и ZwReadVirtualMemory):
.data:004332E0 ZwReadVirtualMemory_Hook proc near .data:004332E0 .data:004332E0 var_124 = dword ptr -124h .data:004332E0 nRetLength = byte ptr -108h .data:004332E0 ProcessInformation= byte ptr -104h .data:004332E0 var_F8 = dword ptr -0F8h .data:004332E0 var_8 = dword ptr -8 .data:004332E0 arg_0 = dword ptr 4 .data:004332E0 arg_4 = dword ptr 8 .data:004332E0 .data:004332E0 push ebx .data:004332E1 call getDelta .data:004332E6 add ebx, 3 .data:004332E9 mov edx, [esp+4+arg_0] .data:004332ED sub esp, 104h .data:004332F3 lea eax, [esp+108h+nRetLength] .data:004332F6 lea ecx, [esp+108h+ProcessInformation] .data:004332FA push eax .data:004332FB push 18h .data:004332FD push ecx .data:004332FE xor eax, eax .data:00433300 push eax .data:00433301 push edx .data:00433302 push edx .data:00433303 mov eax, [ebx+4] ; ZwQueryInformationProcess .data:00433306 call $+5 .data:0043330B add [esp+124h+var_124], 8 .data:0043330F mov edx, esp .data:00433311 sysenter .data:00433313 add esp, 1Ch .data:00433316 mov eax, [esp+108h+var_F8] .data:0043331A mov ecx, [ebx] ; pid .data:0043331C add esp, 100h .data:00433322 cmp ecx, eax .data:00433324 mov eax, [ebx+8] ; ZwReadVirtualMemory .data:00433327 pop ebx .data:00433328 jnz short call_ZwReadVirtualMemory .data:0043332A call $+5 .data:0043332F .data:0043332F crash_debugger: .data:0043332F pop ecx .data:00433330 push ecx .data:00433331 add ecx, 0Ch .data:00433334 mov [esp+8+var_8], ecx .data:00433337 mov edx, esp .data:00433339 sysenter .data:0043333B mov ecx, [esp+8+arg_0] .data:0043333F mov edx, [esp+8+arg_4] .data:00433343 cmp edx, 1 .data:00433346 jz short locret_43336A .data:00433348 push esi .data:00433349 mov esi, eax .data:0043334B mov eax, large fs:18h .data:00433351 mov eax, [eax+24h] .data:00433354 xor eax, 1234A965h .data:00433359 .data:00433359 loc_433359: .data:00433359 xor byte ptr (crash_debugger - 43332Fh)[edx+ecx], al .data:0043335C inc al .data:0043335E mov ah, al .data:00433360 shl ah, 1 .data:00433362 xor al, ah .data:00433364 dec edx .data:00433365 jnz short loc_433359 .data:00433367 mov eax, esi .data:00433369 pop esi .data:0043336A .data:0043336A locret_43336A: .data:0043336A retn 14h .data:0043336D ; --------------------------------------------------------------------------- .data:0043336D .data:0043336D call_ZwReadVirtualMemory: .data:0043336D call $+5 .data:00433372 add [esp+8+var_8], 8 .data:00433376 mov edx, esp .data:00433378 sysenter ; ZwReadVirtualMemory .data:0043337A retn 14h .data:0043337A ZwReadVirtualMemory_Hook endp .data:0043337A .data:0043337D .data:0043337D getDelta proc near .data:0043337D call $+5 .data:00433382 pop ebx .data:00433383 retn .data:00433383 getDelta endp
Перехватчик проверяет PID, при совпадении портит TEB, а также запрошенный буфер, что приводит к крэшу Olly.
Так как в обработчике нет ничего полезного, правим функцию 0x00401580, чтобы она больше не делала противоотладку: по адресу 0x00401597 пишем JMP 0x00402051. И попадаем на вторую часть функции.
Здесь декодируется 16 байт считанных из файла в _main и записываются обратно.
syscall = GetSyscallIndex(1); // ZwReadVirtualMemory
syscall_params_count = GetNumberOfParam(1);
DoSyscall(syscall, syscall_params_count, -1, g_pFileBuffer, pReadBuffer, 16, &a6);
for (l = 0; l < 16; ++l)
{
symb = pReadBuffer[l];
for (m = 0; m < 256; ++m)
{
if (m * symb % 257 == 1)
{
pReadBuffer[l] = m;
break;
}
}
}
syscall = GetSyscallIndex(2); // ZwWriteVirtualMemory
syscall_params_count = GetNumberOfParam(2);
DoSyscall(syscall, syscall_params_count, -1, g_pFileBuffer, pReadBuffer, 16, &a6);
Затем декодируется DecisionFunc и заполняется указатель g_pDecisionFunc.
signed int DecisionFunc(char *pFileBuffer)
{
signed int idx;
unsigned int current;
int value;
int aConstants[16];
aConstants[2] = 86356;
aConstants[3] = 86356;
aConstants[0] = 468517;
aConstants[1] = 56613;
aConstants[4] = 243006;
aConstants[5] = 254505;
aConstants[6] = 373182;
aConstants[7] = 32439;
aConstants[8] = 506585;
aConstants[9] = 271621;
aConstants[10] = 32439;
aConstants[11] = 207142;
aConstants[12] = 56613;
aConstants[13] = 506585;
aConstants[14] = 284318;
aConstants[15] = 3;
idx = 0;
while (1)
{
current = 3;
if (pFileBuffer[idx] > 0)
{
value = pFileBuffer[idx];
do
{
current = current * current % 0x7FFFF;
--value;
}
while (value);
}
if (current != aConstants[idx])
break;
++idx;
if (idx >= 16)
return 0;
}
return 1;
}
Используя этот код, пишет брутер в символов в диапазоне (-127, 128).
Получаем строку 'Hello, cracker!' + '\0'. Затем кодируем его по правилу из предыдущего листинга, получаем
19 1C BC BC 2C 6F F9 87 │ 7C 35 87 F5 1C 7C 94 00 ↓∟??,oщ╪|5╪х∟|"
Записываем эти 16 байт в ctf-pass-file.txt, кладем рядом с artifact.exe, запускам, получаем ответ
you did it! 1b0aacc9baf278a3f9e5bd0b7d746e22
Круто, терь понятно, что там за магия в SleepEx происходила. Я просто сдампил sbox и добрутил вторую часть.