PE-Packers/Crypters : Manual unpacking - parte 1
PREMESSA
LE INFORMAZIONI CHE TROVATE ALL'INTERNO DI QUESTO FILE SONO PER PURO SCOPO DIDATTICO.
L'AUTORE NON INCORAGGIA CHI VOLESSE UTILIZZARLE PER SCOPI ILLEGALI.
DIFFICOLTA'
scala : *=Novizio, **=Apprendista, ***=Esperto, ****=Guru
target: **1/2 ***
TOOLS USATI
* SoftIce 3.25
* ADUMP 1.0
* MAKEPE 1.30
* PE Browse
* HIEW 6.14
* ULTRAEDIT 6.10
* MASM 6.14
LETTERATURA
"Peering Inside the PE: A Tour of the Win32 Portable Executable File Format" (M. Pietrek), Microsoft Systems Journal 3/1994
"Windows 95 Programming Secrets" (M. Pietrek), IDG BOOKS, 1995
"Window Advanced Programming" (J. Ritcher), Microsoft Press, 1997
"Why to Use _declspec(dllimport) & _declspec(dllexport) In Code", MS KB Q132044
"Writing Multiple-Language Resources", MS Knowledge Base Q89866
"The Portable Executable File Format from Top to Bottom" (Randy Kath), MSDN
"The PE file format" (B. Luevelsmeyer), reperibile sulla rete
INTRODUZIONE
Un saluto a tutti,
come avrete constatato e' passato parecchio tempo dal mio ultimo tutorial ma causa lavoro e studio (hey ho una vita "reale" anche io ;) non ho avuto molto tempo per dedicarmi a scrivere le mie stupidaggini (che poi sono pure lunghe ;) Comunque come vi avevo promesso l'ultima volta eccomi di nuovo sull'argomento PE: in questo secondo capitolo della saga ;) cercheremo di sviscerare quello che si cela dietro l'oscura tecnica (o arte ;) del "manual unpacking". In tutto questo tempo ho potuto constatare che l'interesse intorno al PE e alle sue piu' o meno spinte manipolazioni ha incontrato un grosso successo di pubblico; sara' per via che i programmi sono sempre piu' spesso compressi o criptati, sara' perche' ci si e' resi conto che la conoscenza della struttura degli eseguibili win32 consente approcci nuovi al R.E. (leggi code iniection, process pacthing,api hooking, ecc.) ma noto che c'e' un crescente numero di persone che si interessano di PE. Prima di incominciare pero' e' necessaria una piccola premessa: in questo secondo tutorial la conoscenza di base delle strutture del PE e' data per scontata per cui nel caso non le abbiate chiare vi consiglio di leggere prima i testi in letteratura o il mio precedente tutorial (pubblicita' occulta ;), altra cosa.. e' anche necessaria una conoscenza di base sul funzionamento di alcuni dei meccanismi a basso livello del nostro adorato WinSlow (context,address spaces, MMF,ecc.) per cui non sara' una lettura propriamente leggera =)
Ok.. Let's go...
NOZIONI GENERALI
Il PE unpacking e' una tecnica relativamente giovane che pero' ha gia' una sua storia: certamente chi di voi frequenta il sito di +Fravia ricordera' i tutorial di Jazz, Quine, HalVar e quelli ormai famosi di Marigold su VBOX, ecc. e ricordera' anche che l'approccio seguito (battezzato da Marigold stesso "virginity restoring") altro non era che un'operazione di manual unpacking. L'idea di fondo e' molto semplice in se': per quanto possa essere complessa l'encryption del target, di una cosa siamo sicuri, se il programma deve poter essere eseguito,in memoria dobbiamo _necessariamente_ avere un'immagine "in chiaro" del codice/dati/risorse... detto questo e' naturale la conclusione: se possiamo salvare su disco le varie parti del codice, dei dati,ecc. e ricostruire la struttura del PE avremo un eseguibile perfettamente funzionante ,patchabile e/o dissasemblabile. Certo dalle parole ai fatti ci passa parecchio: innanzitutto sappiamo bene che alcune strutture del PE sono modificate durante la creazione di un processo (la IAT, la posizione delle section, ecc.) e che lo stesso codice/dati possono essere alterati (relocation.. cosa tristemente vera per le DLL), pero' sappiamo anche quando ed in base a quali regole questa alterazione avviene. Un'altro problema e' la separazione degli address spaces: secondo i dettami win32 ogni processo a 32bit possiede un suo proprio spazio di indirizzamento virtuale e non puo' accere a quello di un'altro processo: come possiamo allora leggere e salvare il contenuto delle section dell'eseguibile se non conosciamo il mapping delle pagine che utilizza e soprattutto non possiamo accerdevi?
La risposta e' che la premessa e' falsa, nel senso che la separazione dei processi non e' assoluta, o meglio, e' aggirabile in svariati modi, anche semplici (affermazione questa che dipende molto dal S.O. .. i.e. w9x != Nt 4.x,5): il primo che ci viene in mente e' quello di utilizzare del codice a ring0 che abbia accesso alla GDT,alla/e LDT, alle page tables/directory o cmq alle strutture utilizzate dal Memory Manager (VMM) e dal kernel (VWIN32) per creare i context: hey chi di voi ha detto SoftIce ? =) esatto i debugger di sistema come SoftIce, w386dbg o TRW devono necessariamente avere conoscenza dei context e degli address spaces per poter funzionare. E' poi possibile accedere alla memoria di un'altro processo anche da ring3 utilizzando api dedicate ai debuggers come ReadProcessMemory,GetThreadContext,SetThreadContext (questa e' ad exp la strada seguita da ProcDump).
Un'altro sistema potrebbe essere quello di iniettare (utilizzanto system hooks o altri sistemi) del nostro codice (una DLL o un MMF) nel processo target e condividere in tal modo il suo stesso memory context (e' il sistema che ho scelto di usare nei sorgenti del PE-Spy allegato a questo tutorial).
Procediamo con ordine: il metodo del system debugger (SoftIce su tutti) per dumpare l'image dell'eseguibile e' quella piu' usata e flessibile, perche' ci permette al contempo di seguire a runtime le operazioni che il nostro target compie e di avere un controllo preciso sul context e sull'uso della memoria dello stesso.
Comandi come map32, addr, pagein (SoftIce) sono un prezioso aiuto per il nostro compito e ci facilitano non poco la vita. L'unico problema e' dove salvare le aree che ci interessano: la risposta piu' ovvia sarebbe su un file.. ma come sappiamo non e' possibile aprire un file in un context (ad exp un nostro programma di dumping) ed utilizzarlo in un'altro perche' gli handle sono opachi rispetto ai processi; anche un rep movsb verso un buffer da noi allocato non sortirebbe effetto migliore per via della separazione dei context... un bel guaio... una soluzione potrebbe essere creare a runtime ,direttamente in SoftIce, uno snippet di codice che apra il file e usi WriteFile.. ma e' una bella seccatura assemblare il tutto a mano.. la soluzione sta nei Memory Mapped File: come sappiamo questi sono aree shared (in w9x allocate nell'arena > 80000000 < c0000000) e visibili in tutti i processi (in NT a patto di chiamare esplicitamente OpenFileMapping + MapViewOfFile) quindi possiamo usare un MMF per far comunicare il nostro target (via SoftIce) e il dumper.. ci bastera' usare il comando m (=move) e copiare i dati sul MMF e poi semplicemente salvare il contenuto di questo su file dal dumper stesso.
Questo all'incirca e' il principio di funzionamento della maggior parte dei dumper (SofDump,ADUMP,ecc.). Essi creano un MMF ci ritornano un address valido al blocco shared, che poi noi possiamo utilizzare come indirizzo destinazione nel comando m. Una variante a questo metodo ce la offre Icedump che e' una vera e propria patch di SoftIce: utilizzando lo spazio di codice "superfluo", Icedump aggiunge la possibilita' di salvare in un file direttamente da SoftIce. Ancora piu' semplice e' l'uso di TRW che include una serie di comandi per il dump su disco dell'image di un modulo PE (mkPE, PEDUMP). I vantaggi di questi approcci sono che abbiamo un controllo completo della vittima (possiamo steppare, esaminare il codice, i registri, ecc..) e possimo decidere con maggiore precisione quando agire e salvare il contenuto della mem su disco. Lo svantaggio e' che la maggior parte del lavoro (cut&pasting, rebuild della IT, del pe header, bla bla) e' a carico nostro almeno se escludiamo TRW.
Se decidiamo di non usare questi debuggers l'alternativa e' sostanzialmente Procdump. Giunto alla versione 1.4, Procdump ci permette di salvare su file ogni singola sezione dell'eseguibile, l'immagine intera o un blocco parziale. Per fare questo Procdump utilizza l'api toolhelp32/psapi e quella per i dbgs per esaminare il PE header cosi' come mappato a runtime, leggere il contenuto delle sections in un buffer locale (in entrambi i casi attraverso ReadProcessMemory) e quindi salvarlo su file.. non male =)
Benche' il tutto avvenga restando a ring3 il processo di dump e' efficace ed in piu' automatizzato, specialmente per quel che riguarda il rebuild della IT, e dell'header. Procdump offre inoltre una modalita' trace a r3 e/o r0 che affiancata ad un sistema di scripting lo rende molto flessibile. Gli svantaggi sono soprattutto legati all'implementazione stessa.. usando l'api di debug a r3.. procdump e' molto piu' limitato nel controllo dell'esecuzione (il thread hopping gli fa molto male =), ma soprattuto e' detectabile (fs:20,IsDebuggerPresent, test sul TF, ecc.). Un'ultima soluzione e' quella di creare del codice che una volta iniettato nel processo target poi funzioni da "server" di dumping e salvi il contenuto delle aree che ci interessano su disco. La cosa si ottine abbastanza semplicemente utilizzando i system hooks (SetWindowHookEx,ecc., ad exp. msg hooks) o altri sistemi di code iniection e quindi creando una window che funga da server ed esegua direttamente nell'address space della vittima le operazioni di dumping,ecc. che intendiamo compiere.Il vantaggio e' che condividiamo lo stesso context della vittima e che quindi nessun address da questo raggiungibile ci e' precluso. Gli svantaggi: beh siamo sempre a r3 e non abbiamo controllo se non parziale sull'esecuzione del processo. Ok, credo che questa parte disgustosamente teorica vi abbia gia' stancato.. ma come dico sempre la cosa migliore e' conoscere bene il nemico e le armi che abbiamo a disposizione.. non vorrete mika abbattere un f16 con una fionda ? =)
MANUAL UNPACKING
Dopo la doverosa parentesi maniaco-tecnika =) proviamo ad entrare nel vivo del discorso esaminando passo passo una sessione di unpacking. Come primo target utilizzeremo un programma criptato col PESentry: se qualcuno sta gia' mormorando: hey perche' proprio il PESentry e non qualkosa di piu' complesso tipo PE-Shield,PE-Crypt,ecc.. semplice di PESentry ne ho gia' ampiamente spiegato il funzionamento nel precedente tutorial e per di piu' potete fare riferimento anche i sorgenti.. i principi del manual unpacking sono gli stessi quale che sia il target tanto vale spiegarli usando un esempio chiaro per tutti. Come lavoro preliminare quindi utilizzate su un target il PESentry.. chesso' il buon vecchio notepad (io lo uso sempre quando studio i vari packers/crypters =) detto fatto.. ora abbiamo un bel eseguibile cryptato: la prima operazione che faccio quando approccio un target packed e studiarmi il PE header e le strutture connesse cosi' come appaiono su disco.. e' un'operazione fondamentale perche' ci consente di capire parecchie cose sul potenziale funzionamento del packer. Lanciate allora PeBrowse (o per i nostalgici della CUI,il PEDUMP) e cominciate ad esaminare l'header.. prima di tutto diamo un'okkiata all' Optional Header e alla section table:
ImageBase = 0x00400000 SectionAlign = 0x00001000
BaseOfcode = 0x00001000 EntryPoint = 0x0000BA04
BaseOfData = 0x00005000 ImageSize = 0x0000D000
Section Table
01 .text VirtSize: 00003953 VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00003A00
characteristics: E0000020
CODE MEM_EXECUTE MEM_READ MEM_WRITE
02 .bss VirtSize: 0000043A VirtAddr: 00005000
raw data offs: 00000000 raw data size: 00000000
characteristics: C0000080
UNINITIALIZED_DATA MEM_READ MEM_WRITE
03 .data VirtSize: 00000212 VirtAddr: 00006000
raw data offs: 00003E00 raw data size: 00000400
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
04 .idata VirtSize: 00000C9A VirtAddr: 00007000
raw data offs: 00004200 raw data size: 00000E00
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
05 .rsrc VirtSize: 00003000 VirtAddr: 00008000
raw data offs: 00005000 raw data size: 00002E00
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
06 .reloc VirtSize: 00002000 VirtAddr: 0000B000
raw data offs: 00007E00 raw data size: 00001600
characteristics: E0000040
INITIALIZED_DATA MEM_EXECUTE MEM_READ MEM_WRITE
umm.. non ci vuol molto a vedere che qualcosa non quadra =) l'EP e' spostato ad un RVA = BA04 proprio dentro la .reloc che presenta tra l'altro i flags executable ed writable, quest'ultimo attivo anche per ogni altra section (questo fatto e' una classica green light.. ricordate che in genere i packers gestiscono manualmente la base relocation, non e' pensabile usare WriteProcessMemory per applicare i fixups, mentre e' possibile che ne incontriate alcuni che utilizzano VirtualProtect per alterare runtime le protezioni di pagina). L'ImageSize sembra invece corretta, come del resto la coerenza tra RawSize,VirtualSize e VirtualAddress: quest'ultima considerazione e' molto importante perche' ci consente di distinguere in genere un packer da un crypter: a meno che il loader non sia particolarmente sofisticato lo spazio per il mapping delle sezioni verra' riservato cmq attraverso i valori VirtualSize e VirtualAddress specificati nell'header.. quindi se trovate una RawSize molto piu' piccola rispetto al dovuto, o addirittura nulla, ma una VirtualSize correttamente allineata con l'inizio della section seguente..beh con ogni probabilita' avete a che fare con un packer. Continuiamo esaminando le varie data directories:
EXPORT rva: 00000000 size: 00000000
IMPORT rva: 0000BD66 size: 00000C9A
RESOURCE rva: 00008000 size: 00002CE0
BASERELOC rva: 0000BDDA size: 0000000A
TLS rva: 00000000 size: 00000000
BOUND_IMPORT rva: 00000000 size: 00000000
IAT rva: 00000000 size: 00000000
dunque dunque.. nessuna funct esportata (normale di x gli .exe), niente TLS, bound imports, umm.. una base reloc con size 0x0A.. sospetto.. una import table che punta ancora una volta all'interno della .reloc.. molto molto sospetto! =) esaminiamo la IT:
Imports Table:
KERNEL32.DLL
Hint/Name Table: 0000BD9B
First thunk RVA: 0000BDA7
Ordn Name
0 GetProcAddress (IAT: 0000BDB3)
0 GetModuleHandleA (IAT: 0000BDC4)
solo due imports .. eheh una vocina mi dice che e' implementato un imports loader e che il loader gestisce anche la base relocation (dato che non e' probabile che le relocation siano stripped non essendo specificato nel FileHeader ed essendoci una sect .reloc di una certa dimensione... chiamatelo zen... o meglio chiamateli sorgenti =)
Se esaminiamo con PeBrowse le risorse ci accorgiamo subito che:
1) la sezione che la contiene non e' completamente criptata dato che i nodi che costituisco la res directory sono integri
2) le singole risorse invece sono criptate ad esclusione delle icone
Vabbeh.. ci siamo fatti un quadro della situazione.. ora e' il momento di procedere ad analizzare il loader a runtime...riassumiamo i nostri obbiettivi:
1) salvare le sezioni decrittate
2) cercare e salvare la IT originale (eventualmente ricostruirla se distrutta)
3) cercare e salvare la RELOC originale (eventualmente ricostruirla se distrutta) strettamente necessario solo per le DLL,DRV,ecc.
4) salvare le risorse decrittate
5) trovare l'EP originale
7) assemblare le sezioni salvate in un unico file
8) ricostuire un PE header coerente (ImageSize,EP,Section Headers,ecc.)
piccola tips.. ora volendo utilizzare sIce saremmo tentati di lanciare il programma con symbol loader.. in genere e' preferibile evitarlo cosi' non dobbiamo preoccuparci di check su fs:20 o cmq sul PDB (process database) che alcuni packer fanno per evitare process patchers o unpackers alla procdump... sara' sufficente sostituire il primo byte all'EP con 0xCC (=int3, con hiew l'operazione e' velocissima: load; F4 decode; F8 header; F5 entrypoint; F3 modify; F9 save ) e mettere un bpint 3.. quando lanceremo il programma avremo immediatamente il controllo.. quindi E EIP oldbyte, e continuiamo allegramente a steppare nel codice del loader. Per quel che riguarda TRW nelle ultime versioni (>0.72) include un nuovo comando TRnewTCB, che hookando services r0 per la gestione dei threads (VMMCreateThread -> Call_When_Thread_Switched) consente di breckare nell'entrypoint del main thread dell'applicazione. Ora la fase successiva e' comprendere cosa sta facendo il loader ed attenderlo al varco, ovvero quando ha finito di decrittare, e quindi procedere al dump delle singole sezioni. Per il dump utilizzeremo ADUMP (Icedump 4 se avete una ver < 3.24). Lanciamo quindi ADUMP e digitiamo R per avere le informazioni sul MMF creato by default:
STARTOFFS: 0x82E23000
ENDOFFS: 0x82F17240
LIMIT: 0xF4240 (1000000 )
CUROFFS: 0x82E23000
MAPFN: C:\WINDOWS\TEMP\ADump.log
MAPFSIZE: 0xF4240
ANFILTER: A..Z,a..z,0..9
ok.. la dimensione di base (limit) e' sufficiente considerata l'imagesize del nostro target; lo start address e' 0x82E23000.. lanciamo notepad.exe.. SoftIce poppa... sezione notepad!.reloc... ok... diamo un'occhiata all'header: map32 notepad ... ok come da copione.. continuiamo.. nella code window sIce abbiamo ora l'entrypoint del decryptor di PESentry.. chiaramente a questo punto dovremmo steppare nel codice e tentare di capire cosa accade ma dato che abbiamo i sorgenti commentati questa volta possiamo rilassarci e dedicarci solo ad alcune considerazioni generali.La mia idea e' di analizzare le azioni compiute da PESentry ed isolare dei patterns che possano essere applicati per la comprensione di ogni altro packers/crypters. Proviamo a riassumere il comportamento di PESentry in termini di patterns piu' o meno atomici:
1) setup/inizializzazione decryptor
1a) salvataggio dei flags / registri
1b) calcolo del delta -> imagebase
1c) installazione di un exception handler : questa va considerata una red light..
l'exception handler puo' essere usato solo per trappare eventuali errori
ma puo' anche essere indice di codice antidebug che usi ad esempio nested
exceptions + context->eip context->flags->tf, le varie interfaccie int3 di
sIce, ecc.. fate quindi sempre attenzione al codice dell'xHandler.
1d) inizializzazione pseudo IAT : e' evidente che il loader ha bisogno di
utilizzare delle api di WinZoZ, quindi operazione preliminare e' ottenere
gli addresses di queste: tipicamente si possono presentare due casi:
a) il loader importa tutte le funzioni necessarie nella sua IT
b) il loader importa solo GetModuleHandleA,GetProcAddress,LoadLibrary
e inizializza runtime una IAT interna. Questo e' il caso di PESentry,
Ci puo' essere un'ulteriore complicazione: seguendo il codice di
PESentry non troviamo nessuna call a GetProcAddress.. uhm, davvero
strano.. come fa ad importare le funzioni? esaminando il codice ci
accorgiamo che utilizza un export scanner: in pratica invece che
utilizzare GetProcAddress, una volta ottenuto l'hModule emula
GetProcAddress percorrendo la ExportTable->AddressOfNames e/o
ExportTable->AddressOfFunction (nel caso sia importato by ordinal)
2) decryptor loop : tipicamente questo e' table driven, ovvero utilizza una
table/struc che gli fornisce le informazioni su dove e cosa decrittare..
ad exp. PeSentry il loop do PESentry procede dalla prima sezione (0x1000)
e prosegue fino all'ultima (0xB000)
2a) handling separato delle strutture del PE (reloc, res,ecc.): nel caso
di notepad abbiamo ad exp un blocco "if RVA=0x8000 then" che dall'analisi
preventiva sappiamo essere le resourse directory
3) binding della IAT originale del programma. Anche qui possono presentarsi due
casi:
a) l'imports handler utilizza l'IT originale una volta decrittata (tipicamente
la sezione .idata)
b) l'imports handler possiede copia interna compressa/criptata distruttuta
una volta utilizzata
anche nel caso del binding della IAT originale si applicano le osservazioni
del punto 1d)
4) applicazione dei fixup se current imagebase != preferred imagebase
5) deallocazione risorse/memoria utilizzate / ripristino registri / stack
6) jmp host_original_entrypoint: qui le varianti non si contano... dal classico
jmp eax a pop EP_addr..ret e via discorrendo
nel caso di PESentry al termine del decryption loop e' lecito attendersi la seguente situazione:
1) le sezioni codice e dati sono decrittate
2) la sezione .rsrc e' stata decrittata
3) la sezione .idata e' stata decrittata
4) non sono ancora stati applicati i fixups (base relocation)
5) la IAT non e' stata ancora patchata
supponendo di non avere i sorgenti se esaminaste da SIce la sezione .idata e .reloc avreste cmq la conferma delle nostre supposizioni dato che sia le informazioni per i vari image_imports_descriptor che le informazioni di rilocazione sono presenti in chiaro.. se ne dedurrebbe quindi che il loader utilizza la IT e la RELOC originali.. cmq avere la IAT non patchata ci evita il problema di dover ricostuire manualmente i vari image_thunk_data mentre il fatto che che non sia ancora intervenuta nessuna rilocazione (altamente improbabile negli eseguibili, vista la separazione degli address spaces, ma cmq possibile nell'eventualita' che il loader rilochi maliziosamente l'image per rendere inutile ad exp. il full dump di ProcDump o TRW) ci garantisce che codice e dati statici sono nel loro stato originario. Ad ogni modo questo e' indubbiamente il momento migliore per salvare l'immagine delle sezioni, per cui da SoftIce digitiamo:
:map32 notepad
:m 401000 L 3a00 82e23000
:m 405000 L 1000 82e27000
:m 406000 L 1000 82e29000
:m 405000 L 3000 82e2b000
:m 40b000 L 2000 82e2f000
ovvero m sect_start_addr L sect_virtualsize(aligned) MMF_dest_addr nel caso le pagine interessate non siano ancora presenti dovrete forzare il MM di WinSlow a caricarle con pagein (se utilizzate Icedump non avrete di questi problemi).. naturalmente avremmo anche potuto fare il dump dell'intera immagine e poi rimuovere la sezione .BSS ma siccome questo tutorial lo sto scrivendo io vediamo di non rompere le ... ;)).. a questo punto l'unica informazione che ci manca e' l'entrypoint originale.. non ci resta che steppare in sice finche' incontriamo il fatidico jmp eax che riporta l'esecuzione al EP originale di notepad.. EP = 0x401000... next step...uscite da sIce e digitate in ADUMP:
:w c:\working\text_dump.bin 3a00 82e23000
Data written (14848) bytes.
:w c:\working\data_dump.bin 1000 82e27000
Data written (4096) bytes.
:w c:\working\idata_dump.bin 1000 82e29000
Data written (4096) bytes.
:w c:\working\rsrc_dump.bin 3000 82e2b000
Data written (12288) bytes.
:w c:\working\reloc_dump.bin 2000 82e2f000
Data written (8192) bytes.
ed avrete una copia dell'immagine decrittata...well done... pero' per ripristinare l'eseguibile e' necessario innanzitutto ricostruire l'header: in genere utilizzo un header "vergine" che mi son fatto allo scopo.. ma potete anche usare quello del target stesso. Cmq vogliate procedere quello che ci aspetta ora e' un bel lavoretto di taglio e cucito =) quindi aprite ultraedit o il vostro hex editor di fiducia ed inserite l'header: il mio ha size 400h essendo gia' allineato (hey "vedo che il tuo sforzo e' grosso come il mio" nd MelBrooks =) quindi a seguire tutti i file che contengono le sezioni:
ofs 0x400 -> text_dump.bin
ofs 0x3e00 -> reloc_dump.bin
ofs 0x4200 -> idata_dump.bin
ofs 0x5000 -> rsrc_dump.bin
ofs 0x7e00 -> reloc_dump.bin
come vedete l'offset a cui vengono inseriti e' allineato al file align (minimo 200h ma se volete ottimizzare i caricamenti in w98 o nt = 1000h).. inoltre nel caso probabile che il dump sia piu'lungo dei dati reali (exp la .reloc contiene ancora il codice del decryptor) potete ovviamente ridurli, dato che comunque lo spazio non presente su disco verra' allocato a load time da winZoz cosi' come specificato nei section headers. Fatto questo dobbiamo rendere coerente l'header almeno nelle sue parti fondamentali:
NumberOfSection = 6
ImageBase = 0x400000
Section Align = 0x1000
File Align = 0x200 (opzionale 0x1000)
ImageSize = 0xC000 (allineata section align x compatibilita' con NT)
EntryPoint = 0x1000 (rva)
una nota a margine merita il discorso PE CRC.. questo campo andrebbe aggiornato corretamente solo nel caso abbiate a che fare con eseguibili potenzialmete utilizzabili in NT come processi di sistema (services,ecc.) in quanto il loader di NT ne verifica la correttezza. Per calcolare il CRC avete a disposizione svariati metodi: potete utilizzare le funzioni deputate a questo scopo dalla stessa M$ nel modulo IMAGEHLP.DLL.. utilizzare un programma di ricalcolo del crc (quello di Rudeboy/PC va benissimo e sono disponibili anche i srcs), o lo stesso linker del MASM (switch -RELEASE).
quindi dobbiamo inserire i 6 section headers : .text, .data, .bss (questa ha come ovvio RawSize=0, VirtualSize=0x1000), .idata, .rsrc, .reloc (vi ricordo che sono consecutivi a partire dalla fine dell'OptionalHeader)... segue per questioni di spazio la dichiarazione della sola .text :
IMAGE_SECTION_HEADER STRUCT DWORD
Name 8 dup(?) | 2E54455854000000 | .TEXT
VirtualSize dd ? | 003A0000 | 00003A00
VirtualAddress dd ? | 00100000 | 00001000
SizeOfRawData dd ? | 003A0000 | 00003A00
PointerToRawData dd ? | 00040000 | 00000400
PointerToRelocations dd ? | 00000000 | 00000000
PointerToLinenumbers dd ? | 00000000 | 00000000
NumberOfRelocations dw ? | 0000 | 0000
NumberOfLinenumbers dw ? | 0000 | 0000
Characteristics dd ? | 20000060 | CODE+EXECUTE+READ
IMAGE_SECTION_HEADER ENDS
come vedete ho ripristinato i flags in modo coerente (writable = off) ma questa e' generalmente solo un'operazione di cosmesi e non influisce sul funzionamento (salvo il programma avesse gia' di suo il flag writable in questo caso ve ne accorgereste subito ;) .
Ora resta da aggiornare la datadir in modo che riporti correttamente le entry piu' importanti:
------------------------------------------------
* IMPORT rva: 00007000 size: 00000CA0 *
------------------------------------------------
l'import directory e' indubbiamente quella che ci crea piu' grattacapi essendo una struttura one-shot: infatti sappiamo che una volta che il loader ha patchato la IAT, la IT (di cui la IAT e' parte ma che non necessariamente risiede nelle stessa section) diventa inutile. E' quindi evidente che a runtime questa entry non e' necessariamente corretta, anzi lo e' di rado nei packers piu' recenti. Tuttavia la IT e' solo il mezzo non lo scopo, mi spiego meglio: come sappiamo quello che e' vitale per il programma non e' la IT ma proprio la IAT.. questa deve assolutamente trovarsi nella posizione originaria altrimenti le jmp/call [dword] con cui il linker ha risolto le chiamate alle funzioni importate non potranno funzionare. Nessuno ci vieta di spostare la IT (intesa come image_import_descritors) o di crearne una nuova, purche' i vari First_Thunks puntino ai corrispondenti nella IAT originale. Consideriamo ora PESentry: la IT utilizzata e' quella originaria che viene solamente criptata ma che cmq conserva inalterata la sua stuttura, sappiamo inoltre che se interveniamo prima della call HandleImports, la IAT e' i rispettivi image_thunk_data sono perfettamente integri.. e' dunque chiaro che l'entry IMPORT dovra' semplicemente puntare all'RVA a cui runtime troviamo l'inizio della IT (x notepad.exe RVA = 0x7000 = .idata) dato che i valori RVA negli array FirstThunk sono gia' corretti. Quanto alla size essa corrisponde alla dimensione fisica dei dati = 0x4ea0 - 0x4200 = 0x0ca0. Ora immagino qualcuno stara' pensando: "ma se la IT non si trova in una section separata come faccio ad identificarla e sopratutto a stabilirne le dimensioni ?" La risposta e' semplice: o seguite l'esecuzione fino trovare la funzione che parsa la IT (che solitamente presenta dei pattern di facile identificazione.. guardatevi il codice di HandleImports in PESentry) oppure ricercate in memoria le stringhe dei moduli/api (ad exp. S -CU 400000 L d000 "kernel32.dll") e percorrete a ritroso la struttura della IT (su qesto argomento parlero' piu' in dettaglio tra un attimo). Tuttavia quello di PESEntry e' uno dei casi piu' favorevoli.. come comportarsi allora nel caso il packer utilizzi per il binding un copia "usa e getta" (compressa e/o criptata) della IT? Anche in questo caso una possibile soluzione e' cercare di identificare le funzioni che si occupano del binding della IAT e verificare se e' possibile ottenerne una copia valida della IT. Ma supponiamo che il precedente tentativo sia fallito..non restebbe che ricostruire la IT a mano.. sappiamo solo che la IAT e' stata fixata, non conosciamo ne la sua posizione ne le sue dimensioni: immagino stiate dicendo: "cosa??? r u crazy?!" =).. eheh in realta' e' possibile utilizzare MKPE di GROM/UCF, che implementa lo stesso algoritmo di rebuild del procdump ma su base "statica".. qui invece ci interessa trattare proprio dell'estrema ratio.. il rebuild manuale della IT. Innanzitutto cominciamo col trovare la IAT:
0137:00401007 FF1548734000 CALL [KERNEL32!GetCommandLineA]
sappiamo che il linker risolve le imports come jmp/call [DWORD]..l'address 0x407348 corrisponde dunque all' image_thunk_data x GetCommandLineA all'interno della IAT. Ora col l'ausilio della grafica =)) cerchiamo di analizzare la IAT cosi' come ci apparirebbe in sIce:
0137:00407000 00007160 2A504F7F FFFFFFFF 000074E8 `q..OP*.....t..
| | | |
| +-------------+ |
| +---------------------------+
| | | |
| | | | IMAGE_IMPORT_DESCRIPTOR STRUC
+-|---|-----> OrigFirstThunk DD ?
| | +-> TimeDateStamp DD ?
| +-----> ForwarderChain DD ?
+---------> NameRVA DD ?
+-----------> FirstThunk DD ?
| IMAGE_IMPORT_DESCRIPTOR ENDS
|
0137:00407010 00007370 000070D0 320C1CA0 FFFFFFFF ps...p.....2....
.... omissis ....
0137:00407060 0000747C |00000000 00000000 00000000 |t..............
0137:00407070 00000000 00000000 00007C0A 00007BF8 .........|...{..
| |
modules descriptors terminator-+ +-start SHELL32 OriginalThunk
.... omissis ....
0137:004070C0 00007BBA 00007B2C 00007BE4 00000000 .{..,{...{......
^^^^^^^^
0137:004070D0 000075F6 000075EA 000075DA 00007604 .u...u...u...v..
.... omissis ....
0137:00407150 00007640 0000764A 00007654 00000000 @v..Jv..Tv......
^^^^^^^^
0137:00407160 000074D8 000074B4 000074A6 000074C6 .t...t...t...t..
.... omissis ....
0137:00407250 0000770E 000076FC 000076EE 00007812 .w...v...v...x..
0137:00407280 00007C20 00000000 BFF32CF0 BFF324A7 |.......,...$..
| |
Terminator COMDLG32 OriginThunk+ +-start SHELL32 FirstThunk
0137:00407290 BFF3215A BFF34517 BFF32497 BFF324AB Z!...E...$...$..
.... omissis ....
0137:004072C0 BFF324E7 BFF3227E BFF31882 BFF31C1D .$..~"..........
0137:004072D0 BFF324FB BFF324AF BFF31C11 00000000 .$...$..........
|
Terminator SHELL32 FirstThunk -+
0137:004072E0 BFF74904 BFF9CE2C BFF76E13 BFF77395 .I..,....n...s..
.... omissis ....
0137:00407340 BFF82941 BFF7799C BFF89F65 BFF7FB33 A)...y..e...3...
|
-- we land here ----------------> +- GetCommandLineA
0137:00407350 BFF76DF1 BFF8AECD BFF77654 BFF775BD .m......Tv...u..
0137:00407360 BFF9CDBE BFF773FB BFF77425 00000000 .....s..%t......
|
Terminator KERNEL32 FirstThunk -+
0137:00407370 7FDD8F0E 7FDC2B22 7FDD71DD 7FDC1220 ..."+..q. ..
0137:00407380 7FE0B03C 00000000 BFF64CBD BFF64D7C < .......L..|M..
0137:00407390 BFF61718 BFF6406A BFF62B96 BFF62B48 ....j@...+..H+..
.... omissis ....
0137:00407470 BFF64CE9 BFF61508 00000000 7FE84F95 .L...........O.
0137:00407480 7FE868A8 7FE85768 7FE81162 7FE84FA3 .h.hW.b...O.
0137:00407490 7FE8609C 00000000 6853004C 416C6C65 .`.....L.ShellA
|
+- Terminator COMDLG32 FirstThunk
0137:004074A0 74756F62 00090041 67617244 696E6946 boutA...DragFini
0137:004074B0 00006873 7244000B 75516761 46797265 sh....DragQueryF
0137:004074C0 41656C69 00080000 67617244 65636341 ileA....DragAcce
0137:004074D0 69467470 0073656C 6853004E 456C6C65 ptFiles.N.ShellE
0137:004074E0 75636578 00416574 4C454853 2E32334C xecuteA.SHELL32.
.... omissis ....
come vedete e' possibile rintracciare anche a runtime le strutture che compongono la import table.. ovviamente nel dump precedente sono ancora presenti tutti gli IMAGE_IMPORTS_DESCRIPTORs, IMAGE_IMPORT_BY_NAME,ecc... sapere quindi nomi e il numero dei moduli importati e' piu' facile.. ma facciamo finta che non ci siano e prendiamo in considerazione solo i vari FirstThunk: l'address di GetCommandLineA si trova ovviamente all'interno di quello relativo a kernel32.. scorrendo in avanti e poi indietro l'array fino ad incontrare i terminatori ed utilizzando il comando UNASSEMBLE di sIce possiamo determinare i nomi (a patto che siano caricati i simboli relativi of coz :P) di tutte le funzioni..non ci resta quindi che applicare pazientemente lo stesso principio per ogni FirstThunk ed avremo tutte le informazioni di cui abbiamo bisogno. Ora, a seconda del grado di distruzione della IT originale il nostro lavoro spaziera' dalla "semplice" rigenerazione degli elementi costitutivi dei FirsThunk fixati (che vi ricordo devono contenere gli RVA ai corrispondenti IMAGE_IMPORT_BY_NAME oppure gli ordinals: exp. 407348 = BFF89F65 < => .idata RVA 0x7000 + ofs "BC 00 GetCommandLineA" 0x528 = 0x7528) alla creazione ex novo di tutti gli elementi della IT. Se state pensando che tutto questo e' un lavoro lungo,palloso ed infame... beh avete pienamente ragione =) .. tuttavia come vi ho anticipato esiste MKPE che applica lo stesso metodo da noi usato manualmente per ricostruire la IT: quello che dobbiamo fornirgli e' solo l'elenco dei moduli utilizzati dal processo (ottenibile anche con l'utility ModList inclusa) e un dump dell'immagine del file .. poi lui cerchera' la IAT, processera' i vari FirstThunk trovando i moduli che esportano quelle funzioni ed infine rigenerara' gli IMAGE_THUNK_DATA,ecc. della IT (ovviamente se ancora esistente) o ne creara' una nuova appendendola al PE:
lanciate il TARGET.exe.. eseguite :
modlist > TARGET.mod
editate il file generato in modo da lasciare solo l'output relativo ai muduli utilizzati da TARGET.exe e quindi utilizzate MKPE con i seguenti parametri:
mkpe -s -i2 -lTARGET.mod unpacked.exe
se esiste una IT contenuta nell'immagine ma e' stata distrutta/cancellata
mkpe -s -i3 -lTARGET.mod unpacked.exe
se esiste la sola IAT e volete che venga creata una nuova IT da aggiungere al PE.
------------------------------------------------
* RESOURCE rva: 00008000 size: 00002CE0 *
------------------------------------------------
l'rva deriva direttamente dall'header cosi' come si presenta runtime in sIce (map32) questo e' naturale considerando che il base address deve essere necessariamente corretto altrimenti le chiamate alle varie FindResource,LoadBitmap,ecc. fallirebbero.. problemi posso sorgere se i vari image_resource_data_entry sono rilocati dal packer/crypter come ulteriore misura antidump (potete accorgervi di questa eventualita' utilizzando ad exp PESpy che consente di percorre la res hierarchy a runtime ed osservando gli offsets) nel qual caso dovrete procedere ad una ricostruzione della .rsrc, eventualita' cmq remota quando complessa e laboriosa. La size e' invece pari alla dimensione fisicamente occupata = 0x7ce0 - 0x5000 = 0x2ce0.
------------------------------------------------
* BASERELOC rva: 0000BDDA size: 0000000A *
------------------------------------------------
il ripristino delle base relocations e' un problema controverso al pari della Import Table. Come per quest'ultima le relocation posso essere gestite internamente dal loader del packer senza che vi sia necessita' che l'header contenga un riferimento corretto alla relocation directory. Tuttavia in questo caso le cose sono ancora piu' complesse perche' i fixups sono applicati nel codice, nei dati,ecc. senza nessun grado di indirezione (come possiamo considereare la IAT): se non ci e' possibile reversare le funzioni che le gestiscono ed estrapolare le informazioni da queste, una volta applicati i fixups ci troviamo di fronte a quello che possiamo definire "un fatto compiuto". Fortunatamente per gli eseguibili la possibilita' che sia necessario applicare la rilocazione e' sostanziamente nulla dato che ogni modulo che da origine ad un processo viene mappato nel suo virtual address space privato e cio' implica che nessun'altro modulo puo' trovarsi allo stesso virtual address della preferred imagebase. Di conseguenza il ripristino delle relocs per gli eseguibili non e' necessario ma opzionale tant'e' che spesso sono gli stessi programmatori a compilare i propri eseguibili con le relocation stripped.
Tutt'altro discorso invece per i moduli caricati dinamicamente (dll,drv,ecc.): in questo caso l'eventualita' di una collisone fra virtual address e quindi di una rilocazione e' da considerarsi la norma piu' che l'eccezzione specie in win9x dove esiste l'arena shared in cui vengono caricate quelle dll che sono condivise fra piu' processi (anche se il loader si sforza di caricarle nell'address space privato quando puo') ma che deve essere spartita anche con MMF, PIPEs,ecc. Se avete quindi necessita' di unpakkare una dll le cose si fanno difficili: a parte il suddetto reverse del loader l'unica' possibilta' e' realizzare una difference map creata a partire da dump effettuati ad imagebase diversi e tentare di estrapolare gli RVAs dei possibili fixups secondo questa logica:
imagebase delta = 10000
dump1: 0137:00401007 FF15 48734000 CALL [KERNEL32!GetCommandLineA]
dump2: 0137:00501007 FF15 48735000 CALL [KERNEL32!GetCommandLineA]
^^^^
| |^^^^^^^
| +- = 10000 -> aggiungi fixup entry : TYPE = 3
+----- e' una call -> RVA = 1009
ovviamente la cosa migliore sarebbe un programmino su misura che calcoli la diff. map e sappia discernere il "constesto" di ogni differenza distinguendo se il delta e' nella parte diplacement di una call,jmp,mov,ecc. e non garbage che si genera random o valori che cmq non hanno a che fare con i fixups. Fatte queste dovute considerazioni generali venieamo al PESentry: come gia' visto per le imports le base relocation non sono distrutte ma solo cryptate in loco, e per ripristinarle e' sufficente ripristinare l'RVA e la size.. a voi i calcoli =)
Bene se avete eseguito correttamente tutti i passaggi avrete finito il vostro primo target packed .. contenti ? ;))
Sebbene PESentry non sia' certo un target difficile le nozioni che abbiamo utilizzato sono applicabili a qualsiasi packer/crypter: anche se avremo a che fare con codice anti-debug, polimorfo, automodificante, ecc i principi del manual unpacking resteranno gli stessi quindi studiatevi bene il loader di PESentry..memorizzate i pattern di codice (specie quelli relativi a relocs e imports) perche' posso garantirvi che vi aiuteranno a comprendere il funziomento di molti packers in circolazione... (siccome so che siete diffidenti per natura ;) trovate in allegato a questo tutorial i dead listings dei loader di Neolite e ASPack =) ... ricordatevi che la ruota e' stata scoperta alcune miglialia di anni fa' e nessun programmatore si sognerebbe di reinventarla ogni volta ;)
NOTE FINALI
Nelle mie intenzioni questo tutorial avrebbe dovuto comprendere due ulteriori sezioni dedicate ai general unpackers e soprattutto a PROCDUMP e gli aspetti avanzati di questo (scripts,Brahma server,ecc..): tuttavia in questo periodo non ho molto tempo libero (esami,lavoro e chi piu' ne ha piu' ne metta...); mi sono inoltre reso conto che stavo persevernado un'altra volta nel mio solito errore di dilagare in una esposizione lunghissima al limite del logorroico.. per farla breve, ho deciso di spezzare in due parti questo documento e di scrivere quella dedicata a ProcDump&scripts a breve, nonappena passata la bufera esami... almeno spero =).
Ultima cosa :in allegato sono inclusi anche sorgenti del PESpy un'utility dedicata all'analisi del PE sia filebased che a runtime ed intesa come supporto per il dumping.. Allo stadio attuale la considero una sorta di esperimento per quel che vuol essere (almeno nelle mie intenzioni) un dumper made in ringZ3r0 (se avete commenti o volete contribuire alla cosa scrivetemi pure). I sorgenti sono commentati anche se la funzione di detect e scan della IAT non e' ancora implementata ed e' stato testato solo under w95c.. cmq lo sara' molto presto tempo libero permettendo =)
Concludo ringraziando velocemente Neural_Noise e GEnius che si sorbiscono le mie strampalate chiaccherate , +Malattia e Yan Orel per essere fonte continua di notizie e link preziosi.. tutti i membri di RingZ3r0 per essere semplicemnte unici =), un saluto poi a tutti i frequentatori di #Crack-it e in specie a Kry0, MoonShadow, T3X e xAONINO con cui tiro spesso ore assurde =)