Este capítulo invierte la perspectiva: en lugar de explotar, defender. Erickson cubre todo lo que el sistema hace para evitar que un overflow termine en RCE, y luego enseña a saltarse cada protección. La lección clave: las mitigaciones no eliminan los bugs, suben el coste.
- 0x610 — Detección
- 0x620 — Daemons del sistema
- 0x630 — Herramientas defensivas
- 0x640 — Logs
- 0x650 — Lo obvio que se pasa por alto
- 0x660 — Camuflaje avanzado
- 0x670 — La infraestructura completa
- 0x680 — Smuggling y restricciones de buffer
- 0x6a0 — Stack canary
- 0x6b0 — Stack no ejecutable (NX)
- 0x6c0 — ASLR y PIE
Las defensas se agrupan en dos familias:
| Tipo | Qué hace | Ejemplos |
|---|---|---|
| Detectivo | Avisa cuando algo va mal | logs, IDS, EDR, file integrity monitors |
| Preventivo | Impide que algo vaya mal | NX, ASLR, canary, capabilities, seccomp |
Un atacante competente trata las dos por separado: rompo la prevención y evado la detección. El libro se centra en la prevención técnica; aquí añado contexto moderno (EDR, telemetría, SIEM).
Un daemon es un proceso de fondo que escucha y reacciona a eventos. Históricamente son el punto blando de las máquinas Linux: corren como root, escuchan en red, y cualquier overflow en uno de ellos es un RCE remoto sin autenticación.
Daemons clásicos: sshd, httpd, bind, cups, postfix. Modernos: systemd-*, polkit, dbus-broker, avahi.
# qué escucha en mi máquina
ss -tlnp # TCP listening con PID
ss -ulnp # UDP listening
systemctl list-units --type=service --state=running
# desactivar lo que no uso
sudo systemctl disable --now cups avahi-daemonCada daemon debería correr con el menor privilegio posible:
- User dedicado (no root) si la función no necesita privilegios.
- Capabilities en lugar de SUID.
cap_net_bind_serviceen lugar de root para escuchar en puerto <1024. - systemd sandboxing:
PrivateTmp=,ProtectSystem=full,NoNewPrivileges=,CapabilityBoundingSet=,SystemCallFilter=(seccomp). - chroot/pivot_root o namespaces (lo que hacen Docker / Podman / nsjail).
- seccomp filtrando syscalls que el daemon nunca usa.
Ver /etc/systemd/system/ y systemd-analyze security <unit> para ver el score de aislamiento.
| Herramienta | Para qué | Detecta |
|---|---|---|
tripwire / aide |
File integrity monitoring | cambios en /bin, /etc |
auditd |
Auditoría de syscalls | acceso a archivos, ejecución |
fail2ban |
Bloquea IPs por fallos repetidos | brute force en SSH/HTTP |
selinux / apparmor |
MAC (Mandatory Access Control) | un proceso compromise no puede salir de su perfil |
iptables / nftables |
Firewall | tráfico no esperado |
osquery |
SQL sobre estado del sistema | hunting reactivo |
falco / tetragon |
Detección runtime con eBPF | actividad anómala en kernel |
lynis |
Auditoría manual | configuración floja |
Para un Linux moderno bien defendido: AppArmor + auditd + fail2ban + nftables restrictivo + systemd hardening en cada daemon.
/var/log/ es el primer sitio que mira el defensor y el primero que limpia el atacante. Logs típicos:
| Archivo | Qué contiene |
|---|---|
/var/log/auth.log (Debian) o /var/log/secure (RHEL) |
Logins, sudo, SSH |
/var/log/syslog o /var/log/messages |
Mensajes generales del kernel y daemons |
/var/log/kern.log |
Kernel |
/var/log/btmp |
Logins fallidos (binario, ver con lastb) |
/var/log/wtmp |
Logins exitosos (binario, ver con last) |
/var/log/utmp |
Sesiones activas (who) |
/var/log/audit/audit.log |
auditd |
/var/log/journal/ |
systemd-journald (binario, ver con journalctl) |
- Logs lejos del host. Un atacante con root borra logs locales en segundos. Forwardear a un servidor externo (rsyslog, journald-remote, vector, fluentbit) en cuanto se generan.
- Hash chain o append-only. Logs en una particion
chattr +a, o mejor, en un sistema externo donde el host no tiene permisos de borrado. - Correlación. Un sudo solo no dice nada; un sudo + cambio en
/etc/passwd+ nueva sesión SSH desde IP nueva = chain de actividad sospechosa. Eso es el trabajo de un SIEM.
# borrar entradas wtmp/btmp del propio user
utmpdump /var/log/wtmp > /tmp/wtmp.txt
sed -i '/atacante/d' /tmp/wtmp.txt
utmpdump -r < /tmp/wtmp.txt > /var/log/wtmp
# anti-forensics: alterar timestamps con touch
touch -r /etc/hostname /tmp/payload # mismo mtime que /etc/hostnamePor eso el hardening estándar incluye logs externos, FIM y monitoreo de borrados.
Erickson dedica una sección a errores de configuración: la mayor parte de los compromises reales no son 0-days, son detalles humanos. Ejemplos modernos:
- Buckets S3 públicos con backup de la base de datos.
.envoconfig.phpaccesibles vía web (/.envreturns 200).- Tokens de API en commits de Git público (busca con
gitleaks,trufflehog). - SSH con autenticación por password permitida y root accesible.
- Default credentials (
admin:admin) en routers, paneles web, IPMI. - Servicios internos expuestos a Internet (Redis sin auth, MongoDB sin auth, Elasticsearch sin auth).
- DNS rebinding en panels de admin que asumen "solo localhost".
La regla: asume que el atacante vio la diapositiva con la contraseña por defecto antes que tú.
Para el atacante:
- Process hiding: rootkits LKM ocultan procesos al
/proc. Detectables conunhideochkrootkit. - Library injection:
LD_PRELOADpara sobrescribir funciones de libc en un proceso. - In-memory only payloads: no tocan disco. Detectables con EDR que vigila
mmap+exec. - Living off the land (LOLBAS / LOLBins): usar binarios firmados por la distro (
bash,python,awk,find -exec) en lugar de meter binarios propios. Más sigiloso, evita signature-based detection.
ptrace(PTRACE_ATTACH, pid) + PTRACE_POKETEXT + PTRACE_SETREGS permite inyectar código en otro proceso. /proc/<pid>/mem permite leer/escribir memoria de procesos del mismo UID.
Mitigaciones:
/proc/sys/kernel/yama/ptrace_scope = 1(default): solo el padre puede attach./proc/sys/kernel/yama/ptrace_scope = 2: solo con CAP_SYS_PTRACE./proc/sys/kernel/yama/ptrace_scope = 3: ptrace deshabilitado.
Defender un servidor solo no sirve si la red está abierta. La defensa moderna es defense in depth:
Internet
│
┌──┴───┐
│ WAF │ filtrado L7 (Cloudflare, ModSec)
└──┬───┘
│
┌──┴───┐
│ LB │ terminación TLS, rate limiting
└──┬───┘
│
┌──┴───┐ ───► segmentación VPC: solo el LB habla con app
│ APP │
└──┬───┘
│
┌──┴───┐ ───► DB en subnet privada, sin Internet egress
│ DB │
└──────┘
Cada capa falla cerrada: si una se compromete, las siguientes deberían contener el daño.
A veces el buffer es muy pequeño y no cabe shellcode tradicional. Soluciones:
El shellcode etapa 1 (pequeño) hace read(0, buf, n) para leer una etapa 2 mucho más grande del mismo socket o fd. Pwntools shellcraft.staged hace esto.
Si no controlas dónde aterrizas pero hay un payload tuyo en algún lado del proceso (env, otro buffer, heap chunk), puedes incluir un egghunter: un mini-shellcode que escanea memoria buscando un marker (EGG! EGG!) y salta al payload que viene después. ~30 bytes.
Si el filtro acepta solo [A-Za-z0-9], usar msfvenom -e x86/alpha_mixed. La idea: codificar bytes binarios usando solo instrucciones x86 cuyos opcodes caen en ese rango (push, pop, varios inc/dec).
strncpy, snprintf, _FORTIFY_SOURCE cortan tu payload. Soluciones:
- Sobrescribir solo el byte bajo de un puntero (partial overwrite) — solo modifica los últimos 8/16 bits, evita los nulls del medio.
- Encadenar varias escrituras pequeñas (típico en format string
%hhn). - Usar bugs lógicos del programa para introducir el payload por una ruta que no pasa por el filtro.
Compilado con -fstack-protector-strong (default en gcc moderno), antes de la dirección de retorno se guarda un valor random de 8 bytes (el canary). Antes del ret, el código comprueba que el canary sigue intacto. Si no, llama a __stack_chk_fail y el programa muere con *** stack smashing detected ***.
Cómo se ve en assembler:
function:
push rbp
mov rbp, rsp
sub rsp, 0x40
mov rax, qword ptr fs:[0x28] ; lee canary del TLS
mov qword ptr [rbp - 8], rax ; lo guarda justo antes del saved RBP
; ... código ...
mov rax, qword ptr [rbp - 8]
sub rax, qword ptr fs:[0x28] ; canary == el original?
je .ok
call __stack_chk_fail ; no → abort
.ok:
leave
ret- Leak. Una format string vulnerability lee el canary y lo reescribimos con el mismo valor.
- Brute-force. Solo en fork servers donde el canary se hereda y los crashes no rotan: bytewise (256·8 = 2048 intentos máx). En cualquier otro caso (proceso fresco), el canary cambia → no se puede.
- Bypass por estructura. Si el bug sobrescribe variables locales antes del canary y el programa hace algo malo con esas variables antes del
ret, no importa romper el canary. - Sobrescribir punteros a estructuras de datos (vtables, function pointers en heap, exception handler) — no pasa por la pila, no tropieza con el canary.
codigo/0x6b0/canary_test.c compila el mismo overflow con y sin canary y muestra la diferencia.
NX (No-eXecute), también llamado XD o DEP, marca páginas de memoria como no ejecutables. La pila y el heap son no-ejecutables por defecto.
Comprobar:
$ checksec --file=binario
RELRO STACK CANARY NX PIE ...
Full Canary found NX PIE ...Con NX, meter shellcode en la pila y saltar ahí da SIGSEGV en lugar de ejecutar.
La técnica que ganó NX. En vez de meter código nuevo, encadenamos gadgets (secuencias de pocas instrucciones que terminan en ret) que ya están en el binario o en libc. Cada gadget hace una micro-operación, y la cadena completa equivale a un programa.
Ejemplo: para llamar a system("/bin/sh") en x86-64 SysV:
gadget1: pop rdi ; ret ← carga rdi
data1 : addr de "/bin/sh" en libc
gadget2: ret ← alineamiento (movaps en system necesita rsp%16==0)
target : addr de system en libc
Cuando ret ejecuta, salta a gadget1, que hace pop rdi (toma data1 de la pila), luego ret (salta a gadget2), que hace ret (alineamiento), que salta a system. system ve rdi apuntando a /bin/sh.
Encontrar gadgets: ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6.
Si no te apetece escribir todo en gadgets, llama a mprotect(stack_addr, size, PROT_READ|PROT_WRITE|PROT_EXEC) con ROP, lo que vuelve a hacer la pila ejecutable. Luego salta a tu shellcode metido en la propia pila.
ASLR (Address Space Layout Randomization) aleatoriza las bases de:
- Stack
- Heap
- Bibliotecas dinámicas (libc, ld.so)
- Mapas anónimos (
mmap) - Y, si está PIE, el propio binario.
Cada ejecución del proceso tiene direcciones distintas. Esto rompe los exploits que tenían direcciones hardcodeadas.
$ cat /proc/sys/kernel/randomize_va_space
2 # 2 = ASLR completo (stack+heap+libs+brk), default
# 1 = stack+libs aleatorios, brk no
# 0 = OFFPara reproducir el libro:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space # off
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space # onCompilar con -pie -fPIE hace que el propio binario también sea aleatorio. Sin PIE, _start, main, printf@plt están en direcciones fijas (0x400xxx típico) — ROP es trivial. Con PIE, también la base del binario se aleatoriza, así que necesitas un leak previo.
Verificar:
$ file binario | grep "PIE"
binario: ELF 64-bit LSB pie executable ...- Info leak. Un format string, un read excesivo, o cualquier vector que filtre direcciones del proceso. Una vez tienes un puntero a libc, puedes calcular la base y de ahí cualquier función.
- Brute force. Solo en 32-bit (16 bits aleatorios → 65536 intentos en un fork-server). En 64-bit hay 28+ bits aleatorios → impracticable.
- Partial overwrite. Sobrescribir solo los bytes bajos de un puntero. Si la página de la libc tiene 4 KB (12 bits), los 12 bits bajos están fijos; los siguientes son aleatorios pero pequeños. Saltar dentro de la misma página no necesita conocer la base.
- Heap spray + ret-into-heap. Ya casi no funciona (NX en heap), pero variantes con JIT spray sí en navegadores.
$ checksec --file=./binario
RELRO STACK CANARY NX PIE
Full RELRO Canary found NX enabled PIE enabledTabla de protecciones modernas en distros default:
| Distro | Stack canary | NX | ASLR | PIE | RELRO |
|---|---|---|---|---|---|
| Debian/Ubuntu (gcc default) | ✅ | ✅ | ✅ | ✅ | Full |
| Kali | ✅ | ✅ | ✅ | ✅ | Full |
| Alpine (musl) | ✅ | ✅ | ✅ | ✅ | Full |
Para reproducir el libro tienes que apagar todas explícitamente, como hace nuestro Makefile en las secciones inseguras.
Siguiente: 0x700 — Cryptology.