.exe soubor – proč proboha?

Zveřejněno , upraveno - holoubekm Přidat komentář bez kategorie
Znalostí spustitelných souborů sice neuděláte dojem na holku, ale kámoše z IT dostanete do kolen.

Předpokladem úspěšného lovu je dobrá znalost prostředí, ve kterém se náš cíl pohybuje. Stejné to je i v případě reverzního inženýrství – abychom mohli analyzovat programy a jejich chování, musíme znát detaily platformy a strukturu naší binárky. Dnes se tak vydáme na cestu do hlubin spustitelného souboru.

TLDR – pokud vás nezajímá historie (tedy ani současnost :P) můžete přeskočit k nadpisu Jak vypadá binárka uvnitř?.

Motivace příběhem

Abychom pochopili motivaci autorů z pohledu dnešního stavu, je dobré vrátit se v čase do 50. a 60. let. V rychlosti si tak proběhneme kousek historie.

Paměť řídící jednotky v programu Apollo – ručně zadrátovaná feritová jádra. (1966)

 

Pro znalce: Ferit (Fe2O3) je magneticky permeabilní materiál – typicky se z něj tak vyrábí magnetická jádra pro cívky.

Počítače té doby byly většinou prototypy s unikátními vlastnostmi i technologií výroby.

Od počátků 50. let byla technologie jednotlivých komponent prakticky vždy mezigeneračně obměňována. Pro procesory se od generace 0 používala mechanická relé, která trpěla poruchami a jejich nevýhodou byla nízká rychlost. V první generaci (1945) byla relé nahrazena elektronkami, které díky odlišnému principu umožňovaly vyšší rychlosti. Druhá generace zavedla použití tranzistorů a objevila se řada známých prvků – operační systém, assembler a vyšší programovací jazyky (nelíbí se Céčko? Nevadí, tak tam dáme Fortran! Lepší?).

 

V autobiografii Richarda Feynmana (To nemyslíte vážně, pane Feynmane!) jsou skvělým způsobem popsány počátky a využití počítačů v projektu Manhattan.

Pro vstup programu byly využívány kreativní způsoby, jejichž volba byla kompromisem ceny, rychlosti načítání vstupu a šílenosti tvůrce. V halových počítačích tak byl program načítán například ručním zadrátováním paměti, případně z děrné pásky, kterou nahradily děrné štítky a později magnetické pásky.

Děrné štítky navíc na rozdíl od ostatních metod umožňovaly automatické spouštění několika úloh za sebou (batching).

Ale tohle jsme už stejně všichni slyšeli.

S příchodem výkonějších systémů se objevil požadavek na umístění více programů do jedné paměti (adresního prostoru). Z nutnosti byl tak vytvořen operační systém jako sjednodující prvek umožňující oddělení fyzického hardwaru od softwaru.

Princip relokace programů do paměti.

Protože v temných 50. a 60. letech bylo paměti pramálo a byla drahá, nevešly se do ní všechny programy a bylo je nutné načítat z externího uložiště – typicky magnetické pásky. Pro načítání binárky existuje v operačním systému program zvaný loader. Ten umí přečíst spustitelný program v daném formátu a zavést ho na danou adresu v paměti.

První OS umožňovaly načtení pouze jednoho programu zároveň. Postupně však cena paměti klesala a bylo možné vyplnit zbývající místo dalšími programy – multiprogramming.

Všimněte si, že s postupem času už není binárka jen „hloupý“ seznam instrukcí, ale umí další divy.

Způsob, jakým jsou programy kompilovány, vyžadoval přesnou znalost adresy, na kterou byl později načten. Programy totiž obsahují absolutní skoky, které mají v cíli zadanou přesnou adresu – posunutí takového programu v paměti neudělá dobře nikomu.

Existuje několik řešení:

  • ruční správa paměti – programy jsou zkompilovány přesně pro danou adresu tak, aby se nepřekrývaly a neovlivňovaly.
  • dynamická relokace – loader při načtení programu přepočítá veškteré adresy pomocí rad (hintů), které do souboru uložil kompilátor.
  • virtualizace paměti – programy jsou načteny na adresu 0x0000… Při operaci s pamětí jim operační systém sahá pod ruce a adresu každého přístupu do paměti přepočítá tak, aby se programy neovlivnily.
  • kombinace předchozího – spojením segmentace, virtuální paměti a relokace se dostaneme do stavu, který podporují současné operační systémy.
Pokud se dám udělalo z teorie nevolno, tak se doporučuji vyhnout skvělé publikaci Modern Operating Systems od Andrew S. Tanenbaum

Na předchozích řádcích jsem zcela záměrně přeskočil nudné části o správě paměti, segmentaci, virtualizace apod. Pokud se nenudíte dostatečně, ozvěte se a v případě zájmu můžu pokračovat dalším článkem.

Jak vypadá binárka uvnitř?

Pro zjednodušení se zaměříme na .exe soubory známe z operačního systému Windows. Formáty spoustitelných souborů pro ostatní platformy jsou ale velmi podobné.

Naní se můžeme pustit do zkoumání struktur spustitelného souboru.

Binárka je typicky rozdělena do hlaviček a sekcí, z nichž některé jsou volitelné a jiné povinné. Různé operační systémy využívají odlišné otevřené, či proprietární formáty – Windows mají formát Portable Executable (PE), Unix based systémy mohou využívat COFF, či ELF a MacOS beží na formátu Mach-O.

