Программа, которая падала на первой команде

от автора

Нашего клиента донимали отчёты о вылетах, показывавшие, что его программа ломается на самой первой команде.

Я открыл один из дампов вылета: он оказался настолько странным, что отладчик даже не мог понять, что пошло не так.

ERROR: Unable to find system thread FFFFFFFF ERROR: The thread being debugged has either exited or cannot be accessed ERROR: Many commands will not work properly This dump file has an exception of interest stored in it. The stored exception information can be accessed via .ecxr. ERROR: Exception C0000005 occurred on unknown thread FFFFFFFF (61c.ffffffff): Access violation - code c0000005 (first/second chance not available) 0:???> r WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work        ^ Illegal thread error in 'r' 0:???> .ecxr WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work 0:???>

Давайте посмотрим, что за потоки у нас есть.

0:???> ~ WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work    0  Id: 61c.12b4 Suspend: 1 Teb: 000000c7`9604d000 Unfrozen    1  Id: 61c.22d4 Suspend: 1 Teb: 000000c7`9604f000 Unfrozen    2  Id: 61c.1ab0 Suspend: 1 Teb: 000000c7`96051000 Unfrozen    3  Id: 61c.3308 Suspend: 1 Teb: 000000c7`96053000 Unfrozen    4  Id: 61c.2af0 Suspend: 1 Teb: 000000c7`96055000 Unfrozen    5  Id: 61c.2054 Suspend: 1 Teb: 000000c7`96059000 Unfrozen 0:???>

Любопытно, что они делают.

Будем переключаться на каждый из потоков, просто чтобы посмотреть, на какой они находятся команде:

0:???> ~0s WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work ntdll!RtlUserThreadStart: 00007ffa`bb16df50 4883ec78        sub     rsp,78h 0:000> ~*s          ^ Illegal thread error in '~*s' 0:000> ~1s 00000293`42074058 66894340        mov     word ptr [rbx+40h],ax ds:00007ff6`e4600040=1f0e 0:001> ~2s ntdll!ZwWaitForWorkViaWorkerFactory+0x14: 00007ffa`bb1b29c4 c3              ret 0:002> ~3s ntdll!ZwWaitForWorkViaWorkerFactory+0x14: 00007ffa`bb1b29c4 c3              ret 0:003> ~4s ntdll!ZwWaitForWorkViaWorkerFactory+0x14: 00007ffa`bb1b29c4 c3              ret 0:004> ~5s ntdll!ZwDelayExecution+0x14: 00007ffa`bb1af3f4 c3              ret

Кажущейся причиной вылета была недопустимая команда записи, а запись выполнял только поток 1. Давайте приглядимся, куда он пытается выполнить запись.

0:001> !address @rbx  Usage:                  Image Base Address:           00007ff6`e4600000 End Address:            00007ff6`e4601000 Region Size:            00000000`00001000 (   4.000 kB) State:                  00001000          MEM_COMMIT Protect:                00000002          PAGE_READONLY Type:                   01000000          MEM_IMAGE Allocation Base:        00007ff6`e4600000 Allocation Protect:     00000080          PAGE_EXECUTE_WRITECOPY Image Path:             C:\Program Files\Contoso\ContosoDeluxe.exe Module Name:            ContosoDeluxe Loaded Image Name:      ContosoDeluxe.exe Mapped Image Name:      C:\Program Files\Contoso\ContosoDeluxe.exe More info:              lmv m ContosoDeluxe More info:              !lmi ContosoDeluxe More info:              ln 0x7ff6e4600000 More info:              !dh 0x7ff6e4600000  Content source: 2 (mapped), length: 400 0:001> ln @rbx (00000000`00000000)   ContosoDeluxe!__ImageBase

Так, значит, мы выполняем запись в отображённый заголовок образа самого ContosoDeluxe. Это страница только для чтения (PAGE_READ­ONLY), и именно поэтому мы получаем нарушение прав доступа на запись.

На самом деле, мы выполняем запись в заголовок образа, а это довольно необычное поведение. Выглядит довольно подозрительно.

Если заглянуть в стеки, то мы увидим следующее:

