
LID — Linux Integrity Drift: Bypassing AppArmor via eBPF pathname rewriting. Pre-LSM syscall argument manipulation with zero audit footprint. "Linux is Dying"
```
██╗ ██╗██████╗
██║ ██║██╔══██╗
██║ ██║██║ ██║
██║ ██║██║ ██║
███████╗██║██████╔╝
╚══════╝╚═╝╚═════╝
```
— "O Linux está Morrendo" —
Descoberta sistemática de caminhos no kernel que ignoram as garantias de segurança do LSM
O portão nunca foi arrombado. Foi contornado.
O framework Linux Security Module tem uma garantia central que se mantém há mais de 20 anos:
Módulos de segurança só podem adicionar restrições. Eles nunca podem removê-las.
Esta garantia está correta. LID não a quebra.
LID encontra caminhos de código do kernel que ignoram completamente os hooks do LSM — subsistemas que realizam operações sensíveis à segurança sem consultar o framework LSM. A verificação de segurança está correta. O problema é que o kernel nunca pergunta.
Cada descoberta tem duas dimensões distintas que não devem ser confundidas:
O ponto cego arquitetônico. A pergunta não é "um atacante pode explorar isso?", mas sim:
Se uma operação sensível à segurança ocorre e a camada de aplicação nunca a avalia, você tem uma lacuna de visibilidade — independentemente de um atacante poder abusar dela na prática hoje. Isso importa para conformidade, perícia forense e pressupostos de defesa em profundidade.
A questão real de explorabilidade:
Essas duas coisas são diferentes. Uma descoberta pode ser uma lacuna de visibilidade crítica (seu monitoramento está cego) sem ser um escalonamento de privilégio prático (atacante já precisa de root). Por outro lado, uma descoberta pode ser um caminho direto de escalonamento com pré-requisitos mínimos.
Cada descoberta tem requisitos específicos de kernel/config/ privilégio. Se seu ambiente não corresponder, a descoberta não será reproduzida.
Cada descoberta segue o mesmo padrão:``` ┌─────────────────────────────────────────────────────────────┐ │ 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. │ └─────────────────────────────────────────────────────────────┘
Dois subsistemas do kernel. Premissas de confiança incompatíveis. Uma brecha.
<br>
---
## LID-001: Reescrita de Caminhos eBPF
**A descoberta principal.** Uma kprobe BPF em `do_sys_openat2` reescreve o nome do arquivo na memória do usuário antes que o kernel o copie. AppArmor verifica o caminho reescrito, concede acesso. Zero rastro de auditoria.```
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
### Saída de Demonstração```
╔══════════════════════════════════════════════════════════╗
║ 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 com IORING_MSG_SEND_FD transfere descritores de arquivo entre anéis io_uring sem chamar security_file_receive(). Todos os outros mecanismos de transferência de fd o chamam:```
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() ✓
**Localização do bug:** `io_uring/msg_ring.c`, `io_msg_install_complete()` — chama `__io_fixed_fd_install()` diretamente, ignorando o hook do LSM.
**Verificado com ftrace:** `security_file_receive` é acionado para SCM_RIGHTS, mas não para MSG_RING.
**Afetado:** Linux 5.18+ até v7.1-rc3 (não corrigido em 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: Nova API de Montagem Ignora AppArmor
A nova API de montagem (`fsopen` + `fsconfig` + `fsmount` + `move_mount`) **nunca chama `security_sb_mount()`** — o único hook de montagem que o 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 ✓
O AppArmor registra 4 hooks de montagem. O SELinux registra 14. A nova API de montagem usa hooks que apenas o SELinux implementa.
Lacunas adicionais encontradas:
open_tree(OPEN_TREE_CLONE): Zero hooks de segurança — ignora a política de bind-mountmount_setattr(): Nenhum hook LSM existe no kernelmove_mount() with detached mounts: O AppArmor vê caminho de origem NULLDetalhes: findings/lid-003-mount-api/
O subsistema de token BPF (Linux 6.9+) delega capacidades BPF a processos não privilegiados em namespaces de usuário. O kernel define 9 hooks LSM BPF. O SELinux implementa todos os 9 com aplicação real avc_has_perm(). O AppArmor 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
No Ubuntu/Debian, um contêiner com delegação bpffs pode:
- Criar tokens BPF → AppArmor não vê nada
- Usar tokens para carregar programas de rastreamento → AppArmor não vê nada
- Delegar CAP_PERFMON → desativar mitigações Spectre → AppArmor não vê nada
**Detalhes:** [`findings/lid-004-bpf-token/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-004-bpf-token/)
<br>
---
## LID-005: Bypass de Egresso tc AF_XDP do Contêiner Docker Padrão
O caminho de transmissão em modo cópia do AF_XDP (`xsk_generic_xmit` → `__dev_direct_xmit`) **ignora os classificadores de egresso tc** na interface do contêiner. Verificado com tc u32 DROP-ALL — AF_PACKET bloqueado, AF_XDP passa.
**No entanto:** Testado com Cilium v1.19 (cluster kind, NetworkPolicy de egresso negar-tudo) — **Cilium NÃO foi contornado.** O Cilium aplica a política no ingresso do par veth do lado do nó (`cil_from_container` via tcx/ingress) + tem verificação de IP de origem. Pacotes AF_XDP foram descartados como "IP de origem inválido" em `bpf_lxc.c:1603`. O impacto é limitado a ambientes que dependem exclusivamente de classificadores de egresso tc para política de rede (Docker simples + regras tc, sem 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 dinamicamente no kernel 6.8.0 com contêineres padrão do 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)
Pacotes carregam cabeçalhos Ethernet controlados pelo atacante — MAC falsificado, IP falsificado — e alcançam a ponte Docker apesar da política DROP ativa.
**PoC + detalhes:** [`findings/lid-005-afxdp-tc-bypass/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-005-afxdp-tc-bypass/)
<br>
---
## Arquitetura: Por que o BPF LSM não pode corrigir isso```
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>
## Estrutura do Projeto```
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 orientação detalhada de mitigação, veja docs/RESEARCH.md.
Veja a Matriz de Reprodutibilidade para detalhes completos por descoberta. Resumo:
|
Azizcan Daştan
|
Esta ferramenta é publicada para testes de segurança autorizados, pesquisa e fins educacionais apenas. Não use contra sistemas que você não possui ou para os quais não possui permissão explícita por escrito para testar.
LID — porque a integridade nunca foi bloqueada.
| Descoberta | Lacuna de Visibilidade | Escalonamento Prático |
|---|
| LID-001 | Crítica — AppArmor não vê nada, log de auditoria vazio, zero vestígio forense | Limitado — requer root ou CAP_BPF+CAP_PERFMON (já privilegiado). Não é um escalonamento de privilégio. Impacto: evasão de política + cegueira de auditoria. |
| LID-002 | Alta — security_file_receive() nunca dispara, transferência de fd invisível para todos os LSMs | Alta — funciona a partir de userspace não privilegiado via io_uring. Cruza o limite de aplicação do LSM sem qualquer privilégio. |
| LID-003 | Alta — security_sb_mount() ignorado, política de montagem do AppArmor é código morto | Média — requer acesso ao namespace de montagem (CAP_SYS_ADMIN em user ns). Disponível em muitas configurações de contêiner. |
| LID-004 | Crítica — AppArmor não vê nada, zero hooks BPF (0/9), nenhum rastro de auditoria para qualquer operação de token BPF | Média — requer delegação de bpffs pelo host + CAP_BPF em user namespace. Disponível em runtimes de contêiner com delegação BPF (LXD/Incus). |
| LID-005 | Média — classificadores tc de egress em interface de contêiner nunca avaliam tráfego AF_XDP | Limitado — ignora tc egress na eth0 do contêiner (verificado). Cilium NÃO é ignorado — aplica no ingresso veth do lado do nó + verificação de IP de origem (testado). Impacto limitado a configurações Docker simples com filtragem egress apenas tc. |
| Condição | Obrigatório | Observações |
|---|
| Versão do kernel | 5.x+ | Testado em 5.15, 6.1, 6.6, 6.8 |
CONFIG_BPF_SYSCALL | =y | Padrão em todas as principais distros |
CONFIG_BPF_KPROBE_OVERRIDE | =y | Ubuntu/Debian habilitam, RHEL não |
CONFIG_SECURITY_APPARMOR | =y | LSM alvo deve ser AppArmor |
| Perfil AppArmor | Em enforcing, regra de negação no caminho alvo | Funciona com qualquer regra de negação baseada em caminho |
| Privilégios | root ou CAP_BPF + CAP_PERFMON | Não pode ser executado sem privilégio |
kernel.lockdown | none ou integrity | Modo confidentiality bloqueia anexação de kprobe |
kernel.unprivileged_bpf_disabled | Irrelevante | Requer CAP_BPF independentemente |
fs.protected_hardlinks | 0 para links entre usuários | 1 (padrão) ainda permite hard links do mesmo usuário |
| SELinux em vez de AppArmor | Não funciona | SELinux é baseado em inode, não em caminho |
| Condição | Obrigatório | Observações |
|---|
| Versão do kernel | 6.0+ | IORING_MSG_SEND_FD adicionado em 6.0 |
CONFIG_IO_URING | =y | Padrão em todas as principais distros |
| Privilégios | Nenhum | Funciona a partir de userspace não privilegiado |
sysctl io_uring_disabled | 0 (padrão) | 2 bloqueia não privilegiado, 1 bloqueia todos |
| LSM alvo | Qualquer (SELinux, AppArmor, Smack) | security_file_receive() é um hook LSM genérico |
kernel.lockdown | Irrelevante | Nenhum BPF envolvido |
| Condição | Obrigatório | Observações |
|---|
| Versão do kernel | 6.9+ | Token BPF introduzido em 6.9 |
CONFIG_BPF_SYSCALL | =y | Padrão em todas as principais distros |
CONFIG_SECURITY_APPARMOR | =y | Padrão Ubuntu/Debian |
| bpffs com delegação | Sim | Host deve montar com opções delegate_* |
| Privilégios | CAP_BPF em user namespace | Trivialmente disponível para root de userns |
| SELinux em vez de AppArmor | Não afetado | SELinux implementa todos os 9 hooks BPF |
| Condição | Obrigatório | Observações |
|---|
| Versão do kernel | 5.2+ | fsopen/fsmount introduzidos em 5.2 |
CONFIG_SECURITY_APPARMOR | =y | Apenas AppArmor é afetado |
| Privilégios | CAP_SYS_ADMIN em user namespace | Disponível com unshare -m |
| SELinux em vez de AppArmor | Não funciona | SELinux implementa security_sb_kern_mount() |
| Runtime de contêiner | Depende do filtro seccomp | Seccomp padrão do Docker bloqueia fsopen — Podman/LXC podem não bloquear |
| Condição | Obrigatório | Observações |
|---|
| Versão do kernel | 4.18+ | AF_XDP introduzido em 4.18 |
CONFIG_XDP_SOCKETS | =y | Padrão em todas as principais distros |
| Privilégios | Apenas CAP_NET_RAW | Padrão em Docker, pods Kubernetes |
| Runtime de contêiner | Docker, K8s, LXC | Conjunto de capacidades padrão |
| Política de rede baseada em tc | Sim | Cilium eBPF, Calico, tc u32/flower |
| Ambiente | LID-001 | LID-002 | LID-003 | LID-004 | LID-005 |
|---|
| Ubuntu 22.04+ (AppArmor, padrão) | Funciona | Funciona | Funciona | Funciona (6.9+) | Funciona |
| Debian 12+ (AppArmor) | Funciona | Funciona | Funciona | Funciona (6.9+) | Funciona |
| RHEL/Fedora (SELinux) | Não | Funciona | Não | Não | Funciona |
lockdown=confidentiality | Não | Funciona | Funciona | Parcialmente | Funciona |
| Usuário não privilegiado | Não | Funciona | Depende de user ns | Depende de delegação bpffs | Não |
| Contêiner (sem CAP_BPF) | Não | Depende de io_uring | Depende de seccomp | Não | Funciona |
| Contêiner (CAP_NET_RAW removido) | Não | Depende | Depende | Não | Não |
| ID | Vetor | Alvo | O que Acontece |
|---|
| LID-001 | Reescrita de caminho via eBPF kprobe | AppArmor | kprobe reescreve o nome do arquivo antes de copy_from_user → AppArmor verifica caminho errado |
| LID-002 | io_uring MSG_RING SEND_FD | SELinux, AppArmor, Smack | Transferência de fd ignora security_file_receive() — toda outra transferência de fd chama |
| LID-003 | Nova API de montagem (fsopen/fsmount) | AppArmor | security_sb_mount() nunca é chamada — único hook de montagem do AppArmor ignorado |
| LID-004 | Delegação de token BPF | AppArmor, Smack | Zero hooks BPF — criação de token, uso, delegação de capacidade completamente invisíveis |
| LID-005 | AF_XDP __dev_direct_xmit | tc egress, Cilium, Calico | TX em modo de cópia do Docker padrão ignora classificadores tc — pacotes falsificados alcançam a bridge |
| Indicador | Visibilidade | Notas |
|---|
| Registro de auditoria do AppArmor | Nada | Negação nunca ocorre |
auditd / journald | Nada | Nenhum evento de segurança gerado |
dmesg | Aviso único | Mensagem genérica bpf_probe_write_user |
bpftool prog list | Visível | Mostra kprobe anexado (se verificado) |
| Link físico no disco | Detectável | find -samefile (lento, ruidoso) |
| Mitigação | Eficácia | Compensação |
|---|
kernel.lockdown=confidentiality | Bloqueia BPF completamente | Mata monitoramento legítimo |
Desabilitar bpf_probe_write_user | Previne reescrita de caminho LID-001 | Requer reconstrução do kernel |
fs.protected_hardlinks=1 | Limita criação de links físicos | Padrão em kernels modernos |
Monitorar bpftool prog list | Detecta probes anexadas | Requer polling ativo |
| Migrar para SELinux | Baseado em inode, derrota reescrita de caminho | Migração complexa |
| Restringir io_uring | Bloqueia LID-002 | Pode quebrar aplicações |
| Descoberta | Kernel Mínimo | Privilégios | Alvo |
|---|
| LID-001 | 5.x+ | root / CAP_BPF+CAP_PERFMON | Apenas AppArmor |
| LID-002 | 6.0+ | Nenhum | Qualquer (SELinux, AppArmor, Smack) |
| LID-003 | 5.2+ | CAP_SYS_ADMIN (user ns ok) | Apenas AppArmor |
| LID-004 | 6.9+ | CAP_BPF (user ns ok) | AppArmor, Smack |
| LID-005 | 4.18+ | CAP_NET_RAW (Docker default) | tc egress (Cilium, Calico, etc.) |