Hlavičky obsahují metadata o sekcích a ty se chovají jako kontejnery pro uložení dat.

Abychom pochopili, jak soubor vypadá uvnitř, zaměříme se na jeden z dostupných formátů – Windows PE. Žádný strach, na ostatních systémech je struktura binárních souborů velmi podobná.

Formát PE – Portable Executable

Základní struktura spustitelného souboru

PE – Portable Executable – je spustitelný formát používaný zejména operačních systémem Windows. Popisuje množství hlaviček a sekcí, které dynamickému linkeru říkají, jak data umístit do paměti.

Načtený soubor je rozdělen do sekcí, kterým linker nastavuje příslušná oprávnění. Každá sekce musí být v paměti zarovnána na velikost stránky.

Na začátku samotného souboru je tzv. DOS header, archaická hlavička obsahující známý text „This program cannot be run in DOS mode.„. Jejím úkolem je zachování zpětné kompatibility s 16-bit Windows.

Následuje hlavička PE formátu – PE header. Ta obsahuje magic number, či signaturu souboru, z níž je možné určit endianitu systému. Je zde uložena také tzv. data directory– tabulka importovaných knihoven (na windows nejčastěji s příponou .dll.).

K hlavičce souboru je připojena tabulka sekcí. Jde o jednoduché pole, kdy každý index obsahuje informace o dané sekci dále v souboru. Sekce se opět dělí na povinné a nepovinné. Navíc existuje několik typů objektových souborů – od dynamických knihoven .dll, objektových souborů kompilace .obj a spustitelných .exe souborů. Každý z těchno tvarů má svá specifika a liší se v detailech.

Důležitou částí souboru jsou samotné sekce:

  • .bss – neinicializovaná data aplikace – v jazyce C typicky proměnné definované jako static.
  • .rsrc – resources – vestavěné zdroje používané programem – ikony, obrázky, kurzory, databáze atd.
  • .text – nejzajímavější část – strojový kód určený k běhu na CPU
  • .rdata– read-only data – data určená pouze ke čtení – typicky se jedná o řetězce a další const proměnné.
  • .edata – export table – tabulka popisující exportované funkce a proměnné – nejčastějši používaná u knihoven .dll.
  • .idata – import tables – tabulky popisující importované knihovny a jejich funkce.
  • .pdata – exception handlers table – tabulka popisující funkce, určené k zachycování výjimek.
  • .tls – thread local storage – datové uložiště s podporou synchronizace, které umožňuje používání lokálních proměnných unikátních pro každé vlákno
  • .debug – tabulka pro debugger – obsahuje mapování proměnných na položky v datových tabulkách, původní názvy funkcí, mapování výrazů na řádky zdrojového kódu a další užitečné údaje, které IDE automaticky používá.
  • .reloc – PE soubory nejsou typicky position independent – kód v nich počítá s fixní adresou, na kterou bude nahrán. Loader operačního systému tedy musí adresy přepočítat a k tomu využije talkuku .reloc.
void function() {
 //Použití sekce .bss
 static int myBSSVariable;
 //Použití sekce .rdata
 const char myRDataVariale[] = "Tak tohle si pěkně odnesu";
 //Použití sekce .tls
 //Import funkce GetCurrentThreadId z knihovny Kernel32.dll pomocí .idata
 __declspec (thread) DWORD myTLSVariable = GetCurrentThreadId();
 //Použití sekce .pdata
 try {
   myTLSVariable++;
 } catch (...) {
   
 }
}
 
//Funkce exportovaná z .dll pomocí tabulky .edata
__declspec(dllexport) void __cdecl function(void);

Ukázka využití jednotlivých sekcí v jazyce C

Není náhoda, že každá ze sekcí je namapována v operační paměti do jiné stránky. Stránka je totiž nejmenší blok paměti, kterému lze nastavit různé příznaky. Jednoduše se tak můžeme vyhnout některým bezpečnostním zranitelnostem a omezením:

//Když známe cílovou platformu můžeme vytvořit shellcode, 
//který po spuštění vykoná útočníkem určené instrukce na úrovni strojového kódu
char code[] = "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb";
int main(int argc, char **argv) {
 //Přetypování řetězce na funkci a její zavolání
 ((int (*)()) code)();
}

Funkční exploit můžeme technicky zastavit tak, že tabulce .rdata odebereme příznak ke spuštění – ve chvíli kdy se program pokusí zavolat adresu z této tabulky dojde k jeho násilnému ukončení systémem.

Problematika spouštění programů je velmi široká a vtěsnat ji do jednoho článku je nemožné. V příštím díle se tak zaměříme na opomenutá témata – pro všechny, které se problematikou zabývají okrajově a přesto chtějí mít přehled, jak věci fungují.

V pokračování se podrobněji podíváme na strukturu některých sekcí, princip načítání externích knihoven a jejich funkcí. Vyzkoušíme si MiTM útok a modifikujeme funkcionalitu programu změnou importované funkce. Dále se z blízka podíváme na ASLR a relokace, které s ním souvisí, NX bit a tzv. fat executables – .exe soubory umožňující spuštění programů pro .NET.

Vložit komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *