- Messaggi
- 2,332
- Reazioni
- 1,928
- Punteggio
- 134
Struttura PE (Portable Executable), *.exe
a cura di Marco 'DispatchCode' C.
- Preambolo
- Introduzione: nascita PE
- Struttura PE Header
- DOS Header
- Esempio
- NT Header
- Esempio
- Image File Header
- Esempio
- Optional Header
- Esempio
- Section Table
- Esempio
- Image Data Directory
- Import Directory
- Esempio
- Export Directory
- Esempio
- Resource Directory
- DOS Header
- PE Injector
- Conclusione
Preambolo
Questo articolo è dedicato al formato più diffuso di un programma eseguibile in ambiente Windows: il PE (Portable Executable), formato che appartiene anche all'EXE.
Il PE Header è composto da svariate strutture. Non tutti i campi sono utilizzati, e non tutte le strutture sono così importanti.
Sulla base quindi di questa valutazione ho scelto di trattare solo gli aspetti centrali: DOS Header (e lo Stub), l'NT Header (più le due strutture racchiuse nell'NT: File Header e Optional Header), la Section Table, la Import Table e la Export Table. Quelle non trattate riguardano i certificati (la firma con relativo checksum memorizzato in un campo dell'Optional Header), il debug, la sicurezza, ed altre ancora.
L'organizzazione dell'articolo è la seguente: ogni punto del menu è una nuova sezione, ed al termine di ogni sezione vi sarà un esempio in linguaggio C. L'ultima sezione è molto particolare, e credo anche molto interessante. Mi sono chiesto se fosse il caso di interagire con un EXE in un modo un pò diverso dal solito... e così ho pensato subito a PE Analyzer, un mio vecchio programma scritto in assembly che consente di ottenere informazioni da un PE header. Ma non mi sono accontentato. Così è nato quello che posso definire "PE Analyzer 2", che ho chiamato InjMyPE (scritto in Assembly). La sezione è quella che posso definire un argomento 'avanzato' e non correlato alla struttura del PE, bensì alla sua manipolazione per iniettare al suo interno del codice eseguibile.
Buona lettura, e spero apprezziate l'articolo ed il contenuto!
NOTA IMPORTANTE:
L'articolo in tutte le sue parti, così come i sorgenti, risultano scritti qualche anno addietro; la gran parte di quanto esposto è valida come lo era prima, ma ci possono essere alcune valutazioni e/o considerazioni - in particolare nell'ultima parte su InjMyPe - non più valide (e per questi motivi InjMyPe, che non ho più aggiornato, almeno sino ad ora, potrebbe funzionare su un numero di eseguibili inferiore).
1. Introduzione
Tutti conoscono un file EXE, ma non sono invece molti a sapere com'è internamente e com'è nato. Il formato PE, acronimo di Portable Executable File Format, è il formato di un programma binario in ambiente Windows (exe, dll, sys, src); è utilizzato anche per i file oggetto, come ocx. E' un formato standardizzato nel lontano '93 dal TISC (Tool Interface Standard Committee), un'associazione formata dai produttori Software ed Hardware tutt'ora più importanti al mondo: Windows, Intel, Borland, IBM ed altri ancora. Il formato PE è basato (almeno in parte) sul formato COFF (Common Object File) di Unix.
Il nome PE è stato scelto in quanto si voleva dare l'idea di un formato eseguibile su qualsiasi macchina Windows e qualsiasi CPU (compatibile, ovviamente). Uno degli aspetti più interessanti ed importanti è che la struttura di un file PE in memoria è identica a quella che ha su disco. Si tratta quindi di una mappatura di fatto (un pò più sofisticata, in realtà: è il loader a decidere quale parte mappare).
La struttura di un file PE comprende codice, dati e risorse; come detto qui sopra, alcune non verranno mai mappate.
Struttura PE
Come si evince da quanto detto sino ad ora, la struttura di un PE è qualcosa di molto sofisticato, al contrario del formato COM, presente in vecchi OS Windows (ormai è molto poco utilizzato). La struttura di un file PE la si può rappresentare in questo modo (le quadre indicano semplicemente che si tratta di un singolo campo, le tonde con un contenuto dopo al nome, indicano il nome della struttura dati):
- DOS Header (IMAGE_DOS_HEADER);
- DOS Stub;
- PE Header (IMAGE_NT_HEADERS);
-- [Signature];
-- File Header (IMAGE_FILE_HEADER);
-- Optional Header (IMAGE_OPTIONAL_HEADER);
- Section Table
-- Section Header (IMAGE_SECTION_HEADER) array;
-- Import Table
-- Import Directory
- Sections (data, code/text, rdata,...).
A prima vista sembrerà molto complesso, ma vi posso assicurare che entrati nella giusta logica sarà abbastanza semplice.
NOTA: L'analisi è effettuata utilizzando due miei software, ovvero PE Analyzer e HWInfo. La scelta è stata semplice: il primo è predisposto a mostrare alcune info sul PE Header, il secondo è comodo da analizzare.
DOS Header e DOS Stub
La prima informazione utile la troviamo proprio all'inizio, nei primi 2byte. Potete utilizzare l'immagine sottostante per orientarvi:
La selezione in blu indica l'intera zona DOS Header e DOS Stub (analizzata tra breve).
Come dicevo, i primi 2byte forniscono un'importante informazione: la validità del DOS Header. I 2byte mostrati in figura sono 0x4D 0x5A, "MZ" in ASCII. I caratteri non sono casuali e rappresentano infatti il creatore di questo formato, Mark Zbikowski. Questi numeri sono noti come "magic number": se un file non inizia con questa firma, non è un PE valido, e si evitano direttamente gli altri controlli.
C:
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew;
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
Come anticipato, il primo campo si chiama "magic number"... ed infatti lo si evince dal nome stesso (e_magic). La seguente struttura ha un solo altro campo molto interessante, ed è l'ultimo: e_lfanew. Questo campo è l'offset dell'NT Header (IMAGE_NT_HEADERS, descritto in seguito). Facendo riferimento allo screenshot, troverete il rispettivo offset guardando 0x3C (incrociate la riga Offset ndicata con 30, con la colonna 0C). Il numero indicato qui dentro come detto, è l'offset della nuova struttura dati, e coincide ovviamente con il termine della struttura IMAGE_DOS_HEADER; i numeri selezionati nello screenshot dopo a questo indirizzo riguardano lo Stub.
Il DOS Stub è molto poco importante: si tratta molto semplicemente di quel testo che compare quando si tenta di eseguire un programma in DOS, ma che non può essere eseguito ("This program cannot be run in DOS mode"). Il codice che compare infatti dall'offset 0x40 in avanti (sino al termine dello Stub) è tutto a 16bit.
Guardate ora la struttura mostrata in precedenza. e_res e e_res2 sono due array, il primo di 4 elementi ed il secondo di 10. In totale abbiamo 32 + 28 + 4=64byte (0x40).
Tornando all'immagine, 0x40 è l'offset dello Stub, a cui vanno aggiunti quindi i byte che sono 112. Sommando 112 + 64 otteniamo 176(byte) che corrispondono in esadecimale a 0xB0. Qui non lo possiamo vedere sfortunatamente, ma quest'ultimo offset 0xB0 è grande 2byte; quindi siamo a 0xB8.
Il numero non ci è nuovo! Incollo di seguito la parte interessata della prima immagine:
Codice:
00 00 00 00 00 00 00 00 00 00 00 00 B8 00 00 00
questa corrisponde all'offset 0x3C (come prima, il 0x30 è sulla riga, 0xC sulla colonna). Osservando il valore presente in quel campo, scopriamo che è 0xB8. Il che significa che ci troviamo in corrispondenza di e_lfanew, offset della nuova struttura dati del PE Header, IMAGE_NT_HEADERS.
Esempio: IMAGE_DOS_HEADER
Di seguito il sorgente in C per verificare la validità del DOS Header ed ottenere l'indirizzo alla struttura che analizzeremo a breve:
C:
#include<stdio.h>
#include<windows.h>
int main(int argc, char *argv[]) {
HANDLE hFile;
HANDLE hFileMapped;
LPVOID lpFileBase;
IMAGE_DOS_HEADER *pDosHeader;
if(argc == 1) {
printf("Utilizzo: exedump <file.exe>");
return 1;
}
// Apro il file, ottenendo l'HANDLE
hFile = CreateFile(argv[1], GENERIC_READ ,FILE_SHARE_READ,0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
// Handle non valido (si e' verificato un errore)
if(hFile == INVALID_HANDLE_VALUE) {
printf("Errore nell'apertura del file\n%s", argv[1]);
return 1;
}
// Creo una mappa del file in memoria
hFileMapped = CreateFileMapping(hFile, 0, PAGE_READONLY, 0,0,NULL);
// Error Checking
// --------------------------------
if(hFileMapped == 0) {
printf("Errore mapping file");
return 1;
}
// --------------------------------
// Viene restituito un puntatore all'inizio, alla base
lpFileBase = MapViewOfFile(hFileMapped,FILE_MAP_READ,0,0,0);
// Error Checking
// --------------------------------
if(lpFileBase == 0) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Errore MapViewOfFile");
return 1;
}
// --------------------------------
// Casto per ottenere un puntatore alla struttura
pDosHeader = (IMAGE_DOS_HEADER *) lpFileBase;
// A questo punto verifico la firma
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Firma Non Valida!");
return 1;
}
printf("Firma Valida!\n\tPuntatore alla prossima struttura (IMAGE_NT_HEADER): 0x%08X\n\n", pDosHeader->e_lfanew);
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
return 0;
}
Output:
Codice:
>winapi hwinfo.exe
Firma Valida!
Puntatore alla prossima struttura (IMAGE_NT_HEADER): 0x000000B8
PE Header - IMAGE_NT_HEADERS
La struttura dati precedente, compreso lo Stub, ha una dimensione totale di 184byte (0xB8). L'offset del nuovo header è di fatto un puntatore. Chi ha infatti dimestichezza con un linguaggio che permette di manipolare la memoria attraverso puntatore (come C, C++, Assembly) saprà che è possibile effettuare somme anche utilizzando i puntatori. Sommando quindi il DOS Header a questo puntatore, e_lfanew, si otterrà l'offset della nuova struttura dati IMAGE_NT_HEADERS.
La seguente struttura dati è molto importante. Questa contiene a sua volta altre strutture, oltre che a un DWORD (Double Word, 32bit). La struttura è definita in questo modo infatti:
C:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
L'intera struttura IMAGE_NT_HEADERS è di ben 248byte, inizia all'offset 0xB8 e termina all'offset 0x1AF (compreso).
Signature
è la DWORD evidenziata nei primi due byte, 0x50 0x45. Vanno letti al contrario in realtà, in quanto si tratta della notazione di Intel. Quindi il byte è 0x45 0x50 0x00 0x00, ovvero PE (anche qui, non sono casuali i caratteri...), seguito da due caratteri nulli.
Esempio: IMAGE_NT_HEADER
C:
#include<stdio.h>
#include<windows.h>
int main(int argc, char *argv[]) {
HANDLE hFile;
HANDLE hFileMapped;
LPVOID lpFileBase;
IMAGE_DOS_HEADER *pDosHeader;
IMAGE_NT_HEADERS *pImageNtHeader;
if(argc == 1) {
printf("Utilizzo: exedump <file.exe>");
return 1;
}
// Apro il file, ottenendo l'HANDLE
hFile = CreateFile(argv[1], GENERIC_READ ,FILE_SHARE_READ,0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
// Handle non valido (si e' verificato un errore)
if(hFile == INVALID_HANDLE_VALUE) {
printf("Errore nell'apertura del file\n%s", argv[1]);
return 1;
}
// Creo una mappa del file in memoria
hFileMapped = CreateFileMapping(hFile, 0, PAGE_READONLY, 0,0,NULL);
// Error Checking
// --------------------------------
if(hFileMapped == 0) {
printf("Errore mapping file");
return 1;
}
// --------------------------------
// Viene restituito un puntatore all'inizio, alla base
lpFileBase = MapViewOfFile(hFileMapped,FILE_MAP_READ,0,0,0);
// Error Checking
// --------------------------------
if(lpFileBase == 0) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Errore MapViewOfFile");
return 1;
}
// --------------------------------
// Casto per ottenere un puntatore alla struttura
pDosHeader = (IMAGE_DOS_HEADER *) lpFileBase;
// A questo punto verifico la firma
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Firma Non Valida!");
return 1;
}
printf("Firma Valida!\n\tPuntatore alla prossima struttura (IMAGE_NT_HEADER): 0x%08X\n\n", pDosHeader->e_lfanew);
// Punto alla nuova struttura dati, IMAGE_NT_HEADER
pImageNtHeader = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew + (DWORD) pDosHeader);
if(pImageNtHeader->Signature != IMAGE_NT_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("NT Header non valido!");
return 1;
}
printf("Signature valida!\n");
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
return 0;
}
Output:
Codice:
>winapi hwinfo.exe
Firma Valida!
Puntatore alla prossima struttura (IMAGE_NT_HEADER): 0x000000B8
Signature valida!
IMAGE_FILE_HEADER
IMAGE_FILE_HEADER è una struttura molto interessante. Di seguito i campi:
C:
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;
Questa struttura rappresenta l'header COFF.
Machine
Identifica l'architettura del computer, e può avere uno dei seguenti valori: IMAGE_FILE_MACHINE_I386 (x86), IMAGE_FILE_MACHINE_IA64 (Intel Itanium), IMAGE_FILE_MACHINE_AMD64 (x64).
NumberOfSections
Indica il numero delle sezioni presenti nel file. E' la dimensione della Symbol Table, ed è quella che segue l'header. Verrà analizzata in seguito.
TimeDateStamp
Indica la data di compilazione, o più precisamente la data del linking. Mantiene data ed ora.
PointerToSymbolTable
Puntatore alla Symbol Table. Vale 0 se non esiste la tabella COFF.
NumberOfSymbols
Numero dei simboli nella Symbol Table.
SizeOfOptionalHeader
E' la dimensione dell'Optional Header che analizzeremo in seguito.
Characteristics
Indica le caratteristiche dell'immagine stessa.
Riassumendo, si procederebbe in questo modo:
- Caricamento del file da leggere (sia esso exe o altro, nel caso di specie assumiamo sia un exe);
- Il file deve essere mappato in memoria; a questo punto verrà restituito un puntatore utilizzabile come base dell'immagine;
- A questo punto si ottiene la struttura IMAGE_DOS_HEADER e si verifica almeno il campo e_magic: se è valido si prosegue;
- Ora si somma l'indirizzo base a e_lfanew della struttura IMAGE_DOS_HEADER e si ottiene l'offset di IMAGE_NT_HEADER;
- Anche in questo caso si effettua un controllo sull'header analizzando però il campo Signature: se è valido si prosegue;
Abbiamo svolto le operazioni essenziali per iniziare ad analizzare un PE header, in quanto sappiamo che esso è valido. A questo punto la prima struttura che ci si trova davanti è IMAGE_FILE_HEADER, che è uno dei campi di IMAGE_NT_HEADER (che abbiamo già di fatto, in quanto abbiamo verificato il relativo campo Signature).
Per comprenderne a fondo il funzionamento, dobbiamo iniziare a pensare a queste strutture come semplici aree di memoria. Se voglio accedere quindi al secondo membro della struttura IMAGE_NT_HEADER (ovvero IMAGE_FILE_HEADER), dovrò svolgere queste operazioni:
C:
pFileHeader = (BaseAddress + dosHeader.e_lfanew + 4);
Dove BaseAddress è l'indirizzo Base, quello restituito dalla funzione che mappa il file; dosHeader.e_lfanew, come abbiamo già detto, è il puntatore alla struttura IMAGE_NT_HEADER. E perchè sommare quindi il valore 4? Semplicemente perchè il primo campo di IMAGE_NT_HEADER è un DWORD (4byte), che di conseguenza dobbiamo superare. Il campo successivo è infatti l'offset di IMAGE_FILE_HEADER.
Com'è stato detto poco più sopra, la struttura IMAGE_FILE_HEADER contiene un campo chiamato NumberOfSections. Questo campo specifica il numero delle sezioni (che compongono in pratica la Section Table). Le sezioni come già stato detto (lo ripeto poichè so che l'argomento non è di semplice comprensione) seguono l'header, quindi sono in pratica dopo alla IMAGE_NT_HEADER. L'unico problema è che leggendo nel IMAGE_FILE_HEADER noi sappiamo quante sono, ma non dove iniziano. Sapendo però che la struttura è dopo all'IMAGE_NT_HEADER, siamo in grado di calcolarlo. Per trovare quello che si chiama Image Section Header dobbiamo di fatto trovare la prima struttura dati IMAGE_SECTION_HEADER (analizzata in seguito) e sommare anche il SizeOfOptionalHeader.
In seguito vedremo in che modo lo si calcola, e riprenderemo il discorso proprio da qui. Ora procediamo per step, ed andiamo a guardare IMAGE_OPTIONAL_HEADER.
Esempio: IMAGE_FILE_HEADER
C:
#include<stdio.h>
#include<windows.h>
DWORD RVAToOffset(IMAGE_NT_HEADERS*, DWORD);
int main(int argc, char *argv[]) {
HANDLE hFile;
HANDLE hFileMapped;
LPVOID lpFileBase;
IMAGE_DOS_HEADER *pDosHeader;
IMAGE_NT_HEADERS *pImageNtHeader;
IMAGE_FILE_HEADER *pImageFileHeader;
IMAGE_OPTIONAL_HEADER *pImageOptionalHeader;
if(argc == 1) {
printf("Utilizzo: exedump <file.exe>");
return 1;
}
// Apro il file, ottenendo l'HANDLE
hFile = CreateFile(argv[1], GENERIC_READ ,FILE_SHARE_READ,0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
// Handle non valido (si e' verificato un errore)
if(hFile == INVALID_HANDLE_VALUE) {
printf("Errore nell'apertura del file\n%s", argv[1]);
return 1;
}
// Creo una mappa del file in memoria
hFileMapped = CreateFileMapping(hFile, 0, PAGE_READONLY, 0,0,NULL);
// Error Checking
// --------------------------------
if(hFileMapped == 0) {
printf("Errore mapping file");
return 1;
}
// --------------------------------
// Viene restituito un puntatore all'inizio, alla base
lpFileBase = MapViewOfFile(hFileMapped,FILE_MAP_READ,0,0,0);
// Error Checking
// --------------------------------
if(lpFileBase == 0) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Errore MapViewOfFile");
return 1;
}
// --------------------------------
// Casto per ottenere un puntatore alla struttura
pDosHeader = (IMAGE_DOS_HEADER *) lpFileBase;
// A questo punto verifico la firma
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Firma Non Valida!");
return 1;
}
printf("Firma Valida!\n\tPuntatore alla prossima struttura (IMAGE_NT_HEADER): 0x%08X\n\n", pDosHeader->e_lfanew);
// Punto alla nuova struttura dati, IMAGE_NT_HEADER
pImageNtHeader = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew + (DWORD) pDosHeader);
if(pImageNtHeader->Signature != IMAGE_NT_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("NT Header non valido!");
return 1;
}
printf("Signature valida!\n");
// Punto alla IMAGE_FILE_HEADER, e mostro alcuni dati
printf("\n----------------IMAGE_FILE_HEADER---------------------\n\n");
// Punto, come dicevamo sopra (nell'articolo), alla struttura IMAGE_FILE_HEADER
// --------------------------------------------------------------------------------------------------
pImageFileHeader = (IMAGE_FILE_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4);
printf("Machine: 0x%08X ", pImageFileHeader->Machine);
switch(pImageFileHeader->Machine) {
case IMAGE_FILE_MACHINE_I386:
printf("x86\n");
break;
case IMAGE_FILE_MACHINE_IA64:
printf("Intel Itanium\n");
break;
case IMAGE_FILE_MACHINE_AMD64:
printf("x64\n");
}
printf("Numero di sezioni: 0x%08X\n", pImageFileHeader->NumberOfSections);
printf("Size Of Optional Header: 0x%08X\n", pImageFileHeader->SizeOfOptionalHeader);
/*
* Characteristics è una DWORD, i valori (le costanti) sono molti, e vengono aggiunti
* utilizzando l'OR. In questo modo utilizzando un AND si puo' sapere se qualcosa e' settato
*/
printf("\nCharacteristics:\n");
if(pImageFileHeader->Characteristics & IMAGE_FILE_DLL) {
printf("\tIMAGE_FILE_DLL\n");
}
if(pImageFileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
printf("\tIMAGE_FILE_EXECUTABLE_IMAGE\n");
}
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
return 0;
}
Output:
Codice:
>winapi hwinfo.exe
Firma Valida!
Puntatore alla prossima struttura (IMAGE_NT_HEADER): 0x000000B8
Signature valida!
----------------IMAGE_FILE_HEADER---------------------
Machine: 0x0000014C x86
Numero di sezioni: 0x00000003
Size Of Optional Header: 0x000000E0
Characteristics:
IMAGE_FILE_EXECUTABLE_IMAGE
IMAGE_OPTIONAL_HEADER
Questa struttura dati non è grande, di più!
Di seguito la struct:
C:
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_HEADER, *PIMAGE_OPTIONAL_HEADER;
Di seguito l'analisi dei campi principali che la compongono:
Magic
IMAGE_NT_OPTIONAL_HDR_MAGIC indica che è un'immagine eseguibile. In base all'applicazione è di 32 o 64bit, rispettivamente IMAGE_NT_OPTIONAL_HDR32_MAGIC e IMAGE_NT_OPTIONAL_HDR64_MAGIC. Se è una ROM IMAGE_ROM_OPTIONAL_HDR_MAGIC.
MajorLinkerVersion e MinorLinkerVersion
Indicano rispettivamente la versione maggiore e minore del linker.
SizeOfCode
Indica la dimensione della sezione del codice espressa in byte. Se ci sono sezioni di codice multiple conterrà la somma delle sezioni. E' la sezione Code in assembly (.code)
SizeOfInitializedData
Indica la dimensione della sezione dati espressa in byte. Come nel caso del codice, se vi sono più sezioni conterrà la somma delle sezioni. In assembly la si identifica con .data.
SizeOfUninitializedData
Dimensione della sezione dei dati non inizializzati, come sopra, se vi sono più sezioni dati non inizializzate conterrà la somma di queste. Questa sezione è nota anche come Stack (in asm la si identifica con .data?)
AddressOfEntryPoint
Contiene un puntatore all'indirizzo dell'entry point dell'applicazione (l'indirizzo iniziale). E' un parametro opzionale per le DLL (che non possono essere lanciate come un EXE)
BaseOfCode e BaseOfData
Contengono un puntatore all'inizio dell'area del codice e di quella dei dati, rispettivamente (posizione relativa all'ImageBase).
ImageBase
L'indirizzo a cui caricare l'immagine in memoria. Nel caso delle DLL il valore di default è 0x10000000, mentre nel caso degli EXE 0x00400000.
SectionAlignment
E' l'allineamento da dare alle sezioni in memoria. Il valore deve essere grande quanto il FileAlignment o più.
FileAlignment
E' l'allineamento dei dati raw all'interno del file immagine (espresso in byte). La dimensione è quella delle pagine del File System (compresa tra 512byte e 64KB).
MinorOperatingSystemVersion e MajorOperatingSystemVersion
Il numero di versione minore e maggiore richiesta.
MinorImageVersion e MajorImageVersion
Numeri minori e maggiori di versione dell'immagine.
MinorSubsystemVersion e MajorSubsystemVersion
Numero minori e maggiori del subsystem.
Win32VersionValue
Questo membro è riservato, deve valere 0.
SizeOfImage
Deve essere un multiplo di SectionAlignment. Indica la dimensione dell'intera immagine (inclusi gli headers).
SizeOfHeaders
E' la somma degli headers, arrotondata ad un multiplo di FileAlignment (4byte di Signature, e_lfanew del IMAGE_DOS_HEADER, dimensione di IMAGE_FILE_HEADER, dimensione di IMAGE_OPTIONAL_HEADER, e dimensione di tutte le SECTIONS HEADERs)
CheckSum
E' il checksum dell'immagine.
Subsystem
Indica il subsystem che ha richiesto l'esecuzione di questa immagine. I subsystem sono moltissimi, solo per citarne alcuni: drivers, Windows GUI, EFI driver...
DllCharacteristics
Indica le caratteristiche della DLL. Anche qui, vi sono molti valori disponibili (alcuni riservati).
SizeOfStackReserve
Il numero di byte da riservare per lo stack. Il valore non è direttamente quello caricato in fase di caricamento, ma viene è il valore massimo.
SizeOfStackCommit
Il numero di byte impegnati per lo stack (memoria effettivamente occupata al caricamento).
SizeOfHeapReserve
Numero di byte da riservare per l'heap locale. Anche in questo caso è il valore massimo.
SizeOfHeapCommit
Si tratta del valore di heap effettivamente impegnato per l'heap locale.
LoaderFlags
Obsoleto
NumberOfRvaAndSizes
Il numero degli elementi del DataDirectory dell'OptionalHeaders
DataDirectory
Un puntatore alla prima struttura IMAGE_DATA_DIRECTORY.
C'è una struttura molto importante qui, e si tratta di IMAGE_DATA_DIRECTORY. E' un array di strutture dove ciascuna di esse rappresenta una diversa DATA DIRECTORY; le più note sono la IMPORT TABLE e la EXPORT TABLE. In seguito giungeremo anche a questa struttura.
Esempio: IMAGE_OPTIONAL_HEADER
C:
#include<stdio.h>
#include<windows.h>
DWORD RVAToOffset(IMAGE_NT_HEADERS*, DWORD);
int main(int argc, char *argv[]) {
HANDLE hFile;
HANDLE hFileMapped;
LPVOID lpFileBase;
IMAGE_DOS_HEADER *pDosHeader;
IMAGE_NT_HEADERS *pImageNtHeader;
IMAGE_FILE_HEADER *pImageFileHeader;
IMAGE_OPTIONAL_HEADER *pImageOptionalHeader;
if(argc == 1) {
printf("Utilizzo: exedump <file.exe>");
return 1;
}
// Apro il file, ottenendo l'HANDLE
hFile = CreateFile(argv[1], GENERIC_READ ,FILE_SHARE_READ,0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
// Handle non valido (si e' verificato un errore)
if(hFile == INVALID_HANDLE_VALUE) {
printf("Errore nell'apertura del file\n%s", argv[1]);
return 1;
}
// Creo una mappa del file in memoria
hFileMapped = CreateFileMapping(hFile, 0, PAGE_READONLY, 0,0,NULL);
// Error Checking
// --------------------------------
if(hFileMapped == 0) {
printf("Errore mapping file");
return 1;
}
// --------------------------------
// Viene restituito un puntatore all'inizio, alla base
lpFileBase = MapViewOfFile(hFileMapped,FILE_MAP_READ,0,0,0);
// Error Checking
// --------------------------------
if(lpFileBase == 0) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Errore MapViewOfFile");
return 1;
}
// --------------------------------
// Casto per ottenere un puntatore alla struttura
pDosHeader = (IMAGE_DOS_HEADER *) lpFileBase;
// A questo punto verifico la firma
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Firma Non Valida!");
return 1;
}
printf("Firma Valida!\n\tPuntatore alla prossima struttura (IMAGE_NT_HEADER): 0x%08X\n\n", pDosHeader->e_lfanew);
// Punto alla nuova struttura dati, IMAGE_NT_HEADER
pImageNtHeader = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew + (DWORD) pDosHeader);
if(pImageNtHeader->Signature != IMAGE_NT_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("NT Header non valido!");
return 1;
}
printf("Signature valida!\n");
// Punto alla IMAGE_FILE_HEADER, e mostro alcuni dati
printf("\n----------------IMAGE_FILE_HEADER---------------------\n\n");
// Punto, come dicevamo sopra (nell'articolo), alla struttura IMAGE_FILE_HEADER
// --------------------------------------------------------------------------------------------------
pImageFileHeader = (IMAGE_FILE_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4);
printf("Machine: 0x%08X ", pImageFileHeader->Machine);
switch(pImageFileHeader->Machine) {
case IMAGE_FILE_MACHINE_I386:
printf("x86\n");
break;
case IMAGE_FILE_MACHINE_IA64:
printf("Intel Itanium\n");
break;
case IMAGE_FILE_MACHINE_AMD64:
printf("x64\n");
}
printf("Numero di sezioni: 0x%08X\n", pImageFileHeader->NumberOfSections);
printf("Size Of Optional Header: 0x%08X\n", pImageFileHeader->SizeOfOptionalHeader);
/*
* Characteristics è una DWORD, i valori (le costanti) sono molti, e vengono aggiunti
* utilizzando l'OR. In questo modo utilizzando un AND si puo' sapere se qualcosa e' settato
*/
printf("\nCharacteristics:\n");
if(pImageFileHeader->Characteristics & IMAGE_FILE_DLL) {
printf("\tIMAGE_FILE_DLL\n");
}
if(pImageFileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
printf("\tIMAGE_FILE_EXECUTABLE_IMAGE\n");
}
// Inizia la lettura dell'IMAGE_OPTIONAL_HEADER, con alcune informazioni
// ---------------------------------------------------------------------------------------------------
printf("\n\n------------------IMAGE_OPTIONAL_HEADER------------------\n\n");
// Punto all'optional header
pImageOptionalHeader = (IMAGE_OPTIONAL_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER));
// Stampo alcune informazioni
printf("Magic 0x%08X ",pImageOptionalHeader->Magic);
switch(pImageOptionalHeader->Magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
printf("Eseguibile, applicazione 32bit");
break;
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
printf("Eseguibile, applicazione a 64bit");
break;
case IMAGE_ROM_OPTIONAL_HDR_MAGIC:
printf("Immagine ROM");
break;
}
printf("\nDimensione code Section (.code): 0x%08X (%d)\n", pImageOptionalHeader->SizeOfCode, pImageOptionalHeader->SizeOfCode);
printf("\nDimensione dati inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfInitializedData, pImageOptionalHeader->SizeOfInitializedData);
printf("Dimensione dati non inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfUninitializedData, pImageOptionalHeader->SizeOfUninitializedData);
printf("\nInizio sezione codice: 0x%08X\n", pImageOptionalHeader->BaseOfCode);
printf("Inizio sezione data: 0x%08X\n", pImageOptionalHeader->BaseOfData);
printf("ImageBase: 0x%08X\n", pImageOptionalHeader->ImageBase);
printf("\nEntry Point (RVA): 0x%08X\n", pImageOptionalHeader->AddressOfEntryPoint);
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
return 0;
}
Output:
Codice:
>winapi hwinfo.exe
Firma Valida!
Puntatore alla prossima struttura (IMAGE_NT_HEADER): 0x000000B8
Signature valida!
----------------IMAGE_FILE_HEADER---------------------
Machine: 0x0000014C x86
Numero di sezioni: 0x00000003
Size Of Optional Header: 0x000000E0
Characteristics:
IMAGE_FILE_EXECUTABLE_IMAGE
------------------IMAGE_OPTIONAL_HEADER------------------
Magic 0x0000010B Eseguibile, applicazione 32bit
Dimensione code Section (.code): 0x00000600 (1536)
Dimensione dati inizializzati: 0x00000800 (2048)
Dimensione dati non inizializzati: 0x00000000 (0)
Inizio sezione codice: 0x00001000
Inizio sezione data: 0x00002000
ImageBase: 0x00400000
Entry Point (RVA): 0x00001000
La Section Table: IMAGE_SECTION_HEADER
Come dice il nome stesso questa è la tabella delle sezioni. Questa tabella contiene informazioni su ogni sezione presente nell'header. Le sezioni sono: code (ma ci si riferisce ad essa come .text anche), data, bss, rdata ed altre ancora. In generale, di sezioni possono essercene anche altre.
Questa struttura dati è più semplice della precedente, ed è mostrata di seguito:
C:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Prima di descrivere i singoli campi, vediamo a cosa serve.
In precedenza ho descritto come giungere a questa struttura dati, ma non ho mostrato per esteso il calcolo, lasciandolo ad un secondo momento. Ora quel momento è arrivato; il calcolo è il seguente:
NOTA: sono solito indicare le iniziali con p utilizzando la stessa convenzione della WINAPI. Ogni variabile che inizia con p indica un puntatore, ogni variabile che inizia con h indica un Handle e così via.
C:
pImageSectionHeader = (BaseAddress + dosHeader.e_lfanew + ntHeader.FileHeader.SizeOfOptionalHeader + 18h);
A questo punto abbiamo un puntatore ad una struttura IMAGE_SECTION_HEADER. Al suo interno troveremo tutte le informazioni necessarie per raggiungere la singola sezione; dopo tutto, sappiamo che ogni sezione inizia al termine della precedente, quindi sarà sufficiente incrementare un puntatore per raggiungerla.
Questa struttura ci da le seguenti informazioni:
Name
E' una stringa di 8byte con terminatore nullo. Se il nome ha esattamente 8 caratteri non viene inserito il terminatore
Misc
PhysicalAddress
Rappresenta l'indirizzo del file
VirtualSize
La dimensione totale della sezione quando viene caricata in memoria (espresso in byte). Se il valore è più grande del membro SizeOfRawData, la sezione viene riempita con degli 0. Questo membro è valido solo in caso delle immagini eseguibili; negli Object viene settato a 0.
VirtualAddress
E' l'indirizzo del primo byte della sezione quando l'immagine viene caricata in memoria, relativo all'ImageBase
SizeOfRawData
Dimensione dei dati inizializzati. Il valore deve essere un multiplo di FileAlignment (della struct IMAGE_OPTIONAL_HEADER). Se il valore è inferiore a VirtualSize la parte il resto viene riempito con 0.
PointerToRawData
Puntatore alla prima pagina all'interno del COFF.
PointerToRelocations
Un puntatore all'inizio dei membri di rilocazione della sezione. Se non ve ne sono, il membro è 0.
PointerToLinenumbers
Puntatore all'inizio dei line-numbers della sezione; se non ce ne sono vale 0.
NumberOfRelocations
Quantità di chiavi di rilocazione (chiavi inteso come 'entrate').
NumberOfLinenumbers
Il numero di entrate nel line-numbers.
Characteristics
Le caratteristiche dell'immagine. Vi sono molte costanti, e sono utilizzate per indicare se l'immagine contiene dati non inizializzati, dati inizializzati, se la sezione è esegubile e l'allineamento. I dati come negli altri casi analoghi vengono assegnati utilizzando l'operatore OR sulle costanti. Ad esempio (IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE or IMAGE_SCN_MEM_EXECUTE), indica che è scrivibile, leggibile ed eseguibile.
Ecco un esempio di come si presenta (tratto da PEAnalyzer sul file hwinfo.exe):
Codice:
Name: .text
Virtual Size: 0x0000056A
Virtual Address: 0x00001000
Size Of Raw Data: 0x00000600
Pointer To Raw Data: 0x00000400
Characteristics: 0x60000020
Name: .rdata
Virtual Size: 0x0000029A
Virtual Address: 0x00002000
Size Of Raw Data: 0x00000400
Pointer To Raw Data: 0x00000A00
Characteristics: 0x40000040
Name: .data
Virtual Size: 0x00000260
Virtual Address: 0x00003000
Size Of Raw Data: 0x00000200
Pointer To Raw Data: 0x00000E00
Characteristics: 0xC0000040
Ogni IMAGE_SECTION_HEADER è posta sotto alla precedente. Quindi se vogliamo raggiungere la seconda sarà sufficiente aggiungere un adeguato numero di byte al nostro puntatore. Il numero di byte è 28h (40byte, in decimale). In generale comunque possiamo anche non contare a mano i byte della struttura, ma avvalerci del comodo operatore sizeof se vogliamo sapere quanto è grande (ammesso di voler navigare in un PE scrivendo un software).
PE Analyzer si occupa anche dell'aggiunta di una sezione. Per farlo viene incrementato il numero delle sezioni:
Codice:
call findNTHeader
assume esi:ptr IMAGE_FILE_HEADER
mov ax, [esi].NumberOfSections
mov numberOfSections, ax
inc ax
mov [esi].NumberOfSections, ax
come si può osservare, in seguito all'aver individuato l'NT Header ed al puntare all'IMAGE_FILE_HADER, viene letto il membro NumberOfSections, per poi incrementarlo e reinserirlo nella struttura. A questo punto si prosegue con l'allineamento dei dati, ed in seguito alla lettura dell'indirizzo dell'ultima sezione. Una volta individuata si copiano i dati della sezione precedente (o li si inseriscono 'manualmente'); fatto ciò si scrive tutto su disco. Il discorso verrà ripreso nella sezione Avanzata dedicata a InjMyPE.
Esempio: Section Table (IMAGE_SECTION_HEADER)
C:
#include<stdio.h>
#include<windows.h>
DWORD RVAToOffset(IMAGE_NT_HEADERS*, DWORD);
int main(int argc, char *argv[]) {
HANDLE hFile;
HANDLE hFileMapped;
LPVOID lpFileBase;
char sectionName[9] = {0};
IMAGE_DOS_HEADER *pDosHeader;
IMAGE_NT_HEADERS *pImageNtHeader;
IMAGE_FILE_HEADER *pImageFileHeader;
IMAGE_OPTIONAL_HEADER *pImageOptionalHeader;
IMAGE_SECTION_HEADER *pImageSectionHeader;
if(argc == 1) {
printf("Utilizzo: exedump <file.exe>");
return 1;
}
// Apro il file, ottenendo l'HANDLE
hFile = CreateFile(argv[1], GENERIC_READ ,FILE_SHARE_READ,0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
// Handle non valido (si e' verificato un errore)
if(hFile == INVALID_HANDLE_VALUE) {
printf("Errore nell'apertura del file\n%s", argv[1]);
return 1;
}
// Creo una mappa del file in memoria
hFileMapped = CreateFileMapping(hFile, 0, PAGE_READONLY, 0,0,NULL);
// Error Checking
// --------------------------------
if(hFileMapped == 0) {
printf("Errore mapping file");
return 1;
}
// --------------------------------
// Viene restituito un puntatore all'inizio, alla base
lpFileBase = MapViewOfFile(hFileMapped,FILE_MAP_READ,0,0,0);
// Error Checking
// --------------------------------
if(lpFileBase == 0) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Errore MapViewOfFile");
return 1;
}
// --------------------------------
// Casto per ottenere un puntatore alla struttura
pDosHeader = (IMAGE_DOS_HEADER *) lpFileBase;
// A questo punto verifico la firma
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Firma Non Valida!");
return 1;
}
printf("Firma Valida!\n\tPuntatore alla prossima struttura (IMAGE_NT_HEADER): 0x%08X\n\n", pDosHeader->e_lfanew);
// Punto alla nuova struttura dati, IMAGE_NT_HEADER
pImageNtHeader = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew + (DWORD) pDosHeader);
if(pImageNtHeader->Signature != IMAGE_NT_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("NT Header non valido!");
return 1;
}
printf("Signature valida!\n");
// Punto alla IMAGE_FILE_HEADER, e mostro alcuni dati
printf("\n----------------IMAGE_FILE_HEADER---------------------\n\n");
// Punto, come dicevamo sopra (nell'articolo), alla struttura IMAGE_FILE_HEADER
// --------------------------------------------------------------------------------------------------
pImageFileHeader = (IMAGE_FILE_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4);
printf("Machine: 0x%08X ", pImageFileHeader->Machine);
switch(pImageFileHeader->Machine) {
case IMAGE_FILE_MACHINE_I386:
printf("x86\n");
break;
case IMAGE_FILE_MACHINE_IA64:
printf("Intel Itanium\n");
break;
case IMAGE_FILE_MACHINE_AMD64:
printf("x64\n");
}
printf("Numero di sezioni: 0x%08X\n", pImageFileHeader->NumberOfSections);
printf("Size Of Optional Header: 0x%08X\n", pImageFileHeader->SizeOfOptionalHeader);
/*
* Characteristics è una DWORD, i valori (le costanti) sono molti, e vengono aggiunti
* utilizzando l'OR. In questo modo utilizzando un AND si puo' sapere se qualcosa e' settato
*/
printf("\nCharacteristics:\n");
if(pImageFileHeader->Characteristics & IMAGE_FILE_DLL) {
printf("\tIMAGE_FILE_DLL\n");
}
if(pImageFileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
printf("\tIMAGE_FILE_EXECUTABLE_IMAGE\n");
}
// Inizia la lettura dell'IMAGE_OPTIONAL_HEADER, con alcune informazioni
// ---------------------------------------------------------------------------------------------------
printf("\n\n------------------IMAGE_OPTIONAL_HEADER------------------\n\n");
// Punto all'optional header
pImageOptionalHeader = (IMAGE_OPTIONAL_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER));
// Stampo alcune informazioni
printf("Magic 0x%08X ",pImageOptionalHeader->Magic);
switch(pImageOptionalHeader->Magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
printf("Eseguibile, applicazione 32bit");
break;
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
printf("Eseguibile, applicazione a 64bit");
break;
case IMAGE_ROM_OPTIONAL_HDR_MAGIC:
printf("Immagine ROM");
break;
}
printf("\nDimensione code Section (.code): 0x%08X (%d)\n", pImageOptionalHeader->SizeOfCode, pImageOptionalHeader->SizeOfCode);
printf("\nDimensione dati inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfInitializedData, pImageOptionalHeader->SizeOfInitializedData);
printf("Dimensione dati non inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfUninitializedData, pImageOptionalHeader->SizeOfUninitializedData);
printf("\nInizio sezione codice: 0x%08X\n", pImageOptionalHeader->BaseOfCode);
printf("Inizio sezione data: 0x%08X\n", pImageOptionalHeader->BaseOfData);
printf("ImageBase: 0x%08X\n", pImageOptionalHeader->ImageBase);
printf("\nEntry Point (RVA): 0x%08X\n", pImageOptionalHeader->AddressOfEntryPoint);
// --------------------------------------------------------------------------------------------------------
printf("\n-----------------------IMAGE_SECTION_HEADER-------------------\n\n");
// Section Table; punto alla IMAGE_SECTION_HEADER
pImageSectionHeader = (IMAGE_SECTION_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER));
// Leggo il numero delle Sezioni nell'IMAGE_FILE_HEADER
int i = -1;
while(++i < pImageFileHeader->NumberOfSections) {
memcpy(sectionName, pImageSectionHeader[i].Name, IMAGE_SIZEOF_SHORT_NAME);
printf("Nome: %s\n", sectionName);
printf("Virtual Size: 0x%08X\n", pImageSectionHeader[i].Misc.VirtualSize);
printf("Virtual Address: 0x%08X\n", pImageSectionHeader[i].VirtualAddress);
printf("Size Of Raw Data: 0x%08X\n", pImageSectionHeader[i].SizeOfRawData);
printf("Pointer To Raw Data: 0x%08X\n", pImageSectionHeader[i].PointerToRawData);
printf("Characteristics: 0x%08X\n\n", pImageSectionHeader[i].Characteristics);
}
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
return 0;
}
Output:
Codice:
>winapi hwinfo.exe
Firma Valida!
Puntatore alla prossima struttura (IMAGE_NT_HEADER): 0x000000B8
Signature valida!
----------------IMAGE_FILE_HEADER---------------------
Machine: 0x0000014C x86
Numero di sezioni: 0x00000003
Size Of Optional Header: 0x000000E0
Characteristics:
IMAGE_FILE_EXECUTABLE_IMAGE
------------------IMAGE_OPTIONAL_HEADER------------------
Magic 0x0000010B Eseguibile, applicazione 32bit
Dimensione code Section (.code): 0x00000600 (1536)
Dimensione dati inizializzati: 0x00000800 (2048)
Dimensione dati non inizializzati: 0x00000000 (0)
Inizio sezione codice: 0x00001000
Inizio sezione data: 0x00002000
ImageBase: 0x00400000
Entry Point (RVA): 0x00001000
-----------------------IMAGE_SECTION_HEADER-------------------
Nome: .text
Virtual Size: 0x0000056A
Virtual Address: 0x00001000
Size Of Raw Data: 0x00000600
Pointer To Raw Data: 0x00000400
Characteristics: 0x60000020
Nome: .rdata
Virtual Size: 0x0000029A
Virtual Address: 0x00002000
Size Of Raw Data: 0x00000400
Pointer To Raw Data: 0x00000A00
Characteristics: 0x40000040
Nome: .data
Virtual Size: 0x00000260
Virtual Address: 0x00003000
Size Of Raw Data: 0x00000200
Pointer To Raw Data: 0x00000E00
Characteristics: 0xC0000040
La struct IMAGE_DATA_DIRECTORY: ritorno nell'IMAGE_OPTIONAL_HEADER
I più attenti avranno notato che l'ultimo membro della struttura IMAGE_OPTIONAL_HEADER è stato solo accennato in precedenza. E' giunto ora il momento di affrontarlo. Questo è un array di strutture IMAGE_DATA_DIRECTORY, ed è anche relativamente semplice. Tuttavia sarà poi necessario introdurre un altro concetto... ma è meglio avanzare poco alla volta.
Prima di descriverlo, vediamo la struct:
C:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
VirtualAddress
L'indirizzo virtuale della tabella.
Size
La dimensione della tabella.
Ora la domanda: quante sono le DataDirectory? La risposta è: 15.
L'include file di C winnt.h (Window NT) definisce delle costanti che possono essere utilizzate come indice in questo array:
C:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
#define IMAGE_DIRECTORY_ENTRY_TLS 9
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
#define IMAGE_DIRECTORY_ENTRY_IAT 12
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14
La Export Table identifica le funzioni che vengono esportate dal modulo. Sono quelle funzioni che altri moduli possono quindi importare.
La Import Table è forse la più conosciuta, e passatemelo, direi anche la più importante: infatti la analizzeremo subito.
IMAGE_IMPORT_DIRECTORY
Ecco un output di esempio di PE Analyzer (di hwinfo.exe):
Codice:
Import Table:
Module Name: gdi32.dll
Functions:
TextOutA
SetBkMode
...[Press 'ESC' for go back to menu]...
Module Name: user32.dll
Functions:
LoadIconA
MessageBoxA
PostQuitMessage
LoadCursorA
ReleaseDC
ShowWindow
TranslateMessage
UpdateWindow
wsprintfA
GetMessageA
DispatchMessageA
DefWindowProcA
CreateWindowExA
BeginPaint
RegisterClassExA
...[Press 'ESC' for go back to menu]...
Module Name: kernel32.dll
Functions:
GlobalMemoryStatus
GetSystemInfo
GetModuleHandleA
GetCommandLineA
ExitProcess
...[Press 'ESC' for go back to menu]...
Ogni esterno che viene importato (le librerie dll) hanno il proprio elenco di funzioni. Se ne deduce quindi che la seguente tabella è molto importante: è proprio la tabella che legge il loader di Windows al momento del caricamento dell'eseguibile in memoria. La seguente tabella può anche essere manipolata, magari con scopi non esattamente "buoni"...
Cito un frammento di codice di PE Analyzer, dove mostra in quale modo si ottiene l'indirizzo della Import Table:
Codice:
call findNTHeader
add esi, 14h
assume esi:ptr IMAGE_OPTIONAL_HEADER
; Get Import Table address into DataDirectory array
; sizeof IMAGE_DATA_DIRECTORY = 2nd member of the array -> Import Table (VirtualAddress)
mov edx,[esi].DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress
Ciò che avviene è praticamente la stessa cosa dei casi precedenti: viene cercato l'NT Header e viene evitata la struttura IMAGE_FILE_HEADER (add esi, 14h), così da raggiungere IMAGE_OPTIONAL_HEADER. Fatto ciò si accede alla Import Table. Dato che è la seconda dell'array, la troviamo all'indice 1. Con assembly è necessario accedere alla posizione sizeof IMAGE_DATA_DIRECTORY, che è poi la stessa cosa: nel caso di specie è come se moltiplicassimo IAMGE_DATA_DIRECTORY*1. Se volessimo accedere alla Export dovremmo utilizzare IMAGE_DATA_DIRECTORY*0. Se volessimo accedere alla Exception l'indice sarebbe IMAGE_DATA_DIRECTORY*2, e così via. Fatto ciò, viene letto il VirtualAddress (tra poco ne parleremo). Chiaramente è anche possibile utilizzare (ad esempio nel linguaggio C) le costanti mostrate sopra per l'accesso diretto agli elementi.
La struttura della Import Table è la seguente:
C:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
_ANONYMOUS_UNION union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
Prima di continuare con questo discorso voglio introdurre un altro concetto molto importante: l'RVA. I campi che contengono questo tipo di indirizzo sono in realtà molti, come AddressOfEntryPoint di IMAGE_OPTIONALE_HEADER. Cosa significa questo indirizzo? In generale l'RVA è un offset relativo però a dove il file (o la sezione, nel caso del campo VirtualAddress di IMAGE_SECTION_HEADER) va collocato in memoria, ovvero l'RVA è l'indirizzo a cui il loader caricherà quel file o quella sezione. L'indirizzo è quindi specificato dalla Section Table.
Per orientarci con i file offset è necessario convertire un RVA in un file offset. Per farlo si può utilizzare la seguente formula:
RVA=VirtualAddress - Image Base
Quindi:
VA=RVA + Image Base
Quindi:
VA=RVA + Image Base
Facciamo un esempio pratico utilizzando sempre hwinfo.exe:
Codice:
ImageBase =0x00400000 // Ne abbiamo parlato nella sezione dedicata all'IMAGE_OPTIONAL_HEADER
BaseOfCode=0x00001000 // E' un RVA
VA=ImageBase + BaseOfCode=0x00401000
Se apriamo un debugger come OllyDbg come indirizzo di partenza vedremo infatti 0x00401000, e non 0x00400000 come ci si potrebbe aspettare (dato che per un exe, com'è stato detto all'inizio, il valore è proprio quello).
Guardando il riquadro a destra, noterete infatti il registro EIP che contiene 0x00401000 con ModuleEntryPoint.
Come ultima struttura della IMAGE_IMPORT_DESCRIPTOR si trova un'omonima struttura con i campi tutti settati a 0. Passiamo ora ad una descrizione dei campi:
union
Characteristics
-
OriginalFirstThunk
Contiene l'RVA di IMAGE_THUNK_DATA, una struttura descritta qui sotto
TimeDateStamp
Data ed Ora della creazione del file
ForwarderChain
E' un argomento avanzato e non documentato, riguarda il forwarding delle DLL
Name
Contiene l'RVA al nome della libreria dinamica (DLL)
FirstThunk
Come OriginalFirstThunk, contiene un'array di IMAGE_THUNK_DATA; capiremo a breve perchè ci sono due array con le stesse strutture
Qui di seguito la struttura IMAGE_THUNK_DATA:
C:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;
OriginalFirstThunk contiene quindi l'RVA a tale struttura. A sua volta questa struttura contiene un puntatore a IMAGE_IMPORT_BY_NAME.
C:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
Hint
E' l'indice all'interno della Export table della DLL. Non è di vitale importanza
Name
E' il nome di una funzione importata. E' una stringa in formato ASCIIZ (ASCII terminata da uno 0)
Ma facciamo un passo indietro: torniamo alla IMAGE_IMPORT_DESCRIPTOR, ed in particolare a FirstThunk ed OriginalFirstThunk.
Come anticipato, questi due campi hanno puntatori alla stessa struttura. Per comprenderne il funzionamento, immaginate: FirstThunk ed OrigianalFirstThunk sono array, ciascuna posizione di questo array è una IMAGE_THUNK_DATA (niente più di un RVA; infatti la struttura è una union). Ciascuna di queste posizioni dei due campi punta ad una medesima IMAGE_IMPORT_BY_NAME.
La situazione è praticamente quella illustrata:
FirstThunk | IMAGE_IMPORT_BY_NAME | OriginalFirstThunk |
IMAGE_THUNK_DATA -> | Funzione 1 | <- IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA -> | Funzione 2 | <- IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA -> | Funzione N | <- IMAGE_THUNK_DATA |
Quando il loader carica in memoria il PE, va a guardare ogni IMAGE_THUNK_DATA ed ogni IMAGE_IMPORT_BY_NAME per determinare l'indirizzo di ciascuna funzione importata. Successivamente il valore di FirstThunk viene rimpiazzato con l'indirizzo reale a tale funzione. L'aspetto sarà
quindi il seguente:
FirstThunk | IMAGE_IMPORT_BY_NAME | OriginalFirstThunk |
Indirizzo Funzione 1 | Funzione 1 | <- IMAGE_THUNK_DATA |
Indirizzo Funzione 2 | Funzione 2 | <- IMAGE_THUNK_DATA |
Indirizzo Funzione N | Funzione N | <- IMAGE_THUNK_DATA |
NOTA:
All'interno dell'eseguibile ogni chiamata ad una funzione viene in realtà fatta verso la IAT. Vi sono diverse tecniche... una è utilizzando la "JMP Table" (jump table). Il funzionamento è il seguente: tipicamente alla fine del segmento di testo (lo vedo più spesso all'inizio ed alla fine) vengono inseriti dei JMP verso le funzioni reali. Ad esempio, se un programma chiama la MessageBoxA in questo modo:
Codice:
call MessageBoxA
quel "MessageBoxA" avrà in realtà questa forma:
Codice:
call <JMP.&user32.MessageBoxA>
Questa JUMP salta alla "jump table", che a sua volta richiama la funzione stessa.
Questo meccanismo è necessario in quanto un programma non sa dov'è locata la funzione che vuole chiamare (ecco perchè è il loader ad effettuare le opportune sostituzioni).
La situazione è chiarita dall'immagine sottostante:
Qualsiasi chiamata all'interno di hwinfo.exe sarà diretta all'indirizzo 0x0040151C, che a sua volta salterà all'indirizzo che vedete dopo all'opcode FF 25, ovvero 0x00402028 (i byte sono disposti al contrario).
OriginalFirstThunk non ha subito modifiche, e questo consente di poter recuperare il nome di ciascuna funzione.
Osservando ora IMAGE_THUNK_DATA vedrete un campo Ordinal. Questo campo specifica un numero, e più precisamente la posizione della funzione: quindi, in buona sostanza, non si può chiamare per nome ma la si deve chiamare per Ordinal. Quando una funzione viene esportata da un modulo per Ordinal il chiamante la può riconoscere grazie al bit MSB (Most Significant Bit) settato a 1. Il bit più significativo è naturalmente il primo bit partendo da sinistra in un numero.
La Microsoft mette a disposizione una costante, chiamata IMAGE_ORDINAL_FLAG32. Questa costante è una DWORD con valore 0x80000000, quindi con il primo bit settato appunto (in binario 8h è rappresentato da 1000b, ecco che infatti il bit è settato). Se l'Ordinal di una funzione è ad esempio 0x7859, il valore contenuto da IMAGE_THUNK_DATA per la funzione sarà 0x80007859.
In termini pratici il confronto è molto semplice, e PEAnalyzer ovviamente lo deve effettuare.
Codice:
; check if it is imported by name or ordinal
test dword ptr [edi],IMAGE_ORDINAL_FLAG32
jnz importByOrdinal
In Assembly l'istruzione TEST è analoga alla CMP, con la differenza che la prima esegue un AND bit a bit, mentre la seconda una sottrazione ("buttando" il risultato, quindi senza modificare i valori esistenti). Dato che TEST è in pratica una AND, ciò che avviene è questo (userò come valore quello mostrato in precedenza):
Codice:
0x80007859
and
0x80000000
----------
0x80000000
Se il bit è settato (e quindi il risultato è 1), la funzione è importata per ordinal (il JNZ dell'assembly indica un "Salta se è diverso da 0"; l'etichetta che lo segue è esplicativa, ed è quella di destinazione quando il salto si verifica).
A questo punto siamo arrivati ad IMAGE_THUNK_DATA ed alla IMAGE_IMPORT_BY_NAME.Il numero di elementi di IMAGE_IMPORT_DESCRIPTOR dipende dal numero delle DLL importate; così come il numero delle IMAGE_THUNK_DATA dipende dal numero delle funzioni importate. L'importante è che si tenga a mente che IMAGE_THUNK_DATA è solo un RVA, niente di più.
Solitamente si può utilizzare OriginalFirstThunk, ma può capitare di trovarla tutto a 0 (colpa del linker), quindi è sempre bene effettuare un check prima e nel caso utilizzare FirstThunk.
Di seguito verrà mostrato un "riepilogo" e verrà simulato punto per punto come raggiungere queste funzioni, partendo dalla base (caricamento del file PE).
1) apertura del file, che restituisce la base (un HANDLE);
2) verifica del DOS header (MZ); SE valido si prosegue
3) verifica del PE header (PE00);
4) si raggiunge l'OptionalHeader, e si legge l'indirizzo del DataDirectory;
5) a questo punto si ottiene il secondo membro della struttura, che corrisponde all'IMPORT;
6) si effettua il check su OriginalFirstThunk, se è valido lo si utilizza, se non lo è si utilizza FirstThunk;
7) questo indirizzo è un RVA, e deve quindi essere convertito in un VA (come visto in precedenza);
8) ottenuto l'indirizzo si effettua un while e si prende ogni membri dell'array: ogni membro è una puntatore ad IMAGE_THUNK_DATA;
9) si effettua il check per sapere se è importata per Ordinal o per Nome; se è importata per Ordinal possiamo già prendere il numero, altrimenti proseguiamo;
10) se è importata per nome si utilizza l'RVA nella IMAGE_IMPORT_BY_NAME, si sommano 2 byte (il campo Hint), e si legge il nome della funzione; incrementando il puntatore si arriva alla prossima funzione;
11) quando siamo giunti all'ultima funzione importata troveremo un valore nullo; a questo punto usciamo dal ciclo e prendiamo la DLL successiva.
12) al termine delle DLL importate anche IMAGE_IMPORT_DESCRIPTOR avrà un valore 0, così sappiamo che le DLL importate sono terminate.
Ora l'output di PEAnalyzer dovrebbe essere ancora più chiaro:
Codice:
Import Table:
Module Name: gdi32.dll
Functions:
TextOutA
SetBkMode
...[Press 'ESC' for go back to menu]...
Module Name: user32.dll
Functions:
LoadIconA
MessageBoxA
PostQuitMessage
LoadCursorA
ReleaseDC
ShowWindow
TranslateMessage
UpdateWindow
wsprintfA
GetMessageA
DispatchMessageA
DefWindowProcA
CreateWindowExA
BeginPaint
RegisterClassExA
...[Press 'ESC' for go back to menu]...
Module Name: kernel32.dll
Functions:
GlobalMemoryStatus
GetSystemInfo
GetModuleHandleA
GetCommandLineA
ExitProcess
...[Press 'ESC' for go back to menu]...
Esempio: IMAGE_IMPORT_DESCRIPTOR
C:
#include<stdio.h>
#include<windows.h>
DWORD RVAToOffset(IMAGE_NT_HEADERS*, DWORD);
int main(int argc, char *argv[]) {
HANDLE hFile;
HANDLE hFileMapped;
LPVOID lpFileBase;
char sectionName[9] = {0};
IMAGE_DOS_HEADER *pDosHeader;
IMAGE_NT_HEADERS *pImageNtHeader;
IMAGE_FILE_HEADER *pImageFileHeader;
IMAGE_OPTIONAL_HEADER *pImageOptionalHeader;
IMAGE_SECTION_HEADER *pImageSectionHeader;
IMAGE_IMPORT_DESCRIPTOR *pImageImportDescriptor;
IMAGE_IMPORT_BY_NAME *pImageImportByName;
if(argc == 1) {
printf("Utilizzo: exedump <file.exe>");
return 1;
}
// Apro il file, ottenendo l'HANDLE
hFile = CreateFile(argv[1], GENERIC_READ ,FILE_SHARE_READ,0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
// Handle non valido (si e' verificato un errore)
if(hFile == INVALID_HANDLE_VALUE) {
printf("Errore nell'apertura del file\n%s", argv[1]);
return 1;
}
// Creo una mappa del file in memoria
hFileMapped = CreateFileMapping(hFile, 0, PAGE_READONLY, 0,0,NULL);
// Error Checking
// --------------------------------
if(hFileMapped == 0) {
printf("Errore mapping file");
return 1;
}
// --------------------------------
// Viene restituito un puntatore all'inizio, alla base
lpFileBase = MapViewOfFile(hFileMapped,FILE_MAP_READ,0,0,0);
// Error Checking
// --------------------------------
if(lpFileBase == 0) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Errore MapViewOfFile");
return 1;
}
// --------------------------------
// Casto per ottenere un puntatore alla struttura
pDosHeader = (IMAGE_DOS_HEADER *) lpFileBase;
// A questo punto verifico la firma
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Firma Non Valida!");
return 1;
}
printf("Firma Valida!\n\tPuntatore alla prossima struttura (IMAGE_NT_HEADER): 0x%08X\n\n", pDosHeader->e_lfanew);
// Punto alla nuova struttura dati, IMAGE_NT_HEADER
pImageNtHeader = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew + (DWORD) pDosHeader);
if(pImageNtHeader->Signature != IMAGE_NT_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("NT Header non valido!");
return 1;
}
printf("Signature valida!\n");
// Punto alla IMAGE_FILE_HEADER, e mostro alcuni dati
printf("\n----------------IMAGE_FILE_HEADER---------------------\n\n");
// Punto, come dicevamo sopra (nell'articolo), alla struttura IMAGE_FILE_HEADER
// --------------------------------------------------------------------------------------------------
pImageFileHeader = (IMAGE_FILE_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4);
printf("Machine: 0x%08X ", pImageFileHeader->Machine);
switch(pImageFileHeader->Machine) {
case IMAGE_FILE_MACHINE_I386:
printf("x86\n");
break;
case IMAGE_FILE_MACHINE_IA64:
printf("Intel Itanium\n");
break;
case IMAGE_FILE_MACHINE_AMD64:
printf("x64\n");
}
printf("Numero di sezioni: 0x%08X\n", pImageFileHeader->NumberOfSections);
printf("Size Of Optional Header: 0x%08X\n", pImageFileHeader->SizeOfOptionalHeader);
/*
* Characteristics è una DWORD, i valori (le costanti) sono molti, e vengono aggiunti
* utilizzando l'OR. In questo modo utilizzando un AND si puo' sapere se qualcosa e' settato
*/
printf("\nCharacteristics:\n");
if(pImageFileHeader->Characteristics & IMAGE_FILE_DLL) {
printf("\tIMAGE_FILE_DLL\n");
}
if(pImageFileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
printf("\tIMAGE_FILE_EXECUTABLE_IMAGE\n");
}
// Inizia la lettura dell'IMAGE_OPTIONAL_HEADER, con alcune informazioni
// ---------------------------------------------------------------------------------------------------
printf("\n\n------------------IMAGE_OPTIONAL_HEADER------------------\n\n");
// Punto all'optional header
pImageOptionalHeader = (IMAGE_OPTIONAL_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER));
// Stampo alcune informazioni
printf("Magic 0x%08X ",pImageOptionalHeader->Magic);
switch(pImageOptionalHeader->Magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
printf("Eseguibile, applicazione 32bit");
break;
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
printf("Eseguibile, applicazione a 64bit");
break;
case IMAGE_ROM_OPTIONAL_HDR_MAGIC:
printf("Immagine ROM");
break;
}
printf("\nDimensione code Section (.code): 0x%08X (%d)\n", pImageOptionalHeader->SizeOfCode, pImageOptionalHeader->SizeOfCode);
printf("\nDimensione dati inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfInitializedData, pImageOptionalHeader->SizeOfInitializedData);
printf("Dimensione dati non inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfUninitializedData, pImageOptionalHeader->SizeOfUninitializedData);
printf("\nInizio sezione codice: 0x%08X\n", pImageOptionalHeader->BaseOfCode);
printf("Inizio sezione data: 0x%08X\n", pImageOptionalHeader->BaseOfData);
printf("ImageBase: 0x%08X\n", pImageOptionalHeader->ImageBase);
printf("\nEntry Point (RVA): 0x%08X\n", pImageOptionalHeader->AddressOfEntryPoint);
// --------------------------------------------------------------------------------------------------------
printf("\n-----------------------IMAGE_SECTION_HEADER-------------------\n\n");
// Section Table; punto alla IMAGE_SECTION_HEADER
pImageSectionHeader = (IMAGE_SECTION_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER));
// Leggo il numero delle Sezioni nell'IMAGE_FILE_HEADER
int i = -1;
while(++i < pImageFileHeader->NumberOfSections) {
memcpy(sectionName, pImageSectionHeader[i].Name, IMAGE_SIZEOF_SHORT_NAME);
printf("Nome: %s\n", sectionName);
printf("Virtual Size: 0x%08X\n", pImageSectionHeader[i].Misc.VirtualSize);
printf("Virtual Address: 0x%08X\n", pImageSectionHeader[i].VirtualAddress);
printf("Size Of Raw Data: 0x%08X\n", pImageSectionHeader[i].SizeOfRawData);
printf("Pointer To Raw Data: 0x%08X\n", pImageSectionHeader[i].PointerToRawData);
printf("Characteristics: 0x%08X\n\n", pImageSectionHeader[i].Characteristics);
}
// Leggiamo la Import Table...
// ----------------------------------------------------------------------------------------
printf("\n\n---------------------------IMAGE_IMPORT_DESCRIPTOR------------------\n");
DWORD dwOffset = RVAToOffset(pImageNtHeader, pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
if(dwOffset != 0) {
// Puntiamo alla Image Import Directory
pImageImportDescriptor = (IMAGE_IMPORT_DESCRIPTOR*) (dwOffset + (DWORD) pDosHeader);
if(pImageImportDescriptor->FirstThunk == 0) {
printf("First Thunk 0");
return 1;
}
printf("Import Table Individuata!\n\n");
register int index = -1;
while(pImageImportDescriptor[++index].FirstThunk != 0) {
char *Name = (char*) RVAToOffset(pImageNtHeader, pImageImportDescriptor[index].Name) + (DWORD) pDosHeader;
printf("Modulo (DLL): %s 0x%08X\n", Name, pImageImportDescriptor[index]);
printf("Funzioni:\n");
// Come dicevamo nell'articolo, si dovrebbe considerare OriginalFirstThunk
// ma questo potrebbe essere a 0. Questo significa che ora dovremo
// controllare quale dei due considerare
DWORD *Thunk = (DWORD*) (RVAToOffset(pImageNtHeader, (pImageImportDescriptor[index].OriginalFirstThunk == 0) ? pImageImportDescriptor[index].FirstThunk : pImageImportDescriptor[index].OriginalFirstThunk) + (DWORD) pDosHeader);
DWORD *Thunk1 = (DWORD*) (RVAToOffset(pImageNtHeader, (pImageImportDescriptor[index].FirstThunk))+(DWORD)pDosHeader);
register int index1 = -1;
// Iniziamo a scorrere i nomi delle funzioni
while(Thunk[++index1] != 0) {
// Dobbiamo verificare se e' importata per ordinal o per nome
// verificando se l'MSB è settato
if(Thunk[index1] & IMAGE_ORDINAL_FLAG) {
printf("\tOrdinal: 0x%08X", (Thunk[index1]-IMAGE_ORDINAL_FLAG));
} else {
pImageImportByName = (IMAGE_IMPORT_BY_NAME*) (RVAToOffset(pImageNtHeader, Thunk[index1]) + (DWORD)pDosHeader);
printf("\tNome: %s\n", pImageImportByName->Name);
printf("\tIndirizzo RVA (della funzione): 0x%08X\n", Thunk[index1]);
printf("\tIndirizzo VA (jump nell'exe): 0x%08X\n", (pImageImportDescriptor[index].FirstThunk+index1*4)+pImageOptionalHeader->ImageBase);
}
}
printf("\n");
}
// ------------------------------------------------------------------------------------------
}
else {
printf("Offset Non trovato!");
}
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
return 0;
}
// Effettua la conversione da RVA a VA
DWORD RVAToOffset(IMAGE_NT_HEADERS *pImageNtHeader, DWORD dwRva) {
DWORD dwOffset = dwRva;
IMAGE_SECTION_HEADER *pImageSectionHeader = IMAGE_FIRST_SECTION(pImageNtHeader);
WORD wNumberOfSections = pImageNtHeader->FileHeader.NumberOfSections;
if(dwOffset < pImageSectionHeader->PointerToRawData) {
return dwOffset;
}
int index = 0;
while(wNumberOfSections > 0) {
if(dwOffset >= pImageSectionHeader[index].VirtualAddress) {
DWORD dwTemp = (pImageSectionHeader[index].VirtualAddress + pImageSectionHeader[index].SizeOfRawData);
// Dato che il valore e' stato superato,
// l'offset e' in questa sezione
if(dwOffset < dwTemp) {
dwTemp = pImageSectionHeader[index].VirtualAddress;
dwOffset -= dwTemp;
dwTemp = pImageSectionHeader[index].PointerToRawData;
dwTemp += dwOffset;
return dwTemp;
}
}
wNumberOfSections--;
index++;
}
return 0;
}
Output:
Codice:
>winapi hwinfo.exe
Firma Valida!
Puntatore alla prossima struttura (IMAGE_NT_HEADER): 0x000000B8
Signature valida!
----------------IMAGE_FILE_HEADER---------------------
Machine: 0x0000014C x86
Numero di sezioni: 0x00000003
Size Of Optional Header: 0x000000E0
Characteristics:
IMAGE_FILE_EXECUTABLE_IMAGE
------------------IMAGE_OPTIONAL_HEADER------------------
Magic 0x0000010B Eseguibile, applicazione 32bit
Dimensione code Section (.code): 0x00000600 (1536)
Dimensione dati inizializzati: 0x00000800 (2048)
Dimensione dati non inizializzati: 0x00000000 (0)
Inizio sezione codice: 0x00001000
Inizio sezione data: 0x00002000
ImageBase: 0x00400000
Entry Point (RVA): 0x00001000
-----------------------IMAGE_SECTION_HEADER-------------------
Nome: .text
Virtual Size: 0x0000056A
Virtual Address: 0x00001000
Size Of Raw Data: 0x00000600
Pointer To Raw Data: 0x00000400
Characteristics: 0x60000020
Nome: .rdata
Virtual Size: 0x0000029A
Virtual Address: 0x00002000
Size Of Raw Data: 0x00000400
Pointer To Raw Data: 0x00000A00
Characteristics: 0x40000040
Nome: .data
Virtual Size: 0x00000260
Virtual Address: 0x00003000
Size Of Raw Data: 0x00000200
Pointer To Raw Data: 0x00000E00
Characteristics: 0xC0000040
---------------------------IMAGE_IMPORT_DESCRIPTOR------------------
Import Table Individuata!
Modulo (DLL): gdi32.dll 0x000020B4
Funzioni:
Nome: TextOutA
Indirizzo RVA (della funzione): 0x00002124
Indirizzo VA (jump nell'exe): 0x00402000
Nome: SetBkMode
Indirizzo RVA (della funzione): 0x00002118
Indirizzo VA (jump nell'exe): 0x00402004
Modulo (DLL): user32.dll 0x000020D8
Funzioni:
Nome: LoadIconA
Indirizzo RVA (della funzione): 0x0000219C
Indirizzo VA (jump nell'exe): 0x00402024
Nome: MessageBoxA
Indirizzo RVA (della funzione): 0x000021A8
Indirizzo VA (jump nell'exe): 0x00402028
Nome: PostQuitMessage
Indirizzo RVA (della funzione): 0x000021B6
Indirizzo VA (jump nell'exe): 0x0040202C
Nome: LoadCursorA
Indirizzo RVA (della funzione): 0x0000218E
Indirizzo VA (jump nell'exe): 0x00402030
Nome: ReleaseDC
Indirizzo RVA (della funzione): 0x000021DC
Indirizzo VA (jump nell'exe): 0x00402034
Nome: ShowWindow
Indirizzo RVA (della funzione): 0x000021E8
Indirizzo VA (jump nell'exe): 0x00402038
Nome: TranslateMessage
Indirizzo RVA (della funzione): 0x000021F6
Indirizzo VA (jump nell'exe): 0x0040203C
Nome: UpdateWindow
Indirizzo RVA (della funzione): 0x0000220A
Indirizzo VA (jump nell'exe): 0x00402040
Nome: wsprintfA
Indirizzo RVA (della funzione): 0x0000221A
Indirizzo VA (jump nell'exe): 0x00402044
Nome: GetMessageA
Indirizzo RVA (della funzione): 0x00002180
Indirizzo VA (jump nell'exe): 0x00402048
Nome: DispatchMessageA
Indirizzo RVA (della funzione): 0x0000216C
Indirizzo VA (jump nell'exe): 0x0040204C
Nome: DefWindowProcA
Indirizzo RVA (della funzione): 0x0000215A
Indirizzo VA (jump nell'exe): 0x00402050
Nome: CreateWindowExA
Indirizzo RVA (della funzione): 0x00002148
Indirizzo VA (jump nell'exe): 0x00402054
Nome: BeginPaint
Indirizzo RVA (della funzione): 0x0000213A
Indirizzo VA (jump nell'exe): 0x00402058
Nome: RegisterClassExA
Indirizzo RVA (della funzione): 0x000021C8
Indirizzo VA (jump nell'exe): 0x0040205C
Modulo (DLL): kernel32.dll 0x000020C0
Funzioni:
Nome: GlobalMemoryStatus
Indirizzo RVA (della funzione): 0x00002276
Indirizzo VA (jump nell'exe): 0x0040200C
Nome: GetSystemInfo
Indirizzo RVA (della funzione): 0x00002266
Indirizzo VA (jump nell'exe): 0x00402010
Nome: GetModuleHandleA
Indirizzo RVA (della funzione): 0x00002252
Indirizzo VA (jump nell'exe): 0x00402014
Nome: GetCommandLineA
Indirizzo RVA (della funzione): 0x00002240
Indirizzo VA (jump nell'exe): 0x00402018
Nome: ExitProcess
Indirizzo RVA (della funzione): 0x00002232
Indirizzo VA (jump nell'exe): 0x0040201C
IMAGE_EXPORT_DIRECTORY
Passiamo ora al primo membro della DataDirectory (di OptionalHeader), la Export Table:
Codice:
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;
Characteristics
Non è utilizzato; settato a 0.
TimeDateStamp
Data ed ora della creazione del file
MajorVersion e MinorVersion
Non utilizzati, settati a 0
Name
L'RVA ad una stringa ASCIIZ con il nome della DLL
Base
L'ordinal di partenzia per le funzioni esportate. Per ottenere l'ordinal esportato per una funzione questo valore viene sommato all'eleemento appropriato del campo AddressOfNameOrdinals
NumberOfFunctions
Numero di elementi che compongono l'array AddressOfFunctions. Il valore rappresenta anche il numero di funzioni esportate dal modulo.
NumberOfNames
Numero di elementi nell'array AddressOfNames; il valore è in pratica identico al campo NumberOfFunctions
*AddressOfFunctions
E' un RVA e punta ad un array di indirizzi delle funzioni. I valori di quell'array sono le RVA delle funzioni
*AddressOfNames
E' un RVA che punta ad un array di stringhe. Le stringhe sono i nomi delle funzioni esportate dal modulo.
*AddressOfNameOrdinals
E' un RVA, e punta a delle WORD che rappresenta gli ordinals delle funzioni esportate
E' più semplice di quanto possa sembrare dalla descrizione in realtà: il campo Name è appunto il nome di questa DLL, gli ultimi 3 array contengono rispettivamente: un puntatore all'indirizzo della funzione; un puntatore al nome; ed un puntatore all'ordinal.
Normalmente un EXE non avrà questa directory.
Esempio: IMAGE_EXPORT_DIRECTORY
C:
#include<stdio.h>
#include<windows.h>
DWORD RVAToOffset(IMAGE_NT_HEADERS*, DWORD);
int main(int argc, char *argv[]) {
HANDLE hFile;
HANDLE hFileMapped;
LPVOID lpFileBase;
char sectionName[9] = {0};
IMAGE_DOS_HEADER *pDosHeader;
IMAGE_NT_HEADERS *pImageNtHeader;
IMAGE_FILE_HEADER *pImageFileHeader;
IMAGE_OPTIONAL_HEADER *pImageOptionalHeader;
IMAGE_SECTION_HEADER *pImageSectionHeader;
IMAGE_IMPORT_DESCRIPTOR *pImageImportDescriptor;
IMAGE_IMPORT_BY_NAME *pImageImportByName;
IMAGE_EXPORT_DIRECTORY *pImageExportDirectory;
if(argc == 1) {
printf("Utilizzo: exedump <file.exe>");
return 1;
}
// Apro il file, ottenendo l'HANDLE
hFile = CreateFile(argv[1], GENERIC_READ ,FILE_SHARE_READ,0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
// Handle non valido (si e' verificato un errore)
if(hFile == INVALID_HANDLE_VALUE) {
printf("Errore nell'apertura del file\n%s", argv[1]);
return 1;
}
// Creo una mappa del file in memoria
hFileMapped = CreateFileMapping(hFile, 0, PAGE_READONLY, 0,0,NULL);
// Error Checking
// --------------------------------
if(hFileMapped == 0) {
printf("Errore mapping file");
return 1;
}
// --------------------------------
// Viene restituito un puntatore all'inizio, alla base
lpFileBase = MapViewOfFile(hFileMapped,FILE_MAP_READ,0,0,0);
// Error Checking
// --------------------------------
if(lpFileBase == 0) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Errore MapViewOfFile");
return 1;
}
// --------------------------------
// Casto per ottenere un puntatore alla struttura
pDosHeader = (IMAGE_DOS_HEADER *) lpFileBase;
// A questo punto verifico la firma
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("Firma Non Valida!");
return 1;
}
printf("Firma Valida!\n\tPuntatore alla prossima struttura (IMAGE_NT_HEADER): 0x%08X\n\n", pDosHeader->e_lfanew);
// Punto alla nuova struttura dati, IMAGE_NT_HEADER
pImageNtHeader = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew + (DWORD) pDosHeader);
if(pImageNtHeader->Signature != IMAGE_NT_SIGNATURE) {
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
printf("NT Header non valido!");
return 1;
}
printf("Signature valida!\n");
// Punto alla IMAGE_FILE_HEADER, e mostro alcuni dati
printf("\n----------------IMAGE_FILE_HEADER---------------------\n\n");
// Punto, come dicevamo sopra (nell'articolo), alla struttura IMAGE_FILE_HEADER
// --------------------------------------------------------------------------------------------------
pImageFileHeader = (IMAGE_FILE_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4);
printf("Machine: 0x%08X ", pImageFileHeader->Machine);
switch(pImageFileHeader->Machine) {
case IMAGE_FILE_MACHINE_I386:
printf("x86\n");
break;
case IMAGE_FILE_MACHINE_IA64:
printf("Intel Itanium\n");
break;
case IMAGE_FILE_MACHINE_AMD64:
printf("x64\n");
}
printf("Numero di sezioni: 0x%08X\n", pImageFileHeader->NumberOfSections);
printf("Size Of Optional Header: 0x%08X\n", pImageFileHeader->SizeOfOptionalHeader);
/*
* Characteristics è una DWORD, i valori (le costanti) sono molti, e vengono aggiunti
* utilizzando l'OR. In questo modo utilizzando un AND si puo' sapere se qualcosa e' settato
*/
printf("\nCharacteristics:\n");
if(pImageFileHeader->Characteristics & IMAGE_FILE_DLL) {
printf("\tIMAGE_FILE_DLL\n");
}
if(pImageFileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
printf("\tIMAGE_FILE_EXECUTABLE_IMAGE\n");
}
// Inizia la lettura dell'IMAGE_OPTIONAL_HEADER, con alcune informazioni
// ---------------------------------------------------------------------------------------------------
printf("\n\n------------------IMAGE_OPTIONAL_HEADER------------------\n\n");
// Punto all'optional header
pImageOptionalHeader = (IMAGE_OPTIONAL_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER));
// Stampo alcune informazioni
printf("Magic 0x%08X ",pImageOptionalHeader->Magic);
switch(pImageOptionalHeader->Magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
printf("Eseguibile, applicazione 32bit");
break;
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
printf("Eseguibile, applicazione a 64bit");
break;
case IMAGE_ROM_OPTIONAL_HDR_MAGIC:
printf("Immagine ROM");
break;
}
printf("\nDimensione code Section (.code): 0x%08X (%d)\n", pImageOptionalHeader->SizeOfCode, pImageOptionalHeader->SizeOfCode);
printf("\nDimensione dati inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfInitializedData, pImageOptionalHeader->SizeOfInitializedData);
printf("Dimensione dati non inizializzati: 0x%08X (%d)\n", pImageOptionalHeader->SizeOfUninitializedData, pImageOptionalHeader->SizeOfUninitializedData);
printf("\nInizio sezione codice: 0x%08X\n", pImageOptionalHeader->BaseOfCode);
printf("Inizio sezione data: 0x%08X\n", pImageOptionalHeader->BaseOfData);
printf("ImageBase: 0x%08X\n", pImageOptionalHeader->ImageBase);
printf("\nEntry Point (RVA): 0x%08X\n", pImageOptionalHeader->AddressOfEntryPoint);
// --------------------------------------------------------------------------------------------------------
printf("\n-----------------------IMAGE_SECTION_HEADER-------------------\n\n");
// Section Table; punto alla IMAGE_SECTION_HEADER
pImageSectionHeader = (IMAGE_SECTION_HEADER *) ((DWORD) pDosHeader + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER));
// Leggo il numero delle Sezioni nell'IMAGE_FILE_HEADER
int i = -1;
while(++i < pImageFileHeader->NumberOfSections) {
memcpy(sectionName, pImageSectionHeader[i].Name, IMAGE_SIZEOF_SHORT_NAME);
printf("Nome: %s\n", sectionName);
printf("Virtual Size: 0x%08X\n", pImageSectionHeader[i].Misc.VirtualSize);
printf("Virtual Address: 0x%08X\n", pImageSectionHeader[i].VirtualAddress);
printf("Size Of Raw Data: 0x%08X\n", pImageSectionHeader[i].SizeOfRawData);
printf("Pointer To Raw Data: 0x%08X\n", pImageSectionHeader[i].PointerToRawData);
printf("Characteristics: 0x%08X\n\n", pImageSectionHeader[i].Characteristics);
}
// Leggiamo la Import Table...
// ----------------------------------------------------------------------------------------
printf("\n\n---------------------------IMAGE_IMPORT_DESCRIPTOR------------------\n");
DWORD dwOffset = RVAToOffset(pImageNtHeader, pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
if(dwOffset != 0) {
// Puntiamo alla Image Import Directory
pImageImportDescriptor = (IMAGE_IMPORT_DESCRIPTOR*) (dwOffset + (DWORD) pDosHeader);
if(pImageImportDescriptor->FirstThunk == 0) {
printf("First Thunk 0");
return 1;
}
printf("Import Table Individuata!\n\n");
register int index = -1;
while(pImageImportDescriptor[++index].FirstThunk != 0) {
char *Name = (char*) RVAToOffset(pImageNtHeader, pImageImportDescriptor[index].Name) + (DWORD) pDosHeader;
printf("Modulo (DLL): %s 0x%08X\n", Name, pImageImportDescriptor[index]);
printf("Funzioni:\n");
// Come dicevamo nell'articolo, si dovrebbe considerare OriginalFirstThunk
// ma questo potrebbe essere a 0. Questo significa che ora dovremo
// controllare quale dei due considerare
DWORD *Thunk = (DWORD*) (RVAToOffset(pImageNtHeader, (pImageImportDescriptor[index].OriginalFirstThunk == 0) ? pImageImportDescriptor[index].FirstThunk : pImageImportDescriptor[index].OriginalFirstThunk) + (DWORD) pDosHeader);
DWORD *Thunk1 = (DWORD*) (RVAToOffset(pImageNtHeader, (pImageImportDescriptor[index].FirstThunk))+(DWORD)pDosHeader);
register int index1 = -1;
// Iniziamo a scorrere i nomi delle funzioni
while(Thunk[++index1] != 0) {
// Dobbiamo verificare se e' importata per ordinal o per nome
// verificando se l'MSB è settato
if(Thunk[index1] & IMAGE_ORDINAL_FLAG) {
printf("\tOrdinal: 0x%08X", (Thunk[index1]-IMAGE_ORDINAL_FLAG));
} else {
pImageImportByName = (IMAGE_IMPORT_BY_NAME*) (RVAToOffset(pImageNtHeader, Thunk[index1]) + (DWORD)pDosHeader);
printf("\tNome: %s\n", pImageImportByName->Name);
printf("\tIndirizzo RVA (della funzione): 0x%08X\n", Thunk[index1]);
printf("\tIndirizzo VA (jump nell'exe): 0x%08X\n", (pImageImportDescriptor[index].FirstThunk+index1*4)+pImageOptionalHeader->ImageBase);
}
}
printf("\n");
}
// ------------------------------------------------------------------------------------------
}
else {
printf("Offset Non trovato!");
}
// Export directory
// --------------------------------------------------------------------------------------------
printf("\n\n---------------------------IMAGE_EXPORT_DESCRIPTOR------------------\n");
dwOffset = RVAToOffset(pImageNtHeader, pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// Verifico che la Export Table esista
if(dwOffset != 0) {
printf("Export Table individuata!\n");
// Punto alla struttura
pImageExportDirectory = (IMAGE_EXPORT_DIRECTORY*) (dwOffset + (DWORD) pDosHeader);
// Questo e' il nome della DLL
char *Name = (char*) (RVAToOffset(pImageNtHeader, pImageExportDirectory->Name) + (DWORD)pDosHeader);
// I relativi dati, compresi gli array RVA alle funzioni
printf("Nome: %s\n", Name);
printf("Base: 0x%08X\n", pImageExportDirectory->Base);
printf("NumberOfFunctions: 0x%08X\n",pImageExportDirectory->NumberOfFunctions);
printf("NumberOfNames: 0x%08X\n", pImageExportDirectory->NumberOfNames);
printf("AddressOfFunctions: 0x%08X\n", pImageExportDirectory->AddressOfFunctions);
printf("AddressOfNames: 0x%08X\n", pImageExportDirectory->AddressOfNames);
printf("AddressOfNameOrdinals: 0x%08X\n", pImageExportDirectory->AddressOfNameOrdinals);
// Essendo RVA devo convertirli per ottenere i VA
DWORD *Functions = (DWORD*) (RVAToOffset(pImageNtHeader, pImageExportDirectory->AddressOfFunctions) + (DWORD)pDosHeader);
DWORD *Names = (DWORD*) (RVAToOffset(pImageNtHeader, pImageExportDirectory->AddressOfNames) + (DWORD)pDosHeader);
WORD *NameOrdinals = (WORD*) (RVAToOffset(pImageNtHeader, pImageExportDirectory->AddressOfNameOrdinals) + (DWORD)pDosHeader);
int i = -1;
// Scorro attraverso tutte le funzioni esportate
while((++i) < pImageExportDirectory->NumberOfFunctions) {
if(Functions[i] != 0) {
printf("\nOrdinal: 0x%04X Entry Point: 0x%08X\n", (i+pImageExportDirectory->Base), Functions[i]);
int j = -1;
// Verifico se esiste un nome
while((++j) < pImageExportDirectory->NumberOfNames) {
if(NameOrdinals[j] == i) {
Name = (char*) (RVAToOffset(pImageNtHeader, Names[j]) + (DWORD)pDosHeader);
printf("Nome: %s\n", Name);
break;
}
}
}
}
}
else {
printf("Offset Non Trovato!");
}
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapped);
CloseHandle(hFile);
return 0;
}
// Effettua la conversione da RVA a VA
DWORD RVAToOffset(IMAGE_NT_HEADERS *pImageNtHeader, DWORD dwRva) {
DWORD dwOffset = dwRva;
IMAGE_SECTION_HEADER *pImageSectionHeader = IMAGE_FIRST_SECTION(pImageNtHeader);
WORD wNumberOfSections = pImageNtHeader->FileHeader.NumberOfSections;
if(dwOffset < pImageSectionHeader->PointerToRawData) {
return dwOffset;
}
int index = 0;
while(wNumberOfSections > 0) {
if(dwOffset >= pImageSectionHeader[index].VirtualAddress) {
DWORD dwTemp = (pImageSectionHeader[index].VirtualAddress + pImageSectionHeader[index].SizeOfRawData);
// Dato che il valore e' stato superato,
// l'offset e' in questa sezione
if(dwOffset < dwTemp) {
dwTemp = pImageSectionHeader[index].VirtualAddress;
dwOffset -= dwTemp;
dwTemp = pImageSectionHeader[index].PointerToRawData;
dwTemp += dwOffset;
return dwTemp;
}
}
wNumberOfSections--;
index++;
}
return 0;
}
IMAGE_RESOURCE_DIRECTORY
Un file eseguibile ha con sè numerose informazioni, e tra queste vi sono anche barre, immagini e menu. Questi elementi sono presenti nella directory chiamata IMAGE_RESOURCE_DIRECTORY. All'interno di un eseguibile la troviamo con il nome .rsrc.
C:
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
} IMAGE_RESOURCE_DIRECTORY,*PIMAGE_RESOURCE_DIRECTORY;
_ANONYMOUS_STRUCT typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
_ANONYMOUS_UNION union {
_ANONYMOUS_STRUCT struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
}DUMMYSTRUCTNAME;
DWORD Name;
WORD Id;
} DUMMYUNIONNAME;
_ANONYMOUS_UNION union {
DWORD OffsetToData;
_ANONYMOUS_STRUCT struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;
Characteristics
In pratica lo si trova sempre a 0.
TimeDateStamp
Data ed ora della creazione delle risorse
MajorVersion e MinorVersion
Come nel precedente caso, sono settati a 0
NambersOfNamedEntries
Numero di elementi dell'array che seguono questa struttura
NumberOfIdEntries
Analogo a sopra, ma indica il numero di elementi che usano ID
DirectoryEntries[]
Si tratta della struttura Anonima, che è un array (non è un membro di questa struttura in realtà). Ha tanti elementi quanti indicati dalla somma di NumberOfNamedEntries e NumberOfIdEntries.
Prima della spiegazione riporto anche i campi dell'altra struttura, l'array anonimo:
Name
Il campo può contenere un integer ID o un puntatore ad una stringa. Per sapere cosa contiene viene utilizzato il bit più significativo; se è settato a 1 gli altri 31bit sono un offset ad una struttura, IMAGE_RESOURCE_DIR_STRING_U. Questa struttura contiene un contatore di caratteri, ed il nome della risorsa. Se il bit è settato a 0 il campo viene interpretato come un integer
OffsetToData
Questo campo può essere un offset ad un'altra directory oppure un puntatore alle informazioni su una specifica risorsa. Se l'MSB è settato, questo elemento si riferisce ad una sotto-directory. Gli altri 31bit sono quindi un offset relativo all'inizio delle risorse (la root). Se il bit MSB non è settato, i 31bit più bassi puntano ad una struttura IMAGE_RESOURCE_DATA_ENTRY.
La struttura è organizzata ad albero, come le directory di un file system. La prima directory è la root, il secondo livello specifica il nome della risorsa, come ad esempio un MENU, e la terza è l'elemento che appartiene a quella risorsa, come il nome del menu (dell'elemento del menu) o un'immagine.
La struttura di IMAGE_RESOUCE_DIR_STRING_U è la seguente:
C:
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length;
WCHAR NameString[1];
} IMAGE_RESOURCE_DIR_STRING_U,*PIMAGE_RESOURCE_DIR_STRING_U;
Il tutto funziona in questo modo: si converte come sempre l'RVA in VA ottenendo un puntatore alla IMAGE_RESOURCE_DIRECTORY; successivamente si ottiene il numero totale degli elementi che compongono l'array dell'altra struttura. Il numero di elementi dell'array è dato dalla somma dei due campi Number.
A questo punto si ottiene un puntatore alla struttura IMAGE_RESOURCE_DIRECTORY_ENTRY. In che modo? Semplicemente sommando il puntatore alla IMAGE_RESOURCE_DIRECTORY alla dimensione della stessa, ottenuta con sizeof(IMAGE_RESOURCE_DIRECTORY).
A questo punto si verifica se la directory ha un nome è se è un ID. La verifica avviene confrontando il bit di livello più alto del campo Name di questa struct.
C'è sempre una costante anche in questo caso che semplifica un pò il lavoro:
C:
if(pImageResourceDirectoryEntry.Name & IMAGE_RESOURCE_NAME_IS_STRING) {}
se da esito positivo, la directory ha nome. Gli altri 31bit specificano il nome stesso; in caso contrario si tratterà di un ID. Se è un nome, dobbiamo ottenere un puntatore alla struttura IMAGE_RESOURCE_DIR_STRING_U e prendere il valore del campo NameString. Questo valore è un WCHAR, e dovrà essere convertito per poter essere stampato.
PE Injector
Il PE Header può essere alterato in molti modi. Uno di essi è ad esempio manipolare la tabella delle importazioni, oppure iniettare del codice all'interno del code cave (al termine del codice 'utile' del programma spesso si ha un'area vuota, con dei byte a 0: questa parte viene chiamata 'code cave') oppure creando prima una sezione ad-hoc. E' possibile alterarlo in svariati modi, anche una volta caricato in memoria. La "tecnica" mostrata qui inietterà del codice all'interno di una sezione creata ad-hoc.
Prima di continuare, è d'obbligo una precisazione. Per fortuna (o sfortuna, dipende da quale parte ci si trova) Windows ha fatto molti passi avanti da Windows XP a Windows 7 (sino poi alle ultime versione come 8 e 10). Da Windows Vista sono subentrate alcune protezioni che rendono l'injection di un EXE molto complesso (inalcuni casi). Inoltre, a complicare l'injection vi sono packer ed offuscatori. Una delle protezioni di Windows che potrebbe creare problemi è ASLR; un'altra complicazione è la tabella dei certificati. Una versione successiva del software verrà sviluppata per identificare in automatico la sezione (.CERTIFICATE) e per aggirarla.
Su quali eseguibili non funzionerà questo software?
Non funzionerà, in generale, su tutti queli eseguibili compilati con ASLR, come ad esempio Notepad.exe, calc.exe etc. In generale tutto il software di Microsoft sul vostro PC con buone probabilità è stato generato con quel tipo di protezione.
Non funzionerà su eseguibili compressi. Se volete provarlo su un exe packed, vi consiglio prima di rimuovere la protezione.
Non funzionerà su eseguibili con la sezione .CERTIFICATE.
Su quali funzionerà?
Difficile da dire con precisione, ma la cosa certa è che non potete provarlo nei casi sopra citati... ed ovviamente nemmeno su codice a 64bit, che è comunque meno di quanto ci si può aspettare.
Tutti utilizzano ASLR e la sezione .CERTIFICATE?
No, sono in realtà sorprendentemente pochi quelli che lo utilizzano. Firefox ad esempio ha la sezione .CERTIFICATE al suo interno, ma software di una certa importanza distribuiti, da società conosciute (evito di fare nomi...) non utilizzano ASLR e non è detto abbiano sempre quella sezione.
Vi sono prodotti commerciali privi di sezioni .CERTIFICATE. Personalmente non ho mai trovato un prodotto commerciale che utilizzasse ASLR.
L'ostacolo più grande ad oggi credo lo si debba alla sezione .CERTIFICATE, ed in caso di prodotti commerciali probabilmente anche ai packer.
Il software al momento è in una versione base, e permette solo un'injection di esempio di una MessageBoxA. In futuro verrà aggiornato cercando di rendere le injection più semplici ed offrire altro... ma non è questa la sezione corretta in cui parlarne. Si tratta in questo momento di un esempio, ed il più semplice e "visibile" è proprio questo: la MessageBoxA.
Analisi di InjMyPE
Iniziamo da uno screenshot:
Inserito il nome di un eseguibile, come ad esempio hwinfo.exe, il programma effettuerà le seguenti operazioni:
0) Apertura del file exe;
1) Verifica della validità del DOS Header (ricordate 'MZ'?);
2) Verifica della validità dell'NT Header ('PE');
3) Creazione di una nuova sezione chiamata .injcode;
4) Chiusura e riapertura del file, così da vederne le modifiche;
5) verifica del punto 1) e 2);
6) ricerca sezione .DATA;
7) ricerca sezione .injcode;
8) Injection di due stringhe nella sezione .DATA (e salvataggio degli offset in variabili);
9) Injection del codice nella sezione .injcode;
10) Sostituzione dell'AddressOfEntryPoint originale con il nuovo del codice iniettato;
11) Salvataggio del file.
Come funziona
Ho scelto di evitare una spiegazione dettagliata del software, ma di spiegare solo come avviene l'iniezione.
L'aggiunta di una sezione avviene incrementando il numero di sezioni dell'exe all'interno di IMAGE_FILE_HEADER (NumberOfSections). Fatto questo il software assume che ci sia abbastanza spazio tra gli header e le sezioni. Si punta quindi ad una SECTION che viene dopo all'ultima presente sull'exe, e si inseriscono li i propri dati (prima vengono azzerati i byte, per sicurezza). Avvenuta la copia, si procede con il salvataggio.
Un passaggio molto importante è settare tra le Characteristics l'attributo dell'esecuzione (più eventualmente altri).
Fatto ciò, avviene la ricerca del blocco dati. Una volta individuata, ci si sposta al termine della sezione e si inizia a contare a ritroso.
Ci si ferma quando non vi sono più byte a 0. In questo modo si evita di sovrascrivere qualcosa di utile. Gli offset vengono poi salvati e utilizzati più avanti.
A questo punto vengono salvate le informazioni relative all'inizio della sezione DATA, così come della sezione injcode, aggiunta in precedenza, e di altri campi come l'AddressOfEntryPoint.
Prima di proseguire con l'injection del codice, è necessario avere altre informazioni: ad esempio, dove troviamo l'indirizzo della funzione?
Noi assumiamo che la DLL sia presente nella IT e che la funzione venga importata. E' già una semplificazione, ma tuttavia non ci fornisce nessun indirizzo. E' bene sapere infatti che in fase di compilazione il compilatore non inserisce nell'exe chiamate dirette alla DLL, ma verso la "jump table" (il tutto è descritto nella sezione relativo alla IT nell'articolo). Queste JMP saltano poi all'effettivo indirizzo in memoria della DLL.
Il compito di InjMyPE è quindi quello di trovare queste JMP, o almeno di capire a quale indirizzo possiamo saltare per raggiungere la DLL. Questo indirizzo viene calcolato dal FirstThunk.
Codice:
Inserisci il nome del file exe (completo di estensione): hwinfo.exe
File letto con successo!
MZ - DOS Header Valido!
Signature - NT Header Valido!
Aggiunta della sezione .injcode. Size: 1000byte
File letto con successo!
MZ - DOS Header Valido!
Signature - NT Header Valido!
VA all'indirizzo del testo: 0x00403180
VA all'indirizzo del titolo: 0x004031A3
VA all'indirizzo della funzione: 0x00402028
L'indirizzo a cui si trova la MessageBoxA all'interno dell'exe è 0x00402028, e questo non cambia mai!
Iniettare del codice in .injcode è più semplice dato che la sezione ha già dal primo byte tutto a 0 (è vuota insomma). Per rendere possibile l'injection possibile è stato necessario creare un array di byte. Alcuni sono inutili (utilizzati anche per test in realtà) e prendono il nome di NOP (No Operation, 90h), gli altri sono invece le PUSH (68h) e gli OPcode delle Call. Per sbadataggine ho utilizzato una call al posto di una JMP al termine... ma funziona comunque (dovrò fixarlo in futuro).
L'array è mostrato di seguito (ogni riga è un'istruzione):
Codice:
90h,
90h,
90h,
68h,00h,00h,00h,00h,
68h,00h,00h,00h,00h,
68h,00h,00h,00h,00h,
68h,00h,00h,00h,00h,
0FFh, 15h, 00h,00h,00h,00h,
0E8h,00h, 00h, 00h, 00h,
90h
I 90h sono i NOP, ed in pratica consumano solo un ciclo della CPU.
68h indica il PUSH, e ciò che lo segue è un valore. Questo valore può essere uno 0 oppure un indirizzo. Prima della Injection del codice le due PUSH centrali avranno come valori gli indirizzi delle due stringhe iniettate in memoria (e che potete vedere nel frammento di codice sopra). L'altro opcode indica una CALL: la particolarità della call è che come indirizzo riceve direttamente la destinazione; nel caso della hwinfo è 0x00402028. L'ultimo Opcode utile è una CALL: al posto degli 00h verrà inserito l'indirizzo al vecchio AddressOfEntryPoint (ecco perchè in precedenza viene salvato). Questo indirizzo viene calcolato sottraendo all'AddressOfEntryPoint originale l'indirizzo dell'istruzione attuale e sottraendo ancora 5 (i byte dell'istruzione).
A questo punto tutto è pronto per essere scritto sull'exe e poi salvato.
Non posso mostrarvi esempi su software commerciali per ovvi motivi; quindi utilizzo come 'cavia' l'exe di un CrackMe:
Tornando ad hwinfo.exe, questo è il prima e il dopo:
Noterete che ora l'EP non è più quello mostrato nell'articolo. Prestando attenzione vedrete una CALL verso 0x00401000, che è l'EP originale.
Il download del binario ed il sorgente sono reperibili a questo indirizzo.
Conclusione
L'articolo ora è giunto veramente al termine. Spero sia stato interessante e tutto comprensibile. In caso di domande o per qualsiasi altra cosa, lasciate un commento qui sotto.
Link utili
- PE Format (MSDN) ormai aggiornatissima rispetto al passato e con, quasi certamente, più informazioni di quelle apprese nel mio articolo.
Ultima modifica da un moderatore: