Jemny uvod do buffer overflow #3

16.03.2007 17:16 | blackhole_ventYl

O najjednoduchsom pripade buffer overflow uz bolo v predoslych dvoch castiach co-to napisane. V tomto, uz poslednom, pokracovani mierne poodhalim metody, ake ine cesty mozno pouzit na prepasovanie shellcode do aplikacie a ako mozno samotny buffer overflow zistit a eliminovat uz v zarodku.

Ako som napisal uz minule, nas primitivny programcek vulnerable.c nie je az tak celkom primitivny, ako by sa mohlo zdat a shellcode shellcode.S v podstate nema sancu. Sanca na uspech sa zvysuje priamo umerne dlzke pouzitelneho buffra a poctu pokusov, ktore mozno vykonat.

Cim dlhsi je buffer, tym dlhsia moze byt sekcia zlozena z instrukcii NOP, cim sa zvysuje sanca, ze navratovy kod, ktory sa pouzije, sa do tejto sekcie trafi a kod sa vykona. A kedze bazova adresa zasobnika je pri kazdom spusteni programu na inej adrese, ale tento pocet adries je konecny, po istom pocte spusteni sa adresy musia zacat opakovat (aj ked toto cislo je velmi velke).

Kombinaciou tychto dvoch faktorov mozno pomerne jednoducho, ale nie velmi efektivne docielit, ze sa shellcode skor, ci neskor naozaj aj vykona.

Co ale robit, ked buffer nie je dostatocne dlhy na shellcode, alebo na patricne dlhu sekciu NOPov, aby sa do shellcodu dalo zo staku uspesne trafit? Treba najst inu cestu na dopravenie shellcodu do programu. Tychto ciest je hned niekolko, na rozdiel od pouzitia priameho buffer overflow-u je nutne mat moznost priamo spustit program, na ktory sa utoci. Ak tuto moznost mame, je mozne ulozit shellcode bud do premennej prostredia, alebo poslat shellcode programu priamo na prikazovom riadku ako parameter.

Pouzitie premennej prostredia je onieco jednoduchsie hlavne z toho dovodu, ze sa tato premenna da nastavit pomocou wrappera, pricom tlacit shellcode programu na prikazovy riadok moze byt znacne nepohodlne (okrem toho je obvykle dlzka prikazoveho riadka obmedzena na 32kB).

V oboch pripadoch vsak moze existovat (a s vysokou pravdepodobnostou aj bude) obmedzenie, ktore bude vyzadovat, aby shellcode neobsahoval byty s hodnotou 0x00. Znak 0x00 sa totizto pouziva ako terminator stringov, cize vacsina programov a kniznic prekopiruje premennu prostredia len po znak 0x00 (a ten zvysok nerozumie tomu, co je to null terminated string). Pre premenne prostredia v pamati plati to iste, co pre parametre programu a pre zasobnik, a to, ze su dynamicky alokovane, cize ich adresa nie je pevna. Ak vsak mame moznost premenne prostredia programu ovplyvnit, neexistuje teoreticke obmedzenie dlzky shellcodu do premennej prostredia ulozenej.

Kod v premennej prostedia mozno nasledne vhodne kombinovat so shellcode ulozenom na zasobniku. Kedze do premennej prostredia nemozeme vlozit znak 0x00, rozsah cinnosti sa dost podstatne redukuje. Je vsak mozne vyuzit kod vlozeny do premennej prostredia ako stubloader k shellcodu ulozenemu na zasobniku. Ak sa nam podari navratovou adresou zo zasobnika trafit do shellcodu ulozeneho v premennej prostredia, tato vie pomerne presne urcit, kde sa shellcode nachadza.

Uvazujme nasledujucu situaciu: do buffra ulozime nas shellcode, ktory bude obsahovat kod povedzme na vytvorenie suid binarky shellu. Tento kod bude mat 512 bytov vratane prepisu navratovej adresy. Pri navrate z funkcie sa navratova adresa precita a SP sa posunie tak, ze bude ukazovat na prvy byte nad koncom shellcodu. Ak v tomto momente preberie riadenie shellcode ulozeny v premennej prostredia, v podstate mu staci vykonat nasledovne:

    nop
    nop
    nop
    ...
    nop
    nop        ; dostatok NOPov, aby bol terc pekne velky
    nop        ; a viditelny aj z dialky
    sub sp, 512
    jmp word sp;

Kod od vrcholu zasobnika odrata 512 bytov, cim dostane ukazatel na zaciatok shellcode a potom na tuto adresu skoci. Teoreticky by mohol namiesto kratkeho skoku (ono sa sice bude skakat o nejake tie tri 3GB dalej, ale on je to skok kratky) pouzit aj kratke volanie podprogramu (call near), co v tomto pripade vyjde rovnocenne, pretoze posledne 4 byty shellcode su v tomto momente uz nepouzitelne. Vznikne vsak jeden problem inde. Cislo 512 ma dolnych 9 bitov nulovych, cize niekde sa nutne musi zjavit nulovy byte, ktory cely pokus o skok do shellcode na zasobniku zmari.

Rieseni moze byt niekolko. Jednak je mozne na zaciatok shellcode vlozit nevyznamnu instrukciu dlzky 1 byte a posunut SP dozadu iba o 511 bytov (co je pekne cislo plne jedniciek). Druhym sposobom je odcitanie rozdelit na dve po sebe iduce odcitania o taky pocet bytov, ze v sucte daju 512, ale ani v jednom odcitani nebude figurovat nulovy byte. A tretim a najkomplikovanejsim sposobom je pouzitie logickych operacii na dosiahnutie Zelaneho vysledku. Kuprikladu mozno vygenerovat seriu instrukcii XOR, SHL a INC tak, ze v niektorom registri si shellcode pozadovane cislo vytvori a nasledne od registra SP toto cislo odcita.

    XOR EAX, EAX; vynulujeme register EAX bez pouzitia nuloveho bytu
    INC EAX; nastavime LSB na 1
    SHL EAX, 9; posunieme tento jednotkovy bit na poziciu 10

Vysledne cislo je 512, teda to, co sme chceli, a ani to velmi nebolelo. Instrukcia XOR EAX, EAX (pripadne iny register pouzity dvakrat) je vcelku oblubena u polymorfnych virusov a ineho bordelu (takze si mozete pravoplatne pripadat ako hacker - nulujete registre pomocou XORu), ktora sluzi za ucelom obabrania heuristickej analyzy, alebo virusovych vzoriek, pretoze nie kazdeho musi hned napadnut, ze XORovanim si niekto bude nulovat register, co je pri pouziti MOV EAX, 0 zjavne uz na prvy pohlad. Oproti instrukcii MOV EAX, 0 ma este jednu vyhodu, a to tu, ze je skoro o polovicu kratsia, co sa virusom v case svojej najvacsej slavy hodilo. Kuprikladu nasledovny kod vykona odcitanie 512 od registra ESP (32bitovy stack pointer) a neobsahuje znak 0x00. Nezohladnuje carry pri podteceni z vyssej polovice registra do nizsej, ale principialne nie je problem ho tam doplnit. Kod odcita cisla po polregistroch, pretoze vacsina operacii nad 32bitovymi registrami generuje nulovy byte:

    XOR BX, BX
    SHL EBX, 16
    MOV AX, SP
    SUB AX, BX
    SHR SP, 16
    MOV CX, SP
    SHR EBX, 16
    SUB CX, BX
    MOV SP, CX
    SHL SP, 16
    MOV SP, AX
    JMP WORD SP

Nutno uz len poznamenat, ze v pripade, ze niekde vo vnutri tohto kodu dojde k preruseniu, je vcelku mozne, ze obsluha prerusenia prepise cast programu, alebo dovedie program ku padu.

Ak vsak nie je mozne realizovat ulozenie dlheho shellcodu do zasobnika, je mozne uchylit sa este k vacsej sprostarni, napriklad shellcode v premennej prostredia encodovat tak, aby aj pripadne nulove byty boli mofikovane tak, ze nulove nebudu a tento kod potom pred spustenim odkodovat (na tomto mieste sa kodovanim nemysli sifrovanie!).

Fakt, ze stack, premenne prostredia, ani premenne s parametrami programu nemaju pevnu adresu mozno niekedy eliminovat na zaklade toho, ze sa adresa niektorej z premennych objavi v niektorom registri zabudnuta po predoslej operacii. Pomocou navratovej adresy sice nemozno skocit na adresu ulozenu v registri, ale mozno skocit na instrukciu, ktora vyvola skok na adresu ulozenu v danom registri. Taka instrukcia v programe s vysokou pravdepodobnostou nebude priamo, ale moze sa nachadzat vovnutri, alebo na hranici inych instrukcii a obvykle ma pevnu adresu v ramci programu. Preto treba v programe hladat retazec zodpovedajuci pozadovanej premennej a nasledne testovat, ci je jeho adresa pevna. Ak ano, je vyhrane.

Co sa tyka ochrany pred buffer overflow, celu ochranu mozno zhrnut na jednu jedinu operaciu: zabranit pouzitiu prepisanej navratovej adresy. Nie sice priamo toto, ale znemoznenie jednoducheho skoku na zasobnik vykonava uz samotny stubloader programu, kedze generuje adresu zasobnika programu vzdy na nahodne miesto blizko vrcholu pamati procesu, cize adresa tych istych dat na zasobniku je vzdy ina. Nieco ramcovo podobne sa da robit aj na urovni stack frames. Stack frame totizto nemusi mat vzdy rovnaku velkost, cize nie vzdy je nutny rovnaky pocet bytov na uspesny prepis navratovej adresy. To so sebou obvykle nesie dvojite znizenie vykonu, pretoze ak ma byt rozmanitost velkosti stack frame velka, budto musia byt rozdiely velke, alebo sa musi porusit zarovnanie frame-u na 16bytovu hranicu, co v jednom pripade zvysi spotrebu pamate (to je podstatne hlavne u rekurzivnych algoritmov), v druhom pripade znizi rychlost prace s datami na zasobniku na urovni procesora.

Druhym sposobom by bolo znemoznenie vykonavania kodu ulozeneho v zasobniku, co sa sice lahko povie a aj relativne lahko vykona, ale nepaci sa to niektorym programom prelozenym pomocou GCC, ktore vykonatelny kod na zasobnik ukladaju legitimne.

Preto sa zauzival este jeden postup, tzv. ukladanie kanarika na zasobnik. Pri prepise navratovej adresy je nutne prepisat vsetko medzi buffrom a navratovou adresou. Je teda mozne v oblasti medzi lokalnymi premennymi a koncom stack frame-u vytvorit rezervovanu oblast, do ktorej sa normalne nebude zapisovat a tesne pred navratovu adresu ulozit kanarika. Kanarik je cislo lubovolnej bitovej sirky (obvykle 16, alebo 32 bitov), ktore sa generuje z nejakych uz znamych udajov a uklada sa na zasobnik do priestoru pred navratovu adresu. Najjednoduchsou formou kanarika moze byt 32bitova hodnota obsahujuca svoju vlastnu adresu. Takuto hodnotu mozno ziskat pomerne jednoducho ako ESP - 4. Po ukonceni behu funkcie nasledne staci overit, ci kanarik obsahuje svoju vlastnu adresu, a ak nie, program na tomto mieste zostrelit s chybou pretecenie bufferu. To eliminuje zneuzitie buffer overflow na jednu jedinu moznu adresu bazy zasobnika, preto sa obvykle pouzivaju kanariky sofistikovanejsie, ktore pocitaju hodnotu "kanarikovych bytov" z inych znamych hodnot, alebo nahodne generuju a ukladaju do struktury FIFO. Hodnota kanarika sa potom testuje samostatnou funkciou, ktora sa vola tesne pred opustenim osetrovanej funkcie, co do znacnej miery spomajule samotny program. Tuto metodu s oblubou vyuziva Microsoft Visual C kompiler.

Najlepsou metodou ochrany je vsak presne si vsade kontrolovat, kolko bytov sa kopiruje a to, ci cielova buffer je dostatocne velka, pretoze pretecenie buffra nemusi nutne fungovat na bezprostrednej dvojici buffer - navratova adresa, ale moze prepisovat lokalne pointre v ramci funkcie a robit veci priam az nepredstavitelne.

Buffer overflow ako taky je jedna zo zrejme najdlhsie existujucich zranitelnosti programov pisanych v jazyku C a vyzera to, ze ani tak skoro uplne nevymizne, vzhladom k velkosti skaly ukonov, ktore moze taky buffer overflow sposobit a mnozstvu sposobov, akym moze byt vyvolany.

