Cosa succede davvero quando fai doppio clic su un file EXE in Windows

Un file EXE è molto più di un programma da eseguire con un doppio clic. Vediamo come Windows lo trasforma in un processo, quali informazioni contiene il formato PE e come analizzarlo.

Fare doppio clic su un file EXE sembra una delle operazioni più semplici possibili in Windows: il programma si avvia e compare una finestra oppure, se effettuata dal terminale, viene eseguita un’operazione. Dietro questo gesto, però, ci sono una serie di attività che Windows compie in sequenza.

Il sistema operativo deve capire cosa stiamo tentando di avviare, verificare il file, creare un nuovo processo, predisporne lo spazio di memoria, interpretare il formato dell’eseguibile, caricare le DLL necessarie, risolvere gli indirizzi delle funzioni importate, stabilire con quali privilegi il programma potrà operare e infine consegnare il controllo al codice dell’applicazione.

Un file EXE, infatti, ospita una descrizione articolata di un’immagine eseguibile che Windows utilizza per costruire un nuovo processo, successivamente mantenuto in memoria. Microsoft documenta il formato impiegato con il nome di Portable Executable (PE): lo stesso modello generale sta alla base sia dei file EXE sia di molte DLL Windows.

La funzione di Windows che crea un nuovo processo

La funzione Win32 CreateProcess è uno dei punti fondamentali dell’avvio delle applicazioni Windows. Microsoft la descrive in modo molto preciso: crea un nuovo processo e il suo thread primario; il nuovo processo utilizza poi il file eseguibile indicato. Per impostazione predefinita, inoltre, il nuovo processo utilizza il contesto di sicurezza del processo che lo ha avviato.

Il processo è il contenitore che possiede, tra le altre cose:

  • uno spazio di indirizzamento virtuale;
  • handle verso oggetti del sistema;
  • informazioni di sicurezza;
  • moduli caricati;
  • variabili d’ambiente e altri dati di esecuzione.

Il thread, invece, è l’unità che effettivamente esegue istruzioni sulla CPU. Un processo appena creato deve quindi avere almeno un thread iniziale; successivamente il programma potrà crearne molti altri. Vedere un file EXE nell’elenco dei processi all’interno del Task Manager non significa che stia girando un solo flusso di esecuzione: quel processo potrebbe contenere decine o centinaia di thread.

Quando si fa clic su un documento o un file non eseguibile (si pensi a relazione.docx), Windows deve prima determinare quale applicazione gestisce il tipo di file e quindi avviare il programma passandogli il documento come argomento.

Prima di eseguire il codice Windows deve capire com’è fatto il file EXE

Un eseguibile Windows utilizza normalmente il formato PE: al suo interno si trovano intestazioni e sezioni che descrivono come il sistema deve interpretare il file. Microsoft documenta strutture che indicano, tra le altre cose, l’architettura di destinazione, le caratteristiche dell’immagine, l’indirizzo di ingresso, le sezioni e varie directory contenenti informazioni necessarie al loader. La curiosità è che anche i file EXE di oggi conservano la stessa intestazione MZ che risale ai tempi di DOS, riconducibile a Mark Zbikowski, uno degli ingegneri Microsoft che contribuì alla definizione del layout.

Un PE può avere sezioni comunemente denominate:

  • .text contiene generalmente codice eseguibile;
  • .data dati modificabili;
  • .rdata dati prevalentemente in sola lettura;
  • .rsrc risorse come icone, dialoghi e informazioni di versione;
  • .reloc può contenere informazioni necessarie per correggere determinati indirizzi qualora l’immagine non venga caricata all’indirizzo base previsto.

Tra le informazioni più importanti dell’immagine eseguibile c’è l’entry point, ossia l’indirizzo dal quale deve iniziare l’esecuzione dell’immagine dopo che il loader ha completato le operazioni preliminari. Non necessariamente è sovrapponibile con la prima funzione che il programmatore predispone nel sorgente.

Prima dell’avvio entrano in gioco i privilegi

Non tutti i file EXE partono con gli stessi diritti: un eseguibile può contenere un application manifest, ossia metadati XML che descrivono varie caratteristiche richieste dall’applicazione. Microsoft documenta tre valori particolarmente importanti:

  • asInvoker
  • highestAvailable
  • requireAdministrator

