Tworzenie kolejnych procesów i wstrzykiwanie różnych fragmentów złośliwego kodu (np. pierwszy utworzony proces odpowiada za zdekodowanie fragmentu kodu malware, utworzenie kolejnego procesu i załadowanie tam złośliwego kodu) jest pewnym utrudnieniem podczas analizy gdyż niektóre narzędzia (np. Immunity) nie są w stanie podłączyć się do nowo tworzonego procesu w odpowiednim czasie. Czasami uruchamianych jest kolejno 5-6 procesów zanim właściwy złośliwy kod (ten który nas interesuje najbardziej) zostanie uruchomiony.
Poniżej opiszemy jeden ze sposobów analizy takiego zachowania malware.
Zacznijmy od techniki podmiany zawartości procesu. Podmieniane są dane i kod wykonywalny tworzonego procesu. Sekwencja wywoływanych funkcji API może być następująca:
- CreateProcess() – złośliwy kod wywołuje tą funkcję w celu utworzenia nowego procesu. Istotne jest to, że CreationFlags ma ustawioną opcję CREATE_SUSPENDED (0x00000004).
- GetThreadContext() – pobierany jest kontekst nowo utworzonego procesu a dokładniej pierwszego wątku nowego procesu. Struktura CONTEXT zawiera ustawienia rejestrów procesora.
- WriteProcessMemory() – funkcja służy do kopiowania do nowego procesu złośliwego kodu. Malware może również wywoływać funkcje ReadProcessMemory().
- NtUnmapViewOfSection() – funkcja usuwa sekcje nowego procesu, gdyż do pamięci nowego procesu będzie kopiowany inny kod wykonywalny (w przypadku załadowania całego pliku PE a nie tylko wybranych sekcji lub fragmentów sekcji).
- VirtualAllocEx() – alokacja pamięci w nowo utworzonym procesie.
- SetThreadContext() – funkcja będzie wywołana do uaktualnienia zawartości rejestru EAX. Rejestr ten zawiera punkt wejścia (Entry Point) – adres pierwszej instrukcji złośliwego kodu który będzie wywołany przez ResumeThread().
- ResumeThread() – uruchamiany jest pierwszy wątek w nowym procesie – tym razem z nową zawartością.
Ponadto istnieją również inne techniki uruchamiania złośliwego kodu w kontekście innego procesu. Dla przykładu po utworzeniu nowego procesu w stanie SUSPENDED wywoływane są funkcje CreateFileMapping() oraz MapViewOfFile(). W tym momencie malware mapuje cały plik wykonywalny z którego był uruchomiony do pamięci procesu. Tworzony jest współdzielony obszar pamięci i nadawane są uprawnienia do wykonywania (PAGE_EXECUTE_READWRITE). Następnie kopiowane są tam dane ze złośliwym kodem. Jak możemy się domyślać do współdzielonej pamięci będzie miał również dostęp nowo utworzony proces. Poniżej sekwencja wywoływanych funkcji przez przykładowy malware:
Nowy proces może uzyskać dostęp do danych wywołując funkcje OpenFileMapping().
W obu opisanych przypadkach istotne jest to, że funkcja ResumeThread() uruchamia wątek nowo utworzonego procesu a tym samym złośliwy kod. Załóżmy, że chcemy śledzić uruchamiany kod za pomocą dubugger-a lub innego narzędzia (np. do monitorowania aktywność malware).
I tu z pomocą przychodzą narzędzia do przechwytywania funkcji systemowych. Na naszym blogu opisywaliśmy engine MiniHook. Tym razem użyjemy dwóch framework-ów ułatwiających analizę złośliwego kodu - Microsoft Detours oraz Capstone. Ten drugi nie jest obowiązkowy, ale to dobra okazja aby pokazać do czego służy.
Wspominaliśmy, że pierwszy wątek nowego procesu posiada strukturę CONTEXT, która zawiera stany rejestrów procesora. W rejestrze EAX znajduje się adres Entry Point (kodu który będzie uruchomiony po wznowieniu procesu). EBX to struktura PEB. W celu kontrolowania uruchamiania nowego procesu możemy zamienić pierwszą instrukcję wskazywaną przez adres w EAX. Jedna z metod to wstawienie instrukcji skoku bezwarunkowego wskazującego na siebie (czyli adres Entry Point). Jest to opcode 0xEBFE. W momencie uruchomienia nowego procesu w nieskończoność będzie wykonywana instrukcja JMP wskazująca na siebie. Wtedy możemy podłączyć debugger lub załadować do nowego procesu bibliotekę monitorującą aktywność malware. Następnie przywracamy pierwotne wartości i możemy kontynuować analizę.
Przechwycimy jedną funkcję – SetThreadContext() aby kontrolować malware i mieć pewność jaka wartość Entry Point znajduje się w strukturze CONTEXT. Możemy również przechwycić funkcję CreateProcess() a następnie zmodyfikować pierwsza instrukcję w Entry Point.
Trzy podstawowe kroki związane z przechwyceniem funkcji za pomocą Microsoft Detours:
- Zapisanie oryginalnego adresu funkcji przechwytywanej.
- Własna implementacja funkcji, które będzie wywoływana w momencie wywołania przez malware funkcji SetThreadContext(). BOOL WINAPI Nowy_SetThreadContext(HANDLE hThread, const CONTEXT *lpContext){
- Wywołanie funkcji DetourAttach() podmieniającej funkcje – instalującej przechwycenie (tzw. API Hooking).
BOOL (WINAPI *Oryginalny_SetThreadContext)(HANDLE hThread, const CONTEXT *lpContext) = SetThreadContext;
PBYTE pBuffer = new BYTE[8];
SIZE_T * rozmiar = 0;
SIZE_T * rozmiar2 = 0;
PBYTE infiniteloop = new BYTE[1]; //tablica zawierająca opcode EBFE (JMP)
infiniteloop[0] = 0xEB;
infiniteloop[1] = 0xFE;
//odczyt adresu Entry Point i zapisane pierwszych 8 bajtów do bufora
ReadProcessMemory(hProc2, (LPCVOID)lpContext->Eax, pBuffer, 8, rozmiar);
//nadpisanie dwóch bajtów wskazywanych przez adres umieszczony w EAX (Entry Point)
WriteProcessMemory(hProc2, (LPVOID)lpContext->Eax, infiniteloop, 2, rozmiar2);
//fragment związany z implementacją funkcji deasemblującej kod
csh handle;
cs_insn *insn;
size_t count;
char kod_asm[20];
if (cs_open(CS_ARCH_X86, CS_MODE_32, &handle) != CS_ERR_OK)
OutputDebugString(L"Capstone error");
count = cs_disasm(handle, (uint8_t *)pBuffer, 8, 0x1000, 0, &insn);
if (count > 0) {
size_t j;
for (j = 0; j < count; j++) {
//printf("%s\t%s\n", insn[j].mnemonic, insn[j].op_str);
sprintf_s(kod_asm, 20, "%s\t%s\n", insn[j].mnemonic, insn[j].op_str);
// W tym miejscu musimy zapisać do pliku zawartość kod_asm.
// Przykładowa zawartość:
// push ebp
// mov ebp, esp
// call 0x7b30
// warto zapisać opcode gdy będziemy przywracać oryginalne wartości z poziomu debugger-a.
}
cs_free(insn, count);
}
else
OutputDebugString(L"ERROR: Failed to disassemble given code!");
cs_close(&handle);
//wywołanie oryginalnej funkcji SetThreadContext()
return Oryginalny_SetThreadContext(hThread, lpContext);
}
DetourAttach(&(PVOID&)Oryginalny_SetThreadContext, Nowy_SetThreadContext);
Z poziomu naszej funkcji warto do pliku zapisywać:
- Adres Entry Point
- Oryginalne 2 pierwsze bajty
- Dodatkowo kilka pierwszych instrukcji oryginalnego kodu – my wykorzystaliśmy framework Capstone ale BeaEngine też polecamy.
- Funkcje ReadProcessMemory() i WriteProcessMemory() korzystają z uchwytu do procesu. Skąd mamy tą wartość? Możemy przechwycić inną funkcję systemową, która korzysta z tego uchwytu – np. WriteProcessMemory(). W EBX znajduje się adres struktury PEB - Process Environment Block – możemy sprawdzić do którego procesu należy odczytany adres tej struktury.
Edycja zmodyfikowanych dwóch bajtów za pomocą Immunity Debugger:
Źródła:
[1] http://research.microsoft.com/en-us/projects/detours/
[2] http://www.capstone-engine.org/
[3] http://www.autosectools.com/process-hollowing.pdf
[4] https://www.trustwave.com/Resources/SpiderLabs-Blog/Analyzing-Malware-Hollow-Processes/
[5] http://www.codereversing.com/blog/archives/65
[6] http://journeyintoir.blogspot.com/2014/12/prefetch-file-meet-process-hollowing_17.html
[7] https://msdn.microsoft.com/en-us/library/windows/desktop/aa366551%28v=vs.85%29.aspx