Es basiert auf der Annahme, dass die üblichen RunPE APIs genutzt werden: CreateProcess, ZwUnmapViewOfSection, WriteProcessMemory, GetThreadContext, SetThreadContext, ResumeThread. Genauer: es hookt die ResumeThread und WriteProcessMemory API. Daher funktioniert es heutzutage nicht mehr wirklich zuverlässig, da viele "C0der" auf "ZwResumeThread" und "ZwWriteProcessMemory" Aufrufe umgestiegen sind
Zuerst starte ich den Prozess "Suspended":
Nun nutze ich die Tatsache aus, dass zumindest bis unter Vista die Kernel/NTDLL in allen Prozessen die gleiche Adresse hat und ermittele "WriteProcessMemory" und "ResumeThread" Adressen in der Kernel32.DLL meines Programms (die ich dann auf andere Programme "übertragen" kann). Diese speichere ich (zugegebenermaßen unschönerweise) in EDI / ESI, anstatt extra Variablen dafür anzulegen.Code:invoke CreateProcess,file_path,0,0,0,FALSE,CREATE_SUSPENDED,0,0,addr startup,addr proc_info
Nun warte ich in einem Loop:
bis die Kernel32.DLL in dem Zielprozess verfügbar ist (EDI enthält dabei die Adresse von ResumeThread API und es wird immer wieder versucht, daraus etwas zu lesen. Sobald das gelingt, heißt es, dass die DLL im anderen Prozess nun verfügbar ist)Code:invoke ResumeThread,proc_info.hThread invoke Sleep,1 invoke SuspendThread,proc_info.hThread invoke ReadProcessMemory,proc_info.hProcess,edi,addr temp,1,addr startup test eax,eax ;wenn rückgabe=0 dann ist gar nichts verfügbar
Dann kommt die "Debug" Funktion. Ihr Ziel ist es, die Stub / RUNPE Code solange ausführen zu lassen, bis sie den Code entpackt hat - aber nicht den letzen Schritt (ResumeThread) machen lassen.
Dafür werden "Breakpoints" aka EBFEs Platziert
zurst Speicher beschreibbar setzen:
und dasselbe, nur mit ESI statt EDI als Parameter. Ich errinnere nochmal - unschönerweise nutze ich Register als Variablen (weil ich diese ziemlich oft brauche und weil ich faul bin). ESI == WritePRocessMemory Adresse, EDI == ResumeThread Adresse.Code:invoke VirtualProtectEx,proc_info.hProcess,edi,4,PAGE_EXECUTE_READWRITE,addr temp
Ich platziere einen EBFE ähm Breakpoint auf ResumeThread, dann lese ich "Originalbytes" aus der Adresse von ESI==WriteProcessMemory und platziere schließlich auch da einen Breakpoint.
Nun warte ich in einer Schleife - einmal pro Sekunde pausiere ich und prüfe, ob das Programm an einem meiner BPs angehalten wurde:Code:;BPs platzieren invoke WriteProcessMemory,proc_info.hProcess,edi,offset BP1,2,addr temp test eax,eax jz error invoke ReadProcessMemory,proc_info.hProcess,esi,addr orig_bytes,2,addr temp test eax,eax jz error invoke WriteProcessMemory,proc_info.hProcess,esi,offset BP1,2,addr temp test eax,eax jz error
laufen lassen + anhalten
nun BPs prüfen:Code:invoke ResumeThread,proc_info.hThread invoke Sleep,1000 invoke SuspendThread,proc_info.hThread
das Prinzip ist simpel - sobald EIP von dem "debuggten" Prozess gleich ESI (== WritePrzessMemory) Adresse ist, weiß ich, dass nun zum ersten mal etwas in den Speicher der Exe geschrieben wird. Dank MSDN weiß ich auch, dass WriteProcessMemory Function (Windows) den lpBase Parameter an 2-ter Stelle hat. Also lese ich den ESP des "debuggten" Prozess aus, addiere eine 8 dazu (damit erhalte ich die Adresse des 2-ten Parameters auf dem Stack, plus 8 muss sein, weil 4 Bytes für Parameter1 draufgehen und 4 Bytes für die Rücksprungadresse) und lese nun die Zieladresse des ersten "WriteProzessMemory" Aufrufs. Und das ist halt in 99,9% aller RunPEs die ImageBase der "gecrypteten" Exe, die gerade geschrieben wird.Code:mov context.ContextFlags,CONTEXT_FULL invoke GetThreadContext,proc_info.hThread,addr context .if context.regEip==edi ;ist EIP==BP Adresse? mov eax,base jmp @f .elseif context.regEip==esi ;WriteProcessMemory ;originalbytes scheiben invoke WriteProcessMemory,proc_info.hProcess,esi,addr orig_bytes,2,addr temp ;Base auslesen: mov ecx,context.regEsp add ecx,8 invoke ReadProcessMemory,proc_info.hProcess,ecx,addr base,4,addr temp
Danach schreibe ich die originalbytes (die für den EBFE "Breakpoint" des WriteProzessMemory überschrieben wurden) und setze den Prozess fort.
Sobald die EIP von dem "debuggten" Prozess gleich EDI (== ResumeThread) ist, weiß ich, dass der RunPE Code nun alles lauffertig entpackt hat und den Code nun laufen lassen möchte. Damit ist die "Debugfunktion" quasi fertig - und springt zum RET (k.A warum ich das so hässlich gelöst habe, wahrscheinlich stand hier vorher noch irgendeine Fehlerbehandlunsroutine. Solche "hässlichen" Codestellen sind meistens überbleibsel von "Erweiterungen" oder "Codekürzungen")
Die Debug-Funktion gibt (in EAX) die "ImageBase" der nun "entcrypteten" Exe zurück.
Außerdem weiß ich, (solange sie nicht -1 ist), ist alles ok und die Stub hat alles "lauffertig" entcryptet. Nun bräuchte ich nur noch zu dumpen:
DumpChildProzess arbeitet mit CreateSnapshot32 und Process32First um die ganze Prozessliste durchzugehen. Dafür gibt es eher massig Beispiele in vielen Sprachen. Es vergleicht halt die ParentID mit der ID der Anwendung, die wir gereade "Decrypten". Hintergrund: viele Crypter haben ja diesen "integrierten" Bindermechanismus und starten mehrere Prozesse bzw. Trojka Crypter hat aufjedenfall mehrere gestartet und irgenwelche komischen Aktionen gemacht (so genau weiß ich es nun auch nicht mehr).Code:;debugge (lasse die Stub die Anwendung entpacken) invoke Debug cmp eax,-1 je quit ;so, an dieser Position heißt es: Zielanwendung ist entpackt, nun alle kinder dumpen und fixen invoke DumpChildProcess,proc_info.dwProcessId,eax quit: invoke Terminate_childs ret
Im wesentlichen ruft es "invoke DumpAndFix,base,proc_entry.th32ProcessID" für die Prozesse auf.
Dessen Aufgabe ist klar. Das Fixen muss sein, da wir den kompletten Speicherblock auf die Platte speichern und dann natürlich die ganzen Sections nicht mehr den FileAlignment haben (und damit verschieben/ändern sich die ganzen RawAddress/RawSize Werte).
Dazu wird der Prozess geöffnet:
und dann versucht, über den PE Header an die ImageSize dranzukommen (um zu wissen, wieviel tatsächlich gedumpt werden muss)Code:invoke OpenProcess,PROCESS_TERMINATE+PROCESS_VM_READ,FALSE,procid test eax,eax jz error_c
Warum hier 03C und 0x50 Werte verwendet werden, sollte aus dem PE-Header Format klar seinCode:read_pe: mov ecx, base add ecx, 03ch invoke ReadProcessMemory,child_handle,ecx,addr temp,4,addr temp2 test eax,eax jnz @f .... .... add ecx,50h ;image size auslesen invoke ReadProcessMemory,child_handle,ecx,addr imagesize,4,addr temp
dann wir der Speicherinhalt ausgelesen (zuerst extra dafür eigener Speicher alloziert):
der PE Header des ausgelsenen Prozess wird dann gefixtCode:;so, nun imagebase vorhanden+imagesize - auslesen und speichern invoke VirtualAlloc,NULL,imagesize,MEM_COMMIT,PAGE_READWRITE test eax,eax jz error_alloc mov memory, eax invoke ReadProcessMemory,child_handle,base,memory,imagesize,addr temp
und anschließend das ganze gespeichert:Code:;fixen invoke Fix,memory,imagesize
(der Loop ist eine quick&dirty Lösung für automatische Namensvergabe bei mehreren Childprozessen... einfach vorstellen als:Code:invoke CreateFile,offset dump,GENERIC_WRITE,FILE_SHARE_WRITE,0,CREATE_ALWAYS,
Nun noch etwas zum Fix:Code:i=0 while fileexist("dump"+str(i)+".exe": do i = i+1 filename = "dump" + str(i) + ".exe"
im wesentlichen werden hier die "originellsten" RunPE "Verunstaltungstricks" behandelt. Also 1) wird MZ wieder an den Anfang geschrieben (manche Crypter lassen das Weg bzw überschreiben es)
2) wird überprüft ob es einen PE Header gibt
3)in einem Loop werden die SectionHeaders durchlaufen und nun an die Dumpgrößte angepasst. Da wir eine gültige PE-Exe gedumpt haben, können wir davon ausgehen, dass die Sections auch "gültig" im Speicherplatziert waren (also SectionAlignment bzw alle VirtualAddress und VirtualSize Adressen waren ok). Daher brauchen wir auch nur die ganzen RAW Werte auf Virtual umzustellen und sind damit fertig:
Code:fixloop: mov eax,[edi].VirtualAddress mov [edi].PointerToRawData,eax add edi, sizeof IMAGE_SECTION_HEADER loop fixloop




Zitieren
