
LID — Linux Integrity Drift: bypass di AppArmor tramite riscrittura dei pathname via eBPF. Manipolazione degli argomenti delle syscall pre-LSM con impronta di audit pari a zero. "Linux sta morendo"
```
██╗ ██╗██████╗
██║ ██║██╔══██╗
██║ ██║██║ ██║
██║ ██║██║ ██║
███████╗██║██████╔╝
╚══════╝╚═╝╚═════╝
```
— "Linux is Dying" —
Scoperta sistematica dei percorsi del kernel che aggirano le garanzie di sicurezza LSM
Il cancello non è mai stato forzato. È stato semplicemente aggirato.
Il framework Linux Security Module ha una garanzia fondamentale che regge da 20+ anni:
I moduli di sicurezza possono solo aggiungere restrizioni. Non possono mai rimuoverle.
Questa garanzia è corretta. LID non la infrange.
LID trova percorsi del kernel che bypassano completamente gli hook LSM — sottosistemi che eseguono operazioni sensibili per la sicurezza senza consultare il framework LSM. Il controllo di sicurezza è corretto. Il problema è che il kernel non chiede mai.
Ogni scoperta ha due dimensioni distinte che non vanno confuse:
Il punto cieco architetturale. La domanda non è "un attaccante può sfruttare questo?", ma:
Se un'operazione sensibile per la sicurezza avviene e il livello di enforcement non la valuta nemmeno, hai un divario di visibilità — indipendentemente dal fatto che un attaccante possa abusarne praticamente oggi. Questo conta per conformità, forensics e assunzioni di difesa in profondità.
La domanda sulla sfruttabilità nel mondo reale:
Queste due cose sono diverse. Una scoperta può essere un divario di visibilità critico (il tuo monitoring è cieco) senza essere un'escalation di privilegi pratica (l'attaccante ha già root). Al contrario, una scoperta può essere un percorso di escalation diretto con prerequisiti minimi.
Ogni scoperta ha requisiti specifici di kernel/config/privilegi. Se il tuo ambiente non corrisponde, la scoperta non si riprodurrà.
Ogni scoperta segue lo stesso pattern:``` ┌─────────────────────────────────────────────────────────────┐ │ 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. │ └─────────────────────────────────────────────────────────────┘
Due sottosistemi del kernel. Presupposti di fiducia incompatibili. Una lacuna.
<br>
---
## LID-001: Riscrittura del pathname eBPF
**La scoperta principale.** Una kprobe BPF su `do_sys_openat2` riscrive il nome del file nella memoria utente prima che il kernel lo copi. AppArmor controlla il percorso riscritto e concede l'accesso. Zero tracce di audit.```
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
### Output demo```
╔══════════════════════════════════════════════════════════╗
║ 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 trasferisce descrittori di file tra anelli io_uring senza chiamare security_file_receive(). Ogni altro meccanismo di trasferimento di fd lo chiama:```
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() ✓
**Posizione del bug:** `io_uring/msg_ring.c`, `io_msg_install_complete()` — chiama direttamente `__io_fixed_fd_install()`, saltando l'hook LSM.
**Verificato con ftrace:** `security_file_receive` scatta per SCM_RIGHTS ma non per MSG_RING.
**Interessati:** Linux 5.18+ fino a v7.1-rc3 (non corretto al 2026-05-17).
**PoC:** [`findings/lid-002-iouring-msgring/msg_ring_bypass.c`](https://github.com/azqzazq1/lid/blob/main/findings/lid-002-iouring-msgring)
<br>
---
## LID-003: La nuova API mount bypassa AppArmor
La nuova API mount (`fsopen` + `fsconfig` + `fsmount` + `move_mount`) **non chiama mai `security_sb_mount()`** — l'unico hook mount implementato da AppArmor.```
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 hook di mount. SELinux ne registra 14. La nuova API di mount usa hook che solo SELinux implementa.
Ulteriori lacune trovate:
open_tree(OPEN_TREE_CLONE): Zero hook di sicurezza — bypassa la politica di bind-mountmount_setattr(): Non esiste alcun hook LSM nel kernelmove_mount() con mount staccati: AppArmor vede un percorso di origine NULLDettagli: findings/lid-003-mount-api/
Il sottosistema dei token BPF (Linux 6.9+) delega le capacità BPF a processi non privilegiati negli user namespace. Il kernel definisce 9 hook LSM per BPF. SELinux li implementa tutti e 9 con una reale applicazione di avc_has_perm(). AppArmor ne implementa zero.```
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
Su Ubuntu/Debian, un container con delega bpffs può:
- Creare token BPF → AppArmor non vede nulla
- Usare i token per caricare programmi di tracing → AppArmor non vede nulla
- Delegare CAP_PERFMON → disabilitare le mitigazioni Spectre → AppArmor non vede nulla
**Dettagli:** [`findings/lid-004-bpf-token/`](https://github.com/azqzazq1/lid/blob/main/findings/lid-004-bpf-token)
<br>
---
## LID-005: Bypass tc Egress AF_XDP dal Container Docker Predefinito
Il percorso di trasmissione in modalità copia di AF_XDP (`xsk_generic_xmit` → `__dev_direct_xmit`) **bypassa i classificatori di egress tc** sull'interfaccia del container. Verificato con tc u32 DROP-ALL — AF_PACKET bloccato, AF_XDP passa.
**Tuttavia:** Testato con Cilium v1.19 (cluster kind, NetworkPolicy di egress deny-all) — **Cilium NON è stato bypassato.** Cilium applica il controllo sul peer veth lato nodo in ingresso (`cil_from_container` tramite tcx/ingress) + verifica dell'IP sorgente. I pacchetti AF_XDP sono stati scartati come "Invalid source ip" in `bpf_lxc.c:1603`. L'impatto è limitato ad ambienti che si affidano esclusivamente ai classificatori di egress tc per la network policy (Docker semplice + regole tc, senza 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)
Verificato dinamicamente sul kernel 6.8.0 con i container predefiniti di 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)
I pacchetti trasportano header Ethernet controllati dall'attaccante — MAC spoofato, IP spoofato — e raggiungono il bridge Docker nonostante una policy DROP attiva.
**PoC + dettagli:** [`findings/lid-005-afxdp-tc-bypass/`](https://github.com/azqzazq1/lid/blob/main/findings/lid-005-afxdp-tc-bypass)
<br>
---
## Architettura: perché BPF LSM non può risolvere questo problema```
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>
## Struttura del progetto```
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
Per una guida dettagliata alle mitigazioni, vedere docs/RESEARCH.md.
Vedere la Matrice di riproducibilità per i dettagli completi di ogni riscontro. Riepilogo:
|
Azizcan Daştan
|
Questo strumento è pubblicato solo per test di sicurezza autorizzati, ricerca e scopi educativi. Non utilizzarlo su sistemi di cui non sei proprietario o per i quali non disponi di un'esplicita autorizzazione scritta al test.
LID — perché l'integrità non è mai stata bloccata.
| Finding | Divario di visibilità | Escalation pratica |
|---|
| LID-001 | Critico — AppArmor non vede nulla, log di audit vuoto, zero traccia forense | Limitato — richiede root o CAP_BPF+CAP_PERFMON (già privilegiato). Non è un'escalation di privilegi. Impatto: evasione delle policy + cecità dell'audit. |
| LID-002 | Alto — security_file_receive() non scatta mai, il trasferimento di fd è invisibile a tutti gli LSM | Alto — funziona da userspace non privilegiato tramite io_uring. Attraversa il confine di enforcement LSM senza alcun privilegio. |
| LID-003 | Alto — security_sb_mount() bypassato, la policy di mount di AppArmor è codice morto | Medio — richiede accesso al mount namespace (CAP_SYS_ADMIN nella user ns). Disponibile in molte configurazioni container. |
| LID-004 | Critico — AppArmor non vede nulla, zero hook BPF (0/9), nessuna traccia di audit per qualsiasi operazione con BPF token | Medio — richiede delega bpffs da parte dell'host + CAP_BPF nella user namespace. Disponibile nei runtime container con delega BPF (LXD/Incus). |
| LID-005 | Medio — i classificatori tc egress sull'interfaccia container non valutano mai il traffico AF_XDP | Limitato — bypassa tc egress su eth0 del container (verificato). Cilium NON bypassato — applica il controllo sull'ingresso veth lato nodo + verifica IP sorgente (testato). Impatto limitato a setup Docker puri con filtro egress solo tc. |
| Condizione | Richiesta | Note |
|---|
| Versione kernel | 5.x+ | Testato su 5.15, 6.1, 6.6, 6.8 |
CONFIG_BPF_SYSCALL | =y | Default su tutte le principali distro |
CONFIG_BPF_KPROBE_OVERRIDE | =y | Ubuntu/Debian lo abilitano, RHEL no |
CONFIG_SECURITY_APPARMOR | =y | LSM target deve essere AppArmor |
| Profilo AppArmor | Enforcing, regola di deny sul path target | Funziona con qualsiasi regola di deny basata su path |
| Privilegi | root o CAP_BPF + CAP_PERFMON | Non può essere eseguito senza privilegi |
kernel.lockdown | none o integrity | La modalità confidentiality blocca l'attach dei kprobe |
kernel.unprivileged_bpf_disabled | Irrilevante | Richiede comunque CAP_BPF |
fs.protected_hardlinks | 0 per link cross-user | 1 (default) consente comunque hard link stesso-utente |
| SELinux invece di AppArmor | Non funziona | SELinux è basato su inode, non su pathname |
| Condizione | Richiesta | Note |
|---|
| Versione kernel | 6.0+ | IORING_MSG_SEND_FD aggiunto in 6.0 |
CONFIG_IO_URING | =y | Default su tutte le principali distro |
| Privilegi | Nessuno | Funziona da userspace non privilegiato |
Sysctl io_uring_disabled | 0 (default) | 2 blocca i non privilegiati, 1 blocca tutti |
| LSM target | Qualsiasi (SELinux, AppArmor, Smack) | security_file_receive() è un hook LSM generico |
kernel.lockdown | Irrilevante | Nessun BPF coinvolto |
| Condizione | Richiesta | Note |
|---|
| Versione kernel | 6.9+ | BPF token introdotto in 6.9 |
CONFIG_BPF_SYSCALL | =y | Default su tutte le principali distro |
CONFIG_SECURITY_APPARMOR | =y | Default Ubuntu/Debian |
| bpffs con delega | Sì | L'host deve montare con opzioni delegate_* |
| Privilegi | CAP_BPF nella user namespace | Banalmente disponibile per root userns |
| SELinux invece di AppArmor | Non interessato | SELinux implementa tutti i 9 hook BPF |
| Condizione | Richiesta | Note |
|---|
| Versione kernel | 5.2+ | fsopen/fsmount introdotti in 5.2 |
CONFIG_SECURITY_APPARMOR | =y | Solo AppArmor è interessato |
| Privilegi | CAP_SYS_ADMIN nella user namespace | Disponibile con unshare -m |
| SELinux invece di AppArmor | Non funziona | SELinux implementa security_sb_kern_mount() |
| Runtime container | Dipende dal filtro seccomp | Il seccomp default di Docker blocca fsopen — Podman/LXC potrebbero non bloccarlo |
| Condizione | Richiesta | Note |
|---|
| Versione kernel | 4.18+ | AF_XDP introdotto in 4.18 |
CONFIG_XDP_SOCKETS | =y | Default su tutte le principali distro |
| Privilegi | Solo CAP_NET_RAW | Default in Docker, pod Kubernetes |
| Runtime container | Docker, K8s, LXC | Set di capability predefinito |
| Policy di rete basata su tc | Sì | Cilium eBPF, Calico, tc u32/flower |
| Ambiente | LID-001 | LID-002 | LID-003 | LID-004 | LID-005 |
|---|
| Ubuntu 22.04+ (AppArmor, default) | Funziona | Funziona | Funziona | Funziona (6.9+) | Funziona |
| Debian 12+ (AppArmor) | Funziona | Funziona | Funziona | Funziona (6.9+) | Funziona |
| RHEL/Fedora (SELinux) | No | Funziona | No | No | Funziona |
lockdown=confidentiality | No | Funziona | Funziona | Parzialmente | Funziona |
| Utente non privilegiato | No | Funziona | Dipende dalla user ns | Dipende dalla delega bpffs | No |
| Container (senza CAP_BPF) | No | Dipende da io_uring | Dipende da seccomp | No | Funziona |
| Container (CAP_NET_RAW rimosso) | No | Dipende | Dipende | No | No |
| ID | Vettore | Target | Cosa succede |
|---|
| LID-001 | Riscrittura pathname kprobe eBPF | AppArmor | Il kprobe riscrive il filename prima di copy_from_user → AppArmor controlla il path sbagliato |
| LID-002 | io_uring MSG_RING SEND_FD | SELinux, AppArmor, Smack | Il trasferimento di fd salta security_file_receive() — ogni altro trasferimento di fd lo chiama |
| LID-003 | Nuova API di mount (fsopen/fsmount) | AppArmor | security_sb_mount() non viene mai chiamato — l'unico hook di mount di AppArmor è bypassato |
| LID-004 | Delega BPF token | AppArmor, Smack | Zero hook BPF — creazione del token, utilizzo, delega delle capability completamente invisibili |
| LID-005 | AF_XDP __dev_direct_xmit | tc egress, Cilium, Calico | La TX in copy-mode da Docker default bypassa i classificatori tc — i pacchetti spoofati raggiungono il bridge |
| Indicatore | Visibilità | Note |
|---|
| Log di audit di AppArmor | Niente | La negazione non si verifica mai |
auditd / journald | Niente | Nessun evento di sicurezza generato |
dmesg | Avviso una tantum | Messaggio generico bpf_probe_write_user |
bpftool prog list | Visibile | Mostra la kprobe collegata (se controllato) |
| Hard link su disco | Rilevabile | find -samefile (lento, rumoroso) |
| Mitigazione | Efficacia | Compromesso |
|---|
kernel.lockdown=confidentiality | Blocca completamente BPF | Elimina il monitoraggio legittimo |
Disabilitare bpf_probe_write_user | Impedisce la riscrittura del percorso di LID-001 | Richiede la ricompilazione del kernel |
fs.protected_hardlinks=1 | Limita la creazione di hard link | Predefinito sui kernel moderni |
Monitorare bpftool prog list | Rileva le probe collegate | Richiede polling attivo |
| Migrare a SELinux | Basato su inode, vanifica la riscrittura del percorso | Migrazione complessa |
| Limitare io_uring | Blocca LID-002 | Potrebbe rompere le applicazioni |
| Riscontro | Kernel minimo | Privilegi | Target |
|---|
| LID-001 | 5.x+ | root / CAP_BPF+CAP_PERFMON | Solo AppArmor |
| LID-002 | 6.0+ | Nessuno | Qualsiasi (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 (predefinito di Docker) | tc egress (Cilium, Calico, ecc.) |