it-swarm-eu.dev

Jak ASLR a DEP fungují?

Jak funguje randomizace rozložení adresního prostoru (ASLR) a prevence spuštění dat (DEP), pokud jde o zabránění zneužití zranitelností? Lze je obejít?

115
Polynomial

Randomizace rozložení adresového prostoru (ASLR) je technologie, která pomáhá zabránit úspěchu shell kódu. Děje se tak náhodným kompenzováním umístění modulů a určitých struktur v paměti. Zabránění spuštění dat (DEP) zabraňuje určitým sektorům paměti, např. zásobník, od provedení. Při jejich kombinaci je mimořádně obtížné zneužít zranitelnost v aplikacích používajících techniky kódování shell nebo návratu (ROP).

Nejprve se podívejme, jak by mohla být zneužita běžná zranitelnost. Vynecháme všechny podrobnosti, ale řekněme, že používáme chybu přetečení zásobníku zásobníku. Naložili jsme velkou kapku 0x41414141 hodnoty do našeho užitečného zatížení a eip byl nastaven na 0x41414141, takže víme, že je využitelný. Potom jsme odešli a použili jsme vhodný nástroj (např. Metasploit's pattern_create.rb) objevit posun hodnoty načítané do eip. Toto je počáteční offset našeho kódu zneužití. K ověření načteme 0x41 před tímto posunem, 0x42424242 na ofsetu a 0x43 po posunutí.

V procesu bez ASLR a non-DEP je adresa zásobníku stejná při každém spuštění procesu. Víme přesně, kde je v paměti. Podívejme se tedy, jak vypadá zásobník s testovanými daty, které jsme popsali výše:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 42424242   > esp points here
 000ff6b4  | 43434343
 000ff6b8  | 43434343

Jak vidíme, esp ukazuje na 000ff6b0, který byl nastaven na 0x42424242. Hodnoty před tím jsou 0x41 a následující hodnoty jsou 0x43, jak jsme řekli, že by měli být. Nyní víme, že adresa uložená na 000ff6b0 bude přeskočeno. Takže jsme ji nastavili na adresu nějaké paměti, kterou můžeme ovládat:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Hodnotu jsme nastavili na 000ff6b0 tak, že eip bude nastaveno na 000ff6b4 - další posun v zásobníku. To způsobí 0xcc k provedení, což je int3 instrukce. Od té doby int3 je přerušení softwaru, vyvolá výjimku a debugger se zastaví. To nám umožňuje ověřit, zda bylo zneužití úspěšné.

> Break instruction exception - code 80000003 (first chance)
[snip]
eip=000ff6b4

Nyní můžeme nahradit paměť na 000ff6b4 s shellcode, změnou našeho užitečného zatížení. Tím se uzavírá naše vykořisťování.

Aby nedošlo k úspěchu těchto exploitů, byla vyvinuta prevence spouštění dat. DEP vynutí určité struktury, včetně zásobníku, aby byly označeny jako nespustitelné. To je posíleno podporou CPU bitem No-Execute (NX), známým také jako bit XD, EVP bit nebo XN bit, který umožňuje CPU vynucovat prováděcí práva na hardwarové úrovni. DEP byl představen v Linuxu v roce 2004 (jádro 2.6.8) a Microsoft jej představil v roce 2004 jako součást WinXP SP2. Apple přidal podporu DEP, když se v roce 2006 přesunuli do architektury x86. S povoleným DEP nebude naše předchozí využití fungovat:

> Access violation - code c0000005 (!!! second chance !!!)
[snip]
eip=000ff6b4

To se nezdaří, protože zásobník je označen jako spustitelný soubor a pokusili jsme se jej spustit. Abychom to obešli, byla vyvinuta technika zvaná Return-Oriented Programming (ROP). To zahrnuje hledání malých úryvků kódu, nazývaných gadgety ROP, v legitimních modulech procesu. Tyto gadgety se skládají z jedné nebo více pokynů následovaných návratem. Zřetězení těchto hodnot spolu s příslušnými hodnotami v zásobníku umožňuje provedení kódu.

Nejprve se podívejme, jak náš stack vypadá právě teď:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Víme, že nemůžeme spustit kód na 000ff6b4, takže musíme najít nějaký legitimní kód, který můžeme místo toho použít. Představte si, že naším prvním úkolem je získat hodnotu do registru eax. Hledáme pop eax; ret kombinace někde v libovolném modulu v procesu. Jakmile jsme jeden našli, řekněme na 00401f60, vložíme jeho adresu do zásobníku:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 00401f60
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Po spuštění tohoto kódu shellu znovu dostaneme porušení přístupu:

> Access violation - code c0000005 (!!! second chance !!!)
eax=cccccccc ebx=01020304 ecx=7abcdef0 edx=00000000 esi=7777f000 edi=0000f0f1
eip=43434343 esp=000ff6ba ebp=000ff6ff

Procesor nyní provedl následující:

  • Přejít na pop eax instrukce na 00401f60.
  • Vyskočilo cccccccc ze zásobníku na eax.
  • Provedeno ret, objevovat 43434343 do eip.
  • Vyhodil narušení přístupu, protože 43434343 není platná adresa paměti.

Teď si to představte, místo 43434343, hodnota na 000ff6b8 byl nastaven na adresu jiného modulu gadget ROP. To by znamenalo, že pop eax bude popraven, pak náš další gadget. Můžeme takto gadgety spojit dohromady. Naším konečným cílem je obvykle najít adresu API pro ochranu paměti, například VirtualProtect, a označit zásobník jako spustitelný. Potom bychom zahrnuli poslední modul ROP, který provede jmp esp rovná instrukce a spusťte shell kód. Úspěšně jsme obešli DEP!

Za účelem boje proti těmto trikům byl vyvinut ASLR. ASLR zahrnuje náhodně odsazené paměťové struktury a adresy bázových modulů, což ztěžuje odhad umístění gadgetů ROP a API.

V systémech Windows Vista a 7 ASLR randomizuje umístění spustitelných souborů a dll v paměti, stejně jako hromadu a hromady. Když je spustitelný soubor načten do paměti, systém Windows získá čítač časových razítek procesoru (TSC), posune jej o čtyři místa, provede divizní mod 254 a poté přidá 1. Toto číslo se pak vynásobí 64 kB a spustitelný obrázek se načte při tomto posunutí . To znamená, že existuje 256 možných umístění spustitelného souboru. Protože knihovny DLL jsou sdíleny v paměti napříč procesy, jejich kompenzace jsou určeny hodnotou systematického zkreslení, která se vypočítává při spuštění. Hodnota je vypočítána jako TSC CPU, když je funkce MiInitializeRelocations poprvé vyvolána, posunuta a maskována do 8bitové hodnoty. Tato hodnota se vypočítá pouze jednou při spuštění.

Když jsou knihovny DLL načteny, přejdou do oblasti sdílené paměti mezi 0x50000000 a 0x78000000. První DLL, který má být načten, je vždy ntdll.dll, který je načten v 0x78000000 - bias * 0x100000, kde bias je hodnota systematického zkreslení vypočítaná při spuštění. Protože by bylo triviální vypočítat offset modulu, pokud znáte základní adresu ntdll.dll, je pořadí, ve kterém jsou moduly načteny, také randomizováno.

Při vytváření podprocesů je jejich základní umístění zásobníku randomizováno. To se provádí vyhledáním 32 vhodných umístění v paměti a následným výběrem jednoho na základě aktuálního TSC posunutého do 5bitové hodnoty. Jakmile je vypočtena základní adresa, je z TSC odvozena další 9bitová hodnota pro výpočet základní adresy zásobníku. To poskytuje vysoký teoretický stupeň náhodnosti.

Nakonec je umístění haldy a alokace haldy randomizováno. To se počítá jako 5-bitová hodnota odvozená od TSC vynásobená 64 kB, což poskytuje možný rozsah haldy 00000000 to 001f0000.

Pokud jsou všechny tyto mechanismy kombinovány s DEP, nemůžeme provádět shell kód. Je to proto, že nemůžeme spustit zásobník, ale také nevíme, kde budou některé z našich pokynů ROP v paměti. Určité triky lze provést pomocí sáněk nop, aby se vytvořilo pravděpodobnostní vykořisťování, ale nejsou úplně úspěšné a není vždy možné je vytvořit.

Jediný způsob, jak spolehlivě obejít DEP a ASLR, je únikem ukazatele. Toto je situace, kdy hodnota na zásobníku ve spolehlivém umístění může být použita k vyhledání použitelného ukazatele funkce nebo miniaplikace ROP. Jakmile je to hotovo, je někdy možné vytvořit užitečné zatížení, které spolehlivě obchází oba ochranné mechanismy.

Zdroje:

Další čtení:

153
Polynomial

Pro doplnění vlastní odpovědi @ Polynomial: DEP lze ve skutečnosti vynutit na starších počítačích x86 (které předcházejí bitům NX), ale za cenu.

Snadným, ale omezeným způsobem provádění DEP na starém hardwaru x86 je použití segmentových registrů. U současných operačních systémů v takových systémech jsou adresy 32bitové hodnoty v plochém adresním prostoru 4 GB, ale interně každý přístup do paměti implicitně používá 32bitovou adresu a speciální 16bitový registr , nazvaný „segmentový registr“.

V takzvaném chráněném režimu odkazují registry segmentů na interní tabulku („deskriptorová tabulka“ - ve skutečnosti existují dvě takové tabulky, ale jedná se o technickou stránku) a každá položka v tabulce specifikuje vlastnosti segmentu. Zejména typy povolených přístupů a segment = [- velikost. Navíc provádění kódu implicitně používá registr segmentu CS, zatímco přístup k datům používá většinou DS (a přístup k zásobníku, např. S ​​opačnými kódy Push a pop) To umožňuje operačnímu systému rozdělit adresní prostor na dvě části, spodní adresy jsou v rozsahu pro CS i DS, zatímco horní adresy jsou mimo rozsah pro CS. Například se vytvoří segment popsaný CS. mít velikost 512 MB. To znamená, že jakákoli adresa nad 0x20000000 bude přístupná jako data (přečtená nebo zapisovaná pomocí DS jako základní registr), ale pokusy o provedení budou používat CS, v tomto okamžiku CPU vyvolá výjimku (kterou jádro převede na vhodný signál jako SIGILL nebo SIGSEGV, což obvykle znamená smrt procesu, který se dopustil útoku).

(Všimněte si, že segmenty jsou aplikovány na adresní prostor; MMU je stále aktivní, na spodní vrstvě, takže výše popsaný trik je za proces.)

Je to levné: hardware x86 dělá vynucuje segmenty systematicky (a první 80386 to již dělal; skutečně, 80286 již mělo takové segmenty s hranicemi, ale pouze 16bitové posuny) ). Obvykle na ně můžeme zapomenout, protože rozumné operační systémy nastavily segmenty tak, aby začaly s posunem nula a byly dlouhé 4 GB, ale jejich nastavení jinak neznamená žádnou režii, kterou jsme dosud neměli. Jako mechanismus DEP je však nepružný: když je od jádra vyžadován nějaký datový blok, musí jádro rozhodnout, zda se jedná o kód, nebo ne pro kód, protože hranice je pevná. Nemůžeme se rozhodnout dynamicky převádět jakoukoli danou stránku mezi kódovým a datovým režimem.

Zábavný, ale poněkud dražší způsob DEP používá něco, co se nazývá PaX . Abychom pochopili, co to dělá, musíme jít do některých detailů.

Hardware MMU na x86 používá tabulky v paměti, které popisují stav každé 4 kB stránky v adresním prostoru. Adresní prostor je 4 GB, takže existuje 1048576 stránek. Každá stránka je popsána 32bitovou položkou v podtabulce; existuje 1024 dílčích tabulek, z nichž každá obsahuje 1024 záznamů, a existuje jedna hlavní tabulka s 1024 záznamy, které ukazují na 1024 dílčích tabulek. Každá položka říká, kde směřovaný objekt (podtabulka nebo stránka) je v RAM, nebo zda je tam vůbec a jaká jsou jeho přístupová práva. Kořen problému spočívá v tom, že přístupová práva se týkají úrovní oprávnění (kód jádra vs. uživatelská země) a pouze jeden bit pro typ přístupu, takže umožňují „čtení a zápis“ nebo „pouze pro čtení“. „Provedení“ se považuje za druh přístupu ke čtení. Proto MMU nemá ponětí o tom, že by „provádění“ bylo odlišné od přístupu k datům. To, co je čitelné, je spustitelné.

(Od doby Pentium Pro, v předchozím století, procesory x86 vědí o jiném formátu tabulek nazvaném PAE . Zdvojnásobuje velikost záznamů, což ponechává prostor pro adresování více fyzické paměti RAM a také přidání bitů NX - ale tento konkrétní bit byl implementován hardwarem až kolem roku 2004.)

Existuje však trik. RAM je pomalý. Chcete-li provést přístup do paměti, musí procesor nejprve přečíst hlavní tabulku, aby našel podtabulku, kterou musí konzultovat, a poté provést další čtení do této podtabulky a pouze v tomto okamžiku ví procesor, zda by měl být přístup do paměti povolen či nikoli, a kde ve fyzickém RAM) jsou přístupná data skutečně. Jedná se o přístupy ke čtení s plnou závislostí (každý přístup závisí na hodnota čtená předchozím), takže se vyplatí plná latence, která může na moderním CPU představovat stovky hodinových cyklů. CPU tedy obsahuje specifickou mezipaměť, která obsahuje naposledy přístupnou tabulku MMU tabulka) Tato mezipaměť je Translation Lookaside Buffer .

Od 80486 dále nemají CPU x86 jeden TLB, ale dva. Ukládání do mezipaměti pracuje na heuristice a heuristika závisí na přístupových vzorcích a přístupové vzorce pro kód se obvykle liší od přístupových vzorců pro data. Inteligentní lidé ve společnosti Intel/AMD/other považovali za užitečné mít k dispozici TLB věnovaný přístupu k kódu (provádění) a další pro přístup k datům. Navíc má 80486 operační kód (invlpg), který může odstranit konkrétní položku z TLB.

Myšlenka je tedy následující: aby dva TLB měly odlišné pohledy na stejnou položku. Všechny stránky jsou v tabulkách (v RAM) označeny jako „nepřítomné“, takže při přístupu dochází k výjimce. Jádro zachycuje výjimku a výjimka obsahuje některá data o typu přístupu, zejména zda se jednalo o provedení kódu nebo ne. Jádro pak zneplatní nově načtenou položku TLB (ta, která říká „nepřítomná“), poté ji vyplní do RAM s některými právy, která umožňují přístup, pak vynutí jeden přístup potřebného typu ( buď načtení dat nebo provedení kódu), který vloží záznam do odpovídajícího TLB, a pouze ten jeden. Jádro pak rychle nastaví položku v RAM zpět na absent) a nakonec se vrátí do procesu (zpět k opětovnému pokusu o opcode, který spustil výjimku).

