In diesem Kapitel werden die Themen Buffer Overflow und Memory Leaks besprochen. Es soll gezeigt werden, dass diese Probeme kein Mythos sind und wie man diese Vermeiden kann. C C++ C/C++ Sicherheit Buffer Overflow Memory Leaks Sicherheit - Buffer Overflow Memory Leaks Kapitel 26: Sicheres Programmieren

In diesem Kapitel werden zwei Themen angesprochen, die vielleicht auf den ersten Blick nicht allzu interessant erscheinen: Buffer Overflow und Memory Leaks. Da diese beiden Probleme jedoch leider häufiger in Erscheinung treten, sollte sich jeder ernsthafte Programmierer mit ihnen auseinander setzen.

Ein gern übersehener Aspekt ist die sicherheitsbezogene Programmierung. Programmierer setzen dabei Funktionen ein, von denen sie zwar wissen, dass diese nicht ganz sicher sind, aber sie wissen nicht, was diese unsicheren Funktionen bewirken können. Sie haben nach langjähriger Programmiererfahrung dann zwar jeden Algorithmus im Kopf, und ihnen kann keiner etwas vormachen, Sie verwenden aber trotzdem weiter diese Funktionen, weil Sie sie eben immer verwenden und dabei immer noch nicht genau wissen, was daran so schlimm sein soll. Denn das Programm läuft doch. Richtig? - Nein, falsch!

Auch wenn der Konkurrenzkampf und der Zeitdruck bei der Fertigstellung eines Projekts heutzutage enorm ist, sollten Sie diese Einstellung überdenken und sich ernsthaft mit diesem Thema befassen.

Diese zunächst unscheinbaren Unsicherheiten von Beginn an zu berücksichtigen ist ein Bestandteil von vorausschauender Programmentwicklung und trägt wesentlich zur Qualitätssicherung Ihrer Programme bei. Auf diese Weise begegnen Sie unvorhersehbarem Ärger und nachträglich entstehenden hohen Kosten schon im Vorfeld.

Ein Szenario: Sie haben für eine Firma ein Programm zur Verwaltung von Daten geschrieben. In der Firma finden sich einige gewiefte Mitarbeiter, die einen Weg gefunden haben, mithilfe Ihres Programms aus dem Verwaltungsprogramm zu springen, somit ins System zu gelangen und allerlei Unfug anzurichten. Der Kunde wird mit Sicherheit kein Programm mehr von Ihnen entwickeln lassen. Also haben Sie auf jeden Fall schon einen Imageschaden. Da Sie aber versprochen haben, sich um das Problem zu kümmern, müssen Sie alles andere erst einmal stehen und liegen lassen. Damit haben Sie schon kostbare Zeit verloren, die Sie für andere Projekten hätten nutzen können. Da noch weitere Kunden dieses Produkt verwenden, müssen Sie auch sie informieren.

Jetzt ist es an der Zeit, ein Bugfix (Patch) zu schreiben, den der Kunde einspielen muss, um den Fehler zu beheben. Wenn Sie Glück haben, kann der Kunde das Programm unterbrechen und den Patch einspielen. Sollte der Kunde aber rund um die Uhr auf das Programm angewiesen sein, entstehen diesem Ausfallkosten.

Nachdem Sie den Patch aufgespielt haben, treten andere unerwartete Probleme mit dem Programm auf. Somit folgt dem Patch ein weiterer, womit wieder Zeit, Geld und Image verloren gehen. Ich denke, dass jedem schon einmal ein ähnliches Szenario mit einem Programm widerfahren ist.

Die meisten solcher Sicherheitsprobleme treten mit Programmen auf, die in C geschrieben wurden. Dies heißt allerdings nicht, dass C eine unsichere Sprache ist, sondern es bedeutet nur, dass sie eine der am häufigsten eingesetzten ist. Viele Systemtools, Server, Datenbanken, aber auch grafische Oberflächen sind in C geschrieben. Sie sehen also, dass es sich durchaus lohnt, diese Themen aufzugreifen und bei der Entwicklung von Programmen zu berücksichtigen.

