Sociálne siete

SecIT.sk na Facebooku SecIT.sk na Google+ SecIT.sk na Twitteri SE

Podporte nás


V prípade, že Vám obsah nášho portálu niekedy nejakým spôsobom pomohol, či bol pre Vás prínosom prosím podporte jeho chod ľubovoľnou čiastkou. Ďakujeme!

Prihlásenie

Štítky

Vyhľadávanie

You are here

Domov
Upozornenie: Obsah je licenčne chránený a bez písomných súhlasov autora článku a vlastníka webovej stránky nesmie byť v žiadnej forme ďalej kopírovaný a šírený v pôvodnom, či v akokoľvek upravenom stave.

Buffer Overflow – Exploitace od úplného začátku

Po velmi dlouhé době vyšlo zase něco málo času, a tak jsem se rozhodl, že napíši o známém tématu, a to o zneužitelné chybě na principu přetečení lokálního zásobníku (buffer overflow – bof).

UPOZORNĚNÍ: NÁSLEDUJÍCÍ DOKUMENT JE URČEN POUZE KE STUDIJNÍM ÚČELŮM. ZA JAKÉKOLIV ZNEUŽITÍ TOHOTO DOKUMENTU NENESE AUTOR ŽÁDNOU ZODPOVĚDNOST. ČLÁNEK NESMÍ BÝT KOPÍROVÁN BEZ SVOLENÍ AUTORA.

Na toto téma bylo napsáno mnoho článků, a tak mi nepřišlo nijak odůvodnělé psát nějaký další. Důvod mi však poskytl jeden z kolegů, který měl neurčité problémy uvést celý koncept bof do chodu, i když několik těchto článků četl. Důvod těchto problémů byl velmi prostý – některé ze stěžejních informací jsou podány jako holá fakta a nejsou nijak ozřejměna, popřípadě jsou posunuty někam do pozadí, protože pro jejich vysvětlení by bylo zapotřebí znát celý problém hlouběji.

Paměť, zásobník a assembler

V této teoretické části si ukážeme, na čem vlastně celý koncept Bofu staví. Abychom mohli pokračovat, musíme si vysvětlit několik zásadních věcí. Zběhlí assembleristé tuto část mohou přeskočit.

Stack / Zásobník

Stackem rozumíme paměť založenou na datové struktuře typu LIFO zásobník, kdy na první prvek v tomto zásobníku ukazuje registr ESP. Tento zásobník je jediný pro celý proces. Ukládají se do něj návratové adresy skoků, lokální proměnné a další věci. Abychom mohli demonstrovat základní funkce a využití stacku, je nutné představit a vysvětlit základní assembleří instrukce. Důležitým poznatkem je i to, že se zásobník zaplňuje seshora, ukazatel ESP tak při prázdném zásobníku ukazuje na maximální adresu stacku. Přidáním prvku se ukazatel ESP snižuje (dokud nedojde ke spodní hranici - stack underrun).

PUSH parametr:
Tato instrukce má za úkol vložit do stacku hodnotu stanovenou parametrem. Technicky vzato, instrukce PUSH od registru ESP odečte velikost DWORDu (na 32bit systému 4 byty, které se do stacku vždy ukládají), čímž ESP ukazuje na zatím neinicializovanou hodnotu stacku, se kterou může dělat, co chce – sem právě uloží hodnotu stanovenou parametrem. Instrukce PUSH se tak dá nahradit instrukcemi ADD (add x, y – přičte k x y) a MOV (mov x, y – do x uloží y).

POP parametr:
Inverzní instrukce k PUSH, tedy POP, má za úkol odebrat ze zásobníku první hodnotu a vložit ji na místo specifikované parametrem. Technicky funguje tak, že získá hodnotu, na kterou ukazuje ESP, a pak jednoduše k ESP přičte velikost DWORDu. Tato instrukce je tedy nahraditelná instrukcemi MOV a ADD.

Příklad (ESP začíná na 100):

PUSH 1
PUSH 2
PUSH 3

	_________________________________________________
	|Hodnota	|	Adresa			|
	|3		|	88	

Je nutné si tyto základní instrukce zažít a plně jim porozumět, hlavně jak dokáží ovlivnit stack / ukazatel na stack. Nyní si vysvětlíme, jak fungují skoky a volání.

