Jemny uvod do buffer overflow #2

21.01.2007 14:02 | blackhole_ventYl

V predoslej casti bolo povedane nieco o faktoroch prispievajucich k vzniku buffer overflow a k tomu, ze buffer overflow vznika tak jednoducho a moze byt taky zakerny. V tejto casti ukazem absolutny zaklad techniky zneuzivania buffer overflow a popisem aj najjednoduchsiu techniku obrany proti buffer overflow na urovni prekladaca.

Z predosleho dielu vieme, ze do nezapisovatelneho kodoveho segmentu sa da efektivne zapisovat a ze program moze nechtitac menit data pod vrcholom zasobnika. Zmizli teda vsetky zakonne bariery zabranujuce tomu, aby sme zmenili tok programu. Tento diel bude opat trocha dlhsi a ukazeme si v nom zakladnu techniku zneuzivania buffer overflowu.

Ked chceme utocit na nejaky program, ktory obsahuje bufer overflow zranitelnost, obvykle to robime preto, aby sme od toho nieco ziskali. Casto sa preto utoci na programy, ktore bezia s vysokymi pravomocami a ziadame od nich, aby budto vytvorili rootovsky shell, vytvorili suid binarku shellu (to vyzaduje mat lokalny neprivilegovany pristup k stroju), alebo spustili rootovsky shell presmerovany na siet. Ten, kto sa dostane k rootovskemu shellu je totizto v UNIXovom systeme prakticky nezastavitelny a jediny, kto ho dokaze zastavit, je nejaky bezpecnost zvysujuci patch v kerneli, alebo tvrdy reboot systemu, za predpokladu, ze si nevyrobil trvale zadne vratka.

Teraz pred nami vyvstavaju dva klucove problemy:

  1. ako dopravit do programu nas kod - ak chceme po programe nieco, co bezne nevie (a to je vcelku logicke, inac by nemalo velky zmysel nanho utocit), potrebujeme ho k tomu naprogramovat, teda dostat donho nas kod.
  2. ako prinutit program kod vykonat - ked uz tam ten kod mame, nejako ho musime vykonat, inac to takisto nema zmysel.

Tu uz je ale abstrakcia na skodu, preto si splasime ilustracny program, na ktorom budeme cely overflow ilustrovat. Uz z nazvu buffer overflow vyplyva, ze budeme potrebovat program, ktory obsahuje buffer, z toho, co sa pisalo v predoslom dieli vieme, ze tato buffer musi byt lokalna premenna vo funkcii, aby to splnilo svoj ucel.

Listing programu vulnerable.c:

#include <stdio.h>                                                           
int main (int argc, char * argv[]) {
    char buffer[20];
    gets(buffer);
    printf("%s\n", buffer);
    return 0;
}

Tento program nerobi na prvy pohlad nic zaujimave, nacita z klavesnice vstup, ulozi ho do buffra a nasledne ho vypise.

Poznamka: Na tomti mieste si dovolim malu odbocku a poznamenam, ze riadok obsahujuci printf mohol vyzerat aj takto:

printf(buffer);

ale to by vytvorilo priestor pre dalsiu zranitelnost, nakolko pripadne retazce %s by sa interpretovali ako zastupne znaky.

Skompilovat sa da jednoducho pomocou:

gcc -Wall -o vulnerable vulnerable.c

Uz pri kompilacii zistime, ze nieco nie je v poriadku, pretoze linker nas informuje:

/tmp/cc4ddiJO.o(.text+0x24): In function `main':
: warning: the `gets' function is dangerous and should not be used.

Je to prave tym, ze funkcia gets pri naplnani buffra neoveruje jeho hranice - nema ako, neboli jej zadane, preto kopiruje, kym ma co.

Jednoduche overenie existencie buffer overflow v tomto programe si mozeme predviest tak, ze ak napiseme ako vstup retazec dlzky 20 znakov, program v pohode zbehne, ale ked budeme zadavat retazec cim dalej, tym dlhsi, program spadne s hlaskou segmentation fault. Mne program spadol pri dlzke retazca 44 znakov.