Čistým efektem je, že když se provádění vrátí zpět do procesního kódu, TLB pro kód nebo TLB pro data obsahuje příslušnou položku, ale druhý TLB ne a will not protože tabulky v RAM stále říkají "nepřítomné". V tomto okamžiku je jádro v pozici rozhodnout, zda povolit spuštění, nebo ne, nezávisle na tom, zda umožňuje přístup k datům nebo ne. Může tak vynucovat sémantiku podobnou NX.

Ďábel skrývá v detailech; v tomto případě existuje prostor pro celou legii démonů. Takový tanec s hardwarem není snadné implementovat správně. Zejména na vícejádrových systémech.

Režie je následující: když je proveden přístup a TLB neobsahuje relevantní položku, musí být zpřístupněny tabulky v RAM), což samo o sobě znamená ztrátu několika stovek cyklů. náklady, PaX přidává režii výjimky a řídící kód, který vyplňuje správný TLB, čímž mění „několik set cyklů“ na „několik tisíc cyklů“. Naštěstí chybí TLB správně. Lidé PaX tvrdí, že mají měřeno zpomalení o 2,7% na velké kompilační úloze (záleží to však na typu CPU).

Bit NX činí toto vše zastaralým . Upozorňujeme, že sada patchů PaX obsahuje také některé další funkce související s bezpečností, například ASLR, která je nadbytečná u některých funkčnost novějších oficiálních jader.

40
Thomas Pornin