Tweets by @buherablog
profile for buherator at IT Security Stack Exchange, Q&A for IT security professionals

A BitBetyár Blog

Túljártál a nagyokosok eszén? Küldd be a mutatványodat! (e-mail a buherator gmailkomra jöhet)

Full-Disclosure / Névjegy / Coming out


Promó

H.A.C.K.

Címkék

0day (110) adobe (87) adobe reader (21) anonymous (26) apple (60) az olvasó ír (49) blackhat (20) botnet (22) bug (200) buherablog (44) buhera sörözés (39) bukta (49) deface (38) dns (22) dos (29) esemény (82) facebook (26) firefox (64) flash (33) gondolat (31) google (59) google chrome (36) hacktivity (37) hírek (117) incidens (224) internet explorer (88) iphone (35) java (50) jog (22) kína (21) kriptográfia (68) kultúra (21) linux (24) malware (43) microsoft (142) móka (48) mozilla (23) office (26) oracle (40) os x (43) patch (197) php (20) politika (31) privacy (58) programozás (22) safari (34) sql injection (62) windows (85) xss (77) Címkefelhő

Licensz

Creative Commons Licenc

Use-after-free - Első rész

2013.01.20. 19:59 | buherator | 3 komment

Aki nem barlangban töltötte az elmúlt éveket, az tudja, hogy a böngésző biztonság hatalmas hangsúlyt kapott az információbiztonságon belül. Ez (a különböző bővítmények sérülékenységei mellett) a böngészők memóriakorrupciós sérülékenységeinek erős felértékelődését hozta magával, ezen belül pedig a use-after-free (vagy dangling pointer) típusú problémák kiemelt figyelmet kaptak. Mivel az ilyen jellegű sérülékenységek várhatóan még jó ideig a támadók eszköztárának hasznos részei lesznek, úgy gondoltam, hogy mindenki okulására összerakok egy részletes tutorial-sorozatot a use-after-free sérülékenységek kihasználási lehetőségeiről. Ebben az első posztban igyekszem bemutatni az elméleti alapokat és egy nagyon egyszerű példaalkalmazással demonstrálni a kihasználás egyik legegyszerűbb módját. A későbbiekben a célom az, hogy eljussunk a valódi böngésző-exploitokig és a haladó heap-rendezgető technikákig. A leírás alapvetően C++ programokról szól Windows környezetben, de a legtöbb gondolat könnyen átültethető más kontextusba is.

A use-after-free sérülékenységek a folyamatok heap-nek nevezett memóriaterületét érintik. A heapet a programok tipikusan dinamikus (előre nem látható mennyiségű illetve méretű) memória-allokációkor használják: ide mutatnak például a malloc() és rokonai által visszaadott pointerek, és itt kerülnek lefoglalásra a new operátorral létrehozott objektumok is. Egy program legalább egy heap-pel rendelkezik, és bármikor allokálhat magának újakat. Mivel a heap memória részeit a program dinamikusan foglalhatja el és szabadíthatja fel, az operációs rendszernek nyilván kell tartania minimum a foglalt vagy a szabad memóriaterületeket - a gyakorlatban a szabad és a foglalt területek is egy-egy láncolt listában kerülnek nyilvántartásra a hatékonyság érdekében (a heap-nek ilyen értelemben tehát nincs köze az azonos nevű adatstruktúrához).

Use-after-free problémáról akkor beszélünk, amikor a program megpróbál egy olyan memóriaterületet használni, melyet korábban felszabadított. Ez a legtöbb esetben a program összeomlásához vezet, mivel az operációs rendszer nem szereti, ha nem allokált helyeken turkálunk. Előfordulhat azonban, hogy a program idő közben újra lefoglalta a körábban felszabadított területet valamilyen más adat számára: ilyenkor attól függően, hogy a program mit kezd a kapott adattal sok csoda megeshet - a jó hacker célja természetesen az, hogy a vezérlés végül az általa megadott adatokon folytatódjon (sokszor persze egy jó infószivárgás is jól jön pl. ASLR megkerüléshez, de ezzel most nem foglalkozunk).

A use-after-free problémák azért nagyon hasznosak manapság (azon túl, hogy sok van belőlük), mert a megszokott memóriavédelmek, mint pl. a stack és heap sütik, vagy a safe unlinking nem használnak ellene.

Lássunk egy primitív példát:

class A{
public:
    void __construct__(){}

