Return Oriented Programming (ROP) je technika, která je dnes běžně používaná pro tvorbu exploitů. Její znalost je nutností nejen pro specialisty zabývající se ofenzivní bezpečností, ale i pro členy blue týmů či forenzní specialisty.
Hovav Shacham ji popsal již v roce 2007 jako reakci na implementaci různých variant omezení vykonavatelnosti paměťových oblastí, které měly za cíl mitigovat zneužití zranitelností typu Buffer Overflow. V tomto článku popisujeme, jak tato ochrana funguje a jak ji s pomocí ROP obejít a vykonat libovolný kód.
Než se do ROP pustíme, nejprve trochu teorie.
V tomto případě platí staré dobré přísloví, že jeden obrázek vydá za tisíc slov:
Pro naší potřebu je především důležité si uvědomit, kde je alokovaný stack, a že roste směrem k nižším adresám. Z toho později budeme vyvozovat, že neošetřený uživatelský vstup může přepsat na stacku umístěné řídící struktury. Textový segment, tj. oblast kde je uložen náš program, a ze kterého budeme v další části čerpat gadgety pro náš exploit, se na tomto obrázku nachází na adrese 0x08048000 a roste směrem k vyšším adresám.
Obrázek popisuje 32-bit systém (dále se budeme věnovat již systémům 64-bitovým) a starý pohled na svět před Meltdown a Specter zranitelnostmi :), ale hlavní principy zůstavají platné.
Buffer overflow je bezpečnostní zranitelnost, která je způsobená tím, že vývojář dané aplikace nedostatečně ošetřuje vstupy, které příjme od uživatele. Problém se objeví v případě, že je vstup větší než oblast paměti, do které se následně ukládá.
Následkem toho dojde k poškození řídících struktur (RSP, RBP, ...), aplikace se pokusí o přístup do paměti, kterou nemá přidělenou, a jádro operačního systému ji ukončí s chybou segmentace (segmentation fault). Útočník však této situace může využít k vykonání libovolného kódu s právy dané aplikace.
Proč je tomu tak, naznačuje následující schéma:
Útočník vhodnou manipulací vstupu uloží do paměti kód, který chce vykonat (protože data jsou instrukce a instrukce jsou data). Pak přepíše návratovou adresu předchozí funkce, aby směřovala na oblast, která obsahuje dříve implantovaný kód a ten je následně vykonán.
V nejjednoduším případě se jedná o kód umístěný v bufferu na stacku, jak ukazuje následující schéma:
Na moderních platformách lze hardwarově vynutit nevykonavatelnost vybraných oblastí paměti. Tato vlastnost se používá i k ochraně stacku proti zneužití zranitelnosti typu buffer overflow. Příslušná paměť je označena příznakem 63 bitu na úrovni paměťové stránky, jak ukazuje následující schéma:
Jak tedy vykonat kód, když ho nelze spustit přímo? Jednou z technik, kterou můžeme v této situaci použít je tzv. Return Oriented Programming.
Tato technika je založená na tom, že na stack uložíme odkazy na speciální sekvence instrukcí (tzv. gadgety), které jsou již přítomné v textovém segmentu paměti procesu (tedy ze spuštěné aplikace). Posloupnost gadgetů a jejich parametrů nazýváme ROP chain.
Jak již název techniky napovídá, vždy musí být splněna podmínka, že daná sekvence instrukcí končí instrukcí RET. To, co instrukce RET na platformě x86 dělá, lze vyjádřit následujícím pseudokódem:
Zjednodušeně řečeno, instrukce RET nám zajistí vrácení toku na námi definovaný stack a vykonání dalšího gadgetu z ROP chainu. Tímto způsobem vykonáme postupně celý ROP chain.
Jak vypadá vykonávání je naznačeno na následujícím schématu:
V některých případech je důležité zachovat předchozí stack framy (zvláště ve složitějších programech). Pak je potřeba rychle (tedy s minimálním počtem instrukcí) přesměrovat tok do jiné části paměti, kde je již připravený náš ROP chain. Této technice se říká stack pivoting.
Významnou výhodou techniky ROP je jednak to, že máme k dispozici Turingovsky úplnou instrukční sadu, a pak také to, že architektura x86 nemá zarovnání instrukcí. Proto můžeme vygenerovat velké množství gadgetů i z relativně malého programu.
Jak tato technika reálně funguje, si ukážeme na následujícím příkladu. Pro tyto potřeby využijeme aplikaci ropme. Její základní funkcí je, že přečte uložený konfigurační soubor, zobrazí konfigurační parametry uživateli, a na jejich základě provede další akce.
Významnou výhodou pro útočníka je to, že aplikace běží s příznakem SETUID. Další zajímavé vlastnosti této aplikace zjistíme pomocí nástroje file a checksec.
V tomto případě je pro nás velmi zajímavé, že aplikace je zkompilovaná staticky bez PIE (staticky linkovaný spustitelný soubor je v GCC s PIE nekompatibilní). Zranitelnost je obsažená ve funkci, která načítá konfigurační soubor, a ukládá ho bez řádné validce na délku do vývojářem definovaného uložiště.
Situaci nám ještě usnadňuje to, že daný stack není chráněn kanárkem. Takovouto zranitelnost je možné odhalit různými způsoby - nejčastěji pomocí fuzzingu nebo statické analýzy kódu. Pro zkrácení článku se této oblasti nebudeme do hloubky věnovat.
První, co musíme zjistit, je, zda aplikace takovouto zranitelností opravdu trpí:
V tomto případě je jasné, že vstup není dostatečně ošetřený a aplikace přistoupí k části paměti, ke které nemá oprávnění. A tak nám už jen zbýva napsat exploit, kterým bychom získali na systému privilegovaný shell.
Pro další účely využijeme jeden z populárních nástrojů pro reverzní inženýrství - Radare2. Nejprve potřebujeme zjistit, kde je návratová adresa na stacku, tj. kde bude začínat náš ROP chain. K tomuto účelu můžeme použít cyklickou sekvenci, např. de Bruijnovu, kterou vygenerujeme pomocí nástroje ragg2 (obdobně by fungoval libovolný pattern generátor, jako je pattern_create.rb v Metasploitu).
Následně zjistíme pomocí Radaru kýžený offset:
Námi hledaný offset je tedy 56 znaků, za kterými již můžeme návázat vytvořený ROP chain (za offsetem již následuje návratová adresa).
K tomuto účelu je ideální použít některý z nástrojů, který nám umožní vygenerovat katalog gadgetů a umožní nám v něm i pohodlně vyhledávat. Mezi populární nástroje patří např. Ropper nebo ROPgadget. Oba nástroje mají i jednoduchý generátor ROP chainu pro execve, Ropper přidává i mprotect/virtualprotect.
Pro naši potřebu vytvoříme jednoduchý execve ROP chain, který vykonává přibližně následující kód:
Pro sestavení systémového volání můžeme čerpat například z této reference. Python skript pro vygenerování exploitu vypadá následovně:
Vykoušíme jak nám náš exploit funguje:
Podle očekávání se nám spustil shell. Práva ale máme pouze jako lokání uživatel, což je pro SUID binárku trochu škoda. Proto upravíme náš ROP chain, aby nám ještě navýšil práva. K tomu nám postačí dvě jednoduché systémové volání setuid a setgid.
Výsledný kód vypadá následovně:
A výsledný ROP chain je následující:
Vygenerujeme exploit a vyzkoušíme funkčnost:
Tím jsme dosáhli našeho cíle a získali privilegovaný shell na cílovém systému.
Na této ukázce jsme viděli, že samotnou ochranu v podobě NX stacku není pro útočníka příliš těžké obejít. I se zapnutým ASLR je bez aktivního PIE jednoduché využít zranitelnosti typu stack overflow.
Jak už to tak v oblasti bezpečnosti bývá, je pro mitigaci rizik spojených nejen s paměťovými zranitelnostmi potřeba zajistit ochranu spustitelných souborů pomcí většího množství technik na různých úrovních, tj. NX, ASLR, PIE, RELRO, Stack Cookies, CFG, sandboxing v rámci aplikace aj.
Autory článku jsou Petr Medonos a Anna Medonosová.