Dost bolo teorie, loadneme si debugger a ideme zistovat, co a ako, najnapomocnejsi k tomu vsetkemu bude disassemblovany kod programu v okoli funkcie main:

    0x080483d4 <main+0>:    push  %ebp
    0x080483d5 <main+1>:    mov    %esp,%ebp
    0x080483d7 <main+3>:    sub    $0x28,%esp
    0x080483da <main+6>:    and    $0xfffffff0,%esp
    0x080483dd <main+9>:    mov    $0x0,%eax
    0x080483e2 <main+14>:  add    $0xf,%eax
    0x080483e5 <main+17>:  add    $0xf,%eax
    0x080483e8 <main+20>:  shr    $0x4,%eax
    0x080483eb <main+23>:  shl    $0x4,%eax
    0x080483ee <main+26>:  sub    %eax,%esp
    0x080483f0 <main+28>:  sub    $0xc,%esp
    0x080483f3 <main+31>:  lea    0xffffffd8(%ebp),%eax
    0x080483f6 <main+34>:  push  %eax
    0x080483f7 <main+35>:  call  0x80482c8 <gets@plt>
    0x080483fc <main+40>:  add    $0x10,%esp
    0x080483ff <main+43>:  sub    $0x8,%esp
    0x08048402 <main+46>:  lea    0xffffffd8(%ebp),%eax
    0x08048405 <main+49>:  push  %eax
    0x08048406 <main+50>:  push  $0x8048524
    0x0804840b <main+55>:  call  0x80482e8 <printf@plt>
    0x08048410 <main+60>:  add    $0x10,%esp
    0x08048413 <main+63>:  mov    $0x0,%eax
    0x08048418 <main+68>:  leave

Zmet instrukcii na zaciatku vytvara samotny stack frame (ulozi adresu stack frame-u do registra EBP - 0x080483d5), odrata od vrcholu zasobnika 0x28 - 0x080483d7 (velkost ulozenych dat lokalnych premennych), zaokruhli na celych 16 nadol - 0x080483da (s premennymi na hranici 16bytovych chunkov sa rychlejsie pracuje), nasledne vykona nejake magicke vypocty (nebudem sa tvarit, ze viem, na zaklade coho vznikli), a posunie vrchol zasobnika este o niekolko bajtov nizsie. Na adrese 0x080483f3 sa nacita adresa premennej buffer, ktora sa ulozi na vrchol zasobnika.

Hodnota v registri EAX po vykonani instrukcie na adrese 0x080483f3 je dolezita, pretoze rozdiel tejto hodnoty a hodnoty v registri EBP urcuje, kolko volneho miesta v stacku pre data je.

+0xbff25008 (EBP)
-0xbff24fe0 (EAX)
-----------
=0x00000028 volne miesto (40 bytov)

Miesto medzi zaciatkom premennej buffer a vrcholom zasobnika predosleho stack frame-u je 40 bytov. To je cislo vcelku blizke poctu znakov, ktore bolo treba k tomu, aby program zletel. Mne bolo treba znakov 44. Preco? Na zaciatku funkcie sa nachadza instrukcia, ktora uklada obsah registra EBP na vrchol zasobnika, co je vlastne adresa stack frame-u predoslej funkcie. Hned za nou (bola vlozena skor, preto je na vyssej adrese) je navratova adresa z nasej funkcie. Teda od zaciatku premennej buffer po prvy byte navratovej hodnoty je to 44 bytov volneho miesta. Retazec znakov v jazyku C je vsak ukonceny znakom \0, co je 45. znak, teda 44 znakovy retazec v skutocnosti zaberie 45 znakov (bytov) a prepise jeden byte navratovej adresy z funkcie, co sposobi pad aplikacie.

Potrebujeme teda 48 znakov dlhy retazec (44 znakov vypln, 4 znaky prepisu navratovu adresu) ukonceny \0, ktory umozni skok na nami volenu adresu.

Vytvorime si preto napriklad nasledovny subor shellcode.S:

start: nop
nop
(37 dalsich riadkov nop)
nop
nop
jmp near start
dd 0xbff24fe0

Tento shellcode (shellcode sa mu hovori preto, ze ak sa spravne napise a pouzije proti patricne opravnenemu programu, dostaneme shell s pravami roota) splna podmienky, pretoze ma 48 bytov, obsahuje 44 bytov vyplne (instrukcie nop znacia ziadnu cinnost), na konci je kod, ktory vykona skok na start (skok je blizky, cize nezalezi na adrese, skace sa relativne o nejaky pocet bytov) a na konci je adresa zaciatku tohto bloku v pamati (ziskana debuggerom z registra EAX). Tento shellcode je velmi primitivny (sposobi zacyklenie programu) a skompilujeme ho napriklad takto:

nasm shellcode.S

Z nasm nam vylezie 48 bytov velky subor shellcode. Spustime teda program vulnerable napriklad takto:

./vulnerable &lt; shellcode

Program vulnerable bude teda citat vstup zo suboru, kde mame shellcode. 44 bytov shellcode sa umiestni do buffra a prepise adresu stack frame-u predoslej funkcie a posledne 4 bajty prepisu navratovu adresu funkcie main(). Po jej skonceni program skoci (vrati sa) na zaciatok shellcodu a zostane v nom uzavrety v nekonecnej slucke.

