Come sbirciare dentro i file binari dalla riga di comando di Linux

Hai un file misterioso? Il comando file di Linux ti dirà rapidamente di che tipo di file si tratta. Se si tratta di un file binario, però, puoi saperne ancora di più. file ha tutta una serie di stablemates che ti aiuteranno ad analizzarlo. Ti mostreremo come utilizzare alcuni di questi strumenti.

Identificazione dei tipi di file

I file di solito hanno caratteristiche che consentono ai pacchetti software di identificare quale tipo di file è, nonché cosa rappresentano i dati al suo interno. Non avrebbe senso provare ad aprire un file PNG in un lettore musicale MP3, quindi è utile e pragmatico che un file porti con sé una qualche forma di ID.

Potrebbe trattarsi di pochi byte della firma all’inizio del file. Ciò consente a un file di essere esplicito sul suo formato e contenuto. A volte, il tipo di file viene dedotto da un aspetto distintivo dell’organizzazione interna dei dati stessi, noto come architettura del file.

Alcuni sistemi operativi, come Windows, sono completamente guidati dall’estensione di un file. Puoi chiamarlo credulone o fiducioso, ma Windows presume che qualsiasi file con estensione DOCX sia davvero un file di elaborazione testi DOCX. Linux non è così, come vedrai presto. Vuole una prova e guarda all’interno del file per trovarlo.

Gli strumenti descritti qui erano già installati sulle distribuzioni Manjaro 20, Fedora 21 e Ubuntu 20.04 che abbiamo usato per ricercare questo articolo. Cominciamo la nostra indagine utilizzando il comando file.

Utilizzando il file Command

Abbiamo una raccolta di diversi tipi di file nella nostra directory corrente. Sono un misto di documenti, codice sorgente, eseguibili e file di testo.

Il comando ls ci mostrerà cosa c’è nella directory e l’opzione -hl (dimensioni leggibili dall’uomo, elenco lungo) ci mostrerà la dimensione di ogni file:

ls -hl

ls -hl in una finestra di terminale.

Proviamo a file su alcuni di questi e vediamo cosa otteniamo:

file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu

file build_instructions.odt in una finestra di terminale.

I tre formati di file vengono identificati correttamente. Ove possibile, file ci fornisce qualche informazione in più. Il file PDF è segnalato come file formato versione 1.5.

Anche se rinominiamo il file ODT in modo che abbia un’estensione con il valore arbitrario di XYZ, il file è comunque identificato correttamente, sia all’interno del browser di file File che sulla riga di comando utilizzando file.

File OpenDocument identificato correttamente nel browser di file File, anche se la sua estensione è XYZ.

All’interno del browser di file File, viene assegnata l’icona corretta. Sulla riga di comando, il file ignora l’estensione e guarda all’interno del file per determinarne il tipo:

file build_instructions.xyz

file build_instructions.xyz in una finestra di terminale.

L’utilizzo di file su supporti, come file di immagini e musicali, di solito fornisce informazioni sul loro formato, codifica, risoluzione e così via:

file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3

file screenshot.png in una finestra di terminale.

È interessante notare che, anche con file di testo semplice, file non giudica il file dalla sua estensione. Ad esempio, se si dispone di un file con estensione “.c”, contenente testo normale standard ma non codice sorgente, il file non lo scambierà per un C autentico file di codice sorgente:

file function+headers.h
file makefile
file hello.c

funzione file + headers.h in una finestra di terminale.

file identifica correttamente il file di intestazione (“.h”) come parte di una raccolta di file di codice sorgente C e sa che il makefile è uno script.

Utilizzo di file con file binari

I file binari sono più una “scatola nera” di altri. I file di immagine possono essere visualizzati, i file audio possono essere riprodotti e i file di documenti possono essere aperti dal pacchetto software appropriato. I file binari, tuttavia, sono più una sfida.

Ad esempio, i file “hello” e “wd” sono eseguibili binari. Sono programmi. Il file chiamato “wd.o” è un file oggetto. Quando il codice sorgente viene compilato da un compilatore, vengono creati uno o più file oggetto. Questi contengono il codice macchina che il computer eseguirà alla fine quando viene eseguito il programma finito, insieme alle informazioni per il linker. Il linker controlla ogni file oggetto per le chiamate di funzione alle librerie. Li collega a tutte le librerie utilizzate dal programma. Il risultato di questo processo è un file eseguibile.

