Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 << zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch für Linux, Unix und Windows
– 2., aktualisierte und erweiterte Auflage 2006
Buch: C von A bis Z

C von A bis Z
1.116 S., mit CD, Referenzkarte, 39,90 Euro
Galileo Computing
ISBN 3-89842-643-2
gp Kapitel 25 Sicheres Programmieren
  gp 25.1 Buffer Overflow (Speicherüberlauf)
    gp 25.1.1 Speicherverwaltung von Programmen
    gp 25.1.2 Der Stack-Frame
    gp 25.1.3 Rücksprungadresse manipulieren
    gp 25.1.4 Gegenmaßnahmen zum Buffer Overflow während der Programmerstellung
    gp 25.1.5 Gegenmaßnahmen zum Buffer Overflow, wenn das Programm fertig ist
    gp 25.1.6 Programme und Tools zum Buffer Overflow
    gp 25.1.7 Ausblick
  gp 25.2 Memory Leaks (Speicherlecks)
    gp 25.2.1 Bibliotheken und Tools zu Memory Leaks
  gp 25.3 Tipps zu Sicherheitsproblemen

Kapitel 25 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 gelangen und allerlei Unfug anrichten. 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 diese 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.


Galileo Computing - Zum Seitenanfang

25.1 Buffer Overflow (Speicherüberlauf)  downtop

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:

/* bufferoverflow1.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
   char *str = "0123456789012";
   char buf[10];
   strcpy(buf, str);
   printf("%s",buf);
   return EXIT_SUCCESS;
}

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
Hier klicken, um das Bild zu Vergrößern

Abbildung 25.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.


Galileo Computing - Zum Seitenanfang

25.1.1 Speicherverwaltung von Programmen  downtop

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:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 25.2   Speicherverwaltung – Die einzelnen Segmente

gp  Code-Segment (Text-Segment) – Hier befinden sich die Maschinenbefehle, die vom Prozessor beim HOLEN-Zyklus eingelesen werden. Oder einfacher: der Programmcode selbst. Das Code-Segment lässt sich nicht manipulieren, hat eine feste Größe und ist gegen Überschreiben geschützt.
gp  Heap-Segment (Daten-Segment) – Hier liegen die Variablen (extern, static), Felder (Arrays) und Tabellen des Programms. Der Maschinenbefehl, der diese Daten benötigt, greift auf dieses Segment zu.
gp  Stack-Segment – Hier befinden sich dynamische Variablen und Rücksprungadressen von Funktionen. Dieser Bereich dient auch dem schnellen Zwischenspeichern von Daten und Parameterübergaben.

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.


Galileo Computing - Zum Seitenanfang

25.1.2 Der Stack-Frame  downtop

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:

/* stackframe.c */
#include <stdio.h>
#include <stdlib.h>
void my_func(int wert1, int wert2) {
   int summe;
   summe = wert1+wert2;
   printf("Summe: %d \n",summe);
}
int main(void) {
   my_func(10,29);
   return 0;
}

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

gp  Mit dem Assemblerbefehl PUSH werden die Parameter wert1 und wert2 auf den Stack geschrieben.
gp  Mit dem Assemblerbefehl CALL wird die Position des Maschinencodes gesichert, damit bei Beendigung der Funktion my_func() wieder in die main()-Funktion zurückgesprungen werden kann. Dies wird mithilfe des Befehlszeigers (Instruktion Pointer, kurz IP) realisiert. Genau genommen wird diese Adresse mithilfe des Befehlszeigers, des Code-Segments (CS) (CS:IP) und des Basis-Pointers (BP) erzeugt. Dies ist die Rücksprungadresse, welche mit CS:IP und BP dargestellt wird.
gp  Jetzt werden die lokalen Variablen der Funktion my_func() eingerichtet, und die Funktion arbeitet die einzelnen Befehle ab.
gp  Am Schluss, wenn diese Funktion beendet ist, springt sie wieder zur main()-Funktion zurück. Dies geschieht mit dem Assemblerbefehl RET, welcher auf die vom Stack gesicherte Adresse, gebildet aus CS:IP und BP, zurückspringt.

Galileo Computing - Zum Seitenanfang

25.1.3 Rücksprungadresse manipulieren  downtop

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:

/* bufferoverflow2.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void overflow(void) {
   char zeichen[5];
   strcpy(zeichen, "1234567"); /*Überlauf*/
}
int main(void) {
   printf("Mein 1.Buffer Overflow\n");
   overflow();
   return EXIT_SUCCESS;
}

Ü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 bufferoverflow2

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 bufferoverflow2.s bufferoverflow2.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:

/* bufferoverflow3.c */
#include <stdio.h>
#include <stdlib.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:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 25.3   Der aktuelle 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:

/* bufferoverflow4.c */
#include <stdio.h>
#include <stdlib.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 EXIT_SUCCESS;
}

Die einfachste Möglichkeit, auf die Rücksprungadresse zurückzugreifen, besteht darin, um die Speichergröße der Variablen temp in der Funktion rückwärts zu springen.

pointer=&tmp-1;

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

*pointer=*pointer+10;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 25.4   Der Zeiger verweist auf die Rücksprungadresse

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 bufferoverflow4
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 Variablen, 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:

gp  Inhalte von Variablen, die auf dem Stack liegen, können verändert werden. Stellen Sie sich das einmal bei einer Funktion vor, die ein Passwort vom Anwender abfragt.
gp  Die Rücksprungadresse wird manipuliert, sodass das Programm an einer beliebigen Stelle im Speicher mit der Maschinencodeausführung fortfährt. Meistens ist dies die Ausführung des vom Angreifer präparierten Codes. Für die Ausführung von fremdem Code werden wiederum die Variablen auf dem Stack, eventuell auch auf dem Heap verwendet.
gp  Dasselbe Schema lässt sich auch mit Zeigern auf Funktionen anwenden. Dabei ist theoretisch nicht einmal ein Buffer Overflow erforderlich, sondern es reicht die Speicheradresse, bei der sich diese Funktion befindet. Die Daten, die für die Ausführung von fremdem Code nötig sind, werden vorzugsweise wieder in einer Variablen gespeichert.

Galileo Computing - Zum Seitenanfang

25.1.4 Gegenmaßnahmen zum Buffer Overflow während der Programmerstellung  downtop

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


Tabelle 25.1   Unsichere Funktion – gets()

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 25.2   Unsichere Funktion – scanf()

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().

Unsichere Funktionen zur Stringbearbeitung


Tabelle 25.3   Unsichere Funktion – strcpy()

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 25.4   Unsichere Funktion – strcat()

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 25.5   Unsichere Funktion – sprintf()

Unsichere Funktion Gegenmaßname
sprintf(buf, "%s", temp); snprintf(buf, 100, "%s", temp);
Bemerkung: Mit sprintf() ist es nicht möglich, die Größe des Zielpuffers anzugeben, daher empfiehlt sich auch hier 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.

Unsichere Funktionen zur Bildschirmausgabe


Tabelle 25.6   Unsichere Funktion – printf()

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().

Weitere unsichere Funktionen im Überblick


Tabelle 25.7   Unsichere Funktionen – getenv() und system()

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.

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

Generell sollte man immer alle printf() und scanf() Funktionen mit Vorsicht und Bedacht verwenden. Häufig lässt es sich hier bspw. wesentlich sicherer mit fwrite() oder fread() arbeiten und die Konversion kann man dabei auch selbst machen. Wenigstens sollte man aber ein Frame um die »unsichereren« Funktionen bauen, welche entsprechende Längenüberprüfungen durchführen, wie bspw. folgendes Listings zeigen soll:

/* check_before_sprintf.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX 10
void check_bevore_sprintf(char *quelle, int max) {
   if(strlen(quelle) < MAX)
      return;
   else
      abort(); /* abort zum Debugger */
}
int main(void) {
   char *ptr1 = "123456789";
   char *ptr2 = "1234567890";
   char string[MAX];
   check_bevore_sprintf(ptr1, MAX);
   sprintf(string, "%s", ptr1);
   printf("string: %s\n", string);
   /* Boom!!! */
   check_bevore_sprintf(ptr2, MAX);
   sprintf(string, "%s", ptr2);
   printf("string: %s\n", string);
   return EXIT_SUCCESS;
}

Einige Programmierer gehen sogar so weit, dass Sie alle printf- und scanf-Funktionen aus Ihren fertigen Programmen verbannen. Diese Entwickler scheuen auch nicht die Arbeit und schreiben hierzu häufig eigene Funktionen (bzw. eine Bibliothek), welche die Benutzereingaben oder Eingabedatei scannen.


Galileo Computing - Zum Seitenanfang

25.1.5 Gegenmaßnahmen zum Buffer Overflow, wenn das Programm fertig ist  downtop

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.

/* danger.c */
#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]);
      return EXIT_FAILURE;
   }
   if ( (fp=fopen(argv[1], "r+")) == NULL) {
      printf("Konnte Datei nicht zum Lesen öffnen\n");
      return EXIT_FAILURE;
   }
   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 EXIT_SUCCESS;
}

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():

/* wrap_gets.c */
#include <stdio.h>
#include <stdlib.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 EXIT_SUCCESS;
}

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():

/* wrap_strcpy.c */
#include <stdio.h>
#include <stdlib.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 EXIT_SUCCESS;
}

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


Galileo Computing - Zum Seitenanfang

25.1.6 Programme und Tools zum Buffer Overflow  downtop

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 einem 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:

gp  Speicherfehler
gp  Speicherlecks
gp  Speicherreservierungsfehler
gp  Verwendung uninitialisierter Variablen
gp  Falsche Variablendefinitionen
gp  Zeigerfehler
gp  Bibliothekenfehler
gp  Logische Fehler

Galileo Computing - Zum Seitenanfang

25.1.7 Ausblick  toptop

Buffer Overflows werden wohl in Zukunft noch vielen Programmierern Probleme bereiten und noch länger eines der häufigsten Angriffsziele 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.


 << zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z
C von A bis Z
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Shell-Programmierung






 Shell-Programmierung


Zum Katalog: Linux-UNIX-Programmierung






 Linux-UNIX-Programmierung


Zum Katalog: C/C++






 C/C++


Zum Katalog: UML 2.0






 UML 2.0


Zum Katalog: Reguläre Ausdrücke






 Reguläre Ausdrücke


Zum Katalog: Linux






 Linux


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de