Formato PE
From UIC
Tutto (o quasi) sul Portable Executable
| Infos | |
|---|---|
| Author: | Ntoskrnl |
| Email: | ntoskrnl@gmail.com |
| Website: | http://ntcore.com |
| Date: | 20/08/2003 (dd/mm/yyyy) |
| Level: |
|
| Language: | Italian |
| Comments: | Tutto sul formato PE. |
Prologo
Per scaricare l'allegato al tutorial clickate qui.
E' già da tanto che il discorso del PE è molto in voga e di materiale ce n'è già diverso, ma anche io ho deciso di dare il mio contributo, cercando magari di non stare a riproporre le stesse cose già dette e ridette. Questo tutorial si propone di fare un discorso sul PE di respiro ampio.
Perché mi sono deciso a fare questo tutorial? Semplicemente perché spesso si trovano in rete tutorials che spiegano il formato ma non l'utilizzo pratico di esso, oppure, dai sorgenti che offrono la persona alle prime armi non riesce a capire un granché. Il mio intento è quello di spiegare il PE dal punto di vista del programmatore e non del reverser (per quello ci sono già i bei tut di Kill3xx), comunque anche i reverser potranno usufruire del tutorial. Inoltre questo tutorial parte proprio da 0, molte persone hanno trovato difficoltà a muovere i primi passi nel mondo del PE e io non ho mai saputo consigliargli un tut scritto proprio per newbies assoluti... Questo tutorial non premette proprio nessuna conoscenza del tipo PE/Reversing, le uniche cose che chiedo è che abbiate una certa familiarietà con la programmazione Win32 e il C.
Fonte di informazioni di cui ho fatto uso? Ci sono tanti tutorial ma le fonti migliori sono quelle lasciateci da Matt Pietrek (fatevi un giro nel MSDN) e non dimentichiamoci dello stesso WinNt.h.
Introduzione
Prima di partire con la pratica faccio una piccola introduzione casomai non sapeste proprio cosa è il PE. Il PE (Portable Executable) è un formato di file, più precisamente un formato standard per gli eseguibili Win32, tale formato è stato scritto dalla Microsoft in base al COFF (Common Object File Format) che è appunto il formato standard per gli object-file su os unix-like e VMS e dal quale derivano tutti i formati quali PE, Elf ecc. Ma a cosa serve il PE? Serve a fornire al loader le informazioni necessarie per creare il processo/modulo. Una volta mappato in memoria il file è comodissimo da gestire e il loader funziona con una semplicità alquanto straordinaria. Per chi non lo sapesse PE non sono solo gli exe Win32 ma pure le Dll, gli Ocx (e anche altri tipi di file). Inoltre su sistemi NT pure i Device Driver sono dei PE (al contrario di win9x/ME che usa VxD, i quali sono dei LE: Linear Executable). Basta con la storia, ci sarebbero anche altre cose da dire ma non voglio essere troppo prolisso su cose che più che altro fanno da decorazione alla nostra conoscenza.
Come forse saprete tutte le strutture e le dichiarazioni riguardanti il PE stanno nel WinNt.h. Anche dare un'occhiata a questo header è sempre utile (anzi utilissimo). Il primo sorgente che vedremo non farà altro che mappare in memoria un eseguibile.
Innanzitutto vediamo la struttura di un PE, ovvero come un normale PE ci si presenta.
DOS HEADER NT HEADERS (PE HEADER) SECTION TABLE SECTIONS
Cosa sono gli headers? Sono un insieme di informazioni riguardanti il corrente File, tutte le informazioni che ci interessano sul PE sono contenute negli Nt Headers che rappresentano l'header vero e proprio del PE e nella Section Table, che è una tabella che contiene le informazioni sulle sezioni presenti nel PE. Le sezioni di un PE sono i luoghi che contengono codice, dati, imports, exports ecc. ecc. del nostro eseguibile. Il discorso delle sezioni lo affronteremo più avanti.
Spieghiamo per prima cosa come accedere alle informazioni di un file PE. Il modo più semplice ed immediato e di caricarcelo tutto in memoria (con questo non voglio assolutamente dire che faremo da loader eh).
#include <stdio.h>
int main()
{
HANDLE hFile, hMapObj, hBaseAddress;
printf("\nOpening File...\n");
hFile = CreateFile("prova.exe", GENERIC_READ, FILE_SHARE_READ, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
hMapObj = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
if (!hMapObj)
{
CloseHandle(hFile);
printf("Cannot Create File Mapping\n");
return -1;
}
if (!(hBaseAddress = MapViewOfFile(hMapObj, FILE_MAP_READ, 0, 0, 0)))
{
CloseHandle(hMapObj);
CloseHandle(hFile);
printf("Cannot create Map View of File\n");
return -1;
}
printf("File Mapped in Memory\n");
UnmapViewOfFile(hBaseAddress);
CloseHandle(hMapObj);
CloseHandle(hFile);
printf("File Closed\n");
return 0;
}
Questo codice non fa altro che caricare in memoria un file, sia esso o no un PE. Ho usato le funzioni di mapping per mappare (appunto) il file in memoria, questo metodo è molto comune ma se dovete fare operazioni di riallocazione di memoria è meglio se optate per un CreateFile, GetFileSize, Allocazione Memoria, ReadFile; in questo modo otterrete lo stesso risultato ma con libertà di riallocazione. Siccome le Api per mappare un file in memoria sono sconosciute ai newbies ne spiego brevemente l'utilizzo.
Come detto il codice non fa altro che mappare un eseguibile in memoria. Cominciamo ad analizzare il sorgente di questo programma, come prima cosa il prog apre il file con un CreateFile (spero che questa funzione non ve la debba spiegare), dopodiché utilizzando l'handle riportato da CreateFile il programma chiama la funzione CreateFileMapping. Questa funzione serve a creare un oggetto di File-Mapping in memoria in modo da consentire di chiamare poi MapViewOfFile che mapperà poi il file in memoria (quella del nostro processo). Infine viene poi unmappato (bel termine eh) il file dalla memoria e chiusi i restanti handle. Sintassi delle funzioni:
HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName
);
hFile specifica l'handle del file dal quale creare un mapping.
lpFileMappingAttributes di questo parametro non ci frega nulla.
flProtect questo parametro specifica la protezione del map in memoria, i parametri possibili sono: PAGE_READONLY ( se il file è stato aperto con GENERIC_READ), PAGE_READWRITE (se è stato aperto con un GENERIC_READ | GENERIC_WRITE), PAGE_WRITECOPY (come per PAGE_READWRITE).
dwMaximumSizeHigh specifica in high-order la grandezza massima per l'oggetto.
dwMaximumSizeLow specifica in low-order la grandezza massima per l'oggetto, se entrambi questi parametri sono settati uguali a 0, la grandezza per l'oggetto sarà della stessa dimensione del file.
lpName specifica il nome per l'oggetto, se il parametro viene settato uguale a 0, l'oggetto sarà senza nome.
Se la funzione viene eseguita con successo, il valore di ritorno sarà l'handle per l'oggetto creato, altrimenti sarà 0.
Ora vediamo la funzione MapViewOfFile che mapperà il file in memoria.
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap
);
hFileMappingObject l'handle dell'oggetto creato con CreateFileMapping.
dwDesiredAccess specifica l'accesso che può essere: FILE_MAP_WRITE, FILE_MAP_READ, FILE_MAP_ALL_ACCESS (che è la stessa cosa di FILE_MAP_WRITE) e FILE_MAP_COPY. Ovviamente i paramteri vanno messi corrispondenti a quelli per l'oggetto.
dwFileOffsetHigh e dwFileOffsetLow non fanno altro che determinare l'offset di partenza dal quale mappare il file, anche questi li possiamo settare a 0.
dwNumberOfBytesToMap specifica il numero di bytes da mappare, se questo parametro viene settato a 0, l'intero file viene mappato.
Il valore di ritorno è l'indirizzo da cui parte il file mappato se la funzione viene eseguita correttamente, altrimenti è 0. La variante di MapViewOfFile è MapViewOfFileEx (tanto per essere precisi).
Come abbiamo visto nel sorgente il file viene poi unmappato dalla memoria, la funzione che svolge questo compito è UnmapViewOfFile:
LPCVOID lpBaseAddress
);
lpBaseAddress specifica l'indirizzo di partenza in memoria del file mappato. Spero di non dover aggiungere altro su UnmapViewOfFile.
Breve riepilogo: MapViewOfFile ci restituisce l'indirizzo del file mappato, se non usate le funzioni di mapping l'indirizzo che vi serve è quello che avete preso con l'allocazione di memoria (che sia fatta con funzioni ansi o api fa uguale).
Dos Header
Noi sappiamo che il primo insieme di elementi che troviamo si chiama Dos Header, vediamo il Dos header di un comune PE file (uso il mio whex che è il mio hex editor preferito, okok non sono proprio imparziale):
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZÉ.�...�... ..
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ©.......@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 E8 00 00 00 ............Þ...
Ok, sappiamo che queste sono le informazioni contenute nel Dos Header, ma come ricaviamo ogni singolo elemento? Semplice useremo un puntatore a struttura, noi abbiamo la dichiarazione della struttura (ovvero l'insieme di elementi) del Dos Header nel winnt.h:
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Se ci dichiaramo un puntatore a struttura così:
Basterà farlo puntare al nostro BaseAddress del PE file per avere una struttura piena di elementi, es:
Di questa immensa strutturona ci servono solamente 2 elementi (gli altri elementi possono anche essere 0): e_magic e e_lfanew. e_magic come vedete è una WORD e ci serve per un primo controllo, ovvero e_magic deve corrispondere alle lettere ascii MZ, se non corrisponde allora il nostro file non è da considerarsi come eseguibile, nel winnt è definito IMAGE_DOS_SIGNATURE (0x4D5A = 'MZ') che sarà il valore con cui confrontare e_magic. Invece e_lfanew è un file offset che ci dice dove stanno gli Nt Headers ovvero il PE Header. Solitamente il PE Header viene dopo il Dos Header e dopo il Dos Stub (che serve solo se un programma Win32 viene avviato in Dos: ovvero dice che non è possibile, una cosa del genere: This program cannot be run in Dos mode), cmq un PE può benissimo fare a meno del Dos Stub. e_lfanew fa in modo che il PE header possa essere praticamente ovunque nel file anche se in genere si trova dove abbiamo detto, questa cosa è bene tenerla a mente. Quindi per ottenere l'inizio della struttura NtHeaders basterà sommare all'indirizzo base del file in memoria il file offset contenuto in e_lfanew.
Prima di andare avanti vediamo brevemente un piccolo sorgente che non fa altro che controllare la validità del MZ HEADER e mostrarci l'RVA del PE HEADER, tanto per andare passo passo.
#include <stdio.h>
int main()
{
HANDLE hFile, hMapObj, hBaseAddress;
IMAGE_DOS_HEADER *ImageDosHeader;
printf("\nOpening File...\n");
hFile = CreateFile("prova.exe", GENERIC_READ, FILE_SHARE_READ, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
hMapObj = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
if (!hMapObj)
{
CloseHandle(hFile);
printf("Cannot Create File Mapping\n");
return -1;
}
if (!(hBaseAddress = MapViewOfFile(hMapObj, FILE_MAP_READ, 0, 0, 0)))
{
CloseHandle(hMapObj);
CloseHandle(hFile);
printf("Cannot create Map View of File\n");
return -1;
}
printf("File Mapped in Memory\n");
// si inizia dal Base Address
ImageDosHeader = (IMAGE_DOS_HEADER *) hBaseAddress;
// controlla il Dos Header
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("Invalid Dos Header\n");
goto Close_Handles;
}
printf("Valid Dos Header\n");
printf("File Offset: %X\n", ImageDosHeader->e_lfanew);
Close_Handles:
UnmapViewOfFile(hBaseAddress);
CloseHandle(hMapObj);
CloseHandle(hFile);
printf("File Closed\n");
return 0;
}
Ok, andiamo avanti.
PE Header
Siamo finalmente giunti al PE Header ovvero agli NT Headers. Vediamoci tale struttura:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Ok, cosa abbiamo qua? Una dword e due strutture che conterranno cose di cui ancora non siamo a conoscenza. Iniziamo dal primo membro che sarebbe la dword-signature, questa dword come la word e_magic serve per controllare l'effettiva validità del PE file. Ovvero questo campo deve corrispondere ai caratteri 'PE00'. Come possiamo vedere in un comune eseguibile:
000000E0 50 45 00 00 4C 01 06 00 PE..L�.
000000F0 7A AD 80 3F 00 00 00 00 00 00 00 00 E0 00 0E 01 z¡Ç?........Ó.�
00000100 0B 01 06 00 00 A0 02 00 00 D0 00 00 00 00 00 00 � �..á..ð.......
Dalle lettere PE inizia il nostro PE Header. La definizione di tale signature è IMAGE_NT_SIGNATURE. Adesso vediamo entrambe le strutture andando in ordine, partiamo dal FileHeader.
File Header
Ecco la dichiarazione di tale struttura:
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine specifica per quale CPU è stato designato il file. Le varie CPU definite sono queste:
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
NumberOfSections beh il nome già dice tutto...Il numero delle sezioni che il PE dovrà contenere.
TimeDateStamp da qui si ricava data e ora di creazione del file (inutile direi).
PointerToSymbolTable offset per la symbol table (utile solo per debug).
NumberOfSymbols numero di simboli nella symbol table.
SizeOfOptionalHeader specifica la grandezza dell'OPTIONAL HEADER (parametro mooolto importante).
Characteristics specifica alcune informazioni sul file, eccovi quelle definite nel winnt:
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable
//(i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle 2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media,
// copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net,
// copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
35;define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed
Non ho tradotto visto che i commenti in inglese trovati nel winnt mi parevano già abbastanza chiari (se non lo sono, allora dovete gettare via questo tutorial e mettervi a studiare le basi dell'inglese, non del PE). Non vi fate spaventare da tutti questi campi che sembrano così oscuri, vi assicuro che di essenziale in questa struttura vi è solamente il campo NumberOfSections e il SizeOfOptionalHeader: questo campo ci dice le dimensioni dell'Optional Header e tramite queste potremo ricavare l'indirizzo della Section Table.
Optional Header
Vediamo adesso la struttura dell'OPTIONAL HEADER che è una struttura interessantissima. Vi avverto che questa struttura è grandicella. Non fatevi impressionare: cercherò di farvi capire tutto.
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic questa word identifica lo stato dell'image file, i valori definiti sono:
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64bit
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107 // ROM
MajorLinkerVersion e MinorLinkerVersion rappresentano la versione del linker che ha creato il file (del tipo il mio VC++ 6 crea campi 06-00).
SizeOfCode numero di bytes di codice, sono quindi sommate tutte le varie sezioni con proprietà di EXECUTABLE, visto che in genere la sezione è solamente una di codice, questo parametro spesso corrisponde al numero di bytes in quella sezione (è un campo superfluo, ovvero il loader non ci fa veramente caso, potrebbe anche essere sballato).
SizeOfInitializedData somma di tutte le varibili inizializzate (sempre superfluo).
SizeOfUninitializedData indovinate un po'?
AddressOfEntryPoint è un RVA che corrisponde all'Entry Point del PE, specifica da dove deve partire il codice all'interno del processo. Servono adesso dei chiarimenti, ho usato il termine RVA cosa vuol dire? Perché ho detto processo e non file? Procediamo con ordine, dato che la prima risposta può rendere intuitiva quella alla seconda domanda. Innanzitutto dovete sapere che quando il loader carica un PE in memoria non lo carica come è sul disco ma per quanto riguarda le sezioni le carica dove la Section Table gli dice di caricarle. Inoltre vi è un altro campo nel PE che specifica l'indirizzo al quale deve essere mappato il PE (il campo si chiama Image Base), in genere per gli exe l'ImageBase è 00400000h e questo è un Virtual Address. Se la Section Table ci dice che la prima sezione (mettiamo sia quella che contiene il codice, in genere è così) che sta al File Offset (ovvero l'offset della sezione su disco) 200h deve in memoria essere collocata all'RVA (Relative Virtual Address) 1000h, avremo la sezione al VA 00401000h. Da questo possiamo enunciare che: RVA = VA - ImageBase e VA = RVA + ImageBase. Come arrivare da un RVA al File Offset lo vedremo dopo. In ogni caso passando alla seconda domanda, una volta mappato il file PE come la Section Table dice al loader, il campo AddressOfEntryPoint punta direttamente all'indirizzo di memoria. Siccome noi lavoriamo sempre e solo caricando il File direttamente dal disco senza tenere conto della Section Table quando lavoriamo con RVA dovremo sempre riconvertirli in File Offsets. Non vi preoccupate se non avete ancora capito tanto procederemo sempre gradualmente.
BaseOfCode BaseAddress per il codice, ovvero da dove inizia (superfluo).
BaseOfData BaseAdress per la sezione data (superfluo).
ImageBase specifica l'indirizzo dal quale deve essere mappato in memoria il file (quello che abbiamo citato parlando dell'AddressOfEntryPoint).
SectionAlignment specifica l'allineamento del file in memoria, ovvero delle varie sezioni (zona headers compresa). Dunque ci sono due tipi di allineamento: quello in memoria e quello su disco. Diciamo che su disco il PE è allineato a 200h, questo vuol dire che a partire dalla zona header tutto è allineato a 200h:
SECTION 1 - IDEM
SECTION 2 - IDEM
Se una sezione è misura 522h su disco la sua grandezza sarà 600h. In memoria invece sarà grande 1000h. Gli allineamenti possibili su disco devono essere 200h o suoi multipli (tranne per i loaders su NT/2k/XP che consentono anche allineamenti di 1 byte).
FileAlignment specifica l'allineamento del file (come detto appunto).
MajorOperatingSystemVersion e MinorOperatingSystemVersion specificano la versione dell'OS (superfluo).
MajorImageVersion e MinorImageVersion versione immagine (superfluo).
MajorSubsystemVersion e MinorSubsystemVersion versione sottosistema (superfluo).
Win32VersionValue boh non lo so (ma tanto fa parte di quei parametri totalmente inutili).
SizeOfImage specifica le dimensioni dell'immagine del PE una volta mappato in memoria, su 9x questo parametro non viene considerato ma se è sballato su NT l'exe non si esegue. Ho dato una spiegazione un po' vaga, vediamo di capire meglio di che cosa si tratta, abbiamo detto che esiste un Section Alignment... Ecco se noi sommiamo la grandezza della Header Zone (allineata) e la grandezza di tutte le sezioni (ognuna di queste allineata) allora abbiamo calcolato il SizeOfImage. La grandezza della Header Zone è facile ricavarla basta prendere l'RVA dal quale inizia la prima sezione e sommare poi le grandezze di tutte le sezioni.
SizeOfHeaders specifica le dimensioni degli headers (grandezza allineata al File Alignment), per sapere la grandezza basta prendere l'indirizzo fisico della prima sezione del PE (questo se non volete ricalcolare la grandezza per ottimizzazione, in quel caso dovreste sommare i vari header fino alla Section Table e allineare).
CheckSum corrisponde al checksum dell'immagine del PE (lo calcola il compilatore, ma c'è anche un Api per calcolare il valore... Ma la vedremo dopo).
Subsystem specifica il sottosistema nel quale l'eseguibile lavora, i valori definiti sono:
#define IMAGE_SUBSYSTEM_NATIVE 1 // non richiede sottosistema,
//per esempio questo vale per i device drivers
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // sottosistema Windows GUI
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // sotto sistema Windows CONSOLE
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // OS/2 CONSOLE
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // Posix CONSOLE
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // driver 9x nativo
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // sottosistema Windows CE
DllCharacteristics parametro obsoleto che specifica in quali circostanze inizializzare la DLL (se il PE è una Dll).
SizeOfStackReserve specifica la quantità di memoria da riservare per lo stack iniziale del thread.
SizeOfStackCommit specifica la quantità di memoria inizialmente presa per il thread iniziale.
SizeOfHeapReserve specifica la quanità di memoria da riservare per l'heap iniziale del processo.
SizeOfHeapCommit specifica la quantità di memoria inizialemente presa nel heap del processo.
LoaderFlags parametro obsoleto che serve per il debug.
NumberOfRvaAndSizes numero di entry nell'array di DataDirectory.
DataDirectory questo array serve al loader per trovare in modo rapido diverse sezioni (da non confondere con le sezioni della Section Table) chiamate directory.
DWORD VirtualAddress; // non è veramente un VA ma un RVA
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
le directory sono: Export Table, Import Table, Resource Direcotry, Exception, Security, Base Relocation, Debug, Copyright, Global Ptr, Thread Local Storage, Load Configuration, Bound Import Table, Import Address Table, Delay Import, COM Descriptor. Queste in fondo non sono altro che (semplificando) sottosezioni delle sezioni che specifica la Section Table. In pratica la DataDirectory non è altro che un'ulteriore suddivisione. In ogni caso per adesso non ci interessa la DataDirecotry, dobbiamo ancora trattare prima la Section Table.
Section Table
Inanzitutto cos'è la Section Table? E' un array di strutture IMAGE_SECTION_HEADER (una per ogni sezione, vedremo dopo la struttura) e il numero di elementi nella struttura è determinato dal NumberOfSections nella struttura File Header. Vediamo come giungiamo alla Section Table: bisogna sommare l'indirizzo dal quale inizia l'NT headers alla grandezza dell'Optional Header (campo del File Header, ricordate?) a 18h (che sarebbe la grandezza del File Header + la dword-signature).
Una comoda macro fornitaci dal winnt.h è IMAGE_FIRST_SECTION, la sua sintassi è:
Passando semplicemente l'indirizzo degli NT Headers mi ritorna l'indirizzo della prima sezione in un PE. Ma cosa è ImageSectionHeader? E' un puntatore a struttura IMAGE_SECTION_HEADER, vediamo tale struttura della quale come detto ve ne è una per ogni sezione.
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // nome sezione max 8 caratteri
union {
DWORD PhysicalAddress; // serve solo se il file è .obj
DWORD VirtualSize; // la grandezza della sezione
} Misc;
DWORD VirtualAddress; // indica da quale RVA il loader deve mappare la sezione
DWORD SizeOfRawData; // grandezza della sezione arrotondata in base
// al file alignement
DWORD PointerToRawData; // file offset della sezione
DWORD PointerToRelocations; // negli exe questo campo è sempre 0
//dato che non serve (solo per obj)
DWORD PointerToLinenumbers; // come PointerToRelocations
WORD NumberOfRelocations; // solo obj
WORD NumberOfLinenumbers; // mai visto utilizzato manco questo (nei comuni PE dico)
DWORD Characteristics; // caratteristiche della sezione
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER 40
Riassumendo i campi fondamentali sono: Nome della sezione, Virtual Address (è una RVA quindi per sapere a che VA sta una sezione di un processo/modulo dovrete sommare l'image base a questo valore), VirtualSize (in genere il virtual size corrisponde alla grandezza della sezione vera e propria, senza Section Alignment, dato che ci pensa il loader ad allineare e quindi in genere l'unica sezione con Virtual Size maggiore di Raw Size è la .data), Raw Address e Raw Size (questi ultimi vengono anche chiamati Physical Address e Physical Size). L'ultimo campo importante è Characteristic, esso indica le caratteristiche della sezione. I flag più importanti sono (ricordatevi che sono tutti combinabili):
00000040h - contiene dati inizializzati
00000080h - contiene dati non inizializzati
10000000h - sezione condivisibile
20000000h - sezione eseguibile
40000000h - sezione leggibile
80000000h - la sezione è scrivibile
Ma per essere esaurienti eccovi tutti i flags disponibili:
// Section contains initialized data.
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040
// Section contains uninitialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080
// Reserved.
#define IMAGE_SCN_LNK_OTHER 0x00000100
// Section contains comments or some other type of information.
#define IMAGE_SCN_LNK_INFO 0x00000200
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.
// Section contents will not become part of image.
#define IMAGE_SCN_LNK_REMOVE 0x00000800
// Section contents comdat.
#define IMAGE_SCN_LNK_COMDAT 0x00001000
// 0x00002000 // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000
// Reset speculative exceptions handling bits
// in the TLB entries for this section.
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000
// Section content can be accessed relative to GP
#define IMAGE_SCN_GPREL 0x00008000
#define IMAGE_SCN_MEM_FARDATA 0x00008000
// IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 0x00080000
// Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_1BYTES 0x00100000
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000
// Unused 0x00F00000
// Section contains extended relocations.
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000
// Section can be discarded.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000
// Section is not cachable.
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000
// Section is not pageable.
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000
// Section is shareable.
#define IMAGE_SCN_MEM_SHARED 0x10000000
// Section is executable.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000
// Section is readable.
#define IMAGE_SCN_MEM_READ 0x40000000
// Section is writeable.
#define IMAGE_SCN_MEM_WRITE 0x80000000
Come detto le sezioni contengono tutto ciò che c'è in un File PE (headers esclusi), vi sono diverse sezioni in genere in un PE (nulla ci vieta di mettere tutto in una sola eh), ma generalmente quelle che vengono prodotte dai compilatori sono .text e .code (codice), .data (variabili inizializzate), .bss (variabili non iniziallizzate), .rdata (può contenere diverse cose: info di debug, description string, GUIDs e TLS Directory), .idata (imports), .edata (exports), .tls (TLS Direcory), .rsrc (resource), .reloc (relocations) ecc. Chiaramente le descrizioni fra parentesi vi sembrano vage, ma non preoccupatevi, parleremo più avanti delle sottosezioni, quando parleremo della DataDirectory. Comunque non mi va di legare i nomi delle sezioni alle sottosezioni dato che i nomi delle sezioni sono convenzionali ma non obbligatori.
Fra un po' vedremo un sorgente che aggiungerà una sezione ad un PE, ma prima di fare ciò vi mostro un piccolo sorgente che non fa altro che elencare le sezioni di un PE (tanto per non andare troppo in fretta). Ah lo dico subito da questo sorgente in poi rinuncio alle funzioni MapViewOfFile ecc., di quelle volevo solo dare una dimostrazione di funzionamento dato che sono molto usate... Adesso si fa tutto con ReadFile.
#include <stdio.h>
int main()
{
HANDLE hFile;
BYTE *BaseAddress;
WORD x;
DWORD FileSize, BR;
IMAGE_DOS_HEADER *ImageDosHeader;
IMAGE_NT_HEADERS *ImageNtHeaders;
IMAGE_SECTION_HEADER *ImageSectionHeader;
char FileName[MAX_PATH];
char SectionName[9] = { 0 };
printf("\nFile Name: ");
scanf("%s", FileName);
printf("\nOpening File...\n");
hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
FileSize = GetFileSize(hFile, NULL);
BaseAddress = (BYTE *) malloc(FileSize);
if (!ReadFile(hFile, BaseAddress, FileSize, &BR, NULL))
{
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageDosHeader = (IMAGE_DOS_HEADER *) BaseAddress;
// controlliamo il Dos Header
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("Invalid Dos Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageNtHeaders = (IMAGE_NT_HEADERS *)
(ImageDosHeader->e_lfanew + (DWORD) ImageDosHeader);
// controlliamo il PE Header
if (ImageNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
printf("Invalid PE Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// prende l'indirizzo della prima sezione
ImageSectionHeader = IMAGE_FIRST_SECTION(ImageNtHeaders);
// mostra la section table
for (x = 0; x < ImageNtHeaders->FileHeader.NumberOfSections; x++)
{
memcpy(SectionName, ImageSectionHeader[x].Name,
IMAGE_SIZEOF_SHORT_NAME);
printf("\n\nSection Name: %s\n"
"Virtual Address: %08X\n"
"Virtual Size: %08X\n"
"Raw Address: %08X\n"
"Raw Size: %08X\n"
"Characteristics: %08X",
SectionName, ImageSectionHeader[x].VirtualAddress,
ImageSectionHeader[x].Misc.VirtualSize,
ImageSectionHeader[x].PointerToRawData,
ImageSectionHeader[x].SizeOfRawData,
ImageSectionHeader[x].Characteristics);
}
printf("\n\nThis was the Section Table\n");
free(BaseAddress);
CloseHandle(hFile);
return 0;
}
Il sorcio è semplice e non credo necessiti di ulteriori chiarimenti. Voglio farvi presente una cosa: quando si lavora con file PE e quindi con indirizzi di memoria sarebbe opportuno lavorare con una gestione delle eccezioni altrimenti il primo PE sballato vi farà crashare il programma. Io nei miei codici di esempio non metto la gestione delle eccezioni (né controllo i permessi di accesso alla memoria) per non appesantire troppo il codice, questi sono esempi e servono per farvi imparare, non voglio che troppe cose distolgano la vosta attenzione dalle righe di codice importanti (ecco perché programmo in console).
Ed adesso complico un poco le cose, vi propongo un source che serve ad aggiungere una nuova sezione alla Section Table di un PE.
#include <windows.h>
#include <stdio.h>
DWORD CalcAlignment(DWORD Alignment, DWORD TrueSize);
int main(int argc, char *argv[])
{
HANDLE hFile;
BYTE *BaseAddress;
WORD nSection;
DWORD FileSize, BRW, NameSize, Size;
IMAGE_DOS_HEADER *ImageDosHeader;
IMAGE_NT_HEADERS *ImageNtHeaders;
IMAGE_SECTION_HEADER *ImageSectionHeader;
// controlla numero argomenti
if (argc < 4)
{
printf("\nNeed More Arguments\n");
return -1;
}
printf("\nOpening File...\n");
hFile = CreateFile(argv[1], GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
FileSize = GetFileSize(hFile, NULL);
BaseAddress = (BYTE *) malloc(FileSize);
if (!ReadFile(hFile, BaseAddress, FileSize, &BRW, NULL))
{
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageDosHeader = (IMAGE_DOS_HEADER *) BaseAddress;
// controlliamo il Dos Header
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("Invalid Dos Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageNtHeaders = (IMAGE_NT_HEADERS *)
(ImageDosHeader->e_lfanew + (DWORD) ImageDosHeader);
// controlliamo il PE Header
if (ImageNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
printf("Invalid PE Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// prende le dimensioni
Size = atoi(argv[3]);
printf("Creating New Section...\n");
nSection = ImageNtHeaders->FileHeader.NumberOfSections;
// aggiorna il numero di sezioni nel FILE HEADER
ImageNtHeaders->FileHeader.NumberOfSections++;
ImageSectionHeader = IMAGE_FIRST_SECTION(ImageNtHeaders);
// aggiorna Size Of Image
ImageNtHeaders->OptionalHeader.SizeOfImage +=
CalcAlignment(ImageNtHeaders->OptionalHeader.SectionAlignment, Size);
// teoricamente dovremmo controllare se c'è
// abbastanza spazio tra la fine della section
// table e la prima sezione e se non c'è
// dobbiamo ingrandire lo spazio che intercorre
// tra la header zone e la prima sezione per
// inserire la nostra sezione, ma dato che
// questo è un esempio lasciamo perdere
// e prendiamo per buono che lo spazio c'è
ZeroMemory(&ImageSectionHeader[nSection],
IMAGE_SIZEOF_SECTION_HEADER);
if (strlen(argv[2]) <= IMAGE_SIZEOF_SHORT_NAME)
NameSize = strlen(argv[2]);
else
NameSize = IMAGE_SIZEOF_SHORT_NAME;
// copia il nome della nuova sezione
memcpy(&ImageSectionHeader[nSection].Name, argv[2], NameSize);
// calcola il Virtual Address della nuova sezione
ImageSectionHeader[nSection].VirtualAddress =
CalcAlignment(ImageNtHeaders->OptionalHeader.SectionAlignment,
(ImageSectionHeader[nSection - 1].VirtualAddress +
ImageSectionHeader[nSection - 1].Misc.VirtualSize));
// Virtual Size = Size vero della sezione
// potremmo anche allinearlo però
ImageSectionHeader[nSection].Misc.VirtualSize = Size;
// controllo se la sezione precedente è
// allineata correttamente
if (ImageSectionHeader[nSection - 1].SizeOfRawData %
ImageNtHeaders->OptionalHeader.FileAlignment)
{
// se no ci penso io
ImageSectionHeader[nSection - 1].SizeOfRawData =
CalcAlignment(ImageNtHeaders->OptionalHeader.FileAlignment,
ImageSectionHeader[nSection - 1].SizeOfRawData);
SetFilePointer(hFile,
(ImageSectionHeader[nSection - 1].PointerToRawData +
ImageSectionHeader[nSection - 1].SizeOfRawData), NULL,
FILE_BEGIN);
SetEndOfFile(hFile);
}
// setto l'address fisico
ImageSectionHeader[nSection].PointerToRawData =
GetFileSize(hFile, NULL);
// e la grandezza fisica
ImageSectionHeader[nSection].SizeOfRawData =
CalcAlignment(ImageNtHeaders->OptionalHeader.FileAlignment, Size);
// caratteristiche (readable)
ImageSectionHeader[nSection].Characteristics = IMAGE_SCN_MEM_READ;
// aggiungo i byte in fondo al file
SetFilePointer(hFile, ImageSectionHeader[nSection].SizeOfRawData,
NULL, FILE_END);
SetEndOfFile(hFile);
// salvo le modifiche
SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
WriteFile(hFile, BaseAddress, FileSize, &BRW, NULL);
free(BaseAddress);
CloseHandle(hFile);
printf("\nNew Section Added\n");
return 0;
}
// calcola l'allineamento in base alla grandezza originale
DWORD CalcAlignment(DWORD Alignment, DWORD TrueSize)
{
DWORD CalculatedAlignment;
for(CalculatedAlignment = Alignment; ; CalculatedAlignment
+= Alignment)
{
if (TrueSize <= CalculatedAlignment) break;
}
return CalculatedAlignment;
}
Ok, non credo che il sorcio necessiti di altri commenti. Cercate di capirlo altrimenti sarà per voi molto arduo continuare a leggere il tutorial dato che adesso ci stiamo avvicinando alle cose veramente difficili (non davvero però rispetto alla roba vista finora...).
Data Directory
Ok siamo arrivati alla DataDirectory che è un array di strutture:
DWORD VirtualAddress; // RVA della sezione
DWORD Size; // grandezza
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY
contenute nell'Optional Header. L'array contiene in tutto 15 entry, vediamo queste entry:
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
In questo tutorial discuteremo ogni singola entry. Andiamo con ordine, come possiamo vedere la prima entry corrisponde alla Export Directory e questa sarà la prima entry che discuteremo.
Suppongo che tutti abbiate già sentito parlare di Export Table... Quando si dice che una Dll esporta una funzione si intende che quella funzione all'interno dell'Export Table di quella Dll è segnata come funzione da esportare. Esempio: la funzione MessageBoxA è esportata dalla USER32.dll. Sto parlando di Dll ma non pensate che un Exe non possa esportare una funzione eh!
Vediamo adesso di addentrarci all'interno della Export Table. Ah, va prima detta una cosuccia. Vi ricorderete del discorso "VA - RVA - File Offset", ovvero la struttura di ogni entry nella Data Directory ci dice l'RVA del soggetto, quindi per trovare tale elemento dovremo convertire un RVA in un File Offset. Come è possibile? Vediamo velocemente questa piccola funzione:
{
DWORD Offset = Rva, Limit;
IMAGE_SECTION_HEADER *Img;
Img = IMAGE_FIRST_SECTION(NT);
if (Rva < Img->PointerToRawData)
return Rva;
for (int i = 0; i < NT->FileHeader.NumberOfSections; i++)
{
if (Img[i].SizeOfRawData)
Limit = Img[i].SizeOfRawData;
else
Limit = Img[i].Misc.VirtualSize;
if (Rva >= Img[i].VirtualAddress &&
Rva < (Img[i].VirtualAddress + Limit))
{
if (Img[i].PointerToRawData != 0)
{
Offset -= Img[i].VirtualAddress;
Offset += Img[i].PointerToRawData;
}
return Offset;
}
}
return NULL;
}
Questa funzione (che ho copiato da un foglio del mio wark) non fa altro che controllare gli spazi fisici tra le sezioni e calcolare l'RVA corrispondente a un certo offset fisico. Come vedete i paremetri della funzione sono solamente due: un Pointer a ImageNtHeader e l'RVA da convertire. Se la funzione non trova un offset fisico corrispondente ritorna NULL.
Ah piccola nota, tutte le entry della Data Dir si chiamano directory (come già detto) ma io mi ostino a chiamarle sezioni per abitudine (directory fa schifo, scusate). Quindi, se mi scappasse ogni tanto 'sezione' al posto di 'directory', non fateci caso, thx.
Export Table
Bene, adesso che sappiamo anche quest'ultimo cosa possiamo finalmente parlare di Export Table.
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Vediamo ad uno ad uno i campi:
Characteristics non so a cosa possa servire, in tutti i PE sta sempre a 0.
TimeDateStamp specifica la data di creazione.
MajorVersion e MinorVersion altri campi superflui che troverete sempre a 0.
Name è un RVA che punta al nome interno del modulo. Del tipo la kernel32.dll ha nome interno: KERNEL32.dll.
Base è un numero che va sommato all'indice della funzione per ottenere l'ordinal di tale funzione, la prima funzione esportata da un PE è pari a 0, se Base è pari a 1 dovremo fare 0 + 1 = 1 e avremo ottenuto l'ordinal di tale funzione.
NumberOfFunctions indica il numero di funzioni esportate dal modulo.
NumberOfNames indica il numero di funzioni esportate con nome. Come spero sappiate è possibile importare (e quindi anche esportare) una funzione sia per ordinal che per nome... Avete presente? No? Avete mai provato a usate GetProcAddress? Anche quest'API consente di prenderci l'indirizzo di una funzione di un modulo caricato in memoria sia per ordinal che per nome. Be' se non avete presente aprite un PE editor e l'API Reference.
AddressOfFunctions è un RVA che punta ad un array che contiene gli entry points di ogni funzione esportata. La grandezza di questo array è specificata da NumberOfFunctions.
AddressOfNames è un RVA che punta a un array di RVAs che puntano ai vari nomi delle funzioni. Come detto non è obbligatorio che a una funzione corrisponda anche un nome.
AddressOfNameOrdinals è un array di WORDs che contengono gli ordinals delle funzioni con nome. La grandezza di questo array è data da NumberOfNames, questo array ci serve per scovare quali sono le funzioni che hanno anche un nome.
Se siete dei Newbies sarete sicuramente ancora molto confusi... Io dovrei parlare ancora dell'Export Forwarding, ma lo faccio dopo; per adesso è meglio proporre un sorcio riassuntivo il cui scopo è quello di elencare le funzioni esportate da un modulo. Ah, del Size che ci fornisce la DataDir dell'ET me ne frego, non viene neanche considerato per enumerare le exports (tanto ancora non siamo arrivati al Forwarding).
#include <windows.h>
#include <stdio.h>
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva);
int main(int argc, char *argv[])
{
HANDLE hFile;
BYTE *BaseAddress;
DWORD FileSize, BR, ET_Offset;
IMAGE_DOS_HEADER *ImageDosHeader;
IMAGE_NT_HEADERS *ImageNtHeaders;
IMAGE_EXPORT_DIRECTORY *ImageExportDir;
DWORD *Functions, *Names;
WORD *NameOrds, x, y;
char *Name, *FName;
// controlla numero argomenti
if (argc < 2)
{
printf("\nNeed More Arguments\n");
return -1;
}
printf("\nOpening File...\n");
hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
FileSize = GetFileSize(hFile, NULL);
BaseAddress = (BYTE *) malloc(FileSize);
if (!ReadFile(hFile, BaseAddress, FileSize, &BR, NULL))
{
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageDosHeader = (IMAGE_DOS_HEADER *) BaseAddress;
// controlliamo il Dos Header
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("Invalid Dos Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageNtHeaders = (IMAGE_NT_HEADERS *)
(ImageDosHeader->e_lfanew + (DWORD) ImageDosHeader);
// controlliamo il PE Header
if (ImageNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
printf("Invalid PE Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
if (!ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
{
printf("This PE Doesn't contain an ET\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// converte in offset fisico l'RVA
ET_Offset = RvaToOffset(ImageNtHeaders,
ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// è valido?
if (ET_Offset == NULL)
{
printf("This PE Doesn't contain an ET\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
printf("Export Table:\n");
// ricava la Export Dir
ImageExportDir = (IMAGE_EXPORT_DIRECTORY *) (ET_Offset +
(DWORD) BaseAddress);
Name = (char *) (RvaToOffset(ImageNtHeaders,
ImageExportDir->Name) + (DWORD) BaseAddress);
// stampa le info importanti dell'ET
printf("\n\nName: %s\n"
"Base: %08X\n"
"Number Of Functions: %08X\n"
"Number Of Names: %08X\n"
"Addr Of Functions: %08X\n"
"Addr Of Names: %08X\n"
"Addr Of Name Ords: %08X\n\n"
"Exports:\n", Name,
ImageExportDir->Base,
ImageExportDir->NumberOfFunctions,
ImageExportDir->NumberOfNames,
ImageExportDir->AddressOfFunctions,
ImageExportDir->AddressOfNames,
ImageExportDir->AddressOfNameOrdinals);
// prende i vari arrays
Functions = (DWORD *) (RvaToOffset(ImageNtHeaders,
ImageExportDir->AddressOfFunctions) + (DWORD) BaseAddress);
Names = (DWORD *) (RvaToOffset(ImageNtHeaders,
ImageExportDir->AddressOfNames) + (DWORD) BaseAddress);
NameOrds = (WORD *) (RvaToOffset(ImageNtHeaders,
ImageExportDir->AddressOfNameOrdinals) + (DWORD) BaseAddress);
// enumera le funzioni
for (x = 0; x < ImageExportDir->NumberOfFunctions; x++)
{
// controllo se l'EP è 0
// se sì allora passa alla prossima funzione
if (Functions[x] == 0)
continue;
printf("\nOrd: %04X\nEP: %08X\n",
(x + ImageExportDir->Base), Functions[x]);
// vedo se la funzione ha anche un nome
for (y = 0; y < ImageExportDir->NumberOfNames; y++)
{
// trovata una funzione con nome?
if (NameOrds[y] == x)
{
FName = (char *) (RvaToOffset(ImageNtHeaders,
Names[y]) + (DWORD) BaseAddress);
printf("Name: %s\n", FName);
break;
}
}
}
free(BaseAddress);
CloseHandle(hFile);
return 0;
}
// sappiamo a cosa serve
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva)
{
DWORD Offset = Rva, Limit;
IMAGE_SECTION_HEADER *Img;
WORD i;
Img = IMAGE_FIRST_SECTION(NT);
if (Rva < Img->PointerToRawData)
return Rva;
for (i = 0; i < NT->FileHeader.NumberOfSections; i++)
{
if (Img[i].SizeOfRawData)
Limit = Img[i].SizeOfRawData;
else
Limit = Img[i].Misc.VirtualSize;
if (Rva >= Img[i].VirtualAddress &&
Rva < (Img[i].VirtualAddress + Limit))
{
if (Img[i].PointerToRawData != 0)
{
Offset -= Img[i].VirtualAddress;
Offset += Img[i].PointerToRawData;
}
return Offset;
}
}
return NULL;
}
Uhm, non credo ci sia altro da aggiungere ai commenti, il source è relativamente semplice. Adesso voglio perdere due paroline (anche perché in fondo non c'è molto da dire) sull'Export Forwarding. Ho visto che anche Pietrek lo ha trattato in alcuni articoli sul PE, ma a quanto pare anche Matt non ne sa più di quanto ne so io. La prima volta che mi trovai di fronte all'Export Forwarding (2 anni fa circa) era mentre stavo spulciando la mia wsock32.dll, volevo recarmi con IDA all'indirizzo di una funzione esportata per analizzarmela e mi sono trovato di fronte ad una stringa di questo tipo:
In effetti recandomi all'RVA-File Offset della funzione ho trovato questa forma:
cioè il nome di una dll seguito da un punto, quello di una funzione e il terminatore 0. A cosa serve tutto ciò si capisce intuitivamente, il primo nome indica il nome della dll da caricare e il secondo quello della funzione. Spiegandomi meglio, la funzione WSAAsyncGetHostByAddr nella Dll wsock32.dll corrisponde in verità alla funzione WSAAsyncGetHostByAddr nella Dll ws2_32.dll. Come si fa a capire se una funzione è presente nel modulo stesso o deve essere fowardata? Beh bisogna vedere se l'RVA della funzione è contenuto nella Export Table (basatevi su indirizzo e grandezza che vi fornisce la Data Directory). Con ciò dichiaro concluso il paragrafo sulla Export Table.
Import Table
Ridendo e scherzando (pluralis majestatis, voi avete poco di cui stare allegri) siamo giunti alla seconda entry della Data Directory: la Import Table. Questa è veramente una directory importantissima, senza averla capita non potete procedere in nulla... Anzi verranno proprio a bastonarvi a casa. In ogni caso state tranquilli, sebbene non sia più semplice della ET è almeno più divertente. Inoltre per la maggior parte di voi sarà sicuramente più proficuo conoscere la IT che la ET (senza voler sminuire la ET di importanza), specialmente chi vuole dedicarsi al reversing di crypters/packers deve conoscere perfettamente la IT, altrimenti il massimo che riuscirà a fare è di farsi ricostruire la IT da un tool ad hoc (wark compreso).
Per quanto riguarda l'IT ci serve solo sapere l'RVA di dove sta, il Size è totalmente inutile. Quindi se partiamo dall'RVA e ce lo convertiamo in File Offset, partendo dall'indirizzo calcolato troveremo un array di strutture IMAGE_IMPORT_DESCRIPTOR (ognuna di queste corrisponde ad un modulo da cui vengono importate funzioni). L'array si conclude con una struttura IMAGE_IMPORT_DESCRIPTOR nulla. Vediamoci tale struttura:
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
Vediamo i vari elementi della struttura:
OriginalFirstThunk è un RVA che punta ad un array di dwords ricordandoci che stiamo parlando a 32bit, sarebbe quindi più corretto dire un array dire strutture IMAGE_THUNK_DATA (che altro non sono che dwords o qword se siamo a 64bit) che possono avere significa diversi (che vedremo dopo). L'array si conclude con un elemento IMAGE_THUNK_DATA nullo.
TimeDateStamp generalmente è 0 ma se la IT è stata bind-ata (da bind: che neologismo. Probabilmente non sapete cosa vuol dire ma è una cosa che vedremo dopo quando parleremo della IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT, quindi se adesso non capite non preoccupatevi) teoricamente dovrebbe contenere la data del modulo che viene importato da questo descriptor, in ogni caso dovrebbe essere diverso da 0.
ForwarderChain serve ancora se la IT è stata bindata, comunque anche in quel caso semplicemente basterà settarlo diverso da 0 poiché fa parte di un metodo obsoleto per bindare.
Name questo campo è un RVA che punta a una stringa terminata da uno 0 che altro non è che il nome del modulo da importare. A questo proposito una piccola curiosità: se il nome del modulo da importare manca di estensione il loader di Win2k cerca un modulo senza estensione come suggerito, mentre quello di XP se non trova estensione fa automaticamente un strcat(NomeModulo, ".DLL").
FirstThunk è esattamente come OriginalFirstThunk un RVA che punta ad un array di IMAGE_THUNK_DATA ma non è assolutamente lo stesso array di OriginalFirstThunk, cioè può benissimo esserlo ma a quel punto, come in seguito vedremo, si può fare direttamente a meno dell'OriginalFirstThunk.
Bene, ma adesso cerchiamo di capire cosa può contenere un IMAGE_THUNK_DATA. Siccome il discorso non è proprio semplicissimo da capire alla prima lettura cerco di spiegarmi il meglio possibile. Dunque abbiamo detto che OriginalFirstThunk (dimentichiamoci un secondo di FirstThunk) è un array di DWORDs (sempre 32bit speaking: adesso smetto di dirlo eh) che servono per importare funzioni, ogni dword corrisponde ad una funzione importata dal modulo importato dal descriptor, l'array è terminato da una dword nulla. Ok, ma cosa vuol dire: serve ad importare una funzione? Ci sono due modi di importare una funzione: per nome e per ordinal (ordinal non credo che debba spiegarvi, semmai rileggetevi il paragrafo sull'ET). Se la funzione è importata per nome allora la dword è un RVA che punta ad una struttura IMAGE_IMPORT_BY_NAME, vediamone la dichiarazione:
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
La struttura consiste di due membri: una word che sarebbe l'ordinal della funzione da importare (che generalmente è sempre settato a 0 dato che non ce ne frega nulla di sapere l'ordinal se abbiamo il nome della funzione) e il nome della funzione che segue la word, il nome è terminato da uno 0. Quindi nel PE troviamo una cosa tipo: WORD_ORDINAL - NOME_FUNZIONE - 0.
L'altro metodo di importare una funzione è per ordinal, non abbiamo il nome della funzione né alcuna struttura IMAGE_IMPORT_BY_NAME e quindi risparmiamo diverso spazio... Però in genere è sconsigliato importare per ordinal poiché spesso le Dll (soprattutto quelle di sistema) non hanno sempre lo stesso ordinal per una funzione: su una versione di windows potrei avere una funzione con un certo ordinal e in un'altra versione quell'ordinal corrisponde ad una funzione diversa e questo ovviamente porterebbe a cose alquanto spiacevoli. Se una funzione è importata per ordinal il bit più alto della dword è settato, quindi non resta che prendersi solo gli altri 31 e considerarli come ordinal. Spiego meglio, sappiamo che se il bit più alto di una dword è settato (e solo quello) allora la dword è 80000000h, se l'ordinal della funzione che voglio importare è 45 allora l'IMAGE_THUNK_DATA sarà 80000045h (se siamo a 64 bit sarà 8000000000000045h). Per sapere se una funzione viene importata per ordinal basta fare un controllo come questo:
{
// la funzione è importata per ordinal
}
Spero sia tutto chiaro fin qui. FirstThunk svolge inizialmente (se il PE non è bindato) lo stesso compito di OriginalFirstThunk, anch'esso è un RVA che punta a un array di dword che a loro volta contengono gli stessi valori degli elementi dell'array a cui punta OriginalFirstThunk. Per esempio se l'array di OriginalFirstThunk contiene IMAGE_THUNK_DATA che puntano a strutture IMAGE_IMPORT_BY_NAME, FirstThunk punterà alle stesse strutture (cioè entrambi gli array contengono dword che devono importare la stessa funzione). Quando il loader carica un PE legge l'array di OriginalFirstThunk e sovrascrive quello di FirstThunk con i veri indirizzi delle funzioni in memoria ecco perché l'array puntato da FirstThunk costituisce la Import Address Table (o più semplicemente IAT).
Facciamo qualche specificazione, non è strettamente obbligatoria la presenza dell'array OriginalFirstThunk (e chi ha avuto a che fare coll'un/packing sa cosa intendo dire), si può fare a meno di questa prensenza settando OriginalFirstThunk a 0, in questo caso il loader considererà l'array puntato da FirstThunk per ricavare le funzioni da importare (quindi una volta loadato il PE in memoria non troveremo solo un array di FirstThunk sovrascritto). Nel caso però che un eseguibile lo si voglia bindare è strettamente necessaria la presenza di un OriginalFirstThunk: questo mi sembra importante ricordarlo per evitare di prendere il discorso troppo alla leggera: è bene che i PE abbiano entrambi gli array, poi se qualche packer elimina l'OFT (abbrevio) allora pazienza.
Spero che abbiate capito tutto, semmai rileggete... In ogni caso eccovi un esempio per fissare le idee (l'ho scritto per 32bit per semplificare le cose come tutti gli esempi in questo tutorial).
#include <windows.h>
#include <stdio.h>
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva);
int main(int argc, char *argv[])
{
HANDLE hFile;
BYTE *BaseAddress;
DWORD FileSize, BR, IT_Offset;
UINT x = 0, y;
IMAGE_DOS_HEADER *ImageDosHeader;
IMAGE_NT_HEADERS *ImageNtHeaders;
IMAGE_IMPORT_DESCRIPTOR *ImageImportDescr;
DWORD *Thunks;
char *Name;
IMAGE_IMPORT_BY_NAME *ImgName;
// controlla numero argomenti
if (argc < 2)
{
printf("\nNeed More Arguments\n");
return -1;
}
printf("\nOpening File...\n");
hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ,
0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
FileSize = GetFileSize(hFile, NULL);
BaseAddress = (BYTE *) malloc(FileSize);
if (!ReadFile(hFile, BaseAddress, FileSize, &BR, NULL))
{
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageDosHeader = (IMAGE_DOS_HEADER *) BaseAddress;
// controlliamo il Dos Header
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("Invalid Dos Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageNtHeaders = (IMAGE_NT_HEADERS *)
(ImageDosHeader->e_lfanew + (DWORD) ImageDosHeader);
// controlliamo il PE Header
if (ImageNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
printf("Invalid PE Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
if (!ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)
{
printf("This PE Doesn't contain an IT\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// converte in offset fisico l'RVA
IT_Offset = RvaToOffset(ImageNtHeaders,
ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// è valido?
if (IT_Offset == NULL)
{
printf("This PE Doesn't contain an IT\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
printf("Import Table:\n");
// ricava la Import Dir
// e quindi i descriptors
ImageImportDescr = (IMAGE_IMPORT_DESCRIPTOR *) (IT_Offset +
(DWORD) BaseAddress);
// passa in rassegna tutti i descriptors
while (ImageImportDescr[x].FirstThunk != 0)
{
Name = (char *) (RvaToOffset(ImageNtHeaders,
ImageImportDescr[x].Name) + (DWORD) BaseAddress);
printf("\nModule Name: %s\n\nFunctions:\n\n", Name);
// guarda quale array considerare
Thunks = (DWORD *) (RvaToOffset(ImageNtHeaders,
ImageImportDescr[x].OriginalFirstThunk != 0 ?
ImageImportDescr[x].OriginalFirstThunk :
ImageImportDescr[x].FirstThunk) + (DWORD) BaseAddress);
y = 0;
// passa in rassegna le funzioni
while (Thunks[y] != 0)
{
// è importata per ordinal?
if (Thunks[y] & IMAGE_ORDINAL_FLAG)
{
printf("Ordinal: %08X\n", (Thunks[y] -
IMAGE_ORDINAL_FLAG));
y++;
continue;
}
ImgName = (IMAGE_IMPORT_BY_NAME *) (RvaToOffset(
ImageNtHeaders, Thunks[y]) + (DWORD) BaseAddress);
printf("Name: %s\n", &ImgName->Name);
y++;
}
x++;
}
free(BaseAddress);
CloseHandle(hFile);
return 0;
}
// sappiamo a cosa serve
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva)
{
DWORD Offset = Rva, Limit;
IMAGE_SECTION_HEADER *Img;
WORD i;
Img = IMAGE_FIRST_SECTION(NT);
if (Rva < Img->PointerToRawData)
return Rva;
for (i = 0; i < NT->FileHeader.NumberOfSections; i++)
{
if (Img[i].SizeOfRawData)
Limit = Img[i].SizeOfRawData;
else
Limit = Img[i].Misc.VirtualSize;
if (Rva >= Img[i].VirtualAddress &&
Rva < (Img[i].VirtualAddress + Limit))
{
if (Img[i].PointerToRawData != 0)
{
Offset -= Img[i].VirtualAddress;
Offset += Img[i].PointerToRawData;
}
return Offset;
}
}
return NULL;
}
Spero sia tutto chiaro, come detto i concetti di IT, IAT ecc sono fondamentali per continuare, assicuratevi di averli assimilati.
NOTA
Prima di andare avanti è giunto il momento per introdurvi la imagehlp.dll che è una dll molto utile. Io ve la introduco ma non capirete ancora a cosa servono tutte le funzioni esportate... Però prima o poi devo comunque parlarne e farlo alla fine del tutorial mi pare un po' tardi.
Vediamo alcune delle funzioni più interessanti (riguardanti il PE) che esporta questa dll:
RemoveRelocations serve per rimuovere le rilocazioni da un PE (roba che comunque tratteremo più avanti.
BindImage/Ex di queste due funzioni discuterò quando tratterò di come bindare un exe.
CheckSumMappedFile calcola il checksum di un file mappato in memoria, vi ricordate del campo CheckSum nell'Optional Header vero?
MapFileAndCheckSumA/W mappa un file e calcola il suo checksum.
ImageDirectoryEntryToData/Ex servono per ricavare un puntatore per una specificata entry nella DataDir.
ImageNtHeader rivaca per voi l'ImageNtHeader partendo dalla base del file caricato in memoria.
ImageRvaToSection vi restituisce il section header della sezione a cui appartiene l'RVA passato.
ImageRvaToVa converte un RVA in un VA. Queste tre ultime funzioni in verità sono alquanto inutili dato che basta un secondo di coding per non doverle importare.
MapDebugInformation prende le informazioni di debug per un immagine e le mette in una struttura IMAGE_DEBUG_INFORMATION appositamente allocata. Dopo l'utilizzo è necessario usare UnmapDebugInformation per deallocare la struttura.
ReBaseImage questa funzione serve per cambiare il load address di un modulo per ridurre il tempo necessario per la sua esecuzione (tutti i cambiamenti che devono essere in seguito fatti vengono calcolati da questa funzione: dbg info, checksum ecc.). Capirete meglio a cosa serve la funzione quando arriveremo alle relocations.
In ogni caso ci sono anche altre funzioni esportate da questa dll interessanti che magari non sono essenziali per conoscere il PE ma che potrebbero fare comodo prima o poi quindi datevi uno sguardo a questa dll.
Bene passiamo al prossimo paragrafo.
Resource Directory
Siamo giunti alla entry più noiosa di tutto il formato PE... Che bello! Suppongo che tutti abbiate almeno presente cosa contiene questa directory, be' nel caso così non fosse, essa contiene tutte le risorse tipo icone, bitmaps, dialogs, menus, string tables, version info di un PE. Se non avete presente provate ad aprire una volta Resource Hacker (che tra l'altro non amo neanche troppo come prog... D'altronde non c'è di meglio). C'è da dire che, nonostante l'interessamento che questa directory può suscitare, essa è sempre stata trattata poco rispetto alle altre directory. Ovviamente è doveroso ringraziare Pietrek per aver portato (già agli albori del PE) la luce sull'argomento risorse, ma in ogni caso io per questa directory ho fatto affidamento solo al WinNt.h (che contiene un casino di informazioni), ad un hex editor e a un resource walker. In ogni caso non è tutto sto gran casino, è solo un po' ostica. Vedrete che alla fin fine tutto si riduce solo ad una gerarchia di strutture.
Partiamo dalla Data Directory, la prima struttura che ci viene incontro è IMAGE_RESOURCE_DIRECTORY. Vediamoci la struttura.
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
Characteristics primo membro inutile (sempre a 0).
TimeDateStamp dovrebbe segnare il tempo di creazione delle risorse: secondo membro inutile.
MajorVersion e MinorVersion terzo e quarto membro inutili.
NumberOfNamedEntries numero di Directory Entries ad avere un nome.
NumberOfIdEntries numero di Directory Entries ad avere un ID.
DirectoryEntries array di strutture IMAGE_RESOURCE_DIRECTORY_ENTRY che seguono la struttura IMAGE_RESOURCE_DIRECTORY. Per ottenere la grandezza è necessario sommare NumberOfNamedEntries a NumberOfIdEntries. Faccio notare che non è veramente un membro della struttura.
Ok, ma cosa vuol dire tutto ciò? Dunque in un PE tutte le risorse sono ordinate per tipo, le dialogs stanno nella directory delle dialogs, le bitmaps in quella delle bitmaps ecc. Se voi aprite un programma come ad esempio Resource Hacker, ma in ogni caso qualsiasi resource walker andrà bene, esso vi mostrerà le diverse directory: per esempio aprendo la Resource Section del wark trovo le seguenti directory:
CURSORS - BITMAPS - ICONS - MENUS - DIALOGS - STRING TABLES - ACCELERATORS - CURSOR GROUPS - ICON GROUPS - VERSION INFO
Potremmo definire queste directory il secondo livello della Resource Section (il primo livello è dato dalla Resource Dir da cui parte tutto). Vediamo la struttura IMAGE_RESOURCE_DIRECTORY_ENTRY:
union {
struct {
DWORD NameOffset:31;
DWORD :1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
La struttura è composta da due dword sole:
Name se in questa prima dword (ho scelto name perché era appunto l'unica dword della union) il bit più alto è settato allora i rimanenti 31 bit indicano l'offset del nome dell'entry a partire dalla Reource Dir, ovvero: Name = NameOffset + ResourceDirOffset. Altrimenti se il bit più alto è nullo, allora è necessario considerare solo l'ID. Capire se il bit più alto è settato o no è facile, si può fare in due modi:
oppure:
Nel caso avesse effettivamente un nome e noi trovassimo l'offset allora dovremmo ricondurre questo offset ad una struttura IMAGE_RESOURCE_DIR_STRING_U:
WORD Length;
WCHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
La word indica la lungezza della stringa seguita da un array di words che rappresenterebbe la la stringa (unicode). C'è da fare però ancora un discorso sugli ID, nel caso di directories predefinite come DIALOGS et simil, esse non usufruiscono di nome ma di ID, quindi ci sono alcuni ID che vanno necessariamente associati ad un nome (ricordatevi che stiamo parlando del secondo livello, mi raccomando!), eccovi un piccolo schema:
1 CURSORS
2 BITMAPS
3 ICONS
4 MENUS
5 DIALOGS
6 STRING TABLES
7 FONT DIRECORY
8 FONTS
9 ACCELERATORS
10 RCDATA
11 MESSAGE TABLES
12 CURSOR GROUPS
14 ICON GROUPS
16 VERSION INFO
23 HTML PAGES
24 CONFIGURATION FILES
Dal .NET in poi esiste anche questa risorsa, penso anzi di essere il primo che l'ha menzionata in un tutorial sul PE, all'interno vi sono info generali/direttive sullo startup, l'esecuzione ecc. Se volete approfondire il discorso, sul msdn trovate tutte le info che vi servono (che non sono poche). Inoltre eccovi una piccola descrizione approssimativa sempre presa dal msdn:
Configuration Files are standard XML files. The .NET Framework defines a set of elements that implement configuration settings. This section describes the configuration schema for the machine configuration file, application configuration files, and the security configuration file.
Accorgermi dello schema sovrastante (al tempo) non è stato difficile è bastato stampare, con un prog fatto da me, gli IDs e confrontarli con il risultato di un resource walker: associare quindi un ID ad un nome è stato semplice (alcuni prog dovrebbero anche dirvi l'ID oltre al nome, quindi: figuriamoci). In ogni caso la mia tabella è forse più completa di molte altre proprio perché deriva dall'osservazione di tanti PE. Between, ho visto che Pietrek nel suo celebre PEDump (ultima versione) omette gli ultimi due tipi (23, 24) e ne mette di altri prima che però a essere sincero non mi è mai capitato di vedere... In ogni caso siete liberi di fare altre ricerche per approfondire il discorso.
OffsetToData questa dword invece ci interesserà quando parleremo del terzo e successivi livello/i, il bit più alto indica se settato che i 31 bit rappresentano l'offset (a partire dalla Res Dir) di una struttura IMAGE_RESOURCE_DIRECTORY, se invece il bit non è settato allora l'offset punta (sempre a partire dalla Res Dir) ad una struttura IMAGE_RESOURCE_DATA_ENTRY. Per capire se il bit alto è settato potete fare:
o:
Ma adesso prima di andarci ad occupare degli altri livelli costituiti da altre Res Dir (oltre a quella principale da cui parte tutto: primo livello) e prima di andare a vedere cosa è la struttura IMAGE_RESOURCE_DATA_ENTRY, vediamo un piccolo prog che non fa altro fuorché elencarci le directories di secondo livello.
#include <windows.h>
#include <stdio.h>
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva);
char *ResNames[] = {
"CURSORS", "BITMAPS", "ICONS", "MENUS", "DIALOGS",
"STRING TABLES", "FONT DIRECORY", "FONTS", "ACCELERATORS",
"RCDATA", "MESSAGE TABLES", "CURSOR GROUPS"
};
int main(int argc, char *argv[])
{
HANDLE hFile;
BYTE *BaseAddress;
DWORD FileSize, BR, Res_Offset;
IMAGE_DOS_HEADER *ImageDosHeader;
IMAGE_NT_HEADERS *ImageNtHeaders;
IMAGE_RESOURCE_DIRECTORY *ImgResDir;
IMAGE_RESOURCE_DIRECTORY_ENTRY *ImgResDirEntry;
DWORD ResDirs, x;
IMAGE_RESOURCE_DIR_STRING_U *uString;
char DirName[100];
// controlla numero argomenti
if (argc < 2)
{
printf("\nNeed More Arguments\n");
return -1;
}
printf("\nOpening File...\n");
hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ,
0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
FileSize = GetFileSize(hFile, NULL);
BaseAddress = (BYTE *) malloc(FileSize);
if (!ReadFile(hFile, BaseAddress, FileSize, &BR, NULL))
{
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageDosHeader = (IMAGE_DOS_HEADER *) BaseAddress;
// controlliamo il Dos Header
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("Invalid Dos Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageNtHeaders = (IMAGE_NT_HEADERS *)
(ImageDosHeader->e_lfanew + (DWORD) ImageDosHeader);
// controlliamo il PE Header
if (ImageNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
printf("Invalid PE Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
if (!ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress)
{
printf("This PE Doesn't Contain Resources\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// converte in offset fisico l'RVA
Res_Offset = RvaToOffset(ImageNtHeaders,
ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress);
// è valido?
if (Res_Offset == NULL)
{
printf("This PE Doesn't Contain Resources\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// ricavo la Resource Dir
ImgResDir = (IMAGE_RESOURCE_DIRECTORY *)(DWORD)
(Res_Offset + (DWORD) BaseAddress);
// ricavo il numero delle Resource Dirs
ResDirs = ImgResDir->NumberOfIdEntries +
ImgResDir->NumberOfNamedEntries;
printf("\nNumber of Resource Directories: %d\n", ResDirs);
// ricavo il puntatore alle Res Dirs
ImgResDirEntry = (IMAGE_RESOURCE_DIRECTORY_ENTRY *)
(DWORD) (sizeof (IMAGE_RESOURCE_DIRECTORY) + (DWORD) ImgResDir);
printf("\nDirectories:\n\n");
// mostro il 'secondo livello
for (x = 0; x < ResDirs; x++)
{
printf("\nDirectory %d", (x + 1));
// la dir ha nome?
if (ImgResDirEntry[x].NameIsString == TRUE)
{
uString = (IMAGE_RESOURCE_DIR_STRING_U *)
(DWORD) (ImgResDirEntry[x].NameOffset +
(DWORD) ImgResDir);
ZeroMemory(DirName, sizeof(DirName));
// converto la stringa unicode
WideCharToMultiByte(CP_ACP, NULL,
(LPCWSTR) &uString->NameString, uString->Length,
DirName, sizeof (DirName), NULL, NULL);
printf(" Name: %s\n", DirName);
}
else
{
// stampa l'ID
printf(" ID: %d", ImgResDirEntry[x].Id);
// controllo se l'ID risulta tra quelli identificati
if (ImgResDirEntry[x].Id > 0 &&
ImgResDirEntry[x].Id < 13)
{
printf(" - %s", ResNames[ImgResDirEntry[x].Id - 1]);
}
else if (ImgResDirEntry[x].Id == 14)
{
printf(" - ICON GROUPS");
}
else if (ImgResDirEntry[x].Id == 16)
{
printf(" - VERSION INFO");
}
else if (ImgResDirEntry[x].Id == 23)
{
printf(" - HTML PAGES");
}
else if (ImgResDirEntry[x].Id == 24)
{
printf(" - CONFIGURATION FILES");
}
printf("\n");
}
}
free(BaseAddress);
CloseHandle(hFile);
return 0;
}
// sappiamo a cosa serve
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva)
{
DWORD Offset = Rva, Limit;
IMAGE_SECTION_HEADER *Img;
WORD i;
Img = IMAGE_FIRST_SECTION(NT);
if (Rva < Img->PointerToRawData)
return Rva;
for (i = 0; i < NT->FileHeader.NumberOfSections; i++)
{
if (Img[i].SizeOfRawData)
Limit = Img[i].SizeOfRawData;
else
Limit = Img[i].Misc.VirtualSize;
if (Rva >= Img[i].VirtualAddress &&
Rva < (Img[i].VirtualAddress + Limit))
{
if (Img[i].PointerToRawData != 0)
{
Offset -= Img[i].VirtualAddress;
Offset += Img[i].PointerToRawData;
}
return Offset;
}
}
return NULL;
}
Questo esempio elenca, nel caso siano presenti, prima le dirs con nomi, perché? Semplicemente perché fisicamente le dirs con nomi vengono prima di quelle identificate per ID.
Ok, adesso possiamo parlare dei livelli successivi. Cominciamo col parlare del terzo livello che è comune a tutte le normali dir (parleremo dopo di altri livelli). Noi sappiamo cosa rappresenta il secondo livello ma prendiamo una dir di questo secondo livello, per esempio quella delle dialogs (ho scelto a caso eh) a cosa punterà la entry della dialogs (ID 5) ? Be' semplice, ad un'altra IMAGE_RESOURCE_DIRECTORY a cui seguirà un array di IMAGE_RESOURCE_DIRECTORY_ENTRY (una per ogni dlg). Come per le dir ci saranno Dlgs con nome o con ID. Una volta arrivati alle entry delle Dlgs queste a loro volta punteranno nuovamente a una IMAGE_RESOURCE_DIRECTORY che avrà solamente una IMAGE_RESOURCE_DIRECTORY_ENTRY che questa volta punterà a una struttura IMAGE_RESOURCE_DATA_ENTRY, vediamo tale struttura:
DWORD OffsetToData;
DWORD Size;
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
OffsetToData offset della raw data a partire dalla Res Dir (è sempre così).
Size dimensioni della raw data.
CodePage oramai è sempre unicode page.
Reserved .... Riservato? gh
So che il discorso non è così semplice ma eccovi un esempio di codice, che sebbene sia stato scritto in modo veloce, potrà chiarirvi forse le idee.
#include <windows.h>
#include <stdio.h>
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva);
int main(int argc, char *argv[])
{
HANDLE hFile;
BYTE *BaseAddress;
DWORD FileSize, BR, Res_Offset;
IMAGE_DOS_HEADER *ImageDosHeader;
IMAGE_NT_HEADERS *ImageNtHeaders;
IMAGE_RESOURCE_DIRECTORY *ImgResDir;
IMAGE_RESOURCE_DIRECTORY_ENTRY *ImgResDirEntry;
DWORD ResDirs, x;
IMAGE_RESOURCE_DIR_STRING_U *uString;
char DlgName[100];
BOOL bFound = FALSE;
IMAGE_RESOURCE_DIRECTORY *ImgDlgsResDir;
IMAGE_RESOURCE_DIRECTORY_ENTRY *ImgDlgsResDirEntry;
DWORD Dlgs;
IMAGE_RESOURCE_DIRECTORY *ImgDlgDir;
IMAGE_RESOURCE_DIRECTORY_ENTRY *ImgDlgEntry;
IMAGE_RESOURCE_DATA_ENTRY *ImgDlgDataEntry;
// controlla numero argomenti
if (argc < 2)
{
printf("\nNeed More Arguments\n");
return -1;
}
printf("\nOpening File...\n");
hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ,
0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Cannot Open the File\n");
return -1;
}
FileSize = GetFileSize(hFile, NULL);
BaseAddress = (BYTE *) malloc(FileSize);
if (!ReadFile(hFile, BaseAddress, FileSize, &BR, NULL))
{
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageDosHeader = (IMAGE_DOS_HEADER *) BaseAddress;
// controlliamo il Dos Header
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("Invalid Dos Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
ImageNtHeaders = (IMAGE_NT_HEADERS *)
(ImageDosHeader->e_lfanew + (DWORD) ImageDosHeader);
// controlliamo il PE Header
if (ImageNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
printf("Invalid PE Header\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
if (!ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress)
{
printf("This PE Doesn't Contain Resources\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// converte in offset fisico l'RVA
Res_Offset = RvaToOffset(ImageNtHeaders,
ImageNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress);
// è valido?
if (Res_Offset == NULL)
{
printf("This PE Doesn't Contain Resources\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// ricavo la Resource Dir
ImgResDir = (IMAGE_RESOURCE_DIRECTORY *)(DWORD)
(Res_Offset + (DWORD) BaseAddress);
// ricavo il numero delle Resource Dirs
ResDirs = ImgResDir->NumberOfIdEntries +
ImgResDir->NumberOfNamedEntries;
printf("\nNumber of Resource Directories: %d\n", ResDirs);
// ricavo il puntatore alle Res Dirs
ImgResDirEntry = (IMAGE_RESOURCE_DIRECTORY_ENTRY *)
(DWORD) (sizeof (IMAGE_RESOURCE_DIRECTORY) + (DWORD) ImgResDir);
printf("\nDialogs:\n\n");
// trovo la Res Dir per le dialogs
for (x = 0; x < ResDirs; x++)
{
if (ImgResDirEntry[x].NameIsString == FALSE &&
ImgResDirEntry[x].Id == 5)
{
bFound = TRUE;
break;
}
}
// non esiste?
if (bFound == FALSE)
{
printf("This PE Doesn't contain a Dialogs Dir\n");
free(BaseAddress);
CloseHandle(hFile);
return -1;
}
// tanto mi serve solo questa Entry per le Dlgs
ImgResDirEntry = (IMAGE_RESOURCE_DIRECTORY_ENTRY *)
(DWORD) &ImgResDirEntry[x];
// ci dovrà pur essere una Res Dir per le dlgs
if (ImgResDirEntry->DataIsDirectory == TRUE)
{
ImgDlgsResDir = (IMAGE_RESOURCE_DIRECTORY *)(DWORD)
(ImgResDirEntry->OffsetToDirectory +
(DWORD) ImgResDir);
// sommo le dlgs con nome a quelle senza
// e ottengo il totale
Dlgs = ImgDlgsResDir->NumberOfNamedEntries +
ImgDlgsResDir->NumberOfIdEntries;
printf("\nNumber of Dialogs: %d\n", Dlgs);
// prendo l'array di dlgs
ImgDlgsResDirEntry = (IMAGE_RESOURCE_DIRECTORY_ENTRY *)
(DWORD) (sizeof (IMAGE_RESOURCE_DIRECTORY) +
(DWORD) ImgDlgsResDir);
// elenco tutte le dlgs
for (x = 0; x < Dlgs; x++)
{
printf("\nDialog %d", (x + 1));
// la dlg ha nome?
if (ImgDlgsResDirEntry[x].NameIsString == TRUE)
{
uString = (IMAGE_RESOURCE_DIR_STRING_U *)
(DWORD) (ImgDlgsResDirEntry[x].NameOffset +
(DWORD) ImgResDir);
ZeroMemory(DlgName, sizeof(DlgName));
// converto la stringa unicode
WideCharToMultiByte(CP_ACP, NULL,
(LPCWSTR) &uString->NameString, uString->Length,
DlgName, sizeof (DlgName), NULL, NULL);
// stampa il nome
printf(" Name: %s\n", DlgName);
}
else
{
// stampa l'ID
printf(" ID: %d\n", ImgDlgsResDirEntry[x].Id);
}
// be' è sempre uguale
// devo ricavare la res dir per la dlg
if (ImgDlgsResDirEntry[x].DataIsDirectory == TRUE)
{
ImgDlgDir = (IMAGE_RESOURCE_DIRECTORY *)
(DWORD) (ImgDlgsResDirEntry[x].OffsetToDirectory +
(DWORD) ImgResDir);
ImgDlgEntry = (IMAGE_RESOURCE_DIRECTORY_ENTRY *)
(DWORD) (sizeof (IMAGE_RESOURCE_DIRECTORY) +
(DWORD) ImgDlgDir);
// finalmente arrivo alla Data Entry
ImgDlgDataEntry = (IMAGE_RESOURCE_DATA_ENTRY *)
(DWORD) (ImgDlgEntry->OffsetToData +
(DWORD) ImgResDir);
// stampe le info riguardanti la dlg
// ometto offset e reserved che sono
// inutili a titolo di Info
printf("Size: %d bytes - Code Page: %d\n",
ImgDlgDataEntry->Size, ImgDlgDataEntry->CodePage);
}
}
}
free(BaseAddress);
CloseHandle(hFile);
return 0;
}
// sappiamo a cosa serve
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva)
{
DWORD Offset = Rva, Limit;
IMAGE_SECTION_HEADER *Img;
WORD i;
Img = IMAGE_FIRST_SECTION(NT);
if (Rva < Img->PointerToRawData)
return Rva;
for (i = 0; i < NT->FileHeader.NumberOfSections; i++)
{
if (Img[i].SizeOfRawData)
Limit = Img[i].SizeOfRawData;
else
Limit = Img[i].Misc.VirtualSize;
if (Rva >= Img[i].VirtualAddress &&
Rva < (Img[i].VirtualAddress + Limit))
{
if (Img[i].PointerToRawData != 0)
{
Offset -= Img[i].VirtualAddress;
Offset += Img[i].PointerToRawData;
}
return Offset;
}
}
return NULL;
}
L'esempio è stato effettuato su delle dlg, ma qualsiasi tipo di risorsa standard va bene. Per quanto riguarda i livelli dicevo, possono anche aumentare come ben capite, ma per le normali risorse i livelli sono tre (quindi non preoccupatevi). Ora non vi resta che approfondire il discorso sui 'tipi' di risorse che si possono trovare, ma questo va al di là dell'argomento trattato da questo tutorial che è appunto la struttura del PE.
Beh se siete arrivati fino a questo punto del tutorial e avete compreso tutto allora adesso la strada è tutta in discesa... Le cose peggiori sono passate.
Exceptions Directory
Questo paragrafo sarà molto breve anche perché non c'è molto da dire o al momento non vedo la necessità di tirare il discorso per le lunghe. Alcune architetture fanno uso di tabelle per segnalare le funzioni nelle quali potrebbe verificarsi un'eccezione, nelle tabelle sono contenute diverse informazioni a seconda dell'architettura. La sezione Exceptions è solo un array di strutture IMAGE_RUNTIME_FUNCTION_ENTRY, il numero di elementi dell'array lo ricaviamo dividendo la grandezza della sezione per le dimensioni della struttura IMAGE_RUNTIME_FUNCTION_ENTRY. Il nome IMAGE_RUNTIME_FUNCTION_ENTRY è solo il ricavato di un typedef che cambia a seconda dell'architettura sulla quale stiamo compilando, ecco le diverse strutture dichiarate nel Winnt.h:
typedef struct _IMAGE_IA64_RUNTIME_FUNCTION_ENTRY {
DWORD BeginAddress;
DWORD EndAddress;
DWORD UnwindInfoAddress;
} IMAGE_IA64_RUNTIME_FUNCTION_ENTRY, *PIMAGE_IA64_RUNTIME_FUNCTION_ENTRY;
// per alpha/alpha64
typedef struct _IMAGE_ALPHA_RUNTIME_FUNCTION_ENTRY {
DWORD BeginAddress;
DWORD EndAddress;
DWORD ExceptionHandler;
DWORD HandlerData;
DWORD PrologEndAddress;
} IMAGE_ALPHA_RUNTIME_FUNCTION_ENTRY, *PIMAGE_ALPHA_RUNTIME_FUNCTION_ENTRY;
typedef struct _IMAGE_ALPHA64_RUNTIME_FUNCTION_ENTRY {
ULONGLONG BeginAddress;
ULONGLONG EndAddress;
ULONGLONG ExceptionHandler;
ULONGLONG HandlerData;
ULONGLONG PrologEndAddress;
} IMAGE_ALPHA64_RUNTIME_FUNCTION_ENTRY, *PIMAGE_ALPHA64_RUNTIME_FUNCTION_ENTRY;
// per Win CE
typedef struct _IMAGE_CE_RUNTIME_FUNCTION_ENTRY {
DWORD FuncStart;
DWORD PrologLen : 8;
DWORD FuncLen : 22;
DWORD ThirtyTwoBit : 1;
DWORD ExceptionFlag : 1;
} IMAGE_CE_RUNTIME_FUNCTION_ENTRY, * PIMAGE_CE_RUNTIME_FUNCTION_ENTRY;
A tutte le strutture è comune il BeginAddres e EndAddress (in Win CE ricavabile) della funzione, le informazioni aggiuntive riguardano la gestione dell'eccezione per la funzione relativa alla struttura. Per approfondire i parametri di gestione fatevi un giro sul msdn e andate a cercare per l'architettura che vi interessa, sono sicuro che troverete tutto.
Non credo che per questo paragrafo sia necessario un esempio di codice.... Sono 4 stupidaggini... Quindi passiamo pure al prossimo paragrafo.
Security Directory
Non c'è nulla da dire su questa directory, evviva l'inutilità!