Jak nahlédnout do binárních souborů z příkazového řádku Linuxu

Máte soubor, jehož obsah je vám záhadou? Příkaz `file` v Linuxu vám rychle napoví, o jaký typ souboru se jedná. Pokud se ovšem jedná o binární soubor, můžete o něm zjistit mnohem více. Pro analýzu binárních souborů existuje řada dalších nástrojů. V tomto článku si ukážeme, jak některé z nich používat.

Rozpoznávání typů souborů

Soubory obvykle obsahují data, která softwarovým aplikacím umožňují rozpoznat jejich typ a obsah. Například by nemělo smysl pokoušet se otevírat obrázek ve formátu PNG v hudebním přehrávači MP3. Proto je praktické, že soubory nesou určitou formu identifikace.

Identifikace může spočívat v několika úvodních bajtech, které slouží jako podpisový vzor. To explicitně určuje formát a obsah souboru. Někdy je typ souboru určen charakteristickou vnitřní strukturou dat, což se nazývá architektura souboru.

Některé operační systémy, jako Windows, spoléhají výhradně na příponu souboru. Systém Windows automaticky předpokládá, že soubor s příponou DOCX je textový dokument ve formátu DOCX. Linux ovšem takto nefunguje. Vyžaduje důkaz a pro určení typu souboru nahlíží do jeho obsahu.

Nástroje, které si zde představíme, byly předinstalovány v distribucích Manjaro 20, Fedora 21 a Ubuntu 20.04, které jsme použili pro tento článek. Začneme příkazem `file`.

Použití příkazu `file`

V aktuálním adresáři máme kolekci souborů různých typů. Najdeme zde dokumenty, zdrojový kód, spustitelné soubory i prosté textové soubory.

Příkaz `ls` zobrazí obsah adresáře a volba `-hl` (dlouhý výpis s velikostmi čitelnými pro člověka) ukáže velikost každého souboru:

ls -hl

Zkusíme příkaz `file` na několik z nich a podíváme se na výsledek:

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

Všechny tři formáty souborů byly správně identifikovány. Pokud je to možné, příkaz `file` poskytne i doplňující informace. Například u souboru PDF uvádí verzi formátu 1.5.

I když soubor ODT přejmenujeme na příponu XYZ, příkaz `file` ho správně identifikuje. Stejně tak i správce souborů ho zobrazí se správnou ikonou.

Správce souborů Files zobrazuje správnou ikonu. V příkazové řádce příkaz `file` ignoruje příponu a podle obsahu souboru určí jeho typ:

file build_instructions.xyz

Při použití příkazu `file` na multimediální soubory, jako jsou obrázky a hudba, obvykle získáme informace o jejich formátu, kódování, rozlišení a podobně.

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

Zajímavé je, že ani u souborů s prostým textem se příkaz `file` neřídí jejich příponou. Pokud například máte soubor s příponou „.c“, který ovšem obsahuje obyčejný text, nikoli zdrojový kód, příkaz `file` ho nezamění za pravý zdrojový kód jazyka C:

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

Příkaz `file` správně identifikuje hlavičkový soubor (.h) jako součást zdrojového kódu C a ví, že makefile je skript.

Použití příkazu `file` s binárními soubory

Binární soubory jsou ve srovnání s ostatními soubory spíše „černou skříňkou“. Obrázky můžeme prohlížet, hudební soubory přehrávat a textové dokumenty otevírat pomocí specializovaného softwaru. U binárních souborů je situace komplikovanější.

Například soubory „hello“ a „wd“ jsou binární spustitelné soubory, tedy programy. Soubor „wd.o“ je objektový soubor. Při kompilaci zdrojového kódu se vytváří jeden nebo více objektových souborů. Ty obsahují strojový kód, který počítač spouští při spuštění hotového programu, a také informace pro linker. Linker analyzuje jednotlivé objektové soubory, hledá volání funkcí z knihoven a propojí je se všemi potřebnými knihovnami. Výsledkem je spustitelný soubor.

Soubor „watch.exe“ je binární spustitelný soubor, který byl křížově zkompilován pro systém Windows:

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

