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

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

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

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

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

Комментарии (0)