26.1. Buffer Overflow (Speicherüberlauf)            zurück  zum Inhaltsverzeichnis

Eines der bekanntesten und am häufigsten auftretenden Sicherheitsprobleme ist der Buffer Overflow (dt. Speicherüberlauf, Pufferüberlauf), häufig auch als "Buffer Overrun" bezeichnet. Geben Sie einmal in einer Internet-Suchmaschine den Begriff "Buffer Overflow" ein, und Sie werden überrascht sein, angesichts der enormen Anzahl von Ergebnissen. Es gibt unzählige Programme, welche für einen Buffer Overflow anfällig sind. Das Ziel des Angreifers ist es dabei, den Buffer Overflow auszunutzen, um in das System einzubrechen.

Aufgabe dieses Kapitels ist es nicht, Ihnen beizubringen, wie Sie Programme hacken können, sondern zu erklären, was ein Buffer Overflow ist, wie dieser ausgelöst wird und was Sie als Programmierer beachten müssen, damit Ihr Programm nicht anfällig dafür ist.

Für den Buffer Overflow ist immer der Programmierer selbst verantwortlich. Der Overflow kann überall dort auftreten, wo Daten von der Tastatur, dem Netzwerk oder einer anderen Quelle aus in einen Speicherbereich mit statischer Größe ohne eine Längenüberprüfung geschrieben werden. Hier ein solches Negativbeispiel:

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

int main()
{
   char *str = "0123456789012";
   char buf[10];

   strcpy(buf, str);
   printf("%s",buf);
   return 0;
}

Hier wurde ein Buffer Overflow mit der Funktion strcpy() erzeugt. Es wird dabei versucht, in den char-Vektor, der Platz für 10 Zeichen reserviert hat, mehr als diese 10 Zeichen zu kopieren.

Abbildung 26.1: Pufferüberlauf mit der Funktion strcpy()

Abbildung 26.1: Pufferüberlauf mit der Funktion strcpy()

Die Auswirkungen eines Buffer Overflows sind stark vom Betriebssystem abhängig. Häufig stürzt dabei das Programm ab, weil Variablen mit irgendwelchen Werten überschrieben wurden. Manches Mal bekommen Sie aber auch nach Beendigung des Programms eine Fehlermeldung zurück, etwa Speicherzugriffsfehler. Dies wird ausgegeben, wenn z.B. die Rücksprungadresse des Programms überschrieben wurde, und das Programm irgendwo in eine unerlaubte Speicheradresse springt.

Wird aber bewusst diese Rücksprungadresse manipuliert und auf einen speziell von Ihnen erstellen Speicherbereich verwiesen bzw. gesprungen, welcher echten Code enthält, haben Sie einen so genannten Exploit erstellt.

26.1.1 Speicherverwaltung von Programmen
Ein Programm besteht aus drei Speicher-Segmenten, die im Arbeitsspeicher liegen. Der Prozessor (CPU) holt sich die Daten und Anweisungen aus diesem Arbeitsspeicher. Damit der Prozessor unterscheiden kann, ob es sich bei den Daten um Maschinenbefehle oder den Datenteil mit den Variablen handelt, werden diese Speicherbereiche in einzelne Segmente aufgeteilt. Hier die grafische Darstellung der einzelnen Segmente:

Datensegmente eines Programms

Abbildung 26.2: Datensegmente eines Programms

Es sei hierbei noch erwähnt, dass der Stack-Bereich nach unten und der Heap nach oben anwächst. Der Stack ist auch das Angriffsziel für einen Buffer Overflow.

26.1.2 Der Stack-Frame
Für jede Funktion steht ein so genannter Stack-Frame im Stack zur Verfügung, worin die lokalen Variablen gespeichert werden. Wichtiger noch, im Stack befinden sich Registerinhalte des Prozessors, die vor dem Funktionsaufruf gesichert wurden, welche nötig sind, um bei Beendigung der Funktion auf die aufrufende Funktion zurückspringen zu können. Beispielsweise wird in der main()-Funktion die Funktion mit den Parametern my_func(wert1, wert2) aufgerufen:

