Нашего клиента донимали отчёты о вылетах, показывавшие, что его программа ломается на самой первой команде.
Я открыл один из дампов вылета: он оказался настолько странным, что отладчик даже не мог понять, что пошло не так.
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_READONLY
) и именно поэтому мы получаем нарушение прав доступа на запись.
На самом деле, мы выполняем запись в заголовок образа, а это довольно необычное поведение. Выглядит довольно подозрительно.
Если заглянуть в стеки, то мы увидим следующее:
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_READWRITE
. Плохой признак. Это походит на инъецирование зловредного кода, потому что для обычного кода крайне необычно быть 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
даёт понять, что указатель функции — это RegOpenKeyExW
, так что полностью вызов функции должен иметь вид
RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\systemconfig", 0, KEY_READ, &key);
Дальнейшее дизассемблирование показало, что если код успешно открывает ключ, то пытается считать из него какие-то значения. Я предполагаю, что код хранит своё состояние в systemconfig
.
Что ж, возможно, я смогу ускорить анализ, выполнив дамп строк и посмотрев, найдутся ли какие-то подсказки, позволяющие нам идентифицировать этот код. Вспомним, что команда !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
Отлично, похоже, это зловред, или, по крайней мере, он идентифицирует себя, как руткит. Поискав в Интернете название этого руткита, можно выяснить, что его исходный код публичен.
Хорошая новость для разработчика заключается в том, что он не виноват в проблеме. Плохая новость: поскольку дампы вылетов отправляются анонимно, мы никак не можем связаться с пользователями, чтобы сообщить о заражении зловредом.