0:001> ~*k     0  Id: 61c.12b4 Suspend: 1 Teb: 000000c7`9604d000 Unfrozen Child-SP          RetAddr               Call Site 000000c7`962ffd48 00000000`00000000     ntdll!RtlUserThreadStart     1  Id: 61c.22d4 Suspend: 1 Teb: 000000c7`9604f000 Unfrozen Child-SP          RetAddr               Call Site 000000c7`963ff900 00007ff6`e4600000     0x00000293`42074058     2  Id: 61c.1ab0 Suspend: 1 Teb: 000000c7`96051000 Unfrozen Child-SP          RetAddr               Call Site 000000c7`964ff718 00007ffa`bb145a0e     ntdll!ZwWaitForWorkViaWorkerFactory+0x14 000000c7`964ff720 00007ffa`ba25244d     ntdll!TppWorkerThread+0x2ee 000000c7`964ffa00 00007ffa`bb16df78     kernel32!BaseThreadInitThunk+0x1d 000000c7`964ffa30 00000000`00000000     ntdll!RtlUserThreadStart+0x28     3  Id: 61c.3308 Suspend: 1 Teb: 000000c7`96053000 Unfrozen Child-SP          RetAddr               Call Site 000000c7`965ff6a8 00007ffa`bb145a0e     ntdll!ZwWaitForWorkViaWorkerFactory+0x14 000000c7`965ff6b0 00007ffa`ba25244d     ntdll!TppWorkerThread+0x2ee 000000c7`965ff990 00007ffa`bb16df78     kernel32!BaseThreadInitThunk+0x1d 000000c7`965ff9c0 00000000`00000000     ntdll!RtlUserThreadStart+0x28     4  Id: 61c.2af0 Suspend: 1 Teb: 000000c7`96055000 Unfrozen Child-SP          RetAddr               Call Site 000000c7`966ffad8 00007ffa`bb145a0e     ntdll!ZwWaitForWorkViaWorkerFactory+0x14 000000c7`966ffae0 00007ffa`ba25244d     ntdll!TppWorkerThread+0x2ee 000000c7`966ffdc0 00007ffa`bb16df78     kernel32!BaseThreadInitThunk+0x1d 000000c7`966ffdf0 00000000`00000000     ntdll!RtlUserThreadStart+0x28     5  Id: 61c.2054 Suspend: 1 Teb: 000000c7`96059000 Unfrozen Child-SP          RetAddr               Call Site 000000c7`968ffcb8 00007ffa`bb165833     ntdll!ZwDelayExecution+0x14 000000c7`968ffcc0 00007ffa`b88f9fcd     ntdll!RtlDelayExecution+0x43 000000c7`968ffcf0 00000293`420a1efd     KERNELBASE!SleepEx+0x7d 000000c7`968ffd70 00000000`00000000     0x00000293`420a1efd

Поток 1 — это тот самый подозрительный поток, совершивший нарушение доступа.

Есть и ещё один подозрительный поток под номером 5, который находится в вызове SleepEx, выполняемом из того же подозрительного источника 0x00000293`420xxxxx. Вероятно, этот поток ждёт, пока что-то произойдёт, так что давайте взглянем на это.

Для начала посмотрим, из какого типа памяти происходит выполнение.

0:001> !address 00000293`420a1ee0  Usage:                  <unknown> Base Address:           00000293`420a0000 End Address:            00000293`420ca000 Region Size:            00000000`0002a000 ( 168.000 kB) State:                  00001000          MEM_COMMIT Protect:                00000040          PAGE_EXECUTE_READWRITE Type:                   00020000          MEM_PRIVATE Allocation Base:        00000293`420a0000 Allocation Protect:     00000040          PAGE_EXECUTE_READWRITE

Ой-ёй, PAGE_EXECUTE_READ­WRITE. Плохой признак. Это походит на инъецирование зловредного кода, потому что для обычного кода крайне необычно быть read-write. Но давайте продолжим надеяться, что всему этому есть невинное объяснение, и нам просто нужно его найти.

Давайте рассмотрим выполняемый код.