#include <stdio.h>

void my_func(int wert1, int wert2)
{
   int summe;
   summe = wert1+wert2;
   printf("Summe: %d \n",summe);
}

int main()
{
   my_func(10,29);
   return 0;
}

Dies geschieht jetzt - ohne zu sehr ins Detail zu gehen - in folgenden Schritten auf dem Stack:

26.1.3 Rücksprungadresse manipulieren
In diesem Abschnitt folgt ein Beispiel, das zeigt, wie die Rücksprungadresse manipuliert werden kann. Es ist hierbei nicht Ziel und Zweck, Ihnen eine Schritt-für-Schritt-Anleitung zur Programmierung eines Exploits an die Hand zu geben und bewusst einen Buffer Overflow zu erzeugen, sondern eher soll Ihnen vor Augen geführt werden, wie schnell und unbewusst kleine Unstimmigkeiten im Quellcode Hackern Tür und Tor öffnen können - einige Kenntnisse der Funktionsweise von Assemblern vorausgesetzt.

Zur Demonstration des folgenden Beispiels werden der Compiler gcc und der Diassembler objdump verwendet. Das Funktionieren dieses Beispiels ist nicht auf allen Systemen garantiert, da bei den verschiedenen Betriebssystemen zum Teil unterschiedlich auf den Stack zugegriffen wird.

Folgendes Listing sei gegeben:

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

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

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

Übersetzen Sie das Programm und verwenden Sie anschließend den Diassembler, um sich den Maschinencode und den Assemblercode des Programms anzusehen. Hierfür wird der Diassembler objdump verwendet, welcher auf fast jedem System vorhanden sein dürfte. Rufen Sie den Diassembler mit folgender Option in der Kommandozeile auf:

objdump -d programmname

Jetzt sollte in etwa folgende Ausgabe auf dem Bildschirm erscheinen (gekürzt):

…
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. In der Adresse "08048490" fängt in diesem Beispiel die Funktion overflow() an. Diese Adresse wurde zuvor etwa von der main()-Funktion mit

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

aufgerufen. In der zweiten Spalte befindet sich der Maschinencode (Opcode). Dieser Code ist schwer für den Menschen nachvollziehbar. Aber alle Zahlen haben ihre Bedeutung. So steht z.B. die Zahl "55" für push %ebp als das Sichern des Basis-Pointers auf dem Stack, "5d" entfernt den Basis-Pointer 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 dritten Spalte befindet sich der Assmblercode, beispielsweise:

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

Es ist wichtig, zu verstehen, wie oder besser gesagt woraus ein Programm eigentlich besteht. Ein einfaches C-Konstrukt wie die for-Schleife wird z.B. in hunderte kleine Maschinencodes (Opcodes) zerlegt. Vielleicht wissen Sie nun, wenn Sie das nächste Mal mit einem Hexeditor ein Programm öffnen, ein bisschen mehr darüber, was diese Zahlen (Maschinencode) und Zeilen (Adressen) bedeuten.

Um es gleich vorwegzunehmen. Dies hier wird kein Assembler-Kurs oder Ähnliches. Das Thema ist recht komplex. Möchten Sie dennoch etwas mehr über Assembler erfahren, ohne aber gleich professionell programmieren zu wollen, so finden Sie im Anhang weiterführende Links und Literatur dazu.

Übersetzen Sie das Programm von eben nochmals mit

gcc -S -o programm.s programm.c 

Jetzt befindet sich im Verzeichnis eine Assemblerdatei (*.s oder *.asm) des Programms. Wir wollen uns diese in gekürzter Fassung ansehen:

main:
pushl %ebp        ;Framepointer auf dem Stack
movl  %esp, %ebp  ;Stackpointer(esp) in Framepointer(ebp) kopieren
subl  $8, %esp    ;Stackpointer um 8 Bytes verringern
subl  $12, %esp   ;Stackpointer um 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 um 16 Bytes erhöhen
call  overflow    ; overflow aufrufen, 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 ist ein kleiner Überblick über die Assembler-Schreibweise des Programms. Hier ist ja nur die Rücksprungadresse des Aufrufs call overflow von Interesse.

Da Sie jetzt wissen, wie Sie an die Rücksprungadresse eines Programms herankommen, können Sie nun ein Programm schreiben, bei dem der Buffer Overflow, welcher ja hier durch die Funktion strcpy() ausgelöst wird, zum Ändern der Rücksprungadresse genutzt wird. Es wird dabei im Fachjargon von Buffer overflow exploit gesprochen. Bei dem folgenden Beispiel soll die Rücksprungadresse manipuliert werden:

#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);
}

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

Zustand es Stacks

Abbildung 26.3: Zustand des Stacks

Wie kommen Sie nun am einfachsten zur Rücksprungadresse? Mit einem Zeiger. Also benötigen Sie zuerst einen Zeiger, der auf diese Rücksprungadresse verweist. Anschließend manipulieren Sie die Adresse der Rücksprungadresse, auf die der Pointer zeigt, und zwar so, dass 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 auf dem Stack 4 Bytes zurück
      Sollte jetzt auf die Rücksprungadresse zeigen */
   pointer=&tmp-1;
   /*Rücksprungadresse, auf die Pointer zeigt, 10 Bytes weiter*/
   *pointer=*pointer+10;
}

int main(void)
{
   int a;
   a=0;
   funktion(7,"hallo");
   a=1;
   printf("wert = %d\n",a);
   return 0;
}
Die einfachste Möglichkeit, auf die Rücksprungadresse zurückzugreifen, besteht darin, um die Speichergröße der Variable temp in der Funktion rückwärts zu springen.
pointer=&tmp-1;  

Manipulation der Rücksprungadresse

Abbildung 26.4: Manipulation der Rücksprungadresse

Jetzt können Sie die Rücksprungadresse manipulieren, auf die der pointer zeigt:

*pointer=*pointer+10; 

Warum habe ich hier die Rücksprungadresse um 10 Bytes erhöht? Dazu müssen Sie wieder objdump einsetzen (ohne Opcodes im Beispiel):

objdump -d programmname
080484e0 <main>:
…
 80484f7:   call   8048490 <funktion>  ;Aufruf funktion
 80484fc:   add    $0x10,%esp       ;Stack wieder freigeben
 80484ff:   movl   $0x1,0xfffffffc(%ebp)   ;wert=1
 8048506:   sub    $0x8,%esp
 8048509:   pushl  0xfffffffc(%ebp)    ;printf vorbereiten
 804850c:   push   $0x804859e
 8048511:   call   8048360 <_init+0x58> ;printf aufrufen
…

Die zu überspringende Adresse liegt in diesem Fall ja zwischen "80484ff" und "8048509". Somit ergibt sich folgende Rechnung:

8048509 - 80484ff = A

A ist der hexdezimale Wert für 10. Hiermit haben Sie die Rücksprungadresse Ihres eigenen Programms manipuliert. Ziel dieser Manipulation ist es aber selten (wie hier dargestellt), die Rücksprungadresse zu manipulieren, um den Programmcode an einer beliebigen Stelle weiter auszuführen, sondern meistens wird dabei die CPU mit einem eigenen Maschinencode gefüttert. Dabei wird der Maschinencode in einer Variablen auf dem Stack geschrieben und die Rücksprungadresse auf die Startadresse eines fremden Programmcodes gesetzt. Hat der fremde Maschinencode keinen Platz in der Variable, kann auch der Heap verwendet werden.

Beendet sich hierbei die Funktion, wird durch RET auf die Rücksprungadresse gesprungen, welche Sie bereits manipuliert haben, und der Hacker kann nun bestimmte Codesequenzen ausführen.

Ihnen dies jetzt hier zu demonstrieren, würde zum einen den Umfang des Kapitels bei weitem sprengen und vor allem am Thema vorbeigehen. Zum anderen würde dies neben der gründlichen Kenntnis von C auch gute Kenntnisse im Assembler-Bereich (und unter Linux u.a. auch der Shell-Programmierung) erfordern. Weiterführende Links und Literaturempfehlungen zum Thema "Buffer Overflow" selbst finden Sie im Anhang.

Zusammengefasst lassen sich Buffer Overflows für folgende Manipulationen ausnutzen:

26.1.4 Gegenmaßnahmen zum Buffer Overflow während der Programmerstellung
Steht Ihr Projekt in den Startlöchern, haben Sie Glück. Wenn Sie diesen Abschnitt durchgelesen haben, ist das Gefahrenpotenzial recht gering, dass Sie während der Programmerstellung eine unsichere Funktion implementieren.

Die meisten Buffer Overflows werden mit den Funktionen der Standard-Bibliothek erzeugt. Das Hauptproblem dieser unsicheren Funktionen ist, dass keine Längenüberprüfung der Ein- bzw. Ausgabe vorhanden ist. Daher wird empfohlen, sofern diese Funktionen auf dem System vorhanden sind, alternative Funktionen zu verwenden, die diese Längenüberprüfung durchführen. Falls es in Ihrem Programm auf Performance ankommt, muss jedoch erwähnt werden, dass die Funktionen mit der n-Alternative (etwa strcpy -> strncpy) langsamer sind als die ohne. Hierzu folgt ein Überblick zu anfälligen Funktionen und geeigneten Gegenmaßnahmen, die getroffen werden können.

Unsicheres Einlesen von Eingabestreams

Unsichere Funktion Gegenmaßname
gets(puffer); fgets(puffer, MAX_PUFFER, stdin);
Bemerkung: Auf Linux-Systemen gibt der Compiler bereits eine Warnmeldung aus, wenn die Funktion gets() verwendet wird. Mit gets() lesen Sie von der Standardeingabe bis zum nächsten ENTER einen String in einen statischen Puffer ein. Als Gegenmaßnahme wird die Funktion fgets() empfohlen, da diese nicht mehr als den bzw. das im zweiten Argument angegebenen Wert bzw. Zeichen einliest.

Tabelle 26.1: Unsichere Funktion - gets()

Unsichere Funktion Gegenmaßname
scanf("%s",str); scanf("%10s",str);
Bemerkung: Auch scanf() nimmt keine Längenprüfung vor bei der Eingabe. Die Gegenmaßname dazu ist recht simpel. Sie verwenden einfach eine Größenbegrenzung bei der Formatangabe (%|SIZE|s). Selbiges gilt natürlich auch für fscanf().

Tabelle 26.2: Unsichere Funktion - scanf()

Unsichere Funktionen zur Stringbearbeitung

Unsichere Funktion Gegenmaßname
strcpy(buf1, buf2); strncpy(buf1, buf2, SIZE);
Bemerkung: Bei strcpy() wird nicht auf die Größe des Zielpuffers geachtet, mit strncpy() hingegen schon. Trotzdem kann mit strncpy() bei falscher Verwendung ebenfalls ein Buffer Overflow ausgelöst werden:char buf1[100]='\0';char buf2[50];fgets(buf1, 100, stdin);/* buf2 hat nur Platz für 50 Zeichen */strncpy(buf2, buf1, sizeof(buf1));

Tabelle 26.3: Unsichere Funktion - strcpy()

Unsichere Funktion Gegenmaßname
strcat(buf1 , buf2); strncat(buf1, buf2, SIZE);
Bemerkung: Bei strcat() wird nicht auf die Größe des Zielpuffers geachtet, mit strncat() hingegen schon. Trotzdem kann mit strncat() bei falscher Verwendung wie schon bei strncpy() ein Buffer Overflow ausgelöst werden.

Tabelle 26.4: Unsichere Funktion - strcat()

Unsichere Funktion Gegenmaßname
sprintf(buf, "%s", temp); sprintf(buf, "%100s", temp);// oder snsprintf();
Bemerkung: Wie schon bei scanf() kann hier eine Größenbegrenzung verwendet werden oder alternativ die n-Variante snprintf(). Gleiches gilt übrigens auch für die Funktion vsprintf(). Auch hier können Sie sich zwischen der Größenbegrenzung und vsnprintf() entscheiden.

Tabelle 26.5: Unsichere Funktion - sprintf()

Unsichere Funktionen zur Bildschirmausgabe

Unsichere Funktion Gegenmaßname
printf("%s", argv[1]); printf("%100s",argv[1]);
Bemerkung: Die Länge der Ausgabe von printf() ist nicht unbegrenzt. Auch hier würde sich eine Größenbegrenzung gut eignen. Gleiches gilt auch für fprintf().

Tabelle 26.6: Unsichere Funktion - printf()

Weitere unsichere Funktionen im Überblick

Unsichere Funktion Bemerkung
getenv() Funktion lässt sich ebenfalls für einen Buffer Overflow verwenden.
system() Diese Funktion sollte möglichst vermieden werden. Insbesondere, wenn der Anwender den String selbst festlegen darf.

Tabelle 26.7: Unsichere Funktionen - getenv() und system()

Abhängig von Betriebssystem und Compiler gibt es noch eine Menge mehr solcher unsicheren Funktionen. Die wichtigsten wurden aber hier erwähnt.

26.1.5 Gegenmaßnahmen zum Buffer Overflow, wenn das Programm fertig ist
Wenn das Programm bereits fertig ist, und Sie es noch nicht der Öffentlichkeit zugänglich gemacht haben, können Sie sich die Suchen-Funktion des Compilers zu Nutze machen oder eine eigene Funktion schreiben. Hier ein solcher Ansatz. Das Listing gibt alle gefährlichen Funktionen, welche in der Stringtabelle danger eingetragen sind, auf dem Bildschirm aus.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX 255

char *danger[] = {
                   "scanf", "sscanf", "fscanf",
                   "gets", "strcat", "strcpy",
                   "printf", "fprintf", "sprintf",
                   "vsprintf", "system", NULL
                   /* u.s.w. */
                  };

int main(int argc, char * argv[])
{
   FILE *fp;
   char puffer[MAX];
   int i, line=1;

   if(argc < 2)
      {
         printf("Anwendung: %s <datei.c>\n\n", argv[0]);
         exit(0);
      }

   if ((fp=fopen(argv[1], "r+"))==NULL)
      {
         printf("Konnte Datei nicht zum Lesen öffnen\n");
         exit(0);
      }

   while( (fgets(puffer, MAX, fp)) != NULL)
      {
         i=0;
         while(danger[i] != NULL)
            {
               if((strstr(puffer,danger[i]))!=0)
                  printf("%s gefunden in Zeile %d\n",
                                        danger[i],line);
               i++;
            }
         line++;
      }
   fclose(fp);
   return 0;
}

Eine weitere Möglichkeit ist es, eine so genannte Wrapper-Funktion zu schreiben. Eine Wrapper-Funktion können Sie sich als Strumpf vorstellen, den Sie einer anfälligen Funktion überziehen. Als Beispiel dient hier die Funktion gets():

#include <stdio.h>
#define MAX  10
/*Damit es keine Kollision mit gets aus stdio.h gibt */
#define gets(c) Gets(c)

void Gets(char *z)
{
   int ch;
   int counter=0;
   while((ch=getchar()) != '\n')
      {
         z[counter++]=ch;
         if(counter >= MAX)
            break;
      }
   z[counter] = '\0';     /* Terminieren */
}

int main(int argc, char **argv)
{
   char puffer[MAX];
   printf("Eingabe : ");
   gets(puffer);
   printf("puffer = %s\n",puffer);
   return 0;
}

Zuerst musste vor dem Compilerlauf die Funktion gets() mit

#define gets(c) Gets(c) 

ausgeschalten werden. Jetzt kann statt der echten gets()-Version die Wrapper-Funktion Gets() verwendet werden. Genauso kann dies bei den anderen gefährlichen Funktionen vorgenommen werden. Beispielsweise mit der Funktion strcpy():

#include <stdio.h>
#include <string.h>
#define MAX  10
 /*Damit es keine Kollision mit strcpy in string.h gibt*/
#define strcpy Strcpy
#define DEBUG
/* #undef DEBUG */

void Strcpy(char *ziel, char *quelle)
{
   int counter;
#ifdef DEBUG
   /* DEBUG-INFO */
   size_t size = strlen(quelle)+1;
   if( size > MAX )
      printf("DEBUG-INFO: Pufferüberlaufversuch\n");
   /* DEBUG-INFO Ende */
#endif

   for(counter=0; quelle[counter] != '\0' && counter < MAX-1;
       counter++)
      ziel[counter]=quelle[counter];
/* Terminieren */
   ziel[counter] = '\0';
}

int main(int argc, char **argv)
{
   char puffer[MAX];
   strcpy(puffer, "0123456789012345678");
   printf("puffer = %s\n",puffer);
   return 0;
}

Hier wird zum Beispiel noch eine DEBUG-Info mit ausgegeben, falls dies erwünscht ist. Ansonsten muss einfach die Direktive undef auskommentiert werden.

26.1.6 Programme und Tools zum Buffer Overflow
Es gibt z.B. auf dem Linux-Sektor zwei gute Bibliotheken, StackShield und StackGuard. Beide Bibliotheken arbeiten etwa nach demselben Prinzip. Beim Aufruf einer Funktion greifen diese Bibliotheken ein und sichern die Rücksprungadresse. Dafür wird natürlich ein extra Code am Anfang und Ende des Funktionsaufrufs eingefügt. Wird hierbei versucht, die Rücksprungadresse zu manipulieren, schreibt das Programm eine Warnung in das Syslog des Systems und beendet sich.

Voraussetzung dafür, dass Sie eine der beiden Bibliotheken verwenden können, ist, dass Sie im Besitz des Quellcodes des Programms sind, das Sie vor einem Buffer Overflow schützen wollen. Denn das Programm muss mit den Bibliotheken von StackShield und StackGuard neu übersetzt werden.

Einen anderen Weg geht die Bibliothek libsafe. Diese entfernt gefährliche Funktionsaufrufe und ersetzt sie durch sichere Versionen der Bibliothek. Diese besitzen zusätzlich noch einen Schutz vor Überschreiben des Stack-Frames. Firmen mit einen etwas größeren Geldbeutel sei das Programm Insure++ von Parasoft ans Herz gelegt. Das Programm lässt sich als Testversion einige Zeit kostenlos ausprobieren. Der Anschaffungspreis rechnet sich über die Zeit allemal. Das Programm ist für alle gängigen Systeme erhältlich und kann außer dem Buffer Overflow noch eine Menge weiterer Fehler aufdecken. Einige davon sind:

Für mehr Details sei die Webseite der Firma, die dieses Programm vertreibt, empfohlen. Sie finden die URL dazu im Anhang Weiterführende Links und empfehlenswerte Literatur.

26.1.7 Ausblick
Buffer Overflows werden wohl in Zukunft noch vielen Programmierern Probleme bereiten und noch länger eines der häufigsten Angriffziele von Hackern darstellen. Daher lohnt es, sich mit diesem Thema zu befassen.

Es wird wohl noch eine Generation vorbeiziehen, bis Betriebssysteme auf den Markt kommen, welche solche Probleme von selbst erkennen und ausgrenzen. Erste Ansätze dazu gibt es zwar schon (Solaris), aber clevere Programmierer haben bereits einen Weg gefunden, auch dies auszuhebeln.

Hinweis
 

Um es richtig zu stellen: Der Hacker findet Fehler in einem System heraus und meldet diese dem Hersteller des Programms. Entgegen der in den Medien verbreiteten Meinung ist ein Hacker kein Bösewicht. Die Bösewichte werden Cracker genannt.

Weiter mit 26.2. Memory Leaks (Speicherlecks)            zum Inhaltsverzeichnis