PE-Crypters : uno sguardo da vicino al c.d. "formato" PE
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
* 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 semplicemente 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 solitofile 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 ;))))