Eseguibile portatile

Eseguibile portatile Immagine in Infobox. Visualizzazione dell'inizio di un file PE con WinHex Caratteristiche
Estensione .cpl , exe , dll , ocx , sys , .scr , .drv , .efi
Tipo MIME application/vnd.microsoft.portable-executable, application/efi
PUID x-fmt / 411
Firme 4D 5A( hexa )
4D5A*50450000( PRONOM regex )
Tipo di formato Binario , eseguibile , codice oggetto , DLL
Basato su Eseguibile MZ
COFF

Il formato PE ( eseguibile portatile , laptop eseguibile ) è il formato di file eseguibili e librerie sui sistemi operativi Windows a 32 bit e 64 bit .exe (programmi), .ocx ( OLE e ActiveX ), .dll e. Cpl ( Windows Elemento del pannello di controllo ).

È anche il formato utilizzato per gli eseguibili UEFI (.efi).

È un formato derivato da COFF .

Storico

Microsoft ha adottato il formato PE con l'introduzione di Windows NT 3.1 . Tutte le versioni successive di Windows, incluso Windows 95/98 / ME, supportano questo formato. In precedenza, i file "eseguibili" erano in NE - New Executable File Format , "  New  " si riferiva a CP / M e * .com - file .

La creazione del formato PE è stata indotta dal desiderio di Microsoft di progettare una struttura di file che potesse adattarsi a diverse macchine che utilizzano Windows NT, che inizialmente era in grado di supportare architetture diverse da Intel x86 ( Power PC e Motorola 68000 per esempio). L'idea era quindi quella di creare una struttura come queste architetture .

Diagramma formato PE

Un file eseguibile PE è strutturato come segue:

Struttura del formato PE

I primi due byte del file rappresentano i caratteri MZ .

Intestazione MZ in MS-DOS

L'intestazione MZ consente al sistema operativo di riconoscere il file come eseguibile valido se viene avviato da MS-DOS , in modo che possa eseguire il suo segmento DOS. Ecco la struttura dell'intestazione, in linguaggio C  :

typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Riconosciamo ad esempio:

  • e_magic che deve essere "MZ"
  • e_lfanew che contiene l'indirizzo di inizio dell'intestazione PE

Segmento DOS

Il segmento DOS viene eseguito quando Windows non riconosce il file come in formato PE o se viene eseguito in MS-DOS . Di solito visualizza un messaggio del tipo Questo programma non può essere eseguito in modalità DOS , tradotto letteralmente, Questo programma non può essere eseguito in modalità DOS .

Intestazione PE

L'intestazione PE è un insieme di strutture , raggruppate in un'unica struttura denominata IMAGE_NT_HEADER , qui è il suo prototipo in C lingua .

typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
  • Firma che consente di identificare il file, che deve essere uguale a 0x00004550 , oppure "PE \ 0 \ 0" (\ 0 è un byte nullo).
  • FileHeader è una struttura denominata IMAGE_FILE_HEADER contenente informazioni sulla struttura del file e prototipata come segue:
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;

Visualizza (en) http://msdn.microsoft.com/en-us/library/windows/desktop/ms680313%28v=vs.85%29.aspx

  • OptionalHeader è una struttura denominata IMAGE_OPTIONAL_HEADER

Visualizza (en) http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339%28v=vs.85%29.aspx per i dettagli

Indirizzi fisici, virtuali e di memoria

La prima cosa da sapere è che l'eseguibile viene caricato in memoria all'indirizzo ImageBase (presente in OptionalHeader ) se questo indirizzo è disponibile. Altrimenti viene caricato in un altro indirizzo che sarà il nuovo valore di ImageBase .

Nell'intestazione e nel corpo di un file PE, ci sono tre diverse mappature:

  • Gli indirizzi fisici rappresentano una posizione nel file. Nell'intestazione, il loro nome inizia con PointerTo
  • Gli indirizzi virtuali indicano una posizione di memoria relativa a ImageBase . Nell'intestazione, il loro nome inizia con Virtual
  • Anche gli indirizzi di memoria sono una posizione nella memoria ma questa volta assoluta, questi indirizzi sono più spesso presenti nel codice dell'applicazione e nei dati ma non nelle intestazioni. Se ImageBase è stato modificato, la tabella di rilocazione indica la posizione degli indirizzi di memoria da correggere.