JMP parametr:
Instrukce JMP přemístí ukazatel na aktuálně prováděnou instrukci (EIP) na základě parametru.

Příklad:

1:	push x
2:	push y
3:	jmp 6		//skočí na řádek 6
4:	…
5:	…
6:	pop eax

Hlavní pro nás je, že instrukce JMP nijak nemodifikuje stack. Prostě skočí. Otázkou nyní je, jak vytvořit volání nějaké naší funkce, a to včetně předání parametrů. Tzn. budeme mít náš určitý kód a v něm budeme chtít skočit do naší funkce a odtud se vrátit zpět. To můžeme provést velmi jednoduše:

1:	push y
2:	push x
3:	push 5
4:	jmp nase_funkce
5:	… pokračujeme

Nyní máme ve stacku hodnoty 5, x, y. ESP ukazuje na hodnotu 5:

Stack:
_________________________________________________
|Hodnota	|	Adresa			|
|5		|	88	

Nase_funkce je velmi jednoduchá funkce, která sečte parametry x a y, na které se odkazuje relativně k ESP (viz schéma stacku výše), výsledek je uložen do EAX. Následně přičte k ESP 4 (tzn., že ESP nyní ukazuje na parametr x) a provede skok na řádek stanovený na předchozí první hodnotě stacku ([ESP-4]), což je „řádek 5“. Pokud bychom nepřičetli k ESP 4 a provedli skok na [ESP], museli bychom v místě, kde naši funkci voláme, počítat s tím, že musíme odstranit i návratovou adresu, aby zůstal stack konzistentní, což je zcela a nezbytně nutné (pokud ovšem nemáme nějaký záměr). Takto ve stacku po návratu z funkce zůstanou pouze pushnuté parametry. Ve většine jazyků však odstraňuje zavolaná funkce i parametry (není tomu tak např. u konvence CDECL používané jazykem C).

CALL parametr:
Instrukce CALL volá adresu specifikovanou parametrem. Dělá to tím způsobem, který jsme my provedli ručně. Do stacku uloží návratovou adresu rovnající se adrese další instrukce za CALLem a provede skok.

RET parametr:
Instrukce RET je určena na návrat z volání funkce a odebrání jejích parametrů ze stacku. Nejprve skočí na první hodnotu ve stacku (tj. [ESP]) a následně odebere n bytů ze stacku (kde n je specifikováno parametrem). Instrukce RET je tak nahraditelná instrukcemi ADD a JMP.

Příklad (ESP = 100, stack je prázdný):

10:	PUSH 2
11:	PUSH 1
12:	CALL moje_fce
13:	...

Stack:
_________________________________________________
|Hodnota		|	Adresa		|
|13 (návr. Addr)	|	88 

Po provedení RET 8 je ESP = 100 a stack je prázdný – návratová adresa a 8 bytů (2x dword) byly smazány. Adresa právě prováděné instrukce EIP = 13.

Lokální proměnné

Tato část je stěžejní - právě na implementaci lokálních proměnných ve stacku stojí celý princip bofu. Uvidíte, že nejde vůbec o nic složitého.

Úvodní otázkou je, kam vlastně ve funkci ukládat lokální proměnné. My už víme, že vše, co je pod ESP, jsou neinicializované a nepoužité hodnoty, se kterými můžeme dělat vše, co chceme, a naopak, co je výše nebo rovno ESP, jsou důležité věci, jako návratové adresy, parametry a další. Není tedy nic jednoduššího, než použít pro lokální proměnné právě stack, konkrétně nepoužité hodnoty stacku pod ESP.

Stack:
_________________________________________________
|Hodnota	|	Adresa			|
|[lokální B]	|	84	

Pokud se budeme na lokální proměnné odkazovat relativně k ESP, tj. například přístup k lokální proměnné A pomocí „[esp-8]“, dojdeme k zádrhelu – jakmile pushneme, nebo jinak změníme stack, ESP se změní, a tudíž odkaz esp-8 nepovede na lokální proměnnou A, ale na něco úplně jiného. Protože víme, kolik lokálních proměnných bude funkce používat, je použit následující model:
Na začátku funkce se uloží aktuální ukazatel ESP do registru určeného právě pro tento účel – do registru EBP. Od ESP se pak odečte n bytů tak, aby nově vytvořené místo ve stacku vytvořilo místo pro všechny lokální proměnné.
Např. fuknce Sečti(x, y), lokální proměnné A, B, C:

Stack po inicializaci funkce (ESP = 100):
_________________________________________________
|Hodnota	|	Adresa			|
|[lokální B]	|	80	[lokální A]	|	84	Návr. adresa	|	88	X		|	92	Y		|	96	

Registr EBP je pro celou funkci zachován a je neměnný, tudíž se ve funkci na lokální proměnné a parametry odkazuje právě pomocí EBP. Jakmile dojde k volání jakékoliv další funkce, je nutné, aby hodnota EBP byla pro předchozí funkci zachována (ebp se jednoduše na začátku funkce pushne - mění se tak jen vzdálenost mezi EBP a prvním parametrem o velikost EBP). Úvod a konec funkce tak povětšinou vypadá právě následovně:

push ebp	;záloha předchozího ebp
		;(tzn. hodnota ebp funkce,
		; která tuto funkci zavolala)
mov ebp, esp	;esp nyní ukazuje na 
		;zálohovanou hodnotu ebp
sub esp, 8	;místo pro 2 lokální proměnné
… kód funkce
add esp, 8	;odstranění lokálek ze stacku
pop ebp		;obnovení předchozího ebp, esp
		;nyní ukazuje na návratovou adresu
ret		;skok zpět

Možná už cítíte, kde je zakopaný pes. (lol)

Přetečení lokální proměnné

Nyní se už dostáváme k jádru věci – proč je přetečení lokální proměnné nebezpečné a jak se dá zneužít. Pro názornost si představme funkci, která načte jméno uživatele do lokálního bufferu o velikosti 12 bytů. Funkce by mohla vypadat nějak takto:

Int nacti()
{
	char jmeno[12];
	Nacti_Jmeno(jmeno);
	…
}

Tzn.:

push ebp				
mov ebp, esp
sub esp, 12
Zavolej Nacti_Jmeno (ebp-12)
…
add esp, 12
pop ebp
ret

Stack před voláním funkce Nacti_Jmeno, která načte vstup uživatele do našeho 12ti bytového bufferu, vypadá následovně:

Stack:
_________________________________________________
|Hodnota	|	Adresa			|
|[ ][ ][ ][ ]	|	[ ][ ][ ][ ]	|	[ ][ ][ ][ ]	|	Návr. Adresa	|	

Pokud načteme jméno „Pavel Novák“, bude stack vypadat následovně:

Stack:
_________________________________________________
|Hodnota	|	Adresa			|
|[P][a][v][e]	|	[l][ ][N][o]	|	[v][á][k][ ]	|	Návr. Adresa	|	

Určitě už vidíte problém, nyní načteme jméno „AAAABBBBCCCCDDDDEEEE“:

Stack:
____________________________________________________________________
|Hodnota	|	Stack					   |	
|[A][A][A][A]	|	[B][B][B][B]	|	[C][C][C][C]	|	[E][E][E][E]	|	

Jakmile funkce dojde na instrukci RET, dojde ke skoku na adresu „EEEE“. Program zhavaruje. Tato jednoduchá chyba stojí za celým konceptem. Otázkou však je, jakým způsobem tuto chybu využít (zneužít).

Zneužití chyby

Nyní už víme, že dokážeme přepsat adresu ve stacku, která určuje, kam se má aktuálně prováděná funkce po skončení vrátit. K čemu je to ale dobré a hlavně, jak propašovat tímto způsobem do aplikace s touto chybou nějaký kód, který stáhne například backdoor z Internetu? Nejprve začnu tím, jak propašovat kód, který zařídí všechno, co chceme – tzv. shellcode.

Shellcode

