
LID — Linux Integrity Drift: обход AppArmor через переписывание путей в eBPF. Манипулирование аргументами системных вызовов до LSM с нулевым следом в аудите. «Linux is Dying»
```
██╗ ██╗██████╗
██║ ██║██╔══██╗
██║ ██║██║ ██║
██║ ██║██║ ██║
███████╗██║██████╔╝
╚══════╝╚═╝╚═════╝
```
— «Linux умирает» —
Систематическое обнаружение путей в коде ядра, обходящих гарантии безопасности LSM
Ворота никогда не взламывали. Их просто обошли.
У фреймворка Linux Security Module есть одна ключевая гарантия, которая держится уже 20+ лет:
Модули безопасности могут только добавлять ограничения. Они никогда не могут их снимать.
Эта гарантия верна. LID её не нарушает.
LID находит пути в коде ядра, которые полностью обходят хуки LSM — подсистемы, выполняющие чувствительные с точки зрения безопасности операции, не обращаясь к фреймворку LSM. Проверка безопасности корректна. Проблема в том, что ядро её просто не запрашивает.
Каждая находка имеет два различных аспекта, которые не следует смешивать:
Архитектурное слепое пятно. Вопрос не в том, «может ли атакующий это эксплуатировать?», а в следующем:
Если чувствительная с точки зрения безопасности операция выполняется, а уровень принудительного контроля её даже не оценивает, у вас пробел видимости — независимо от того, может ли атакующий практически злоупотребить этим сегодня. Это имеет значение для комплаенса, криминалистики и предположений об эшелонированной обороне.
Вопрос реальной эксплуатируемости:
Это две разные вещи. Находка может быть критическим пробелом видимости (ваш мониторинг слеп), не будучи практическим повышением привилегий (атакующему уже нужен root). И наоборот, находка может быть прямым путём эскалации с минимальными предпосылками.
У каждой находки есть конкретные требования к ядру/конфигурации/привилегиям. Если ваше окружение не совпадает, находка не воспроизведётся.
Каждая находка следует одной и той же закономерности:``` ┌─────────────────────────────────────────────────────────────┐ │ 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. │ └─────────────────────────────────────────────────────────────┘
Two kernel subsystems. Incompatible trust assumptions. One gap.
<br>
---
## LID-001: eBPF Pathname Rewriting
**The flagship finding.** A BPF kprobe on `do_sys_openat2` rewrites the filename in user memory before the kernel copies it. AppArmor checks the rewritten path, grants access. Zero audit trace.```
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
### Демо-вывод```
╔══════════════════════════════════════════════════════════╗
║ 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 с IORING_MSG_SEND_FD передаёт файловые дескрипторы между кольцами io_uring без вызова security_file_receive(). Любой другой механизм передачи fd вызывает его:```
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() ✓
**Bug location:** `io_uring/msg_ring.c`, `io_msg_install_complete()` — напрямую вызывает `__io_fixed_fd_install()`, пропуская хук LSM.
**Verified with ftrace:** `security_file_receive` срабатывает для SCM_RIGHTS, но не для MSG_RING.
**Affected:** Linux 5.18+ вплоть до v7.1-rc3 (не исправлено по состоянию на 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: Новый API монтирования обходит AppArmor
Новый API монтирования (`fsopen` + `fsconfig` + `fsmount` + `move_mount`) **никогда не вызывает `security_sb_mount()`** — единственный хук монтирования, который реализует 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 регистрирует 4 mount-хука. SELinux регистрирует 14. Новый mount API использует хуки, которые реализует только SELinux.
Обнаружены дополнительные пробелы:
open_tree(OPEN_TREE_CLONE): Ноль security-хуков — обход политики bind-mountmount_setattr(): В ядре вообще не существует LSM-хукаmove_mount() с отсоединёнными монтированиями: AppArmor видит исходный путь как NULLПодробности: findings/lid-003-mount-api/
Подсистема BPF token (Linux 6.9+) делегирует BPF-возможности непривилегированным процессам в пользовательских пространствах имён. Ядро определяет 9 BPF LSM-хуков. SELinux реализует все 9 с реальным контролем через avc_has_perm(). AppArmor не реализует ни одного.```
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
В Ubuntu/Debian контейнер с делегированием bpffs может:
- Создавать BPF-токены → AppArmor ничего не видит
- Использовать токены для загрузки программ трассировки → AppArmor ничего не видит
- Делегировать CAP_PERFMON → отключать механизмы защиты от Spectre → AppArmor ничего не видит
**Подробности:** [`findings/lid-004-bpf-token/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-004-bpf-token/)
<br>
---
## LID-005: Обход tc Egress в AF_XDP из стандартного Docker-контейнера
Путь передачи AF_XDP в режиме копирования (`xsk_generic_xmit` → `__dev_direct_xmit`) **обходит классификаторы tc egress** на интерфейсе контейнера. Проверено с помощью tc u32 DROP-ALL — AF_PACKET блокируется, AF_XDP проходит.
**Однако:** Протестировано с Cilium v1.19 (kind-кластер, deny-all egress NetworkPolicy) — **Cilium обойти не удалось.** Cilium применяет контроль на ingress veth-пира на стороне узла (`cil_from_container` через tcx/ingress) + проверку исходного IP-адреса. Пакеты AF_XDP отбрасывались как "Invalid source ip" в `bpf_lxc.c:1603`. Влияние ограничено средами, которые полагаются исключительно на классификаторы tc egress для сетевых политик (обычный Docker + правила tc, без 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)
Динамически проверено на ядре 6.8.0 с контейнерами 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)
Пакеты несут управляемые атакующим Ethernet-заголовки — поддельный MAC, поддельный IP — и достигают моста Docker, несмотря на активную политику DROP.
**PoC + подробности:** [`findings/lid-005-afxdp-tc-bypass/`](https://github.com/azqzazq1/lid/blob/HEAD/findings/lid-005-afxdp-tc-bypass/)
<br>
---
## Архитектура: почему BPF LSM не может это исправить```
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>
## Структура проекта```
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
Подробные рекомендации по мерам защиты см. в docs/RESEARCH.md.
Полные подробности по каждой находке см. в Матрице воспроизводимости. Сводка:
|
Azizcan Daştan
|
Этот инструмент опубликован только для авторизованного тестирования безопасности, исследований и образовательных целей. Не используйте его против систем, которыми вы не владеете или на тестирование которых у вас нет явного письменного разрешения.
LID — потому что целостность никогда не была заблокирована.
| Находка | Пробел видимости | Практическая эскалация |
|---|
| LID-001 | Критический — AppArmor ничего не видит, журнал аудита пуст, ноль криминалистических следов | Ограниченная — требует root или CAP_BPF+CAP_PERFMON (уже привилегирован). Не повышение привилегий. Воздействие: обход политики + слепота аудита. |
| LID-002 | Высокий — security_file_receive() никогда не срабатывает, передача fd невидима для всех LSM | Высокая — работает из непривилегированного пользовательского пространства через io_uring. Пересекает границу принудительного контроля LSM без каких-либо привилегий. |
| LID-003 | Высокий — security_sb_mount() обойдён, политика монтирования AppArmor — мёртвый код | Средняя — требует доступ к namespace монтирования (CAP_SYS_ADMIN в user ns). Доступен во многих конфигурациях контейнеров. |
| LID-004 | Критический — AppArmor ничего не видит, ноль BPF-хуков (0/9), нет следов аудита для любой операции с BPF-токеном | Средняя — требует делегирования bpffs хостом + CAP_BPF в user namespace. Доступен в контейнерных рантаймах с делегированием BPF (LXD/Incus). |
| LID-005 | Средний — классификаторы tc egress на интерфейсе контейнера никогда не оценивают трафик AF_XDP | Ограниченная — обходит tc egress на eth0 контейнера (подтверждено). Cilium НЕ обойдён — применяет контроль на входящем veth со стороны узла + проверку исходного IP (протестировано). Воздействие ограничено конфигурациями plain-Docker с фильтрацией только через tc egress. |
| Условие | Требуется | Примечания |
|---|
| Версия ядра | 5.x+ | Протестировано на 5.15, 6.1, 6.6, 6.8 |
CONFIG_BPF_SYSCALL | =y | По умолчанию во всех крупных дистрибутивах |
CONFIG_BPF_KPROBE_OVERRIDE | =y | Ubuntu/Debian включают, RHEL нет |
CONFIG_SECURITY_APPARMOR | =y | Целевой LSM должен быть AppArmor |
| Профиль AppArmor | Enforcing, правило deny на целевой путь | Работает с любым правилом deny на основе пути |
| Привилегии | root или CAP_BPF + CAP_PERFMON | Не может работать без привилегий |
kernel.lockdown | none или integrity | Режим confidentiality блокирует присоединение kprobe |
kernel.unprivileged_bpf_disabled | Не важно | В любом случае требуется CAP_BPF |
fs.protected_hardlinks | 0 для межпользовательских ссылок | 1 (по умолчанию) всё ещё разрешает жёсткие ссылки того же пользователя |
| SELinux вместо AppArmor | Не работает | SELinux основан на inode, а не на пути |
| Условие | Требуется | Примечания |
|---|
| Версия ядра | 6.0+ | IORING_MSG_SEND_FD добавлен в 6.0 |
CONFIG_IO_URING | =y | По умолчанию во всех крупных дистрибутивах |
| Привилегии | Нет | Работает из непривилегированного пользовательского пространства |
sysctl io_uring_disabled | 0 (по умолчанию) | 2 блокирует непривилегированных, 1 блокирует всех |
| Целевой LSM | Любой (SELinux, AppArmor, Smack) | security_file_receive() — универсальный хук LSM |
kernel.lockdown | Не важно | BPF не задействован |
| Условие | Требуется | Примечания |
|---|
| Версия ядра | 6.9+ | BPF-токен появился в 6.9 |
CONFIG_BPF_SYSCALL | =y | По умолчанию во всех крупных дистрибутивах |
CONFIG_SECURITY_APPARMOR | =y | По умолчанию в Ubuntu/Debian |
| bpffs с делегированием | Да | Хост должен монтировать с опциями delegate_* |
| Привилегии | CAP_BPF в user namespace | Тривиально доступен для root в userns |
| SELinux вместо AppArmor | Не затронут | SELinux реализует все 9 BPF-хуков |
| Условие | Требуется | Примечания |
|---|
| Версия ядра | 5.2+ | fsopen/fsmount появились в 5.2 |
CONFIG_SECURITY_APPARMOR | =y | Затронут только AppArmor |
| Привилегии | CAP_SYS_ADMIN в user namespace | Доступно с unshare -m |
| SELinux вместо AppArmor | Не работает | SELinux реализует security_sb_kern_mount() |
| Контейнерный рантайм | Зависит от seccomp-фильтра | Стандартный seccomp Docker блокирует fsopen — Podman/LXC могут не блокировать |
| Условие | Требуется | Примечания |
|---|
| Версия ядра | 4.18+ | AF_XDP появился в 4.18 |
CONFIG_XDP_SOCKETS | =y | По умолчанию во всех крупных дистрибутивах |
| Привилегии | Только CAP_NET_RAW | По умолчанию в Docker и подах Kubernetes |
| Контейнерный рантайм | Docker, K8s, LXC | Стандартный набор capabilities |
| Сетевая политика на основе tc | Да | Cilium eBPF, Calico, tc u32/flower |
| Окружение | LID-001 | LID-002 | LID-003 | LID-004 | LID-005 |
|---|
| Ubuntu 22.04+ (AppArmor, по умолчанию) | Работает | Работает | Работает | Работает (6.9+) | Работает |
| Debian 12+ (AppArmor) | Работает | Работает | Работает | Работает (6.9+) | Работает |
| RHEL/Fedora (SELinux) | Нет | Работает | Нет | Нет | Работает |
lockdown=confidentiality | Нет | Работает | Работает | Частично | Работает |
| Непривилегированный пользователь | Нет | Работает | Зависит от user ns | Зависит от делегирования bpffs | Нет |
| Контейнер (без CAP_BPF) | Нет | Зависит от io_uring | Зависит от seccomp | Нет | Работает |
| Контейнер (снят CAP_NET_RAW) | Нет | Зависит | Зависит | Нет | Нет |
| ID | Вектор | Цель | Что происходит |
|---|
| LID-001 | Переписывание пути через eBPF kprobe | AppArmor | kprobe переписывает имя файла до copy_from_user → AppArmor проверяет не тот путь |
| LID-002 | io_uring MSG_RING SEND_FD | SELinux, AppArmor, Smack | Передача fd пропускает security_file_receive() — любой другой способ передачи fd вызывает его |
| LID-003 | Новый API монтирования (fsopen/fsmount) | AppArmor | security_sb_mount() никогда не вызывается — единственный хук монтирования AppArmor обойдён |
| LID-004 | Делегирование BPF-токенов | AppArmor, Smack | Ноль BPF-хуков — создание токена, его использование и делегирование capabilities полностью невидимы |
| LID-005 | AF_XDP __dev_direct_xmit | tc egress, Cilium, Calico | TX в copy-режиме из стандартного Docker обходит tc-классификаторы — подделанные пакеты достигают моста |
| Индикатор | Видимость | Примечания |
|---|
| Журнал аудита AppArmor | Ничего | Отказ в доступе не происходит |
auditd / journald | Ничего | Событие безопасности не генерируется |
dmesg | Разовое предупреждение | Общее сообщение bpf_probe_write_user |
bpftool prog list | Виден | Показывает прикреплённый kprobe (если проверить) |
| Жёсткая ссылка на диске | Может быть обнаружена | find -samefile (медленно, шумно) |
| Мера защиты | Эффективность | Компромисс |
|---|
kernel.lockdown=confidentiality | Полностью блокирует BPF | Ломает легитимный мониторинг |
Отключение bpf_probe_write_user | Предотвращает перезапись пути LID-001 | Требует пересборки ядра |
fs.protected_hardlinks=1 | Ограничивает создание жёстких ссылок | По умолчанию включено на современных ядрах |
Мониторинг bpftool prog list | Обнаруживает прикреплённые зонды | Требует активного опроса |
| Переход на SELinux | Основан на inode, нейтрализует перезапись пути | Сложная миграция |
| Ограничение io_uring | Блокирует LID-002 | Может нарушить работу приложений |
| Находка | Мин. версия ядра | Привилегии | Цель |
|---|
| LID-001 | 5.x+ | root / CAP_BPF+CAP_PERFMON | только AppArmor |
| LID-002 | 6.0+ | Нет | Любая (SELinux, AppArmor, Smack) |
| LID-003 | 5.2+ | CAP_SYS_ADMIN (user ns достаточно) | только AppArmor |
| LID-004 | 6.9+ | CAP_BPF (user ns достаточно) | AppArmor, Smack |
| LID-005 | 4.18+ | CAP_NET_RAW (по умолчанию в Docker) | tc egress (Cilium, Calico и т. д.) |