Directory

Le directory sono parti del file che vengono utilizzate durante il caricamento. La posizione e la dimensione dei dati di queste directory sono mostrate nel campo DataDirectory di OptionalHeader che è un array di strutture IMAGE_DATA_DIRECTORY descritte come segue:

typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

VirtualAddress è l'indirizzo di inizio del contenuto della directory una volta che le sezioni sono state caricate in memoria, relativo a ImageBase (presente in OptionalHeader ). E dimensiona le sue dimensioni.

Sebbene il campo NumberOfRvaAndSizes di OptionalHeader indichi il loro numero, di solito ce ne sono 16:

Posizione Nome Descrizione
0 IMAGE_DIRECTORY_ENTRY_EXPORT Tabella di esportazione
1 IMAGE_DIRECTORY_ENTRY_IMPORT Importa tabella
2 IMAGE_DIRECTORY_ENTRY_RESOURCE Tabella delle risorse
3 IMAGE_DIRECTORY_ENTRY_EXCEPTION Tabella delle eccezioni
4 IMAGE_DIRECTORY_ENTRY_SECURITY Tabella dei certificati
5 IMAGE_DIRECTORY_ENTRY_BASERELOC Tavolo di trasferimento
6 IMAGE_DIRECTORY_ENTRY_DEBUG Informazioni di debug
7 IMAGE_DIRECTORY_ENTRY_COPYRIGHT / IMAGE_DIRECTORY_ENTRY_ARCHITECTURE Dati specifici per copiare i diritti o l'architettura
8 IMAGE_DIRECTORY_ENTRY_GLOBALPTR puntatori globali
9 IMAGE_DIRECTORY_ENTRY_TLS Tabella di archiviazione locale dei thread (TLS)
10 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG Carica la tabella di configurazione
11 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT Tabella delle importazioni correlate
12 IMAGE_DIRECTORY_ENTRY_IAT Tabella degli indirizzi di importazione
13 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT Descrittore di importazione differito
14 IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR Intestazione runtime COM +
15 - riservato: deve essere vuoto.

Tabella delle sezioni

La tabella delle sezioni si trova subito dietro l'intestazione PE. Questo è un array contenente più strutture IMAGE_SECTION_HEADER .

Queste strutture contengono informazioni sulle sezioni del file binario da caricare in memoria.

Il NumberOfSections campo della IMAGE_FILE_HEADER struttura indica quante voci ci sono in questa tabella. Il massimo supportato da Windows è di 96 sezioni.

La tabella delle sezioni è prototipata come segue:

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;

Ogni sezione ha un nome di 8 caratteri, questo nome non è significativo ma di solito possiamo trovare quanto segue:

Nome Descrizione
.testo Il codice (istruzioni) del programma
.bss Variabili non inizializzate
.reloc La tabella di rilocazione (la sesta directory)
.dati Le variabili inizializzate
.rsrc Le risorse del file (la terza directory: cursori, suoni, menu, ecc.)
.rdata Dati di sola lettura
.idata La tabella di importazione (la seconda directory)
.upx Segno di compressione UPX, specifico per il software UPX
.aspack Segno di un pacchetto ASPACK, specifico per il software ASPACK
.adata Segno di un pacchetto ASPACK, specifico per il software ASPACK

Tavolo di trasferimento

La tabella di rilocazione è presente nella sesta directory "Tabella di rilocazione di base". Indica gli indirizzi virtuali dei valori che rappresentano un indirizzo di memoria. Consente, nel caso in cui l'eseguibile non sia stato caricato in memoria all'indirizzo ImageBase , di sostituire tutti i riferimenti di memoria per corrispondere al nuovo valore di ImageBase .

Il tavolo di trasferimento è una serie di strutture di dimensioni variabili. Ciascuno è costituito da un'intestazione di tipo IMAGE_BASE_RELOCATION quindi da un array di valori di tipo WORD (16 bit di dimensione). A volte l'ultimo valore è 0, nel qual caso viene utilizzato solo per l'allineamento a 4 byte.

typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION,*PIMAGE_BASE_RELOCATION;
  • SizeOfBlock è la dimensione in byte della struttura, inclusa l'intestazione.
  • VirtualAddress è l'indirizzo virtuale di base (relativo a ImageBase ) per i valori

Non conosciamo il numero di strutture presenti nel tavolo di ricollocazione, ma conosciamo le sue dimensioni e quindi ci fermiamo quando siamo arrivati ​​alla fine. Allo stesso modo, non sappiamo quanti valori seguono ogni intestazione, ma lo deduciamo dalla dimensione.

Ogni valore è composto da 4 bit di informazione e 12 bit di dati. La parte informativa può essere uno di questi valori:

Valore Descrizione
IMAGE_REL_BASED_ABSOLUTE
IMAGE_REL_BASED_HIGH
IMAGE_REL_BASED_LOW
IMAGE_REL_BASED_HIGHLOW Il dato è un indirizzo relativo alla base (virtual address = VirtualAddress + data)
IMAGE_REL_BASED_HIGHADJ
IMAGE_REL_BASED_MIPS_JMPADDR

Molto spesso viene utilizzato solo IMAGE_REL_BASED_HIGHLOW .

Il valore dell'indirizzo di memoria così calcolato verrà spostato in base alla differenza tra l' ImageBase originale e l'indirizzo di inizio della memoria allocata al programma.

Tabella di importazione - IAT

Lo IAT, acronimo di Import Address Table , contenuto nella seconda directory Import table (solitamente nella sezione .idata o .rdata ), indica gli indirizzi delle API importate dal software, nonché i nomi delle DLL che le importano funzioni. Le API, contenute in queste DLL , consentono al software di funzionare correttamente.

La sua esistenza è dovuta al fatto che le API vengono indirizzate in modo diverso a seconda del sistema operativo.

Innanzitutto, dovresti sapere che una struttura denominata IMAGE_IMPORT_DESCRIPTOR viene utilizzata per ogni DLL chiamata; più un ultimo di 5 DWORD azzerati che definiscono una terminazione.

Per ogni DLL importata, verrà utilizzata una struttura denominata IMAGE_THUNK_DATA per ciascuna API di quella DLL; ci saranno quindi tanti IMAGE_THUNK_DATA quante sono le funzioni esportate da una DLL, più un ultimo DWORD che specifica la terminazione di questa DLL.