To by bola teoria o tom, ako dostat do programu svoj vlastny shellcode a ako tento shellcode spustit. Tato teoria by za beznych okolnosti aj fungovala, keby boli ludia na patricnych miestach tupi a nerobili by proti tomu nieco (nastastie). Avsak dotycni si boli vedomi velkeho nebezpecia plynuceho z existencie buffer overflowu a zacali proti nemu nieco robit.

Samotny buffer overflow ma, ako uz bolo vyssie popisane, dva klucove momenty, a to su vlozenie cudzieho kodu a spustenie kodu.Zatial, co vlozeniu cudzieho kodu zabranit nemozeme, pretoze inac by v programe nesmela existovat buffer, mozeme aspon ciastocne zabranit spusteniu tohoto kodu. Najjednoduchsia metoda, ktora sa da aplikovat plosne na urovni prekladaca, je nemoznost urcit adresu ulozenych dat. Pri praci so zasobnikom plati, ze ak sa nemeni poradie vykonavanych instrukcii, je pozicia tych istych dat vzdy rovnaka oproti dnu zasobnika.

Ak by si niekto tento shellcode vyskusal na program vulnerable aplikovat, s najvyssou pravdepodobnostou by dosiel k zaveru, ze nefunguje a program padne namiesto toho, aby sa zacyklil. Ako sa ale pisalo v predoslom dieli, dno zasobnika u x86 nemozno nijako urcit, pretoze neexistuje ziadne vseobecne miesto, kde by sa dno zasobnika ukladalo. Dnesne prekladace preto pri inicializacii zasobnika tento neumiestnuju na pevne miesto v adresnom priestore, ale snazia sa jeho dno umiestnit pseudonahodne. Nezabrani to sice utocnikovi v zhodeni programu prepisom navratovej hodnoty, ale znizi jeho sance na uspesne spustenie kodu, pretoze musi utok previest niekolko desiatok az stovak krat na dostatocne velkom bufferi, aby mal sancu, ze sa navratovou adresou trafi do shellcodu.

V dalsom pokracovani preto uvediem niekolko dalsich metod, ktore sa pouzivaju k ochrane systemu pred zneuzitim tejto diery a niekolko dalsich metod zneuzitia buffer overflow, ktore ale uz nie su cisto o pretekani buffrov, zatial, co zvysuju sance utocnika na uspech.

    • Tak prave som si precital 23.01.2007 | 14:43
      gza   Návštevník

      Tak prave som si precital oba diely a som z toho paf, ale to bude asi preto ze neviem programovat ;]]
      -------
      I'm lowkey like seashells.

    • postreh 25.01.2007 | 07:02
      srigi   Návštevník
      Presne toto ma zaujimalo, po precitani #1. Mam buffer napr. cca 20 bajtof, povedzne, ze navratova instrukcia je na 24 byte v zasobniku (neberme do uvahy zarovnanie), cize mam 22 byte pre svoj kod (predpokladam, ze adresa pre navrat z funkcie je 32b). Ako kod viem co mam poslat, ale ako zistim co mam poslat ako adresu? OS predsa funkciu ulozi na nahodne miesto v pamati. A este som sa kcel spytat, ako su v takom Linuxe oddelene jednotlive segmenty. Pisal si, ze adresa vo flat modeli je ta ista pre kodovy, datovy a stack register. Ale Stack segment musi byt dost velke suvisle miesto.
      • musis poslat adresu zaciatku 25.01.2007 | 14:53
        Avatar borgcube   Používateľ
        musis poslat adresu zaciatku toho buffra, kde to ma skocit. takze adresou, kde zacina localny buffer prepises povodnu navratovu adresu.
      • re: 25.01.2007 | 15:26
        disorder   Návštevník

        > A este som sa kcel spytat, ako su v takom Linuxe oddelene jednotlive segmenty.

        AFAIK nijak, pretoze segmentacia je specialitou x86 architektury...

        > Pisal si, ze adresa vo flat modeli je ta ista pre kodovy, datovy a stack register. Ale Stack segment musi byt dost velke suvisle miesto.

        to nie je adresa, ale segment (suvisla oblast). vo flat modeli ma kazdy segment obsiahnutu celu pamat (na 32bit su to cele 4GB). ochrana pamate sa riesi len strankovanim (kde nie je namapovana stranka - a ani nebola alokovana, tam nic nezapises, vyvolas len SIGSEGV).

        ---
        Windows NT was supposed to hit Unix hard. It did - like a bug hitting a windshield.