ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß
Û Û
² [x] ringZ3r0 Proudly Presents [x] ²
² ²
± ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ±
± ±
³ PE-Crypters : uno sguardo da vicino al c.d. "formato" PE ³
Kill3xx
02 Marzo,1999
SORGENTI
--==[ 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: ***
--==[ TOOLS USATI ]==-------------------------------------------------------------------------
* TASM
* PROCDUMP 1.3
* PE Browse
* HIEW 6.01
--==[ 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
"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 ]==------------------------------------------------------------------------
Salve gente :)
Quello che vi presento questa volta e' il primo di una serie di tre tutorial sul formato PE
e soprattutto sull'utilizzo/abuso che ne fanno i vari PE-Crypters/Packers/Wrappers.
Come avete sicuramente notato negli ultimi tempi c'e' stata un'esplosione di crypters e
packers freeware/share/commerciali e sopratutto un loro massiccio impiego come parte della
protezione di un programma: questo si spiega con il fatto che il formato PE e' oramai piu'
o meno conosciuto e che soprattutto e' noto come il loader di win95/Nt crea un processo a
partire dall'immagine su disco (qui dobbiamo ringraziare i vari Pietrek,Shulman,ecc. per aver
aperto il vaso di Pandora;)
Nel primo (quello che state leggendo :)) trattero' del formato PE in generale e cerchero' di
commentare i sorgenti di un semplice pe-crypter da me scritto per l'occasione. Nel secondo
parleremo di unpacking "a mano" o assistito :) (Procdump,SoftDump,ecc.), ed infine nel terzo
se tutto filera' liscio vedremo come realizzare un decrypter e alcune tecninche anti-dumping.
Una premessa: nell'analizzare questo formato non mi dilungero' su quali siano le origini di
questo formato o su il significato di tutte le strutture e/o campi che lo compongono:
in primo luogo perche' potete trovare dettagliate informazioni nei testi riportati nella
"letteratua", in secondo luogo perche' molte di queste strutture/campi o non sono coerenti
fra i vari linker o sono obsolete, o semplicemente non sono fondamentali per i nostri scopi
(ricordate che parliamo di pe-crypters).
--==[ IL FORMATO PORTABLE EXECUTABLE ]==------------------------------------------------------
Per una visione di insieme del formato PE dobbiamo far ricorso alla fonte princiapale di
documentazione (l'unica prima dei testi di Pietrek e Kath):
winnt.h
Questo ominipresente file header (fornito con tutti gli SDK,DDK di M$) contiene la definizione
delle principali strutture e costanti che interessano il formato PE, quindi per qualsiasi cosa
dovremmo fare riferimento a questo file. La principale caratteristica di questo formato e' la
relativa facilita' con cui il loader puo' reperire le informazioni con cui "creare" un nuovo
processo, che si traduce poi in una maggior velocita' di caricamento/esecuzione di una
applicazione/modulo, e chi ha presente il formato NE sa cosa voglio dire!
Il layout di un exe PE e' tendenzialmente (si esatto proprio "tendenzialmente") questo:
+===================+ +00 -> dos header.[3C] ---+
| DOS (MZ) Header | |
+-------------------+ +40 |
| DOS Stub | |
+===================+ +00 -> inizio PE header <-+
| NT (PE) Header |
|- - - - - - - - - -| +04
| file-header |
|- - - - - - - - - -| +1A
| optional header |
|- - - - - - - - - -| +78
| data directories |
| |
+===================+ <- PE header + FileHeader.SizeOfOptionalHeader +
sizeOf(FileHeader)
| section headers |
| array |
~-------------------~
|......padding......|
~-------------------~
| |
| dati section2 |
| |
+-------------------+
| |
| dati section2 |
| |
+-------------------+
| .............. |
+-------------------+
| |
| dati section n |
| |
+-------------------+
Come vedete la prima struttura che incontriamo e' il DosHeader (buon vecchio dos ;)):
IMAGE_DOS_HEADER STRUC
e_magic DW ? ;+00 ; Magic number
......
e_lfanew DD ? ;+3C ; Address of PE header
IMAGE_DOS_HEADER ENDS
Di questa struttura cio' che maggiormente ci interessa sono
e_magic : questa DW contiene la signature che identifica un file eseguibile DOS valido ed
e' definita come : 0x05A4 che corrisponde alla stringa MZ (no dai!?? ;))
e_lfanew: questa DD invece e' invece la chiave di accesso alla nuovo header PE.
Si tratta di un RVA (ne parliamo dopo degli RVA) che punta all'inizio della
struttura NT Headers. Di conseguenza se volete ad exp. ottenere l'offset del
PE Header relativo all'inizio di file mappato in memoria dovrete prima leggere
questo valore e quindi sommarlo alla base del vista del Memory Mapped File
(da ora MMF per gli amici ;)
La presenza del campo e_lfanew si spiega con il fatto che di seguito al Dos Header possiamo
trovare uno stub ms-dos: questo altro non e' che quel mini-programma ci avverte
cordialmente :)) che l'eseguibile e' destinato all'ambiente Win32,OS/2, ecc.. Dato che questo
stub e' _opzionale_ si e' reso necessario fornire un pratico sistema al loader per evitare
di impaltanarsi nel caso lo stub non fosse linkato o di dimensioni diverse.
Ora guardiamo piu' in dettaglio la struttura IMAGE_NT_HEADERS:
31 0 31 0
+-------------------------------------------------------+ <--+
| SIGNATURE | MACHINE | # SECTIONS | |
+---------------------------+-------------+-------------+ | Signature +
| TIME/DATE STAMP | POINTER TO SYMBOL TABLE | | FileHeader
+---------------------------+-------------+-------------+ |
| NUMBER OF SYMBOL | NT HDR SIZE| IMAGE FLAGS | <--+
+=============+======+======+=============+=============+ <--+
| MAGIC |LMAJOR|LMINOR| SIZE OF CODE | |
+-------------+------+------+---------------------------+ |
| SIZE OF INITIALIZED DATA | SIZE OF UNINITIALIZED DATA| |
+---------------------------+---------------------------+ |
| ENTRYPOINT RVA | BASE OF CODE | |
+---------------------------+---------------------------+ |
| BASE OF DATA | IMAGE BASE | |
+---------------------------+---------------------------+ |
| SECTION ALIGNMENT | FILE ALIGNMENT | |
+-------------+-------------+-------------+-------------+ | Optional Header
| OS MAJOR | OS MINOR | USER MAJOR | USER MINOR | |
+-------------+-------------+-------------+-------------+ |
| SUBSYS MAJOR| SUBSYS MINOR| WIN32 VERSION | |
+-------------+-------------+---------------------------+ |
| IMAGE SIZE | HEADER SIZE | |
+---------------------------+-------------+-------------+ |
| FILE CHECKSUM | SUBSYSTEM | DLL FLAGS | |
+---------------------------+-------------+-------------+ |
| STACK RESERVE SIZE | STACK COMMIT SIZE | |
+---------------------------+---------------------------+ |
| HEAP RESERVE SIZE | HEAP COMMIT SIZE | |
+---------------------------+---------------------------+ |
| LOADER FLAGS | # INTERESTING RVA/SIZES | |
+===========================+===========================+ | <-+
| EXPORT TABLE RVA | TOTAL EXPORT DATA SIZE | | |
+---------------------------+---------------------------+ | |
| IMPORT TABLE RVA | TOTAL IMPORT DATA SIZE | | |
+---------------------------+---------------------------+ | |
| RESOURCE TABLE RVA | TOTAL RESOURCE DATA SIZE | | |
+---------------------------+---------------------------+ | |
| EXCEPTION TABLE RVA | TOTAL EXCEPTION DATA SIZE | | |
+---------------------------+---------------------------+ | |
| SECURITY TABLE RVA | TOTAL SECURITY DATA SIZE | | | Data Directory
+---------------------------+---------------------------+ | |
| FIXUP TABLE RVA | TOTAL FIXUP DATA SIZE | | |
+---------------------------+---------------------------+ | |
| DEBUG TABLE RVA | TOTAL DEBUG DIRECTORIES | | |
+---------------------------+---------------------------+ | |
| IMAGE DESCRIPTION RVA | TOTAL DESCRIPTION SIZE | | |
+---------------------------+---------------------------+ | |
| MACHINE SPECIFIC RVA | MACHINE SPECIFIC SIZE | | |
+---------------------------+---------------------------+ | |
| THREAD LOCAL STORAGE RVA | TOTAL TLS SIZE | | |
+---------------------------+---------------------------+ | |
| LOADER CONFIGURATION RVA | LOADER DATA SIZE | | |
+---------------------------+---------------------------+ | |
| BOUNDED IMPORTS TABLE | BOUNDED IMPORTS DATA SIZE | | |
+---------------------------+---------------------------+ | |
| IMPORT ADDRESSES TABLE | TOTAL IAT SIZE | | |
+---------------------------+---------------------------+ <--+ <-+
come vedete e' l'unione di due strutture , l'IMAGE_FILE_HEADER e IMAGE_OPTIONAL_HEADER,
piu' una DWORD, la c.d. signature: questo ci porta ad una prima considerazione ovvero
che gli headers del PE sono _CONSECUTIVI_ in memoria (o su disco) e quindi i campi possono
essere letti con semplicita' come offsets relativi all'inizio degli NT headers.
Ora analizziamo i campi piu' importatanti (per questioni di spazio non riporto la
dichiarazione degli headers, plz fate riferimento al file imghdr.inc)
* Signature : questo signature ha la funzione di identificare il tipo di eseguibile e il S.O.
(o sottosistema per NT) a cui e' destinato l'eseguibile; ad esempio :
IMAGE_OS2_SIGNATURE 0x0454E = NE = new executable = os/2 o win3x
IMAGE_NT_SIGNATURE 0x000004550 = PE00 = win9x / winNT
#IMAGE_FILE_HEADER#
* Machine: indica il processore target (ricordate che NT e' multiplatform)
* TimeDateStamp: time stamp usata per identificare la versione del modulo (ad esempio
nel meccanismo di import binding), ma spesso inconsistente. Non fidatevi!
* NumberOfSections: indica il numero di sezioni presenti, nonche' il numero di entries
nel section headers table. In teoria dovrebbe essere consistente con il numero
di sezioni presenti ma non fidatevi visto che il loader non pare curarsene.
* ImgFlags: indica il tipo di immagine (ad esempio eseguibile,dll) ed alcune caratteristiche
che la riguardano e che sono derivate dalla opzioni di compilazione/linking
(ad esempio se sono presenti le informazioni di debug,numeri di linea,ecc.
se e' stata impostata una imagebase fixed,ecc.
*SizeOfOptionalHeaders: indica la size degli optional headers (normamente 0xE0). La presenza
di questo campo e' la conseguenza della natura estensibile del formato PE.
#IMAGE_OPTIONAL_HEADER#
Questa struttura e' diciamo la piu' importante, in quanto raccoglie molte delle informazioni
vitali che verranno utilizzate dal loader per recuperare i dati dalle sezioni e quindi creare
il process in memoria. Anche in questo caso analizzeremo le piu' importanti in quanto come
vi ho gia' anticipato gli altri campi non appaiono essere consistenti da linker a linker o
tra versioni diverse di questi, o addirittura ignorati (rientrano in questa categoria anche
i vari SizeOfCode,SizeOfInitializedData,SizeOfUnitializedData):
* AddressOfEntryPoint: questo campo contine l'RVA dell'entrypoint del modulo, cioe' il punto
in cui il loader trasferira' l'esecuzione una volta terminata la fase di
caricamento/ inizializzaione: inevitabilmente punta all'interno di una sezione
che possiede i flag readable/executable (solitamente .text, CODE)
* BaseOfCode: indica l'RVA della prima sezione di codice (.text, CODE) ed utilizzata
presumibilmene dal loader nella fase di mapping per settare gli attributi di pagina
* BaseOfData: idem come sopra ma per la prima sezione dati
* ImageBase: questo campo e' di vitale importanza in quanto riporta la cosidetta "preferred
imagebase" ovvero l'indirizzo lineare nello spazio di indirizzamento privato
utilizzato dal linker per risolvere gran parte dei fixup nonche' la base a cui si
riferiscono tutti gli RVA: questo significa che se il loader di windowz deve
mappare l'immagine ad un indirizzo diverso sara' necessario applicare le base
relocations (parlero piu' in dettaglio delle implicazini della imagebase nella
sezione RVA e base rilocations).
* SectionAlignment: quando il loader di window mappa in memoria il file immagine utilizza i
MMF in modo che occupi uno blocco consecutivo di memoria nello spazio di
indirizzamento.Tuttavia per questioni di ottimizzazione nella gestione della
memoria virtuale (ad exp. nello share di porzioni di codice, nel caricamento di
pagine non presenti,ecc.) in w9x ogni sezione deve essere allineata ad un multiplo
della unita' minima gestita dal VMM : 1 pagina x86 = 4096 = 1000h (attenzione a non
confonderla con la granularita' di allocazione che e' di 64k). Questa limitazione
non si applica a NT (il minimo e' 32byte) ma non credo che vogliate degli eseguibili
"incompatibili".
* FileAlignment: questo campo e' un antico retaggio di quando windowz95 utilizzava il
filesystem FAT, e per ottimizzare i caricamenti si era pensato di allineare i dati
delle sezioni su disco ad un multiplo della grandezza di un settore (200h = 512 b).
Nel caso sia necessario i linkers zero-paddano (azz che espessione :) lo spazio non
utilizzato.
* SizeOfImage: ecco un esempio di come i membri della famiglia Win32 non comunichino molto!:)
questo campo riporta la grandezza dell'immagine una volta in memoria e quindi lo
spazio totale che il loader deve riservare per il suo caricamento. E' costituita
dalla somma dell'header + le VirtualSize delle sezioni presenti ed arrotondata
al multiplo piu' vicino della SectionAlignment. Quest'ultimo fatto e' stato fonte
di problemi per molti coders che avevano testato le loro creature solo con win95
dato che questo ignora l'allineamento continuando pacificamente mentre NT si
inkazza non poko se non trova un valore consistente.
Mi raccomando non fate inkazzare NT ;)
* SizeOfHeaders: il valore qui riportato altro non e' che la somma delle dimensioni dei vari
headers che precedono i dati delle sections
(DosHeader+Stub+NtHeaders,SectionHeaders):
in sostanza e' una sorta di puntatore ai rawdata dato che ImageBase+SizeOfHeaders
vi porta direttamente all'inizio della prima sezione, sia che stiate lavorando con
l'immagine di un processo in memoria,o su disco/MMF.
* CheckSum: altro esempio di differente comportamento fra 9x/Nt: questo valore rappresenta un
checksum dell'immagine del file PE, concepita per evitare che il loader carichi un
eseguibile corrotto e/o inconsistente, solo che questa verifica e' effettivamente
compiuta solo dal loader di NT e esclusivamente per file di sistema. Win9x ignora
totalmente questo campo tant'e' che i linker normalmente lasciano a 0 questo
campo. Nel caso vogliate modificare un PE che sapete essere di utilizzato da Nt a
livello di sistema (ad exp. un service, una dll, ecc.) e' auspicabile che aggorniate
correttamente il campo. L'algoritmo di calcolo e' ofcoz propietario M$ ma cmq
e' possibile utilizzare la funzione CheckSumMappedFile esportata dalla ImgHlp.dll
ormai molto in voga sui sistemi M$;)
* NumberOfRvaAndSizes: questo campo indica la dimensione dell'array di strutture
IMAGE_DATA_DIRECTORY che inizia dal campo DataDirectory. Attualmente e' fissato a
16 elementi ma non necessariamente per sempre ;)
* DataDirectory: questo pseudocampo in realta e' un array di strutture che rappresentano per
il loader una sorta di shortcut per accedere velocemente alle informazioni piu'
sensibili per la creazione/inizializzazione del processo: ogni entry (indici da
0..15) riporta l'RVA e la VirtualSize di specifiche informazioni/strutture:
le piu' importati sono:
0 : funzioni esportate dal modulo (ET)
1 : funzioni importate ma non bounded (IT)
2 : inizio della resource directory (resROOT)
5 : base relocations
9 : blocco thread local storage (TLS)
11: funzioni importate bound (BIT)
12: import addresse table (IAT)
Una cosa importante da dire e' che il loader fa sempre riferimento a questa
tabella per accedere ai dati del processo e non alla tabella dei section headers.
Se volete ad exp. reperire le informazioni su dove reperire le risorse (ad exp.
per evitare di criptarle) non utilizzate i nomi delle section tipo .rsrc visto che
questi sono _puramente_ convenzionali: nessuno ci garantisce cosa ci sia dentro o
che qualcuno li abbia rinominati (molti crypters lo fanno). Detto questo va da se
che i dati qui presenti devono essere ASSOLUTAMENTE coerenti o il programma si
piantera'inesorabilmente.
Di seguito al OptionalHeader inzia l'array di strutture IMAGE_SECTION_HEADER noto come
sections table: ogni elemento di questo array descrive i dati essenziali di una sezione
presente nel file di cui come al solito analizziamo i piu' importanti:
* SName: stringa di 8 byte con il nome della sezione (attenzione che non e' null termined)
* SVirtualSize: convenzionalmente contiene la dimensione fisica (vedi SizeOfRawData) dei dati
arrotondata ad un multiplo del section aligment. Questo campo in pratica dovrebbe
dire al loader quanto spazio riservare in memoria per questa sezione. Notate che
ho usato il condizionale perche' il loader sembra perfettamente ignorare questo
campo in presenza di una rawsize "valida" ed effetuare da se i calcoli per una
VSize corretta. Questo probabilmente spiega anche il fatto che la ImageSize venga
ignorata da w9x. Cmq e' anche perfettamente lecito avere una rawsize = 0 e una
VSize=0x1000,tant'e' che i packer sfruttano proprio questa caratteristica
cambiando la rawsize ma lasciando inalterata la VSize (a dir il vero la VSize puo'
anche sovrapporsi alla sezione successiva dato che e' cmq uno spazio solo
"riservato" e non necessariamente utilizzato) purche' ovviamente non ci sia vera
sovrascrizione :) Morale: il loader di win32 e' meno fesso del previsto, e scieglie
con oculatezza (in pratica e' probabile faccia max(VSize,RawSize) quali informazioni
siano piu' coerenti o se le calcola da se. Prendete esempio :))
* SVirtualAddress: tada'ecco un altro RVA :) .. questo permette di calcolare la posizione che
avra' la sezione una volta caricata in memoria dal loader. Come ormai avrete
capito deve essere maggiore, o un multiplo, del section alignment (che lo ricordiano
non puo' essere minore di 0x1000 per compatibilita' con 9x)
* SizeOfRawData: la dimensione fisicamente occupata dai dati su disco solitamente allineata
al file alignment. Questo campo puo' essere totalmente indipendente dalla VSize
ad exp. spesso incontrerete sezioni con rawsize = 0 ma che occupano spazio in
memoria (tipicamente sezioni con dati non inizializzati (BSS, TLS, ecc.), ma cmq
e'importante capire che almeno uno dei due valori dovra' contenere l'informazione
dello spazio da minimo da riservare in memoria. Tenete conto di questa anomalia
quando calcolate la ImageSize.
* PointerToRawData: l'offset "fisico" a cui troverete i dati della sezione
* SFlags: i flag che identificano le caratterestiche (codice,dati,ecc.) e quindi le i flags
e le protezioni di pagina che verranno applicate (writable,readable,ecc.)
Bene abbiamo analizzato gli headers che precedono i dati veri e propri delle sezioni..
resta solo da notare che in effetti tra la fine dell'ultimo section header e l'inizio dei
dati spesso si trova una "cavita'" ovvero un blocco non utilizzato ma presente per questioni
di allineamento. Queste cavita' presenti anche tra le sezioni possono essere sfruttate per
salvare codice e/o dati a patto che siano abbastanza grandi (i virus sono un classico esempio
di utilizzatori di queste cavita'). Fra tutte queste cavita' quella che piu' ci interessa
(miii che squallidi doppi sensi ;)) e' proprio quella fra la sections table e l'inizio
della prima sezione, in quanto e' li che possiamo introdurre una nuova sezione
seplicemente incrementando il campo FileHeader. NumberOfSection e accodando una struttura
IMAGE_SECTION_HEADER all'array.. ovviamente questo discorso e' valido se c'e' abbastanza
spazio (attenzione che per spazio va inteso quella tra la fine degli NTHeaders e l'RVA della
prima sezione e non solo lo spazio "fisico", che normalmente e' minore per via che di solito
file alignment < section alignment) alrimenti dobbiamo "necessariamente" appendere il nostro
codice/dati nell'ultima sezione del file (oddio non e'proprio necessario che sia l'ultima,
potremmo sciegliere una sezione qualsiasi, ma sicuramente e' molto piu' semplice che alterare
gli RVA di tutte quelle sucessive).
Ok, ora dovremmo trattare le strutture collegate alla IT,(la ET la trattero' nella terzo
tutorial), rilocazione e alle risorse ma credo che sia meglio che le vediate all'opera quando
commentero' il codice del crypter. Prima di tuffarci nel codice sara' pero' il caso che
parliamo dei concetti di ImageBase e RVA che come avrete constatato permeano tutta la
struttura del PE.
#ImageBase e Relative Virtual Address#
L'image base e' sostanzialmente l'indirizzo lineare a cui il loader mappera' l'immagine
dell'eseguibile quando crea un nuovo processo, o carica un modulo (DLL). Questo indirizzo,
riportato nel campo OptionalHeader.ImageBase, e' _specifico_ per ogni eseguibile ed e'
essenzialmente l'indirizzo utilizzato (o specificato da noi) dal linker per risolvere i
fixup. Tuttavia non sempre il loader puo' caricare l'immagine alla ImageBase specificata
(detta appunto "preferred"): questa eventualita' (chiamata "collisione"), e' sostanzialmente
impossibile per gli eseguibili (ovviamente se consideriamo il fatto che ogni processo win32
ha un suo spazio di indirizzamento "assolutamnete" privato.. per gli exe vedrete infatti
sempre specificata come imagebase 0x400000) ma e' altamente probabile per una DLL che invece
puo' essere caricata nello arena condivisa ( > 2gb e < 3gb in 9x; Nt non ha spazi r3 shared)
o cmq in un'area gia' impegnata da una precedente allocazione di memoria. Se si verifica una
collisione il loader per permettere all'esegubile di funzioanare sara' costretto ad applicare
la c.d. base relocation, a patchare cioe' tutti qui riferimenti assoluti che il programma
utilizza in modo che siano di nuovo coerenti. Considerati questi problemi si e' pensato di
"virtualizzare" gli indirizzi assoluti almeno delle strutture utilizzate dal loader rendendo
cosi' possibile referenziare le informazioni salvate dal linker a prescindere dalla imagebase:
ecco quindi nascere l'idea dell'RVA, che e' appunto un scostamento relativo alla imagebase:
quindi se volete leggere il valore di una DWORD che sta ad un RVA = 1234 basta che gli
sommiate l'imagebase ed otterrete il suo Virtual Address (VA) cioe' l'indirizzo nello spazio
di indirizzamento del processo:
VA = RVA + ImageBase
0x401234 = 0x1234 + 0x400000
Ovviamente questo ragionamento e' valido se l'eseguibile e' stato mappato dal loader, perche'
come sappiamo questo terra' conto del section alignment... ma se volessimo ottenere un offset
"fisico" (su disco,MMF) dato un VA ?
in questo caso dovremmo utilizzare le informazioni relative alla sezione che contiene
quell'indirizzo (ovviamente dobbiamo trovarla cercando nella section table verificando che
SVirtualAddress <= VA <= SVirtualAddress + SVirtualSize), relativizzare l'indirizzo rispetto
all'inizio di quella sezione sottraendo l'imagebase e VA della sezione, ottenendo cosi' un
offset che andremo a sommare all'offset fisico della sezione stessa:
RAW OFS = (VA - ImageBase - SVirtualAddress) + PointerToRawData
0x834 = (0x401234 - 0x400000 - 0x1000 ) + 0x600
Bene queto e' tutto per i concetti di base: ora passiamo al codice vero e proprio.
--==[ UN ESEMPIO PRATICO ]==-------------------------------------------------------------------
I sorgenti che vi presento sono un esempio di semplice scheletro di crypter che supporta sia
l'append che l'inserimento di una nuova sezione. Il crypter e' capace di gestire sia
sezioni codice, dati (esclusa .rdata), relocations info,import table, e risorse.
Quello che ancora non fa e' gestire tutta la casistica presente nei formati PE diciamo
"non convenzionali" (come al solito mamma M$ in testa!) , e cioe' forwarding , pre-binding
old-style e new-style, deferred dll, o la gestione del TLS. Altra mancanza di rilievo
(voluta visto che l'ho fatto in poko tempo e che sono sorgenti didattici.. ehhe non posso
mika svelarvi tutto del crypter che sto facendo :)) e l'assenza di forme di anti-dump,
anti-debug o anti-disasm. Ad ogni buon conto e' sufficientemente completo per iniziare a
capire il funzionamento del PE. Ovviamente non vi riporto qui tutti i sorgenti (fate
riferimento a pesentry.asm) ma solo alcuni passaggi diciamo piu' cruciali ed alcune scelte
d'implementazione.
open_file:
mov [lpszFileName],edi
call OpenFileEx ; open file with attribes ovveride
cmp eax, INVALID_HANDLE_VALUE
jz @@file_error
prima considerazione : la funzione OpenFileEx apre il file assicurandosi pero' di salvare
gli attributi del file nonche' data,ora di creazione,ecc.. mi sempra un modo + pulito di
operare :)
add eax,loader_len+(2000h) ; loader size + typical file align * 2
call CreateFileMapping,[hFile],NULL,PAGE_READWRITE,0,eax,NULL
or eax,eax
jz @@unable_to_map
mov [hFileMap],eax
call MapViewOfFile,eax,FILE_MAP_WRITE,0,0,0 ; map entire file
or eax,eax
jz @@unable_to_map
mov [Image_Base],eax
mov edi,eax
come vedete ho scelto di utilizzare i MMF per manipolare l'eseguibile, la ragione e' che in
questo modo posso gestire gli offset direttamente come scostamenti in memoria essendo sicuro
di avere il file mappato in modo lineare. Questo metodo di procedere e' in sostanza lo stesso
che utilizza il loader.. va notato pero' che i MMF hanno una loro piccola pecca, non possono
essere ridimensionati una volta creati.. cio' ci costringe a prevedere un blocco abbastanza
grande da contenere anche il nostro loader: sara' sufficente che sommiamo alla dimensione del
file la size del codice/dati nostro loader + 2 pagine. Se prevedete di realizzare un packer o
cmq di manipolare pesantemente il PE , vi consiglio (vero xOA ? :) di usare buffers allocati
con VirtualAlloc che sono modificabili senza denneggiare i dati gia' caricati (VirtualReAlloc).
Il puntatore ottenuto dalla MapViewOfFile costituisce ora la nostra ImageBase.
Una piccola nota: come vedete i commenti nei sorgenti sono in inglese.. ehhe ragazzi
sorry ma sono abituato cosi'.. l'inglese e' piu' conciso per certe cose :)
call GetNtHeader
or eax,eax ; on exit EDI = lpPEHeader
jnz @@invalid_pe
mov [lpPEHeader],edi
questa call esegue un check per verificare che effetivamente abbiamo a che fare con
un file pe eseguibile e nel caso affermativo torna il ptr al NTHeaders:
GetNtHeader:
push ebp
mov ebp,esp
push ebp ; save safe ESP
push offset @@on_PE_except ; our simple handler
push dword ptr fs:[0] ; save previous frame
mov fs:[0],esp ; establish our SEH frame
cmp word ptr [edi],IMAGE_DOS_SIGNATURE ; check MZ signature
jnz short @@not_PE
mov eax,[edi.e_lfanew]
add edi,eax
cmp dword ptr [edi],IMAGE_NT_SIGNATURE ; check PE signature
jb short @@not_PE
mov eax,dword ptr [edi.FileHeader.ImgFlags]
not al
or al,IMAGE_FILE_EXECUTABLE_IMAGE ; check for executable flag
jz short @@is_PE
or ax,IMAGE_FILE_DLL
jz short @@not_PE
@@is_PE:xor eax,eax
jmp short @@valid_pe
@@on_PE_except:
mov eax,[esp+8] ; get ERR structure
mov ebp,[eax+8] ; ERR + 8 = safe ESP
@@not_PE:
stc
sbb eax,eax
@@valid_pe:
pop dword ptr fs:[0] ; remove SEH frame
mov esp,ebp
pop ebp
ret
come vedete verifico le due signature e la presenza dei flag caratteristici degli
eseguibili.. l'unica cosa degna di nota oltre a questo e' la presenza di un exception
frame.. un modo decisamente piu' rapito che una serie di call a IsBadxxxxxPtr,ecc. per
verificare i puntatori. da qui in poi EDI sara' il puntatore agli NTHeaders
movzx eax, [edi.FileHeader.SizeOfOptionalHeader] ; size of optional header
lea eax,[edi+eax+18h]
mov [lpSectionTable],eax
qui otteniamo il puntatore all'inizio della Section table, che ci servira' per leggere
le info di ogni section e per aggiungere il nostro loader creando una sezione nuova
o espandendo l'ultima
mov eax,[edi.OptionalHeader.ImageBase]
; mov eax,400000h
mov [preferred_base],eax
mov eax,[edi.OptionalHeader.DataDirectory.(IMAGE_DIR_IMPORT).VirtualAddress]
mov [it_rva],eax
mov eax,[edi.OptionalHeader.DataDirectory.(IMAGE_DIR_EXPORT).VirtualAddress]
mov [et_rva],eax
mov eax,[edi.OptionalHeader.DataDirectory.(IMAGE_DIR_RELOC).VirtualAddress]
mov [reloc_rva],eax
mov eax,[edi.OptionalHeader.DataDirectory.(IMAGE_DIR_TLS).VirtualAddress]
mov [tls_rva],eax
mov eax,[edi.OptionalHeader.DataDirectory.(IMAGE_DIR_RESOURCE).VirtualAddress]
mov [rsrc_rva],eax
qui salviamo in variabili statiche allocate nel loader gli RVA delle principali directories
che poi ci serviranno sia per modificare gli RVA in modo che puntino alle nostre stutture sia
al loader per riaggiuistare le cose a runtime. (nota il mov 0x400000 e' li' nel caso vogliate
sperimentare con la rilocazione.. in questo caso dovrete cambiare l'imagebase del file da
procdump in modo da forzare il load ad un altro linear address.
Fatto questo si passa a cryptare le varie sezioni:
encrypt_objects:
movzx edx,[edi.FileHeader.NumberOfSections] ; number of section as counter
xor ebx,ebx
@@next_obj:
call IsEncryptableObj ; check if section is encryptable
or eax,eax
jz short @@proceed
mov dword ptr [crypt_flag],20202020h ; display status = skipped
jmp short @@no_encrypt
come counter per il loop utilizziamo il numero di sezioni riportate nell'optional header:
questo potrebbe essere una potenziale fonte di problemi visto che come ho detto il loader
non si fila molto questo valore. per cui in un file potrebbe essere maliziosamente
(si' perche' non vedo quale cacchio di linker si metterebbe a giocare con questo campo!??)
incoerente. So far so good.. continuiamo..
la call IsEncryptableObj verifica che la sezione che stiamo per elaborare sia effetivamente
criptabile: ad exp. la sezione .rdata e' una una di quelle che ci conviene evitare visto che
e' spesso utilizzata da M$ (mortacci a loro!) per inserirci la export, la TLS,ecc. altra
sezione da cui star lontano e' .edata che dovrebbe contenere esplicitamente la export table..
come criterio di verifica ho adottato un check "euristico" basato sugli rva presenti nella
DataDirectory, sulle rawsize delle sezioni, tranne che per .rdata che e' verificata in base
al nome :(
Ora qui si pone un interessante problema: ma se ad exp. la export table fosse contenuta
nella sezione .text (altra porkeria assolutamente possibile ma abbastanza remota per fortuna)
il crypter skipperebbe tutta la sezione.. risposta positiva !.. per ovviare al problema
bisogna identificare dove risiedono i blocchi non cryptabili in termini di RVA e quindi
costruirsi una mappa di quello che si deve effettivamente cryptare (puo' bastare un array
RVA + SIZE) che poi verra' usata sia dalla routine di encryption che dal loader. Ovviamente
in questo sorgente non e' implementato questo meccanismo (te pareva ;)) perche' avrebbe
complicato il codice che e' gia lungo di per se'..
@@proceed:
mov eax,[esi.SVirtualAddress]
mov [section_array.section_rva+ebx*8], eax ; save rva to loader table
mov ecx,[esi.SizeOfRawData]
mov [section_array.section_vsize+ebx*8], ecx ; save raw size
ok.. se siamo qui vuol dire che la sezione e' criptabile.. salviamo gli RVA e le dimensioni
delle sezioni cryptate in una tabella in modo che il loader sappia cosa abbiamo criptato...
quindi usiamo SizeOfRawData come grandezza del blocco da crittare
pusha ; save lpPEHeader
mov edi,[esi.PointerToRawData] ; calc pointer to raw data
add edi,[Image_Base]
cmp eax,[rsrc_rva]
jz short @@handle_res
call Encrypt
jmp short @@dummy_e
@@handle_res:
mov eax,offset ResEncryptCallBack
call EnumResources
@@dummy_e:
popa
inc ebx ; update loader table index
inc byte ptr [sections_num] ; update loader section counter
mov dword ptr [crypt_flag],53455920h ; display status = processed
ok.. questo codice mi pare autoesplicativo.. innanzitutto calcola il ptr ai dati in memoria
quindi verifica che quella che stiamo elaborando non sia la sezione delle risorse.. in caso
affermativo switcha alla routine di attraversamento dell'albero delle risorse (la spieghero'
piu' avanti..) quindi incrementa il contatore delle sezioni effetivamente criptate che poi
il loader usera'a runtime..
@@no_encrypt:
call show_stats ; display some stats
or [esi.SFlags],IMAGE_SCN_MEM_WRITE ; enable write bit always
add esi,IMAGE_SECTION_HEADER_ ; next section in table
dec edx
jnz short @@next_obj
ret
questa parte invece merita qualche commento perche' immagino qualcuno si stia domandando
perche' setto il flag writable per tutte le sezioni e non solo per quelle criptate..
la ragione e' semplice: la base rilocation! gia'.. sicomme saremo noi a gestirla al posto
del loader dobbiamo assicurarci che ogni sezione sia scrivibile alrimenti a runtime dovremmo
usare WriteProcessMemory per superare le protezioni di pagina ed applicare i fixup..
per inciso questo e' uno dei classici indicatori per sapere se un file e' cryptato con un
crypter che gestiste anche la .reloc
movzx eax,[edi.FileHeader.NumberOfSections] ; number of sections
inc eax ; +1
mov ecx,IMAGE_SECTION_HEADER_ ; * sizeOf(section_header)
mul ecx ;
add eax,[lpSectionTable] ; offset of object table
mov esi,eax
mov edx,edi ; + lpPEHeader
add edx,[edi.OptionalHeader.SizeOfHeaders] ; + SizeOfHeaders
cmp eax,edx
jg @@append_to_last
ecco qui un'altra porkeria ;) : questo blocco verifica nel modo piu' semplice se c'e'
abbastanza spazio tra la fine della section table e l'inizio delle raw section in caso
positivo il crypter creara' una nuova section. Se invece non dovesse essereci spazio optera'
per l'append. Ora vediamo in breve in meccanismo di aggiunta di una section:
sub esi,IMAGE_SECTION_HEADER_
mov [lpLoaderSection],esi7
inc [edi.FileHeader.NumberOfSections] ; add our section
ok.. otteniamo in ESI un puntatore allo spazio non utilizzato che segue l'ultima section;
e quindi incrementiamo il numero di sezioni nell'header
mov eax,[(esi-IMAGE_SECTION_HEADER_).SVirtualSize]
mov ebx,[(esi-IMAGE_SECTION_HEADER_).SizeOfRawData]
cmp ebx,eax
jle @dummy_sz
xchg eax,ebx
@dummy_sz:
add eax,[(esi-IMAGE_SECTION_HEADER_).SVirtualAddress]
call SectionAlign
mov [ldr_obj_VA],eax
mov [loader_rva],eax
ora dobbiamo calcolare l'RVA della nostra nuova sezione in memoria, come vedete il codice
utilizza la maggiore quantita' fra la VSize e SizeOfRawData allineata al section alignment
e la somma al VA dell'ultima sezione; il fatto di utilizzara max(VSize,RawSize) e' quello
che io chiamo safe programming.. come dire meglio prevenire che curare ;)
xchg dword ptr [edi.OptionalHeader.AddressOfEntryPoint],eax
mov [original_erva],eax
calcolato l'RVA della nostra sezione abbiamo anche l'RVA del nuovo entrypoint, dato che si
presuppone che l'inizio del vostro codice coincida con l'inizio dei dati nella nuova sezione
(in caso contrario dovrete solo sommarci lo scostamento), quindi lo scriviamo nell'header
assicurandoci pero' di salvare il vecchio entrypoint che servira' poi al loader per restituire
il controllo al programma una volta decrittato
mov eax,loader_len
call SectionAlign
mov [ldr_obj_VS],eax
mov eax,loader_len
call FileAlign
mov [ldr_obj_RWS],eax
quindi calcoliamo la nuova VSize e RawSize
mov ebx,[(esi-IMAGE_SECTION_HEADER_).PointerToRawData]
mov eax,[(esi-IMAGE_SECTION_HEADER_).SizeOfRawData]
add ebx,eax
xor edx,edx
mov ecx,[edi.OptionalHeader.FileAlignment]
div ecx
or edx,edx ; previus section already file aligned ?
mov eax,ebx
jz short @@no_zpad
add ebx,[Image_Base] ; no cave
xor cl,cl
@@zpad:
mov byte ptr [ebx],cl
inc ebx
dec edx
jnz @@zpad
@@no_zpad:
call FileAlign
mov [ldr_obj_RWA], eax ; file align loader section
questo snippet non fa altro che calcolare l'offset in cui dobbiamo scrivere i nostri dati
ovvero dalla fine dei dati precedenti accertandosi pero' che quest'ultimo offset sia
allineato
al file alignment e proveddendo allo zeropad nel caso non lo fosse..
mov eax,[ldr_obj_VS]
add eax,[edi.OptionalHeader.SizeOfImage]
call SectionAlign
mov [edi.OptionalHeader.SizeOfImage],eax
ora aggiustiamo l'imagesize aggiungendo la vsize della nostra sezione, cosi' NT non si
inkazzera' con noi
call RedirectReloc ; redirect reloc table to our
call RedirectIT ; redirect IT to loader built-in one
eheh questo invece e' un simpatico giochetto che va spiegato:
con queste due call sostituiamo nella data directory gli RVA della import table e della
reloc table in modo che puntino a quelle hardcoded che abbiamo approntato nel codice
del nostro loader. Questa operazione ha diversi vantaggi:
1) siccome la nostra reloc table e' vuota diciamo al loader di winsoz di non applicare
alcuna relocation in caso ci sia una collisione altrimenti sarebbe una catastrofe
in fase di decrittazione
2) impostando la nuova import table, facciamo in modo che sia windows stesso a patcharci
la nostra IAT e a fornirci gli address delle API di cui necessitiamo evitandoci di
ricorre a metodi piu' o meno euristici (usati in molti virii) come quello di trovare
il base address di kernel32 (che come noto puo' cambiare con ogni nuova versione ed
e' differente in 9x e Nt) quindi scannare la export table manualmente per ricavare
gli address degli entrypoint di GetProcAddress, LoadLibraryA/W, GetModuleHandleA.
3) in questo modo abbiamo anche alterato l'NT Header e questo ci garantisce che un
eventuale cracker che si accinga ad unpakkare la nostra creatura dovra' anche
ripristinare correttamente gli RVA nella data entry se vorra' che il programma funzioni
Siccome so che siete attenti,avrete notato che non ho ridiretto l'RVA della sezione
risorse: eheh in effeti questo e' abbastanza semplice come sistema, e' sufficente
harcodare una resource directory nel nostro loader con lo stesso metodo che abbiamo usato
per la IT. In questo modo potremmo ad esempio avere la possibilita' di visualizzare
delle dialog, dei bmp, o al limite sostituire l'icona del programma con la nostra.
Per far in modo poi che il programma "ritrovi" le sue risorse sara' sufficiente che
reimpostiamo l'RVA originale nell'header (attenzione che dovrete usare WriteProtectMemory
per patchare runtime se non volete un bel gpf). Ma allora perche'non ho messo il codice
per questa features.. semplice.. lo spieghero' nel terzo tutorial quando affronteremo le
tecniche antidump.. per ora accontentatevi!.. ho gia' scritto un mezzo romanzo! ;)))
mov edi,offset loader_obj
xchg edi,esi
mov ecx, IMAGE_SECTION_HEADER_
rep movsb
mov edi,[ldr_obj_RWA] ; edi = offset to loader section
mov ebx,[Image_Base]
add edi,ebx
jmp @@write_loader
bene ora abbiamo impostato corretamente i dati della sezione non ci resta che copiarla
in coda all'array della sections table, et voila'..
quello che segue invece e' codice per appendere il nostro loader nell'ultima sezione.
In genere questo metodo e' da preferirsi a quello precedente della nuova sezione, perche'
vi permette di "cammuffare" il fatto che il programma sia criptato, dato che con un
hexeditor o un pe-browser tutto sembrera' normale ad occhi non esperti.
Per ragioni di spazio (e crampi alle dita ;)) saro' succinto nei commenti anche perche'
non c'e' molto da dire se avete letto con attenzione la parte precente:
mov ebx,[esi.SVirtualAddress]
mov eax,[esi.SizeOfRawData]
lea ebx,[eax+ebx+4] ; calculate new entrypoint rva
mov [loader_rva],ebx
xchg dword ptr [edi.OptionalHeader.AddressOfEntryPoint],ebx
mov [original_erva],ebx
l'RVA del nuovo entrypoint e' sostanzialmente uguale a (RVA sezione precedente +
RawSize sezione precedente + 4) dove il +4 si spiega con il fatto che ci garantiamo che
ci sia una DWORD nulla tra noi e la fine dei dati originali, questo perche' con ogni
probabilita' quella che modificheremo sara' la .reloc e quindi dobbiamo mantenere
un spazio vuoto che funga da terminatore per i dati per la relocation
add eax,loader_len
call SectionAlign
mov [esi.SVirtualSize],eax
add eax,[esi.SVirtualAddress] ; imagesize = last_obj.VA + last_obj.VS
mov [edi.OptionalHeader.SizeOfImage],eax
classico direi: aggiunstiamo la imagesize come somma dell'RVA dell'ultima sezione
e la nuova VSize ottenuta dall'allineamento della RawSize al section alignment
mov eax,[esi.SFlags]
and eax,IMAGE_SCN_MEM_NOT_DISCARDABLE
or eax,IMAGE_SCN_MEM_EXECUTE + \
IMAGE_SCN_MEM_READ + \
IMAGE_SCN_MEM_WRITE
mov [esi.SFlags],eax
forziamo i flags writable, readable, executable per essere sicuri di non aver problemi
mov ebp,[esi.SizeOfRawData]
lea eax,[ebp+loader_len+4]
call FileAlign
mov [esi.SizeOfRawData],eax
allineamo la rawsize al file alignment
mov edi,[esi.PointerToRawData]
add edi,ebp
mov ebx,[Image_Base]
add edi,ebx
xor eax,eax ; calc offset to the end of rawdata
stosd ; last dword = 0 (mark end of reloc)
forziamo a zero quel pad di 4byte di cui sopra e quindi ora siamo pronti a copiare
il nostro loader..
Again, non riporto il codice che copia il nostro loader perche' e' semplicissimo,
l'unica nota e' che ho previsto che zeropaddi l'eventuale cavita' che si crea alla
fine del file per via dell'allineamento. Finita la copia del loader il crypter usa
UnmapViewOfFile, CloseHandle per rilasciare il MMF e chiama SetFilePointer e SetEndOfFile
per troncare la dimensione del file a quella effetivamente necessaria (= ESI calcolato
prendendo l'offset finale in uscita dal blocco di copia allineato al file alignment).
That's All.
Ora invece discuteremo di alcune delle piu' importanti funzioni utilizzate dal crypter
RedirectIT:
mov eax,[loader_rva] ; rva of decryptor
add eax,it_start-ldr_start ; add delta
mov [edi.OptionalHeader.DataDirectory.(IMAGE_DIR_IMPORT).VirtualAddress],eax
mov [edi.OptionalHeader.DataDirectory.(IMAGE_DIR_IMPORT).Size], it_len
add dword ptr k32_original,eax ; kernel32
add dword ptr k32_dll,eax ;
add dword ptr k32_first,eax ;
xor edx,edx
@@adj_k32iat:
add [func_k32+edx*4],eax
add [apiGetProcAddress+edx*4],eax
inc edx
cmp edx,size_k32_iat
jnz short @@adj_k32iat
add dword ptr u32_original,eax ; user32
add dword ptr u32_dll,eax ;
add dword ptr u32_first,eax ;
xor edx,edx
@@adj_u32iat:
add [func_u32+edx*4],eax
add [apiGetProcAddress+edx*4],eax
inc edx
cmp edx,size_u32_iat
jnz short @@adj_u32iat
ret
questa funzione in sostanza riaggiusta gli RVA interni alla IT in modo che siano coerenti
con la posizione (quindi ancora RVA) in cui sara' mappato il nostro codice. Per comprendere
il perche' di queste correzioni bisogna che analizziamo la struttura della IT.
La import table come sapete permette al loader di reperire le informazioni relative
alle funzioni importate da moduli esterni in modo implicito.
Per far questo esso percorre una serie di strutture nella IT che contengono il nome o
l'ordinal (= ID che identifica univocamente la funzione e relativo alla sua posizione
nell'array AddressOfFunctions della ET) delle funzioni importate ordinate per modulo di
appartenenza.. quindi mappa il modulo nello spazio di indirizzamento del processo
(LoadLibrary),scanna l'ET del modulo (GetProcAddress) per trovare l'address dell'entrypoint
della funzione e quindi patcha la IAT con quest'ultimo.
Ora mi aspetto una vostra domanda del tipo: "ma cos'e' la IAT" ?
Immaginate la Import Address Table come un array di DWORD che contiene gli indirizzi delle
funzioni delle DLL che il programma utilizza. L'esistenza della IAT e' dovuta al fatto che
sia il compilatore, sia il linker non possono conoscere a priopri l'address a cui verra'
caricata la dll e quindi per consetire al programmatore di utilizzare nel suo codice ad exp.
MessageBoxA devono approntare un meccanismo di indirezione: una chiamata da un linguaggio
ad alto livello a MessageBoxA verra' tradotta dal compilatore e dal linkere (attraverso
una import lib) in una call ad un thunk (normalmente in fondo alla sezione con il codice
.text,CODE,ecc) che si prensenta cosi':
JMP DWORD PTR [0x12345678]
dove 0x12345678 e' proprio l'indirizzo della DWORD presente nella IAT che a runtime conterra'
l'entrypoint di MessageBoxA. Alternativamente nei compilatori piu' recenti e' possibile usare
il modificatore __declspec(dllimport) per specificare che il simbolo esterno e' proprio una
funzione esportata da una dll: questo permette al compilatore di eliminare il thunk e di
tradurre la chiamata in una piu' performante
CALL DWORD PTR [0x12345678]
Come vedete la IAT e' di vitale importanza e come logicamente si puo' intuire non facilmente
ridirezionabile tant'e' che sebbene noi modificiamo l'header in modo che punti alla nostra
IAT, l'RVA in cui andremo a patchare gli indirizzi restera' quello originale (quest'ultimo
punto e' di vitale importanza per comprendere come sia possibile per un cracker intercettare
la IAT originale). Torniamo alla IT:
questa inizia con un array di strutture IMAGE_IMPORT_DESCRIPTOR:
* OriginalFirstThunk: e' un RVA ad un array di strutture IMAGE_THUNK_DATA che contengono
le informazioni per ogni funzione importata da questo modulo. La fine dell'array
e' segnalato da un elemento IMAGE_THUNK_DATA nullo. Questo array a differenza di quello
a cui punta FirstThunk non e' patchato dal Loader di Win32. Tuttavia la sua presenza
non e' garantita dato che alcuni linker per ottimizzire (vedi borland) lo omettono per
cui accertatevi sempre che questo RVA sia diverso da zero.
* TimeDateStamp: questo campo ha una duplice funzione a seconda che siano presenti o meno
funzioni bound (= gli address delle funzioni sono assoluti e gia'patchati dal linker
o dall'utility bind (fornita con l'SDK NT) che assume una determinata imagebase per
quel modulo):
- nel caso di funzioni bound avra' valore diverso da zero: se vale 0xFFFFFFFF siamo
in presenza di un pre-binding new-style, se invece e' diverso da 0xFFFFFFFF si tratta
di pre-binding old-style
- se invece vale 0 come nella stragrande maggioranza dei casi non ci sono import bound
e non serve a null'altro
* ForwarderChain: altro campo mistico =P indica l'indice nell'array FirstThunk del primo
elemento della forwarders chain, ovvero della lista di funzioni che sono importate da un
modulo in cui a loro volta sono forwarded. Si come avete intuito e' un bel casino :) cmq
non abbiate a preoccuparvi.. sia le funzioni bound che quelle farwarded sono merce
estremamente rara e dubito che ne incontrerete mai salvo decidiate di cryptare moduli di
sistema... pessima idea cmq ;)
* Name: questo RVA punta ad una stringa null-terminated con il nume del modulo
* FirstThunk: questo array e' simile a quello in OriginalFirstThunk con l'unica differenza
che ne e' garantita _sempre_ l'esistenza dato che gli elementi IMAGE_THUNK_DATA qui
contenuti verranno patchati dal loader di windoz con gli address delle funzioni...
come avete capito questo array e' tristemente =) noto come IAT
nella IT avremo quindi in successione un elemento IMAGE_IMPORT_DESCRIPTOR per ogni modulo
da cui importiamo una o piu' funzioni; l'array e' terminato come al solito con il classico
elemento nullo. Quanto alle import bound e forwarded non mi addentro oltre in questo argomento
perche' non credo che ne troverete esempi "reali" in quanto entrambi sono meccanismi utilizzati
principalmente per dll di windowz stesso e soprattuto sotto NT. Nel caso vogliate approfondire
vi consiglio l'ottimo documento di B. Luevelsmeyer. Molto piu' importante invece parlare degli
array OriginalFirstThunk e FirstThunk. Come anticipato entrambi puntano ad due array paralleli
di IMAGE_THUNK_DATA: ogni IMAGE_THUNK_DATA e' costituito da una sola DWORD che rappresenta una
RVA ad un elemento IMAGE_IMPORT_BY_NAME. Ogni IMAGE_IMPORT_BY_NAME e' invece cosi' dichiarato:
Hint WORD
Name BYTE DUP (?)
Hint rapresenta l'ordinal della funzione ma e' coerente solo se l'elemento IMAGE_THUNK_DATA
che lo punta ha il bit piu alto accesso (usate la mask IMAGE_IMPORT_BY_ORDINAL).
Name invece e' una stringa null-terminated che riporta il nome della funzione importata.
Ecco fatto :) .. queste sono tutte le strutture coinvolte nella IT: quindi ora e' chiaro
quale sia la sequenza che il loader segue:
1) legge un IMAGE_IMPORT_DESCRIPTOR -> ricava il nome del modulo -> LoadLibrary
2) legge un elemento IMAGE_THUNK_DATA dell'array FirstThunk (o OriginalFirstThunk se presente)
e ricava il corrispondente elemento IMAGE_IMPORT_BY_NAME ; contemporaneamente verifica
il bit IMAGE_IMPORT_BY_ORDINAL
3) dalla struct IMAGE_IMPORT_BY_NAME ricava nome/ordinal -> GetProcAddress
4) patcha nell'array FirstThunk (IAT) l'elemento IMAGE_THUNK_DATA corrente con l'address
della funzione costruendo cosi' la IAT
5) ripete la 2) per ogni IMAGE_THUNK_DATA (= funzione importata) finche' incontra un elemento
nullo
6) ripete la 1) per ogni IMAGE_IMPORT_DESCRIPTOR (= modulo "linkato") finche' incontra un
elemento nullo
Se guadardate il codice del nostro loader vedrete che la funzione HandleIT non fa altro che
eseguire queste operazioni.
RedirectReloc:
mov eax,[loader_rva] ; rva of decryptor
add eax,NULL_RELOC-ldr_start ; add delta
mov [edi.OptionalHeader.DataDirectory.(IMAGE_DIR_RELOC).VirtualAddress],eax
mov [edi.OptionalHeader.DataDirectory.(IMAGE_DIR_RELOC).Size], 10
ret
Questa funzione e' sostanzialmente gemella della precedente solo che riaggiusta, e sostituisce
nella data directory, l'RVA della nostra relocation table che come potete constatare dai
sorgenti e' vuota (fatto naturale visto che saremo noi e non il loader di windoz a gestire le
relocations). Vediamo ora la struttura della relocation table perche' una volta che vi
sara' chiara comprenderete il funzionamento della funzione HandleReloc.
La relocations table e' un sequenza di strutture IMAGE_BASE_RELOCATION che viene utilizzata
dal loader per patchare i punti dell'eseguibile in cui si e' fatto uso di indirizzi
assoluti relativi all'imagebse assunta a link-time e che ,nel caso di rilocazione, non
sarebbero piu' validi: immaginate una cosa tipo MOV EAX,[046707].. come vedete carica
un valore dall'address 0x46707.. ma cosa succederebbe se l'imagebase fosse 50000 ?!
l'indirizzo 0x46707 non sarebbe piu' valido e il programma leggerebbe un valore errato
o generebbe un gpf.. e' quindi necessario che il loader calcoli il DELTA (=50000-40000=10000)
e quindi lo sommi all'operando dell'istruzione MOV in modo che tutto torni a posto.
Ogni IMAGE_BASE_RELOCATION descrive i fixup da applicare per ognuna delle pagine da 4k
(0x1000 = x86 page per chi se ne fosse dimenticato ;) in cui viene suddivisia l'immagine
dell'eseguibile. Come si puo' arguire la "struttura" IMAGE_BASE_RELOCATION non ha una
dimensione fissa ma se ne puo' conoscere la dimensione attraverso il suo header:
IMAGE_BASE_RELOCATION STRUC
RVirtualAddress DD 0 < header
SizeOfBlock DD 8 <
TypeOffset DW ?
IMAGE_BASE_RELOCATION ENDS
SizeOfBlock contiene appunto la dimensione del blocco incluso l'header. Se vogliamo conoscere
quante sono le relocations per questa pagina di eseguibile dobbiamo quindi fare:
RelocNumber = ('SizeOfBlock'- sizeof(IMAGE_BASE_RELOCATION.header) idiv 2
Il campo RVirtualAddress rappresenta invece l'RVA a cui inizia la pagina in cui andranno
applicati i fixup. Il campo TypeOffset invece e' un array di WORD, ognuna delle quali
specifica 1) nel nibble piu' alto il tipo di rilocazione 2) nei restanti 12 bit lo scostamento
che sommato all'RVA ci da la posizione in cui applicare il fixup.
Il modo in cui applicheremo i fixup e' determinato dal tipo di rilocazione. Nei sorgenti e'
presente il codice per i 4 tipi che "dovrebbero" presentarsi in eseguibili per la piattaforma
x86 ma come vedete solo il tipo 3 IMAGE_REL_BASED_HIGHLOW e' effettivamente attivo: questo
perche' non ho _mai_ trovato un eseguibile che presenti fixup diversi dal tipo 0 (usato solo
come padding per l'allineamento a DWORD) o 3 e non ho informazioni in merito all'utilizzo
dei tipi 1,2,4. Cmq sia il modo di procedere avendo un fixup tipo IMAGE_REL_BASED_HIGHLOW
e' il seguente: dobbiamo innanzitutto sommare i 12bit dell'offset all'RVA RVirtualAddress
e quindi sommarci l'imagebase corrente, fatto questo all'indirizzo cosi' ottenunto dovremmo
sommare _tutti_ i 32bit del DELTA. Per i restanti tipi vi rimando ai sorgenti ed alla letture.
Ok, anche con le relocations siamo a posto.. ora vediamo alla risourse directory anche perche'
e' quella che presenta la struttura piu' elaborata. Innanzitutto va detto che le risorse
sono un composte dalle seguenti strutture organizzate gerarchicamente in un albero:
IMAGE_RESOURCE_DIRECTORY
IMAGE_RESOURCE_DIRECTORY_ENTRY
IMAGE_RESOURCE_DATA_ENTRY
il nodo iniziale e' sempre una struttura
IMAGE_RESOURCE_DIRECTORY i cui campi di nostro interesse sono:
* NumberOfNamedEntries
* NumberOfIdEntries
che indicano rispettivamente il numero di IMAGE_RESOURCE_DIRECTORY_ENTRY che utilizzano
NOMI o ID numerici come identificativi. Per cui ad ogni IMAGE_RESOURCE_DIRECTORY segue un
numero (NumberOfNamedEntries + NumberOfNamedEntries) di IMAGE_RESOURCE_DIRECTORY_ENTRY
che ha invece questa struttura:
IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT
NameID DD ?
OffsetToData DD ?
IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS
Il significato di Name dipende dal bit piu' alto:
se questo vale IMAGE_RESOURCE_NAME_IS_STRING i restanti 31bit sono un offset,relativo all'inzio
delle risorse, ad una struttura IMAGE_RESOURCE_DIR_STRING_U che in definitiva contiene il nome
(in formato UNICODE) della risorsa.. nel caso il bit non sia settato allora Name rappresenta
un ID numerico. Quest'ultimo nel caso ci troviamo nella root, rappresenta il tipo di risorsa
che troveremo nel ramo corrispispondente (definite con le costanti RT_xxxxx in imghdr.inc).
Il campo OffsetToData e' anch'esso relativo al valore del MSB: se abbiamo che e'settata la mask
IMAGE_RESOURCE_DATA_IS_DIRECTORY allora i restanti 31 bit sono un offset, sempre relativo
all'inizio delle risorse, ad un'altra IMAGE_RESOURCE_DIRECTORY che descrive il nodo di livello
inferiore, altrimenti se il bit non e' settato i 31bit sono l'offset ad una struttura
IMAGE_RESOURCE_DATA_ENTRY di cui ci interessano:
* rdOffsetToData: questo e' un RVA al blocco che contine i dati per questa risorsa
* rdSize: la dimensione del blocco dati della risorsa
Come vedete le strutture assumono un significato diverso a seconda del livello a cui ci
troviamo, ma va detto che in genere non troverete piu' di tre livelli prima di arrivare
ai dati veri e propri di una risorsa:
ROOT
RESOURCE_DIRECTORY : NUM ENTRY 3
|
+----------------------+-----------------------+
| | |
RESOURCE_ENTRY RESOURCE_ENTRY RESOURCE_ENTRY
menu dialog icon
| | |
RESOURCE_DIRECTORY: 3 RESOURCE_DIRECTORY: 2 RESOURCE_DIRECTORY: 3
| | |
+-----+-----+ +-+----+ +-+----+----+
| | | | | | |
RESOURCE_ENTRY RESOURCE_ENTRY
"main" "popup" 0x10 "maindlg" 0x100 0x110 0x120
|
DATA_ENTRY
Ok spero che la rappresentazione "grafica" sia chiara... ad ogni modo nei miei sorgenti ho
scento di percorre l'albero delle risorse con una funzione ricorsiva:
EnumResources:
push ebp
mov ebp,esp
push ebp ; save safe ESP
push offset @@on_r_except ; our simple handler
push dword ptr fs:[0] ; save previous frame
mov fs:[0],esp ; establish our SEH frame
xor ecx,ecx
call EnumResourceDirs,edi,edi,eax,ecx,ecx
xor eax,eax
jmp @@enum_exit
@@on_r_except:
mov eax,[esp+8] ; get ERR structure
mov ebp,[eax+8] ; ERR + 8 = safe ESP
stc
sbb eax,eax
@@enum_exit:
pop dword ptr fs:[0] ; remove SEH frame
mov esp,ebp
pop ebp
ret
questa codice prepara l'attraversamento delle risorse impostando l'adress base delle
risorse, il livello inziale (0) e la callback che verra' invocata ad ogni nodo (notate
che ho impostato un exception frame ..la sfiga e' sempre in agguato ;))
Ho scelto di utilizzare una callback per avere a disposizione un "engine" di attraversamento
dell'albero delle risorse che mi consentisse di compiere qualsiasi tipo di operazione sui vari
nodi (ad esempio e' possibile rilocare l'intero tree semplicemente cambiando gli RVA dei data
entry mentre lo attraversiamo) avendo a disposizione le informazioni relative al livello ed al
tipo di nodo in cui ci troviamo. Infatti se guardate i sorgenti la callback utilizzate per
criptare (i.e.ResCryptCallBack) le risorse e' uin grado di lasciare inalterate le risorse
RT_ICON,RT_GROUP_ICON in modo che il programma possa mostrare la sua icona nell'explorer.
Tutto questo avviene grazie a chiamate ricorsive fra EnumResourceDirs e EnumResourceEntry
che a loro volta chiamano la callback passandogli i dati relativi al livello in cui
ci troviamo nel ramo, il tipo di nodo, ed ogni informazione utile come la base delle risorse.
Come al solito non riporto i sorgenti.. ma credo che la spiegazione sia chiara.
Bene, ora non resta che esaminare il loader. Come e' ovvio il nostro codice dovra' essere
indipendente dalla imagebase altrimenti anche noi avremmo il problema della rilocazione..
bene la soluzione sta nell'usare il buon vecchio trucco del delta-offset usato dai tempi
immemori del dos e tanto caro a virii coderz. In questo modo non avremmo piu' riferimenti
assoluti ma solo relativi. e sara' facile calcore l'imagebase a siamo stati mappati
con questo semplice codice facendo riferimento all'RVA del nostro loader:
ldr_start:
pushfd ; save host reg state
pushad
call delta ; get delta offset
delta:
pop ebp
sub ebp, (delta - ldr_start) ; ebp = delta offset
mov eax, ebp ; calculate current imagebase
sub eax, [(loader_rva-ldr_start)+ebp]
mov [@image_base+ebp],eax ; store for later
a dir il vero potevamo anche usare GetModuleHandle, ma diciamo che cosi' fa piu scena ;))
mov edx,[(original_erva-ldr_start)+ebp] ; original entry point rva
add edx,eax
mov [esp+28],edx ; save host ret address
trovata l'imagebase , possiamo anche calcolarci l'entrypoint originale a cui restituiremo
il controllo una volta finito il nostro sporco lavoro
lea eax,[@loader_eHandler+ebp] ; our hanlder
push esp ; save safe ESP
push ebp ; save delta
push eax
push dword ptr fs:[0]
mov fs:[0],esp ; establish a SEH frame
stabiliamo un bel exception frame per ogni eventualita' in modo che il programma
in caso di problemi mostri una MessageBox piu' gentile di quello di windoz
(notate che l'address dell'hander e' calcolato con il solito delta)
xor edx,edx
next_object:
; read section data from decryptor table
mov edi, [@image_base+ebp]
mov eax, [ebp+(@section_array.section_rva)+edx*8] ; RVA
add edi, eax ; imagebase+rva= VA of section
mov ecx, [ebp+(@section_array.section_vsize)+edx*8] ; VSize
cmp eax,[@rsrc_rva+ebp]
pusha
jz short @@handle_res_d
call Decrypt
jmp short @@dummy_d
@@handle_res_d:
lea eax,[@ResDecryptCallBack+ebp]
xor ecx,ecx
call EnumResourceDirs,edi,edi,eax,ecx,ecx
@@dummy_d:
popa
quindi il loop che decritta i dati.. che e' perfettamente simmetrico a quello dell'encryptor.
L'algoritmo di crittazione e' decisamente semplice ma serve a dimostrare che il meccanismo
della rilocazione funziona (se avessimo usato un'encryption additiva non ci sarebbe bisogno
di prendersi cura delle rilocazioni ( A-B = C anche (A+x)-(B+x) = C).
Una volta decrittati i dati delle sezioni, il nostro loader si occupa di gestire una eventuale
rilocazione ( HandleReloc ), e successivamente la risoluzione delle imports con il caricamento
delle DLL nello spazio di indirizzamento del programma, e la costruzione della IAT che poi
verra' utilizzata dallo stesso. Eseguite queste operazioni l'immagine dell'eseguibile
e' stata ricostruita in memoria e di conseguenza possiamo restituire il controllo al
codice originale attraverso il canonico jmp eax (l'utilizzo eax non e' casuale: quando il
loader di win9x passa il controllo al programma in eax c'e' infatti proprio l'address
dell'entrypoint, quindi per eviate problemi e' meglio mimare il comportamento di windowz)
pop dword ptr fs:[0] ; remove seh frame
add esp,0Ch ; clean stack
popa ; restore host regs
popfd
jmp eax ; jump to original entry point
--==[ NOTE FINALI]==----------------------------------------------------------------------
Miii , quando ho iniziato questo tutorial non pensavo credevo che avrei scritto tanto:
e' davvero' lunghetto, per cui se vi siete rotti il cz, e non l'avete finito di leggere,
avete tutta la mia solidarieta' =)
Spero di essere stato chiaro, e abbastanza dettagliato, in modo che anche chi si avvicina
per la prima volta al problema dei pe-crypters possa capirci qualcosa. Chi invece e' gia'
esperto in materia mi aguro abbia apprezzato lo sforzo di coagulare le informazioni
che si possono repire sull'argomento in tutorial che presenta anche un esempio pratico.
Ok, ora e' il tempo dei greetings (tranquilli saranno brevissimi ;).
Innanzitutto voglio ringraziare +Fravia & +HCU tutta,Stone/UCF,Virogen/PC,Hyras,Izelion,
i membri del 29/a e Ikx per aver messo a dispozione del pubblico le loro conoscenze/sorgenti
fondamento di molte delle mie conoscenze. Ringraziamenti anche a Matt Pietrek e Andraw Shulman,
Jeffey Rithcher.. grazie di esistere :))
I miei ringraziamenti vanno poi a tutti i memberz di ringzer0 e frequentatori di #crack-it:
along3x, furbet, metalhead,suby,t3x, kry0, e tutti gli altri.. un tnx speciale va a:
Daemon: perche' riesce sempre a farmi sparlare di M$ e VB ;) (..salutami patrizia!)
Insanity : che pubblichera' questo tute tempestivamente! ;)
Genius : che continua a sperare che finiremo quel benetto api-hooker a r0 ;))
+Malattia: perche' e' un po' che non ci sentiamo.. fatti vivo ammorbato! :)
Neural_Notepad_Noise ;) per essere assolutamente assurdo e per aver fatto da cavia nei test ;)
Pusillus: per il suo entusiasmo incondizionato verso ringzer0 :)
xAONON : per le brevi ma intense chiaccherate sul PE
Yan-orel: che sta sempre ad ascoltare le mie cazzate ad ore assurde :)
War-lock: perche'..... beh lasciamo stare ahaha ;))))
byz Kill3xx