00000293`420a1ed9 add     rsp,30h 00000293`420a1edd pop     rdi 00000293`420a1ede ret 00000293`420a1edf int     3 00000293`420a1ee0 push    rbx 00000293`420a1ee2 sub     rsp,20h 00000293`420a1ee6 call    00000293`420a13e0 00000293`420a1eeb mov     qword ptr [00000293`420c0c78],rax 00000293`420a1ef2 mov     ecx,3E8h 00000293`420a1ef7 call    qword ptr [00000293`420b4028]                   ^^^^^^^^ МЫ ЗДЕСЬ 00000293`420a1efd call    00000293`420a13e0 // do it again 00000293`420a1f02 mov     rdx,rax 00000293`420a1f05 mov     rbx,rax 00000293`420a1f08 call    00000293`420a19d0 00000293`420a1f0d test    eax,eax 00000293`420a1f0f jne     00000293`420a1f22 00000293`420a1f11 mov     rax,qword ptr [00000293`420c0c78] 00000293`420a1f18 mov     qword ptr [00000293`420c0c78],rbx 00000293`420a1f1f mov     rbx,rax 00000293`420a1f22 mov     rcx,rbx 00000293`420a1f25 call    00000293`420a17f0 00000293`420a1f2a jmp     00000293`420a1ef2

Первые несколько команд до int 3 оказались концом предыдущей функции, поэтому можно начать наш анализ с push rbx.

    push rbx                        ; сохраняем регистр     sub rsp, 20h                    ; кадр стека     call 00000293`420a13e0          ; загадочная функция 1     mov  [00000293`420c0c78],rax    ; сохраняем ответ глобально  00000293`420a1ef2:     mov  ecx, 3E8h                  ; десятичное 1000     call [00000293`420b4028]        ; загадочная функция 2     ^^^^^^^^ YOU ARE HERE      call 00000293`420a13e0          ; загадочная функция 1     mov  rdx, rax                   ; возвращаемое значение становится param1     mov  rbx, rax                   ; сохраняем возвращаемое значение в rbx     call 00000293`420a19d0          ; загадочная функция 3     test eax,eax                    ; вопрос: завершилось ли успешно?     jne  00000293`420a1f22          ; нет: пропускаем     mov  rax, [00000293`420c0c78]   ; берём предыдущее значение     mov  [00000293`420c0c78], rbx   ; заменяем новым значением     mov  rbx, rax                   ; сохраняем предыдущее значение в rbx  00000293`420a1f22:     mov   rcx, rbx                  ; rcx = обновлённое значение в rbx     call    00000293`420a17f0       ; загадочная функция 3     jmp     00000293`420a1ef2       ; бесконечный возврат к началу цикла

Здесь очевидно одно: поток не выполняет выход. Это бесконечный цикл.

Сначала давайте разберёмся, сможем ли мы идентифицировать загадочные функции.

Самой простой, вероятно, будет загадочная функция 2, потому что она похожа на вызов импортированной функции.

0:001> dps 00000293`420b4028 L1 00000293`420b4028  00007ffa`ba258370 kernel32!SleepStub

Ага, загадочная функция 2 — это Sleep, а вызов — это Sleep(1000). В принципе, мы это знали из трассировки стека, но получить подтверждение всегда полезно.

Но давайте посмотрим поблизости от этого адреса, потому что это может быть частью более крупной таблицы указателей функций.

00000293`420b4000  00007ffa`baa59810 advapi32!RegCloseKeyStub 00000293`420b4008  00007ffa`baa596e0 advapi32!RegQueryInfoKeyWStub 00000293`420b4010  00007ffa`baa595a0 advapi32!RegOpenKeyExWStub  00000293`420b4018  00007ffa`baa5ab30 advapi32!RegEnumValueWStub 00000293`420b4020  00000000`00000000 00000293`420b4028  00007ffa`ba258370 kernel32!SleepStub 00000293`420b4030  00007ffa`ba250cc0 kernel32!GetLastErrorStub 00000293`420b4038  00007ffa`ba266b60 kernel32!lstrcatW 00000293`420b4040  00007ffa`ba25ff00 kernel32!CloseHandle 00000293`420b4048  00007ffa`ba254380 kernel32!CreateThreadStub

Бинго! Похоже, это таблица указателей импортированных функций.

Выглядит так, что, загадочная функция 1 вызывается для запуска всего, а затем снова вызывается в цикле, поэтому, наверно, она важна. Давайте разберёмся, что это такое.

00000293`420a13e0 mov     qword ptr [rsp+8],rbx 00000293`420a13e5 mov     qword ptr [rsp+10h],rsi 00000293`420a13ea mov     qword ptr [rsp+18h],rdi 00000293`420a13ef push    rbp 00000293`420a13f0 mov     rbp,rsp 00000293`420a13f3 sub     rsp,80h 00000293`420a13fa mov     rax,qword ptr [00000293`420bf010] 00000293`420a1401 xor     rax,rsp 00000293`420a1404 mov     qword ptr [rbp-8],rax 00000293`420a1408 mov     ecx,40h 00000293`420a140d call    00000293`420a8478 // загадочная функция 3

Это выглядит как типичная функция на C, а не ассемблерный код. После сохранения неизменяющихся регистров он создаёт кадр стека, а mov rax, [global] с последующим xor rax, rsp походит на canary /GS стека.

По крайней мере, здорово, что этот загадочный код компилировался с защитой от переполнения буфера стека. Осторожность никогда не бывает лишней.

Давайте взглянем на загадочную функцию 3.

00000293`420a8478     push rbx     sub  rsp, 20h     mov  rbx, rcx     jmp  00000293`420a8492  00000293`420a8483     mov  rcx, rbx     call 00000293`420aad50     test eax, eax     je   00000293`420a84a2     mov  rcx, rbx  00000293`420a8492     call 00000293`420aadb4     test rax, rax     je   00000293`420a8483     add  rsp, 20h     pop  rbx     ret  00000293`420a84a2     cmp  rbx, 0FFFFFFFFFFFFFFFFh     je   00000293`420a84ae      call 00000293`420a8c80     int  3  00000293`420a84ae     call 00000293`420a8ca0     int  3  00000293`420a84b4     jmp  00000293`420a8478

При обратной компиляции мы получаем

uint64_t something(uint64_t value) {     uint64_t p;     while (uint64_t p = func00000293420aadb4(value); !p) {         if (!func00000293420aad50(value)) {             if (value == ~0ULL) {                 func00000293420a8c80();             } else {                 func00000293420a8c80();             }             // NOTREACHED         }     }     return p; }

Похоже, код многократно вызывает функцию func00000293420aadb4.

00000293`420aadb4 jmp     00000293`420acf8c

Это выглядит как thunk компоновки с приращением. Что бы это ни было, но выглядит, как будто скомпилировали это в режиме отладки.

00000293`420acf8c     push rbx     sub  rsp, 20h     mov  rbx,rcx     cmp  rcx, 0FFFFFFFFFFFFFFE0h     ja   00000293`420acfd7     test rcx, rcx     mov  eax, 1     cmove rbx, rax     jmp  00000293`420acfbe  00000293`420acfa9     call 00000293`420b02c0     test eax, eax     je   00000293`420acfd7     mov  rcx, rbx     call 00000293`420aad50     test eax, eax     je   00000293`420acfd7  00000293`420acfbe      mov  rcx, [00000293`420c07f8]     mov  r8, rbx     xor  edx, edx     call [00000293`420b4298]     test rax, rax     je   00000293`420acfa9     jmp  00000293`420acfe4  00000293`420acfd7     call  00000293`420ac71c     mov   [rax], 0Ch     xor   eax, eax     add   rsp, 20h     pop   rbx     ret

Первоначальное сравнение с 0xFFFFFFFF`FFFFFFFE заставило меня заподозрить, что это malloc() или operator new, потому что эти функции начинаются с проверки избыточного размера распределения, чтобы избежать целочисленного переполнения.

И в самом деле, по сути, как следует из косвенного вызова функции, именно в этом и заключается её задача:

0:005> dps 00000293`420b4298 L1 00000293`420b4298  00007ffa`bb14cca0 ntdll!RtlAllocateHeap

Итак, значит, мы нашли malloc() или operator new.

Это позволит нам гораздо лучше понять загадочную функцию 1.

00000293`420a13e0     mov     [rsp+8], rbx     mov     [rsp+10h], rsi     mov     [rsp+18h], rdi     push    rbp     mov     rbp, rsp     sub     rsp, 80h     mov     rax, [00000293`420bf010]     xor     rax, rsp     mov     [rbp-8], rax      ; canary /GS     mov     ecx, 40h     call    00000293`420a8478 ; распределяем 64 байта     xorps   xmm0, xmm0     mov     ecx, 18h     mov     rdi,rax           ; сохраняем первое распределение     movups  [rax],xmm0        ; обнуляем первое распределение     movups  [rax+10h],xmm0     movups  [rax+20h],xmm0     movups  [rax+30h],xmm0     call    00000293`420a8478 ; распределяем 24 байта     xor     esi,esi     mov     ecx, 80h     mov     rbx,rax           ; сохраняем второе распределение     mov     [rax+0Ch], rsi    ; обнуляем второе распределение     mov     [rax+14h], esi     mov     [rax], esi     mov     [rax+4], 10h     mov     [rax+8], 1     call    00000293`420a84b4 ; загадочная функция 4     mov     [rbx+10h], rax    ; сохраняем результат     lea     ecx, [rsi+10h]    ; ecx = 0x10     mov     [rdi], rbx     call    00000293`420a8478 ; третье распределение     lea     ecx, [rsi+40h]    ; ecx = 0x40     mov     rbx, rax     mov     [rax+8], rsi      ; инициализируем третье распределение     mov     [rax], esi     mov     [rax+4], 10h     call    00000293`420a84b4 ; загадочная функция 4     mov     [rbx+8], rax     lea     ecx, [rsi+18h]    ; ecx = 0x18

Итак, эта функция сначала распределяет множество блоков памяти и инициализирует их.

Давайте сразу перейдём к месту, где она наконец делает что-то интересное.

    lea     rdx, [00000293`420bba90] ; LR"(SOFTWARE\systemconfig)"     lea     rax, [rbp-50h]     mov     [rdi+38h], rbx     mov     r9d, 20119h       ; KEY_READ     mov     [rsp+20h], rax     xor     r8d, r8d     mov     rcx,0FFFFFFFF80000002h ; HKEY_LOCAL_MACHINE     call    qword ptr [00000293`420b4010] ; RegOpenKeyExW     test    eax, eax

dps 00000293`420b4010 даёт понять, что указатель функции — это Reg­Open­Key­ExW, так что полностью вызов функции должен иметь вид

RegOpenKeyExW(HKEY_LOCAL_MACHINE,     L"SOFTWARE\\systemconfig", 0, KEY_READ, &key);

Дальнейшее дизассемблирование показало, что если код успешно открывает ключ, то пытается считать из него какие-то значения. Я предполагаю, что код хранит своё состояние в system­config .

Что ж, возможно, я смогу ускорить анализ, выполнив дамп строк и посмотрев, найдутся ли какие-то подсказки, позволяющие нам идентифицировать этот код. Вспомним, что команда !address сообщила нам, что блок памяти выглядит так:

0:001> !address 00000293`420a1ee0 Base Address:           00000293`420a0000 End Address:            00000293`420ca000

Попросим у расширения отладчика !mex найти в этом блоке памяти строки.

0:005> !mex.strings 00000293`420a0000 00000293`420ca000 ... 00000293420bbd10 system 00000293420bc1d4 H:\rootkit\r77-rootkit-master\vs\x64\Release\r77-x64.pdb

Отлично, похоже, это зловред, или, по крайней мере, он идентифицирует себя, как руткит. Поискав в Интернете название этого руткита, можно выяснить, что его исходный код публичен.

Хорошая новость для разработчика заключается в том, что он не виноват в проблеме. Плохая новость: поскольку дампы вылетов отправляются анонимно, мы никак не можем связаться с пользователями, чтобы сообщить о заражении зловредом.


ссылка на оригинал статьи https://habr.com/ru/articles/862116/