Con asInvoker, il programma utilizza il livello di autorizzazione del processo che lo ha avviato. Con requireAdministrator, invece, l’applicazione dichiara di aver bisogno di privilegi amministrativi e Windows può quindi attivare il meccanismo di elevazione UAC.

Diritti esecuzione EXE Windows

Nell’immagine, i diritti richiesti da un eseguibile, estratti con l’utilità sigcheck.exe (vedere più avanti).

SmartScreen può intervenire ancora prima

Un altro controllo che l’utente può incontrare riguarda Microsoft Defender SmartScreen. Nel caso di file scaricati da Internet, SmartScreen può valutare la reputazione dell’applicazione prima di consentirne l’esecuzione: l’azienda di Redmond spiega che il sistema utilizza diversi elementi per stilare un giudizio e può mostrare avvisi per applicazioni sconosciute, poco diffuse o considerate potenzialmente pericolose.

È importante non confondere SmartScreen con UAC: il primo cerca di rispondere, semplificando, alla domanda “è opportuno fidarsi di questo programma?” UAC interviene invece su un’altra questione: “il programma ha il diritto di utilizzare privilegi più elevati?”

Un’applicazione perfettamente legittima può quindi generare un prompt UAC perché richiede diritti amministrativi; allo stesso modo un EXE che non necessita dell’amministratore può essere fermato da SmartScreen perché scaricato da Internet e privo di una reputazione sufficiente.

Arriva il momento di caricare le librerie DLL

Ben pochi programmi Windows contengono all’interno del rispettivo file EXE tutto il codice necessario per funzionare: molte caratteristiche arrivano da DLL, Dynamic-Link Library, opportunamente caricate. Un’applicazione può dipendere, per esempio, da funzioni messe a disposizione dal sistema operativo e da librerie installate insieme al programma.

Il formato PE contiene un’Import Table, nella quale possono essere indicate le DLL e le funzioni necessarie. Uno dei compiti del loader Windows consiste proprio nel localizzare le dipendenze e rendere disponibili al programma gli indirizzi delle funzioni importate.

Se una DLL richiesta in fase di caricamento non è disponibile oppure una funzione importata non esiste nella versione della libreria disponibile sul dispositivo, il loader può non essere in grado di completare la preparazione del processo.

Quando viene indicato soltanto il nome della DLL e non un percorso completo, Windows applica precise regole di ricerca delle librerie. Microsoft documenta l’ordine e le varianti utilizzate dal loader, compreso il cosiddetto Safe DLL Search Mode, abilitato per impostazione predefinita, che modifica la posizione della directory corrente nell’ordine di ricerca per ridurre alcuni rischi di caricamento di librerie indesiderate.

Il tema è infatti importante anche dal punto di vista della sicurezza: se un’applicazione cerca semplicemente la libreria example.dll e un aggressore riesce a fare in modo che venga trovata prima una DLL malevola con quello stesso nome, può verificarsi un attacco di DLL hijacking.

Cosa succede se il file EXE è .NET?

Un programma .NET utilizza comunque il contenitore PE, ma all’interno contiene Intermediate Language (IL) e metadati .NET anziché soltanto codice macchina nativo destinato direttamente alla CPU.

Durante l’avvio deve quindi entrare in gioco anche il runtime .NET, che prepara l’ambiente gestito e può utilizzare la compilazione JIT, Just-In-Time, per produrre codice macchina necessario all’esecuzione.

L’utente continua a vedere un file eseguibile del tipo Applicazione.exe ma internamente la strada tra PE e istruzioni effettivamente eseguite può essere molto diversa rispetto a un tradizionale programma C/C++ compilato. L’estensione .exe descrive insomma il formato di avvio Windows, non certo il linguaggio con cui è stato scritto il programma.

Facciamo l’autopsia di un EXE: cosa si può scoprire senza avviarlo

Prima ancora di fare doppio clic su un file EXE, possiamo ricavarne una quantità sorprendente di informazioni. Non parliamo soltanto del nome del produttore o della versione mostrati nelle proprietà di Windows: il formato PE conserva dati sufficienti per capire per quale architettura è stato costruito il programma, quali protezioni di sicurezza supporta, quali DLL dichiara di utilizzare, se contiene un manifest, se è firmato, quanto appare compresso o impacchettato e perfino se contiene dati aggiunti che Windows non mapperà mai nel processo.

Per una prima radiografia particolarmente efficace possiamo utilizzare Sigcheck, strumento Microsoft Sysinternals. Il comando seguente consente l’estrazione delle informazioni estese, degli hash, catalogo, catena di firma e manifest:

sigcheck -a -h -i -m programma.exe

Un file costituito da dati molto ripetitivi ha un’entropia relativamente bassa (sigcheck -a); dati fortemente compressi, cifrati o apparentemente casuali tendono invece verso valori più elevati. Il manifest, invece, restituisce un’indicazione su come l’EXE vuole essere trattato da Windows (sigcheck -m).

Quanto alla firma, la presenza di una firma valida indica che Windows riesce a verificare l’associazione tra il file e il firmatario e che l’integrità prevista dalla firma risulta rispettata. Non è una certificazione della bontà del programma.

Un EXE può avere dati “in coda” che Windows non esegue

C’è un’altra verifica interessante: un PE può avere byte presenti oltre la fine dell’ultima sezione effettivamente descritta dagli header. Questa parte è spesso chiamata overlay e può contenere archivi compressi, file di installazione, configurazioni, payload, risorse proprietarie e dati aggiunti da un packer.

La presenza di un overlay non implica che il file sia automaticamente sospetto: molti installer e programmi autoestraenti usano tecniche simili.

Il punto interessante è un altro: la dimensione del file EXE può essere molto superiore alla quantità di dati che il loader Windows considera effettivamente parte delle sezioni dell’immagine. Inoltre, in linea generale, Windows può caricare in memoria RAM una porzione ridotta rispetto al volume di dati conservato nel file eseguibile.

Le stringhe interne sono una miniera, soprattutto quelle Unicode

Microsoft distribuisce anche Strings, altra utilità Sysinternals capace di cercare sequenze ASCII e Unicode all’interno di un file.

Provando a usare il comando strings programma.exe, eventualmente aumentando la lunghezza minima delle stringhe, così da ridurre il rumore, si possono estrarre informazioni testuali molti interessanti. Alcune stringhe, come un’ipotetica C:\Builds\Release\Client\Client.pdb, può far intuire la struttura utilizzata sul sistema di compilazione.

Ancora, una chiave Software\Azienda\Prodotto può suggerire dove cercare configurazioni nel Registro di sistema: è un po’ quello che dicevamo nell’approfondimento su dove rimangono i residui di un programma Windows dopo la disinstallazione. Un indirizzo HTTPS, anch’esso presente sotto forma di stringa, può suggerire una destinazione da controllare successivamente con Process Monitor o strumenti di rete.

La presenza di una stringa non prova che sia effettivamente utilizzata dal programma e quindi dal file EXE: rappresenta soltanto una pista.

Process Explorer: dal file EXE al processo che Windows ha costruito

Un’utilità come Process Explorer non serve ad analizzare direttamente il codice macchina contenuto in un EXE come farebbero Ghidra, IDA o un decompilatore .NET (ne parliamo brevemente più avanti). Il suo ruolo è diverso e, nello studio di un programma, altrettanto importante: permette di osservare che cosa diventa il file EXE dopo che Windows lo ha caricato e trasformato in un processo.

Dopo aver avviato il programma, conviene individuare il processo corrispondente e aprirne le Properties: nell’interfaccia di Process Explorer, basta un semplice doppio clic.

Già qui emergono informazioni che spesso chiariscono immediatamente come l’applicazione sia stata avviata: percorso completo dell’immagine, riga di comando, processo padre, utente, livello di integrità, architettura, directory corrente e altri dati relativi all’ambiente di esecuzione. La command line è particolarmente utile perché due processi basati sullo stesso identico EXE possono comportarsi in modo completamente diverso in funzione degli argomenti ricevuti.

La parte più interessante arriva però portandosi nel pannello orizzontale posto nella parte inferiore dell’interfaccia di Process Explorer e scegliendo la scheda DLL. Process Explorer mostra i moduli effettivamente mappati nel processo: non soltanto quelli dichiarati direttamente nella Import Table dell’EXE, ma anche DLL caricate successivamente, dipendenze introdotte da altre librerie, plugin, runtime e componenti eventualmente inseriti da software di terze parti.

