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 ![Wink](images/smilies/wink.png)
Zuerst starte ich den Prozess "Suspended":
Code:
invoke CreateProcess,file_path,0,0,0,FALSE,CREATE_SUSPENDED,0,0,addr startup,addr proc_info
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.
Nun warte ich in einem Loop:
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
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)
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 ![Wink](images/smilies/wink.png)
zurst Speicher beschreibbar setzen:
Code:
invoke VirtualProtectEx,proc_info.hProcess,edi,4,PAGE_EXECUTE_READWRITE,addr temp
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.
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.
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
Nun warte ich in einer Schleife - einmal pro Sekunde pausiere ich und prüfe, ob das Programm an einem meiner BPs angehalten wurde:
laufen lassen + anhalten
Code:
invoke ResumeThread,proc_info.hThread
invoke Sleep,1000
invoke SuspendThread,proc_info.hThread
nun BPs prüfen:
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
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.
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:
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
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).
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:
Code:
invoke OpenProcess,PROCESS_TERMINATE+PROCESS_VM_READ,FALSE,procid
test eax,eax
jz error_c
und dann versucht, über den PE Header an die ImageSize dranzukommen (um zu wissen, wieviel tatsächlich gedumpt werden muss)
Code:
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
Warum hier 03C und 0x50 Werte verwendet werden, sollte aus dem PE-Header Format klar sein ![Wink](images/smilies/wink.png)
dann wir der Speicherinhalt ausgelesen (zuerst extra dafür eigener Speicher alloziert):
Code:
;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
der PE Header des ausgelsenen Prozess wird dann gefixt
Code:
;fixen
invoke Fix,memory,imagesize
und anschließend das ganze gespeichert:
Code:
invoke CreateFile,offset dump,GENERIC_WRITE,FILE_SHARE_WRITE,0,CREATE_ALWAYS,
(der Loop ist eine quick&dirty Lösung für automatische Namensvergabe bei mehreren Childprozessen... einfach vorstellen als:
Code:
i=0
while fileexist("dump"+str(i)+".exe":
do i = i+1
filename = "dump" + str(i) + ".exe"
Nun noch etwas zum Fix:
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