Podívejte se ještě jednou na schéma stacku výše. Na přepsání všech lokálních proměnných potřebujeme 3*4bytů + 4 byty na přepsání zálohy EBP, další 4 byty na přepsání návratové adresy a zbytek můžeme libovolně využít – například tak, že zde napíšeme náš shellcode. Vstup uživatele by tak mohl vypadat: „AAAABBBBCCCCDDDDEEEE###“, kde # je libovolný (skoro) sled znaků shellcodu. Když se podíváme na nějaký exe soubor a jeho strojový kód, můžeme si všimnout, že většina bytů kódu je zobrazitelná jako nějaký znak, ať už jako písmeno, nebo nějaký čtverec, atp. Je tedy možné nakopírovat přímo tyto znaky a připsat je za vložené jméno „AAAABBB…“. Vstup uživatele je však v drtivé většině načítán jako textový řetězec ukončený null-charem. Vložené znaky představující kód tak nesmí obsahovat nulový byte, protože ten by useknul zbytek shellcodu (v unicode to bude trochu jinak). Hlavní je si tedy uvědomit, že kód celého exploitu je napsán přímo se vstupem uživatele. Více se shellcodem budeme zabývat později.

Zavolání shellcode

Nyní jsme ve fázi, kdy jsme zaplnili lokální buffer, přepsali návratovou hodnotu a za ní ve stacku následuje náš kód (ovšem musíme počítat s tím, že bude odebráno n parametrů ze stacku,a tak je nutné použít ještě další vycpávku). Nyní přichází na řadu otázka, jak vlastně ten kód zavolat. První, co nás napadne, je, že přepíšeme návratovou adresu na ESP, protože víme,že ESP je při návratu z dané funkce tolik a tolik. To ale vlastně nevíme z jednoduchého důvodu - nevíme kdy a po jakých dalších funkcích je naše exploitovaná funkce zavolána,a tak nemůžeme znát a ani předvídat hodnotu ESP. Potřebovali bychom nějaký pevný opěrný bod, který by skočil přímo na ESP a ten existuje. Nejjednodušeji se dá najít v kódu kombinace dvou bytů, tedy push esp a ret. Kouzelné, že? Push esp vloží do stacku ukazatel na esp a ret na něj skočí. Co je na vrcholu stacku? Náš kód, který se začne provádět. Instrukce push esp & ret (54 C3) můžeme nalézt například v ntdll.dll a určitě se bude dát nalézt i v kódu exploitované aplikace – záleží pouze na našem rozhodnutí. Stack exploitované funkce může tedy vypadat následovně:

Stack:
____________________________________________________________________
|[A][A][A][A]	|	[B][B][B][B]	|	[C][C][C][C]	|	7C92C35C	|	návratová adresa!!!    |
|[F][F][F][F]	|	#ß	|	

Při ukončování funkce odstraní lokálky a skočí na 7C92C35C (tj. push esp & ret v ntdll) a odebere 8 bytů (dva parametry):

add esp, 12
ret 8

Esp nyní ukazuje přímo na náš shellcode:

push esp
ret

EIP je nyní roven ESP a začínají se provádět instrukce shellcodu, ať už dělají cokoliv...

Psaní shellcode

Jak již bylo řečeno, shellcode, který se vkládá jako textový vstup, nesmí obsahovat nulové znaky, s čímž programátor shellcodu musí nutně počítat. Používá se nespočet triků, ať už záměny instrukcí, které nulový byte obsahují za další, které nuly nemají, nebo „zašifrování“ části kódu s nulami pomocí XORu. Zbytek už je jen na znalostech, dovednostech a představivosti programátora. Je nutné si také uvědomit, že není vhodné použít hardcoded adresy API funkcí, které budou v shellcodu použity. Nejlepším způsobem tak je, uložit si jména funkcí a kódem si je pak najít (např. PEB -> moduly -> kernel32.dll -> EAT a máme LoadLibrary a GetProcAddr). Cílem tohoto článku bylo popsat princip, na kterém tento exploit stojí, nebudu tedy rozebírat psaní celého shellcode (krom toho existuje i automatický generátor shellcodů).

Příklady zneužití

Jak můžete vidět, princip exploitu buffer overflow není nijak zvlášť složitý. Avšak efektivitu tohoto exploitu již předvedlo nemálo virů, včetně Confickeru, který použil tento exploit na základě analýzy funkce NetpwPathCanonicalize z netapi32.dll. Použití buffer overflow exploitu se však neomezuje pouze na špatně napsané síťové funkce. Terčem jsou i obyčejné aplikace, jako jsou pakovače souborů, přehrávače hudby, a dokonce i hry. Hlavním hnacím motorem tohoto exploitu je, že není potřeba v podstatě žádné interakce uživatele, a pokud ano, pak má uživatel před očima úplně něco jiného, než podezřelý spustitelný soubor. Možnosti jsou prakticky neomezené a představivosti se meze nekladou. Kdo by se například nepodíval do ZIP souboru, který mu přišel na email? Už jen to, že tento ZIP rozbalíte, může způsobit buffer overflow. Pouhým spuštěním mp3 souboru je možné si infikovat systém, protože ve verzi přehrávače, kterou máte, je zrovna tato zneužitelná chyba, protože byl programátor líný a vynechal kontrolu délky mp3 tagů při načítání. Stejně tak není problém najít podobnou chybu v prohlížečích PDF souborů (velmi častý terč), ale i v oblíbených programech, jako jsou ICQ klienty. Před několika měsíci měl jeden z icq klientů kritický update, který opravoval zneužitelnou chybu přetečení zásobníku při zpracovávání PNG avatarů – stačilo si tedy jako fotku dát speciálně upravený obrázek a mohli jste infikovat libovolného člověka s tímto klientem jen tím, že jste mu poslali zprávu. Analogicky je to s hrami - jedna z verzí nejznámější FPS multiplayer hry nekontrolovala délku textového řetězce, který popisoval nastavení serveru, a bylo tak při jeho správném nastavení možné infikovat kohokoliv díky přetečení zásobníku na klientovi.

Závěr

Závěrem by se patřilo říci, že buffer overflow stojí na chybném konceptu programování, který možná dříve nebo později úplně zmizí. Stojí za tím starší / nízko úrovňové programovací jazyky (Asm, C, …), které nemají vlastní režii práce s nebezpečnými typy proměnných, jako jsou právě textové řetězce, a programátor je tak donucen hlídat vše sám. V protikladu tak existují jazyky (většinou objektové – Delphi, VB, …), které tuto režii mají, a programátor se tak nemusí o nic starat. U těchto jazyků je více méně velmi obtížné najít jiný druh chyby, který by tento klasický bof zastoupil.
Windows jako takové se snaží těmto zneužitím předcházet. Windows XP mají možnost zapnout DEP - Data Execution Prevention, kdy procesor (pokud to CPU umožňuje - hardwarový DEP) podá systému přerušení, jakmile je zpracovávaná instrukce na paměťové stránce, která nemá oprávnění pro provádění instrukcí (execute). Pokud takovou funkci CPU nemá, je použita softwarová emulace. Windows Vista používají další trik, který zneužití předchází - vždy náhodně rozmístí moduly a tak není možné se odkazovat na "push & ret" instrukce, které se při aktuálním spuštění nacházejí na určité adrese.

Komentáre

Narazil jsem na jeden problém: Při pokusu o přepsání EIP na stacku pomocí adresy instrukce PUSH ESP - RET. Musel jsem tuto adresu instrukce nahradit adresou instrukce JMP ESP (případně CALL ESP). V tom případě již fungovalo vše tak, jak má. Chtěl bych se proto zepat, zda by mi nemohl někdo vysvětlit, proč mi v článku zmiňovaný postup s PUSH ESP - RET nefunguje (ač to pravděpodobně tuším). Předem díky.

A měl bych dotaz, jak by se řešil problém s canary bytem, který byl přidán do Windows XP SP-2 a vyšší právě pro ochranu před přetečením/porušením stacku (konkrétně EBP a EIP). Předpokladem je hádání hrubou silou, protože možných hodnot je pouze 255. Předem díky za odpověď.

Na prekonanie cookie (canary) je mozne pouzit napriklad exploitovaciu techniku SEH, ak je to mozne. Aj tam vsak robi Microsoft pokroky - SafeSEH, SEHOP.. happy exploiting :-)

Díky za tip :) Už dřív jsem o tom něco četl, ale tohle jsem nevěděl. Pořádně na to kouknu, abych věděl, co jsem nevěděl :D Ještě jednou díky :)

Podporte nás


Páčil sa Vám tento článok? Ak áno, prosím podporte nás ľubovoľnou čiastkou. Ďakujeme!


ITC manažer Security-portal.cz Spamy.cz PHP Fushion Soom.cz
Hoax.cz Antivirove centrum Crypto-world.info SPYEMERGENCY.com