Come estrarre informazioni utili sul comportamento dell’eseguibile

Per ogni modulo è possibile verificare informazioni come percorso, versione, produttore e firma digitale. È così possibile accorgersi, per esempio, che programma.exe, installato in C:\Program Files, sta caricando una DLL da AppData, da una directory temporanea o da una posizione inattesa. È un’informazione preziosa quando si cercano crash, conflitti tra versioni, plugin indesiderati o possibili casi di DLL hijacking.

Passando invece alla vista Handles si osservano gli oggetti che il processo mantiene aperti: file, directory, chiavi del Registro di sistema, eventi, sezioni di memoria condivisa, thread e oggetti di sincronizzazione. Un handle verso un database, un file di configurazione o una chiave del Registro può rivelare quali risorse il programma continua a utilizzare anche quando è apparentemente inattivo.

Handles Process Explorer

Process Explorer permette inoltre di scendere fino ai thread del processo: ordinandoli per utilizzo della CPU è possibile individuare quello responsabile di un consumo anomalo e, con simboli sufficienti, esaminarne lo stack delle chiamate. Si passa quindi dalla constatazione generica “questo EXE usa molta CPU” a una domanda molto più utile: “quale thread, dentro quale DLL e lungo quale catena di funzioni sta occupando la CPU?”

Anche le informazioni sulla memoria meritano attenzione. Working set, private bytes e memoria virtuale descrivono aspetti differenti e aiutano a distinguere le pagine realmente residenti, la memoria privata del processo e le immagini o sezioni condivise con altri programmi. Osservare la crescita progressiva dei Private Bytes, per esempio, può fornire un primo indizio di un possibile memory leak. Ne parliamo diffusamente nell’articolo dedicato a RAMMap.

Come si studia davvero un EXE: dal doppio clic alla singola decisione della CPU

Analizzare seriamente un eseguibile significa passare da una visione incentrata sul file a una visione basata sul flusso di esecuzione. L’obiettivo non è necessariamente ricostruire tutto il sorgente originale: spesso basta individuare il punto nel quale il programma prende una determinata decisione e capire quali dati la influenzano.

Supponiamo di costruire noi stessi un piccolo programma:

int valore = controlla_configurazione();

if (valore == 1)
MessageBoxW(NULL, L"Percorso A", L"Test", MB_OK);
else
MessageBoxW(NULL, L"Percorso B", L"Test", MB_OK);

Dopo la compilazione, quell’if non esiste più come costrutto di alto livello: la CPU vede soltanto istruzioni macchina. Una possibile traduzione potrebbe assomigliare a quella che segue:

call controlla_configurazione
test eax,eax
je percorso_B

; percorso A
...
jmp fine

percorso_B:
...

Comprendere come si comporta un programma compilato significa imparare a riconoscere queste strutture. Nel codice macchina non troviamo il costrutto if; su x86/x64 incontriamo invece istruzioni come:

  • JE; jump if equal
  • JNE; jump if not equal
  • JG; greater
  • JGE; greater or equal
  • JL; less
  • JLE; less or equal
  • JA; above, confronto unsigned
  • JB; below, confronto unsigned

Queste istruzioni prendono una decisione leggendo i flag della CPU e prima del salto compare frequentemente un’istruzione cmp eax,1 oppure test eax,eax.

La prima regola del reverse engineering: partire da qualcosa che risulta osservabile

Cercare alla cieca un JE all’interno di un programma reale è quasi inutile: un’applicazione moderna può contenere migliaia di salti.

Il metodo pratico consiste nel partire invece da un evento osservabile come può essere la comparsa di una finestra con un certo messaggio, l’apertura di un file, la lettura di una specifica chiave nel Registro di Windows, l’avvio di una richiesta di rete, la creazione di un processo o la modifica di un valore nell’interfaccia grafica (GUI).

Da questo punto, una volta individuato qualcosa di osservabile si procede all’indietro. Le stringhe estratte in precedenza con l’apposita utilità Sysinternals possono diventare punti d’ingresso nel codice particolarmente utili.

