Stack Buffer Overflow – Limitace Prostoru
Dnešní článek volně pokračuje na svou první variantu, kde jsme se zabývali vytvářením exploitů v nepříznivých podmínkách (za pomoci omezené znakové sady), avšak nyní nebude překážka na straně použitelných znaků, ale jak název napovídá, omezení se týká dostupného prostoru pro náš spustitelný kód. Opět není principem článku popisovat obecné techniky vytváření exploitů a zneužívaní zranitelnosti Buffer Overflow (BoF), nýbrž nastínit možný způsob, jak se k tvorbě exploitů v omezeném prostředí stavět.
Prerekvizity:
- Základní znalost principu útoků typu Buffer Overflow
- Základní znalost tvorby exploitů
- Základní znalost stacku, registrů, assembler instrukcí a opcodes
Různých způsobů, jak se vypořádat s nedostatkem prostoru existuje více, my se v článku podíváme a detailněji popíšeme dvě často používané metody: Pomocí načtení externí knihovny a opětovné použití síťového spojení (funkce recv).
Pro vytvoření payloadu (škodlivého kódu) k navázaní zpětného spojení, tzv. „reverse shell“, je zapotřebí několik stovek bytů. Na obrázku níže je znázorněný typický příklad, kdy jsou nepovolenými znaky null byte a ve Windows prostředí odřádkování (\x00, \x0A a \x0D). K nalezení nejmenšího možného payloadu vygenerovaného nástrojem msfvenom je zapotřebí 346 bytů:
Existují i jiné způsoby jak payload vytvořit „na míru“ pro konkrétní verzi operačního systému, a tím velikost snížit, avšak velikost bude s největší pravděpodobností stále větší jak 200 bytů (v případě dynamicky hledaných potřebných offsetů k příslušným funkcím / metodám).
Jestliže zranitelné místo v aplikaci neposkytuje dostatečně velký prostor, řekněme pouze 100 bytů, k podvrhnutí škodlivého kódu je zapotřebí využít některé z metod obcházející výše zmíněnou limitaci.
Načtení externí knihovny
První popisovaná metoda využívá faktu, že v dnešní době vývojáři při tvorbě aplikací, prakticky téměř vždy, používají externí / systémové knihovny, díky čemuž jsou dané knihovny při běhu aplikace načteny pomocí funkce LoadLibraryA a následně spuštěny. LoadLibraryA funkce, dle dokumentace https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya, očekává na vstupu jeden parametr:
Hodnota parametru lpLibFileName může být libovolná cesta ke knihovně (.dll) nebo k spustitelnému souboru (.exe), která je pro Windows rozpoznatelná – může se jednat o cestu absolutní, relativní, ale také i o cestu síťovou (UNC path). V omezeném prostoru přijdou také vhod některé vlastnosti zmíněné funkce:
- Pokud není definována přípona souboru, defaultně je automaticky přidána přípona knihovny (.DLL)
- Funkce akceptuje, v případě IP adresy, desítkovou notaci neobsahující tečky
K demonstraci použití funkce pro načtení knihovny využijeme jednoduchou aplikaci KnFTP Server ve verzi 1.0.0 obsahující známou zranitelnost BoF v příkazu „USER“. Aplikace byla zvolena k ukázkovému účelu pro její snadnou exploitaci a zároveň je prostor, jak si ukážeme dále, limitován.
Všechny následující kroky byly provedeny v rámci testovacího prostředí na operačním systému Windows 7 x64 s využitím nástroje Immunity Debugger.
Po nalezení potřebného offsetu k přepsání EIP registru, vypořádání se s mechanismem Structured Exception Handling (SEH) a nepovolenými znaky, zjistíme, že v našem případě máme k dispozici necelých 80 bytů volného prostoru ke spuštění payloadu (může se lišit verzí operačního systému).
K provolání knihovní funkce LoadLibraryA je nezbytné na zásobník vložit adresu paměti obsahující cestu k souboru – již zmiňovaný parametr lpLibFileName, tzn. cestu ke knihovně, která má být funkcí načtena. Vzhledem k faktu, že ukazatel zásobníku (ESP registr) má hodnotu o hodně nižší, než je hodnota EIP registru (aktuálně provolávaná instrukce), není potřeba zásobník zarovnávat / posouvat – při načtení knihovny nedojde k přepsání našeho payloadu.
K minimalizaci délky UNC cesty – SMB server hostující knihovnu, kterou budeme exploitem načítat – byl zvolen nejkratší možný název adresáře a samotné knihovny (jednoznakové názvy):
- \\192.168.154.128\s\x.dll
Poznámka: IP adresa je námi kontrolovaný stroj
Aby bylo možné řetězec znaků (cestu) vložit na zásobník, je potřeba ji nejprve rozdělit do bloků po 4 bytech v reverzním pořadí. Vzhledem k délce cesty je nutné na konec přidat padding, v našem případě tři mezery:
K ukončení řetězce je nezbytné na zásobník vložit null byte k terminaci stringu, ten je ovšem zakázaným znakem. Z obrázku výše je patrné, že můžeme např. využít registr EAX, který v danou chvíli obsahuje nulovou hodnotu. Nejprve tedy provoláme instrukci PUSH EAX, následně před každý jednotlivý řádek se 4 byty cesty přidáme instrukci PUSH a na závěr vložíme lokaci řetězce na zásobník (instrukce PUSH ESP):
Dalším krokem je získání adresy lokace samotné funkce LoadLibraryA z knihovny kernel32. V nástroji Immunity Debugger stiskem pravého tlačítka myši v CPU okně zvolíme možnost „Search for -> All intermodulars calls“ v modulu knftpd – neboli přímo v aplikaci z důvodu větší přenositelnosti na jiné verze operačních systémů, neboť binární soubor nepoužívá žádné bezpečnostní mechanismy.
Na adrese 00403537 se nachází námi hledána funkce, respektive volání „reference“ funkce LoadLibraryA. Zobrazením adresy v okně s instrukcemi zjistíme, že se jedná o instrukci CALL s relativním offsetem 955:
Druhý, mnohem jednoduší, způsob k zjištění adresy knihovní funkce LoadLibraryA je prostý dvojklik v nástroji Immunity Debugger na příslušnou instrukci:
Při tvorbě exploitu je možné tedy použít dvě volání:
- JMP 00403537
- CALL 00403E8C
Jelikož obě adresy obsahují nepovolený znak (\x00), nepřináší ani jedna z možností žádnou výhodu a můžeme využít libovolné volání. V našem exploitu bude použito CALL 00403E8C:
V tuto chvíli je prakticky exploit již připravený, zbývá jen vygenerovat DLL knihovnu x.dll s požadovaným shellcodem a poskytnout ji na námi kontrolovaném SMB serveru. Pro demonstraci exploitace bude spuštěna na stroji oběti kalkulačka.
Vygenerování knihovny x.dll:
Spuštění SMB serveru:
V průběhu exploitace je síťová cesta ke knihovně x.dll vložena na zásobník, zavolána funkce LoadLibraryA a následně spuštěna kalkulačka:
Celková velikost payloadu k načtení a spuštění externí knihovny je 47 bytů:
Velikost exploitu je možné dále zmenšit např. vynecháním přípony (díky vlastnosti funkce loadLibraryA, která sama příponu doplní), čímž dojde k ušetření 5 bytů. Zde je vhodné upozornit na fakt, že systém doplní příponu velkými písmeny (.DLL) a vhledem k tomu, že námi kontrolovaný stroj hostující SMB server běží na operačním systému Linux (case sensitive), je potřeba, aby v sdílené složce příslušný soubor existoval, např. x.DLL.
Velmi velkou úsporu místa skýtá případ, kdy jsou oba stroje (útočník i oběť) ve stejné síti a není překážkou, že dojde k omezení i dalších legitimních uživatelů. S využitím nástroje Responder můžeme podvrhnout odpovědi na broadcastové dotazy ohledně neexistujícího hostname, a tím „přinutit“ stroj oběti k navázání komunikace s naším SMB serverem. Popsaným způsobem můžeme zvolit libovolnou cestu, jejíž délka bude modulární 4, např. \\32\s\x. Výsledný payload má pak velikost pouze 22 bytů.
Opětovné použití síťového spojení
Druhý popisovaný způsob využívá vlastnosti socketů, respektive jejich znovupoužití. V případech, kdy aplikace akceptuje síťové spojení, je možné použít techniky zvané Socket Reuse – pokud máme k socketu přístup, můžeme libovolně provolávat odpovídající funkce pro odesílání a přijímání „dat“ (funkce send a recv) a provádět síťové operace, avšak to bohužel nelze vždy. Typické příklady jsou, kdy ke spuštění (triggování) exploitu dojde až po uzavření socketu, nebo není možné identifikátor socketu dynamicky získat, ale o tom více později. Pro účely exploitace je důležitá pouze funkce recv, ve své podstatě donutit aplikaci k přijmutí dalších „dat“.
K demonstraci techniky Socket Reuse bude použita aplikace Simple Web Server od společnosti PMSoftware – byť prostor není prakticky nijak omezen, byla aplikace zvolena vzhledem ke svým vlastnostem při využívání socketů, včetně výhod i limitací.
Dle oficiální dokumentace, https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv, funkce recv očekává na vstupu následující parametry:
Ve stručnosti popis daných parametrů je následovný:
- První argument je tzv. „socket file descriptor“, dále jen „identifikátor socketu“, neboli identifikátor síťového spojení
- Druhý argument je ukazatel do části paměti, kde budou přijatá „data“ socketem uložena
- Třetí argument je délka, respektive velikost „dat“, kterou má funkce očekávat
- Poslední parametr je příznak ovlivňující chování funkce
Celý exploit bude rozdělen do tří částí:
- Stage 1 – inicializační exploitace, ovládnutí EIP registru a kontrola běhu programu (není předmětem článku)
- Stage 2 – nutné akce k opětovnému použití síťového spojení
- Stage 3 – payload obsahující spustitelný kód (shellcode)
Po úvodním spuštění a zobrazení „All intermodular calls“ (stejný princip jako v případě načtení externí knihovny) si můžeme všimnout, že modul, který obhospodařuje síťové spojeni je „swscore.dll“. Ve výpisu je patrné, že aplikace provolává funkci „CALL recv“ hned na několika místech, v našem případě na adresách: 6D58201F, 6D5820DA, 6D582F22, 6D587660, 6D58E0E6, nicméně v době běhu exploitu je aplikací využita jen poslední zmíněná adresa, tedy 6D58E0E6. Všechna uvedená volání však odkazují na knihovní funkci (recv) na stejné adrese 6D58D4DC:
Naším cílem je tedy zjistit identifikátor spojení, nalezení vhodného místa k uložení následných „dat“, provolání funkce recv a odeslání třetí části exploitu do již existujícího socketu – tedy části kódu, který má být spuštěn (Stage 3).
Vložením breakpointu na adresu 6D58E0E6 můžeme prozkoumat konkrétní hodnoty parametrů, které jsou funkci recv předávány v případě legitimní komunikace:
Na vrcholu zásobníku se nachází identifikátor socketu, jediný parametr, který nás zajímá, neboť ostatní budeme nastavovat podle našich potřeb, v našem případě tedy 0x134. Zmíněný identifikátor se bude měnit podle operačního systému a ve většině případů i při „každém“ spuštění aplikace. Je tedy potřeba identifikátor získat dynamicky. Vrátíme-li se zpět k instrukcím, které jsou volány před instrukcí CALL, zjistíme, že identifikátor socketu je nastaven pomocí příkazu MOV EAX,DWORD PTR DS:[EBX], následujícím příkazem je pak hodnota vložena na zásobník:
Hodnota registru EBX je 00AAFE64:
Zobrazením „Memory map“ v nástroji Immunity Debugger zjistíme, že se jedná o část paměti, jejíž adresa je náhodně přidělována operačním systémem při spuštění aplikace, jinými slovy bude se pokaždé lišit – lze jednoduše ověřit ponecháním breakpointu před voláním instrukce CALL a opětovném spuštění aplikace a navázání nového síťového spojení.
V době běhu inicializačního exploitu (způsobující přepsání EIP registru a ovládnutí běhu programu, tedy Stage 1) zjistíme, že registr EBX byl přepsán naším payloadem:
Vzhledem k faktu, že registr EBX již neukazuje na potřebné místo v paměti, kde se nachází identifikátor socketu, potřebná adresa se v paměti nikde nenachází a ani není možné vypočítat relativní vzdálenost na základě hodnoty jiných registrů, využijeme vhodné vlastnosti aplikace – identifikátor socketu je vždy stejná hodnota, tedy 0x134 (i po restartu stroje). Jedná se celkem o unikátní stav, neboť v drtivé většině případů se bude identifikátor pokaždé lišit. Na konci článku si ukážeme i trochu krkolomný způsob, jak identifikátor získat dynamicky, pro jednoduchost v tuto chvíli použijeme k tvorbě exploitu (Stage 2) pevně danou hodnotu 0x134.
Jelikož ukazatel zásobníku (ESP registr) má hodnotu vzdálenou od našeho exploitu, nehrozí tedy přepsání instrukcí a můžeme začít vkládat na zásobník příslušné parametry. Pro rekapitulaci, na zásobník je třeba vložit 4 parametry (v reverzním pořadí). Nejprve tedy příznak funkce recv. Jelikož není potřeba chování funkce měnit, vložíme na zásobník hodnotu 0. Z obrázku výše je patrné, že můžeme použít registr EAX, který již hodnotu 0 obsahuje:
Druhý parametr je délka payloadu (Stage 3), který budeme funkci recv posílat. Pro demonstrační účely použijeme vygenerovaný shellcode nástrojem msfvenom pro spuštění kalkulačky, který má kolem 220 bytů. Abychom si nechali rezervu, nastavíme parametr délky na hodnotu 512 bytů = 0x200. Znovu použijeme registr EAX, respektive jeho rozpadu na registry AX (16 bytů), AH (následujících 8 bytu), AL (posledních 8 bytů) k vyhnutí se v instrukci ADDnull bytu:
Další parametr je adresa v paměti, kde bude poslaný payload (Stage 3) uložen. Jelikož ovládáme EIP registr, nabízí se vložit adresu paměti v námi kontrolovaném bufferu – po návratu z funkce recv by se mohl rovnou spustit payload (Stage 3) obsahující shellcode pro spuštění kalkulačky. Bohužel v našem případě nezbývá v alokovaném bufferu dostatek místa, pouze přibližně 250 bytů:
Zvolíme tedy adresu nacházející se vysoko nad aktuální polohou v paměti (028AFEEF). Jak je patrné z obrázku výše, k tomuto účelu poslouží registr EDX (028AF62C) – abychom prováděli co nejméně operací, a tím snížili velikost exploitu (Stage 2) na minimum. Zároveň provedeme „zálohu“ hodnoty registru EDX pro budoucí použití – při volání funkce recv dojde k přepsání registru EDX:
Poslední parametr je identifikátor socketu, jak bylo již nastíněno použijeme pevně danou hodnotu 0x134. K výpočtům můžeme opět využít registr EAX (aktuálně s hodnotou 0x200):
V danou chvíli máme na zásobníku všechny potřebné parametry. Stačí tedy už jen provolat funkci recv na adrese 6D58D4DC:
Na konec exploitu vložíme ještě skok na adresu nově přijatého payloadu (Stage 3) – druhý parametr funkce recv / třetí vkládaný parametr na zásobník:
Kompletní exploit pro vložení potřebných parametrů a zavolání funkce recv (Stage 2) je tedy následovný:
Jak můžeme vidět na obrázku níže, všechny instrukce se provedly dle představ a na zásobníku jsou uloženy požadované hodnoty:
Abychom měli jistotu, že veškeré operace se stihly řádně spustit, vložíme sleep na 5 vteřin mezi Stage 2 a Stage 3. Následně odešleme 512 bytový payload (Stage 3) obsahující instrukce se softwarovým breakpointem (\xCC):
Jak můžeme vidět, vše zafungovalo správně a v Immunity Debuggeru se běh programu zastavil na instrukci \xCC – jelikož došlo k vypnutí a znovu zapnutí aplikace i Immunity Debuggeru, hodnota adresy původně v EDX registru se liší:
Posledním krokem je tedy záměna payloadu (Stage 3) za vygenerovaný shellcode pro spuštění kalkulačky. Celková velikost exploitu (Stage 2) činí 23 bytů:
Dynamické nalezení identifikátoru socketu
Na závěr si ještě ukážeme, jak v konkrétním případě Simple Web Serveru naleznout identifikátor socketu dynamicky a nespoléhat se pouze na statickou hodnotu. Vložením breakpointu na instrukci nastavující identifikátor, zjistíme, že poslední dva byty jsou neměnné – hodnota registru EBX (XXXXFE64):
Po opětovném spuštění aplikace a zastavení běhu programu na stejném místě dojde v registru EBX ke změně pouze prvních dvou bytů (na obrázku níže jen v druhém bytu):
Jak již bylo v článku zmíněno, potřebná hodnota (registru EBX) se nikde v paměti ani na stacku nevyskytuje. Prozkoumáním konce alokované paměti si však můžeme všimnout, že se zde objevuje adresa jiné instrukce, konkrétně 009DFE20. Při každém spuštění se hodnota též změní, ale opět jen v prvních dvou bytech a to na stejnou hodnotu jako mají první dva byty na obrázku výše – jelikož jsou poslední dva byty neměnné (FE64) a první dva byty můžeme vyčíst ze specifické části paměti, jsme schopni identifikátor socketu nalézt i dynamicky.
V době běhu exploitu (Stage 2) ukazuje vrchol zásobníku v registru ESP na adresu 0280FED0 (hodnota se bude lišit). Naším cílem tedy bude získat hodnotu DWORD z adresy 0280FFAC a následně přičíst 0x44 –> XXXXFE20 + 0x44 = XXXXFE64, neboli adresa identifikátoru spojení:
Celková velikost exploitu (Stage 2) se však zvýší na 31 bytů.
Závěrem
V článku jsme si představili dva způsoby jak se popasovat s limitací prostoru pro kód, který má být spuštěn – načtením externí knihovny a znovupoužitím síťového spojení. Jak je vidět, i zdánlivě velice omezující překážku lze, relativně snadno, překonat. Část paměti o velikosti pár desítek bytů je plně dostačující ke spuštění libovolného kódu, a tím pádem ke kompromitaci zařízení oběti.
V dnešní době se zranitelnost Buffer Overflow stále napříč systémy hojně vyskytuje, i když bylo (a nejspíše i bude) vyvinuto nespočet ochran / limitací, vždy se najde způsob jak je obejít.