Нашего клиента донимали отчёты о вылетах, показывавшие, что его программа ломается на самой первой команде.
Я открыл один из дампов вылета: он оказался настолько странным, что отладчик даже не мог понять, что пошло не так.
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
Отлично, похоже, это зловред, или, по крайней мере, он идентифицирует себя, как руткит. Поискав в Интернете название этого руткита, можно выяснить, что его исходный код публичен.
Хорошая новость для разработчика заключается в том, что он не виноват в проблеме. Плохая новость: поскольку дампы вылетов отправляются анонимно, мы никак не можем связаться с пользователями, чтобы сообщить о заражении зловредом.
ссылка на оригинал статьи https://habr.com/ru/articles/862116/
Добавить комментарий