Anche le API di Windows importate nel file EXE possono fornire punti di aggancio. Se vogliamo capire dove il programma legge il Registro possiamo cercare riferimenti a funzioni come RegOpenKeyExW, RegQueryValueExW e RegGetValueW. Per i file si possono prendere in considerazione CreateFileW, ReadFile, WriteFile; per i processi CreateProcessW e ShellExecuteExW. Per la rete potremmo incontrare WinHTTP, Winsock o altri componenti.

Microsoft mette a disposizione DUMPBIN (fa parte di Visual Studio), capace di visualizzare DLL e funzioni importate dall’immagine. Il comando dumpbin /disasm programma.exe visualizza anche il disassemblato delle sezioni contenenti codice.

Il salto di qualità arriva con un debugger

Uno strumento come WinDbg può eseguire il debug live delle applicazione in esecuzione, mostrare il disassembly e ispezionare registri e memoria. Un debugger permette di modificare temporaneamente lo stato e verificare come cambia il flusso del programma.

Una volta raggiunta la zona interessante, durante l’utilizzo dell’applicazione, WinDbg permette di procedere un’istruzione alla volta. Immaginiamo una struttura del genere:

call controlla_valore
test eax,eax
je errore

Eseguendo CALL possiamo osservare il valore restituito: con uno Step Into possiamo entrare dentro controlla_valore; con uno Step Over possiamo invece lasciare che la funzione venga eseguita e fermarci all’istruzione successiva. Direttamente nella finestra Disassembly, è possibile impostare breakpoint.

Il debugger Windows supporta anche data breakpoint, ossia breakpoint che scattano quando una determinata area di memoria viene letta o scritta (WinDbg mette a disposizione il comando ba).

WinDbg

EXE nativi e .NET: quando il binario torna quasi a essere sorgente

Non tutti i file EXE si lasciano analizzare allo stesso modo. La distinzione più importante è tra codice nativo, tipicamente prodotto da compilatori C e C++, e assembly .NET, che conservano una quantità molto maggiore di informazioni strutturate.

Nel primo caso il compilatore trasforma il programma in istruzioni macchina e, soprattutto con le ottimizzazioni attive, può eliminare o riorganizzare completamente molte strutture del sorgente. Nomi delle variabili locali, commenti, parte dei nomi delle funzioni e costrutti possono non sopravvivere in forma riconoscibile.

Strumenti come Ghidra e IDA riconoscono funzioni, ricostruiscono riferimenti incrociati, generano grafi del flusso di controllo e utilizzano un decompiler per produrre pseudocodice simile al C. Esempio:

if (result == 0) {
...
}

Il risultato non coincide con il sorgente originale: rappresenta una ricostruzione semantica ottenuta a partire dal codice macchina. Tipi, nomi delle variabili e struttura dei blocchi possono essere dedotti; inoltre una singola sequenza assembly può ammettere più rappresentazioni equivalenti ad alto livello. Proprio per questo il lavoro dell’analista consiste spesso nel rinominare progressivamente funzioni anonime, assegnare tipi e seguire le cross-reference fino a restituire un significato a ciò che inizialmente appare come una successione di indirizzi e salti.

Il caso particolare di .NET

Con i programmi .NET la situazione può essere radicalmente diversa. Un assembly tradizionale contiene namespace, classi, metodi, proprietà, firme, tipi dei parametri, valori restituiti, attributi, riferimenti ad altri assembly e numerose altre informazioni. Strumenti come ILSpy, dotPeek e dnSpy, al quale abbiamo dedicato un’intera guida, possono quindi ricostruire codice C# spesso sorprendentemente vicino a quello scritto dallo sviluppatore.

dnSpy .NET

La conseguenza pratica è notevole. Se un’applicazione .NET visualizza una certa stringa, avvalendosi delle utilità citate si può cercare direttamente quel testo, individuare il metodo che lo utilizza e seguire la catena delle chiamate fino alla condizione che determina il comportamento.

Decompilare non significa recuperare magicamente il progetto originale e far apparire il sorgente bensì ridurre progressivamente la distanza tra ciò che Windows esegue e ciò che un essere umano può comprendere. Con alcuni assembly .NET quella distanza è sorprendentemente piccola; con un eseguibile nativo ottimizzato può invece diventare un vero lavoro di ricostruzione.

Ti consigliamo anche

Link copiato negli appunti