viernes, 18 de mayo de 2007

El origen del mal - 0x03

El logo del mal
Bien ya sabemos que podemos meter en memoria código y conocemos la herramienta gdb para depurar los programas y saber qué ocurre en la memoria y en los registros. El código que podemos inyectar en un programa vulnerable debe ser entendible por el procesador, por eso lo que hay que generar es un shellcode. Un shellcode es un conjunto de instrucciones simples que puede tener llamadas al sistema operativo para realizar distintas operaciones; la más común es invocar un interprete de comandos o shell.

Hoy vamos a ver cómo se crean, prueban y ejecutan los shellcodes. Existen ya muchos shellcodes hechos y probados para distintas plataformas, incluso una herramienta muy sistemática llamada metasploit, pero para entender cómo funciona empezaremos con ejemplos muy simples.

[día 3: los shellcodes]
Bien; si metemos código en memoria va a pasar directamente al procesador. Por tanto ese código debe entenderlo perfectamente y no puede ser código de alto nivel.Lo más parecido que tenemos para acercarnos al procesador son las instrucciones de ensamblador.
Pero para además del procesador hay que tener en cuenta que entre el hardware y los programas hay un sistema operativo por tanto tendremos que usar llamadas de sistema o syscalls.

Vamos a ver qué pasos deben seguirse para generar un shellcode. Se parte de un programa en c usando llamadas de sistemas, se compila, y se extrae el código en formato hexadecimal.
Este sería el ejemplo más simple:
// shell_exit.c : invoca la syscall exit() que termina el proceso actual.
#include
int main ()
{
exit(1);
}

Lo compilamos:
linux$ gcc -o shell_exit shell_exit.c

Y lo ejecutamos:
linux$ ./shell
linux$

Cuando lo ejecutamos aparentemente no ocurre nada. Es lógico ya que exit
hace que el proceso termine. Pero si su parámetro es 0 podemos comprobar
que realmente funciona haciendo:
linux$ ./shell && echo OK
OK
linux$

Al salir del proceso con un 0 el sistema considera que todo va bien y el valor lógico del comando es true, y por tanto al hacerle un AND con otro porceso (echo OK) este se ejecuta y devuelve OK.

Supongamos que queremos convertir ese código en un shellcode y no tenemos ni idea de lenguaje ensamblador. No problemo! gcc puede generar el código ensamblador
correspondiente con el flag -S.
linux$ gcc -S shell_exit.c
linux$ more shell_exit.s
.file "shell_exit.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
subl %eax, %esp
movl $0, (%esp)
call exit
.size main, .-main
.section .note.GNU-stack,"",@progbits
.ident "GCC: (GNU) 3.3.5 (XXX)"
linux$

Lo malo de esto es que utiliza la sintaxis de ensamblador de ATT y puede que estemos acostumbrados a la de intel. Además no hay rastro de la instrucción int 80h, la instrucción para llamadas de sistema. Esto se debe al kernel 2.6 y a que no se hacen llamadas directas con int80h. Pero implementar exit(0) en ensamblador
no es muy complicado:

global _start
_start:
xor eax, eax ; hacemos xor sobre eax, equivale a eax = 0
mov al, 1 ; guardamos 1 en al. 1 identifica la syscall exit
xor ebx, ebx ; hacemos xor sobre ebx, equivale a ebx = 0
int 80h

Lo ensamblamos con la herramienta nasm:
linux$ nasm -f elf exitasm.asm
Lo enlazamos y la esta listo para ejecutar:
linux$ ld -o exitasm exitasm.o
linux$ ./exitasm && echo OK
OK
linux$

Sacando el shellcode
Cómo podemos sacar el correspondiente shellcode de ese programa? Hay distintas formas. Una de ellas, la vía dura, es usar el depurador:
mediante x/bx podemos ir sacando todos los bytes de los que consta el programita en formato hexadecimal.

$ gdb exitasm
Copyright 2004 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-linux"...(no debugging symbols found)
Using host libthread_db library "/lib/tls/libthread_db.so.1".

(gdb) disas _start
Dump of assembler code for function _start:
0x08048080 : xor %eax,%eax
0x08048082 : mov $0x1,%al
0x08048084 : xor %ebx,%ebx
0x08048086 : int $0x80
End of assembler dump.
(gdb) x/bx _start
0x8048080 : 0x31
(gdb)
0x8048081 : 0xc0
(gdb)
0x8048082 : 0xb0
(gdb)
0x8048083 : 0x01
(gdb)
0x8048084 : 0x31
(gdb)
0x8048085 : 0xdb
(gdb)
0x8048086 : 0xcd
(gdb)
0x8048087 : 0x80
(gdb)

¡Ese es el shellcode!:
0x31, 0xc0, 0xb0, 0x01, 0x31, 0xdb, 0xcd, 0x80

Como string de lenguaje C quedaría así:
char shellcode[] = "\xb0\x01\x31\xdb\xcd\x80";

Otra forma más directa es usar la utilidad objdump:

linux$ objdump -d exitasm
exitasm: file format elf32-i386
Disassembly of section .text:

08048080 :
8048080: 31 c0 xor %eax,%eax
8048082: b0 01 mov $0x1,%al
8048084: 31 db xor %ebx,%ebx
8048086: cd 80 int $0x80

Probando, probando
Bien, ahora vamos a probar el shellcode.

Para probar los shellcodes podemos usar programas muy sencillos en C que tomen el shellcode y lo ejecuten. Estos dos son los más utilizados en la documentación que te puedes encontrar.
// testshellcode1.c

char shellcode[]= "--aquí se mete el shellcode--";

int main()
{
int *ret;

ret = (int *)&ret + 2;
*ret = shellcode;
}

Y este el otro:
// testshellcode2.c

char shellcode[] = "--aquí el shellcode--";
int main()
{
// Definimos un puntero a una función
int (*mifuncion)();

// Le asignamos a la función el shellcode
mifuncion = (int (*)()) shellcode;

// Invocamos la funcio
(int)(*mifuncion)();
}

Vamos a probar nuestro shellcode con el segundo:

char shellcode[] = "\x31\xc0\xb0\x01\x31\xdb\xcd\x80";
int main()
{
// Definimos un puntero a una función
int (*mifuncion)();

// Le asignamos a la función el shellcode
mifuncion = (int (*)()) shellcode;

// Invocamos la funcio
(int)(*mifuncion)();
}

Lo compilamos
linux$ gcc -o shellcode2 shellcode2.c

Y lo ejecutamos:

linux$ ./shellcode2 && echo OK
OK
linux$

OK! funciona perfectamente.
De momento esto servirá como introducción para el desarrollo de shellcodes.
Ya conocemos las herramientas: depurador, utilidades, sistemas para hacer pruebas, y ahora hay que aprender a crear shellcodes más complejos.

Una vez veamos lo que podemos hacer con un shellcode pondremos todo junto: los pasos que debemos seguir para meter el shellcode en un programa vulnerable.

1 comentario: