
LID — Linux Integrity Drift: Bypassing AppArmor via eBPF pathname rewriting. Pre-LSM syscall argument manipulation with zero audit footprint. "Linux is Dying"
```
██╗ ██╗██████╗
██║ ██║██╔══██╗
██║ ██║██║ ██║
██║ ██║██║ ██║
███████╗██║██████╔╝
╚══════╝╚═╝╚═════╝
```
— « Linux est en train de mourir » —
Découverte systématique de chemins de code du noyau qui contournent les garanties de sécurité des LSM
La porte n'a jamais été forcée. On l'a contournée.
Le framework des modules de sécurité Linux (LSM) repose sur une garantie centrale qui tient depuis plus de 20 ans :
Les modules de sécurité ne peuvent qu'ajouter des restrictions. Ils ne peuvent jamais les supprimer.
Cette garantie est correcte. LID ne la brise pas.
LID trouve des chemins de code du noyau qui contournent complètement les hooks LSM — des sous-systèmes effectuant des opérations sensibles pour la sécurité sans consulter le framework LSM. La vérification de sécurité est correcte. Le problème est que le noyau ne demande jamais.
Chaque découverte a deux dimensions distinctes qu'il ne faut pas confondre :
Le point aveugle architectural. La question n'est pas « un attaquant peut-il exploiter cela ? » mais :
Si une opération sensible pour la sécurité se produit et que la couche d'application ne l'évalue même pas, vous avez un écart de visibilité — indépendamment de la possibilité pratique pour un attaquant d'en abuser aujourd'hui. Cela compte pour la conformité, la criminalistique et les hypothèses de défense en profondeur.
La question pratique de l'exploitabilité :
Ces deux choses sont différentes. Une découverte peut être un écart de visibilité critique (votre surveillance est aveugle) sans être une escalade de privilèges pratique (l'attaquant a déjà besoin de root). Inversement, une découverte peut être un chemin d'escalade direct avec des prérequis minimaux.
Chaque découverte a des exigences spécifiques de noyau/config/privlèges. Si votre environnement ne correspond pas, la découverte ne se reproduira pas.
Chaque découverte suit le même schéma :``` ┌─────────────────────────────────────────────────────────────┐ │ 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. │ └─────────────────────────────────────────────────────────────┘
Deux sous-systèmes du noyau. Hypothèses de confiance incompatibles. Une faille.
<br>
---
## LID-001: Réécriture des chemins par eBPF
**La découverte phare.** Une kprobe BPF sur `do_sys_openat2` réécrit le nom de fichier dans la mémoire utilisateur avant que le noyau ne le copie. AppArmor vérifie le chemin réécrit et accorde l'accès. Aucune trace d'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
### Sortie de démonstration```
╔══════════════════════════════════════════════════════════╗
║ 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 avec IORING_MSG_SEND_FD transfère des descripteurs de fichier entre des rings io_uring sans appeler security_file_receive(). Tous les autres mécanismes de transfert de fd l'appellent :```
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() ✓
**Emplacement du bogue :** `io_uring/msg_ring.c`, `io_msg_install_complete()` — appelle directement `__io_fixed_fd_install()`, en contournant le hook LSM.
**Vérifié avec ftrace :** `security_file_receive` se déclenche pour SCM_RIGHTS mais pas pour MSG_RING.
**Affecté :** Linux 5.18+ jusqu'à v7.1-rc3 (non corrigé au 17/05/2026).
**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 nouvelle API de montage contourne AppArmor
La nouvelle API de montage (`fsopen` + `fsconfig` + `fsmount` + `move_mount`) **n'appelle jamais `security_sb_mount()`** — le seul hook de montage qu'AppArmor implémente.```
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 enregistre 4 hooks de montage. SELinux enregistre 14. La nouvelle API de montage utilise des hooks que seul SELinux implémente.
Autres lacunes identifiées :
open_tree(OPEN_TREE_CLONE) : Zéro hook de sécurité — contourne la politique de bind-mountmount_setattr() : Aucun hook LSM n'existe dans le noyaumove_mount() avec des montages détachés : AppArmor voit un chemin source NULLDétails : findings/lid-003-mount-api/
Le sous-système de token BPF (Linux 6.9+) délègue les capacités BPF à des processus non privilégiés dans des espaces de noms utilisateur. Le noyau définit 9 hooks LSM BPF. SELinux implémente les 9 avec une véritable application de avc_has_perm(). AppArmor n'en implémente aucun.```
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
Sur Ubuntu/Debian, un conteneur avec délégation bpffs peut :
- Créer des jetons BPF → AppArmor ne voit rien
- Utiliser des jetons pour charger des programmes de tracing → AppArmor ne voit rien
- Déléguer CAP_PERFMON → désactiver les atténuations Spectre → AppArmor ne voit rien
**Détails :** [`findings/lid-004-bpf-token/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-004-bpf-token/)
<br>
---
## LID-005 : Contournement de la sortie tc egress depuis un conteneur Docker par défaut via AF_XDP
Le chemin de transmission en mode copie d'AF_XDP (`xsk_generic_xmit` → `__dev_direct_xmit`) **contourne les classifieurs tc egress** sur l'interface du conteneur. Vérifié avec tc u32 DROP-ALL — AF_PACKET bloqué, AF_XDP passe.
**Cependant :** Testé avec Cilium v1.19 (cluster kind, NetworkPolicy egress deny-all) — **Cilium n'a PAS été contourné.** Cilium applique les règles sur le veth pair côté nœud en ingress (`cil_from_container` via tcx/ingress) + possède une vérification de l'adresse IP source. Les paquets AF_XDP ont été rejetés comme « Adresse source invalide » à `bpf_lxc.c:1603`. L'impact est limité aux environnements qui reposent uniquement sur des classifieurs tc egress pour la politique réseau (Docker simple + règles tc, pas de 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)
Vérifié dynamiquement sur le noyau 6.8.0 avec les conteneurs par défaut 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)
Les paquets transportent des en-têtes Ethernet contrôlées par l'attaquant — MAC usurpée, IP usurpée — et atteignent le pont Docker malgré une politique DROP active.
**Preuve de concept + détails :** [`findings/lid-005-afxdp-tc-bypass/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-005-afxdp-tc-bypass/)
<br>
---
## Architecture: Pourquoi BPF LSM ne peut pas corriger cela```
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>
## Structure du projet```
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
Pour des conseils d'atténuation détaillés, voir docs/RESEARCH.md.
Voir la Matrice de reproductibilité pour tous les détails par constatation. Résumé :
|
Azizcan Daştan
|
Cet outil est publié uniquement à des fins autorisées de test de sécurité, de recherche et d'éducation. Ne l'utilisez pas contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite explicite de test.
LID — parce que l'intégrité n'a jamais été verrouillée.
| Découverte | Écart de visibilité | Escalade pratique |
|---|
| LID-001 | Critique — AppArmor ne voit rien, journal d'audit vide, aucune trace forensique | Limitée — nécessite root ou CAP_BPF+CAP_PERFMON (déjà privilégié). Pas d'escalade de privilèges. Impact : contournement de politique + cécité d'audit. |
| LID-002 | Élevé — security_file_receive() jamais déclenché, transfert de fd invisible pour tous les LSM | Élevé — fonctionne depuis un espace utilisateur non privilégié via io_uring. Franchit la limite d'application LSM sans aucun privilège. |
| LID-003 | Élevé — security_sb_mount() contourné, la politique de montage AppArmor est du code mort | Moyen — nécessite un accès à l'espace de noms de montage (CAP_SYS_ADMIN dans user ns). Disponible dans de nombreuses configurations de conteneurs. |
| LID-004 | Critique — AppArmor ne voit rien, zéro hooks BPF (0/9), aucune trace d'audit pour toute opération de jeton BPF | Moyen — nécessite une délégation bpffs par l'hôte + CAP_BPF dans l'espace de noms utilisateur. Disponible dans les environnements d'exécution de conteneurs avec délégation BPF (LXD/Incus). |
| LID-005 | Moyen — les classificateurs tc egress sur l'interface du conteneur n'évaluent jamais le trafic AF_XDP | Limitée — contourne tc egress sur eth0 du conteneur (vérifié). Cilium PAS contourné — applique sur l'ingress veth côté nœud + vérification IP source (testé). Impact limité aux configurations Docker simples avec filtrage tc egress uniquement. |
| Condition | Requis | Notes |
|---|
| Version du noyau | 5.x+ | Testé sur 5.15, 6.1, 6.6, 6.8 |
CONFIG_BPF_SYSCALL | =y | Par défaut sur toutes les distributions majeures |
CONFIG_BPF_KPROBE_OVERRIDE | =y | Ubuntu/Debian l'activent, RHEL non |
CONFIG_SECURITY_APPARMOR | =y | Le LSM cible doit être AppArmor |
| Profil AppArmor | Enforcing, règle de refus sur le chemin cible | Fonctionne avec toute règle de refus basée sur le chemin |
| Privilèges | root ou CAP_BPF + CAP_PERFMON | Ne peut pas s'exécuter non privilégié |
kernel.lockdown | none ou integrity | Le mode confidentiality bloque l'attachement kprobe |
kernel.unprivileged_bpf_disabled | Sans importance | Nécessite CAP_BPF quoi qu'il arrive |
fs.protected_hardlinks | 0 pour les liens inter-utilisateurs | 1 (par défaut) autorise encore les liens physiques du même utilisateur |
| SELinux au lieu d'AppArmor | Ne fonctionne pas | SELinux est basé sur les inodes, pas sur les noms de chemin |
| Condition | Requis | Notes |
|---|
| Version du noyau | 6.0+ | IORING_MSG_SEND_FD ajouté dans 6.0 |
CONFIG_IO_URING | =y | Par défaut sur toutes les distributions majeures |
| Privilèges | Aucun | Fonctionne depuis un espace utilisateur non privilégié |
sysctl io_uring_disabled | 0 (par défaut) | 2 bloque les non privilégiés, 1 bloque tout |
| LSM cible | Tout (SELinux, AppArmor, Smack) | security_file_receive() est un hook LSM générique |
kernel.lockdown | Sans importance | Aucun BPF impliqué |
| Condition | Requis | Notes |
|---|
| Version du noyau | 6.9+ | Jeton BPF introduit dans 6.9 |
CONFIG_BPF_SYSCALL | =y | Par défaut sur toutes les distributions majeures |
CONFIG_SECURITY_APPARMOR | =y | Valeur par défaut Ubuntu/Debian |
| bpffs avec délégation | Oui | L'hôte doit monter avec les options delegate_* |
| Privilèges | CAP_BPF dans l'espace de noms utilisateur | Trivialement accessible au root d'userns |
| SELinux au lieu d'AppArmor | Non affecté | SELinux implémente les 9 hooks BPF |
| Condition | Requis | Notes |
|---|
| Version du noyau | 5.2+ | fsopen/fsmount introduits dans 5.2 |
CONFIG_SECURITY_APPARMOR | =y | Seul AppArmor est affecté |
| Privilèges | CAP_SYS_ADMIN dans l'espace de noms utilisateur | Disponible avec unshare -m |
| SELinux au lieu d'AppArmor | Ne fonctionne pas | SELinux implémente security_sb_kern_mount() |
| Environnement d'exécution de conteneur | Dépend du filtre seccomp | Le seccomp par défaut de Docker bloque fsopen — Podman/LXC peuvent ne pas le faire |
| Condition | Requis | Notes |
|---|
| Version du noyau | 4.18+ | AF_XDP introduit dans 4.18 |
CONFIG_XDP_SOCKETS | =y | Par défaut sur toutes les distributions majeures |
| Privilèges | CAP_NET_RAW uniquement | Par défaut dans Docker, pods Kubernetes |
| Environnement d'exécution de conteneur | Docker, K8s, LXC | Ensemble de capacités par défaut |
| Politique réseau basée sur tc | Oui | Cilium eBPF, Calico, tc u32/flower |
| Environnement | LID-001 | LID-002 | LID-003 | LID-004 | LID-005 |
|---|
| Ubuntu 22.04+ (AppArmor, par défaut) | Fonctionne | Fonctionne | Fonctionne | Fonctionne (6.9+) | Fonctionne |
| Debian 12+ (AppArmor) | Fonctionne | Fonctionne | Fonctionne | Fonctionne (6.9+) | Fonctionne |
| RHEL/Fedora (SELinux) | Non | Fonctionne | Non | Non | Fonctionne |
lockdown=confidentiality | Non | Fonctionne | Fonctionne | Partiellement | Fonctionne |
| Utilisateur non privilégié | Non | Fonctionne | Dépend de user ns | Dépend de la délégation bpffs | Non |
| Conteneur (sans CAP_BPF) | Non | Dépend de io_uring | Dépend de seccomp | Non | Fonctionne |
| Conteneur (CAP_NET_RAW retiré) | Non | Dépend | Dépend | Non | Non |
| ID | Vecteur | Cible | Ce qui se passe |
|---|
| LID-001 | Réécriture de chemin par kprobe eBPF | AppArmor | Le kprobe réécrit le nom de fichier avant copy_from_user → AppArmor vérifie le mauvais chemin |
| LID-002 | io_uring MSG_RING SEND_FD | SELinux, AppArmor, Smack | Le transfert de fd ignore security_file_receive() — tous les autres transferts de fd l'appellent |
| LID-003 | Nouvelle API de montage (fsopen/fsmount) | AppArmor | security_sb_mount() jamais appelé — le seul hook de montage d'AppArmor contourné |
| LID-004 | Délégation de jeton BPF | AppArmor, Smack | Zéro hooks BPF — création de jeton, utilisation, délégation de capacité complètement invisibles |
| LID-005 | AF_XDP __dev_direct_xmit | tc egress, Cilium, Calico | La TX en mode copie depuis Docker par défaut contourne les classificateurs tc — les paquets falsifiés atteignent le pont |
| Indicateur | Visibilité | Notes |
|---|
| Journal d'audit AppArmor | Rien | Aucun refus ne se produit |
auditd / journald | Rien | Aucun événement de sécurité généré |
dmesg | Avertissement unique | Message générique bpf_probe_write_user |
bpftool prog list | Visible | Affiche le kprobe attaché (si vérifié) |
| Lien physique sur le disque | Détectable | find -samefile (lent, bruyant) |
| Atténuation | Efficacité | Compromis |
|---|
kernel.lockdown=confidentiality | Bloque complètement BPF | Tue la surveillance légitime |
Désactiver bpf_probe_write_user | Empêche la réécriture de chemin LID-001 | Nécessite une reconstruction du noyau |
fs.protected_hardlinks=1 | Limite la création de liens physiques | Par défaut sur les noyaux modernes |
Surveiller bpftool prog list | Détecte les sondes attachées | Nécessite une interrogation active |
| Migrer vers SELinux | Basé sur les inodes, contrecarre la réécriture de chemin | Migration complexe |
| Restreindre io_uring | Bloque LID-002 | Peut casser des applications |
| Constatation | Noyau min | Privilèges | Cible |
|---|
| LID-001 | 5.x+ | root / CAP_BPF+CAP_PERFMON | AppArmor uniquement |
| LID-002 | 6.0+ | Aucun | Tous (SELinux, AppArmor, Smack) |
| LID-003 | 5.2+ | CAP_SYS_ADMIN (user ns ok) | AppArmor uniquement |
| LID-004 | 6.9+ | CAP_BPF (user ns ok) | AppArmor, Smack |
| LID-005 | 4.18+ | CAP_NET_RAW (par défaut Docker) | tc egress (Cilium, Calico, etc.) |