426 lines
20 KiB
Markdown
426 lines
20 KiB
Markdown
|
|
# food / fooc — un desbordamiento de búfer de pila, desde ambos lados
|
||
|
|
|
||
|
|
Un laboratorio de seguridad en C99 en dos mitades:
|
||
|
|
|
||
|
|
- **`food.c`** — un demonio TCP deliberadamente vulnerable. Tiene un
|
||
|
|
desbordamiento de búfer de pila real, de libro de texto (CWE-120), más un par
|
||
|
|
de errores de propina.
|
||
|
|
- **`fooc.c`** — un exploit contra él. Calcula el offset del desbordamiento
|
||
|
|
desensamblando el programa objetivo en tiempo de ejecución, lee las fugas de
|
||
|
|
direcciones del demonio y consigue un shell en la "víctima" sobrescribiendo
|
||
|
|
una dirección de retorno guardada.
|
||
|
|
|
||
|
|
El punto no es el shell. El punto es que puedas seguir de principio a fin cómo
|
||
|
|
un error de seguridad de memoria se convierte en ejecución de código arbitrario
|
||
|
|
— y después ver exactamente qué mitigaciones detienen cada eslabón de esa
|
||
|
|
cadena. Cada línea de ambos programas está comentada, porque el mecanismo es la
|
||
|
|
lección.
|
||
|
|
|
||
|
|
```
|
||
|
|
tu terminal
|
||
|
|
|
|
||
|
|
./fooc (exploit)
|
||
|
|
|
|
||
|
|
TCP 127.0.0.1:2342
|
||
|
|
|
|
||
|
|
./food (demonio vulnerable)
|
||
|
|
|
|
||
|
|
fork() -> vulnerable_handler() -> overflow -> ret -> tu código
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ Lee esto primero
|
||
|
|
|
||
|
|
**`food` es un servicio de red deliberadamente roto. Solo se enlaza a
|
||
|
|
`127.0.0.1`, y ese valor por defecto es deliberado — déjalo así.**
|
||
|
|
|
||
|
|
- **No** lo ejecutes en una máquina que te importe, ni en nada que contenga
|
||
|
|
datos.
|
||
|
|
- **No** lo enlaces a `0.0.0.0` ni a una interfaz de red real. Está
|
||
|
|
deliberadamente diseñado para ser explotable de forma remota.
|
||
|
|
- Apuntar `fooc` a un host que no posees o para el que no tienes permiso
|
||
|
|
escrito de prueba es un delito informático en la mayoría de las
|
||
|
|
jurisdicciones — también bajo la UK Computer Misuse Act y la US Computer
|
||
|
|
Fraud and Abuse Act.
|
||
|
|
- Se enlaza a un puerto no privilegiado (>1024), así que no necesitas root. No
|
||
|
|
lo "mejores" añadiendo capabilities o ejecutándolo como servicio del sistema.
|
||
|
|
- Cada conexión se gestiona en un hijo `fork()`, y `food` hace reap de él, así
|
||
|
|
que los crashes no se acumulan. Si luego encuentras docenas de `sh`
|
||
|
|
sueltos, `pkill -x sh` es la limpieza.
|
||
|
|
|
||
|
|
En caso de duda: este laboratorio es para una máquina virtual o un contenedor,
|
||
|
|
en una red que tú controlas, en una máquina sin nada que echaras de menos.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Inicio rápido
|
||
|
|
|
||
|
|
```sh
|
||
|
|
make # compila food, fooc y los harness de prueba
|
||
|
|
make run # arranca food en 127.0.0.1:2342, desacoplado en segundo plano
|
||
|
|
make test # ejecuta las tres técnicas de exploit
|
||
|
|
make stop # detiene el demonio
|
||
|
|
```
|
||
|
|
|
||
|
|
Después, a mano:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
./fooc -t leak # mira las fugas de direcciones que food revela
|
||
|
|
./fooc -t demo -v # envía basura; ve morir a food con SIGSEGV
|
||
|
|
./fooc -t ret2win -i # salta a una función que ya existe -> shell
|
||
|
|
```
|
||
|
|
|
||
|
|
### Requisitos
|
||
|
|
|
||
|
|
| Herramienta | Para qué | Notas |
|
||
|
|
|---|---|---|
|
||
|
|
| `gcc` (o clang) | compilar | C99. Probado con gcc 16.2 |
|
||
|
|
| `objdump` | `fooc` | binutils. `fooc` lo invoca en tiempo de ejecución |
|
||
|
|
| `nasm` | `make verify` | solo para contrastar el shellcode; se omite si falta |
|
||
|
|
| `gdb` | `make debug` | opcional |
|
||
|
|
| Linux, x86-64 | ambos | el payload y la caza de gadgets dependen de la arquitectura |
|
||
|
|
|
||
|
|
`fooc` también necesita `-ldl` para `dlsym()`; el Makefile lo gestiona.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## El bug
|
||
|
|
|
||
|
|
Una línea en `food.c` es toda la superficie de ataque:
|
||
|
|
|
||
|
|
```c
|
||
|
|
char buf[FOOD_BUFSZ]; /* 64 bytes */
|
||
|
|
n = read(fd, buf, FOOD_READMAX); /* hasta 512 bytes de la red */
|
||
|
|
```
|
||
|
|
|
||
|
|
64 bytes de destino, 512 aceptados. El atacante sobrescribe 448 bytes más allá
|
||
|
|
del final del búfer, y como la pila crece hacia abajo, "más allá del final"
|
||
|
|
significa "dentro del marco superior" — y ahí es exactamente donde están el
|
||
|
|
puntero de marco guardado y la **dirección de retorno guardada**.
|
||
|
|
|
||
|
|
En una función x86-64 compilada a `-O0`:
|
||
|
|
|
||
|
|
```
|
||
|
|
direcciones altas
|
||
|
|
+------------------------+ rbp + 16 : locales de la llamadora
|
||
|
|
| ... |
|
||
|
|
+------------------------+ rbp + 8 : DIRECCIÓN DE RETORNO GUARDADA <-- se vuelve RIP
|
||
|
|
| saved rbp (8 bytes) |
|
||
|
|
+------------------------+ rbp : nuestro puntero de marco
|
||
|
|
| line[128] |
|
||
|
|
| buf[64] | <- rsp: lo que read() llena
|
||
|
|
+------------------------+
|
||
|
|
direcciones bajas
|
||
|
|
```
|
||
|
|
|
||
|
|
Cuando la función retorna, `leave; ret` hace pop de los 8 bytes en `RIP`, y la
|
||
|
|
CPU salta donde el atacante ha decidido. Todo lo demás en este laboratorio es
|
||
|
|
aritmética sobre hacia dónde apuntar.
|
||
|
|
|
||
|
|
Para esta compilación, los números son: `buf` mide 64 bytes, el `rbp` guardado
|
||
|
|
mide 8, así que la dirección de retorno está en el offset **88** desde el
|
||
|
|
inicio de `buf`. `fooc` no hardcodea eso — desensambla `food` y encuentra el
|
||
|
|
`lea -0x50(%rbp)` delante de `call read@plt`, así que sigue funcionando si
|
||
|
|
cambias `FOOD_BUFSZ`.
|
||
|
|
|
||
|
|
> gcc ya te lo dice. Compilar `food` imprime:
|
||
|
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||
|
|
> destination [-Wstringop-overflow=]`. Nunca silencies esa advertencia en
|
||
|
|
> código real. Es seguridad gratuita.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Las tres técnicas
|
||
|
|
|
||
|
|
`fooc -t <technique>`. Están en el orden en que un atacante real trabajaría en
|
||
|
|
ellas, porque cada una necesita lo que la anterior te enseñó.
|
||
|
|
|
||
|
|
### 1. `ret2win` — controla el puntero de instrucción
|
||
|
|
|
||
|
|
```
|
||
|
|
[ 88 bytes de basura ][ la dirección del win() de food ]
|
||
|
|
^ saved rbp
|
||
|
|
^ se vuelve RIP
|
||
|
|
```
|
||
|
|
|
||
|
|
`win()` es una función del programa objetivo que hace exec de `/bin/sh`.
|
||
|
|
Sobrescribir la dirección de retorno con su dirección es todo el exploit.
|
||
|
|
|
||
|
|
**Lo que enseña:** tienes control arbitrario del puntero de instrucción.
|
||
|
|
Tampoco necesita fuga, porque el binario está compilado con `-no-pie`, así que
|
||
|
|
`win()` está en una dirección fija para siempre.
|
||
|
|
|
||
|
|
**El equivalente del mundo real** no es "los ataques son fáciles", sino "no
|
||
|
|
envíes backdoors no documentadas en binarios de red". Si existe una función
|
||
|
|
como `win()` en tu binario, un desbordamiento de búfer la encontrará. Es
|
||
|
|
literalmente la clase de CVE de backdoor de Juniper ScreenOS.
|
||
|
|
|
||
|
|
**Defensa:** `-fPIE` (o ASLR) randomiza la dirección de carga, así que el
|
||
|
|
atacante debe conocer la dirección — lo que normalmente significa que primero
|
||
|
|
necesita una fuga. Por eso `ret2win` falla contra `food_hardened`.
|
||
|
|
|
||
|
|
### 2. `ret2libc` — llama a lo que sea, por su nombre
|
||
|
|
|
||
|
|
```
|
||
|
|
[ basura ][ pop rdi; ret ][ dirección de "/bin/sh" ][ dirección de system() ]
|
||
|
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
pone rdi la cadena a enviar la función a llamar
|
||
|
|
```
|
||
|
|
|
||
|
|
En ejecución: `ret` hace pop de `pop rdi; ret` en RIP; eso hace pop del puntero
|
||
|
|
`"/bin/sh"` en `RDI`; su `ret` hace pop de `system()` en RIP, mientras `RDI`
|
||
|
|
sigue sosteniendo la cadena. `system("/bin/sh")` se ejecuta.
|
||
|
|
|
||
|
|
Los gadgets (`pop rdi; ret`) no están en `food` — esta glibc no tiene
|
||
|
|
`__libc_csu_init` — así que `fooc` los encuentra escaneando la memoria viva de
|
||
|
|
libc en busca del par de bytes `5f c3`. Localiza libc vía `/proc/self/maps`,
|
||
|
|
encuentra los offsets de `system` y `"/bin/sh"` con `dlsym()` y calcula la base
|
||
|
|
a partir de la fuga que `food` divulga. Nada está hardcodeado, así que
|
||
|
|
sobrevive a una actualización de libc.
|
||
|
|
|
||
|
|
**Lo que enseña:** una vez que puedes controlar `RIP`, puedes encadenar
|
||
|
|
instrucciones *existentes*. Eso es return-oriented programming, y así se ven
|
||
|
|
casi todos los exploits reales, porque no requiere memoria ejecutable provista
|
||
|
|
por el atacante.
|
||
|
|
|
||
|
|
**Defensa:** ninguno de los flags del compilador lo detiene solo. Funciona
|
||
|
|
contra un binario PIE, con NX, con canary — mientras el atacante tenga una
|
||
|
|
fuga. Las defensas son "no tengas el desbordamiento" y "no fugues
|
||
|
|
direcciones". Ver la tabla más abajo.
|
||
|
|
|
||
|
|
### 3. `shellcode` — ejecuta tu propio código máquina
|
||
|
|
|
||
|
|
23 bytes, colocados al inicio del búfer, con `RIP` apuntando a ellos:
|
||
|
|
|
||
|
|
```asm
|
||
|
|
xor esi, esi ; envp = NULL
|
||
|
|
xor edx, edx ; argv = NULL
|
||
|
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" como 8 bytes crudos
|
||
|
|
push rdi ; deja la cadena en la pila
|
||
|
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||
|
|
push 0x3b ; 59 = __NR_execve
|
||
|
|
pop rax
|
||
|
|
syscall ; ahora somos un shell
|
||
|
|
```
|
||
|
|
|
||
|
|
Esta es la forma más pura del bug: el atacante entrega *las instrucciones*, no
|
||
|
|
solo la dirección de instrucciones que ya existen. No se necesitan offsets de
|
||
|
|
libc, así que en principio funciona contra un objetivo estáticamente enlazado,
|
||
|
|
totalmente randomizado.
|
||
|
|
|
||
|
|
`make verify` ensambla `shellcode.S` y lo compara con el array de bytes embebido
|
||
|
|
en `fooc.c`, para que no puedan divergir.
|
||
|
|
|
||
|
|
**Defensa:** **NX** (también llamado W^X, "no execute"). Marcar la pila como no
|
||
|
|
ejecutable hace que el hardware se niegue a buscar instrucciones en ella, y el
|
||
|
|
`ret` aterriza en una página que no puede ejecutarse. Por eso `make food` pasa
|
||
|
|
`-z execstack`: una pila Linux normal es `rw-p`, no `rwx`, y la técnica muere
|
||
|
|
con SIGSEGV en `RIP = la dirección del payload`. La lección más importante del
|
||
|
|
laboratorio es que cada uno de estos bytes funciona solo porque se le dijo al
|
||
|
|
compilador que dejara la pila ejecutable. Ese flag está activado para bien de
|
||
|
|
nadie.
|
||
|
|
|
||
|
|
### También incluido
|
||
|
|
|
||
|
|
| Modo | Qué hace |
|
||
|
|
|---|---|
|
||
|
|
| `-t leak` | se conecta, imprime fugas, no envía nada |
|
||
|
|
| `-t demo` | envía `rip_off + 8` bytes de `0x41`, así que `RIP` se vuelve `0x4141...` y el demonio muere. Prueba el bug sin ningún conocimiento de direcciones |
|
||
|
|
| `-t sled` | un ret-sled, conservado deliberadamente como ejemplo **fallido**. Sin una fuga, harías fuerza bruta a ASLR llenando el búfer con la dirección de un `ret`. No puede funcionar aquí: `food` acepta 512 bytes, así que el sled tiene ~53 ranuras frente a ~28 bits de entropía. Implementado para que puedas verlo fallar y confirmar que el mecanismo es de verdad "la CPU sigue una cadena de rets" |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## La tabla de mitigaciones
|
||
|
|
|
||
|
|
Esta es la parte que hay que recordar. Cada fila es una defensa real, y la
|
||
|
|
columna derecha muestra qué hace realmente con la cadena de eventos.
|
||
|
|
|
||
|
|
| Mitigación | Cómo activarla | Qué detiene | Qué *no* detiene |
|
||
|
|
|---|---|---|---|
|
||
|
|
| **Limita read** | `n = read(fd, buf, sizeof buf - 1);` | **Todo.** El bug no existe, así que nada aguas abajo importa | Nada — es el único fix completo |
|
||
|
|
| **Canary de pila** | `-fstack-protector-strong` (por defecto en gcc) | El `ret`: la canary se comprueba al final de la función, así que la destrucción se detecta y el proceso aborta antes de que se haga pop de `RIP` | Un error en una función *sin* array (nada que proteger); un desbordamiento que se mantiene por debajo de la canary; todo lo que no retorna normalmente |
|
||
|
|
| **NX / W^X** | `-z noexecstack` (el valor por defecto) | Shellcode. Las instrucciones del payload no pueden buscarse | ret2win y ret2libc por completo. Son *la razón* de que exista ROP |
|
||
|
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (ambos por defecto) | Las direcciones hardcodeadas de ret2win. Todo se mueve en cada ejecución | Todo donde el atacante tenga una fuga. ASLR sube el precio de un exploit; no es un fix. Nota que la pila, el heap y mmap se randomizan, pero el *contenido* del binario principal no — eso es lo que usan las cadenas ROP |
|
||
|
|
| **No fugues** | ningún `printf("%p")` a clientes; inicializa antes de imprimir | La fuga de información que hace que ASLR sea "gratis" en vez de "caro" | — |
|
||
|
|
| **No uses `printf(user_data)`** | `printf("%s", buf)` en lugar de `printf(buf)` | Errores de cadena de formato: lecturas de pila `%x`, escrituras arbitrarias `%n` — un *otro* camino a RCE | — |
|
||
|
|
| **No uses rutas no confiables** | valida y `openat()` bajo un directorio fijo | Path traversal (CWE-22) | — |
|
||
|
|
| **CET / shadow stack** | `-fcf-protection=full`, soporte de kernel y CPU | El `ret` en sí: la shadow stack recuerda la *verdadera* dirección de retorno y falla ante un desajuste. Atrapa cadenas ROP que usan el `ret` de hardware | Ataques que nunca `ret` (call-oriented, o sobrescribir el objetivo de un puntero de función con una cadena de gadgets que no necesita retorno) |
|
||
|
|
| **Lenguajes seguros** | Rust, Go, C# para código nuevo | Toda la clase. Las comprobaciones de límites se imponen en ejecución, no se esperan en la revisión | — |
|
||
|
|
|
||
|
|
### Compruébalo por ti mismo
|
||
|
|
|
||
|
|
```sh
|
||
|
|
make run # demonio vulnerable
|
||
|
|
make test # las tres técnicas funcionan
|
||
|
|
|
||
|
|
make test-hardened # el mismo código fuente, mitigaciones activadas
|
||
|
|
```
|
||
|
|
|
||
|
|
`test-hardened` compila `food_hardened` con `-fstack-protector-strong -fPIE
|
||
|
|
-pie -z noexecstack`, lo intercambia, vuelve a ejecutar las tres y luego
|
||
|
|
restaura el vulnerable. Verás:
|
||
|
|
|
||
|
|
```
|
||
|
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||
|
|
--- ret2win was stopped by the mitigations (as expected)
|
||
|
|
--- ret2libc was stopped by the mitigations (as expected)
|
||
|
|
--- shellcode was stopped by the mitigations (as expected)
|
||
|
|
```
|
||
|
|
|
||
|
|
Y en el log del demonio endurecido, la canary que se dispara:
|
||
|
|
|
||
|
|
```
|
||
|
|
*** stack smashing detected ***: terminated
|
||
|
|
```
|
||
|
|
|
||
|
|
Léelo con cuidado, porque es la línea más importante de todo el laboratorio:
|
||
|
|
**la canary atrapó a ret2win, no PIE.** Las tres técnicas mueren en la canary,
|
||
|
|
porque las tres pasan por el mismo `read()` y destruyen el mismo marco. NX solo
|
||
|
|
detiene además el *código* del shellcode; PIE solo rompe además la dirección
|
||
|
|
hardcodeada. Actívalas una a una, y descubrirás que la mayoría de las
|
||
|
|
mitigaciones individuales te dejan expuesto a algo.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Archivos
|
||
|
|
|
||
|
|
| Archivo | Propósito |
|
||
|
|
|---|---|
|
||
|
|
| `food.c` | el demonio vulnerable. 6 errores numerados, cada uno con su fix en el comentario |
|
||
|
|
| `fooc.c` | el exploit. Reconocimiento de offset basado en objdump, reconocimiento de libc basado en `/proc`, 4 constructores de payload |
|
||
|
|
| `shellcode.S` | los 23 bytes de shellcode como assembly, para que sean legibles y verificables. `fooc` los lleva en línea y no lo necesita en ejecución |
|
||
|
|
| `Makefile` | compila, prueba y la comparación endurecida |
|
||
|
|
| `tests/pty_test.c` | conduce a `fooc` a través de un pseudo-terminal y comprueba salida real de shell |
|
||
|
|
| `tests/sock_test.c` | verificador independiente sobre un socket crudo, para que el resultado no dependa de `fooc` |
|
||
|
|
| `food.log` | el log del demonio. Tu prueba de lo que ocurrió |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Dos errores de este laboratorio que merece la pena entender
|
||
|
|
|
||
|
|
No son los errores del programa objetivo. Son errores del exploit y de su
|
||
|
|
harness de prueba, y ambos produjeron mentiras convincentes. Están
|
||
|
|
documentados en la fuente donde viven; están aquí porque los patrones de fallo
|
||
|
|
son instructivos.
|
||
|
|
|
||
|
|
### Alineación de pila: el crash que no es una desreferencia NULL
|
||
|
|
|
||
|
|
**Síntoma.** La toma de control aterriza correctamente — `gdb` te muestra
|
||
|
|
dentro de `win()` — y entonces muere lo primero que hace `win()`, un
|
||
|
|
`dprintf()`. El handler de SIGSEGV informa de `RIP` profundo dentro del
|
||
|
|
formateador de glibc y una dirección de error de `(nil)`, lo que parece
|
||
|
|
exactamente un puntero corrupto.
|
||
|
|
|
||
|
|
**Causa.** La ABI System V AMD64 exige una alineación de pila de 16 bytes. Un
|
||
|
|
`ret` normal restaura `%rsp` exactamente como el `call` correspondiente lo
|
||
|
|
guardó, así que la invariante se preserva gratis. Nuestro `ret` desnudo no:
|
||
|
|
después de él, `%rsp = buf + rip_off`. Aquí, `buf` está alineado a 16 bytes y
|
||
|
|
`rip_off` es 88, así que la callee recibe una pila de 8 mod 16. glibc está
|
||
|
|
compilada con SSE2, y `movaps` **falla** ante un operando mal alineado. En x86
|
||
|
|
eso levanta `#GP`, no `#PF`, así que el kernel no tiene dirección de error e
|
||
|
|
informa `si_addr = 0`. Ese NULL es la pista: un error de alineación disfrazado
|
||
|
|
de desreferencia NULL.
|
||
|
|
|
||
|
|
**Fix.** Un gadget `ret` *en el offset `rip_off`*, que desplaza el objetivo
|
||
|
|
real 8 bytes, porque cada `ret` añade exactamente 8 a `%rsp`. El orden es
|
||
|
|
crítico: una versión anterior pegaba el `ret` *después* del objetivo y
|
||
|
|
producía `[ padding | target | ret ]`, donde el `ret` final nunca se alcanza y
|
||
|
|
el fix no hace nada en silencio. Un `ret` perdido que parece un error es casi
|
||
|
|
siempre intencional.
|
||
|
|
|
||
|
|
### Un socket, dos lectores: el byte que desapareció
|
||
|
|
|
||
|
|
**Síntoma.** El shellcode se reportó como funcionando. Luego se endureció el
|
||
|
|
harness pty (apagar `ECHO`, para que la terminal dejara de ecoar su propia
|
||
|
|
línea de comandos hacia sí misma), y la técnica empezó a fallar. Más
|
||
|
|
profundo, cada técnica perdía exactamente un byte del inicio de cada trozo de
|
||
|
|
salida: `uid=1000(hanez)` se imprimía como `id=1000(hanez)`, `PWNED-OK` como
|
||
|
|
`WNED-OK`, `Linux 7.2.7` como `inux 7.2.7`.
|
||
|
|
|
||
|
|
**Causa.** `fooc` solía hacer `dup2()` del socket sobre su propio stdin/stdout
|
||
|
|
y `execv()` de un `/bin/sh` *local*, mientras un hijo relay forkado también leía
|
||
|
|
el mismo socket para mover la salida al terminal. Al kernel le da igual que los
|
||
|
|
dos cooperen. Un socket de stream tiene **un** cursor de lectura, y cada lector
|
||
|
|
lo mueve, así que los bytes se reparten entre ellos de forma impredecible. El
|
||
|
|
shell local — un shell de login interactivo — leía exactamente un byte y lo
|
||
|
|
desechaba, cada vez. `strace -f` lo mostró de inmediato:
|
||
|
|
|
||
|
|
```
|
||
|
|
read(0, "u", 1) <- el shell local, comiéndose un byte
|
||
|
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- el relay, 1 byte corto
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix.** No hay ningún shell en este lado, punto. Hay exactamente un shell en
|
||
|
|
todo el cuadro, y está en la víctima, dentro del proceso secuestrado, con la
|
||
|
|
conexión TCP como su stdin/stdout. Este lado solo mueve bytes. Si alguna vez
|
||
|
|
necesitas dos consumidores de un stream, ese stream necesita un único lector
|
||
|
|
que lo demultiplexe deliberadamente.
|
||
|
|
|
||
|
|
**La meta-lección.** El primer resultado "funcionante" fue un falso positivo,
|
||
|
|
producido porque la pty ecoaba su propia línea de comandos hacia sí misma, y el
|
||
|
|
fix de ese falso positivo es lo que reveló el error real. Las pruebas que no
|
||
|
|
pueden fallar son peores que ninguna prueba, porque convierten "no lo sé" en
|
||
|
|
"funciona". Un harness de prueba merece la misma sospecha que el código que
|
||
|
|
prueba.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Experimentar con ello
|
||
|
|
|
||
|
|
Cosas que merece la pena probar, más o menos en el orden en que más aprendes de
|
||
|
|
ellas:
|
||
|
|
|
||
|
|
1. **Cambia `FOOD_BUFSZ` a 128.** Vuelve a ejecutar `fooc`. Debería seguir
|
||
|
|
funcionando sin cambios, porque lee el offset del desensamblado. Luego
|
||
|
|
rómpelo a mano — hardcodea 88 — y míralo crashear. Después añade un segundo
|
||
|
|
array entre `buf` y los registros guardados, y mira cómo lo gestiona el
|
||
|
|
reconocimiento automático.
|
||
|
|
|
||
|
|
2. **Añade `-Wformat-security` y mira qué hace el camino de cadena de
|
||
|
|
formato.** Envía `%p %p %p %n` y mira a `food` fugando la pila.
|
||
|
|
|
||
|
|
3. **Usa gdb.** `make debug`, luego:
|
||
|
|
```gdb
|
||
|
|
(gdb) break food.c:393 # el read() que se desborda
|
||
|
|
(gdb) run -p 2342
|
||
|
|
(gdb) info registers rsp rbp
|
||
|
|
(gdb) x/24gx $rsp # observa dónde está la dirección de retorno
|
||
|
|
(gdb) c # en otra terminal: ./fooc -t ret2win
|
||
|
|
```
|
||
|
|
El handler de SIGSEGV registra `REG_RIP` y `REG_RSP`, así que `food.log` te
|
||
|
|
dice si la toma de control aterrizó, incluso cuando el hijo muere antes de
|
||
|
|
que puedas adjuntarte.
|
||
|
|
|
||
|
|
4. **Borra el fix de alineación** en `fooc.c` y mira el error `#GP` con la
|
||
|
|
firma `si_addr = 0`. Luego lee `/proc/sys/kernel/randomize_va_space` y
|
||
|
|
piensa qué randomiza ASLR y qué no.
|
||
|
|
|
||
|
|
5. **Rompe la resolución de símbolos de libc** y mira cómo se adapta `fooc`.
|
||
|
|
Todo el punto del enfoque `/proc/self/maps` es que ningún offset está
|
||
|
|
hardcodeado.
|
||
|
|
|
||
|
|
6. **Escribe una cuarta técnica.** Una cadena tipo `ret2csu` si puedes
|
||
|
|
encontrar `__libc_csu_init`, o una cadena SROP (los marcos `sigreturn` te
|
||
|
|
dejan controlar todos los registros a la vez). Ambas son ROP puro y no
|
||
|
|
necesitan memoria ejecutable.
|
||
|
|
|
||
|
|
7. **Arregla `food.c` de verdad**, un error a la vez, y vuelve a ejecutar el
|
||
|
|
exploit después de cada fix. El orden de la tabla al inicio de `food.c` es
|
||
|
|
más o menos el orden correcto en que pensar: limita primero el read, porque
|
||
|
|
nada más importa hasta que el bug desaparece.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Limpieza
|
||
|
|
|
||
|
|
```sh
|
||
|
|
make stop # detiene food
|
||
|
|
make clean # elimina los productos de compilación; deja food.log en paz
|
||
|
|
pkill -x sh # solo si tienes shells sueltos de una prueba que salió mal
|
||
|
|
```
|
||
|
|
|
||
|
|
Nota: `pkill -x food` coincide exactamente con el **nombre** del proceso. No
|
||
|
|
uses `pkill -f ./food` — ese patrón también coincide con el shell donde lo
|
||
|
|
escribes y mata tu propia sesión. No es una hipótesis; ocurrió mientras se
|
||
|
|
construía este laboratorio.
|