Il file “watch.exe” è un eseguibile binario che è stato cross-compilato per essere eseguito su Windows:

file wd
file wd.o
file hello
file watch.exe

file wd in una finestra di terminale.

Prendendo per primo l’ultimo, il file ci dice che il file “watch.exe” è un eseguibile PE32 +, programma per console, per la famiglia di processori x86 su Microsoft Windows. PE sta per formato eseguibile portatile, che ha versioni a 32 e 64 bit. PE32 è la versione a 32 bit e PE32 + è la versione a 64 bit.

Gli altri tre file sono tutti identificati come Formato eseguibile e collegabile (ELF). Si tratta di uno standard per file eseguibili e file di oggetti condivisi, come le librerie. A breve daremo un’occhiata al formato dell’intestazione ELF.

Ciò che potrebbe attirare la tua attenzione è che i due eseguibili (“wd” e “hello”) sono identificati come Linux Standard Base (LSB) oggetti condivisi e il file oggetto “wd.o” viene identificato come LSB rilocabile. La parola eseguibile è ovvia in sua assenza.

I file oggetto sono riposizionabili, 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é sono stati creati dal linker dai file oggetto in modo tale da ereditare questa capacità.

Ciò consente a Randomizzazione del layout dello spazio degli indirizzi (ASMR) per caricare gli eseguibili in memoria agli indirizzi di sua scelta. Gli eseguibili standard hanno un indirizzo di caricamento codificato nelle intestazioni, che determina dove vengono caricati in memoria.

ASMR è una tecnica di sicurezza. Il caricamento di eseguibili in memoria a indirizzi prevedibili li rende suscettibili agli attacchi. Questo perché i loro punti di ingresso e le posizioni delle loro funzioni saranno sempre noti agli aggressori. Posizione eseguibili indipendenti (PIE) posizionato a un indirizzo casuale superare questa suscettibilità.

Se noi compila il nostro programma con il compilatore gcc e forniamo l’opzione -no-pie, genereremo un eseguibile convenzionale.

L’opzione -o (file di output) ci consente di fornire un nome per il nostro eseguibile:

gcc -o hello -no-pie hello.c

Useremo il file sul nuovo eseguibile e vedremo cosa è cambiato:

file hello

La dimensione dell’eseguibile è la stessa di prima (17 KB):

ls -hl hello

gcc -o ciao -no-pie ciao.c in una finestra di terminale.

Il binario è ora identificato come eseguibile standard. Lo facciamo solo a scopo dimostrativo. Se compili le applicazioni in questo modo, perderai tutti i vantaggi dell’ASMR.

Perché un eseguibile è così grande?

Il nostro programma hello di esempio è 17 KB, quindi difficilmente potrebbe essere definito grande, ma in fondo è tutto relativo. Il codice sorgente è di 120 byte:

cat hello.c

Cosa sta aumentando il volume del binario se tutto ciò che fa è stampare una stringa nella finestra del terminale? Sappiamo che c’è un’intestazione ELF, ma è lunga solo 64 byte per un binario a 64 bit. Chiaramente, deve essere qualcos’altro:

ls -hl hello

cat hello.c in una finestra di terminale.

Andiamo scansiona il binario con il stringhe come un semplice primo passo per scoprire cosa c’è dentro. Lo convogliamo in meno:

strings hello | less

corde ciao |  meno in una finestra di terminale.

Ci sono molte stringhe all’interno del binario, oltre a “Hello, Geek world!” dal nostro codice sorgente. La maggior parte sono etichette per regioni all’interno del binario e nomi e informazioni di collegamento di oggetti condivisi. Questi includono le librerie e le funzioni all’interno di quelle librerie da cui dipende il file binario.

Il comando ldd ci mostra le dipendenze degli oggetti condivisi di un binario:

ldd hello

ldd ciao in una finestra di terminale.

Ci sono tre voci nell’output e due di esse includono un percorso di directory (la prima no):

