
eBPF 기반 Linux 루트킷 탐지기: 다중 채널 교차 뷰 분석(sched_switch, NMI, /proc)을 사용하여 DKOM, 트레이스포인트 변조 및 프로세스 은닉을 탐지하고, 하드웨어 수준 무결성 검증을 제공합니다.
시스템 프로세스 무결성 및 교차 뷰 분석
"노래할게, 그러니 반짝여, SPiCa..."
SPiCa는 Rust로 작성된 eBPF 기반 Linux 루트킷 탐지기입니다. 이름은 하츠네 미쿠의 노래 SPiCa와 그 노래가 언급하는 별인 Spica(처녀자리 알파별)에서 유래했습니다. Spica는 맨눈으로는 하나의 별처럼 보이지만 실제로는 분광 쌍성입니다: 서로 궤도를 도는 두 개의 별이지만 스펙트럼을 측정하지 않으면 별개의 천체로 구별할 수 없습니다. SPiCa는 동일한 원리를 커널 관찰에 적용합니다: 여러 독립적인 채널이 물리적으로 구별되는 메커니즘을 통해 동일한 커널 상태를 측정하며, 하나를 감추는 루트킷은 다른 채널에 의해 노출됩니다.
면책 조항: 이 코드베이스의 상당 부분은 GLM의 도움으로 생성되거나 리팩토링되었습니다. 엄격한 테스트와 반복적인 설계가 적용되었지만, 프로덕션 사용 전에 보안 및 성능을 위해 코드를 검토하십시오.
SPiCa는 eBPF 제한된 적을 무력화하도록 설계되었습니다. 이 적은 상승된 권한(CAP_BPF 또는 CAP_SYS_ADMIN)을 가져 특권 eBPF 프로그램을 커널에 로드하는 공격자입니다. 이 적은 BPF 검증기가 강력한 제약을 부과하기 때문에 LKM 루트킷보다 본질적으로 약합니다:
부트 후 LSM 게이트에 의해 LKM이 차단되면, 이 제한된 적이 현실적인 남은 위협입니다. SPiCa의 회피 방지 메커니즘은 이 위협에 맞춰 조정되었습니다. 모든 방어는 자신이 무엇을 커버하고 무엇을 커버하지 않는지 솔직하게 명시합니다.
init_module 없이 임의 커널 쓰기를 가능하게 하는 메모리 손상. SPiCa는 쉬운 LKM 벡터를 차단함으로써 기준선을 높이지만 적의 상한선을 제한하지는 않습니다.SPiCa는 심층 방어 스택의 최후의 수단 계층이며, 위의 계층을 대체하지 않습니다.
SPiCa는 커널 후크에 연결된 4개의 eBPF 프로그램과, 시스템의 자체 뷰(/proc)에 대한 출력을 교차 상관시키는 사용자 공간 탐지 엔진을 실행합니다.
핵심 아키텍처 속성: 루트킷이 동시에 세 채널을 모두 억제할 수 없으며, 억제 자체가 탐지 가능하거나 시스템을 불안정하게 만듭니다. NMI를 억제하려면 IDT(인터럽트 설명자 테이블)를 패치해야 하며, 대부분의 커널에서 패닉을 유발합니다. 이것이 "살아있는 폭탄"입니다. 공격자가 완전한 블라인드 상태에 도달할 수 있는 유일한 경로는 시스템을 충돌시키는 것입니다.
sched sees it, /proc doesn't → DKOM NMI finds sched's heartbeat stopped → TAMPER sched goes globally silent, /proc != empty → SILENT
각 탐지 클래스는 차등 판정입니다: 둘 이상의 채널 간의 불일치입니다. 탐지 엔진은 레지스트리 + /proc 스냅샷 + 채널 타임스탬프에 대한 순수 함수입니다 — I/O가 없고, 부작용이 없으며, 완전히 유닛 테스트 가능합니다.
### NMI 재설계: 관찰에서 무결성으로
원래 설계에서 NMI는 CPU를 샘플링하고 어떤 태스크가 실행 중인지 보고하는 두 번째 프로세스 관찰 채널이었습니다. 이는 중복이었습니다: sched\_switch가 이미 스케줄링을 관찰하고 있었고, NMI는 다른 메커니즘을 통해 동일한 데이터를 샘플링했습니다. 이 중복으로 인해 초당 CPU당 약 1000개 이상의 링 버퍼 이벤트가 발생했으며, 이 데이터의 99.999%는 '예, 스케줄러가 예상대로 작동하고 있습니다'를 확인하는 데 사용되었습니다.
재설계된 아키텍처에서 **NMI는 프로세스 관찰에서 트레이스포인트 무결성 검증으로 용도가 변경되었습니다.** 더 이상 CPU에서 실행 중인 프로세스를 보고하지 않습니다. 대신, 공유 `.bss` 하트비트를 읽어 `sched_switch`가 실제로 실행되고 있는지 확인합니다. 이로 인해:
1. NMI 링 버퍼 트래픽의 약 99%를 제거합니다 (정상 상태에서 거의 제로 이벤트)
2. 트레이스포인트 분리, 억제 및 BTF/어태치 실패를 직접 탐지합니다 (원래 BTF 버그 — [§11](#11-the-btf-bug-incident) 참조)
3. 하드웨어 인터럽트에서 실행되며, 트레이스포인트 디스패치 경로 외부에 있으므로 `bpf_override_return`, kprobe 차단, funcs-array 조작에 면역입니다
4. `.bss` 전역 변수를 BPF 헬퍼가 아닌 직접 메모리 접근을 통해 읽으므로, 헬퍼 함수에 대한 `fmod_ret`에 면역입니다
---
## 3. sched\_switch 관찰 채널
`sched_switch` 트레이스포인트에 연결된 eBPF 프로그램은 커널이 CPU에 프로세스를 스케줄링할 때마다 실행됩니다. 전통적인 (비 BTF) 고정 오프셋 읽기를 사용하여 트레이스포인트 인수에서 들어오는 태스크의 PID와 comm을 직접 읽습니다:```
ctx.read_at::<u32>(56) → next_pid
ctx.read_at::<[u8;16]>(40) → next_comm
의도적으로 BTF/CO-RE를 사용하지 않음. 트레이스포인트 인수 레이아웃은 커널 버전 간에 안정적입니다 (트레이스포인트 ABI의 일부입니다). 하드코딩된 오프셋을 사용하면 BTF로 해석된 구조체 네비게이션의 커널 버전 취약성을 피할 수 있습니다. 이는 §11에 문서화된 의도적인 설계 선택입니다.
매 호출 시, 프로그램은:
next_pid 및 next_comm을 읽습니다.ProcessInfo 구조체를 BASE_KEY로 XOR 난독화합니다.sc_sched 링 버퍼에 제출합니다.bpf_ktime_get_ns()를 .bss 전역 변수 SCHED_HEARTBEAT에 씁니다 — NMI 무결성 검사기가 모니터링하는 하트비트입니다.프로그램은 이 컨텍스트에서 의도적으로 bpf_get_current_pid_tgid()를 사용하지 않습니다. sched_switch 시점에서 "현재"는 나가는 태스크이지 들어오는 태스크가 아닙니다. 트레이스포인트 인수가 올바른 (들어오는) 프로세스 식별자를 제공합니다.
감지 엔진은 단일 단조 시간 기준을 사용합니다: SPiCa의 프로세스 시작 이후 나노초 (run_detection() 진입 이후 Instant::now()). 스케줄 이벤트가 도착하면, 이벤트 핸들러는 이벤트에 포함된 eBPF bpf_ktime_get_ns() 값이 아닌 호출자가 제공한 now (프로세스 로컬 나노초)를 저장합니다. 이는 커널 부트 시간과 프로세스 로컬 시간이 혼합될 경우 발생할 수 있는 시간 기준 불일치를 방지합니다 — 이전 버전에 존재했으며 모든 활성 조건이 조용히 실패하게 만든 버그입니다.
NMI 프로그램은 하드웨어 인터럽트 레벨에서 발생하는 하드웨어 성능 카운터 (HW_CPU_CYCLES)에 연결됩니다. NMI는 cli로 비활성화할 수 없습니다 — 이를 억제하려면 PMU를 다시 프로그래밍하거나 IDT를 후킹해야 하며, 둘 다 뚜렷하고 불안정을 초래합니다.
무결성 검사 메커니즘은 매우 간단합니다:``` .bss (shared within SPiCa's ELF object — all programs see the same memory): SCHED_HEARTBEAT ← written by sched_switch on every invocation NMI_LAST_HB ← NMI's record of the last heartbeat it observed NMI_FIRST_TICK ← first NMI invocation timestamp (grace period reference) NMI_LAST_EMIT ← throttle: last event emission timestamp
sched_switch runs → SCHED_HEARTBEAT = bpf_ktime_get_ns()
NMI fires (once per second, throttled): if SCHED_HEARTBEAT == NMI_LAST_HB: if past grace period (5 seconds since first NMI tick): → sched_switch stopped running → emit event_type=1 (TAMPER) else: → sched_switch is alive → emit event_type=0 (heartbeat OK) NMI_LAST_HB = SCHED_HEARTBEAT
### 이 범위 내의 모든 공격을 탐지하는 이유
| 공격 | 발생 상황 | NMI가 감지하는 것 |
|---|---|---|
| Tracepoint가 funcs 배열에서 분리됨 | sched\_switch 중단 | `SCHED_HEARTBEAT` 동결 → TAMPER |
| BTF 버그 / 연결 실패 (무음) | sched\_switch가 실행되지 않음 | `SCHED_HEARTBEAT` 0 유지 → TAMPER (유예 후) |
| 스케줄러 함수에 `bpf_override_return` | tracepoint 발동을 방해하는 경우 | `SCHED_HEARTBEAT` 동결 → TAMPER |
| 바이트코드 제자리 패치 | 임의의 커널 쓰기 필요 (LKM 수준) | eBPF 위협 모델 외부 |
| 링 버퍼 소비자 포인터 조작 | sched 이벤트가 사용자 공간에 도달하지 않음 | `SCHED_HEARTBEAT`는 여전히 진행됨 (프로그램 실행) → 거짓 TAMPER 없음; 사용자 공간이 `max(sched_last)` 스테일을 통해 탐지 → SILENT |
### `.bss`를 특별히 사용하는 이유
`.bss` 전역 변수는 BPF 프로그램의 내부 데이터 섹션에 저장되며, 로더가 관리하는 내부 배열 맵이 지원합니다. 그 특징은 다음과 같습니다:
- **별도로 고정 불가능** — `/sys/fs/bpf/`에 이름이 있는 맵으로 나타나지 않음
- **`bpf_map_update_elem` 훅을 통한 가로채기 불가** — `.bss` 쓰기는 직접 메모리 저장이며, 맵 업데이트 시스템 콜이 아닙니다. 이전 `sc_canary` 메커니즘 (`.bss` 복사본을 이름 있는 맵 복사본과 비교하여 `bpf_map_update_elem` 가로채기를 탐지)은 더 이상 필요하지 않습니다.
- **동일한 ELF 객체 내 프로그램 간 공유** — sched\_switch와 NMI가 외부 인터페이스 없이 `.bss`를 통해 통신
### BPF 기반 가로채기에 대한 면역
NMI 무결성 검사기는 근본적인 특성 때문에 BPF 재정의 공격에 구조적으로 면역입니다: `bpf_override_return`은 함수 **호출**을 가로채지만, NMI 검사기는 검증하는 대상을 *호출*하지 않습니다 — 직접 `.bss` 메모리를 *읽습니다*. 메모리 읽기는 함수 호출이 아니므로 반환 값을 재정의할 수 없습니다.
추가로:
- `bpf_probe_read_kernel` (대안 설계에서 커널 구조체 읽기에 사용됨)은 모든 주소를 허용하는 오류 안전 헬퍼입니다 — 하지만 SPiCa의 `.bss` 하트비트 설계는 이것조차 필요하지 않습니다. 검사기는 직접 로드 명령어를 통해 `.bss` 전역 변수를 읽습니다.
- NMI 프로그램은 NMI 컨텍스트에서 실행되며, 이 컨텍스트에서 kprobes는 구조적으로 신뢰할 수 없습니다 (커널이 이를 지연 또는 억제합니다). 검사기 실행에 대한 kprobe 기반 공격은 하드웨어와 싸우는 것과 같습니다.
### NMI 이벤트 의미론
NMI 링 버퍼 (`sc_nmi`)는 경량 이벤트를 전달합니다:
| `event_type` | 의미 | 사용자 공간 동작 |
|---|---|---|
| 0 | 하트비트 — NMI 활성, sched\_switch 활성 | `last_nmi_heartbeat` 타임스탬프 업데이트 |
| 1 | TAMPER — NMI 활성, sched\_switch 하트비트 동결 | 즉시 `[TAMPER]` 출력 |
이벤트는 최대 1초에 한 번씩 전송됩니다 (`NMI_LAST_EMIT`에 의해 제한됨). NMI 링 버퍼가 5초 이상 무음 상태가 되면, 사용자 공간에서 `[SILENT]`를 발생시킵니다 — NMI 채널 자체가 죽은 것입니다.
---
## 5. 탐지 로직```mermaid
graph TD
subgraph RING0["Kernel Space: Four eBPF Programs"]
direction TB
SCHED_P["TracePoint, sched_switch<br/>read next_pid, next_comm<br/>write SCHED_HEARTBEAT (.bss)<br/>XOR obfuscate → sc_sched"]
NMI_P["PerfEvent hardware NMI<br/>read SCHED_HEARTBEAT (.bss)<br/>compare to NMI_LAST_HB<br/>frozen → event_type=1 (TAMPER)<br/>alive → event_type=0 (heartbeat)<br/>→ sc_nmi"]
LSM_P["BPF LSM, kernel_read_file<br/>READING_MODULE<br/>gate=0: allow + log<br/>gate=1: EPERM + log → sc_lsm"]
WATCH_P["TracePoint, sched_process_exit<br/>current == SPICA_PID (.bss)? → sc_wd flag"]
end
subgraph RING3["User Space: Differential Engine"]
ENGINE["SPiCa (Tokio async)"] -->|XOR deobfuscate| RB_S[(sc_sched RingBuf)]
ENGINE -->|event_type=0: heartbeat<br/>event_type=1: TAMPER| RB_N[(sc_nmi RingBuf)]
ENGINE -->|raw read| RB_L[(sc_lsm RingBuf)]
ENGINE -->|read_dir| PROC[" /proc"]
RB_S --> FSM{Detection FSM}
PROC --> FSM
FSM -->|scheduled, absent /proc, >2s| D1["[DKOM]"]
FSM -->|in /proc, never seen by sched, >5s| D2["[GHOST]"]
RB_N -->|event_type = 1| D3["[TAMPER]"]
FSM -->|sched channel silent, /proc ≠∅| D4["[SILENT]"]
FSM -->|raw getdents64 ≠ libc readdir| D9["[HOOK]"]
RB_S -->|same TGID, different start_time_ns| D5["[DUPE]"]
RB_L -->|allowed = 1| D7["[LKM-ALLOW]"]
RB_L -->|allowed = 0| D8["[LKM-DENY]"]
WATCH_P -.->|pin exists at startup| D6["[WATCHDOG]"]
end
classDef kernbox fill:#fdf2f8,stroke:#9333ea,stroke-width:2px,color:#581c87;
classDef userbox fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a8a;
classDef alertbox fill:#fff7ed,stroke:#ea580c,stroke-width:1px,color:#7c2d12;
classDef storage fill:#f8fafc,stroke:#64748b,stroke-width:1px;
class RING0 kernbox;
class RING3 userbox;
class D1,D2,D3,D4,D5,D6,D7,D8,D9 alertbox;
class RB_S,RB_N,RB_L,PROC storage;
SILENT는 채널 수준 판정이며, 프로세스별 판정이 아닙니다. 다음 조건에서 발생합니다:
sched 채널 사망: 모든 레지스트리 레코드에 걸친 max(sched_last)가 /proc이 비어 있지 않은 상태에서 CHANNEL_DEAD_NANOS보다 오래 지남. 비어 있지 않은 /proc 가드는 활성 보장입니다: 실행 중인 모든 Linux 시스템에서 /proc은 비어 있지 않으며(init, 커널 스레드, kworker), 스케줄러는 결코 진정한 유휴 상태가 아닙니다(타이머 틱, I/O 대기자, RCU 콜백). 비어 있지 않은 시스템에서 max(sched_last)가 오래되었다는 것은 시스템이 유휴 상태라는 것이 아니라 채널이 죽었다는 의미입니다.
NMI 채널 사망: NMI 링 버퍼에서 하트비트 이벤트(event_type=0)가 5초 이상 도착하지 않음. 이는 NMI perf 이벤트가 중단되었음을 의미합니다 — perf 이벤트가 분리되었거나 PMU가 재프로그래밍된 것입니다.
두 검사 모두 30초의 쿨다운이 있어 알람 과잉을 방지합니다.
[HOOK])SPiCa는 매 틱마다 두 가지 독립적인 메커니즘을 통해 /proc을 읽습니다:
원시 getdents64 시스템 콜 (read_tgids()) — Rust의 std::fs::read_dir이 libc::syscall(SYS_getdents64, ...)을 통해 직접 호출하므로 모든 libc 함수 훅을 우회합니다. LD_PRELOAD 루트킷은 이 경로를 가로챌 수 없습니다.
libc readdir (read_tgids_libc()) — FFI를 통해 libc::opendir / libc::readdir을 호출하므로 동적 링커를 거쳐 /etc/ld.so.preload 훅에 완전히 차단될 수 있습니다.
getdents64가 libc readdir이 보지 못하는 PID를 발견하면, 사용자 공간 루트킷이 readdir을 훅하여 프로세스를 숨기고 있는 것입니다. SPiCa가 [HOOK]를 발생시킵니다:```
[HOOK] 3 PID(s) hidden from libc readdir — LD_PRELOAD rootkit suspected
이 기술은 SPiCa의 커널 수준 권한을 사용자 공간 루트킷의 능동적 탐지로 전환합니다. 원시 시스템 호출 경로는 절대 진실입니다 — 사용자 공간 훅은 이를 가릴 수 없습니다. libc 경로는 일반 도구(ps, ls)가 보는 "인지된" 뷰입니다. 그 차이는 라이브러리 인터셉션의 명백한 증거입니다.
**`[HOOK]`에 의해 탐지된 루트킷:**
| Rootkit | Hiding mechanism | Detected |
|---------|-----------------|----------|
| Symbiote | Parasitic LD_PRELOAD, hooks `readdir` | Yes (when actively hiding) |
| JynxKit | LD_PRELOAD, `MAGIC_GID` hiding via `readdir` | Yes |
| Azazel | LD_PRELOAD, `readdir` + `stat` hooks | Yes |
| Medusa/OrBit | LD_PRELOAD, `readdir` + credential theft | Yes |
**실제 Symbiote로 테스트:** 우분투 24.04 VM에서 `/etc/ld.so.preload`를 통해 실제 Symbiote 샘플(SHA256 `f55af21f...`, MalwareBazaar)이 배포되었습니다. 샘플은 `readdir`, `readdir64`, `stat`, `fstatat`, `pam_authenticate`, `pcap_loop`, `recvmsg`, `fopen`, `read`, `execve`를 훅합니다. LD_PRELOAD 훅이 `readdir`에서 PID를 능동적으로 은닉할 때, SPiCa는 한 틱 주기(<1초) 내에 `[HOOK]`을 발생시킵니다. Symbiote가 로드되었지만 수동적(능동적으로 은닉하지 않음)일 때, SPiCa는 0개의 오탐을 생성합니다.
### Grace window
50ms 유예 창은 새로 포크된 프로세스에서 오탐을 방지합니다. 채널 수준 SILENT 검사에는 5초의 유예 기간(NMI 프로그램의 `NMI_FIRST_TICK` 참조)이 있어 시작 지연 시간이 잘못된 TAMPER 경고를 생성하지 않습니다.
---
## 6. Verifier-Bounded Address Secrecy
### Definition
**Verifier-bounded address secrecy**는 eBPF-제한적 적대자 모델 하에서 성립하는 계산적 보안 속성입니다. 이는 다음과 같습니다:
> CAP_BPF를 로드할 수 있지만 BPF 검증기에 의해 제한된(제한된 루프, 제한된 헬퍼, 임의의 커널 쓰기 불가) 적대자가 있을 때, BPF 프로그램의 `.bss` 데이터 섹션의 커널 주소를 계산적으로 발견하는 것이 불가능합니다.
### The four mechanisms
이 속성은 네 가지 독립적인 장벽의 합성입니다:
1. **kASLR** — 커널 주소 공간 배치 무작위화(Kernel Address Space Layout Randomization)는 BPF 맵 메모리(`.bss` 백킹 맵 포함)가 할당되는 커널 힙 영역을 무작위화합니다. 정확한 주소는 부팅할 때마다 변경됩니다.
2. **Verifier instruction bound** — BPF 검증기는 프로그램당 총 명령어를 약 100만 개로 제한하고 종료가 증명 가능한 루프를 요구합니다. 적대자의 스캐닝 프로그램은 `bpf_probe_read_kernel`을 통해 로드당 최대 N ≈ 10^6 주소를 조사할 수 있습니다. kASLR로 무작위화된 검색 공간이 주어지면, 프로그램당 성공 확률은 무시할 수 있습니다.
3. **LSM map-access gate** ([§7](#7-lsm-map-access-gate) 참조) — 게이트가 없으면 적대자는 ID로 BPF 맵을 열거하고(`BPF_MAP_GET_NEXT_ID` → `BPF_MAP_GET_FD_BY_ID` → `BPF_MAP_LOOKUP_ELEM`) `.bss` 내용을 직접 읽을 수 있습니다. 게이트는 이 쉬운 경로를 제거하여 적대자를 검증기-제한 메모리 스캔 경로로 강제합니다.
4. **`.bss` placement** — `.bss` 전역 변수는 사람이 읽을 수 있는 이름을 가진 명명된 맵 열거 인터페이스에 나타나지 않습니다. 내부 `.bss` 백킹 맵은 libbpf/aya에 의해 `.bss`로 명명되지만, 그 내용(실제 전역 변수)은 맵 fd를 가지고 있어야만 접근할 수 있습니다. 맵 fd는 로딩 프로세스의 fd 테이블 또는 맵 ID(게이트에 의해 차단됨)가 필요합니다.
### Formal argument structure```
Adversary's goal: locate SPiCa's .bss in kernel virtual memory
Search space: kernel heap region, kASLR-randomized (~2^30 entropy)
Path A — map enumeration (blocked):
BPF_MAP_GET_NEXT_ID → BPF_MAP_GET_FD_BY_ID → BPF_MAP_LOOKUP_ELEM
→ blocked by LSM gate (§7) for non-SPiCa processes
Path B — memory scan (bounded):
bpf_probe_read_kernel(addr, buf, len) — fault-safe, accepts any addr
Per-program capacity: N ≈ 10^6 probes (instruction limit)
Success probability per load: N / |search space| ≈ 10^6 / 2^30 ≈ 10^-24
Can reload programs, but: each load is a syscall (observable), and
the target address doesn't change between loads (same boot session)
Path C — timing side channel (theoretical):
bpf_probe_read_kernel timing varies on fault vs success
Binary search over the heap could reduce scan complexity to O(log |A|)
→ O(30) probes, well within instruction budget
LIMITATION: must be acknowledged. Mitigated by rate-limiting BPF_PROG_LOAD
via the LSM gate (future work).
대부분의 BPF 보안 연구는 검증기를 공격 표면 (검증기 버그, 불건전성, JIT 스프레이)으로 취급합니다. 이 연구는 프레이밍을 뒤집습니다: 검증기의 제약 조건은 공격자의 탐색 공간을 제한하는 방어적 기본 요소입니다. 비밀은 암호학적이지 않습니다(AES가 아닙니다); 계산적입니다 — 검증기가 공격자에게 허용하는 계산에 의해 제한됩니다.
bpf_probe_read_kernel은 타이밍 관찰이 가능합니다(실패 vs 성공에 다른 사이클 소요). 힙에 대한 타이밍 기반 이진 탐색은 이론적으로 선형 스캔보다 효율적입니다. 명령어 바운드는 프로그램당 프로브 수를 제한하지만, 이진 탐색은 프로브 수를 O(log |A|) ≈ 30으로 줄일 수 있습니다. 이는 실제 한계점입니다. 완화 방법: LSM 게이트를 통해 BPF_PROG_LOAD를 속도 제한합니다..bss 전역 변수는 내부 BPF 맵으로 백업됩니다. CAP_BPF 권한이 있는 모든 프로세스는 시스템의 모든 맵을 열거할 수 있습니다:```
bpf(BPF_MAP_GET_NEXT_ID, …) → iterate all map IDs
bpf(BPF_MAP_GET_FD_BY_ID, id) → get fd to any map
bpf(BPF_MAP_LOOKUP_ELEM, fd, key, value) → read map contents
eBPF 공격자(시스템 콜을 호출할 수 없는)에게 `.bss`는 숨겨집니다. 사용자 공간 루트에게는 표준 맵으로 완전히 읽을 수 있습니다. LSM 게이트가 이 격차를 메웁니다.
### 방어
`bpf` 시스템 콜에 대한 BPF LSM 후크는 맵 접근 명령을 확인합니다:```
hook = "bpf"
read cmd (arg 0)
if cmd == BPF_MAP_GET_FD_BY_ID:
read map_id from userspace bpf_attr (bpf_probe_read_user)
if map_id matches any of SPiCa's stored IDs (.bss):
if caller_tgid != SPICA_PID:
return -EPERM
Map IDs are written to .bss by the userspace loader right after program load, before the LSM hook is armed (same timing pattern as the existing sc_gate for LKM blocking). No BTF needed — cmd and map_id come from syscall arguments, not kernel structs.
맵 ID는 프로그램 로드 직후, LSM 후크가 활성화되기 전에 userspace 로더에 의해 .bss에 기록됩니다(LKM 차단을 위한 기존 sc_gate와 동일한 타이밍 패턴). BTF는 필요하지 않습니다. cmd와 map_id는 커널 구조체가 아닌 시스템 호출 인수에서 비롯됩니다.
Surgical: only blocks access to SPiCa's specific map IDs. Other processes' BPF tools (tcpdump, bpftrace, bcc) access their own maps and are unaffected.
정밀함: SPiCa의 특정 맵 ID에 대한 접근만 차단합니다. 다른 프로세스의 BPF 도구(tcpdump, bpftrace, bcc)는 자체 맵에 접근하며 영향을 받지 않습니다.
ptrace / /proc/pid/mem — Another root process could read SPiCa's process memory directly via ptrace, bypassing the BPF syscall entirely. This is a fundamental limitation: you can't protect against a process with the same privilege level reading your memory via /proc/pid/mem. Defense: off-host log shipping (if the attacker can read local memory, they can also suppress local alerts — only remote logging helps).
SPiCa's own PID compromised — If the attacker gains control of SPiCa's process, they have legitimate access to the maps.
ptrace / /proc/pid/mem — 다른 루트 프로세스가 ptrace를 통해 SPiCa의 프로세스 메모리를 직접 읽어 BPF 시스템 호출을 완전히 우회할 수 있습니다. 이는 근본적인 한계입니다: 동일한 권한 수준의 프로세스가 /proc/pid/mem을 통해 메모리를 읽는 것을 막을 수 없습니다. 방어: 호스트 외부 로그 전송(공격자가 로컬 메모리를 읽을 수 있으면 로컬 알림도 억제할 수 있으므로 원격 로깅만 도움이 됩니다).
SPiCa 자체 PID 손상 — 공격자가 SPiCa의 프로세스 제어권을 획득하면 맵에 대한 정당한 접근 권한을 가지게 됩니다.
On TPM-equipped hosts, the 64-bit XOR obfuscation key is sourced from TPM2_GetRandom via the tss-esapi crate (direct library call to libtss2-esys). The key transits no pipe, no subprocess command line, no disk. It exists only in TPM hardware and in kernel volatile memory (.bss backing map) for the duration of the SPiCa process.
TPM이 장착된 호스트에서는 tss-esapi 크레이트(libtss2-esys에 대한 직접 라이브러리 호출)를 통해 TPM2_GetRandom에서 64비트 XOR 난독화 키를 가져옵니다. 키는 파이프, 서브프로세스 명령줄, 디스크를 거치지 않습니다. SPiCa 프로세스가 유지되는 동안 TPM 하드웨어와 커널 휘발성 메모리(.bss 백업 맵)에만 존재합니다.
On hosts without a TPM (VMs without vTPM, containers, machines without a TPM chip), SPiCa falls back to a per-build compile-time key generated from /dev/urandom by the build script. The fallback is visibly loud at startup with a warning banner.
TPM이 없는 호스트(vTPM이 없는 VM, 컨테이너, TPM 칩이 없는 시스템)에서는 SPiCa가 빌드 스크립트가 /dev/urandom에서 생성한 빌드별 컴파일 타임 키로 대체합니다. 이 대체 방식은 시작 시 경고 배너와 함께 시각적으로 명확하게 표시됩니다.
.bss placement.bss 배치The following values live in the eBPF program's .bss section, written by userspace via set_global() before any program is loaded:
다음 값들은 eBPF 프로그램의 .bss 섹션에 있으며, 프로그램이 로드되기 전에 userspace가 set_global()을 통해 작성합니다:
Named BPF maps are enumerable via the bpf() syscall's map iteration interface (blocked by the LSM gate — see §7); .bss globals are accessed through the same internal map but are not separately named or pinnable.
명명된 BPF 맵은 bpf() 시스템 호출의 맵 반복 인터페이스를 통해 열거 가능합니다(LSM 게이트에 의해 차단됨 — §7 참조). .bss 전역 변수는 동일한 내부 맵을 통해 접근되지만 별도로 명명되거나 고정될 수 없습니다.
All ProcessInfo fields are XOR-folded with the 64-bit BASE_KEY before being written to ring buffers. The event_type field is intentionally left unobfuscated so userspace can read it as a sentinel before deobfuscating the rest.
모든 ProcessInfo 필드는 링 버퍼에 기록되기 전에 64비트 BASE_KEY로 XOR 접힙니다. event_type 필드는 의도적으로 난독화되지 않은 상태로 남겨져 userspace가 나머지를 해독하기 전에 센티넬로 읽을 수 있습니다.
This is obfuscation against read-leakage, not encryption against a capable adversary. XOR with an 8-byte repeating key is structurally vulnerable to known-plaintext: predictable comm values ("bash", "systemd", "kthreadd") XOR'd against the ciphertext recover key bytes directly. The defense is appropriate for the eBPF adversary (who can't easily read the ring buffer — it requires the same capability gap that protects .bss), not for an adversary with binary access + ring buffer interception.
이는 읽기 누출에 대한 난독화이지, 유능한 공격자에 대한 암호화가 아닙니다. 8바이트 반복 키를 사용하는 XOR는 구조적으로 알려진 평문 공격에 취약합니다. 예측 가능한 comm 값("bash", "systemd", "kthreadd")을 암호문과 XOR하면 키 바이트를 직접 복구할 수 있습니다. 이 방어는 eBPF 공격자(링 버퍼를 쉽게 읽을 수 없으며, .bss를 보호하는 것과 동일한 능력 격차가 필요함)에게는 적절하지만, 바이너리 접근 + 링 버퍼 가로채기가 가능한 공격자에게는 적절하지 않습니다.
Status: Design phase. Not yet implemented. This section documents the target architecture for the research paper.
상태: 설계 단계. 아직 구현되지 않음. 이 섹션은 연구 논문을 위한 대상 아키텍처를 문서화합니다.
PCR-bound TPM sealing moves the key distribution boundary from the software layer into silicon. The key is sealed at install time against expected Platform Configuration Register (PCR) values, and can only be unsealed if the system's boot state matches the expected measurements.
PCR 바인딩 TPM 실링은 키 배포 경계를 소프트웨어 계층에서 실리콘으로 이동시킵니다. 키는 설치 시 예상되는 플랫폼 구성 레지스터(PCR) 값에 대해 실링되며, 시스템의 부팅 상태가 예상 측정값과 일치하는 경우에만 언실링될 수 있습니다.
During boot, the firmware, bootloader, and kernel measure critical components into PCRs:
부팅 중에 펌웨어, 부트로더 및 커널은 중요한 구성 요소를 PCR로 측정합니다:
Note: PCR allocation is boot-chain-dependent. GRUB measures the kernel to PCR 4; systemd-stub measures to PCR 4 and the command line to PCR 8. The sealing policy must match the target boot chain.
참고: PCR 할당은 부트 체인에 따라 다릅니다. GRUB은 커널을 PCR 4로 측정합니다. systemd-stub은 PCR 4로, 명령줄은 PCR 8로 측정합니다. 실링 정책은 대상 부트 체인과 일치해야 합니다.
Seal (install time): The 64-bit key is sealed against expected PCR values using TPM2_Create with a TPM2_PolicyPCR session. The sealed blob is stored on disk. It is encrypted with the TPM's internal key and can only be decrypted when the specified PCRs match.
실링(설치 시간): TPM2_PolicyPCR 세션과 함께 TPM2_Create를 사용하여 64비트 키를 예상 PCR 값에 대해 실링합니다. 실링된 blob은 디스크에 저장됩니다. TPM의 내부 키로 암호화되며 지정된 PCR이 일치할 때만 복호화할 수 있습니다.
Unseal (early boot, initramfs stage): Before any untrusted userspace code executes, SPiCa's initramfs hook requests TPM2_Unseal. If the current PCR values match the seal policy, the TPM releases the key.
언실링(초기 부팅, initramfs 단계): 신뢰할 수 없는 userspace 코드가 실행되기 전에 SPiCa의 initramfs 후크가 TPM2_Unseal을 요청합니다. 현재 PCR 값이 실링 정책과 일치하면 TPM이 키를 해제합니다.
Inject: The key is written to .bss via set_global() before programs are loaded.
주입: 프로그램이 로드되기 전에 키가 set_global()을 통해 .bss에 기록됩니다.
For the research paper, present the strong policy and discuss the re-seal tradeoff in limitations.
연구 논문에서는 강력함 정책을 제시하고 한계에서 재실링 트레이드오프를 논의하십시오.
The two mechanisms are complementary:
두 메커니즘은 상호 보완적입니다:
PCR sealing protects the key at boot — it ensures the key is only available on a trusted system state.
Verifier-bounded address secrecy + LSM gate protect the key at runtime — they ensure the eBPF adversary can't locate or read the .bss where the key lives.
PCR 실링은 부팅 시 키를 보호합니다 — 신뢰할 수 있는 시스템 상태에서만 키를 사용할 수 있도록 보장합니다.
검증기 제한 주소 비밀성 + LSM 게이트는 런타임에 키를 보호합니다 — eBPF 공격자가 키가 있는 .bss를 찾거나 읽을 수 없도록 보장합니다.
Neither is sufficient alone. PCR sealing doesn't help if the key is compromised at runtime (map enumeration). Address secrecy doesn't help if the system was compromised before SPiCa started (hostile initramfs).
어느 것 하나만으로는 충분하지 않습니다. 런타임에 키가 손상되면(맵 열거) PCR 실링은 도움이 되지 않습니다. SPiCa가 시작되기 전에 시스템이 손상되면(적대적 initramfs) 주소 비밀성은 도움이 되지 않습니다.
Runtime LSM program detachment — The BPF LSM programs (spica_lsm_modblock, the map-access gate) are loaded at runtime and are NOT measured to any PCR. A rootkit that detaches them after boot isn't caught by PCR sealing. What catches that is the NMI heartbeat (detecting that the detection system itself stopped running).
Post-boot kernel exploitation — If the kernel is exploited after boot (memory corruption → arbitrary write), PCRs are unchanged. This is the "nation-state" non-goal.
런타임 LSM 프로그램 분리 — BPF LSM 프로그램(spica_lsm_modblock, 맵 접근 게이트)은 런타임에 로드되며 어떤 PCR에도 측정되지 않습니다. 부팅 후 이를 분리하는 루트킷은 PCR 실링으로 잡을 수 없습니다. 이를 잡는 것은 NMI 하트비트(탐지 시스템 자체가 실행을 중단했음을 감지)입니다.
부팅 후 커널 익스플로잇 — 부팅 후 커널이 익스플로잇되면(메모리 손상 → 임의 쓰기) PCR은 변경되지 않습니다. 이것은 "국가 수준" 비목표입니다.
SPiCa is the last enforcement layer. It complements a properly configured system, not replaces the layers above.
SPiCa는 최후의 시행 계층입니다. 위의 계층을 대체하는 것이 아니라 적절히 구성된 시스템을 보완합니다.```mermaid
flowchart TD
SB["UEFI Secure Boot
─────────────────────────────
Verifies bootloader signature against
the UEFI db certificate store
Measured to PCR 7"]
MS["Kernel Module Signing
─────────────────────────────
CONFIG_MODULE_SIG_FORCE=y
Kernel rejects unsigned .ko at load time"]
IMA["IMA, Integrity Measurement Architecture
─────────────────────────────
Measures file hashes to TPM PCR10
Appraise policy blocks non-matching signatures"]
SPICA["SPiCa
─────────────────────────────
LSM gate: blocks LKM loads + map enumeration
sched_switch + NMI integrity channels
Differential detection: DKOM · GHOST · TAMPER · SILENT · DUPE
PCR-bound key sealing (design phase)"]
SB -->|"boot chain verified"| MS
MS -->|"signed modules only"| IMA
IMA -->|"measured + appraised"| SPICA
classDef firmware fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
classDef kernel fill:#fdf2f8,stroke:#9333ea,stroke-width:2px,color:#581c87
classDef spica fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
class SB firmware
class MS,IMA kernel
class SPICA spica
SPiCa는 시작 시 네 가지 레이어를 모두 확인하고 상태를 출력합니다.
---
## 11. BTF 버그 사건
### 발생 상황
Ubuntu(최신 커널) 테스트 중 BTF 비호환성으로 인해 `sched_switch` 트레이스포인트 프로그램이 성공적으로 연결되었지만 **단 하나의 이벤트도 발생하지 않았습니다.** `attach()` 시스템 호출은 `Ok(())`를 반환하여 SPiCa는 정상적으로 진행되었지만, sched 링 버퍼는 비어 있었습니다. 탐지 엔진에 스케줄링 데이터가 없었기 때문에 탐지 경보가 발생하지 않았습니다. **SPiCa는 실패 징후 없이 눈이 먼 상태로 실행되었습니다.**
보안 도구에게 최악의 실패 모드는 바로 침묵하는 맹목입니다.
### 탐지되지 않은 이유
원래 탐지 엔진은 **프로세스별**로 추론했습니다. 각 프로세스 레코드에는 `sched_last` 및 `nmi_last` 타임스탬프가 있었고, 활성 상태는 레코드별로 계산되었습니다. sched\_switch가 전역적으로 중단되었을 때:
1. `sched_live`는 모든 레코드에 대해 false가 되었습니다(새 sched 이벤트 없음 → 모든 `sched_last` 값이 만료됨).
2. 프로세스별 `TAMPER` 조건(`in_proc && nmi_live && !sched_live`)이 발생할 수 있었지만, `nmi_live`가 *연속적으로* 2초 동안 유지되어야 했습니다. NMI는 드물게 샘플링되므로(10M 주기), `suspect_since`가 샘플링 지터로 인해 계속 재설정되어 성숙되지 않았습니다.
3. 프로세스별 `SILENT` 조건은 `sched_live`가 필요했지만, 지금은 모든 것에 대해 false였습니다. 조건은 정확히 탐지하려는 조건에서 반전되었습니다.
4. **채널 수준의 활성 상태 확인이 없었습니다.** — "sched가 EVER 발생했는가?" 또는 "max(sched_last)가 오래되었는가?"와 같은 메커니즘이 없었습니다.
또한 시간 기반 불일치가 발견되었습니다. `sched_last`는 `bpf_ktime_get_ns()`(커널 부트 나노초)를 저장하는 반면, `evaluate()`는 `nanos_since_startup()`(프로세스-로컬 나노초)와 비교했습니다. 이 서로 다른 시간 기반의 `wrapping_sub`는 거대한 값을 생성하여 모든 활성 조건이 무의미하게 false가 되었습니다. 탐지 로직은 실제 운영 환경에서 올바르게 작동한 적이 없었습니다. BTF 버그가 이벤트가 전혀 도착하지 못하게 하여 이를 가렸을 뿐입니다.
### 리디자인이 문제를 해결한 방법
| 문제 | 수정 |
|---|---|
| 채널 수준의 활성 상태 확인 부재 | `evaluate()`는 이제 `max(sched_last)`를 계산하고 `/proc`이 비어 있지 않은 상태에서 오래되면 `[SILENT]`를 발생시킴 |
| 프로세스별 TAMPER가 성숙되지 않음(지터 재설정) | TAMPER는 이제 NMI 프로그램의 직접 신호(`.bss` 하트비트 비교)이며, 임계값이 있는 프로세스별 FSM이 아님 |
| sched가 중단되었을 때 프로세스별 SILENT가 반전됨 | SILENT는 이제 채널 수준이며, 레코드별 조건이 아닌 집계에서 계산됨 |
| 시간 기반 불일치 | `sched_last`는 이제 `bpf_ktime_get_ns()`가 아닌 프로세스-로컬 시간(`now` 매개변수)을 저장함 |
| NMI는 중복 관찰이었음 | NMI는 이제 무결성 검사기로, `.bss` 하트비트를 통해 sched 사망을 직접 탐지함 |
---
## 12. 알려진 제한 사항 및 공격 표면
### eBPF 위협 모델 내
| 공격 | 상태 | 참고 |
|---|---|---|
| **링 버퍼 소비자 포인터 조작** | **감지됨** (SILENT) | `max(sched_last)`가 오래됨 → SILENT. `SCHED_HEARTBEAT`는 여전히 진행됨(프로그램 실행) → 잘못된 TAMPER 없음. |
| **Perf 이벤트 구조체 DKOM**(`state` 필드 0) | **감지됨** (SILENT) | NMI 중단 → 하트비트 이벤트 없음 → NMI 채널에서 SILENT |
| **`task_struct` 필드 스푸핑**(pid/tgid 덮어쓰기) | **부분적** (DUPE) | 스푸핑된 TGID가 다른 `start_time_ns`를 가진 기존 레코드와 일치하면 DUPE가 발생. `start_time_ns`도 스푸핑되면 통과함. |
| **eBPF 프로그램 분리**(funcs 배열에서 제거) | **감지됨** (TAMPER) | NMI 하트비트 정지 → TAMPER |
| **바이트코드 인플레이스 패치** | **감지되지 않음** | 임의 커널 쓰기(LKM 수준) 필요 — eBPF 위협 모델 외부 |
| **`bpf_probe_read_kernel`의 타이밍 부채널** | **이론적 위험** | 힙에 대한 이진 검색으로 약 30번의 프로브에서 `.bss`를 찾을 수 있음. [§6](#6-verifier-bounded-address-secrecy) 제한 사항 참조. |
### eBPF 위협 모델 외부(인정됨, 해결되지 않음)
| 공격 | 상태 | 참고 |
|---|---|---|
| **IDT/NMI 핸들러 후크** | **활성 폭탄** | 구조적으로 완전한 우회. 거의 확실한 커널 패닉. |
| **이중 채널 억제** | **활성 폭탄** | sched + NMI를 동시에 억제. NMI 후킹의 불안정성을 상속받음. |
| **검증기 익스플로잇** | **비목표** | 임의 커널 코드 실행으로 탈출. 별도의 위협 클래스. |
| **ptrace / `/proc/pid/mem`** | **근본적 한계** | 동등 권한 메모리 읽기. 오프호스트 로그 전송만 도움이 됨. |
| **소비자 포인터 + 하트비트 패치** | **LKM 수준** | 공격자가 임의 커널 쓰기를 가지고 있다면 소비자 포인터를 진행시키고 가짜 하트비트를 쓸 수 있음. 그러나 임의 커널 쓰기 = LKM 수준 = 위협 모델 외부. |
---
## 13. 빌드 및 실행
### 전제 조건
- `CONFIG_DEBUG_INFO_BTF=y`가 활성화된 Linux 커널 >= 5.15(LSM 후크용)
- 모듈 차단: `CONFIG_BPF_LSM=y` 및 커널 cmdline에 `lsm=bpf`
- TPM 2.0 칩 + `tpm2-tss` 라이브러리(선택 사항, 눈에 띄는 경고와 함께 대체됨)
- Nightly Rust 툴체인
> **참고:** `generate-vmlinux` 단계는 **더 이상 필요하지 않습니다.** eBPF 프로그램은 기존 트레이스포인트 오프셋과 `.bss` 전역 변수를 사용합니다. CO-RE/BTF 구조체 탐색이 필요하지 않습니다. xtask `generate-vmlinux` 명령은 향후 사용을 위해 유지되지만 빌드 파이프라인의 일부는 아닙니다.
BPF LSM이 활성화되었는지 확인: `cat /sys/kernel/security/lsm`에 `bpf`가 포함되어야 합니다.
### 설정```shell
make install-deps # system packages + nightly Rust (pacman/apt/dnf auto-detected)
make install-tools # bpf-linker
make build # compiles eBPF + userspace (no vmlinux generation needed)
make run # sudo ./target/release/spica
### initramfs에 설치 (초기 부트 보호)```shell
sudo make install # spica install — auto-detects Debian (initramfs-tools) or Fedora (dracut)
sudo insmod some_module.ko # should fail with EPERM (gate locked) cat /sys/kernel/security/lsm # should contain 'bpf' ls /sys/fs/bpf/spica_watchdog # exists if previous run was killed ungracefully
### 개발 (macOS 또는 Linux)
eBPF 프로그램은 macOS에서 실행할 수 없습니다. `cargo check` 및 탐지 로직에 대한 단위 테스트는 작동합니다. 전체 런타임 검증은 Linux가 필요합니다.```shell
make check # cargo check for workspace
make check-ebpf # cargo check for the eBPF crate (bpfel-unknown-none)
make test # unit tests for detection FSM, key derivation, obfuscation
GetRandom 전용 TPM 사용을 완전한 하드웨어 루트 오브 트러스트로 대체합니다. §9 참조.bpf 시스템 콜에 BPF LSM 훅을 걸어 SPiCa의 내부 상태에 대한 맵 열거를 비-SPiCa 프로세스로부터 차단합니다. §7 참조.BPF_PROG_LOAD 속도 제한 — LSM 게이트를 확장하여 비-SPiCa 프로세스의 BPF 프로그램 로드를 속도 제한함으로써 .bss 발견에 대한 타이밍 부채널 공격 위험을 완화합니다. §6 제한 사항 참조.siphasher 의존성이 있음)으로 교체합니다.SPiCa 엔진 라이선스: MIT OR Apache-2.0 (워크스페이스). eBPF 프로그램은 _license 정적 변수를 통해 GPL 라이선스를 내보냅니다. 이는 GPL 라이선스 헬퍼를 사용하는 eBPF 프로그램에 대한 커널 요구 사항이며, 로드된 eBPF 바이트코드에만 적용되고 사용자 공간 바이너리에는 적용되지 않습니다.
캐릭터 저작권 표시: "하츠네 미쿠" 및 관련 캐릭터 아트워크는 Crypton Future Media, INC. (www.piapro.net)의 저작권 재산입니다. 이 프로젝트는 Crypton Future Media와 제휴하지 않은 독립적인 비상업적 연구 도구입니다. 캐릭터는 Piapro 캐릭터 라이선스(PCL)에 따라 사용됩니다.
SPiCa 프로젝트 이름은 Toku-P의 원곡에서 영감을 받았습니다.
| 제약 | 적에 대한 영향 |
|---|
| 제한된 루프 (프로그램당 ~100만 명령어) | 모든 커널 메모리를 스캔할 수 없음 |
| 포인터 안전성 (직접 역참조하려면 증명 가능한 유효하고 제한된 대상 필요) | 임의 포인터를 따라갈 수 없음 |
| 제한된 헬퍼 (프로그램 유형별 허용 목록) | kmalloc, vmalloc 또는 커널 할당 함수를 호출할 수 없음 |
임의 커널 쓰기 불가 (bpf_probe_write_kernel 크게 제한됨) | task_struct, tracepoint funcs 배열 또는 BPF 프로그램 바이트코드를 수정할 수 없음 |
| 맵 접근은 로드된 참조로 범위 제한됨 | 다른 프로그램에 속한 맵에 접근 불가 (ID로 열거되지 않는 한 — §7 참조) |
| 채널 | 메커니즘 | 무엇이 억제하는가 | 억제 비용 |
|---|
/proc | procfs 디렉터리 열거 | DKOM (프로세스를 작업 목록에서 제거) | 낮음 — 잘 알려진 루트킷 기술 |
sched_switch | 스케줄러의 eBPF tracepoint | funcs 배열에서 프로그램 분리; 컨텍스트 필터링 | 중간 — funcs 배열에 대한 커널 쓰기 필요 |
| NMI | 하드웨어 성능 카운터 인터럽트 → eBPF perf 이벤트 | IDT/NMI 핸들러 후킹 | 거의 확실한 커널 패닉 — 하드웨어 인터럽트, 마스킹 불가 |
| Alert | Condition | What it detects |
|---|
[DKOM] | 커널이 스케줄링한 TGID가 /proc에 2초 이상 없음 | 직접 커널 객체 조작 — 사용자 공간 열거에서 프로세스 숨김 |
[GHOST] | TGID가 /proc에는 있지만 sched_switch에서 5초 이상 보이지 않음 | /proc 스푸핑 — DKOM 검사를 우회하기 위해 가짜 항목 삽입 |
[TAMPER] | NMI가 sched_switch 하트비트 동결 감지 (event_type=1) | Tracepoint 분리, 억제, 또는 BTF/연결 실패 |
[SILENT] | 전체 레지스트리에서 max(sched_last)가 오래되었고 /proc이 비어 있지 않음; 또는 NMI 하트비트가 5초 이상 없음 | 채널 수준 관찰 손실 — sched 또는 NMI 채널이 죽음 |
[DUPE] | 동일한 TGID, 이벤트 간 다른 start_time_ns | task_struct 필드 스푸핑 — 루트킷이 tgid를 패치하여 합법적인 프로세스로 위장 |
[HOOK] | 원시 getdents64로는 보이지만 libc readdir에는 없는 PID | LD_PRELOAD 루트킷 — 사용자 공간 라이브러리 가로채기로 프로세스를 ps, ls 및 기타 도구에서 숨김 (Symbiote, JynxKit, Azazel, Medusa/OrBit) |
[WATCHDOG] | 시작 시 /sys/fs/bpf/spica_watchdog 핀이 존재함 | 이전 인스턴스가 비정상 종료됨 (SIGKILL, OOM, 충돌) |
[LKM-ALLOW] | 게이트가 열린 상태(부트 윈도우)에서 READING_MODULE 가로채기 | 감사 기록: 게이트 잠기기 전에 모듈 로드됨 |
[LKM-DENY] | 게이트가 잠긴 상태에서 READING_MODULE 가로채기 | 초기화 후 insmod/modprobe 차단 |
| Global | Purpose | Written by |
|---|
BASE_KEY | XOR obfuscation key | userspace at load time |
SPICA_PID | SPiCa's own TGID (watchdog) | userspace at load time |
SCHED_HEARTBEAT | sched_switch liveness timestamp | sched_switch program on each invocation |
NMI_LAST_HB | NMI's record of last sched heartbeat | NMI program on each check |
NMI_FIRST_TICK | First NMI invocation ktime (grace period) | NMI program on first invocation |
NMI_LAST_EMIT | Throttle: last event emission ktime | NMI program on each emission |
| 전역 변수 | 용도 | 작성 주체 |
|---|
BASE_KEY | XOR 난독화 키 | 로드 시 userspace |
SPICA_PID | SPiCa 자체 TGID (감시) | 로드 시 userspace |
SCHED_HEARTBEAT | sched_switch 활성 상태 타임스탬프 | 각 호출 시 sched_switch 프로그램 |
NMI_LAST_HB | 마지막 sched 하트비트의 NMI 기록 | 각 확인 시 NMI 프로그램 |
NMI_FIRST_TICK | 첫 번째 NMI 호출 ktime (유예 기간) | 첫 번째 호출 시 NMI 프로그램 |
NMI_LAST_EMIT | 스로틀: 마지막 이벤트 방출 ktime | 각 방출 시 NMI 프로그램 |
| PCR | What it measures | Stability |
|---|
| PCR 4 | Bootloader code + kernel image (GRUB measures both) | Changes on kernel update |
| PCR 5 | GPT/MBR partition table, boot configuration | Stable across updates |
| PCR 7 | Secure Boot policy (SI policy, MOK, db/dbx) | Stable across kernel updates |
| PCR 8 | Kernel command line (systemd-stub measurements) | Stable unless cmdline changes |
| PCR 9 | Initramfs (GRUB measures initrd here) | Changes on initramfs update |
| PCR 10 | IMA measurement list | Changes as executables are measured |
| PCR | 측정 대상 | 안정성 |
|---|
| PCR 4 | 부트로더 코드 + 커널 이미지(GRUB이 둘 다 측정) | 커널 업데이트 시 변경 |
| PCR 5 | GPT/MBR 파티션 테이블, 부트 구성 | 업데이트 전반에 걸쳐 안정적 |
| PCR 7 | 시큐어 부트 정책(SI 정책, MOK, db/dbx) | 커널 업데이트 전반에 걸쳐 안정적 |
| PCR 8 | 커널 명령줄(systemd-stub 측정) | cmdline이 변경되지 않으면 안정적 |
| PCR 9 | Initramfs(GRUB이 initrd를 여기서 측정) | initramfs 업데이트 시 변경 |
| PCR 10 | IMA 측정 목록 | 실행 파일이 측정됨에 따라 변경 |
The trap: If an attacker has modified the kernel (PCR 4 mismatch), swapped the initramfs (PCR 9 mismatch), or altered the Secure Boot policy (PCR 7 mismatch), the PCR hashes diverge. The TPM refuses to unseal, and SPiCa fails safe — it refuses to start rather than running blind with a compromised key.
덫: 공격자가 커널을 수정했거나(PCR 4 불일치), initramfs를 교체했거나(PCR 9 불일치), 시큐어 부트 정책을 변경한 경우(PCR 7 불일치) PCR 해시가 달라집니다. TPM은 언실링을 거부하고, SPiCa는 안전하게 실패합니다 — 손상된 키로 맹목적으로 실행되지 않고 시작을 거부합니다.
| Policy | Sealed against | Strength | Operational cost |
|---|
| Strong | PCR 4 + 7 + 9 + 10 | Catches kernel, initramfs, Secure Boot, and IMA changes | Re-seal after every kernel/initramfs update |
| Balanced | PCR 7 + 10 | Catches Secure Boot and IMA appraisal changes; stable across kernel updates | Re-seal only on Secure Boot policy or IMA policy changes |
| Minimal | PCR 7 only | Catches Secure Boot state changes only | Very stable; weakest binding |
| 정책 | 실링 대상 | 강도 | 운영 비용 |
|---|
| 강력함 | PCR 4 + 7 + 9 + 10 | 커널, initramfs, 시큐어 부트 및 IMA 변경 감지 | 모든 커널/initramfs 업데이트 후 재실링 필요 |
| 균형 | PCR 7 + 10 | 시큐어 부트 및 IMA 평가 변경 감지; 커널 업데이트 전반에 걸쳐 안정적 | 시큐어 부트 정책 또는 IMA 정책 변경 시에만 재실링 |
| 최소 | PCR 7만 | 시큐어 부트 상태 변경만 감지 | 매우 안정적; 가장 약한 바인딩 |
| 용어 | 정의 |
|---|
| BPF | 버클리 패킷 필터 — 샌드박스 처리된 프로그램을 위한 커널 내 실행 엔진. 최신 BPF(eBPF)는 패킷을 넘어 추적, 보안 및 네트워킹으로 확장됩니다. |
| BTF | BPF 유형 형식 — CO-RE(한 번 컴파일, 모든 곳에서 실행) 프로그램이 커널 구조체를 이식 가능하게 탐색할 수 있도록 하는 커널 디버그 정보입니다. |
| CO-RE | 한 번 컴파일, 모든 곳에서 실행 — BTF를 사용하여 다른 커널 버전에 적응하는 이식 가능한 프로그램을 작성하는 BPF 기술입니다. |
| DKOM | 직접 커널 객체 조작 — 프로세스를 커널의 연결 리스트에서 제거하여 /proc에서 숨기는 루트킷 기술입니다. |
| fmod_ret | BPF 트램펄린을 통해 커널 함수의 반환 값을 수정하는 BPF 프로그램 유형입니다. |
| freplace | BPF 프로그램 확장 — 다른 BPF 프로그램의 특정 (하위)함수에 연결하여 실행을 가로챕니다. |
| funcs 배열 | 커널 트레이스포인트 구조체에 있는 함수 포인터 배열로, 트레이스포인트가 발생할 때 호출할 콜백 함수(BPF 프로그램 포함)를 보유합니다. |
| IDT | 인터럽트 디스크립터 테이블 — 인터럽트 벡터를 핸들러 함수에 매핑하는 CPU 구조체입니다. NMI 진입점을 후킹하려면 IDT를 패치해야 합니다. |
| kASLR | 커널 주소 공간 레이아웃 무작위화 — 부팅 시마다 커널 코드/데이터 주소를 무작위화하여 악용을 방해합니다. |
| NMI | 마스크 불가능 인터럽트 — 소프트웨어(cli)로 비활성화할 수 없는 하드웨어 인터럽트입니다. perf 카운터가 하드웨어 수준 관찰에 사용합니다. |
| PCR | 플랫폼 구성 레지스터 — 부팅 구성 요소의 측정값(해시)을 축적하는 TPM 레지스터입니다. 재설정할 수 없으며(재부팅 제외), 확장만 가능합니다. |
| PMU | 성능 모니터링 유닛 — CPU의 하드웨어 카운터로, 이벤트(사이클, 캐시 미스 등)를 계수하고 임계값에서 인터럽트(NMI)를 트리거할 수 있습니다. |
| TPM | 신뢰할 수 있는 플랫폼 모듈 — 하드웨어 기반 키 저장소, 난수 생성 및 측정 증명을 제공하는 암호화 보조 프로세서입니다. |
| Verifier | BPF 검증기 — 로드 전에 BPF 프로그램을 정적으로 분석하여 종료하고 안전하지 않은 메모리에 액세스하지 않도록 보장하는 커널 구성 요소입니다. |