Főoldal Követelmények Segédanyagok Oktatók

Formátum sztring sérülékenység

Formátum sztring példakódok: bevezető, egyszerű, picit kevésbé egyszerű

Elméleti bevezető

Az alábbi pár kép illetve a letölthető animáció segít felfrissíteni C programok memóriájáról illetve a függvényhívás menetéről tanultakat. A képek, leírások elsősorban x86-os, 32 bites architektúrákra vonatkoznak. Egy C program memóriájának átlagos felépítése az alábbi képen látható:
memory
Forrás: Link
Néhány észrevétel a kép alapján:
  • A veremben tárolódik egy-egy függvényhívás adata, vagyis (egyebek mellett) a lokális változók, a függvényeknek átadott paraméterek. A felszabadítás automatikusan, a meghívott függvényből való visszatéréskor megtörténik (emiatt veszélyes egy függvény lokális változójának memóriacímét visszaadni, hiszen az a terület automatikusan felszabadul) A verem általában a magasabb memóriacímek felől növekszik az alacsonyabb memóriacímek felé.
  • A heap-en/dinamikus memóriában tárolódik mindaz, amit dinamikusan (new, malloc, calloc) foglaltunk le. A heap általában az alacsonyabb címek felől növekszik a magasabb címek felé. Ennek felszabadítása nem automatikus, a programozónak kell nem elfelejtenie (delete, free). Ezt a memóriaterületet használják a dinamikusan betölött library-k is.
  • BSS (Block Started by Symbol, Uninitalized Data Segment): olyan globális és statikus változók kerülnek ide, amelyeket a programozó nem inicializált saját maga, ezért a program indulásakor automatikusan 0 értéket kapnak. Pl. C-ből megszokott globális változók, függvények static kulcsszóval ellátott változói.
  • DS (Initalized Data Segment): olyan globális és statikus változók, amiket a programozó ellátott kezdőértékkel.
  • Text: a programból készült gépi utasítások helye
A memória felépítését tudjuk megvizsgálni ennek a kódnak a segítségével. A végén kommentek tartalmazzák egy teszt lefutás eredményét (hová, milyen memóriacímekre kerülnek az egyes változók, hol vannak a szegmenshatárok). A kód lefordítható ezzel a Makefile-al (figyelem, egy későbbi példakód fordítása is target!)

A verem tehát egy-egy függvényhívás adatát tartalmazza. Ezek az adatok frame-eket/kereteket alkotnak. Függvényhívás hatására egy új keret kerül a verem tetjére. A megértéshez fontos regiszterek listája:
  • ESP: Stack Pointer. A verem tetejére mutat.
  • EBP: Base Pointer. Az aktuális keret alját mutatja, ehhez képest lehet meghatározni, hogy hol vannak az átadott paraméterek és lokális változók.
  • EAX: Függvények visszatérési értékét és bizonyos számítások eredményeit (szorzás, osztás stb.) tároló általános célú regiszter.
Az alábbi kép szemlélteti a verem változását függvényhívás hatására. Figyelem! A verem teteje most a kép alján van (ahogy az ESP is mutatja)! Arrafelé növekszik a verem. Függvényhívás hatására a verem tetjére kerülnek az esetleges függvény paraméterek (ilyet az ábra nem mutat most), a visszatérési cím (return address), ennek tetejére az EBP lesz elmentve (a hívó függvény keretének az aljára mutat. Az új EBP az ESP aktuális értéke lesz), végül jön maga a függvény kerete a lokális változókkal.
stack
Forrás: Link
Egy stack frame felépítése úgy, hogy paraméterek is fel vannak tüntetve, illetve van egy lokális változó (fd) is:
stack-frame
Forrás: Link
A printf hívásakor a verem így fest: printf("%s %d %08x",a,b,&c). A négy paraméter fordított sorrendben kerül a veremre (utoljára kerül be a formátum sztring). Általában ez az átadási sorrend, de nem szabvány. Amikor a printf végrehajtja a kiíratást jobbról balra kezd lépkedni a paramétereken. Mi történik, hogy ha a programozó elfelejt átadni egy paramétert? A printf változó számú paramétert képes fogadni, tehát fordítási hibát nem kap (esetleg warningot, ha használja a -Wall kapcsolót). A printf nem tudja, hogy ő kevesebb paramétert kapott. Egyszerűen kiolvas egy, a formázó karakternek megfelelő méretű adatot a veremről. Itt jön majd be a formátum sztring sérülékenység, amit a következőkben tárgyalunk.
printf-stack
Forrás: Link
Itt látható a függvényhívás menetéről egy kiváló összefoglaló prezentáció (animált ppt letöltése). Az animáció részletesen szemlélteti a függvényhívás során bekövetkező változásokat. A prezentációban szereplő példakód letöltése itt. A fordításhoz Makefile itt (tartalmazza a memory_layout.c kód lefordításához kellő utasítást is!)