linux-vdso.so: Oggetto virtuale dinamico condiviso (VDSO) è un meccanismo del kernel che consente a un insieme di routine dello spazio del kernel di accedere a un binario dello spazio utente. Questo evita il sovraccarico di un cambio di contesto dalla modalità kernel utente. Gli oggetti condivisi VDSO aderiscono al formato ELF (Executable and Linkable Format), consentendo loro di essere collegati dinamicamente al binario in fase di esecuzione. Il VDSO viene allocato dinamicamente e sfrutta ASMR. La capacità VDSO è fornita dallo standard Libreria GNU C. se il kernel supporta lo schema ASMR.
libc.so.6: il Libreria GNU C. oggetto condiviso.
/lib64/ld-linux-x86-64.so.2: questo è il linker dinamico che il binario vuole usare. Il linker dinamico interroga il binario per scoprire quali dipendenze ha. Avvia quegli oggetti condivisi in memoria. Prepara il binario per essere eseguito ed essere in grado di trovare e accedere alle dipendenze in memoria. Quindi, avvia il programma.

L’intestazione ELF

Noi possiamo esaminare e decodificare l’intestazione ELF utilizzando l’utility readelf e l’opzione -h (intestazione file):

readelf -h hello

readelf -h ciao in una finestra di terminale.

L’intestazione viene interpretata per noi.

Uscita da readelf -h hello in una finestra di terminale.

Il primo byte di tutti i file binari ELF è impostato sul valore esadecimale 0x7F. I successivi tre byte sono impostati su 0x45, 0x4C e 0x46. Il primo byte è un flag che identifica il file come binario ELF. Per renderlo più chiaro, i prossimi tre byte indicano “ELF” in ASCII:

Classe: indica se il file binario è un eseguibile a 32 o 64 bit (1 = 32, 2 = 64).
Dati: indica il file endianness in uso. La codifica Endian definisce il modo in cui vengono memorizzati i numeri multibyte. Nella codifica big-endian, un numero viene memorizzato prima con i suoi bit più significativi. Nella codifica little-endian, il numero viene memorizzato prima con i suoi bit meno significativi.
Versione: la versione di ELF (attualmente, è 1).
OS / ABI: rappresenta il tipo di interfaccia binaria dell’applicazione in uso. 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: il architettura del set di istruzioni. Indica la piattaforma di destinazione per la quale è stato creato il file binario.
Versione: impostare sempre su 1, per questa versione di ELF.
Indirizzo del punto di ingresso: l’indirizzo di memoria all’interno del binario da cui inizia l’esecuzione.

Le altre voci sono dimensioni e numero di regioni e sezioni all’interno del file binario in modo che le loro posizioni possano essere calcolate.

Una rapida occhiata ai primi otto byte del binario con hexdump mostrerà il byte della firma e la stringa “ELF” nei primi quattro byte del file. L’opzione -C (canonica) ci fornisce la rappresentazione ASCII dei byte insieme ai loro valori esadecimali e l’opzione -n ​​(numero) ci consente di specificare quanti byte vogliamo vedere:

hexdump -C -n 8 hello

hexdump -C -n 8 ciao in una finestra di terminale.

objdump e la vista granulare

Se vuoi vedere i dettagli nitidi, puoi usare il comando objdump con l’opzione -d (disassembla):

objdump -d hello | less

objdump -d ciao |  meno in una finestra di terminale.

Questo disassembla il codice macchina eseguibile e lo visualizza in byte esadecimali insieme all’equivalente in linguaggio assembly. La posizione dell’indirizzo del primo arrivederci in ogni riga è mostrato all’estrema sinistra.

Questo è utile solo se sai leggere il linguaggio assembly o sei curioso di cosa succede dietro le quinte. C’è molto output, quindi lo abbiamo convogliato in meno.

Putput da objdump -d ciao |  meno in una finestra di terminale.

Compilazione e collegamento

Esistono molti modi per compilare un file binario. Ad esempio, lo sviluppatore sceglie se includere le informazioni di debug. Anche il modo in cui il file binario è collegato gioca un ruolo nel suo contenuto e dimensione. Se i riferimenti binari condividono oggetti come dipendenze esterne, sarà inferiore a quello a cui le dipendenze si collegano staticamente.

La maggior parte degli sviluppatori conosce già i comandi che abbiamo trattato qui. Per altri, tuttavia, offrono alcuni semplici modi per rovistare e vedere cosa si trova all’interno della scatola nera binaria.