A format string sérülékenység

A format string sérülékenységeket meglepően későn, csak 1999-ben fedezték fel. Eleinte senki nem feltételezte, hogy a format string hibák komoly gondot okoznának, később azonban kiderült, hogy ezeket a hibákat kihasználva a sebezhető alkalmazás összeomolhat, vagy akár a támadó által injektált kódot is lefuttathat.

Ez a fajta sebezhetőség szinte kizárólag C/C++ alkalmazásokban található meg. Egy program akkor támadható meg ilyen módon, ha a programozó rosszul használja a printf függvénycsalád tagjait (nprintf, sprintf, snprintf, fprintf, vprintf, stb.), és a felhasználó által (legalább részben) kontrollált értéket ad meg a függvénynek paraméterül, format string explicit megadása nélkül. Ez lényegében egy kód injektálást tesz lehetővé, mivel az adat amit a felhasználó megad, részben kódként lesz értelmezve. Nézzünk egy leegyszerűsített példát:

#include <stdio.h>

char buffer[80];

int main(void)
{

    printf("Enter the string: ");

    fgets(buffer, 80, stdin);
    printf(buffer);

    return 0;
}

A printf függvény tehát format stringként értelmezi a bufferben található stringet, így a támadó a következőt teheti meg:

$ ./vuln_app
Enter the string: %08x %08x
00000041 75072900

A kimeneten pedig a stack tetején lévő két értéket láthatjuk. Hogy lehet, hogy a printf függvényt paraméter nélkül hívtuk meg, és mégis hozzárendelődött a két %x-hez egy-egy érték? Hát úgy, hogy a printf assembly kód szintjén egyszerűen a stack-ről veszi le a paraméterként kapott értékeket. Ha a format string azt mutatja, hogy a stack-ről le kell venni valahány értéket, akkor a printf függvény ezt boldogan megteszi. Nincs semmilyen lehetőség arra, hogy ellenőrizze a megkapott paraméterek számát.

Így tehát bármilyen értéket kiolvashatunk a stack-ről, sőt kis trükközéssel akár tetszőleges memóriacímről. Ezt kihasználva a támadó szenzitív információkhoz juthat hozzá, amik akár további támadásokat is elősegíthetnek. Az alkalmazás összeomlásához elegendő a %s felhasználása. Ha a stack tetején lévő érték nem elérhető a memóriában (mert pl. nem egy cím, hanem egy egyszerű érték), akkor a program hibával leáll.

Az exploitálás legveszélyesebb formája azonban az RCE (remote code execution), amit szintén lehetővé tesz ez a fajta sérülékenység. Ahhoz, hogy kódot tudjunk futtatni a sérülékeny alkalmazást futtató gépen, szükség van egy nagyon fontos tulajdonságra/képességre, amit látszólag a printf hibáinak kihasználásával nem tudunk elérni. Ez a képesség a tetszőleges (kernel spacen kívüli) memóriacímre való írás. Az egyértelmű, hogy a stackre írhatunk, ugyanis ide kerül maga a printf által kiírt adat. Létezik viszont egy olyan formátum jelölő karakter, ami képes a stacken kívülre is írni - az %n.

A %n használatakor a printf egy memóriacímet vár paraméterül. A string kiírásakor erre a memóriacímre az addig a pontig kiírt karakterek száma kerül. Így tehát a támadó adhatja meg a memóriacímet és ő kontrollálja az addig kiírt karakterek számát is, tehát (szinte) tetszőleges helyre írhat. Ez után pedig a buffer overflow hibák kihasználásakor megismert módon érhető el távoli kódfuttatás. Implementációtól függ, hogy a %n hány byte-os pontossággal ír, de ez az érték jellemzően 4 byte. A %hn formátum jelölővel azonban 2 byte-ot írhatunk, ami sok esetben igen hasznos.

Az utolsó fontos kérdés, hogy a %n-t felhasználva hogyan írhatunk tetszőleges értéket akkor is, ha már több karaktert írtunk ki a printf függvénnyel, mint amilyen byte-ot írni akarunk? Ezt egyszerű túlcsordítással tudjuk elérni. Ha a printf eddig 65536 karaktert írt ki, de csak 2 (unsigned) byte-on tároljuk a kiírt karakterek értékét, akkor %hn által a memóriába írt érték 1 lesz.

Kapcsolódó anyagok: