Pull to refresh

Любой достаточно продвинутый деинсталлятор неотличим от зловреда

Level of difficultyMedium
Reading time6 min
Views4K
Original author: Raymond Chen

У нас возник резкий рост количества вылетов 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 и позвольте сделать свою работу. Никакого инъецирования кода, никаких обходных путей, всё задокументировано.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 19: ↑18 and ↓1+17
Comments22

Articles