
LID — Linux Integrity Drift: Evadiendo AppArmor mediante la reescritura de rutas con eBPF. Manipulación de argumentos de syscalls previa a LSM con cero huella de auditoría. "Linux está muriendo"
```
██╗ ██╗██████╗
██║ ██║██╔══██╗
██║ ██║██║ ██║
██║ ██║██║ ██║
███████╗██║██████╔╝
╚══════╝╚═╝╚═════╝
```
— "Linux está muriendo" —
Descubrimiento sistemático de rutas de código del kernel que omiten las garantías de seguridad de LSM
La puerta nunca fue derribada. Simplemente la rodearon.
El framework de módulos de seguridad de Linux tiene una garantía central que se ha mantenido durante más de 20 años:
Los módulos de seguridad solo pueden añadir restricciones. Nunca pueden eliminarlas.
Esta garantía es correcta. LID no la rompe.
LID encuentra rutas de código del kernel que omiten por completo los hooks de LSM — subsistemas que realizan operaciones sensibles para la seguridad sin consultar el framework LSM. La comprobación de seguridad es correcta. El problema es que el kernel nunca pregunta.
Cada hallazgo tiene dos dimensiones distintas que no deben confundirse:
El punto ciego arquitectónico. La pregunta no es "¿puede un atacante explotar esto?", sino:
Si se produce una operación sensible para la seguridad y la capa de aplicación de políticas ni siquiera la evalúa, tienes una brecha de visibilidad — independientemente de si un atacante puede abusar de ella en la práctica hoy. Esto importa para cumplimiento, forense digital y supuestos de defensa en profundidad.
La pregunta de explotabilidad en el mundo real:
Estas dos cosas son diferentes. Un hallazgo puede ser una brecha de visibilidad crítica (tu monitoreo está ciego) sin ser una escalada de privilegios práctica (el atacante ya necesita root). A la inversa, un hallazgo puede ser una ruta de escalada directa con requisitos previos mínimos.
Cada hallazgo tiene requisitos específicos de kernel/config/privilegios. Si tu entorno no coincide, el hallazgo no se reproducirá.
Cada hallazgo sigue el mismo patrón:``` ┌─────────────────────────────────────────────────────────────┐ │ THE LID PATTERN │ │ │ │ Kernel Subsystem A LSM Framework │ │ (eBPF / io_uring / mount API) (AppArmor / SELinux) │ │ │ │ ┌───────────────────┐ │ │ │ Performs operation │──── should call ──── security_*() │ │ │ or manipulates │ but │ │ │ security input │ DOESN'T ██ │ │ └───────────────────┘ │ │ │ │ The LSM framework is correct. │ │ The kernel just doesn't always use it. │ └─────────────────────────────────────────────────────────────┘
Dos subsistemas del kernel. Supuestos de confianza incompatibles. Una brecha.
<br>
---
## LID-001: Reescritura de Nombres de Ruta eBPF
**El hallazgo principal.** Un kprobe de BPF en `do_sys_openat2` reescribe el nombre de archivo en la memoria de usuario antes de que el kernel lo copie. AppArmor comprueba la ruta reescrita, concede el acceso. Cero rastro de auditoría.```
process: open("/tmp/secret.txt")
│
▼
do_sys_openat2()
│
★ LID kprobe fires here
│ bpf_probe_write_user()
│ rewrites "/tmp/secret.txt" → "/tmp/.bypass_link"
│
▼
getname_flags() ← kernel copies the (rewritten) path
│
▼
security_file_open() ← LSM hooks check "/tmp/.bypass_link"
│ AppArmor: ALLOW ✓
▼
VFS opens inode ← same file content (hard link)
│
▼
return fd to process ← success, zero audit trace
sudo ./scripts/check_prerequisites.sh sudo ./scripts/setup_env.sh make sudo ./scripts/setup_demo.sh sudo ./scripts/run_demo.sh
### Salida de demostración```
╔══════════════════════════════════════════════════════════╗
║ Phase 1: AppArmor ENFORCING — access should be DENIED ║
╚══════════════════════════════════════════════════════════╝
$ /tmp/test_reader
[-] DENIED: open() failed: Permission denied (errno=13)
╔══════════════════════════════════════════════════════════╗
║ Phase 2: Loading LID — BPF kprobe pathname rewrite ║
╚══════════════════════════════════════════════════════════╝
[*] BPF kprobe attached to do_sys_openat2
╔══════════════════════════════════════════════════════════╗
║ Phase 3: With LID active — access should be GRANTED ║
╚══════════════════════════════════════════════════════════╝
$ /tmp/test_reader
[+] SUCCESS: Read 44 bytes: SECRET_DATA=this_is_protected_content_12345
╔══════════════════════════════════════════════════════════╗
║ Phase 4: Stealth check — audit log inspection ║
╚══════════════════════════════════════════════════════════╝
$ dmesg | grep apparmor | grep DENIED
(empty — no denial was ever generated)
IORING_OP_MSG_RING con IORING_MSG_SEND_FD transfiere descriptores de archivo entre anillos io_uring sin llamar a security_file_receive(). Todos los demás mecanismos de transferencia de fd lo llaman:```
MSG_RING SEND_FD: __io_fixed_fd_install() ← NO security_file_receive()
FIXED_FD_INSTALL: receive_fd() ← security_file_receive() ✓
SCM_RIGHTS: receive_fd() ← security_file_receive() ✓
binder: security_binder_transfer_file() ✓
**Ubicación del bug:** `io_uring/msg_ring.c`, `io_msg_install_complete()` — llama directamente a `__io_fixed_fd_install()`, omitiendo el hook de LSM.
**Verificado con ftrace:** `security_file_receive` se dispara para SCM_RIGHTS pero no para MSG_RING.
**Afectado:** Linux 5.18+ hasta v7.1-rc3 (sin corregir a fecha de 2026-05-17).
**PoC:** [`findings/lid-002-iouring-msgring/msg_ring_bypass.c`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-002-iouring-msgring/)
<br>
---
## LID-003: La nueva API de montaje omite AppArmor
La nueva API de montaje (`fsopen` + `fsconfig` + `fsmount` + `move_mount`) **nunca llama a `security_sb_mount()`** — el único hook de montaje que AppArmor implementa.```
OLD API: mount("proc", "/mnt", "proc", 0, NULL)
→ security_sb_mount() → AppArmor: DENY ✗
NEW API: fsopen("proc") → fsconfig(CMD_CREATE) → fsmount() → move_mount()
→ security_sb_kern_mount() → AppArmor: (not implemented) → ALLOW ✓
AppArmor registra 4 ganchos de mount. SELinux registra 14. La nueva API de mount utiliza ganchos que solo SELinux implementa.
Brechas adicionales encontradas:
open_tree(OPEN_TREE_CLONE): Cero ganchos de seguridad — evade la política de bind-mountmount_setattr(): No existe ningún gancho LSM en el kernel en absolutomove_mount() con montajes desconectados: AppArmor ve una ruta de origen NULLDetalles: findings/lid-003-mount-api/
El subsistema de token BPF (Linux 6.9+) delega capacidades BPF a procesos sin privilegios en namespaces de usuario. El kernel define 9 ganchos LSM de BPF. SELinux implementa los 9 con una aplicación real de avc_has_perm(). AppArmor implementa cero.```
Kernel defines 9 BPF LSM hooks:
┌─────────────────────────────────────────────────────────────────┐
│ bpf, bpf_map, bpf_prog, bpf_map_create, bpf_prog_load, │
│ bpf_token_create, bpf_token_free, bpf_token_cmd, │
│ bpf_token_capable │
└─────────────────────────────────────────────────────────────────┘
SELinux: ████████████████████████████ 9/9 implemented AppArmor: 0/9 implemented Smack: 0/9 implemented
On Ubuntu/Debian, un contenedor con delegación de bpffs puede:
- Crear tokens BPF → AppArmor no ve nada
- Usar tokens para cargar programas de tracing → AppArmor no ve nada
- Delegar CAP_PERFMON → deshabilitar mitigaciones de Spectre → AppArmor no ve nada
**Detalles:** [`findings/lid-004-bpf-token/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-004-bpf-token/)
<br>
---
## LID-005: Bypass de egress tc de AF_XDP desde el contenedor Docker predeterminado
La ruta de transmisión en modo copia de AF_XDP (`xsk_generic_xmit` → `__dev_direct_xmit`) **omite los clasificadores de egress tc** en la interfaz del contenedor. Verificado con tc u32 DROP-ALL — AF_PACKET bloqueado, AF_XDP pasa.
**Sin embargo:** Probado con Cilium v1.19 (kind cluster, NetworkPolicy de egress deny-all) — **Cilium NO fue omitido.** Cilium aplica enforcement en el ingress del peer veth del lado del nodo (`cil_from_container` vía tcx/ingress) + tiene verificación de IP de origen. Los paquetes AF_XDP se descartaron como "Invalid source ip" en `bpf_lxc.c:1603`. El impacto se limita a entornos que dependen únicamente de clasificadores de egress tc para la política de red (Docker simple + reglas tc, sin CNI).```
Normal TX path:
socket → dev_queue_xmit() → sch_handle_egress() → tc classifiers → driver
↑
ENFORCED (Cilium eBPF, Calico, tc u32)
AF_XDP copy-mode TX:
xsk_sendmsg() → xsk_generic_xmit() → __dev_direct_xmit() → driver
↑
SKIPPED (tc never runs)
Verificado dinámicamente en el kernel 6.8.0 con los contenedores predeterminados de Docker:``` tc egress DROP-ALL on container eth0:
AF_PACKET sendto → ENOBUFS (BLOCKED by tc) AF_XDP sendto → 0 (BYPASS — 3 spoofed packets reached docker0 bridge)
Packets carry attacker-controlled Ethernet headers — spoofed MAC, spoofed IP — and reach the Docker bridge despite active DROP policy.
**PoC + detalles:** [`findings/lid-005-afxdp-tc-bypass/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-005-afxdp-tc-bypass/)
<br>
---
## Arquitectura: Por qué BPF LSM no puede arreglar esto```
LSM Hook Chain: call_int_hook(file_open, ...)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Lockdown │──>│ Capability │──>│ AppArmor │──>│ BPF LSM │
│ return 0 │ │ return 0 │ │ return -13 │ │ (never │
│ (allow) │ │ (allow) │ │ (DENY) ██ │ │ reached) │
└─────────────┘ └─────────────┘ └──────┬───────┘ └─────────────┘
│
loop breaks
RC = -EACCES
★ The LSM framework is correct. No module can undo another's denial.
★ LID operates OUTSIDE the framework — before hooks run, or where hooks don't exist.
┌───────────────────────────────────────┐
│ THE ATTACK TIMELINE │
│ │
Syscall Entry │ ★ LID ─── manipulates input │ │ │ (security check sees wrong data) │ ▼ │ │ LSM Check │ LSM allows (or never runs) ✓ │ │ │ │ ▼ │ │ Syscall Exit │ ★ SunnyDayBPF ─── rewrites telemetry │ │ │ (monitoring sees wrong data) │ ▼ │ │ Audit/Log │ SIEM sees nothing ✓ │ │ │ │ Combined: ghost access │ └───────────────────────────────────────┘
<br>
## Estructura del Proyecto```
LID/
├── src/
│ ├── bpf/
│ │ └── lid.bpf.c # BPF kprobe — pathname rewriter (LID-001)
│ └── loader/
│ └── lid_loader.c # Userspace loader + event monitor
├── findings/
│ ├── lid-001-ebpf-pathname/ # eBPF pathname rewriting bypass
│ ├── lid-002-iouring-msgring/ # io_uring MSG_RING missing LSM hook
│ │ ├── msg_ring_bypass.c # PoC with ftrace verification
│ │ └── ADVISORY.md # Technical advisory
│ ├── lid-003-mount-api/ # New mount API AppArmor bypass
│ ├── lid-004-bpf-token/ # BPF token AppArmor/Smack zero coverage
│ └── lid-005-afxdp-tc-bypass/ # AF_XDP tc egress bypass from container
├── tests/
│ └── test_reader.c # Victim binary for LID-001 demo
├── scripts/
│ ├── check_prerequisites.sh # Verify system requirements
│ ├── setup_env.sh # Install build dependencies
│ ├── setup_demo.sh # Create demo environment
│ ├── run_demo.sh # Run full LID-001 demonstration
│ └── teardown.sh # Clean up everything
├── docs/
│ └── RESEARCH.md # Full technical research paper
├── publish/ # Articles for Medium, dev.to, etc.
├── Makefile
├── LICENSE
├── SECURITY.md
└── README.md
Para obtener una guía de mitigación detallada, consulte docs/RESEARCH.md.
Consulte la Matriz de Reproducibilidad para obtener todos los detalles por hallazgo. Resumen:
|
Azizcan Daştan
|
Esta herramienta se publica únicamente para pruebas de seguridad autorizadas, investigación y fines educativos. No la utilice contra sistemas que no posea o para los cuales no tenga permiso explícito por escrito para probar.
LID — porque la integridad nunca estuvo bloqueada.
| Hallazgo | Brecha de visibilidad | Escalada práctica |
|---|
| LID-001 | Crítica — AppArmor no ve nada, log de auditoría vacío, cero rastro forense | Limitada — requiere root o CAP_BPF+CAP_PERFMON (ya privilegiado). No es una escalada de privilegios. Impacto: evasión de políticas + ceguera de auditoría. |
| LID-002 | Alta — security_file_receive() nunca se dispara, la transferencia de fd invisible para todos los LSM | Alta — funciona desde el espacio de usuario sin privilegios mediante io_uring. Cruza el límite de aplicación de LSM sin ningún privilegio. |
| LID-003 | Alta — security_sb_mount() evadido, la política de montaje de AppArmor es código muerto | Media — requiere acceso al espacio de nombres de montaje (CAP_SYS_ADMIN en user ns). Disponible en muchas configuraciones de contenedores. |
| LID-004 | Crítica — AppArmor no ve nada, cero hooks BPF (0/9), sin rastro de auditoría para ninguna operación de token BPF | Media — requiere delegación de bpffs por parte del host + CAP_BPF en el espacio de nombres de usuario. Disponible en runtimes de contenedores con delegación BPF (LXD/Incus). |
| LID-005 | Media — los clasificadores de egress tc en la interfaz del contenedor nunca evalúan el tráfico AF_XDP | Limitada — evita el egress tc en eth0 del contenedor (verificado). Cilium NO es evadido — aplica control en el ingress veth del lado del nodo + verificación de IP de origen (probado). Impacto limitado a configuraciones de Docker simple con filtrado de egress solo con tc. |
| Condición | Requerido | Notas |
|---|
| Versión del kernel | 5.x+ | Probado en 5.15, 6.1, 6.6, 6.8 |
CONFIG_BPF_SYSCALL | =y | Predeterminado en todas las distros principales |
CONFIG_BPF_KPROBE_OVERRIDE | =y | Ubuntu/Debian lo habilitan, RHEL no |
CONFIG_SECURITY_APPARMOR | =y | El LSM objetivo debe ser AppArmor |
| Perfil de AppArmor | Enforcing, regla de denegación en la ruta objetivo | Funciona con cualquier regla de denegación basada en rutas |
| Privilegios | root o CAP_BPF + CAP_PERFMON | No puede ejecutarse sin privilegios |
kernel.lockdown | none o integrity | El modo confidentiality impide adjuntar kprobes |
kernel.unprivileged_bpf_disabled | Irrelevante | Requiere CAP_BPF de todos modos |
fs.protected_hardlinks | 0 para enlaces entre usuarios | 1 (predeterminado) todavía permite enlaces duros del mismo usuario |
| SELinux en lugar de AppArmor | No funciona | SELinux se basa en inodos, no en nombres de ruta |
| Condición | Requerido | Notas |
|---|
| Versión del kernel | 6.0+ | IORING_MSG_SEND_FD añadido en 6.0 |
CONFIG_IO_URING | =y | Predeterminado en todas las distros principales |
| Privilegios | Ninguno | Funciona desde el espacio de usuario sin privilegios |
sysctl io_uring_disabled | 0 (predeterminado) | 2 bloquea a los no privilegiados, 1 bloquea a todos |
| LSM objetivo | Cualquiera (SELinux, AppArmor, Smack) | security_file_receive() es un hook LSM genérico |
kernel.lockdown | Irrelevante | No involucra BPF |
| Condición | Requerido | Notas |
|---|
| Versión del kernel | 6.9+ | El token BPF se introdujo en 6.9 |
CONFIG_BPF_SYSCALL | =y | Predeterminado en todas las distros principales |
CONFIG_SECURITY_APPARMOR | =y | Predeterminado en Ubuntu/Debian |
| bpffs con delegación | Sí | El host debe montar con opciones delegate_* |
| Privilegios | CAP_BPF en el espacio de nombres de usuario | Disponible trivialmente para root de userns |
| SELinux en lugar de AppArmor | No afectado | SELinux implementa los 9 hooks BPF |
| Condición | Requerido | Notas |
|---|
| Versión del kernel | 5.2+ | fsopen/fsmount introducidos en 5.2 |
CONFIG_SECURITY_APPARMOR | =y | Solo AppArmor se ve afectado |
| Privilegios | CAP_SYS_ADMIN en el espacio de nombres de usuario | Disponible con unshare -m |
| SELinux en lugar de AppArmor | No funciona | SELinux implementa security_sb_kern_mount() |
| Runtime de contenedores | Depende del filtro seccomp | El seccomp predeterminado de Docker bloquea fsopen — Podman/LXC puede que no |
| Condición | Requerido | Notas |
|---|
| Versión del kernel | 4.18+ | AF_XDP introducido en 4.18 |
CONFIG_XDP_SOCKETS | =y | Predeterminado en todas las distros principales |
| Privilegios | Solo CAP_NET_RAW | Predeterminado en Docker, pods de Kubernetes |
| Runtime de contenedores | Docker, K8s, LXC | Conjunto de capacidades predeterminado |
| Política de red basada en tc | Sí | Cilium eBPF, Calico, tc u32/flower |
| Entorno | LID-001 | LID-002 | LID-003 | LID-004 | LID-005 |
|---|
| Ubuntu 22.04+ (AppArmor, predeterminado) | Funciona | Funciona | Funciona | Funciona (6.9+) | Funciona |
| Debian 12+ (AppArmor) | Funciona | Funciona | Funciona | Funciona (6.9+) | Funciona |
| RHEL/Fedora (SELinux) | No | Funciona | No | No | Funciona |
lockdown=confidentiality | No | Funciona | Funciona | Parcialmente | Funciona |
| Usuario sin privilegios | No | Funciona | Depende del user ns | Depende de la delegación de bpffs | No |
| Contenedor (sin CAP_BPF) | No | Depende de io_uring | Depende de seccomp | No | Funciona |
| Contenedor (CAP_NET_RAW eliminado) | No | Depende | Depende | No | No |
| ID | Vector | Objetivo | Qué ocurre |
|---|
| LID-001 | Reescritura de pathname mediante eBPF kprobe | AppArmor | el kprobe reescribe el nombre de archivo antes de copy_from_user → AppArmor comprueba la ruta incorrecta |
| LID-002 | io_uring MSG_RING SEND_FD | SELinux, AppArmor, Smack | la transferencia de fd omite security_file_receive() — cualquier otra transferencia de fd lo llama |
| LID-003 | Nueva API de montaje (fsopen/fsmount) | AppArmor | security_sb_mount() nunca se llama — el único hook de montaje de AppArmor es evadido |
| LID-004 | Delegación de token BPF | AppArmor, Smack | Cero hooks BPF — creación, uso y delegación de capacidades del token completamente invisibles |
| LID-005 | AF_XDP __dev_direct_xmit | tc egress, Cilium, Calico | TX en modo copia desde Docker predeterminado evita los clasificadores tc — los paquetes suplantados llegan al bridge |
| Indicador | Visibilidad | Notas |
|---|
| Registro de auditoría de AppArmor | Nada | La denegación nunca ocurre |
auditd / journald | Nada | No se genera ningún evento de seguridad |
dmesg | Aviso único | Mensaje genérico de bpf_probe_write_user |
bpftool prog list | Visible | Muestra el kprobe adjunto (si se comprueba) |
| Enlace físico en disco | Detectable | find -samefile (lento, ruidoso) |
| Mitigación | Efectividad | Compromiso |
|---|
kernel.lockdown=confidentiality | Bloquea BPF por completo | Elimina la monitorización legítima |
Deshabilitar bpf_probe_write_user | Evita la reescritura de rutas de LID-001 | Requiere recompilar el kernel |
fs.protected_hardlinks=1 | Limita la creación de enlaces físicos | Activado por defecto en kernels modernos |
Monitorizar bpftool prog list | Detecta sondas adjuntas | Requiere sondeo activo |
| Migrar a SELinux | Basado en inodos, frustra la reescritura de rutas | Migración compleja |
| Restringir io_uring | Bloquea LID-002 | Puede romper aplicaciones |
| Hallazgo | Kernel mínimo | Privilegios | Objetivo |
|---|
| LID-001 | 5.x+ | root / CAP_BPF+CAP_PERFMON | Solo AppArmor |
| LID-002 | 6.0+ | Ninguno | Cualquiera (SELinux, AppArmor, Smack) |
| LID-003 | 5.2+ | CAP_SYS_ADMIN (user ns ok) | Solo AppArmor |
| LID-004 | 6.9+ | CAP_BPF (user ns ok) | AppArmor, Smack |
| LID-005 | 4.18+ | CAP_NET_RAW (Docker por defecto) | tc egress (Cilium, Calico, etc.) |