
У нас возник резкий рост количества вылетов Explorer из-за того, что указатель команд оказывался в пустоте.
0:000> r
eax=00000001 ebx=008bf8aa ecx=77231cf3 edx=00000000 esi=008bf680 edi=008bf8a8
eip=7077c100 esp=008bf664 ebp=008bf678 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
7077c100 ?? ???
Возможно, нам о чём-то скажет адрес возврата.
0:000> u poi esp
008bf6d4 test eax,eax
008bf6d6 je 008bf6b9
008bf6d8 xor edi,edi
008bf6da cmp dword ptr [esi+430h],edi
Странно, что мы исполняем код из какого-то места, не имеющего имени. Если приглядеться, то можно увидеть, что мы исполняем код из стека: esp
— это 008bf664
, то есть вызывающий проблемы код находится в стеке.
Кто исполняет код из стека?
Конечно, зловреды.
Давайте посмотрим, что пытается сделать это зловредное ПО.
Дизассемблирование кода рядом с последним известным хорошим кодом даёт нам следующее:
008bf6c4 call dword ptr [esi+214h]
008bf6ca inc dword ptr [ebp+8]
008bf6cd push edi
008bf6ce call dword ptr [esi+210h] ; здесь выполняется вызов в пустоту
008bf6d4 test eax,eax
008bf6d6 je 008bf6b9
008bf6d8 xor edi,edi
008bf6da cmp dword ptr [esi+430h],edi
008bf6e0 je 008bf70d
Похоже, полезная нагрузка сохранила указатели функций в esi+210
и esi+214
. Давайте посмотрим, что там. Вероятно, там полезная нагрузка хранит все свои цели вызова.
0:000> dps @esi+200
008bf880 1475ff71
008bf884 00000004
008bf888 76daecf0 kernel32!WaitForSingleObject
008bf88c 76daeb00 kernel32!CloseHandle
008bf890 7077c100
008bf894 76dada90 kernel32!SleepStub
008bf898 76db6a40 kernel32!ExitProcessImplementation
008bf89c 76daf140 kernel32!RemoveDirectoryW
008bf8a0 76da6e30 kernel32!GetLastErrorStub
008bf8a4 770d53f0 user32!ExitWindowsEx
008bf8a8 003a0043
008bf8ac 0050005c
008bf8b0 006f0072
008bf8b4 00720067
008bf8b8 006d0061
Да, здесь находится полезная нагрузка указателей функций. Похоже, этот зловред собирается чего-то дождаться, а затем или выйти из процесса, или удалить папку, или выйти из Windows. Эти байты после user32!ExitWindowsEx
похожи на строку Unicode, так что давайте сдампим их как строку:
0:000> du 008bf8a8
008bf8a8 "C:\Program Files\Contoso\contoso_update.exe"
Постойте-ка, что? Весь этот беспорядок наводит программа автоматического обновления Contoso?
Давайте внимательнее приглядимся к полезной нагрузке зловреда. Возможно, мы разберёмся, что происходит. Похоже, он использует в качестве оперативного плацдарма esi
, так что давайте начнём дизассемблирование с esi
.
008bf684 push ebp ; создание кадра стека
008bf685 mov ebp,esp
008bf687 push ebx ; сохранение ebx
008bf688 push esi ; сохранение esi
008bf689 mov esi,dword ptr [ebp+8] ; параметр
008bf68c push edi ; сохранение edi
008bf68d push 0FFFFFFFFh ; INFINITE
008bf68f push dword ptr [esi+204h] ; данные->hProcess
008bf695 lea ebx,[esi+22Ah] ; адрес пути + 2
008bf69b call dword ptr [esi+208h] ; WaitForSingleObject
008bf6a1 push dword ptr [esi+204h] ; данные->hProcess
008bf6a7 call dword ptr [esi+20Ch] ; CloseHandle
008bf6ad and dword ptr [ebp+8],0 ; count = 0
008bf6b1 lea edi,[esi+228h] ; адрес пути
008bf6b7 jmp 008bf6cd ; вход в цикл
008bf6b9 cmp dword ptr [ebp+8],28h ; ждали слишком долго?
008bf6bd jge 008bf6d8 ; тогда останов
008bf6bf push 1F4h ; 500
008bf6c4 call dword ptr [esi+214h] ; Sleep
008bf6ca inc dword ptr [ebp+8] ; count++
008bf6cd push edi ; путь
008bf6ce call dword ptr [esi+210h] ; DeleteFile
008bf6d4 test eax,eax ; Удалён ли файл?
008bf6d6 je 008bf6b9 ; Если нет (N), входим в цикл и пробуем снова
008bf6d8 xor edi,edi
008bf6da cmp dword ptr [esi+430h],edi ; данные->fRemoveDirectory?
008bf6e0 je 008bf70d ; Нет? Пропускаем
008bf6e2 jmp 008bf6f0 ; Входим в цикл для урезания имени файла
008bf6e4 cmp ax,5Ch ; Обратная косая черта?
008bf6e8 jne 008bf6ed ; Нет? Игнорируем
008bf6ea mov dword ptr [ebp+8],ebx ; Запоминаем место последней обратной косой черты
008bf6ed add ebx,2 ; Переходим к символу
008bf6f0 movzx eax,word ptr [ebx] ; Получаем следующий символ
008bf6f3 cmp ax,di ; Конец строки?
008bf6f6 jne 008bf6e4 ; Нет? Продолжаем искать
008bf6f8 mov ecx,dword ptr [ebp+8] ; Получаем место последней обратной косой черты
008bf6fb xor eax,eax ; eax = 0
008bf6fd mov word ptr [ecx],ax ; Завершаем строку на последней обратной косой черте
008bf700 lea eax,[esi+228h] ; Получаем путь (теперь без имени файла)
008bf706 push eax ; Push адреса
008bf707 call dword ptr [esi+21Ch] ; RemoveDirectory
008bf70d cmp dword ptr [esi+434h],edi ; данные->fExitWindows?
008bf713 je 008bf71e ; Нет? Пропускаем
008bf715 push edi ; dwReason = 0
008bf716 push 12h ; EWX_REBOOT | EWX_FORCEIFHUNG
008bf718 call dword ptr [esi+224h] ; ExitWindowsEx
008bf71e push edi ; dwExitCode = 0
008bf71f call dword ptr [esi+218h] ; ExitProcess
008bf725 pop edi
008bf726 pop esi
008bf727 pop ebx
008bf728 pop ebp
008bf729 ret
; Похоже, этот код не используется
008bf72a push ebp
008bf72b mov ebp,esp
008bf72d push esi
008bf72e mov esi,dword ptr [ebp+10h]
008bf731 test esi,esi
008bf733 jle 008bf746
...
Выполнив реверс-компиляцию обратно на C, получаем
struct Data
{
char code[0x0204];
HANDLE hProcess;
DWORD (CALLBACK* WaitForSingleObject)(HANDLE, DWORD);
BOOL (CALLBACK* CloseHandle)(HANDLE);
DWORD (CALLBACK* MysteryFunction)(PCWSTR);
void (CALLBACK* Sleep)(DWORD);
void (CALLBACK* ExitProcess)(UINT);
BOOL (CALLBACK* RemoveDirectoryW)(PCWSTR);
DWORD (CALLBACK* GetLastError)();
BOOL (CALLBACK* ExitWindowsEx)(UINT, DWORD);
wchar_t path[MAX_PATH];
BOOL fRemoveDirectory;
BOOL fExitWindows;
};
void Payload(Data* data)
{
// Ждём, пока процесс выполнит выход
data->WaitForSingleObject(data->hProcess, INFINITE);
data->CloseHandle(data->hProcess);
// 20 секунд пытаемся сделать что-то с файлом
for (int count = 0;
!data->MysteryFunction(data->path) && count < 40;
count++) {
Sleep(500);
}
if (data->fRemoveDirectory) {
PWSTR p = &data->path[1];
PWSTR lastBackslash = p;
while (*p != L'\0') {
if (*p == L'\\') lastBackslash = p;
p++;
}
*lastBackslash = L'\0';
RemoveDirectoryW(data->path);
}
if (data->fExitWindows) {
ExitWindowsEx(EWX_REBOOT | EWX_FORCEIFHUNG, 0);
}
}
Ага! Это не зловредное ПО, это деинсталлятор!
Скорее всего, загадочная функция — это DeleteFileW
. Она ждёт, пока будет выполнен выход из основного деинсталлятора, чтобы удалить двоичный файл.
На CodeProject есть страница, показывающая, как написать самоудаляющийся файл; похоже многие компании решили использовать этот код в своих деинсталляторах. (Не знаю, соблюдают ли они лицензионные требования этого кода.)
Ну ладно, а почему же происходит вылет? Что не так с DeleteFileW
?
Согласно файлу дампа, место, где должен находиться DeleteFileW
, содержит 7077c100
. Это указатель функции в какой-то загадочной DLL, которая не загружена. Как же так получилось?
Предположу, что функция DeleteFileW
создала обходной путь (detour) в деинсталляторе Contoso. Когда деинсталлятор пытался создать таблицу полезных функций, он получил не адрес DeleteFileW
, а адрес обходного пути. Затем он попытался вызвать из полезной нагрузки этот обходной путь, но поскольку обходной путь не установлен в Explorer (или если он есть, обходной путь находится в каком-то другом месте), в конечном итоге вызов выполнился в пустое пространство.
Ни инъецирование кода, ни создание обходных путей официально не поддерживаются. Возможно, кто-то добавил в деинсталлятор обходной путь, не понимая, что деинсталлятор инъецирует вызов обходного пути в Explorer. А может, обходной путь был инъецирован антивирусом. Или, возможно, обходной путь инъецирован собственным слоем совместимости приложений Windows. Как бы то ни было, результатом становился вылет Explorer.
Из-за этого люди вроде меня тратят много времени на изучение этих вылетов лишь для того, чтобы прийти к выводу, что их причиной стали другие люди, неправильно использующие систему.
Если вам нужно создать самоудаляющийся двоичный файл, пожалуйста, не используйте инъецирование кода в чей-то чужой процесс. Вот как можно удалить двоичный файл, не оставляя следов:
Создайте временный файл cleanup.js
со следующим содержимым:
var fso = new ActiveXObject("Scripting.FileSystemObject");
fso.DeleteFile("C:\Users\Name\AppData\Local\Temp\cleanup.js");
var path = "C:\Program Files\Contoso\contoso_update.exe";
for (var count = 0; fso.FileExists(path) && count < 40; count++) {
try { fso.DeleteFile(path); break; } catch (e) { }
WSH.Sleep(500);
}
Этот скрипт удаляет себя, а затем в течение двадцати секунд пытается удалить contoso_update.exe
. Запустите его командой wscript cleanup.js
и позвольте сделать свою работу. Никакого инъецирования кода, никаких обходных путей, всё задокументировано.