# Laboratorio de RCE root por SUID — `foosd` (demonio) + `foosc` (exploit) Un compañero del laboratorio principal (`food` / `fooc`, un demonio normal donde un desbordamiento de búfer te da un shell de *usuario*). Este añade el cambio de un solo carácter más peligroso de Unix: **el bit setuid**. > `chmod u+s` convierte "el atacante puede ejecutar código en este host" en "el > atacante puede ejecutar código como **root** en este host". Esa frase es todo el laboratorio. Todo lo que sigue es el mecanismo que tiene debajo, escrito, para que cuando escribas tu propio software sepas exactamente qué dos o tres atributos del sistema de archivos y flags del compilador deciden si un error de seguridad de memoria en tu código es una molestia o un shell root. La demo final, cuando `foosd` es setuid-root, es un **shell root** abierto a través de la red ejecutando 32 bytes de shellcode escrita a mano. --- ## 1. Qué hace realmente el bit setuid Cada proceso en Linux lleva tres user-ID, y el bit setuid toca la relación entre ellos: | ID | Nombre | Significado | |----|------|---------| | `ruid` | user-ID real | la cuenta que *inició* el proceso | | `euid` | user-ID efectivo | lo que el kernel comprueba al imponer el acceso | | (saved) | set-user-ID guardado | una "ranura" a la que un proceso privilegiado puede volver más tarde | Un programa normal tiene `ruid == euid`. Cuando ejecutas un binario con el bit setuid puesto, propiedad de root: ```text ruid = tú (p. ej. 1000, "hanez") euid = el dueño (p. ej. 0, "root") ``` El proceso tiene por tanto **la autoridad de root**, aunque el usuario que lo inició sea perfectamente normal. Cada comprobación que hace el kernel — ¿puede este proceso leer `/etc/shadow`? ¿escribir un archivo? ¿matar a otro proceso? — se responde con `euid`, es decir, "sí, es root". `foosd` es un demonio de red. Enlaza un puerto y luego hace `fork()` de un hijo por conexión. Un fork *hereda* el euid, así que cada hijo que gestiona una conexión también es root. El desbordamiento en `vulnerable_handler()` de `foosd` es por tanto un desbordamiento *dentro de un proceso root*. **Diagnostícalo tú mismo cuando el demonio esté corriendo:** ```console $ ./foosd ... # mira la línea de log que imprime al arrancar [foosd 1234] startup: ruid=1000 euid=0 -> ROOT process ``` y desde el exploit: ```console $ ./foosc -t leak foosc: target euid=0 ruid=1000 ``` --- ## 2. El laboratorio de un vistazo | Archivo | Rol | |------|------| | `foosd.c` | El demonio deliberadamente vulnerable (dueño de los errores). Ejecútalo como *binario setuid-root* para la demo del shell root. | | `foosc.c` | El exploit. Usa por defecto la técnica de shellcode `setreuid + execve` de 32 bytes. | | `shellcode.S` | El shellcode de referencia; `make verify` lo compara con el array de bytes en `foosc.c`. | | `tests/pty_suid_test.c` | Harness de prueba. Conduce a `foosc` a través de un pseudo-terminal y prueba tanto "corrió un shell" *como* "era root" (`uid=0(`). | | `Makefile` | Compilación, helpers `setuid`/`unsetuid`, matriz de prueba. | | `README.md` | Este archivo. | > **¿Por qué una pty?** La última acción del exploit es retransmitir tu > terminal al shell que corre en la víctima. Un pipe o un here-doc llega al > lado equivocado de esa retransmisión; se requiere un terminal real. --- ## 3. Inicio rápido ```console $ make # compila todo, como tu usuario normal $ make setuid # una vez, pide sudo: chown root + chmod u+s $ make run # arranca foosd en 127.0.0.1:2343 $ make test-suid # matriz completa; shellcode + ret2win-root deben dar root ``` Prueba de humo interactiva: ```console $ ./foosc -t shellcode ... foosc: target euid=0 ruid=1000 foosc: shell is on the victim (root if foosd is SUID); relaying # id uid=0(root) gid=0(root) groups=0(root) <-- eres root, en la víctima # exit ``` Cuando termines: ```console $ make stop $ make unsetuid # higiene: no dejes nunca un binario root SUID suelto ``` --- ## 4. *¿Cuándo pongo el bit SUID?* — la respuesta que pediste Exactamente **una vez, después de compilar, antes de arrancar el demonio para las demos de shell root** — y solo en una máquina que sea tuya, apta para tirar y desconectada de la red: ```console $ make # compila foosd, foosc, las pruebas $ make setuid # <-- EL MOMENTO. sudo chown root:root foosd && sudo chmod u+s foosd $ make run # arranca DESPUÉS de poner el bit ``` Dos reglas que importan más que el momento exacto: 1. **Ponlo solo cuando el binario esté terminado.** Si recompilas (`make` / `make clean`) después de poner el bit, te topas con "Permission denied" al escribir los archivos de salida propiedad de root — y si fuerzas la recompilación, el toolchain recrea el archivo **sin** la `s` y deshace la configuración en silencio. El orden canónico en cualquier recompilación es por tanto ```console $ make unsetuid && make && make setuid ``` 2. **Quítalo cuando termines.** `make unsetuid`. Un binario setuid vivo, propiedad de root, con un error explotable en tu árbol no es una herramienta pedagógica, es un agujero root con un error de compilación entre él y nada. En una máquina compartida o de producción: **no hagas nada de esto.** El demonio además se niega por defecto a enlazarse a nada que no sea loopback (ver §7). Si ejecutas el exploit *sin* poner nunca el bit, nada se rompe — el payload sigue aterrizando y sigues obteniendo un shell. La diferencia está en un solo número, y el exploit lo dice en voz alta: ```console foosc: WARNING: the daemon is NOT running with euid 0. The payload will still land, but the shell will be a plain user shell, not root. Fix: sudo make setuid ``` El resultado "funcionó, pero no root" es en sí parte del laboratorio. Recuérdalo para la siguiente sección. --- ## 5. El mecanismo — y el giro que hace interesante a SUID ### 5.1 El desbordamiento (idéntico a `food`) El handler de `foosd` da a un `read()` 512 bytes de confianza mientras le ofrece un búfer de pila de 64 bytes: ```c char buf[64]; n = read(fd, buf, 512); /* <- CWE-120: 448 bytes sobre el borde */ ``` En x86-64 la pila crece hacia abajo. El exploit escribe 64 bytes de basura para llenar `buf`, 8 para llenar el puntero de marco guardado y 8 más para reemplazar la **dirección de retorno guardada**. Cuando `vulnerable_handler` ejecuta `ret`, la CPU hace pop del valor del atacante en `RIP` — ejecución de código controlada por el atacante. El exploit encuentra la distancia exacta (88 bytes para esta compilación) analizando la salida de `objdump` en lugar de hardcodearla, así que el número sobrevive a las recompilaciones. ### 5.2 El giro: el shell se niega a ser root Aquí es donde pensar "bug SUID → spawn /bin/sh → root" iría mal, y por qué este laboratorio tiene exactamente la forma que tiene. Cuando corre un programa setuid-root, su `ruid` sigue siendo el usuario que lo inició y su `euid` es root. Si el programa — o el atacante — lanza ahora un shell: * `execve("/bin/sh")` **no** cambia los uids; el nuevo proceso hereda `(ruid=1000, euid=0)`. * bash (y dash) **comprueba exactamente ese estado al arrancar**. Del manual de bash: *"If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, … the effective user id is set to the real user id."* Así que el shell se mira y *suelta root* — una defensa que los autores de shell construyeron exactamente contra este ataque (la justificación histórica era el problema de las shells setuid / scripts setuid). El resultado son los casos "funcionó, pero no root": | Técnica | Qué ejecuta | uid resultante | |-----------|------------------|---------------| | `ret2win` | el `win()` de `foosd` → `execl("/bin/sh")` | **1000** — shell aterrizado, root reseteado por bash | | `ret2libc` | `system("/bin/sh")` → `sh -c '/bin/sh'` fresco | **1000** — el mismo reset, un nivel abajo | | `ret2win-root` | el `win_root()` de `foosd` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid limpiado desde C | | `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid limpiado desde código máquina | Las que alcanzan root se diferencian de las que no lo hacen en exactamente una idea: **limpian el uid *real*, no solo el efectivo.** ```c setuid(0) /* pone euid a 0, pero ruid sigue en 1000: bash sigue viendo euid != ruid y resetea IGUAL. */ setreuid(0, 0) /* pone AMBOS: ruid = euid = 0. bash ve uids iguales y conserva root. */ ``` Por eso el shellcode `/bin/sh` clásico que encuentras por todo internet empieza con un syscall de limpieza de uid — y por eso el shellcode aquí es de 32 bytes en lugar de 23: las primeras cinco instrucciones son ```asm xor edi, edi ; ruid = 0 xor esi, esi ; euid = 0 push 0x71 ; 113 = __NR_setreuid pop rax syscall ``` ### 5.3 Entonces, ¿qué es el exploit, de principio a fin? 1. `foosc` lee el banner de `foosd` por el socket. Obtiene: - `ids=0/1000` — euid/ruid (el autodiagnóstico SUID) - `stack=…` y `libc=…` — punteros (las fugas de ASLR) - `BUF=…` — la dirección exacta del búfer que está a punto de desbordar 2. Del binario objetivo (vía `objdump`) aprende `rip_off` y las direcciones de `win()` / `win_root()`. 3. De *su propia* libc (vía `/proc/self/maps` + `dlsym` + un escaneo de memoria) mide los offsets de `system`, `read`, `/bin/sh` y un gadget `pop rdi; ret` — nada está hardcodeado. 4. Ensambla el payload. Para `-t shellcode`, es: `[código setreuid+execve de 32 bytes][basura hasta RIP][ret-fix][dirección de buf]`. 5. El `read()` de `foosd` se desborda; el `ret` aterriza en el shellcode; el kernel ejecuta `setreuid(0,0)` (sin problema: euid 0 es privilegiado) y luego `execve` de `/bin/sh`. bash arranca con `ruid == euid == 0` y sigue siendo root. 6. `foosc` retransmite tu terminal a ese shell root, hasta que escribes `exit`. Un detalle de comodidad que cuesta caro a la gente si se pasa por alto: el exploit prueba cada comportamiento de limpieza de uid **sin** necesitar primero el bit setuid. Ejecuta `make test` antes de `make setuid`, y verás cada técnica aterrizar un shell con `ROOT=MISSING`; ejecuta `make test-suid` después de `make setuid`, y `ROOT=SEEN` aparece en las dos técnicas que limpian el uid real. Ese A/B es toda la lección, representada en diez segundos. --- ## 6. Los viejos one-liners — y por qué la mayoría están muertos Si has leído sobre SUID, has leído sobre secuestro de `PATH`, `LD_PRELOAD` y shells setuid. Los tres son clásicos, y los tres fallan en un sistema moderno contra *este programa*. Vale la pena saber exactamente por qué, porque las razones son las defensas que obtienes gratis: | Clase de ataque | Vieja afirmación | Por qué falla en una máquina moderna | |--------------|-----------|------------------------------| | `LD_PRELOAD` de una biblioteca maliciosa | "El programa setuid carga mi `.so` y ejecuta mi código como root." | El kernel marca un binario setuid como **AT_SECURE**; glibc ignora entonces `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` y compañía. El entorno se trata como *entrada no confiable*. `LD_PRELOAD` contra un binario setuid es un no-op. | | Secuestro de `PATH` (`system("ls")` con un PATH envenenado) | "Apunta PATH a un directorio con mi `ls` falso; el programa root lo ejecutará." | Otra cara de la misma defensa: un proceso AT_SECURE recibe un **PATH saneado** (un valor por defecto seguro, más o menos `/usr/local/bin:/usr/bin:/bin`) para `system()`/`execvp`, así que el directorio envenenado nunca se consulta. | | Inyección de comando `system()` setuid | "El comando inyectado se ejecuta con euid 0." | `system()` ejecuta el comando en un `/bin/sh` nuevo, y ese shell — §5.2 — resetea `euid = ruid` al arrancar. El comando inyectado se ejecuta con el uid *real*. (Sigue siendo un error; solo que ya no escala vía `/bin/sh`.) | | Shell root setuid en disco (`cp /bin/sh /tmp; chmod u+s`) | "Ejecútalo, consigue root." | Exactamente la defensa de arriba, y esa es la razón por la que las distros modernas no entregan ningún shell root setuid. Incluso si consigues fabricar uno, bash se niega a mantener euid 0 salvo que se inicie con `-p`. | Lo que sigue vivo, y eso es este laboratorio: **el programa *ya* es root cuando corre.** No necesitas el entorno ni `system()`; necesitas que el programa ejecute *tu* código (vía un error de corrupción de memoria) mientras es privilegiado, y que tu código sea lo bastante cuidadoso para corregir él mismo el desajuste de uids — `setreuid(0,0)` — antes de entregarte un shell. La corrupción de memoria + SUID es la combinación que todavía termina en `uid=0`, y eso es exactamente por qué los lenguajes seguros en memoria, las canaries y las pilas no-ejecutables no son una decisión de moda. --- ## 7. Las barandillas de seguridad integradas en el demonio `foosd` es deliberadamente la *peor* pieza de software de este repositorio, así que también lleva más barandillas: 1. **Solo loopback, impuesto.** `foosd` rechaza cualquier dirección de bind fuera del loopback, salvo que pases `-L`. Un listener setuid-root en una interfaz real es un servicio root remoto; el rechazo es el valor por defecto, para que el estado peligroso tenga que escribirse deliberadamente. 2. **Autodiagnóstico.** Al arrancar registra `ruid`/`euid` y si corre como root, para que la consola muestre el estado del que depende el exploit. 3. **El log nunca llega al cliente.** El demonio reserva un descriptor de log privado antes de que los sockets reemplacen a fd 1, para que la salida del crash-reporter y las rutas internas no puedan leerse de vuelta por el cable por el atacante. 4. **Crash-reporter.** Un handler de SIGSEGV registra `RIP`/`RSP` — el valor que el atacante escribió en la dirección de retorno — para que una toma de control exitosa sea visible en `foosd.log` en lugar de ser una muerte silenciosa. 5. **`make unsetuid`.** Quitar el bit está scripteado, porque dejarlo puesto es el modo de fallo que la gente realmente tiene. --- ## 8. Mitigaciones — qué detiene cada una y qué *no* detiene Aplicadas a `foosd` vía `make hardened`, una a una o juntas: | Mitigación | Qué detiene | Qué *no* detiene | |------------|---------------|-------------------------| | `-fstack-protector-strong` (canary) | El desbordamiento: `ret` detecta una canary destruida y aborta antes de que se use la dirección del atacante. Detiene aquí **las cuatro** técnicas — comparten el único `read()` vulnerable. | Nada por *diseño*: el binario sigue siendo setuid-root; otro error (format-string-`%n`, heap-overflow, use-after-free) no tiene canary que disparar. | | `-fPIE -pie` (ASLR para el binario) | El uso de direcciones `win()`/`win_root()` predecibles (las técnicas ret2win). | La técnica de shellcode, si todavía se filtra una dirección de pila (línea `BUF=`). | | `-z noexecstack` (NX / W^X) | El shellcode: la CPU se niega a buscar instrucciones en una página solo-de-datos, así que un salto a `buf` es un SIGSEGV. | ROP — ejecutar código que ya existe (`ret2libc`). | | Las tres juntas | Un binario difícil de desbordar, randomizado, con pila no ejecutable. Así se ve una build endurecida normal. | El bit setuid. **Un binario SUID endurecido sigue siendo un binario SUID.** Si sobrevive cualquier error de memoria alcanzable, sigue siendo "error en un proceso root". | La prueba en consola es `make test-hardened`, que intercambia la build endurecida y muestra las técnicas muriendo en la canary, mientras `foosd_hardened.log` captura `*** stack smashing detected ***`. Dos mitigaciones de nivel de diseño que ningún flag de compilador entrega, y que el laboratorio principal (`food`) también usa: - **Mínimo privilegio.** Un demonio para un puerto no privilegiado (2343 > 1024) no tiene ninguna necesidad legítima de root. Un `foosd` correcto enlazaría y luego haría `setgroups`/`setgid`/`setuid` a una cuenta no privilegiada y *confirmaría que se mantuvo* (la versión correcta está en la fuente como `drop_privs()`, nunca llamada — el no-lamarlo es el error n.º 3 del laboratorio). - **Limita el read.** `n = read(fd, buf, sizeof(buf) - 1)`. Una línea correcta supera a todos los flags de compilador de la tabla. --- ## 9. El protocolo wire (para que puedas leer el demonio con netcat) ```text FOOSD 1.0 - deliberately vulnerable SUID service Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512. FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f... BUF=0x7ffd... ``` * `ids=euid/ruid` — no podía imprimirse como `euid=`/`ruid=`, porque el harness de prueba prueba un shell haciendo grep del `uid=` literal, y el banner no debe contenerlo (una sonda que comparte la firma con la respuesta es una trampa clásica de falso positivo; ver el comentario en `foosd.c`). El harness además exige la forma estricta de salida `id` — `uid=NNN(...)` — para que nada de lo que imprima el demonio o el exploit pueda satisfacer la comprobación por accidente: el propio "target euid=… ruid=…" de `foosc` contiene `uid=` como subcadena, lo que una vez hizo que una prueba endurecida reportara un shell que nunca había corrido. * `stack=`, `libc=`, `BUF=` — las fugas de ASLR: dejan que el shellcode y ret2libc calculen direcciones exactas. --- ## 10. Ejercicios 1. **Considera la degradación no-root.** Ejecuta `make test` *antes* de `make setuid`, y luego otra vez después. Explica el cambio a `ROOT=SEEN` con la historia ruid/euid de la §5.2. 2. **Lee el crash.** Ejecuta `./foosc -t demo -n` y luego lee `foosd.log`. La línea `RIP=0x4141414141414141` es la basura del atacante — la prueba de que es el desbordamiento, no el azar, quien controla la ejecución. 3. **Añade la canary.** `make hardened` y modifica tú mismo el bucle `test-hardened`; la línea de log `*** stack smashing detected ***` es la defensa funcionando. 4. **Desactiva la fuga.** Comenta la línea `BUF=` en `foosd.c`, recompila, y mira `-t shellcode` pasar de determinista a un juego de adivinanzas. Esa única línea es la razón por la que los bypass reales de ASLR son todo un campo. 5. **El experimento `-p`.** En una copia de `win()`, cambia `execl("/bin/sh", "sh", NULL)` por `execl("/bin/sh", "sh", "-p", NULL)` y observa root. `-p` es la salida de emergencia documentada del guardián del shell — y la razón por la que el consejo "solo haz spawn de un shell" de los viejos write-ups es incompleto. 6. **¿Por qué no `setuid(0)`?** Reescribe el shellcode para llamar a `setuid(0)` en lugar de `setreuid(0,0)` (syscall 105). El shell sigue aterrizando — y sigue cayendo a `uid=1000`. Es el experimento de una sola línea más instructivo de todo el repositorio. --- ## 11. Seguridad y limpieza - Solo loopback, por defecto y por diseño; `-L` enlaza más lejos, y solo una VM apta para tirar debería siquiera considerarlo. - Esto es un laboratorio de shell root. No lo ejecutes en una máquina que importe, y no apuntes `foosc -h` a algo que no poseas. - Rito de limpieza: `make stop` y luego `make unsetuid`, y si quieres el árbol impecable otra vez: `sudo make clean`. ```console $ make stop $ make unsetuid ```