Shellcoding
Shellcode¶
En la mayoría de los casos, los programas que queremos explotar no tendrán simplemente funciones tipo "win" o "get shell". Sin embargo, aprovechando los conceptos que hemos aprendido sobre corrupción de memoria, podemos suministrar nuestro propio código y ejecutarlo.
Este nivel introducirá el concepto de shellcode. Shellcode típicamente se refiere a pequeños fragmentos escritos a mano en assembly cuyo propósito principal es ejecutar un shell mediante una explotación basada en corrupción de memoria.
Resultados de aprendizaje¶
- Familiarizarse con el concepto de shellcode
- Aprender cómo se realizan los syscalls a nivel de assembly
Shellcoding¶
En el capítulo anterior vimos cómo la corrupción de memoria dirigida puede usarse para influir en la ejecución del programa. En este capítulo, ampliaremos ese concepto inyectando nuestro propio código en un programa en ejecución. "Inyectar" código vía corrupción de memoria de esta manera se llama shellcoding.
Este binario coloca shellcode en la stack, y luego salta explícitamente hacia él. Ejecuta (run) el binario y observa qué ocurre.
Shellcode¶
El programa ejecutó un shell. Sin embargo, no hay nada en el código fuente que sugiera que este binario tenga esa capacidad.
En su lugar, la variable shellcode contiene código assembly pre-compilado que ejecuta un shell.
Investiguemos esto más a fondo: pon un breakpoint en la instrucción call rax en main, luego run el programa.
Shellcode utilizado: https://www.exploit-db.com/exploits/36858
Execve: comando no encontrado¶
Parece que estás intentando ejecutar un comando, ya sea vía system() o directamente vía sys_execve, que es inválido o no está disponible.
En la mayoría de los casos, esto ocurre cuando uno de los argumentos que proporcionaste es incorrecto o está corrupto. La mejor forma de avanzar es poner un breakpoint justo antes de intentar ejecutar el comando y verificar que todos tus argumentos se estén pasando como esperas.
Presta especial atención en caso de que tus argumentos estén almacenados en la stack u otra región de memoria potencialmente volátil. Un problema bastante común es que el shellcode, funciones de librería (como system()), o incluso efectos secundarios de ROP gadgets modifiquen la memoria durante la ejecución de tu exploit y resulten en un argumento corrupto.
Ahora, el programa está a punto de ejecutar el shellcode llamando a la dirección en $rax. Confirmemos que los datos en $rax coinciden con la variable shellcode.
Fíjate que los bytes coinciden con los datos que están en la variable shellcode.
Echemos un vistazo a qué instrucciones corresponden a esos mismos bytes:
Syscalls¶
Ahora estás viendo la versión "legible" del contenido de la variable shellcode.
Este código se está preparando para hacer un syscall. Puedes pensar en los syscalls como si fueran una API hacia el Sistema Operativo. Son la forma en que los programas en userland piden al kernel que realice ciertas acciones.
Específicamente, este shellcode realiza una llamada a sys_execve. Este syscall reemplaza el programa que se está ejecutando actualmente por uno nuevo, en nuestro caso, /bin/sh.
Al igual que las funciones, los syscalls también tienen una convención de llamadas. Veamos eso más de cerca:
Pon un breakpoint en la instrucción syscall, luego continue hasta ella. Una vez allí, ejecuta
wdb> break *0x4006b6
Breakpoint will be set at 0x4006b6
wdb> run
Started '04_execve'
------------------------------------------------------------
--[ Shellcoding - Execve Shellcode (x64)
------------------------------------------------------------
Calling shellcode...
Breakpoint 1: 0x4006b6, main+121
wdb> x/23bx $rax
0x7fffffffedb0: 0x31 0xf6 0x48 0xbb 0x2f 0x62 0x69 0x6e
0x7fffffffedb8: 0x2f 0x2f 0x73 0x68 0x56 0x53 0x54 0x5f
0x7fffffffedc0: 0x6a 0x3b 0x58 0x31 0xd2 0x0f 0x05
wdb> x/10i $rax
0x7fffffffedb0: xor esi, esi
0x7fffffffedb2: movabs rbx, 0x68732f2f6e69622f
0x7fffffffedbc: push rsi
0x7fffffffedbd: push rbx
0x7fffffffedbe: push rsp
0x7fffffffedbf: pop rdi
0x7fffffffedc0: push 0x3b
0x7fffffffedc2: pop rax
0x7fffffffedc3: xor edx, edx
0x7fffffffedc5:
wdb> break *0x7fffffffedc5
Breakpoint 2 set at 0x7fffffffedc5
Breakpoint 2 set on writable data, debugger may behave unexpectedly
wdb> continue
Breakpoint 2: 0x7fffffffedc5
wdb> info registers
rax: 0x000000000000003b
rbx: 0x68732f2f6e69622f
rcx: 0x00000000fbad2887
rdx: 0x0000000000000000
rsi: 0x0000000000000000
rdi: 0x00007fffffffed98
rbp: 0x00007fffffffedd0
rsp: 0x00007fffffffed98
rip: 0x00007fffffffedc5
r8: 0x00007f0000026e40
r9: 0x0000000000000000
r10: 0x0000000000000194
r11: 0x00007f0000297690
r12: 0x0000000000400520
r13: 0x00007fffffffeeb0
r14: 0x0000000000000000
r15: 0x0000000000000000
fs: 0x0000000000000000
gs: 0x0000000000000000
eflags: 0x0000000000000044 [ PF ZF ]
Para realizar un syscall, necesitamos conocer su syscall number, y luego proporcionar los parámetros apropiados para cualquier argumento que requiera. Para x86-64 Linux, la convención es similar a las llamadas a funciones regulares, con la adición de que el syscall number se pasa en $rax.
La "declaración" de la función para sys_execve es:
Podemos ver que $rax está establecido en 0x3b, y que $rsi y $rdx están en 0.
Vuelca el contenido de $rdi como una cadena usando el comando x:
$rdi está apuntando a la cadena /bin//sh. Podemos pensar en el syscall completo como
Como vimos, esto resulta en que el binario en ejecución sea reemplazado por /bin/sh.
La observación crítica es que este código estaba ubicado en la stack.
En las próximas lecciones combinaremos lo que aprendimos en el capítulo anterior (corrupción de memoria) con este concepto para tomar control arbitrario de programas vulnerables.