Tutto (o quasi) sul Portable Executable

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 <windows.h>
#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 CreateFileMapping(

   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.


LPVOID MapViewOfFile(

   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:


BOOL 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):


Offset     0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  Ascii

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:

typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header

    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ì:

PIMAGE_DOS_HEADER pDosHeader;

Basterà farlo puntare al nostro BaseAddress del PE file per avere una struttura piena di elementi, es:

pDosHeader = (PIMAGE_DOS_HEADER) hBaseAddress;

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 <windows.h>
#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:


typedef struct _IMAGE_NT_HEADERS {

    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:

Offset     0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  Ascii

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:

typedef struct _IMAGE_FILE_HEADER {

    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_UNKNOWN        0  // sconosciuta
#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_RELOCS_STRIPPED       0x0001 // Relocation info stripped from file.
#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.
#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.


typedef struct _IMAGE_OPTIONAL_HEADER {

    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_HDR32_MAGIC     0x10b // normale eseguibile 32bit
#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:

ZONA HEADER  - SIZE: Multiplo di 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_UNKNOWN           0 // sottosistema sconosciuto
#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.


typedef struct _IMAGE_DATA_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 è:


ImageSectionHeader = IMAGE_FIRST_SECTION(ImageNtHeaders);

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.

typedef struct _IMAGE_SECTION_HEADER {
    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):

00000020h - la sezione contiene codice
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:

#define IMAGE_SCN_CNT_CODE                 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA     0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA   0x00000080 // Section contains uninitialized data.

#define IMAGE_SCN_LNK_OTHER                0x00000100 // Reserved.

#define IMAGE_SCN_LNK_INFO                 0x00000200 // Section contains comments or some other type of information.
 // IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.

#define IMAGE_SCN_LNK_REMOVE               0x00000800 // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT               0x00001000 // Section contents comdat.
// 0x00002000 // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000

#define IMAGE_SCN_NO_DEFER_SPEC_EXC        0x00004000 // Reset speculative exceptions handling bits in the TLB entries for this section.
#define IMAGE_SCN_GPREL                    0x00008000 // Section content can be accessed relative to GP
#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

#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 // Default alignment if no others are specified.
#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
#define IMAGE_SCN_LNK_NRELOC_OVFL          0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE          0x02000000 // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED           0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED            0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED               0x10000000 // Section is shareable.
#define IMAGE_SCN_MEM_EXECUTE              0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ                 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE                0x80000000 // Section is writeable.

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 <windows.h>
#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.

// sintassi: nome_file nome_nuova_sezione dimensioni_sezione

#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:


typedef struct _IMAGE_DATA_DIRECTORY {
    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_EXPORT            0 // Export Directory
#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 RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva)
{
   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.


typedef struct _IMAGE_EXPORT_DIRECTORY {
    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).


// sintassi: nome_file

#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:

WSAAsyncGetHostByAddr db 'ws2_32.WSAAsyncGetHostByAddr', 0

In effetti recandomi all'RVA-File Offset della funzione ho trovato questa forma:

ws2_32.WSAAsyncGetHostByAddr, 0

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:


typedef struct _IMAGE_IMPORT_DESCRIPTOR {

    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:


typedef struct _IMAGE_IMPORT_BY_NAME {
    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:


if (ImgTDataValue & IMAGE_ORDINAL_FLAG)
{
    // 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).


// sintassi: nome_file

#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.


typedef struct _IMAGE_RESOURCE_DIRECTORY {
    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:

typedef struct _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:


if (NameIsString == TRUE)

oppure

if (Name & IMAGE_RESOURCE_NAME_IS_STRING)

Nel caso avesse effettivamente un nome e noi trovassimo l'offset allora dovremmo ricondurre questo offset ad una struttura IMAGE_RESOURCE_DIR_STRING_U:

typedef struct _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:

ID DIRECTORY

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:


if (DataIsDirectory == TRUE)

o:

if (OffsetToData & IMAGE_RESOURCE_DATA_IS_DIRECTORY)

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.

// sintassi: nome_file

#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:


typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    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.


// sintassi: nome_file

#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:


// per architettura IA-64

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à!


BASE RELOCATION TABLE

Ecco questo paragrafo mi sembra molto interessante, tranquilli che non è difficile. Questa directory che generalmente rappresenta la sezione .reloc di un PE è molto utile, anche se spesso viene messa inutilmente da dei compilatori (TASM sucks). Immaginate di avere un processo con diverse dlls, mettiamo che due dll abbiano lo stesso Image Base, in questo caso la seconda che viene caricata non può venir caricata allo stesso indirizzo della prima... Il loader è quindi costretto a caricare la dll (insomma un qualsiasi modulo) ad una diversa locazione. Ok però fatto questo tutti gli indirizzi basati su VAs all'interno della dll saranno sbagliati dato che l'Image Base indicato dal PE non è quello utilizzato dal loader. Per ovviare questo problema il loader dovrà aggiornare tutti gli indirizzi all'interno della dll, ma chi glieli dice questi indirizzi? Proprio questo è il compito della Relocation Table. Tutta la sezione non è altro che un array (eh lo so queste ultime sezioni vanno parecchio per array) di strutture IMAGE_BASE_RELOCATION, vediamo tale struttura:


typedef struct _IMAGE_BASE_RELOCATION {
    DWORD VirtualAddress;
    DWORD SizeOfBlock;
 // WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

VirtualAddress si tratta di un RVA che specifica l'indirizzo della zona da aggiornare.

SizeOfBlock specifica i byte di questa base relocation.

TypeOffset (che non è veramente un membro della struttura) è un array di word, le dimensioni di questo array si possono ricavare da SizeOfBlock: bisogna solo sottrarre a SizeOfBlock il valore 8 (che sarebbe la dimensione della struttura senza contare l'array) e dividere per la grandezza di una WORD. Ma a cosa servono queste word? Queste word sono composte da due parti. I 4 bit più alti di ogni dword indicano il tipo di allocazione, i tipi disponibili sono:


#define IMAGE_REL_BASED_ABSOLUTE         0
#define IMAGE_REL_BASED_HIGH             1
#define IMAGE_REL_BASED_LOW              2
#define IMAGE_REL_BASED_HIGHLOW          3
#define IMAGE_REL_BASED_HIGHADJ          4
#define IMAGE_REL_BASED_MIPS_JMPADDR     5
#define IMAGE_REL_BASED_SECTION          6
#define IMAGE_REL_BASED_REL32            7

#define IMAGE_REL_BASED_MIPS_JMPADDR16   9
#define IMAGE_REL_BASED_IA64_IMM64       9
#define IMAGE_REL_BASED_DIR64           10
#define IMAGE_REL_BASED_HIGH3ADJ        11

I più comuni sono:

IMAGE_REL_BASED_ABSOLUTE se la rilocazione è di questo tipo non viene effettuato nulla, è una rilocazione che sta lì per un semplice discorso di allineamento.

IMAGE_REL_BASED_HIGHLOW l'x86 usa sempre questo tipo di rilocazione, il significato è che bisogna aggiornare la zona interessata sia con la parte alta che quella bassa del delta (che è una dword, vedremo dopo cosa è e come si calcola il delta).

IMAGE_REL_BASED_DIR64 per quanto riguarda IA-64 (fidandoci stavolta di Pietrek) è sempre questo il tipo di rilocazione.

I restanti 12 bit della word sono un offset che sommato all'RVA di VirtualOffset portano ad un puntatore dword a cui sommare il, già menzionato, delta. Il delta per le rilocazione è molto semplice da calcolare, è sufficiente sottrarre il vecchio Image Base al nuovo Image Base, es:


Delta = NewImgBase - OldImageBase;

Come già detto arrivando a un offset, sommando VirtualOffset + 12 bit della word, giungiamo ad una dword che punterà ad una zona che deve essere aggiornata, basterà quindi fare Block += Delta per ottenere l'aggiornamento (di indirizzi nella IAT, indirizzi di stringhe... Quello che vi pare). Come capirete questo sistema di rilocazione è molto veloce e facile da usare. Questa volta sebbene sia facile ritengo comunque che sia opportuno mettere un piccolo codice che elenca le rilocazioni in un programma.

// sintassi: nome_file

#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, Reloc_Offset, Reloc_Size;

   IMAGE_DOS_HEADER *ImageDosHeader;
   IMAGE_NT_HEADERS *ImageNtHeaders;

   IMAGE_BASE_RELOCATION *ImageRelocation;

   DWORD Size = 0;

   // 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_BASERELOC].VirtualAddress)
   {
      printf("This PE doesn't contain Relocations\n");
      free(BaseAddress);
      CloseHandle(hFile);
      return -1;
   }

   // converte in offset fisico l'RVA
   Reloc_Offset = RvaToOffset(ImageNtHeaders,
      ImageNtHeaders->OptionalHeader.DataDirectory
      [IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

   Reloc_Size = ImageNtHeaders->OptionalHeader.DataDirectory
      [IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;

   // è valido?
   if (Reloc_Offset == NULL)
   {
      printf("This PE doesn't contain Relocations\n");
      free(BaseAddress);
      CloseHandle(hFile);
      return -1;
   }

   printf("\nRelocations:");

   // continua il ciclo finché
   // la size attuale è minore
   // della totale dimensione
   // della Reloc Table

   while (Size < Reloc_Size)
   {
      // ricavo l'attuale reloc base
      ImageRelocation = (IMAGE_BASE_RELOCATION *)(DWORD)
         (Reloc_Offset + Size + (DWORD) BaseAddress);

      // stampo le info relative alla reloc base
      printf("\n\nVirtual Addr: %X\n"
         "Size Of Block: %X\n"
         "Type Offset Members %d",
         ImageRelocation->VirtualAddress,
         ImageRelocation->SizeOfBlock,
         ((ImageRelocation-> SizeOfBlock -
         IMAGE_SIZEOF_BASE_RELOCATION) / sizeof (WORD)));

      // somma al size attuale la grandezza
      // della reloc attuale

      Size += ImageRelocation->SizeOfBlock;
   }


   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;
}

Be' già il paragrafo è semplice e altri commenti al codice non mi sembrano necessari, quindi andiamo avanti.


DEBUG DIRECTORY

Questa directory consiste solo in un array di strutture IMAGE_DEBUG_DIRECTORY che specificano ogni tipo di informazione che potrebbe servire (ovviamente stiamo parlando di una sezione che non è assolutamente vitale, la trovate negli exe di debug che crea il compilatore per esempio). Il numero di IMAGE_DEBUG_DIRECTORY si ricava dividendo la dimensione della directory per la dimensione della struttura, ma prima di tutto vediamoci la struttura:


typedef struct _IMAGE_DEBUG_DIRECTORY {
    DWORD Characteristics;
    DWORD TimeDateStamp;
    WORD MajorVersion;
    WORD MinorVersion;
    DWORD Type;
    DWORD SizeOfData;
    DWORD AddressOfRawData;
    DWORD PointerToRawData;
} IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;

Characteristics ...Boh.

TimeDateStamp indovinate!

MajorVersion e MinorVersion sempre info moooolto utili, bah.

Type specifica il tipo di informazione di debug a cui la struttura fa riferimento, i tipi dichiarati nel winnt.h sono:


#define IMAGE_DEBUG_TYPE_UNKNOWN         0
#define IMAGE_DEBUG_TYPE_COFF            1
#define IMAGE_DEBUG_TYPE_CODEVIEW        2
#define IMAGE_DEBUG_TYPE_FPO             3
#define IMAGE_DEBUG_TYPE_MISC            4
#define IMAGE_DEBUG_TYPE_EXCEPTION       5
#define IMAGE_DEBUG_TYPE_FIXUP           6
#define IMAGE_DEBUG_TYPE_OMAP_TO_SRC     7
#define IMAGE_DEBUG_TYPE_OMAP_FROM_SRC   8
#define IMAGE_DEBUG_TYPE_BORLAND         9
#define IMAGE_DEBUG_TYPE_RESERVED10     10
#define IMAGE_DEBUG_TYPE_CLSID          11

Il tipo di info di debug più comune è sicuramente il Code View. Gli unici altri tipi di debug info che ho visto sono: Misc (in qualche dll MS) e gli omap (sempre in qualche prog MS). Il Coff si può far usare anche dal VC++ però francamente lo si vede poco (se vi interessa, una descrizione molto accurata del formato di debug info Coff la trovate nell'ormai (ahimé) vecchio libro di Pietrek: Windows Programming Secrets) e per quanto riguarda il borland... Non uso compilatori borland, boh si ho il delphi installato ma mi fa fatica di andare a vedere il tipo di debug info prodotto. L'FPO invece viene prodotto dagli exe compilati dal .NET.

AddressOfRawData sempre 0 lo trovo...

PointerToRawData RVA che punta alle info di debug.

Adesso avendo le info necessario, basta solo considerare il tipo di info e comportarsi di proposito. Nel WinNt.h sono dichiarate alcune strutture relative ad alcuni tipi di debug info:


// Coff

typedef struct _IMAGE_COFF_SYMBOLS_HEADER {
    DWORD NumberOfSymbols;
    DWORD LvaToFirstSymbol;
    DWORD NumberOfLinenumbers;
    DWORD LvaToFirstLinenumber;
    DWORD RvaToFirstByteOfCode;
    DWORD RvaToLastByteOfCode;
    DWORD RvaToFirstByteOfData;
    DWORD RvaToLastByteOfData;
} IMAGE_COFF_SYMBOLS_HEADER, *PIMAGE_COFF_SYMBOLS_HEADER;

// FPO

typedef struct _FPO_DATA {
    DWORD ulOffStart;     // offset 1st byte of function code
    DWORD cbProcSize;     // # bytes in function
    DWORD cdwLocals;      // # bytes in locals/4
    WORD cdwParams;       // # bytes in params/4
    WORD cbProlog : 8;    // # bytes in prolog
    WORD cbRegs : 3;      // # regs saved
    WORD fHasSEH : 1;     // TRUE if SEH in func
    WORD fUseBP : 1;      // TRUE if EBP has been allocated
    WORD reserved : 1;    // reserved for future use
    WORD cbFrame : 2;     // frame type
} FPO_DATA, *PFPO_DATA;

// MISC

typedef struct _IMAGE_DEBUG_MISC {
    DWORD DataType;      // type of misc data, see defines
    DWORD Length;        // total length of record, rounded to four
                         // byte multiple.

    BOOLEAN Unicode;     // TRUE if data is unicode string
    BYTE Reserved[ 3 ];
    BYTE Data[ 1 ];      // Actual data
} IMAGE_DEBUG_MISC, *PIMAGE_DEBUG_MISC;

In ogni caso i formati di debug info non trovano posto in questo tutorial, infatti vanno oltre il discorso PE. Quindi andiamo pure avanti (codice esplicativo mi pare supefluo).


ARCHITECTURE/COPYRIGHT DIRECTORY

Della Architecture Directory potrei anche descrivervi la struttura ma francamente non ho la minima idea a che cosa possa servire, anche perché non l'ho mai vista all'interno di un PE... Per quanto riguarda invece la Copyright: fate conto che questa entry non esista.


GLOBAL PTR DIRECTORY

Ecco fate conto che ANCHE questa entry non esista.


THREAD LOCAL STORAGE

O più brevemente TLS: finalmente una sezione che serve a qualcosa!

Ogni qualvolta che usate la direttiva __declspec(thread) per dichiare una variabile in un vostro programma, questa variabile verrà messa nella sezione .tls. Ma a cosa serve una dichiarazione __declspec(thread) per una variabile? Fa in modo che tutti i thread del processo abbiano una propria copia della variabile dichiarata. Partendo dalla Data Dir troverete la struttura IMAGE_TLS_DIRECTORY, in questa struttura stanno sei membri di cui quattro sono degli indirizzi (StartAddressOfRawData, EndAddressOfRawData, AddressOfIndex, AddressOfCallBacks). E' però molto importante tenere a mente che tutti questi indirizzi non sono degli RVAs bensì sono dei VAs (occhio).


LOAD CONFIGURATION

Vi è una struttura adibita a questa directory, ma francamente non l'ho mai vista in un PE. Inoltre non è documentata nel WinNt.h né da Pietrek... Mi invento il significato dei membri della struttura?


BOUND IMPORT DIRECTORY

A questo paragrafo avevo già fatto riferimento in quello riguardante la IT ed è anche interessante. Come ben sapete, quando un PE viene loadato, il loader riempie la IAT con gli indirizzi effettivi in memoria delle funzioni importate. Il binding dell'IT non consiste in altro che nel fare in modo di saltare questo processo, ovvero la IAT conterrà già nel PE sul disco gli indirizzi di memoria delle funzioni importate. Questo come ben potete immaginare aumenta la velocità di caricamento (bah) ed infatti la gran parte degli eseguibili di sistema hanno una IT bind-ata. Innanzitutto bisogna dire che per bind-are una IT è necessario che vi siano gli OriginalFirstThunk, altrimenti il loader non vi caricherà nemmeno l'exe perché trovando solo una IAT già riempita con indirizzi di memoria penserà che la IT sia distrutta. Ovviamente però non vi è la sicurezza che gli indirizzi della IAT corrispondano poi a quelli effettivi delle funzioni, se non corrispondessero, ci penserebbe il loader a sistemare la IAT. Come fa il loader a verificare però la validità della IAT? Be', se il modulo è stato rilocato allora lo saprà lo stesso loader, altrimenti proprio grazie a questa directory! Per esempio se cambiasse la versione del modulo, quasi sicuramente anche gli entry point delle funzioni non sarebbero più gli stessi e proprio per questo servono gli OFT perché nel caso di invalidità della IAT il loader è costretto a fare quello che fa sempre, ignorando il fatto che la IAT sia bind-ata.

Ci sono due modi di bind-are un exe, il primo di questi è però obsoleto e davvero non merita di essere descritto, anche perché non lo si trova più.

Partendo dalla Data Dir troviamo un insieme di strutture IMAGE_BOUND_IMPORT_DESCRIPTOR (una per ogni modulo a cui l'exe è stato bind-ato), vediamo tale struttura:


typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD TimeDateStamp;
    WORD OffsetModuleName;
    WORD NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

TimeDateStamp contiene appunto il Time/Date Stamp del modulo a cui questa struttura si riferisce.

OffsetModuleName punta (partendo dall'inizio di questa directory) al nome del modulo a cui la struttura si riferisce.

NumberOfModuleForwarderRefs questo parametro serve nel caso il modulo a cui la struttura fa riferimento faccia uso (per una delle funzioni importate dal nostro programma) di Export Forwarding, se così fosse, anche il modulo che contiene effettivamente deve essere segnalato. Quindi questo membro non rappresenta altro che il numero di strutture IMAGE_BOUND_FORWARDER_REF che seguono la corrente struttura IMAGE_BOUND_IMPORT_DESCRIPTOR. Ogni struttura IMAGE_BOUND_FORWARDER_REF fa riferimento ad un modulo che contiene una o più funzioni forwardate dal modulo corrente. Vediamo la struttura IMAGE_BOUND_FORWARDER_REF:


typedef struct _IMAGE_BOUND_FORWARDER_REF {
    DWORD TimeDateStamp;
    WORD OffsetModuleName;
    WORD Reserved;
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;

TimeDateStamp lo sapete già.

OffsetModuleName anche questo lo sapete già ed è sempre a partire dall'inizio della directory di cui stiamo parlando.

Reserved nulla.

In genere gli eseguibili vengono bind-ati durante le installazioni di programmi e comunque è frequente solo nei PE di sistema in genere. Per bind-are un eseguibile non è necessario fare alcuna fatica, basta veramente solo fare uso di una delle due api BindImage/Ex (che avevo tra l'altro già accennato parlando della imagehlp.dll). In ogni caso mi sembra opportuno un piccolo esempio di codice che non bind-a nulla, ma semplicemente mostra questa directory.


// sintassi: nome_file

#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, Bound_Offset;

   IMAGE_DOS_HEADER *ImageDosHeader;
   IMAGE_NT_HEADERS *ImageNtHeaders;

   IMAGE_BOUND_IMPORT_DESCRIPTOR *ImgBoundDescr;
   IMAGE_BOUND_FORWARDER_REF *ImgForwRef;

   DWORD Size = 0, x;

   char *ModName;

   // 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_BOUND_IMPORT].VirtualAddress)
   {
      printf("The IT of this PE isn't bound\n");
      free(BaseAddress);
      CloseHandle(hFile);
      return -1;
   }

   // converte in offset fisico l'RVA
   Bound_Offset = RvaToOffset(ImageNtHeaders,
      ImageNtHeaders->OptionalHeader.DataDirectory
      [IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress);

   // è valido?
   if (Bound_Offset == NULL)
   {
      printf("The IT of this PE isn't bound\n");
      free(BaseAddress);
      CloseHandle(hFile);
      return -1;
   }

   printf("\nBinding Info:");

   while (TRUE)
   {
      // prendo il Bound Descr
      ImgBoundDescr = (IMAGE_BOUND_IMPORT_DESCRIPTOR *)
         (DWORD) (Bound_Offset + Size + (DWORD) BaseAddress);

      // finiti i descriptors?
      if (!ImgBoundDescr->OffsetModuleName)
         break;

      // ricavo il nome basandomi
      // sull'inizio della directory

      ModName = (char *) (DWORD)
         (ImgBoundDescr->OffsetModuleName +
         Bound_Offset + (DWORD) BaseAddress);

      // stampo le info
      printf("\n\nModule Name: %s\n"
         "Time/Date Stamp: %08X\n"
         "Forwarder Mods: %d", ModName,
         ImgBoundDescr->TimeDateStamp,
         ImgBoundDescr->NumberOfModuleForwarderRefs);

      Size += sizeof (IMAGE_BOUND_IMPORT_DESCRIPTOR);

      // ci sono forwarder modules?

      if (ImgBoundDescr->NumberOfModuleForwarderRefs)
         printf("\n\nForwarder Modules:");

      for (x = 0; x < ImgBoundDescr->NumberOfModuleForwarderRefs; x++)
      {
         // vabbe stessa cosa di prima solo fatto
         // per i forwarder mod

         ImgForwRef = (IMAGE_BOUND_FORWARDER_REF *) (DWORD)
            (Bound_Offset + Size + (DWORD) BaseAddress);

         ModName = (char *) (DWORD)
            (ImgForwRef->OffsetModuleName +
            Bound_Offset + (DWORD) BaseAddress);

         printf("\n\nModule Name: %s\n"
            "Time/Date Stamp: %08X", ModName,
            ImgForwRef->TimeDateStamp);

         Size += sizeof (IMAGE_BOUND_FORWARDER_REF);
      }
   }

   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;
}

Mi sembra semplice il codice, andiamo avanti... Coraggio: manca poco ormai.


IMPORT ADDRESS TABLE

Siete giunti a questa directory e non sapete già cosa contiene? A-ha, volevate fare i furbi, sì, voi newbies del reversing, andate alla ricerca di definizioni come IAT ecc per imparare a unpackare??? Vi ho beccati!! Tornate subito all'inizio del tutorial, ahahah.


DELAY LOAD IMPORTS

Capire questa directory non vi costerà molto tempo. In ogni caso è abbastanza frequente soprattutto per quanto riguarda gli eseguibili di sistema. Dunque a cosa serve questa directory? Mettiamo che voi (programmatori) abbiate scritto un programma che importa un tot di dll, ora, voi non sapete se le funzioni in una o più dll verranno chiamate durante l'esecuzione del programma (potrebbe darsi di sì come potrebbe darsi di no). Oppure, altro caso, c'è una o più dll di cui la/e funzione/i non vi serve/ono subito ma solo più avanti nell'esecuzione del programma. In questi due casi si potrebbe aumentare la velocità di caricamento del programma evitando di far caricare al loader subito tutte le dll (e relative funzioni. Il VC++ 6 introduce la possibilità del Delay Load delle Imports, è sufficiente aggiungere tra le lib importate dal programma la Delayimp.lib, dopodiché aggiungere tra le impostazioni del linker /delayload:nome_dll per ogni dll che volete caricare quando c'è la necessità. Non si può però fare il delay loading del kernel32 poiché questa dll serve proprio alla routine (vedremo cosa è) di delay loading all'interno dell'eseguibile. Cioè vediamo di capire, il delay loading non è un processo fornito dal WinNt.h, bensì dal compilatore che aggiunge una routine a cui ogni call a funzione di una delle dll che devono essere caricate se è necessario (specificate dal /delayload insomma) fa riferimento. Per farvi capire meglio mi sono creato un eseguibile col VC++ 6 (un semplice Hello World) e ho specificato come /delayload la user32.dll. Innazitutto nessuna user32.dll risulta da caricare nella Import Table dell'esebuibile, però una qualsiasi chiamata a una delle funzioni della user32, assume finalmente questo aspetto:


:0040141A  push ecx
:0040141B  push edx
:0040141C  push offset UpdateWindow ; nome della funzione che
                                    ; è stata chiamata
:00401421 jmp loc_401376            ; salta alla routine di delay loading

Il programma cerca di caricare la dll in questione:

:00401595  push [ebp+lpLibFileName]
:00401598  call ds:LoadLibraryA

Se non ci riesce, genera un'eccezione:

:004015CD  push eax        ; lpArguments
:004015CE  push 1          ; nNumberOfArguments
:004015D0  push 0          ; dwExceptionFlags
:004015D2  push 0C06D007Eh ; dwExceptionCode
:004015D7  call ds:RaiseException

Poi prova a prendere l'indirizzo in memoria della funzione che deve essere eseguita:

:00401677  push [ebp+lpProcName]
:0040167A  push edi
:0040167B  call ds:GetProcAddress

E anche in questo caso, se non gli riesce chiama RaiseException. Se invece è riuscito a fare tutto, sostituirà nella IAT a cui le funzioni puntano, l'indirizzo vero della funzione di modo che la prossima volta che la funzione viene chiamata non debba venir rieseguita la delay loading function.

A titolo di informazione vi incollo la routine di delay loading del VC++ (contenuta nel DelayHlp.cpp nella dir include):


extern "C"
FARPROC WINAPI __delayLoadHelper(PCImgDelayDescr pidd,
                                 FARPROC * ppfnIATEntry)

{

    // Set up some data we use for the hook procs but also useful for
    // our own use
    //

    DelayLoadInfo dli = {
        sizeof DelayLoadInfo,
        pidd,
        ppfnIATEntry,
        pidd->szName,
        { 0 },
        0,
        0,
        0
    };

    HMODULE hmod = *(pidd->phmod);

    // Calculate the index for the name in the import name table.
    // N.B. it is ordered the same as the IAT entries so the calculation
    // comes from the IAT side.
    //

    unsigned iINT;
    iINT = IndexFromPImgThunkData(PCImgThunkData(ppfnIATEntry), pidd->pIAT);

    PCImgThunkData pitd = &((pidd->pINT)[iINT]);

    if (dli.dlp.fImportByName = ((pitd->u1.Ordinal & IMAGE_ORDINAL_FLAG) == 0)) {
        dli.dlp.szProcName = LPCSTR(pitd->u1.AddressOfData->Name);
    }
    else {
        dli.dlp.dwOrdinal = IMAGE_ORDINAL(pitd->u1.Ordinal);
    }

    // Call the initial hook. If it exists and returns a function pointer,
    // abort the rest of the processing and just return it for the call.
    //

    FARPROC pfnRet = NULL;

    if (__pfnDliNotifyHook) {
        if (pfnRet = ((*__pfnDliNotifyHook)(dliStartProcessing, &dli))) {
        goto HookBypass;
        }
    }

    if (hmod == 0) {
        if (__pfnDliNotifyHook) {
            hmod = HMODULE(((*__pfnDliNotifyHook)(dliNotePreLoadLibrary, &dli)));
        }
        if (hmod == 0) {
            hmod = ::LoadLibrary(dli.szDll);
        }
        if (hmod == 0) {
            dli.dwLastError = ::GetLastError();
            if (__pfnDliFailureHook) {
                // when the hook is called on LoadLibrary failure, it will
                // return 0 for failure and an hmod for the lib if it fixed
                // the problem.
                //

                hmod = HMODULE((*__pfnDliFailureHook)(dliFailLoadLib, &dli));
            }

            if (hmod == 0) {
                PDelayLoadInfo pdli = &dli;

                RaiseException(
                    VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND),
                    0,
                    1,
                    PDWORD(&pdli)
                );

                // If we get to here, we blindly assume that the handler of the exception
                // has magically fixed everything up and left the function pointer in
                // dli.pfnCur.
                //

                return dli.pfnCur;
            }
        }

        // Store the library handle. If it is already there, we infer
        // that another thread got there first, and we need to do a
        // FreeLibrary() to reduce the refcount
        //

        HMODULE hmodT = HMODULE(::InterlockedExchange(LPLONG(pidd->phmod), LONG(hmod)));
        if (hmodT != hmod) {
        // add lib to unload list if we have unload data
            if (pidd->pUnloadIAT) {
                ULI * puli = new ULI(pidd);
                (void *)puli;
            }
        }
        else {
            ::FreeLibrary(hmod);
        }

    }

    // Go for the procedure now.
    dli.hmodCur = hmod;
    if (__pfnDliNotifyHook) {
        pfnRet = (*__pfnDliNotifyHook)(dliNotePreGetProcAddress, &dli);
    }
    if (pfnRet == 0) {
        if (pidd->pBoundIAT && pidd->dwTimeStamp) {
            // bound imports exist...check the timestamp from the target image
            PIMAGE_NT_HEADERS pinh(PinhFromImageBase(hmod));

            if (pinh->Signature == IMAGE_NT_SIGNATURE &&
                TimeStampOfImage(pinh) == pidd->dwTimeStamp &&
                FLoadedAtPreferredAddress(pinh, hmod)) {

                    OverlayIAT(pidd->pIAT, pidd->pBoundIAT);
                    pfnRet = FARPROC(pidd->pIAT[iINT].u1.Function);
                    goto HookBypass;
            }
        }

        pfnRet = ::GetProcAddress(hmod, dli.dlp.szProcName);
    }

    if (pfnRet == 0) {
    dli.dwLastError = ::GetLastError();
        if (__pfnDliFailureHook) {
            // when the hook is called on GetProcAddress failure, it will
            // return 0 on failure and a valid proc address on success
            //

            pfnRet = (*__pfnDliFailureHook)(dliFailGetProc, &dli);
        }
        if (pfnRet == 0) {
            PDelayLoadInfo pdli = &dli;

            RaiseException(
                VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND),
                0,
                1,
                PDWORD(&pdli)
            );

            // If we get to here, we blindly assume that the handler of the exception
            // has magically fixed everything up and left the function pointer in
            // dli.pfnCur.
            //

            pfnRet = dli.pfnCur;
        }
    }


    *ppfnIATEntry = pfnRet;

    HookBypass:
    if (__pfnDliNotifyHook) {
        dli.dwLastError = 0;
        dli.hmodCur = hmod;
        dli.pfnCur = pfnRet;
        (*__pfnDliNotifyHook)(dliNoteEndProcessing, &dli);
    }
    return pfnRet;
}

Ma vediamo di capire come la corrente directory è composta, per farlo non dovremo andare a cercare nel WinNt.h, ma nel DelayImp.H (che non trovate nell'sdk ma sempre tra gli include del VC++). A partire dalla Data Dir questa directory è composta da un array di ImgDelayDescr (uno per ogni modulo segnato come delay import), vediamo la struttura:

typedef struct ImgDelayDescr {
    DWORD grAttrs;
    LPCSTR szName;
    HMODULE * phmod;
    PImgThunkData pIAT;
    PCImgThunkData pINT;
    PCImgThunkData pBoundIAT;
    PCImgThunkData pUnloadIAT;
    DWORD dwTimeStamp;

} ImgDelayDescr, * PImgDelayDescr;

grAttrs attributi.

szName puntatore al nome del modulo a cui la struttura fa riferimento.

phmod indirizzo in memoria del modulo (nel caso fosse già stato caricato tramite una qualche precedente chiamata a funzione di delay loading).

pIAT indirizzo della IAT, per il VC++ 6 questi sono dei VA.

pINT indirizzo della Import Name Table, per i nomi delle funzioni importate da questo modulo.

pBoundIAT nel caso ci fosse una bound IAT, altrimenti 0.

pUnloadIAT indirizzo di una copia opzionale della IAT.

dwTimeStamp questo è generalmente 0, ma se la IAT è bound allora contiene il Time/Date Stamp del modulo corrente, in ogni caso serve solo per IAT che usufruiscono del vecchio metodo per bind-are.

Per chiarire le idee eccovi un piccolo esempio che elenca le info di delay import. Però prima di mostrarvelo un piccolo appunto, vi ho detto che il VC++ 6 usa VA nei suoi indirizzi, ma ciò non vale per il .NET o altri compilatori a 64 bit che fanno uso di RVA. Per capire se l'exe fa uso di RVA o VA bisogna controllare il campo grAttrs. Se il primo bit è settato allora gli indirizzi sono dei RVA altrimenti sono dei VA.


// sintassi: nome_file

#include <windows.h>
#include <stdio.h>
#include <delayimp.h>

DWORD VaToOffset(IMAGE_NT_HEADERS *NT, DWORD Va);
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva);

int main(int argc, char *argv[])
{
   HANDLE hFile;
   BYTE *BaseAddress;
   DWORD FileSize, BR, Delay_Offset;

   IMAGE_DOS_HEADER *ImageDosHeader;
   IMAGE_NT_HEADERS *ImageNtHeaders;

   ImgDelayDescr *ImgDelay;

   DWORD Size = 0;
   char *ModName;

   // 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_DELAY_IMPORT].VirtualAddress)
   {
      printf("There is Delay Import Info\n");
      free(BaseAddress);
      CloseHandle(hFile);
      return -1;
   }

   // converte in offset fisico l'RVA
   Delay_Offset = RvaToOffset(ImageNtHeaders,
      ImageNtHeaders->OptionalHeader.DataDirectory
      [IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].VirtualAddress);

   // è valido?
   if (Delay_Offset == NULL)
   {
      printf("There is Delay Import Info\n");
      free(BaseAddress);
      CloseHandle(hFile);
      return -1;
   }

   printf("\nDelay Import Info:");


   while (TRUE)
   {
      // prendo l'attuale Delay Descriptor
      ImgDelay = (ImgDelayDescr *) (DWORD)
         (Delay_Offset + Size + (DWORD) BaseAddress);

      // siamo giunti alla fine dell'array?
      if (ImgDelay->pIAT == NULL)
         break;

      // prendo il nome del modulo
      // controllando se ho VA o RVA

      if (ImgDelay->grAttrs & 1)
      {
         ModName = (char *)(DWORD)
            (RvaToOffset(ImageNtHeaders, (DWORD) ImgDelay->szName) +
            (DWORD) BaseAddress);
      }
      else
      {
         ModName = (char *)(DWORD)
            (VaToOffset(ImageNtHeaders, (DWORD) ImgDelay->szName) +
            (DWORD) BaseAddress);
      }

      // stampo solo le info più importanti
      printf("\n\nName: %s\n"
         "IAT: %X\nINT: %X", ModName,
         ImgDelay->pIAT, ImgDelay->pINT);
      
      Size += sizeof (ImgDelayDescr);
   
   }

   free(BaseAddress);
   CloseHandle(hFile);

   return 0;
}

// per passare da VA a Offset

DWORD VaToOffset(IMAGE_NT_HEADERS *NT, DWORD Va)
{
   return RvaToOffset(NT,
      (Va - NT->OptionalHeader.ImageBase));
}

// 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;
}

Non mi sembra ci sia altro da dire, se volete vedere cose come la routine per unloadare le dll caricate tramite delay loading, date un'occhiata ai file che vi ho segnalato.


COM DESCRIPTOR

La directory è in sé obsoleta, però viene usata per un header dal .NET però non mi interessa come discorso (almeno per adesso) e non l'ho mai approfondito. In ogni caso potete sempre andare sul msdn...


CONCLUSIONI

Abbiamo finalmente FINITO! Questo è il tipico tutorial che è più faticoso da leggere/comprendere che da scrivere (lavoro di due serate).

Alla prossima.

Daniel Pistelli