Esta pagina se ve mejor con JavaScript habilitado

Introducción a la escritura de shellcodes, exploiting parte 4

 ·  🎃 kr0m

En artículos anteriores hemos utilizado shellcodes como un conjunto de bytes que copiábamos en ciertas posiciones de memoria donde mas tarde hariamos que el EIP terminase apuntando, pero ha llegado el momento de comprender qué son esos bytes y cómo podemos construir nuestras propias shellcodes. Una shellcode normalmente sirve para lanzar una shell (de ahí su nombre), aunque en realidad podemos hacer cualquier cosa con ella.

Una shellcode no es mas que el conjunto de opcodes (instrucciones en hexadecimal) que ejecutará el procesador para realizar un acción en concreto, las shellcodes suelen estar escritas en ensamblador ya que nos permite un control total sobre el proceso de ejecución además de un tamaño inferior de la shellcode.

Antes de nada se debe aclarar que el software accede al hardware a través del kernel, los porgramas realizan llamadas al sistema para ejecutar ciertas acciones, estas son aprobadas o denegadas por el kernel dependiendo del nivel de privilegios del proceso y la acción a ejecutar. Para poder realizar una llamada al sistema se deben asignar determinados valores a ciertos registros, una vez preaparado el escenario de ejecución se llamada a la interrupción 0x80, podemos ver las diferentes llamadas al sistema con:

cat /usr/include/asm-generic/unistd.h |grep ‘NR

El número de la syscall se asignará al registro EAX y los parámetros en EBX, ECX, EDX, ESI y EDI.

Alguién seguro que está pensando, porque no programar en C la función a ejecutar y obtener de esta los opcodes?, la respuesta es sencilla, los compiladores meten “basura” en el código y a nosotros nos interesa que la shellcode sea lo mas pequeña posible. A modo de ejemplo vamos a programar un exit(0) en C y ASM para poder comparar las instrucciones en ASM en cada uno de ellos.

vi salir.c
#include <stdlib.h>
void main() {
        exit(0);
}

Compilamos el binario de forma estática(el código de las librerias incluidas es copiado en nuestro programa) para poder desensamblar desde gdb el código de las funciones definidas en las librerias.

gcc --static salir.c -o salir
gdb salir
(gdb) set disassembly-flavor intel

ASM del main, llama a la función exit:

(gdb) disassemble main

Dump of assembler code for function main:
   0x08048254 <+0>:    push   ebp
   0x08048255 <+1>:    mov    ebp,esp
   0x08048257 <+3>:    and    esp,0xfffffff0
   0x0804825a <+6>:    sub    esp,0x10
   0x0804825d <+9>:    mov    DWORD PTR [esp],0x0
   0x08048264 <+16>:    call   0x8048b30 <exit>
End of assembler dump.

ASM de la función exit, como podemos ver se hacen dos llamadas al sistema(int 0x80) cuando nosotros solo necesitamos la última de ellas:

(gdb) disassemble _exit

Dump of assembler code for function _exit:
   0x0804f730 <+0>:    mov    ebx,DWORD PTR [esp+0x4]
   0x0804f734 <+4>:    mov    eax,0xfc
   0x0804f739 <+9>:    int    0x80
   0x0804f73b <+11>:    mov    eax,0x1
   0x0804f740 <+16>:    int    0x80
   0x0804f742 <+18>:    hlt    
End of assembler dump.

Nuestra shellcode podría funcionar simplemente con:

mov ebx,DWORD PTR [esp+0x4] --> Pone a 0 EBX(parámetro de la función)
mov eax,0x1 --> Se debe ejecutar la INT 1
int 0x80 --> Ejecuta syscall

Para programar en ASM necesitaremos un ensamblador(convierte código ASM a código máquina):

apt-get install nasm
vi salir.asm

section .text
global _start
_start:
xor eax, eax ; EAX --> 0
xor ebx, ebx ; EBX(parametro funcion) --> 0
mov eax, 0x01 ; EAX --> 1
int 0x80 ; Ejecuta SYSCALL

Ensamblamos el código:

nasm -f elf salir.asm
ld salir.o -o salir

Comprobamos que se realiza la llamada al sistema mediante strace:

kr0m@reversedbox:~$ strace ./salir

execve("./salir", ["./salir"], [/* 16 vars */]) = 0
_exit(0) = ?

Obtenemos los opcodes:

kr0m@reversedbox:~$ objdump -M intel-mnemonic -d salir

salir: file format elf32-i386
Disassembly of section .text:

08048060 <_start>:
 8048060:    31 c0                    xor    eax,eax
 8048062:    31 db                    xor    ebx,ebx
 8048064:    b8 01 00 00 00           mov    eax,0x1
 8048069:    cd 80                    int    0x80

NOTA: Hay que tener en cuenta que una shellcode NO puede tener NULLs ya que esto indicaría un fin en la variable provocando así que no se continue con el resto de los opcodes, dejando la shellcode a medio ejecutar.

La shellcode anterior quedaría: x31xc0x31xdbxb8x01x00x00x00xcdx80 como vemos hay carácteres nulos!!

Algunos trucos para evitar NULLs en las shellcodes son:

  • Asignar 0 a un registro: xor REG,REG
  • Resetear registro completo mediante XOR REG,REG y luego utilizar versiones reducidas del registro para asignarle el valor final ya que 00000000 00000001 == 00000001:
xor eax, eax

Sustituimos:

mov eax, 0x01 --> mov al, 0x01

Aplicando estos “trucos” quedaría:

section .text
global _start
_start:
xor eax, eax ; EAX --> 0
xor ebx, ebx ; EBX(parametro funcion) --> 0
mov al, 0x01 ; EAX --> 1
int 0x80 ; Ejecuta SYSCALL

Reensamblamos:

nasm -f elf salir.asm && ld salir.o -o salir
kr0m@reversedbox:~$ objdump -M intel-mnemonic -d salir

salir: file format elf32-i386
Disassembly of section .text:

08048060 <_start>:
 8048060:    31 c0                    xor    eax,eax
 8048062:    31 db                    xor    ebx,ebx
 8048064:    b0 01                    mov    al,0x1
 8048066:    cd 80                    int    0x80

Como podemos observar ya no hay NULLs, la shellcode se ha reducido en tamaño y el resultado de su ejecución es exactamente el mismo:

kr0m@reversedbox:~$ strace ./salir

execve("./salir", ["./salir"], [/* 16 vars */]) = 0
_exit(0) = ?

También es posible realizar la operación inversa, es decir sacar el código ASM a partir de la shellcode:

kr0m@reversedbox:~$ echo -ne “x31xc0x31xdbxb0x01xcdx80” | ndisasm -u -

00000000  31C0              xor eax,eax
00000002  31DB              xor ebx,ebx
00000004  B001              mov al,0x1
00000006  CD80              int 0x80

En este enlace dejo una tabla muy útil sobre instrucciones en x86.

Esto es solo una pequeña introducción al funcionamiento de las shellcodes, su funcionamiento y algunos aspectos a tener en cuenta a la hora de codearlas, realmente una shellcode que ejecuta un exit(0) no resulta muy útil que digamos, en próximos capitulos comenzaremos con shellcodes mas curradas ;)

Si te ha gustado el artículo puedes invitarme a un RedBull aquí