Příkaz `file` nám říká, že „watch.exe“ je spustitelný program pro konzoli ve formátu PE32+ pro procesory x86 v systému Microsoft Windows. PE je zkratka pro Portable Executable, který má 32bitové i 64bitové verze. PE32 je 32bitová verze a PE32+ je 64bitová verze.

Ostatní tři soubory jsou identifikovány jako soubory Executable and Linkable Format (ELF). To je standard pro spustitelné soubory a soubory sdílených objektů (např. knihovny). Formát záhlaví ELF si prozkoumáme později.

Zajímavé je, že dva spustitelné soubory („wd“ a „hello“) jsou označeny jako sdílené objekty Linux Standard Base (LSB) a objektový soubor „wd.o“ jako přemístitelný LSB. U spustitelných souborů slovo „spustitelný“ chybí.

Objektové soubory jsou přemístitelné, což znamená, že jejich kód lze načíst do paměti na libovolném místě. Spustitelné soubory jsou uvedeny jako sdílené objekty, protože byly vytvořeny linkerem z objektových souborů tak, že tuto schopnost zdědily.

Díky tomu může Address Space Layout Randomization (ASLR) načítat spustitelné soubory do paměti na náhodně vybrané adresy. Standardní spustitelné soubory mají v záhlaví uloženou načítací adresu, která určuje, kam se mají v paměti načíst.

ASLR je bezpečnostní technika. Načítání spustitelných souborů do paměti na předvídatelné adresy je činí zranitelnými. Útočníci totiž znají jejich vstupní body a umístění jejich funkcí. Spustitelné soubory nezávislé na pozici (PIE), načtené na náhodnou adresu, tuto zranitelnost eliminují.

Pokud zkompilujeme náš program pomocí kompilátoru `gcc` a přidáme volbu `-no-pie`, vygenerujeme konvenční spustitelný soubor.

Volba `-o` (výstupní soubor) nám umožňuje zadat název našeho spustitelného souboru:

gcc -o hello -no-pie hello.c

Nyní znovu použijeme příkaz `file` na nový spustitelný soubor a podíváme se, co se změnilo:

file hello

Velikost spustitelného souboru zůstala stejná jako dříve (17 kB):

ls -hl hello

Binární soubor je nyní identifikován jako standardní spustitelný soubor. Děláme to pouze pro demonstrační účely. Pokud budete aplikace kompilovat tímto způsobem, přijdete o výhody ASLR.

Proč je spustitelný soubor tak velký?

Náš vzorový program `hello` má 17 kB. Zdrojový kód má pouhých 120 bajtů:

cat hello.c

Co nafukuje binární soubor, když vše, co dělá, je tisk jednoho řetězce do terminálu? Víme, že existuje hlavička ELF, která ale u 64bitového binárního souboru má pouhých 64 bajtů. Je jasné, že tam musí být ještě něco dalšího:

ls -hl hello

Zkusme naskenovat binární soubor pomocí `strings`, jako první krok k odhalení jeho obsahu. Pro lepší přehled si výstup zobrazíme v `less`:

strings hello | less

Uvnitř binárního kódu je mnoho řetězců, kromě „Hello, Geek world!“ z našeho zdrojového kódu. Většina z nich jsou popisky pro různé oblasti binárního souboru a názvy a propojovací informace sdílených objektů. Patří sem knihovny a funkce v těchto knihovnách, na kterých binární soubor závisí.

Příkaz `ldd` zobrazí závislosti binárního souboru na sdílených objektech:

ldd hello

Výstup obsahuje tři položky a dvě z nich obsahují cestu k adresáři (první nikoli):

`linux-vdso.so`: Virtual Dynamic Shared Object (VDSO) je mechanismus jádra, který umožňuje přístup k rutinám v prostoru jádra binárnímu souboru v uživatelském prostoru. Tím se zabrání režii při přepínání kontextu mezi uživatelským a jádrovým režimem. Sdílené objekty VDSO dodržují formát ELF (Executable and Linkable Format), což umožňuje jejich dynamické propojení s binárním souborem za běhu. VDSO je dynamicky alokováno a využívá ASLR. Funkcionalita VDSO je poskytována standardem Knihovna GNU C, pokud to jádro podporuje.
`libc.so.6`: Sdílený objekt Knihovny GNU C.
`/lib64/ld-linux-x86-64.so.2`: Dynamický linker, který má binární soubor použít. Dynamický linker dotazuje binární soubor, aby zjistil jeho závislosti. Tyto sdílené objekty načte do paměti, připraví binární soubor ke spuštění a zajistí, aby našel své závislosti v paměti. Poté spustí program.

Hlavička ELF

Můžeme prozkoumat a dekódovat hlavičku ELF pomocí nástroje `readelf` a volby `-h` (záhlaví souboru):

readelf -h hello

Záhlaví je interpretováno za nás.

První bajt všech binárních souborů ELF má hexadecimální hodnotu 0x7F. Další tři bajty jsou 0x45, 0x4C a 0x46. První bajt slouží jako příznak, který identifikuje soubor jako binární ELF. Další tři bajty v ASCII hláskují „ELF“.

Třída: Určuje, zda je binární soubor 32bitový nebo 64bitový spustitelný soubor (1=32, 2=64).
Data: Určuje endianness. Endian kódování definuje způsob, jakým jsou ukládána vícebajtová čísla. V big-endian je číslo ukládáno s nejvýznamnějšími bity jako první. V little-endian je číslo ukládáno s nejméně významnými bity jako první.
Verze: Verze ELF (aktuálně je to 1).
OS/ABI: Představuje typ binárního rozhraní aplikace. Definuje rozhraní mezi dvěma binárními moduly, jako je program a sdílená knihovna.
Verze ABI: Verze ABI.
Typ: Typ binárního ELF. Běžné hodnoty jsou ET_REL pro přemístitelný objekt (například objektový soubor), ET_EXEC pro spustitelný soubor kompilovaný s volbou `-no-pie` a ET_DYN pro spustitelný soubor s podporou ASLR.
Stroj: Architektura instrukční sady. Určuje cílovou platformu, pro kterou byl binární soubor vytvořen.
Verze: Pro tuto verzi ELF je vždy nastavena na 1.
Adresa vstupního bodu: Adresa paměti v binárním souboru, kde se začíná s prováděním programu.

Další položky určují velikosti a počty oblastí a sekcí v binárním souboru, takže lze vypočítat jejich umístění.

Krátký pohled na prvních osm bajtů binárního souboru pomocí `hexdump` zobrazí podpisové bajty a řetězec „ELF“ v prvních čtyřech bajtech. Volba `-C` (kanonická) poskytne ASCII reprezentaci bajtů spolu s jejich hexadecimálními hodnotami. Volba `-n` (počet) umožňuje určit počet zobrazených bajtů:

hexdump -C -n 8 hello

objdump a detailní pohled

Pokud chcete vidět více detailů, můžete použít příkaz `objdump` s volbou `-d` (rozložit):

objdump -d hello | less

Tím se rozloží strojový kód a zobrazí se v hexadecimálních bajtech spolu s ekvivalentem v jazyce symbolických instrukcí. Adresa prvního bajtu každého řádku je zobrazena úplně vlevo.

To je užitečné, pokud znáte jazyk symbolických instrukcí nebo chcete vědět, co se děje v zákulisí. Výstupu je hodně, takže ho přesměrujeme do `less`.

Kompilace a propojení

Binární soubor lze kompilovat mnoha způsoby. Vývojář se například může rozhodnout, zda zahrnout informace pro ladění. Způsob, jakým je binární soubor propojen, má také vliv na jeho obsah a velikost. Pokud se binární soubor odkazuje na externí závislosti (sdílené objekty), bude menší než binární soubor se staticky propojenými závislostmi.

Většina vývojářů příkazy, které jsme zde probrali, už zná. Pro ostatní představují jednoduchý způsob, jak prozkoumat binární soubory a zjistit, co se v nich skrývá.