ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß
Û Û
² [x] ringZ3r0 Proudly Presents [x] ²
² ²
± ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÚ ±
± ±
³ PE-Packers/Crypters : Manual unpacking - parte 1 ³
Kill3xx
10 Giugno,1999
--==[ 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
corretamnte 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 =)
byz Kill3xx