
eBPFベースのLinuxルートキット検出器。マルチチャンネルクロスビュー分析(sched_switch、NMI、/proc)を使用して、DKOM、トレースポイント改ざん、プロセス隠蔽を検出し、ハードウェアレベルの整合性検証を実施します。
システムプロセスの整合性とクロスビュー分析
「歌うから、輝いて、SPiCa...」
SPiCaはRustで書かれたeBPFベースのLinuxルートキット検出器です。その名前は初音ミクの曲『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)と相互相関させるユーザースペース検出エンジンを実行します。
主要なアーキテクチャ的性質:ルートキットは、3つのチャネルすべてを同時に抑制することはできません。抑制自体が検出可能または不安定になるからです。 NMIの抑制にはIDT(割り込み記述子テーブル)のパッチが必要であり、ほとんどのカーネルでパニックを引き起こします。これが「生きた爆弾」であり、攻撃者が完全な盲目状態に至る唯一の経路は、システムをクラッシュさせる可能性が高いものです。
sched sees it, /proc doesn't → DKOM NMI finds sched's heartbeat stopped → TAMPER sched goes globally silent, /proc != empty → SILENT
各検出クラスは差分判定(differential verdict)である。すなわち2つ以上のチャネル間の不一致に基づく。検出エンジンはレジストリ+/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配列操作の影響を受けない
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 時点では、「current」は 出ていく タスクであり、入ってくるタスクではない。トレースポイント引数は正しい(入ってくる)プロセスIDを提供する。
検出エンジンは単一のモノトニック時間ベースを使用する:SPiCaのプロセス起動以降のナノ秒(run_detection() エントリ以降の Instant::now())。スケジュールイベントが到着すると、イベントハンドラは 呼び出し元が提供する now(プロセスローカルのナノ秒)を保存し、イベントに埋め込まれたeBPFの bpf_ktime_get_ns() 値は使用しない。これにより、カーネルブート時間とプロセスローカル時間が混在した場合に発生する時間ベースの不一致を回避する。このバグは以前のバージョンに存在し、すべての生存性述語を静かに失敗させていた。
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 の観測結果 |
|---|---|---|
| funcs 配列から切り離されたトレースポイント | sched\_switch の実行が停止 | `SCHED_HEARTBEAT` が停止 → TAMPER |
| BTF バグ / アタッチ失敗(サイレント) | sched\_switch が決して実行されない | `SCHED_HEARTBEAT` が 0 のまま → TAMPER(猶予後) |
| スケジューラ関数への `bpf_override_return` | トレースポイントの発火を妨げた場合 | `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 コンテキストで実行され、そこでは kprobe が構造的に信頼できません(カーネルがそれらを延期または抑制します)。チェッカーの実行に対する kprobe ベースの攻撃はハードウェアと戦うことになります。
### NMI イベントのセマンティクス
NMI リングバッファ (`sc_nmi`) は軽量イベントを運びます:
| `event_type` | 意味 | ユーザースペースのアクション |
|---|---|---|
| 0 | ハートビート — NMI 生存、sched\_switch 生存 | `last_nmi_heartbeat` タイムスタンプを更新 |
| 1 | TAMPER — NMI 生存、sched\_switch ハートビートが停止 | 即座に `[TAMPER]` を出力 |
イベントは最大でも1秒に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) が CHANNEL_DEAD_NANOS を超えて古く、かつ /proc が空でない。空でない /proc のガードは生存保証です。動作中のLinuxシステムでは /proc は空ではありません(init、カーネルスレッド、kworker)、そしてスケジューラは決して真にアイドル状態にはなりません(タイマーチック、I/Oウェイター、RCUコールバック)。空でないシステムで max(sched_last) が古いままということは、システムがアイドル状態というわけではなく、チャネルが停止したことを意味します。
NMIチャネル停止: NMIリングバッファからのハートビートイベント(event_type=0)が5秒以上到着しない。これはNMI perfイベントが停止したことを意味します。perfイベントが切り離されたか、PMUが再プログラムされた可能性があります。
両方のチェックには30秒のクールダウンがあり、アラートの氾濫を防ぎます。
[HOOK])SPiCa は毎ティックごとに 2つの独立したメカニズム で /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]`で検出されるルートキット:**
| ルートキット | 隠蔽機構 | 検出 |
|---------|-----------------|----------|
| Symbiote | 寄生型LD_PRELOAD、`readdir`をフック | はい(能動的に隠蔽している場合) |
| JynxKit | LD_PRELOAD、`readdir`による`MAGIC_GID`隠蔽 | はい |
| Azazel | LD_PRELOAD、`readdir` + `stat`フック | はい |
| Medusa/OrBit | LD_PRELOAD、`readdir` + 資格情報窃取 | はい |
**本物のSymbioteで試験済み:** ライブのSymbioteサンプル(SHA256 `f55af21f...`、MalwareBazaar)がUbuntu 24.04 VM上で`/etc/ld.so.preload`経由で展開されました。このサンプルは`readdir`、`readdir64`、`stat`、`fstatat`、`pam_authenticate`、`pcap_loop`、`recvmsg`、`fopen`、`read`、`execve`をフックします。LD_PRELOADフックが能動的にPIDを`readdir`から隠蔽すると、SPiCaは1ティックサイクル(<1秒)以内に`[HOOK]`を発します。Symbioteがロードされているが受動的(能動的に隠蔽していない)場合、SPiCaは誤検出を一切生成しません。
### 猶予ウィンドウ
50ミリ秒の猶予ウィンドウにより、フォーク直後のプロセスにおける誤検出を防止します。チャネルレベルのSILENTチェックには5秒の猶予期間(NMIプログラムの`NMI_FIRST_TICK`参照)があり、起動レイテンシが偽のTAMPERアラートを生成しないようにしています。
---
## 6. 検証器境界アドレス秘匿性
### 定義
**検証器境界アドレス秘匿性**は、eBPF制約のある敵対者モデルの下で成立する計算論的なセキュリティ特性です。それは次のように述べています:
> eBPFプログラムをロードできる(CAP\_BPF)が、BPF検証器により制約される(有界ループ、制限付きヘルパー、任意のカーネル書き込み不可)敵対者を想定すると、BPFプログラムの`.bss`データセクションのカーネルアドレスを発見することは計算上不可能です。
### 4つのメカニズム
この特性は、4つの独立した障壁の合成です:
1. **kASLR** — カーネルアドレス空間レイアウトランダム化は、BPFマップメモリ(`.bss`バッキングマップを含む)が割り当てられるカーネルヒープ領域をランダム化します。正確なアドレスは起動のたびに変わります。
2. **検証器命令上限** — BPF検証器はプログラムあたりの総命令数を約100万に制限し、ループの停止が証明可能であることを要求します。敵対者のスキャンプログラムは`bpf_probe_read_kernel`を介して、ロードあたり最大 N ≈ 10^6 個のアドレスを調査できます。kASLRでランダム化された探索空間を考慮すると、プログラムあたりの成功確率は無視できます。
3. **LSMマップアクセスゲート**([§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`の配置** — `.bss`グローバルは、人間が読める名前を持つ名前付きマップ列挙インターフェースには現れません。内部の`.bss`バッキングマップはlibbpf/ayaによって`.bss`と名付けられますが、その内容(実際のグローバル変数)にアクセスできるのは、マップfdを持っている場合のみです。マップfdは、ロードプロセスのfdテーブルかマップID(ゲートでブロックされる)のいずれかを必要とします。
### 形式的議論構造```
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 はタイミングが観測可能です(フォールトと成功で異なるサイクル数)。ヒープ上のタイミングベースの二分探索は、理論的には線形スキャンよりも効率的です。命令制限はプログラムごとのプローブ数を制限しますが、二分探索はプローブ数を 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
Against the eBPF adversary (who can't call syscalls), `.bss` is hidden. Against userspace root, it's a standard map — fully readable. The LSM gate closes this gap.
### 防御
`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
マップIDはプログラムロード後、LSMフックが有効になる直前に、ユーザースペースローダーによって .bss に書き込まれます(LKMブロッキング用の既存の sc_gate と同じタイミングパターン)。BTFは不要です— cmd と map_id はカーネル構造体ではなく、システムコール引数から取得されます。
外科的処理: SPiCaの特定のマップIDへのアクセスのみをブロックします。他のプロセスのBPFツール(tcpdump、bpftrace、bcc)は自身のマップにアクセスし、影響を受けません。
/proc/pid/mem — 別のルートプロセスがptrace経由でSPiCaのプロセスメモリを直接読み取り、BPFシステムコールを完全にバイパスする可能性があります。これは基本的な制限です:同じ特権レベルのプロセスが /proc/pid/mem 経由でメモリを読み取るのを防ぐことはできません。防御策:オフホストログ転送(攻撃者がローカルメモリを読み取れる場合、ローカルアラートも抑制できます—リモートログのみが役立ちます)。TPM搭載ホストでは、64ビットXOR難読化鍵は tss-esapi クレートを介して TPM2_GetRandom から取得されます( libtss2-esys への直接ライブラリ呼び出し)。鍵はパイプ、サブプロセスのコマンドライン、ディスクを経由しません。SPiCaプロセスの存続期間中、TPMハードウェアとカーネルの揮発性メモリ( .bss バッキングマップ)にのみ存在します。
TPMがないホスト(vTPMのないVM、コンテナ、TPMチップのないマシン)では、SPiCaはビルドスクリプトによって /dev/urandom から生成されたビルドごとのコンパイル時鍵にフォールバックします。フォールバックは起動時に警告バナーで 明示的に通知 されます。
.bss 配置以下の値はeBPFプログラムの .bss セクションに存在し、プログラムがロードされる前に set_global() によってユーザースペースから書き込まれます:
名前付きBPFマップは bpf() システムコールのマップ反復インターフェースを介して列挙可能です(LSMゲートによってブロックされます— §7 を参照)。 .bss グローバル変数は同じ内部マップを介してアクセスされますが、個別に名前付けも固定もできません。
すべての ProcessInfo フィールドは、リングバッファに書き込まれる前に64ビットの BASE_KEY でXOR折りたたみされます。 event_type フィールドは意図的に難読化されないまま残され、ユーザースペースは残りを復号化する前のセンチネルとして読み取ることができます。
これは読み取り漏洩に対する難読化であり、有能な敵対者に対する暗号化ではありません。 8バイトの繰り返し鍵によるXORは、既知平文に対して構造的に脆弱です:予測可能な comm 値("bash"、"systemd"、"kthreadd")を暗号文とXORすると、鍵バイトが直接回復されます。この防御はeBPF敵対者(リングバッファを簡単に読み取れない— .bss を保護するのと同じ能力のギャップが必要)には適切ですが、バイナリアクセス+リングバッファ傍受能力を持つ敵対者には適していません。
ステータス: 設計段階。まだ実装されていません。このセクションは研究論文のための目標アーキテクチャを文書化しています。
注記: PCR割り当ては起動チェーン依存です。GRUBはカーネルをPCR 4に測定します。 systemd-stubはPCR 4とコマンドラインをPCR 8に測定します。シーリングポリシーは対象の起動チェーンと一致する必要があります。
シール(インストール時): 64ビット鍵は TPM2_PolicyPCR セッションを使用して TPM2_Create で期待されるPCR値に対してシールされます。シールされたブロブはディスクに保存されます。これはTPMの内部鍵で暗号化され、指定されたPCRが一致した場合にのみ復号できます。
アンシール(起動初期、initramfs段階): 信頼されていないユーザースペースコードが実行される前に、SPiCaのinitramfsフックが TPM2_Unseal を要求します。現在のPCR値がシールポリシーと一致する場合、TPMは鍵を解放します。
注入: 鍵はプログラムがロードされる前に set_global() を介して .bss に書き込まれます。
トラップ: 攻撃者がカーネルを変更した場合(PCR 4不一致)、initramfsを交換した場合(PCR 9不一致)、またはSecure Bootポリシーを変更した場合(PCR 7不一致)、PCRハッシュが異なります。TPMはアンシールを拒否し、SPiCaはフェイルセーフします— 侵害された鍵で盲目的に実行する代わりに起動を拒否します。
研究論文では、strongポリシーを提示し、制限事項で再シールのトレードオフについて議論します。
2つのメカニズムは相補的です。
.bss を特定または読み取れないことを保証します。どちらも単独では十分ではありません。PCRシーリングは鍵が実行時に漏洩した場合(マップ列挙)には役立ちません。アドレス秘密性はSPiCaが起動する前にシステムが侵害された場合(悪意のあるinitramfs)には役立ちません。
spica_lsm_modblock、マップアクセスゲート)はランタイムでロードされ、どのPCRにも測定されません。ブート後にそれらをデタッチするルートキットはPCRシーリングではキャッチされません。それをキャッチするのはNMIハートビート(検出システム自体が停止していることを検出)です。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は起動時に4つのレイヤーすべてをチェックし、それらのステータスを表示します。
---
## 11. BTFバグインシデント
### 発生したこと
Ubuntu(最新カーネル)でのテスト中、BTFの非互換性により、`sched_switch` トレースポイントプログラムのアタッチは成功したものの、**イベントが1つも発火しませんでした。** `attach()` システムコールは `Ok(())` を返したため、SPiCaは正常に動作を続けましたが、schedリングバッファは空のままでした。検出エンジンにスケジューリングデータがなかったため、検出アラートは1つも発火しませんでした。**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が一度でも発火したか?」や「max(sched_last) が古くなっていないか?」という機構がありませんでした。
さらに、時間ベースの不一致が発見されました:`sched_last` は `bpf_ktime_get_ns()`(カーネル起動ナノ秒)を格納していたのに対し、`evaluate()` は `nanos_since_startup()`(プロセスローカルナノ秒)と比較していました。これらの異なる時間ベースの `wrapping_sub` は巨大な値を生成し、すべての生存性述語を自明に偽にしました。検出ロジックは本番環境で正しく動作したことがありませんでした — BTFバグがイベントの到着を妨げることで、それを隠していたのです。
### 再設計による修正方法
| 問題 | 修正 |
|---|---|
| チャネルレベルの生存確認がない | `evaluate()` は `max(sched_last)` を計算し、`/proc` が空でない場合に古くなったら `[SILENT]` を発火する |
| プロセス単位のTAMPERが成熟しなかった(ジッタリセット) | TAMPERはNMIプログラムからの直接信号(`.bss` ハートビート比較)になり、閾値付きのプロセス単位FSMではなくなった |
| プロセス単位のSILENTがsched停止時に逆転 | 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`フィールドをゼロにする) | **検出済み** (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. ビルドと実行
### 前提条件
- Linuxカーネル >= 5.15 で `CONFIG_DEBUG_INFO_BTF=y` が有効であること(LSMフックのみに必要)
- モジュールブロック用: カーネルコマンドラインに `CONFIG_BPF_LSM=y` と `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 Engine ライセンス: 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関数配列、またはBPFプログラムバイトコードを変更できない |
| マップアクセスはロード済み参照に限定 | 他のプログラムに属するマップにアクセスできない(IDで列挙されない限り — §7参照) |
| チャネル | メカニズム | 抑制方法 | 抑制のコスト |
|---|
/proc | procfsディレクトリ列挙 | DKOM(プロセスをタスクリストから切断) | 低 — よく知られたルートキット手法 |
sched_switch | スケジューラ上のeBPF tracepoint | プログラムをfuncs配列からデタッチ;コンテキストをフィルタリング | 中 — funcs配列へのカーネル書き込みが必要 |
| NMI | ハードウェアパフォーマンスカウンタ割り込み → eBPF perfイベント | IDT/NMIハンドラをフック | ほぼ確実なカーネルパニック — ハードウェア割り込み、マスク不可 |
| Alert | 条件 | 検出内容 |
|---|
[DKOM] | カーネルスケジュール対象のTGIDが /proc から2秒以上不在 | 直接カーネルオブジェクト操作 — ユーザ空間の列挙からプロセスを隠蔽 |
[GHOST] | TGIDが /proc に存在するが、 sched_switch で5秒以上確認されない | /proc スプーフィング — DKOMチェックを回避するための偽エントリ挿入 |
[TAMPER] | NMIが sched_switch のハートビートが停止していることを検出(event_type=1) | トレースポイントが切り離される、抑制される、BTF/アタッチに失敗 |
[SILENT] | レジストリ全体で max(sched_last) が /proc が空でないにもかかわらず古いまま。またはNMIハートビートが5秒以上不在 | チャネルレベルの観測損失 — schedチャネルまたはNMIチャネルが停止 |
[DUPE] | 同じTGID、異なる start_time_ns がイベント間で発生 | task_struct フィールドのスプーフィング — ルートキットが tgid にパッチを当てて正規プロセスになりすます |
[HOOK] | 生の getdents64 で見えるPIDが、libcの readdir では見えない | 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 | 目的 | 書き込み元 |
|---|
BASE_KEY | XOR難読化鍵 | ロード時にユーザースペース |
SPICA_PID | SPiCa自身のTGID(ウォッチドッグ) | ロード時にユーザースペース |
SCHED_HEARTBEAT | sched_switch生存タイムスタンプ | 呼び出しごとに sched_switch プログラム |
NMI_LAST_HB | 最後のschedハートビートのNMI記録 | チェックごとにNMIプログラム |
NMI_FIRST_TICK | 最初のNMI呼び出しktime(猶予期間) | 最初の呼び出し時にNMIプログラム |
NMI_LAST_EMIT | スロットル:最後のイベント発行ktime | 発行ごとにNMIプログラム |
| PCR | 測定内容 | 安定性 |
|---|
| PCR 4 | ブートローダーコード+カーネルイメージ(GRUBが両方を測定) | カーネル更新時に変更 |
| PCR 5 | GPT/MBRパーティションテーブル、起動設定 | 更新全体で安定 |
| PCR 7 | Secure Bootポリシー(SIポリシー、MOK、db/dbx) | カーネル更新全体で安定 |
| PCR 8 | カーネルコマンドライン(systemd-stubによる測定) | cmdlineが変更されない限り安定 |
| PCR 9 | Initramfs(GRUBがここでinitrdを測定) | initramfs更新時に変更 |
| PCR 10 | IMA測定リスト | 実行ファイルが測定されるたびに変更 |
| ポリシー | シール対象 | 強度 | 運用コスト |
|---|
| Strong | PCR 4 + 7 + 9 + 10 | カーネル、initramfs、Secure Boot、およびIMAの変更をキャッチ | カーネル/initramfs更新のたびに再シール |
| Balanced | PCR 7 + 10 | Secure BootとIMA評価の変更をキャッチ;カーネル更新全体で安定 | Secure BootポリシーまたはIMAポリシーの変更時のみ再シール |
| Minimal | PCR 7 only | Secure Bootの状態変更のみをキャッチ | 非常に安定;最も弱い結合 |
| 用語 | 定義 |
|---|
| BPF | Berkeley Packet Filter — サンドボックス化されたプログラム向けのカーネル内実行エンジン。最新の BPF (eBPF) はパケットを超えて、トレーシング、セキュリティ、ネットワークに拡張されています。 |
| BTF | BPF Type Format — CO-RE (Compile Once, Run Everywhere) プログラムがカーネル構造体を移植可能にナビゲートできるようにするカーネルデバッグ情報。 |
| CO-RE | Compile Once, Run Everywhere — BTF を使用して、異なるカーネルバージョンに適応するポータブルなプログラムを作成する BPF 技術。 |
| DKOM | Direct Kernel Object Manipulation — プロセスをカーネルのリンクリストから削除して /proc から隠すルートキット技術。 |
| fmod_ret | BPF プログラムタイプ。BPF トランポリンを介してカーネル関数の戻り値を変更します。 |
| freplace | BPF プログラム拡張 — 別の BPF プログラムの特定の(サブ)関数にアタッチし、その実行をインターセプトします。 |
| funcs 配列 | カーネルトレースポイント構造体内の関数ポインタ配列。トレースポイントが発火したときに呼び出すコールバック関数(BPF プログラムを含む)を保持します。 |
| IDT | 割り込み記述子テーブル — 割り込みベクターをハンドラ関数にマッピングする CPU 構造。NMI エントリをフックするには IDT にパッチを当てる必要があります。 |
| kASLR | Kernel Address Space Layout Randomization — 起動ごとにカーネルコード/データアドレスをランダム化し、悪用を妨げます。 |
| NMI | Non-Maskable Interrupt — ソフトウェア (cli) で無効化できないハードウェア割り込み。パフォーマンスカウンタによるハードウェアレベル観測に使用されます。 |
| PCR | Platform Configuration Register — ブートコンポーネントの測定値(ハッシュ)を累積する TPM レジスタ。リセット不可(再起動を除く)、拡張のみ可能。 |
| PMU | Performance Monitoring Unit — CPU 内のハードウェアカウンタ。イベント(サイクル、キャッシュミスなど)をカウントし、閾値で割り込み(NMI)をトリガーできます。 |
| TPM | Trusted Platform Module — ハードウェアルートの鍵ストレージ、乱数生成、測定アテステーションを提供する暗号コプロセッサ。 |
| Verifier | BPF 検証器 — BPF プログラムをロード前に静的に解析し、停止性と安全でないメモリアクセスがないことを保証するカーネルコンポーネント。 |