Toto bol jemny uvod do problematiky pretekania buffrov, problemov s tym sposobenych a ochrany proti tomuto neduhu.

    • Re: Jemny uvod do buffer overflow #3 17.03.2007 | 15:14
      Avatar vid   Používateľ

      velmi pekny clanok

      Druhym sposobom by bolo znemoznenie vykonavania kodu ulozeneho v zasobniku, co sa sice lahko povie a aj relativne lahko vykona, ale nepaci sa to niektorym programom prelozenym pomocou GCC, ktore vykonatelny kod na zasobnik ukladaju legitimne.
      fiha. to v akom pripade robi, a preco?

      Instrukcia XOR EAX, EAX (pripadne iny register pouzity dvakrat) je vcelku oblubena u polymorfnych virusov a ineho bordelu (takze si mozete pravoplatne pripadat ako hacker - nulujete registre pomocou XORu), ktora sluzi za ucelom obabrania heuristickej analyzy, alebo virusovych vzoriek, pretoze nie kazdeho musi hned napadnut, ze XORovanim si niekto bude nulovat register, co je pri pouziti MOV EAX, 0 zjavne uz na prvy pohlad. Oproti instrukcii MOV EAX, 0 ma este jednu vyhodu, a to tu, ze je skoro o polovicu kratsia, co sa virusom v case svojej najvacsej slavy hodilo.

      pouzivanie XORu na nulovanie registra je uplne legitmny sposob. Zdaleka to nebolo len pre virusy, pouzival to KAZDY. Je to rychlejsie, a nove procesory uz maju zabudovanu podporu takehoto nulovania. Viac k teme nulovania registra tu.

      ... pretoze vacsina operacii nad 32bitovymi registrami generuje nulovy byte:
      v tomto nastastie nemas pravdu, iba operacie ktore obsahuju 32bit immediate (konstantu) zvyknu mat nulovy byte. Samotne operacie bez konstant nulovy byte obsahuju malokedy. Preto aj ten kod na odcitanie je trochu nesikovny, kludne to ide takto:

      xor eax, eax ;31 C0
      inc eax ;40
      shl eax, 9 ;C1 E0 09
      sub esp, eax ;29 C4

      alebo:
      xor eax, eax ;31 C0
      bts eax, 9 ;0F BA E8 09
      sub esp, eax ;29 C4

      alebo aj s konstantami:
      sub esp, 512+01010101h ;81 EC 01 03 01 01 01
      ;ak tu nastane prerusenie sme v riti
      add esp, 01010101h ;81 C4 01 01 01 01

      Osobne by ma skor trapilo aby zopar poslednych bytov bolo mensich ako 0x80, aby to nebol pri UTF-8 nekompletny string.

      • Re: Jemny uvod do buffer overflow #3 17.03.2007 | 19:27
        Avatar blackhole_ventYl   Používateľ

        Viem, ze pouzivanie XORu na nulovanie registra je uplne legitimne :) Toto som tam viacmenej len vsunul ako vsuvku, kde sa to pouzivalo tak trocha zvlastne a aky bol zamer.

        Co sa tyka tych instrukcii, ano... kym si umelo vsetky bity hodnej polovice registra vynulujes pri immediate, nebudes mat nuly, inac ich mat budes. Ten tvoj prvy listing som uvadzal, ten druhy je to iste v pepitovom (povodne som instrukciu BTS nepoznal, ale moze buci) a ten posledny sposob je tam popisany tiez, ale slovne. Ale inac mas pravu, mne to len pri skusani v NASMe v niektorych pripadoch generovalo nuly, tak som zvolil ten neohrabany sposob. Ten posledny sposob je najelegantnejsi.

        A co sa tyka toho UTF-ka, tak to, co je za koncom STUBu shellcode uz nikoho netrapi, takze si tam kludne mozes dat aj UTF kompatibilizujuci string :)

        Ad ukladanie funkcii na zasobnik, ja konkretne neviem, preco to GCC robi, ale je to tak, tym funkciam sa hovori trampolines (http://www.haible.de/bruno/documentation/ffcall/trampoline/trampoline.ht...,
        http://www.haible.de/bruno/documentation/ffcall/trampoline/trampoline.ht...) a nejake patche mas naptriklad tuto http://gcc.gnu.org/ml/gcc-patches/2003-06/msg00302.html. Ak bol konkretne tento patch uz do GCC zaradeny, tak potom nove GCCcka nastavuju stack ako non-executable, ak program trampolines nepouziva a tudiz defaultne execovatelny stack nepotrebuje.
        ---
        Cuchat s nadchou, to je ako sniffovat bez promiscu.

        --- Cuchat s nadchou, to je ako sniffovat bez promiscu.
    • Re: Jemny uvod do buffer overflow #3 18.03.2007 | 18:04
      Avatar vid   Používateľ

      ohladne tych nulovych bytov v kode:
      na pisanie tych shell kodu mozete pouzit FASM, a v nom tento kus kodu ktory overi ci sa vo vystupe nenachadzaju nulove byty:

      repeat $-$$
        load x byte from $$+%-1
        if x=0
          'a je to fpici'
        end if
      end repeat

      treba to dat nakoniec