Seznamte se: zásobník
Buffer je vyrovnávací paměťový prostor, sloužící pro přechodné uložení přesouvaných dat z rychlejšího paměťového média na pomalejší, pro uložení proměnných programu, mezivýsledků, návratových adres při volání podprogramů apod. Buffer tak plní roli jakéhosi mezičlánku (např. aby pomalejší médium mělo data stále k dispozici a aby nebylo rychlejší médium blokováno zdlouhavým přenosem). Buffer overflow, neboli česky „přetečení zásobníku“, je situace, která označuje anomálii, kdy program (lhostejno, zda záměrně nebo nechtěně) zapíše data za konec alokované části paměti.
Paměť programu se skládá ze tří částí, a to z kódu programu, inicializovaných a neinicializovaných dat programu a ze zásobníku/hromady. Část kódu programu je určena pro uložení vykonávaných instrukcí a její obsah lze pouze číst. Zásobník (stack) je pak část paměti, do níž jsou ukládána často používaná data. Zásobník je zpravidla definován jako LIFO (Last-In, First-Out), kdy se poslední vložený prvek musí vyjmout jako první a kdy nelze pořadí prvků upravovat. Hromada (heap) je pak ta část paměti, která slouží pro dynamické přidělování paměti během činnosti programu. Rozdíl mezi zásobníkem a hromadou není ve funkčnosti, ale ve způsobu fungování: zatímco velikost zásobníku je pevně daná, u hromady je dynamicky přidělovaná (podle potřeby).
Podívejme se nyní na to, jak vlastně zásobník funguje. Začněme popisem struktury procesu v paměti. Ta vypadá zhruba následovně:
n zásobník,
n neinicializovaná data,
n inicializovaná data,
n kód programu.
Zásobník je na nejvyšších adresách (s tím, jak do něj ukládáme data, roste směrem dolů), na nejnižších je naopak kód programu. Na architektuře Intel x86 ukazuje vrchol zásobníku (u zásobníku je vše obráceně, vrcholem se tedy rozumí nejvyšší ještě volná buňka paměti – těsně pod posledně vloženými daty) registr zvaný Stack Pointer (SP). Data se do zásobníku ukládají („push“) nebo vyzvedávají („pop“). Ukládaná hodnota se přitom kopíruje na adresu, na níž odkazuje SP. Ta se přitom následně upraví tak, aby stále ukazovala na první volné místo. Obdobné je to i při vyzvedávání dat.
Zásobník má význam především při volání funkcí. Před voláním podprogramu se do zásobníku uloží parametry funkce, návratová adresa (adresa instrukce následující těsně za voláním funkce), starý Base Pointer (BP) a lokální proměnné dané funkce. Po návratu z funkce se všechny hodnoty obnoví, parametry se odstraní a provede se skok na uloženou návratovou adresu, takže hlavní program může pokračovat přesně tam, kde skončil. Takto je pochopitelně ošetřeno vložené nebo i zpětné volání funkcí.
Přetečení zásobníku
A oč tedy jde v případě slavného (a nebezpečného) přetečení zásobníku? Dochází při něm k výjimce při přístupu k paměti, čímž nastává pád programu nebo spuštění nekorektní části kódu (viru apod.). Jinými slovy: je to anomálie, kdy se proces pokouší uložit data za vyhrazené hranice. Výsledkem pak je nekorektní běh – třeba přepsání dalších bufferů, proměnných, dat…
Přetečení zásobníku vznikne nesmírně snadno a velmi jednoduše se zneužije. Přitom se jedná o nebezpečný problém.
Rozlišujeme dva typy přetečení bufferu, a to přetečení na zásobníku (stack overflow) a přetečení na hromadě (heap overflow). Typicky dochází k přetečení při zpracování vstupních dat. Buffer overflow nejčastěji vzniká jako důsledek chyby nebo nevhodného použití programovacího jazyka (zpravidla C nebo C++, které nemají standardně implementované mechanizmy ochrany paměti – ty musí manuálně vytvářet programátoři, kteří tak mnohdy nečiní nebo to dělají s chybami). Jedním z možných důsledků této chyby přitom je, že může dojít k přepsání nebo poškození korektních dat. Celý problém je o to nebezpečnější, že nemusí být na první pohled patrný, protože skutečný důsledek buffer overflow závisí na mnoha okolnostech.
Je to vůbec nebezpečné?
Bohužel se při nalezení možnosti vzniku přetečení zásobníku lze často setkat s reakcí typu „dokažte, že je to opravdu napadnutelné a zranitelné“. To je ale obhajování vlastních chyb, protože nikde není psáno, že když něco nedokážu napadnout já, nedokáže to ani nikdo jiný.
Chyba zkrátka chybou zůstane, i kdyby trakaře padaly. To opravdu musíme čekat na skutečný a velký problém, abychom něco udělali?
Navíc je technika zneužití problémů závislá na architektuře, operačním systému, použitém programovacím jazyce – čili na mnoha skutečnostech.
Michael Howard a David LeBlanc v knize Writing Secure Code naprosto jasně prohlašují: „Neopravujte pouze chyby, o nichž si myslíte, že jsou zneužitelné. Opravujte všechny chyby!“ A buffer overflow chybou bez diskuse je.
Uvědomte si také ten rozdíl: opravená chyba představuje jen vydání další záplaty. Naproti tomu zneužití neopravené chyby (o níž navíc víte!) může znamenat ohromné problémy, ohrožení citlivých dat v mnoha společnostech apod.
Mnozí programátoři se přitom domnívají, že třeba buffer overflow lze vytvořit jen na zásobníku, nikoliv na hromadě. Neboť zatímco zásobník má velikost paměti pevně danou, na hromadě je přidělována dynamicky. Domnívají se, že v případě dynamického přidělování (kolik je potřeba) prostě zásobník nemůže přetéci. To je ale kardinální omyl.
Vytvořit buffer overflow na hromadě představuje sice náročnější hackerskou techniku – ale je proveditelná! Dalším problémem je fakt, že existují nástroje pro kontrolu přetečení zásobníku, ale pro kontrolu přetečení na hromadě nikoliv. Navíc některé operační systémy a čipové architektury mohou být konfigurovány tak, že jsou odolné přímo proti přetečení zásobníků, leč nikoliv proti přetečení hromad (tuto konfiguraci si ale kdekdo vykládá tak, že systém je odolný proti buffer overflow obecně).
Důsledky buffer overflow
Hlavní důsledky přetečení zásobníku jsou bezpečnostní: díky tomuto problému může dojít ke spuštění kódu v datech, která by měla být jen zpracovávána – a nikoliv vykonávána. Do těchto dat tedy může hacker umístit svůj připravený kód a spustit útok vůči systému.
V praxi buffer overflow funguje tak, že při zápisu do bufferu dochází (zpravidla záměrně nebo vinou výše zmíněné programátorské chyby) k pokusu o zápis většího než povoleného objemu dat. Data, která se nacházejí za vyhrazeným bufferem, jsou pak přepsána – a právě sem může útočník pohodlně vložit svůj programový kód.
Nejoblíbenějším cílem útoků buffer overflow jsou webové servery a další aplikace. Je to pochopitelné: útok na jedné straně nepotřebuje zásah uživatele, ale zároveň musí být vykonaný v prostředí, kde se předpokládá vstup uživatele (například URL, hlavičky HTTP, těla HTTP požadavku). Vstup uživatele pak má podobu vhodně zvolených sekvencí, obsahujících kód v assembleru, čímž je narušena paměť s dalšími důsledky: zkolabování serveru nebo výše zmíněné vykonání předpřipraveného kódu. Dávka dat od hackera přesně zaplní buffer a nahradí bezprostředně za ním následující data.
Správně vytvořené a testované programy by přitom měly kontrolovat délku vstupujících dat, a to právě z důvodu, aby nedocházelo k přetečení bufferu – třeba i nechtěnému. Jenomže tato skutečnost je často přehlížena, zvláště nezkušenými programátory.
Jak již bylo uvedeno, problémy s buffer overflow jsou zpravidla způsobeny špatným programováním v jazycích C nebo C++, které byly konstruovány s ohledem na rychlost, nikoliv na bezpečnost. To samozřejmě neznamená, že tyto jazyky jsou špatné: k problémů dochází díky programátorským chybám, nikoliv díky špatné volbě jazyka (i když zvláště méně zkušení programátoři by mohli používat jazyky odolnější proti buffer overflow).
Naproti tomu programovací jazyky jako Java a Lisp řídí vyčlenění paměti automaticky a používají kombinaci různých technik (run time checking, static analysis) k tomu, aby nějaký kód nabízel nepravděpodobnou nebo zcela vyloučenou možnost buffer overflow. Perl zase nabízí průběžné měnění velikosti polí, aby se přetečení zásobníku vyhnul.
Přes tyto snahy ale mají i výše uvedené „bezpečnější“ jazyky problémy s buffer overflow, a to díky chybám v knihovnách v interní implementaci ve svých kontrolních systémech. Znovu podotýkáme: jde o programátorské chyby, nikoliv o chyby jazyka!
A co s tím?
V případě problému buffer overflow je velmi důležité spoléhat hlavně na prevenci. V první řadě je dobré věnovat pozornost použití bezpečných knihoven a vůbec softwaru. Přetečení zásobníku se dá zabránit udržováním vysoké korektnosti kódu (buffer management). Dobře napsané a otestované kódy, které dokáží automaticky provádět buffer management, představují jeden ze základních pilířů ochrany. Nástroje na detekci chyb v programech přitom mohou být statické (analyzují zdrojový kód programu, aniž by jej prováděly), a dynamické (kód je prováděný a sleduje se jeho chování).
Pak je tu řada dalších možných opatření. Třeba použití IDS (Intrusion Detection Software), neboť tyto programy mají možnost detekovat pokusy právě o útok buffer overflow. Většina podobných útoků totiž obsahuje dlouhé pole instrukcí, které v korektním kódu chybí. IDS tak mohou blokovat příchozí pakety obsahující větší počet podobných instrukcí. Nicméně tato ochrana není stoprocentní, protože příslušná pole mohou být napsána i pomocí jiné syntaxe. V poslední době se lze setkat i s alfanumerickými, polymorfními nebo sebemodifikujícími útočnými kódy, aby proklouzly před IDS.
Pak jsou tu speciální softwarové nástroje pro ochranu poškození zásobníku, které dokáží detekovat nejobvyklejší útoky tím, že sledují, zda nedošlo po návratu funkce ke změně zásobníku. Pokud k nějaké změně došlo, program je okamžitě ukončen.
Ochrana je možná i na úrovni operačních systémů. Ve Windows existuje několik softwarových možností, které se s problémem dokáží vypořádat. Jedná se např. o DEP (Data Execution Prevention) v XP SP2 nebo W2003 SP1, OSsurance a Anti-Execute. Nejinak je tomu v případě Linuxu, kde může např. knihovna Libsafe přesměrovávat volání potenciálně nebezpečných funkcí na sebe, Openwall Kernel Patch doplňuje do jádra systému vlastnosti „non-executable-stack“, nebo StackGuard, kdy je zamezeno vzniku přetečení doplněním kontrolního slova na konec vyrovnávací paměti. Jinými slovy: rozhodně se nejedná o neřešitelný bezpečnostní problém, kvůli kterému bychom měli mít neklidný spánek.
Jak už jsme několikrát zdůraznili, buffer overflow je především problém programátorských chyb, které jsou hackerskou komunitou obratně a rychle zneužívány. Důsledným dodržováním pravidel programování a kvalitním testováním se jim dá úspěšně předcházet.
Historie přetékání
„Přetečení zásobníku“ představuje typ útoku, známý již velmi dlouho a velmi hojně používaný. Ostatně nevznikl jako útok, nýbrž jako „regulérní“ programátorská chyba. A když se pak pátralo po příčinách „padání“ programu nebo jiných kolizí, zjistilo se, že důvodem je právě přetečení bufferu. A pak už jen stačilo, aby si nějaká vynalézavá hlava uvědomila, že tuto chybu lze vyvolat uměle a že ji lze využít k útoku.
První větší zneužití buffer overflow jsme zaznamenali v roce 1988, a to ve slavném škodlivém kódu Worm (červ) Roberta T. Morrise z Cornell University – který dal ostatně jméno celé kategorii škodlivých kódů.
Program Worm měl pouhých 99 řádků kódu, ale dokázal navždy změnit svět. Dokázal napadnout zhruba šest tisíc počítačů. Podle dnešních měřítek je to samozřejmě směšně málo, ale musíme si uvědomit, že tehdy šlo o plných osm procent strojů, připojených k síti! Z hlediska procentního zavirování všech připojených počítačů neměl Worm už nikdy konkurenta. Pro úplnost dodejme, že Robert Morris dostal za svůj čin tříletou podmínku, několik finančních pokut ve výši 10 050 dolarů a samozřejmě byl vyloučen ze školy.
Ani toto varování však nestačilo. Na problém jménem buffer overflow se na několik let pozapomnělo, znovu ho objevil v roce 1995 Thomas Lopatic, který své analýzy zveřejnil v seznamu Bugtraq. Tím nejen varoval IT veřejnost, ale zároveň ukázal hackerům možnost vstupu do mnoha systémů cestou velmi malého odporu. Zatímco veřejnost se nepoučila (testování na odolnost vůči buffer overflow je dodnes v případě některých programátorů i vývojářských společností prachbídné), hackeři svoji příležitost rozpoznali velmi rychle. Svědčí o tom třeba velmi podrobný článek Eliase Levyho (aka Aleph One), publikovaný v časopise Phrack pod názvem „Smashing the Stack for Fun and Profit“ (Rozbíjení zásobníku pro zábavu i zisk). Ten nepojal problém jako varování, ale krok za krokem rozebral praktické aspekty zneužití buffer overflow. Zatímco dosud mohli tyto útoky provádět jen lidé s vysokou úrovní znalostí problematiky, nyní si mohl buffer overflow vyzkoušet opravdu každý. To mělo v následujících letech za následek obrovský rozmach tohoto typu útoků.
Za připomenutí rozhodně stojí i událost z roku 2001, kterou známe pod jedním slovem: CodeRed. První verze tohoto internetového červa nebyla příliš nebezpečná, protože měl nešťastně navržený mechanizmus šíření. O plný týden (!) později se ovšem objevila verze druhá, jen nepatrně modifikovaná (rozdíl byl víceméně jen v několika parametrech, nastavených v mechanizmu šíření). Pouze díky tomuto vylepšenému mechanizmu bylo celosvětově napadeno pomocí buffer overflow (na problém přitom již několik týdnů existovala záplata!) za méně než 14 hodin 359 tisíc počítačů. Způsobené škody byly přesto vyčísleny na 2,6 mld. dolarů.
Zatím poslední globální útok zneužívající buffer overflow pak přišel v roce 2003 a měl podobu internetového červa Slammer. Ten se mimochodem stal nejrychleji se šířícím kódem v historii počítačů. Během pouhopouhých deseti minut dokázal napadnout devadesát procent všech napadnutelných počítačů – těch bylo asi 75 tisíc. V průběhu první minuty šíření přitom dokázal zdvojnásobit počet napadených počítačů každých 8,5 sekundy! (Pro srovnání: populace CodeRedu se zdvojnásobila každých 37 minut!) Tajemství rychlosti Slammeru bylo ukryto především v jeho malé velikosti (376 znaků) a také v tom, že se šířil pomocí paketů UDP (ty jsou nesrovnatelně rychlejší než TCP-SY).