Ti sei mai imbattuto in un file dal contenuto sconosciuto? In ambiente Linux, il comando `file` ti offre una rapida identificazione del tipo di file. Tuttavia, se si tratta di un file binario, le tue indagini possono andare ben oltre. Esistono diversi strumenti correlati a `file` che ti consentono di analizzare a fondo il file. In questo articolo, esploreremo alcuni di questi strumenti.
Riconoscere i Tipi di File
I file contengono elementi distintivi che aiutano il software a identificarne il tipo e a comprendere i dati che racchiudono. Ad esempio, non ha senso provare ad aprire un file PNG con un riproduttore musicale MP3. È quindi essenziale che un file includa un meccanismo di identificazione.
Questo meccanismo può consistere in una sequenza di byte caratteristica all’inizio del file, una sorta di “firma” che ne definisce il formato e il contenuto. A volte, il tipo di file viene dedotto analizzando l’organizzazione interna dei dati, una caratteristica chiamata architettura del file.
Alcuni sistemi operativi, come Windows, si basano esclusivamente sull’estensione del file. Questo approccio, a volte considerato ingenuo, presuppone che un file con estensione DOCX sia un documento di testo in formato DOCX. Linux, al contrario, adotta un approccio più rigoroso. Il sistema esamina il contenuto del file per determinarne la reale natura.
Gli strumenti presentati in questo articolo erano già inclusi nelle distribuzioni Manjaro 20, Fedora 21 e Ubuntu 20.04 utilizzate per la nostra analisi. Iniziamo la nostra esplorazione utilizzando il comando `file`.
L’uso del Comando `file`
Nella directory di lavoro, abbiamo diversi file di vario tipo, tra cui documenti, codice sorgente, eseguibili e file di testo.
Il comando `ls` ci mostra il contenuto della directory, mentre l’opzione `-hl` (dimensione leggibile, elenco dettagliato) ci fornisce le dimensioni dei file:
ls -hl
Analizziamo alcuni di questi file con il comando `file`:
file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu
Il comando `file` ha identificato correttamente i tre tipi di file. In aggiunta, fornisce ulteriori informazioni, ad esempio, la versione 1.5 del formato PDF per il file `build_instructions.pdf`.
Anche rinominando il file ODT con un’estensione arbitraria, ad esempio XYZ, il file viene comunque riconosciuto correttamente, sia dal browser di file che dal comando `file`.
Il browser di file associa l’icona corretta al file. Da riga di comando, il comando `file` ignora l’estensione e analizza il contenuto del file per determinarne il tipo:
file build_instructions.xyz
Usando il comando `file` su file multimediali come immagini o file audio, si ottengono informazioni aggiuntive sul formato, la codifica, la risoluzione, ecc:
file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3
Anche con i file di testo, il comando `file` non si basa sull’estensione. Ad esempio, un file con estensione “.c” contenente testo semplice, ma non codice sorgente, non sarà erroneamente identificato come codice sorgente C:
file function+headers.h
file makefile
file hello.c
Il comando `file` identifica correttamente il file di intestazione (“.h”) come parte di codice sorgente C e riconosce il makefile come uno script.
Analizzare File Binari
I file binari sono più opachi rispetto ad altri formati. I file immagine possono essere visualizzati, i file audio possono essere riprodotti e i documenti possono essere aperti con i programmi appropriati. I file binari, tuttavia, presentano maggiori sfide.
Ad esempio, i file “hello” e “wd” sono file eseguibili binari, ovvero programmi. Il file “wd.o” è un file oggetto. Quando il codice sorgente viene compilato, vengono creati uno o più file oggetto contenenti il codice macchina che il computer eseguirà, insieme a informazioni per il linker. Il linker collega questi file alle librerie utilizzate dal programma. Il risultato di questo processo è un file eseguibile.
Il file “watch.exe” è un eseguibile binario compilato per essere eseguito su Windows:
file wd
file wd.o
file hello
file watch.exe
Il file “watch.exe” viene identificato come eseguibile PE32+, un programma per console destinato alla famiglia di processori x86 su Windows. PE sta per Portable Executable, un formato eseguibile con versioni a 32 e 64 bit. PE32 è la versione a 32 bit e PE32+ è la versione a 64 bit.
Gli altri tre file sono identificati come file in formato eseguibile e collegabile (ELF), uno standard per file eseguibili e file oggetto condivisi, come le librerie. Analizzeremo presto il formato dell’intestazione ELF.
È interessante notare che i due eseguibili (“wd” e “hello”) sono identificati come oggetti condivisi Linux Standard Base (LSB), mentre il file oggetto “wd.o” è identificato come rilocabile LSB. La parola “eseguibile” è assente in questo caso.
I file oggetto sono rilocabili, il che significa che il codice al loro interno può essere caricato in memoria in qualsiasi posizione. Gli eseguibili sono elencati come oggetti condivisi perché ereditano questa capacità durante il processo di linking.
Ciò permette alla randomizzazione del layout dello spazio degli indirizzi (ASMR) di caricare gli eseguibili in memoria ad indirizzi casuali. Gli eseguibili standard hanno un indirizzo di caricamento codificato nelle intestazioni, il quale determina dove vengono caricati in memoria.
ASMR è una misura di sicurezza. Caricare gli eseguibili in memoria ad indirizzi prevedibili li rende vulnerabili ad attacchi, poiché i punti di ingresso e le posizioni delle loro funzioni sono sempre noti agli aggressori. Gli eseguibili indipendenti dalla posizione (PIE), caricati in indirizzi casuali, superano questa vulnerabilità.
Se compiliamo il nostro programma con il compilatore gcc e usiamo l’opzione `-no-pie`, generiamo un eseguibile convenzionale.
L’opzione `-o` (file di output) consente di specificare un nome per il nostro eseguibile:
gcc -o hello -no-pie hello.c
Analizziamo il nuovo eseguibile con `file`:
file hello
La dimensione dell’eseguibile è la stessa (17 KB):
ls -hl hello
Ora l’eseguibile è identificato come eseguibile standard. Questo è solo a scopo dimostrativo. Compilando le applicazioni in questo modo, si perdono tutti i vantaggi di ASMR.
Perché un Eseguibile è così Grande?
Il nostro programma “hello” è di 17 KB, non è molto grande, ma il termine “grande” è relativo. Il codice sorgente è di soli 120 byte:
cat hello.c
Cosa fa aumentare le dimensioni del binario, se l’unica cosa che fa è stampare una stringa sulla finestra del terminale? L’intestazione ELF è di soli 64 byte per un binario a 64 bit. Chiaramente c’è qualcos’altro:
ls -hl hello
Analizziamo il binario con il comando `strings` per vedere cosa c’è dentro. Usiamo `less` per visualizzare l’output:
strings hello | less
Nel binario ci sono molte stringhe, oltre a “Hello, Geek world!” dal nostro codice sorgente. La maggior parte sono etichette per aree all’interno del binario, nomi e informazioni di collegamento per oggetti condivisi. Questi includono le librerie e le funzioni all’interno di tali librerie da cui dipende il binario.
Il comando `ldd` mostra le dipendenze di un binario:
ldd hello
L’output mostra tre dipendenze, due delle quali includono il percorso della directory (la prima no):
`linux-vdso.so`: Il virtual dynamic shared object (VDSO) è un meccanismo del kernel che consente a un insieme di routine dello spazio del kernel di accedere a un binario dello spazio utente, evitando il sovraccarico di un cambio di contesto dalla modalità utente alla modalità kernel. Gli oggetti condivisi VDSO aderiscono al formato ELF e vengono collegati dinamicamente al binario durante l’esecuzione. Il VDSO è allocato dinamicamente e sfrutta ASMR. La funzionalità VDSO è fornita dalla Libreria GNU C se il kernel supporta ASMR.
`libc.so.6`: L’oggetto condiviso della Libreria GNU C.
`/lib64/ld-linux-x86-64.so.2`: Il linker dinamico che il binario vuole usare. Il linker dinamico esamina il binario per scoprire le sue dipendenze, carica questi oggetti in memoria e prepara il binario per l’esecuzione.
L’Intestazione ELF
Possiamo analizzare l’intestazione ELF usando il comando `readelf` e l’opzione `-h` (intestazione del file):
readelf -h hello
L’intestazione viene decodificata:
Il primo byte di ogni file binario ELF è impostato al valore esadecimale `0x7F`. I successivi tre byte sono impostati a `0x45`, `0x4C` e `0x46`. Il primo byte identifica il file come binario ELF. Per chiarire, i successivi tre byte rappresentano la stringa “ELF” in ASCII.
Classe: indica se il binario è un eseguibile a 32 o 64 bit (1 = 32, 2 = 64).
Dati: indica l’uso del endianness, che definisce come sono memorizzati i numeri multi-byte. Nel big-endian, i byte più significativi di un numero sono memorizzati per primi, mentre nel little-endian, i byte meno significativi vengono memorizzati per primi.
Versione: la versione di ELF (attualmente è 1).
OS/ABI: indica il tipo di interfaccia binaria dell’applicazione (ABI), che definisce l’interfaccia tra due moduli binari, come un programma e una libreria condivisa.
Versione ABI: la versione dell’ABI.
Tipo: il tipo di binario ELF. I valori comuni sono ET_REL per una risorsa rilocabile (come un file oggetto), ET_EXEC per un eseguibile compilato con il flag `-no-pie` e ET_DYN per un eseguibile compatibile con ASMR.
Macchina: L’architettura del set di istruzioni, che indica la piattaforma di destinazione per cui il file binario è stato creato.
Versione: impostata sempre a 1 per questa versione di ELF.
Indirizzo del punto di ingresso: l’indirizzo di memoria da cui inizia l’esecuzione del binario.
Le altre voci contengono le dimensioni e il numero delle aree e sezioni all’interno del binario, consentendo di calcolarne la posizione.
Un’occhiata rapida ai primi otto byte del binario usando `hexdump` mostrerà il byte della firma e la stringa “ELF”. L’opzione `-C` (canonico) mostra la rappresentazione ASCII dei byte insieme ai loro valori esadecimali, e l’opzione `-n` (numero) specifica quanti byte vogliamo visualizzare:
hexdump -C -n 8 hello
`objdump` per una Vista Dettagliata
Se vuoi una vista granulare, puoi usare il comando `objdump` con l’opzione `-d` (disassembla):
objdump -d hello | less
Questo disassembla il codice macchina e lo visualizza in byte esadecimali, insieme all’equivalente in linguaggio assembly. La posizione dell’indirizzo del primo byte di ogni riga è mostrata all’estrema sinistra.
Questo è utile se sai leggere il linguaggio assembly o se sei curioso di capire cosa succede “dietro le quinte”. L’output è abbondante, per questo lo abbiamo inviato a `less`.
Compilazione e Linking
Esistono diversi modi per compilare un file binario. Ad esempio, lo sviluppatore può scegliere se includere o meno informazioni di debug. Anche il modo in cui il binario è collegato gioca un ruolo nel suo contenuto e dimensione. Se un binario usa dipendenze esterne condivise, sarà più piccolo rispetto a un binario con dipendenze collegate staticamente.
La maggior parte degli sviluppatori ha familiarità con i comandi presentati qui. Per altri, questi strumenti offrono un modo semplice per esplorare il contenuto della “scatola nera” dei file binari.