    virtual int vF1(){
        cout << "A::vF1" << endl;
        return 1;
    }
};

int main(){
    A *a=new A();
    a->vF1();
    free(a);
    cout << "a freed" << endl;
    a->vF1();
    return 0;
}

Debuggerben futtatva a programot valami hasonlót kapunk:

(9f4.be4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=006bbff8 ebx=7ffdf000 ecx=5b10ed48 edx=5b10ed40 esi=0019f9ac edi=0019fac4
eip=011a166f esp=0019f9ac ebp=0019fac4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
uaf!main+0x15f:
011a166f 8b10            mov     edx,dword ptr [eax]  ds:0023:006bbff8=????????

Amint látjuk, az eax regiszter egy nem-allokált memóriaterületre hivatkozik, de hogy lesz ebből kódfuttatás? Nézzük mi is történik pontosan:

   46 00be166c 8b45f8          mov     eax,dword ptr [ebp-8]
   46 00be166f 8b10            mov     edx,dword ptr [eax]
   46 00be1671 8bf4            mov     esi,esp
   46 00be1673 8b4df8          mov     ecx,dword ptr [ebp-8]
   46 00be1676 8b02            mov     eax,dword ptr [edx]
   46 00be1678 ffd0            call    eax


Az a objektumunkat az ebp-8 címen elhelyezkező pointer mutatja, és amint látható, egy kétszeres indirekciót (a mutatót mutató mutatót :) követve máris egy függvény (szubrutin) hívás történik, ami nekünk nagyon jó, hiszen a hívott cím futás közben számítódik és elméletileg mi is kontrollálhatjuk a korábban felszabadított terület újra foglalásával.

Mindez azért van így, mert az access violation-t okozó utasítás egy ún. virtuális függvényhívás része. A virtuális függvények célja és működése legegyszerűbben egy példán keresztül demonstrálható:

class A{
public:
    void __construct__(){}
    virtual int vF1(){
        cout << "A::vF1" << endl;
        return 1;
    }

    void doSomething(){
        return;
    }
};

class B: public A{
public:
void __construct__(){}
virtual int vF1(){
        cout << "B::vF1" << endl;
        return 2;
    }
    virtual int vF2(){
        cout << "B::vF2" << endl;
        return 2;
    }
};

B osztály az A osztály leszármazottja, így minden B típusú objektum egyúttal A típusú is (minden négyzet téglalap). Megtehetjük a következőt:

A* arr[1337];
// ... arr megtöltése A-kkal és B-kkel ...
for (int i=0;i<1337;i++){
    arr[i]->vF1();
}

(20140216: Példakód javítva, köszi BaT!)

Ha a vF1() nem lenne virtuálisként definiálva, a függvényhívásokat a fordító elintézné annyival, hogy "A típusú objektum esetében az A::vF1()-t hívjuk", tekintet nélkül arra, hogy a leszármazottak esetleg felüldefiniálhatták az ős eljárásait - így a program 1337-szer írná ki, hogy "A::vF1". Virtuális függvények esetében a hívás mikéntje futásidőben, az adott objektum pontos típusa alapján dől el (dinamikus kötés): az objektum memóriaképének első szava egy mutató az objektumhoz tartozó virtuális függvény táblára (vftable), ami az aktuális osztályhoz tartozó függvény-megvalósítások címeit tartalmazza. Így a függvényhívás a demóprogramnál látható utasítássorhoz hasonlóan általánosan megoldható, és mindig az objektum "valódi" típusa által definiálit függvénymegvalósítás fut le, vagyis a program "B::vF1" kimeneteket is fog adni. 

A vftable manipulálása a use-after-free sérülékenységek kihasználásának egyik leggyakoribb, nagyjából általánosan használható módszere, de természetesen a program más jellegű kódútvonalai is eltéríthetők hasonló módon.

A kérdés ezek után az, hogy hogyan érjük el, hogy a program lefoglalja nekünk éppen azt a felszabadított memóriaszeletet, amit később megpróbál újrahasznosítani. A válasz egyszerre nehéz és könnyű: Az operációs rendszerek általában többféle, viszonylag bonyolult memóriafoglalási algoritmust is használnak a töredezettség visszaszorítására, figyelembe véve az alkalmazás korábbi igényeit. Ez egy igen komplex rendszer képét festi elénk, de nem kell megijedni, az algoritmusoknak ugyanis megvan az az előnyük, hogy determinisztikusak, az adaptivitás pedig sok esetben lehetővé teszi, hogy kikényszerítsük a saját szájízünknek megfelelő stratégia alkalmazását (erről majd a későbbi részekben).

A mi primitív példánknál maradva elég végiggondolnunk, hogy miért okoz problémát a töredezettség: egy adott méretű memóriaszeletben a méretnél nagyobb adat nyilván nem tárolható, kisebb adat tárolása esetén pedig a maradék, fel nem használt helyet már kisebb eséllyel lehet a későbbiekben hasznosítani. A heap-menedzser számára tehát minden esetben az az optimális stratégia, ha igyekszik a tárolandó adat méretének éppen megfelelő helyet keresni. Az alábbi kis programot lefuttatva láthatjuk, hogy az a és p változók ugyanazt a címet kapják:

int main(){
A *a=new A();
a->vF1();
int size=sizeof(A);

printf("Address of a: %p - Size: %d \n",a,size);
free(a);
void *p=malloc(size);
printf("Address of p: %p - Size: %d \n",p,size);
memset(p,0x41,size);

a->vF1();
return 0;
}

Az access voilation:

(d8c.f44): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=006343e8 ebx=7ffd5000 ecx=006343e8 edx=41414141 esi=0018f6b8 edi=0018f7b8
eip=008f1612 esp=0018f6b8 ebp=0018f7b8 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010216
uaf_pub!main+0x102:
008f1612 8b02 mov eax,dword ptr [edx] ds:0023:41414141=????????

Az assembly kód gyakorlatilag megegyezik a fentivel:

 49 008f1608 8b45f8 mov eax,dword ptr [ebp-8]
49 008f160b 8b10 mov edx,dword ptr [eax]
49 008f160d 8bf4 mov esi,esp
49 008f160f 8b4df8 mov ecx,dword ptr [ebp-8]
49 008f1612 8b02 mov eax,dword ptr [edx]
49 008f1614 ffd0 call eax

Látható, hogy a második foglalással kontrolláljuk a dinamikusan kötött függvényhívást, így a megfelelő memóriacímek ismeretében általunk választott helyre irányíthatjuk a program futását. És itt sajnos képbe kerül az ASLR: ha mázlink van (és elég gyakran van), a célalkalmazás betölt néhány /DYNAMICBASE nélkül fordított könyvtárat, de ha nincs szerencsénk, akkor sem kell kétségbe esni: a következő részben - ahol már hús-vér böngészőkkel fogunk foglalkozni - megtudjátok miért :)

A posztban emlegetett példakódok elérhetők Gitoriouson.

A poszt összeállításához inspirációt és sok segítséget fb1h2s hasonló doksija adott. Kérdések, javaslatok (túlzottan/nem eléggé szájbarágós, hülyeségazegész, stb.) kommentben jöhetnek.

Címkék: tutorial use-after-free

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

arvabalazs 2013.01.20. 21:19:21

Szia

Nagyon jó cikk. Gratulálok és várom a folytatást!
new+free combo teljesen illegális, ilyet (remélhetőleg) pro developer nem ír. Szóval szerintem legyen new/delete vagy malloc/free. stackoverflow.com/questions/240212/what-is-the-difference-between-new-delete-and-malloc-free

Nem tudom mi lesz pontosan a folytatásokban, de nem ártana egy rész, ami arról szó, hogy hogyan előzhetők meg az efféle problémák (miért ne használjunk raw pointert, RAII, smart ptr). Illetve hogy igazából a problémakör a sz*r API-k miatt van, amikhez igazodni kell egy amúgy biztonságos programból.

Üdv

buherator · http://buhera.blog.hu 2013.01.20. 22:29:38

@arvabalazs: Kösz a visszajelzést, a kritika természetesen jogos - elég sok egyéb probléma is van ezekkel a kódokkal, remélem senki nem ezeket veszi majd alapul, ha c++ fejlesztésbe kezd :)

A javasolt témák beemelését nem terveztem (a sorozatot inkább offenzív jellegűnek szántam), de ha van kedved írni a témában, szívesen megjelenítek ilyen anyagot.

theshadow · http://hackstock.blog.hu/ 2013.01.22. 23:14:35

Nagyon hasznos cikk, szeretem az ilyen típusú ismeret bővítő leírásokat és a példákat.

Eddig is rendszeresen olvastam a blogod, de ezután kettőzött érdeklődéssel figyelem. :)