
LID — Linux Integrity Drift: eBPF 경로명 재작성을 통한 AppArmor 우회. 감사(Audit) 흔적이 전혀 없는 Pre-LSM 시스템콜 인자 조작. "리눅스는 죽어가고 있다"
```
██╗ ██╗██████╗
██║ ██║██╔══██╗
██║ ██║██║ ██║
██║ ██║██║ ██║
███████╗██║██████╔╝
╚══════╝╚═╝╚═════╝
```
— "리눅스는 죽어가고 있다" —
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. │ └─────────────────────────────────────────────────────────────┘
두 개의 커널 서브시스템. 호환되지 않는 신뢰 가정. 하나의 허점.
<br>
---
## LID-001: eBPF 경로명 재작성
**핵심 발견.** `do_sys_openat2`에 대한 BPF kprobe는 커널이 파일 이름을 복사하기 전에 사용자 메모리에서 파일 이름을 다시 씁니다. AppArmor는 다시 쓰인 경로를 확인하고 접근을 허용합니다. 감사 추적이 전혀 남지 않습니다.```
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 후크를 건너뜁니다.
**ftrace로 확인됨:** `security_file_receive`는 SCM_RIGHTS에서는 발생하지만 MSG_RING에서는 발생하지 않습니다.
**영향받는 버전:** 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/main/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개의 마운트 후크를 등록합니다. SELinux는 14개를 등록합니다. 새 마운트 API는 SELinux만 구현하는 후크를 사용합니다.
추가로 발견된 사각지대:
open_tree(OPEN_TREE_CLONE): 보안 후크 0개 — 바인드 마운트 정책을 우회함mount_setattr(): 커널에 LSM 후크가 전혀 존재하지 않음move_mount()(분리된 마운트 사용 시): AppArmor는 NULL 소스 경로를 봅니다상세 정보: findings/lid-003-mount-api/
BPF 토큰 서브시스템(Linux 6.9+)은 BPF capabilities를 사용자 네임스페이스의 권한 없는 프로세스에 위임합니다. 커널은 9개의 BPF LSM 후크를 정의합니다. SELinux는 실제 avc_has_perm() 적용으로 9개를 모두 구현합니다. AppArmor는 0개를 구현합니다.```
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/main/findings/lid-004-bpf-token)
<br>
---
## LID-005: 기본 Docker 컨테이너에서 AF_XDP tc Egress 우회
AF_XDP의 copy-mode 전송 경로(`xsk_generic_xmit` → `__dev_direct_xmit`)는 컨테이너 인터페이스의 **tc egress classifier를 우회합니다**. tc u32 DROP-ALL로 검증했습니다 — AF_PACKET은 차단되고 AF_XDP는 통과합니다.
**하지만:** Cilium v1.19(kind 클러스터, 모든 egress를 거부하는 NetworkPolicy)로 테스트했습니다 — **Cilium은 우회되지 않았습니다.** Cilium은 노드 측 veth peer ingress(`cil_from_container` via `tcx/ingress`)에서 강제하며 소스 IP 검증도 수행합니다. AF_XDP 패킷은 `bpf_lxc.c:1603`에서 "Invalid source ip"로 드롭되었습니다. 영향 범위는 네트워크 정책을 위해 tc egress classifier에만 의존하는 환경(일반 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 — 를 실어 나르며, 활성 DROP 정책에도 불구하고 Docker 브리지에 도달합니다.
**PoC + 세부 정보:** [`findings/lid-005-afxdp-tc-bypass/`](https://github.com/azqzazq1/lid/blob/main/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가 아무것도 보지 못함, 감사 로그 비어 있음, 포렌식 추적 0건 | 제한적 — root 또는 CAP_BPF+CAP_PERFMON 필요(이미 권한 보유). 권한 상승은 아님. 영향: 정책 우회 + 감사 맹목성. |
| LID-002 | 높음 — security_file_receive()가 결코 호출되지 않음, fd 전송이 모든 LSM에 보이지 않음 | 높음 — io_uring을 통해 비특권 사용자 공간에서 작동. 어떤 권한도 없이 LSM 강제 경계를 넘음. |
| LID-003 | 높음 — security_sb_mount() 우회됨, AppArmor 마운트 정책은 죽은 코드 | 중간 — 마운트 네임스페이스 접근 필요(사용자 네임스페이스의 CAP_SYS_ADMIN). 많은 컨테이너 구성에서 가능. |
| LID-004 | 심각 — AppArmor가 아무것도 보지 못함, BPF 후크 0개(0/9), BPF 토큰 작업에 대한 감사 추적 없음 | 중간 — 호스트의 bpffs 위임 + 사용자 네임스페이스의 CAP_BPF 필요. BPF 위임이 있는 컨테이너 런타임(LXD/Incus)에서 가능. |
| LID-005 | 중간 — 컨테이너 인터페이스의 tc egress 분류기가 AF_XDP 트래픽을 결코 평가하지 않음 | 제한적 — 컨테이너 eth0의 tc egress 우회(검증됨). Cilium은 우회되지 않음 — 노드 측 veth ingress에서 강제 + 소스 IP 검증(테스트됨). 영향은 tc 전용 egress 필터링을 사용하는 순수 Docker 구성으로 제한됨. |
| 조건 | 요구 사항 | 비고 |
|---|
| 커널 버전 | 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(기본값)은 동일 사용자 하드 링크는 여전히 허용 |
| AppArmor 대신 SELinux | 작동하지 않음 | SELinux는 경로명 기반이 아닌 inode 기반 |
| 조건 | 요구 사항 | 비고 |
|---|
| 커널 버전 | 6.0+ | IORING_MSG_SEND_FD는 6.0에서 추가됨 |
CONFIG_IO_URING | =y | 모든 주요 배포판에서 기본값 |
| 권한 | 없음 | 비특권 사용자 공간에서 작동 |
io_uring_disabled sysctl | 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 | userns root가 손쉽게 보유 가능 |
| AppArmor 대신 SELinux | 영향 없음 | SELinux는 9개의 BPF 후크를 모두 구현 |
| 조건 | 요구 사항 | 비고 |
|---|
| 커널 버전 | 5.2+ | fsopen/fsmount는 5.2에서 도입됨 |
CONFIG_SECURITY_APPARMOR | =y | AppArmor만 영향받음 |
| 권한 | 사용자 네임스페이스의 CAP_SYS_ADMIN | unshare -m으로 가능 |
| AppArmor 대신 SELinux | 작동하지 않음 | SELinux는 security_sb_kern_mount()를 구현 |
| 컨테이너 런타임 | seccomp 필터에 따라 다름 | Docker 기본 seccomp는 fsopen을 차단 — Podman/LXC는 차단하지 않을 수 있음 |
| 조건 | 요구 사항 | 비고 |
|---|
| 커널 버전 | 4.18+ | AF_XDP는 4.18에서 도입됨 |
CONFIG_XDP_SOCKETS | =y | 모든 주요 배포판에서 기본값 |
| 권한 | CAP_NET_RAW만 | Docker, Kubernetes 파드에서 기본값 |
| 컨테이너 런타임 | Docker, K8s, LXC | 기본 capability 집합 |
| 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 | 아니요 | 작동 | 작동 | 부분적으로 | 작동 |
| 비특권 사용자 | 아니요 | 작동 | 사용자 네임스페이스에 따라 다름 | 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 후크 0개 — 토큰 생성, 사용, capability 위임이 완전히 보이지 않음 |
| LID-005 | AF_XDP __dev_direct_xmit | tc egress, Cilium, Calico | 기본 Docker의 copy-mode TX가 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 ok) | AppArmor 전용 |
| LID-004 | 6.9+ | CAP_BPF (user ns ok) | AppArmor, Smack |
| LID-005 | 4.18+ | CAP_NET_RAW (Docker 기본값) | tc egress (Cilium, Calico 등) |