Una terza struttura, IMAGE_IMPORT_BY_NAME , definisce il nome delle API e il loro numero ORDINALE (numero a 16 bit che identifica una funzione all'interno di una DLL). Ce ne sono tante quante sono le API importate da DLL. Ad esempio, nell'immagine di esempio sopra, ci sono tre IMAGE_IMPORT_BY_NAME definiti per advapi32.dll, perché solo tre delle sue API sono utilizzate dal programma.

typedef struct _IMAGE_IMPORT_DESCRIPTOR { _ANONYMOUS_UNION union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // It points to the first thunk IMAGE_THUNK_DATA } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound DWORD ForwarderChain; // -1 if no forwarders DWORD Name; // RVA of DLL Name. DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR; typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; //Ordinal Number BYTE Name[1]; //Name of function } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; typedef union _IMAGE_THUNK_DATA { PDWORD Function; PIMAGE_IMPORT_BY_NAME AddressOfData; } IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;

Tabella di esportazione - EAT

EAT = Esporta tabella indirizzi

typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; /* 0x00 */ DWORD TimeDateStamp; /* 0x04 */ WORD MajorVersion; /* 0x08 */ WORD MinorVersion; /* 0x0a */ DWORD Name; /* 0x0c */ DWORD Base; /* 0x10 */ DWORD NumberOfFunctions; /* 0x14 */ DWORD NumberOfNames; /* 0x18 */ DWORD AddressOfFunctions; // 0x1c RVA from base of image DWORD AddressOfNames; // 0x20 RVA from base of image DWORD AddressOfNameOrdinals; // 0x24 RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Il PE Loader

PE Loader è una parte di Windows che consente di riconoscere e caricare i file PE in memoria. È grazie ad esso che Windows può eseguire le istruzioni di un tale file.

  • Ecco come funziona:
  1. Il caricatore PE esamina l'intestazione MZ per trovare l'offset dell'intestazione PE. Se lo trova, ci "salta" sopra.
  2. Il PE Loader verifica la validità dell'intestazione PE. In tal caso, "salta" alla fine di questa intestazione.
  3. Il PE Loader legge le informazioni sulle sezioni e quindi mappa queste sezioni in memoria utilizzando un processo di "Mappatura file" (copia di un file in memoria).
  4. Quando tutto è stato mappato in memoria, lo stesso PE Loader si comporta come una parte logica del file PE, come una tabella di importazione.

Reverse Engineering del formato PE

Definizione

Il Reverse Engineering, a volte abbreviato in Reversing , è una tecnica di analisi del software che consente di trovare "bug", difetti, exploit o crack di progettazione o correggere problemi nel software. Si tratta di una tecnica volta a capire come funziona il software, senza dover ricorrere al suo codice sorgente.

Un altro utilizzo del Reversing è l'analisi del Malware al fine di comprenderne il comportamento e la struttura, al fine di creare un vaccino contro di esso. Questi cosiddetti vaccini possono quindi essere integrati in un antivirus per proteggere il sistema in futuro. Si noti che la maggior parte dei virus / antivirus si concentra su Windows, che è di gran lunga il sistema operativo più utilizzato / venduto dal pubblico in generale. Sebbene ci sia malware che sfrutta altre infrastrutture, come Stuxnet, che prende di mira i sistemi SCADA.

Questa pratica consiste in un'analisi approfondita del Bytecode di un programma (qualunque sia il formato), per rivelarne il funzionamento, il comportamento. Mette in evidenza un'ampia conoscenza del sistema operativo ( FreeBSD , Windows , Linux , Mac ), della programmazione ( ASM , C / C ++ ) e dell'architettura ( x86 , SPARC ) da parte di quello in esecuzione, comunemente chiamato Reverser o Cracker.

Questo articolo si occupa del formato PE (Portable Executable), quindi le informazioni qui si concentreranno su questo formato di file.

Si noti che è ovviamente possibile eseguire tutti i passaggi seguenti in formati come ELF (Executable & Linkable Format) o Mach-O (Mach Object File Format). D'altra parte, cambiando il formato, cambieranno a loro volta gli strumenti, non il principio ma i programmi stessi; si faranno sentire anche sfumature sulle protezioni anti-inversione.

Analisi statica

Un'analisi statica del software consiste nel leggere il Bytecode così come esiste sull'hard disk, cioè senza essere in esecuzione.

Una lettura del Bytecode di un programma non è né più né meno che una trasformazione della serie di byte presenti nel programma, in linguaggio ASM , un linguaggio mnemonico che consente una comprensione umana di ciò che il programma deve intraprendere.

Strumenti sono stati sviluppati appositamente per questo scopo, come Win32Dasm o Pydasm ( Python Module ).

Analisi dinamica

Un'analisi dinamica comprende due fasi: lettura del codice - esecuzione del codice.

Per intraprendere un tale approccio, è necessario disporre di uno strumento software chiamato Debugger , come OllyDbg o GDB (GNU Debugger).

Il principio è quello di smontare il programma durante l'esecuzione, detta anche analisi passo passo o passo passo , in modo da poterne modificare il comportamento in tempo reale, per comprenderne più nel dettaglio il funzionamento e la possibili modifiche.

Livelli di privilegio

La maggior parte delle scansioni viene eseguita nell'anello 3 , ovvero in modalità utente. Sopra arriviamo al Ring 0 , ovvero il Kernel , ma questo tipo di analisi approfondita è raro e riservato agli avventurieri (sviluppo di driver, hacking, ecc.).

Un altro metodo, un po 'insolito (ma funzionale) è utilizzare la virtualizzazione hardware. Damien Aumaitre ha presentato il suo VirtDbg strumento a SSTIC 2010 .

Vedi (fr) http://esec-lab.sogeti.com/dotclear/public/publications/10-sstic-virtdbg_slides.pdf

Vedere Anello di protezione

Protezione anti-inversione

Considerando l'inversione come una tecnica invadente per quanto riguarda il software closed-source (codice sorgente proprietario e non divulgato), i programmatori hanno quindi avuto l'idea di includere routine specifiche nei loro programmi, che avranno un solo obiettivo: impedire informazioni sul comportamento del software quanto più possibile.

Per fare ciò ci sono molti metodi, incluso l'uso di Packer ( crittografia / compressione del codice), codici del rilevatore di debugger, inserimento di Checksum ecc.

Packers

Un packer è un piccolo programma il cui obiettivo è comprimere il software in modo da ridurne le dimensioni iniziali, pur mantenendo l'aspetto eseguibile.

Tuttavia, nel tempo, sono comparsi nuovi imballatori; questi ora hanno la capacità di crittografare il codice del programma di destinazione, il che ha l'effetto di ridurne il peso e allo stesso tempo di modificare il suo codice di esecuzione. Un algoritmo ben noto, e uno dei più semplici da aggirare poiché la chiave è generalmente scritta nel programma, è senza dubbio XOR (Exclusive-OR).

Naturalmente, la parte relativa alla decrittazione - eseguita all'avvio del software in pacchetto / crittografato - verrà eseguita in modo trasparente per l'utente. Ma un inverso lo vedrà in modo diverso, visualizzando un codice ASM diverso da quello normale.

UPX è un esempio di un imballatore; consente di ridurre la dimensione di un eseguibile di quasi il 50%.

Visualizzazione del rapporto di compressione su notepad.exe Rilevamento del punto di interruzione

Il breakpoint detection consiste nel rilevare durante l'esecuzione di un binario la chiamata alla funzione INT 3 , ovvero 0xCC in esadecimale . Un codice simile a questo eseguirà questa operazione:

if(byte XOR 0x55 == 0x99){ // Si Byte = 0xCC -> 0xCC XOR 0x55 == 0x99 printf("Breakpoint trouvé !!"); } Falso punto di interruzione

La tecnica del falso punto di interruzione consiste nel simulare un punto di interruzione nel codice software come farebbe un debugger, utilizzando l'istruzione INT 3 , ovvero 0xCC in esadecimale . Quando un debugger incontra questa funzione, si fermerà da solo ricevendo un segnale SIGTRAP .

Per impostazione predefinita, un processo termina quando riceve un segnale SIGTRAP . Il trucco sta nel modificare questo comportamento predefinito utilizzando la funzione signal () applicata a SIGTRAP.

Ecco un esempio di codice sorgente in linguaggio C, per sistemi Linux:

#include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> void sighandler(int signal) { printf("Je suis une fonction ordinaire... "); exit(0); } int main(void) { signal(SIGTRAP,sighandler); // On place le sighandler __asm__("int3"); // On place un faux Breakpoint printf("Pris au piège..."); // Le débogueur arrive ici return EXIT_FAILURE; }


Rilevamento del debugger

In pratica, ci sono diversi modi per rilevare un debugger. Nel modo più semplice possibile, con la funzione IsDebuggerPresent (void) su Windows:

#include <stdio.h> #include <windows.h> int main(int argc, char* argv[]) { // Attention: cette fonction vaut 0 s'il n'y a pas de débogueur. if(!IsDebuggerPresent()) { printf("Helloworld !"); } else { printf("Debogueur detecte !"); } return 0; }

Poi ci sono diversi trucchi per rilevare il debugger. Per esempio:

#include <stdio.h> #include <windows.h> #include <time.h> int main(int argc, char* argv[]) { int start = clock(); Sleep(100); int DeltaTemps = abs(clock() - start - 100); printf("deltaTemps: %d\n", DeltaTemps); if(DeltaTemps < 4) { printf("Helloworld !"); } else { printf("Debogueur detecte !"); } return 0; }

Questo codice calcola una differenza di orario. Con uno Sleep di 100 ms, normalmente la differenza è 100 (entro pochi millisecondi). Quindi calcoliamo la differenza, sottraiamo 100 e otteniamo un risultato. Applichiamo la funzione abs, per ottenere sempre un valore positivo. Nella modalità passo-passo, il programma verrà notevolmente rallentato e, quindi, la differenza sarà maggiore e il programma si bloccherà.

Gli attrezzi

Vedi anche

link esterno