Formátum sztring sérülékenység C-ben

A sérülékenység alapja, hogy a nem vagy rosszul ellenőrzött user input a formátum sztring helyén lesz a printf (és a családjába tartozó) függvényeknek átadva. Emiatt ha az a sztring formázó karaktereket (%x, %s stb.) tartalmaz az nem szövegesen lesz kiírva, hanem a printf kiolvas a veremből egy memóriaterületet (ott kéne lennie a formázó karakterhez tartozó paraméternek). Így a veremből információ lopható ki, programelszállást lehet okozni illetve még a memória tartalma is módosítható.

Bemutató videók

A formátum sztring sérülékenység bemutatására az alábbi videók készülték. Úgy gondoltam mindannyiunknak előnyére válik, hogy ha a videóknak nincs hangja.

Az első videó szemlélteti mit tapasztalunk, ha eggyel kevesebb paramétert adunk át mint ahány formázó karakter van.

Ebben a videóban két egyszerű példát látunk, amikor a user input a formátum sztring helyén kerül átadásra (printf(user_input);) illetve hogy ennek a hibának a felismerése nem mindig evidens. A példakód ennek az anyagnak az elején tölthető le. Átismételjük a %08x fomrázó karakter jelentését (8 karakter széles, vezető nullákkal feltöltött hexadecimális szám kiíratása). A több futtatás során tapasztaljuk, hogy a memóriacímek folyamatosan változnak. Ez az ASLR (Adress Space Layout Randomization) miatt van, a kernel védelmi funkciója. Randomizálva osztja ki a virtuális memória területeit a programoknak, így lefutásról lefutásra változik, hogy éppen melyik memóriacímen lesz egy utasítás vagy egy változó.

A formátum sztring sérülékenység kihasználása

Attention: a videóknak a visszajelzéseknek megfelelően megint van hangja
A következő videóban a %n formázó karakter van ismertetve. Ez nem összetévesztendő a \n karakterrel, ami sortörést okoz a kiíratásban. A %n paramétere egy pointer, ahová beírásra kerül, hogy a %n-ig hány karakter lett az adott printf hívásban kiírva. Ez az ártalmatlannak tűnő formázó karakter tehát alkalmas a memória manipulációjára!

Ha a kódba belecsúszik egy formátum sztring sérülékenység, a %n rosszindulatú használata esetén programelszállást vagy a letutás módosulását lehet előidézni. Az utóbbit szemlélteti az alábbi feladatmegoldás:

A továbbiakban GDB használatra is támaszkodni fogunk. Ehhez lentebb található egy bőséges használati segédlet. Azon alaputasítások ismerete szükséges csak, amelyek a videókban is szerepelnek (de az érdeklődők kedvéért fent van ez a részletesebb dokumentum is):

  • run: program elindítása
  • continue: felfüggesztett futás újraindítása
  • break: breakpoint berakása
  • x: memóriaterület kiíratása
  • set: változó, regiszter beállítása

Ez a videó az előző példafeladat megvizsgálása/megoldása GDB segítségével. Az x utasítás segítségével megnézzük a verem tartalmát és meggyőződünk róla, hogy a formátum sztring sérülékenység kihasználásakor valóban a verem tartalmát írattuk ki. A set utasítás segítségével átírjuk a lokális változót, lefuttatva így a titkos üzenet kiíratását.

A teljesség kedvéért pedig itt arról láttok egy rövid demót, hogy a bináris birtokában a formátum sztring sérülékenység kihasználása nélkül is meg lehet szerezni a titkos értékeket.

Ez a jellegű formátum sztring sérülékenység, amit a fentiekben tárgyaltunk tipikusan C/C++ jellegzetesség: a %n támogatása és a verem védelmének hiánya veszélyeztetetté teszi a programokat. Más nyelvek esetén is előfordulhat azonban különböző súlyossággal ez a sérülékenység, ha támogatják a formátum sztringes kiíratási módot (Java, PHP, Pearl, Ruby, Python). Verem védelmi funkciók, illetve a %n támogatásának hiánya vagy egyéb beépített ellenőrző funkciók azonban nehezebben kihasználhatóvá teszik. Itt lehet olvasni egy kis összefoglalót más nyelvek érintettségéről.

Megfelelelő user inputtal pythonban is van lehetőség nem kívánt adatok kiolvasására a format() függvény segítségével.

Példakód. Próbáljuk ki a következő két inputtal a programot: