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

Inhaltsverzeichnis
Vorwort
Vorwort des Gutachters
1 Einstieg in C
2 Das erste Programm
3 Grundlagen
4 Formatierte Ein-/Ausgabe mit »scanf()« und »printf()«
5 Basisdatentypen
6 Operatoren
7 Typumwandlung
8 Kontrollstrukturen
9 Funktionen
10 Präprozessor-Direktiven
11 Arrays
12 Zeiger (Pointer)
13 Kommandozeilenargumente
14 Dynamische Speicherverwaltung
15 Strukturen
16 Ein-/Ausgabe-Funktionen
17 Attribute von Dateien und das Arbeiten mit Verzeichnissen (nicht ANSI C)
18 Arbeiten mit variabel langen Argumentlisten – <stdarg.h>
19 Zeitroutinen
20 Weitere Headerdateien und ihre Funktionen (ANSI C)
21 Dynamische Datenstrukturen
22 Algorithmen
23 CGI mit C
24 MySQL und C
25 Netzwerkprogrammierung und Cross–Plattform-Entwicklung
26 Paralleles Rechnen
27 Sicheres Programmieren
28 Wie geht’s jetzt weiter?
A Operatoren
B Die C-Standard-Bibliothek
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch
Buch: C von A bis Z

C von A bis Z
3., aktualisierte und erweiterte Auflage, geb., mit CD und Referenzkarte
1.190 S., 39,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1411-7
Pfeil 27 Sicheres Programmieren
Pfeil 27.1 Buffer-Overflow (Speicherüberlauf)
Pfeil 27.1.1 Speicherverwaltung von Programmen
Pfeil 27.1.2 Der Stack-Frame
Pfeil 27.1.3 Rücksprungadresse manipulieren
Pfeil 27.1.4 Gegenmaßnahmen zum Buffer-Overflow während der Programmerstellung
Pfeil 27.1.5 Gegenmaßnahmen zum Buffer-Overflow, wenn das Programm fertig ist
Pfeil 27.1.6 Programme und Tools zum Buffer-Overflow
Pfeil 27.1.7 Ausblick
Pfeil 27.2 Memory Leaks (Speicherlecks)
Pfeil 27.2.1 Bibliotheken und Tools zu Memory Leaks
Pfeil 27.3 Tipps zu Sicherheitsproblemen

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

27 Sicheres Programmieren

Ein Aspekt, der oft übersehen wird, ist die sicherheitsbezogene Programmierung. Programmierer setzen oft 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 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 nicht so genau wissen, was daran 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 sind, 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 schon im Vorfeld unvorhersehbarem Ärger, und nachträglich entstehen hohe Kosten.

Ein Szenario: Sie haben für eine Firma ein Programm zur Verwaltung von Daten geschrieben. In der Firma haben einige gewiefte Mitarbeiter einen Weg gefunden, mithilfe Ihres Programms aus dem Verwaltungsprogramm zu springen, wodurch sie 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 Projekte 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 ihm 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 Sprachen 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.


Rheinwerk Computing - Zum Seitenanfang

27.1 Buffer-Overflow (Speicherüberlauf) Zur nächsten ÜberschriftZur vorigen Überschrift

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 angesichts der enormen Anzahl von Ergebnissen überrascht sein. Es gibt unzählige Programme, die für einen Buffer-Overflow anfällig sind. Das Ziel des Angreifers ist es dabei, den Buffer-Overflow auszunutzen, um in das System einzubrechen.

Die Aufgabe dieses Abschnitts 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 sehen Sie 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 27.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 erstellten Speicherbereich verwiesen bzw. gesprungen, der echten Code enthält, haben Sie einen sogenannten Exploit erstellt.


Rheinwerk Computing - Zum Seitenanfang

27.1.1 Speicherverwaltung von Programmen Zur nächsten ÜberschriftZur vorigen Überschrift

Ein Programm besteht aus drei Speichersegmenten, 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. In Abbildung 27.2 sind die einzelnen Segmente schematisch dargestellt.

Abbildung 27.2 Speicherverwaltung – die einzelnen Segmente

  • Code-Segment (Text-Segment) – Hier befinden sich die Maschinenbefehle, die vom Prozessor beim HOLEN-Zyklus eingelesen werden – oder einfacher gesagt: der Programmcode selbst. Das Code-Segment lässt sich nicht manipulieren, hat eine feste Größe und ist gegen Überschreiben geschützt.
  • 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.
  • 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.


Rheinwerk Computing - Zum Seitenanfang

27.1.2 Der Stack-Frame Zur nächsten ÜberschriftZur vorigen Überschrift

Für jede Funktion steht ein sogenannter Stack-Frame im Stack zur Verfügung, in dem die lokalen Variablen gespeichert werden. Wichtiger noch: Im Stack befinden sich Registerinhalte des Prozessors, die vor dem Funktionsaufruf gesichert wurden. Sie sind nötig, 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:

1. Mit dem Assembler-Befehl PUSH werden die Parameter wert1 und wert2 auf den Stack geschrieben.
2. Mit dem Assembler-Befehl 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 (Instruction 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, die mit CS:IP und BP dargestellt wird.
3. Jetzt werden die lokalen Variablen der Funktion my_func() eingerichtet, und die Funktion arbeitet die einzelnen Befehle ab.
4. Am Schluss, wenn diese Funktion beendet ist, springt sie wieder zur main()-Funktion zurück. Dies geschieht mit dem Assembler-Befehl RET, der auf die vom Stack gesicherte Adresse zurückspringt, die aus CS:IP und BP gebildet wird.

Rheinwerk Computing - Zum Seitenanfang

27.1.3 Rücksprungadresse manipulieren Zur nächsten ÜberschriftZur vorigen Überschrift

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 Ihnen soll 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 Assembler-Code des Programms anzusehen. Hierfür wird der Diassembler objdump verwendet, der 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. An 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, was den Basis-Pointer auf dem Stack sichert, und »5d« entfernt den Basis-Pointer wieder vom Stack. »c3« bedeutet ret, also return. Mit »c3« wird also wieder an die Rücksprungadresse gesprungen, die in der main()-Funktion ebenfalls auf den 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 Assembler-Code, beispielsweise:

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

Es ist wichtig, dass Sie verstehen, 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.

Übersetzen Sie das Programm von eben nochmals mit:

gcc -S -o bufferoverflow2.s bufferoverflow2.c

Jetzt befindet sich im Verzeichnis eine Assembler-Datei (*.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 erhoehen
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                    ;Zurueck 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, der 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 27.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 um 4 Bytes zurücksetzen.
      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 27.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. Das Ziel dieser Manipulation ist es aber selten (wie hier dargestellt), die Rücksprungadresse zu ändern, 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, die Sie bereits manipuliert haben, und der Hacker kann nun bestimmte Codesequenzen ausführen.

Ihnen dies jetzt 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.

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

  • 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.
  • 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.
  • 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, an 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.

Rheinwerk Computing - Zum Seitenanfang

27.1.4 Gegenmaßnahmen zum Buffer-Overflow während der Programmerstellung Zur nächsten ÜberschriftZur vorigen Überschrift

Steht Ihr Projekt in den Startlöchern, haben Sie Glück. Wenn Sie diesen Abschnitt durchgelesen haben, ist die Gefahr 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 27.1 Unsichere Funktion – »gets()«

Unsichere Funktion Gegenmaßnahme
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 27.2 Unsichere Funktion – »scanf()«

Unsichere Funktion Gegenmaßnahme
scanf("%s",str);
scanf("%10s",str);

Bemerkung: Auch scanf() nimmt bei der Eingabe keine Längenprüfung vor. Die Gegenmaßnahme 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 27.3 Unsichere Funktion – »strcpy()«

Unsichere Funktion Gegenmaßnahme
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 27.4 Unsichere Funktion – »strcat()«

Unsichere Funktion Gegenmaßnahme
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 27.5 Unsichere Funktion – »sprintf()«

Unsichere Funktion Gegenmaßnahme
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 27.6 Unsichere Funktion – »printf()«

Unsichere Funktion Gegenmaßnahme
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 27.7 Unsichere Funktionen – »getenv()« und »system()«

Unsichere Funktion Bemerkung
getenv()

Diese Funktion lässt sich ebenfalls für einen Buffer-Overflow verwenden.

system()

Diese Funktion sollte möglichst vermieden werden – insbesondere dann, wenn der Anwender den String selbst festlegen darf.


Abhängig von Betriebssystem und Compiler gibt es noch eine Menge mehr solcher unsicherer 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 beispielsweise 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, die entsprechende Längenüberprüfungen durchführen, wie beispielsweise folgendes Listing 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, hierzu eigene Funktionen (bzw. eine Bibliothek) zu schreiben, die die Benutzereingaben oder Eingabedatei scannen.


Rheinwerk Computing - Zum Seitenanfang

27.1.5 Gegenmaßnahmen zum Buffer-Overflow, wenn das Programm fertig ist Zur nächsten ÜberschriftZur vorigen Überschrift

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 zunutze machen oder eine eigene Funktion schreiben. Im Folgenden sehen Sie einen solchen Ansatz. Das Listing gibt alle gefährlichen Funktionen, die 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
   /* usw. */
};

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 oeffnen\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 sogenannte 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 Compiler-Lauf die Funktion gets() mit

#define gets(c) Gets(c)

ausgeschaltet werden. Jetzt kann statt der echten gets()-Version die Wrapper-Funktion Gets() verwendet werden. Genauso kann dies bei den anderen gefährlichen Funktionen gemacht 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: Pufferueberlaufversuch\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.


Rheinwerk Computing - Zum Seitenanfang

27.1.6 Programme und Tools zum Buffer-Overflow Zur nächsten ÜberschriftZur vorigen Überschrift

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.

Die 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. Sie entfernt gefährliche Funktionsaufrufe und ersetzt sie durch sichere Versionen. Diese besitzen zusätzlich noch einen Schutz vor dem Ü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 im Laufe der 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:

  • Speicherfehler
  • Speicherlecks
  • Speicherreservierungsfehler
  • Verwendung uninitialisierter Variablen
  • falsche Variablendefinitionen
  • Zeigerfehler
  • Bibliothekenfehler
  • logische Fehler

Rheinwerk Computing - Zum Seitenanfang

27.1.7 Ausblick topZur vorigen Überschrift

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 dauern, bis Betriebssysteme auf den Markt kommen, die 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 diese auszuhebeln.


Hinweis

Um es richtigzustellen: 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.




Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z

 C von A bis Z
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: C/C++






 C/C++


Zum Katalog: Einstieg in C






 Einstieg in C


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
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.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de