0xKirisame/SPiCa

GitHub: 0xKirisame/SPiCa

基于 eBPF 的双通道交叉视图 rootkit 检测引擎,通过 sched_switch 跟踪点和 NMI 硬件中断两个独立观测源对进程空间进行差异分析,识别 DKOM、eBPF 挂钩、/proc 伪造等内核级隐藏行为。

Stars: 98 | Forks: 4

# SPiCa **系统进程完整性与交叉视图分析**

SPiCa

SPiCa 是一个基于 eBPF 的高性能 rootkit 检测引擎,使用 Rust 编写。这个名字同时来源于两个地方:初音未来的歌曲 *SPiCa*,以及它所命名的真实恒星——角宿一(即室女座 α 星,室女座中最亮的恒星)。角宿一并不是一颗单一的恒星。它是一个**分光双星**:两颗巨大的恒星在紧密的相互轨道上运行,由于彼此的引力作用被拉成蛋形,每四天完成一次完整的公转,肉眼无法将它们分辨为两颗独立的星体。 SPiCa 检测器正是基于相同的原理构建的。在架构上,它是一个**双星系统**:两个独立的观测通道围绕相同的进程空间运行,每个通道都锚定于不同的物理机制,形成一个无法通过攻击单一通道来使其静默的检测系统。 SPiCa 通过从 CPU 执行事件和直接读取内核内存 (BTF/CO-RE) 来建立基础事实 (ground truth),从而强制执行**内核主权**,并故意绕过那些可能被 rootkit 挂钩 (hook) 的辅助函数。 ## 架构 SPiCa 维护两个独立的观测通道和一个用户态差异引擎: ### 通道 1 — BTF 跟踪点 (sched_switch) 附加到内核 `sched_switch` BTF 跟踪点的 eBPF 程序会在每次将进程调度到 CPU 上时触发。SPiCa 并没有使用 `bpf_get_current_pid_tgid()`(它返回的是*即将让出 CPU 的*任务,并且可以被挂钩),而是通过 CO-RE (`bpf_probe_read_kernel`) 直接读取传入的 `task_struct *next` 指针,从内核内存中提取 `pid`、`tgid` 和 `comm`。事件通过 RingBuf 被推送到用户态。 ### 通道 2 — NMI Perf 事件 (硬件 CPU 周期计数器) 第二个 eBPF 程序由硬件 PMU 周期计数器驱动的**不可屏蔽中断** (NMI) 触发,独立附加到每个逻辑 CPU。NMI 无法通过软件中的 `cli`/`sti` 进行屏蔽——压制此通道需要重新编程特定于模型的 PMU 寄存器(一种硬件级别的特权操作)。因此,该通道能够抵抗那些击败商业 EDR 产品的软件挂钩攻击。 ### 混淆层 — 编译时 Per-CPU PID 掩码 诸如 Singularity 之类的 rootkit 会挂钩 `bpf_ringbuf_submit` 并检查传出的 `ProcessInfo` 结构体,丢弃其 `pid`/`tgid` 出现在隐藏 PID 位图中的事件。SPiCa 通过在写入任何一个环形缓冲区**之前**混淆 PID 值来挫败这一点。 在编译时,构建脚本从 `/dev/urandom` 中提取一个 64 位的 `BASE_KEY`,并将其作为常量直接烘焙到 eBPF 字节码中——没有使用 BPF map,在运行时没有任何可供枚举的内容。两个 eBPF 程序都将 per-CPU 密钥派生为 `BASE_KEY ^ cpu_id`,然后在构造 `ProcessInfo` 之前,将 `pid` 与低 32 位进行异或,将 `tgid` 与高 32 位进行异或。rootkit 的过滤器接收到的值与其隐藏 PID 位图不匹配,因此会将事件不加过滤地放行。 由于每个 CPU 都带有一个不同的密钥,攻击者必须独立提取每个逻辑 CPU 的密钥,而不是读取单个共享的 map 条目。用户态在任何检测逻辑运行之前,应用逆向异或(使用相同的编译时常量和每个事件中携带的 `cpu` 字段)来恢复真实值。 ### 差异引擎 (用户态) 一个基于 Tokio 的用户态 FSM 读取两个环形缓冲区和 `/proc`,然后交叉关联五个检测信号: ``` graph TD subgraph "Kernel Space (Ring 0)" A[Process Scheduled] -->|Trigger| B(BTF Tracepoint: sched_switch) A -->|NMI × N CPUs| C(Perf Event: CPU Cycles) B -->|pid ^ key_lo, tgid ^ key_hi, cpu| D[(RingBuf: EVENTS_SCHED)] C -->|pid ^ key_lo, tgid ^ key_hi, cpu| E[(RingBuf: EVENTS_NMI)] end subgraph "User Space (Ring 3)" F[SPiCa Engine] -->|Async read + inverse XOR| D F -->|Async read + inverse XOR| E F -->|Read| G["/proc Filesystem"] D -- "sched_seen map" --> H{Differential FSM} E -- "nmi_seen map" --> H G -- "Reported TGIDs" --> H D & E --> K{Liveness Monitor} H -->|Both channels + /proc| I[CLEAN] H -->|/proc absent > 2s| J["[DKOM]"] H -->|NMI seen, sched never| L["[TAMPER]"] H -->|/proc only, no eBPF > 5s| M["[GHOST]"] H -->|start_time mismatch same TGID| O["[DUPE]"] K -->|One channel silent > 10s| N["[SILENT]"] end classDef kernel fill:#f9f2f4,stroke:#d63384,stroke-width:2px; classDef user fill:#e7f5ff,stroke:#0d6efd,stroke-width:2px; classDef logic fill:#fff3cd,stroke:#ffc107,stroke-width:2px; class A,B,C,D,E kernel; class F,G user; class H,I,J,L,M,K,N,O logic; ``` ## 检测逻辑 引擎跨五个警报类别对进程状态进行分类: | 警报 | 条件 | 检测到的规避技术 | |-------|-----------|---------------------------| | `[DKOM]` | 内核调度的 TGID 在 `/proc` 中缺失超过 2 秒 | 直接内核对象操纵 | | `[TAMPER]` | NMI 通道看到 TGID;而 sched_switch 通道**从未**看到过它 | eBPF 跟踪点挂钩 / sched_switch 输出清理 | | `[GHOST]` | TGID 存在于 `/proc` 中,但在超过 5 秒的时间内任何 eBPF 通道都未看到 | `/proc` 伪造(虚假条目以通过 DKOM 检查) | | `[SILENT]` | 在另一个通道处于活动状态时,一个通道超过 10 秒未产生事件 | perf_event 结构体 DKOM、eBPF 程序分离、环形缓冲区消费者指针操纵 | | `[DUPE]` | 两个结构上完全不同的进程报告了相同的 TGID | `task_struct` 字段伪造(rootkit 修补 `tgid` 以冒充合法进程) | 一个 50 毫秒的宽限窗口会在短期进程达到可疑状态之前对其进行过滤。所有针对特定 TGID 的警报都有 30 秒的冷却时间,以防止日志泛洪。 ## 技术栈 * **语言:** Rust (内存安全 + 高性能) * **编排:** Aya (Rust 的 eBPF 库) * **内核插桩:** BTF 跟踪点 (`sched_switch`) + NMI perf 事件 (硬件 CPU 周期,per-CPU) * **内核类型访问:** BTF/CO-RE (对 `task_struct` 执行 `bpf_probe_read_kernel`) * **事件传递:** RingBuf (基于推送,微秒级延迟) * **异步运行时:** Tokio (非阻塞环形缓冲区读取 + 信号处理) ## 前置条件 ### 系统依赖 **Arch Linux:** ``` sudo pacman -S --needed base-devel clang llvm libelf bpf ``` **Debian/Ubuntu:** ``` sudo apt-get update && sudo apt-get install -y build-essential clang llvm libelf-dev linux-tools-common bpftool ``` **Fedora:** ``` sudo dnf install -y clang llvm elfutils-libelf-devel bpftool ``` ### Rust 工具链 1. **Nightly Rust (必需):** `rustup toolchain install nightly --component rust-src && rustup override set nightly` 2. **BPF Linker + aya-tool:** `make install-tools` 或者直接运行 `make install-deps` 来同时处理系统软件包和 Rust nightly 工具链。 ## 构建与运行 **完整设置 (一次性):** ``` make install-deps # system packages + nightly Rust make install-tools # bpf-linker and aya-tool make all # generate-vmlinux → build make run # sudo ./target/release/spica ``` **单独目标:** | 目标 | 命令 | 备注 | |--------|---------|-------| | 系统依赖 | `make install-deps` | 运行一次,需要 root 权限 | | Rust 工具 | `make install-tools` | 运行一次 | | BTF 绑定 | `make generate-vmlinux` | 每次内核更新后运行一次 | | eBPF 探针 | `make build-ebpf` | 仅用于开发/检查——正常构建不需要 | | 完整构建 | `make build` | 生成密钥,一步编译 eBPF 和用户态 | | 完整流水线 | `make all` | generate-vmlinux → build | | 运行检测器 | `make run` | 需要 root 权限 | | 清理 | `make clean` | 移除构建产物 | 运行 `make help` 以查看所有可用目标。 ## 反规避缓解措施 SPiCa 针对那些知道其存在并试图使其失效的 rootkit,实施了多层的主动防御。 ### 编译时密钥 — 没有运行时枚举目标 最初的设计使用一个 `CONFIG` BPF 数组 map 来保存 XOR 混淆密钥。拥有内核读取权限的 rootkit 可以通过一条指令对其调用 `bpf_map_lookup_elem`。现在这个 map 已经不存在了。密钥现在由构建脚本从 `/dev/urandom` 生成,并作为编译时常量直接烘焙到 eBPF 字节码中——没有 map,在运行时没有任何可供枚举的东西。 为了恢复密钥,攻击者必须从内核内存中反汇编已加载的 eBPF 字节码。编译器将 64 位密钥拆分为两个独立的 32 位指令立即数,位于不相邻的偏移量处,且均未标记。低半部分在运行时还会与当前的 `cpu_id` 寄存器结合,因此即使提取出两个立即数,也不能直接得出有效的 per-CPU 密钥。 ### Per-CPU NMI — 强制 N 次密钥提取 以前仅将单个 NMI perf 事件附加到 CPU 0 上。rootkit 可以将隐藏进程固定到任何其他 CPU,并且永远不会出现在 NMI 通道中。SPiCa 现在为每个逻辑 CPU 附加一个独立的 NMI perf 事件。每个 CPU 的密钥是 `BASE_KEY ^ cpu_id`,因此提取了一个 CPU 密钥的攻击者对其他 CPU 的密钥仍然一无所知。 ### 通道活跃度 — 检测静默压制 最干净地绕过双通道模型的攻击方式是不与检测逻辑对抗——它们只是简单地将某个通道关闭: - 将 SPiCa 的 `perf_event` 内核结构体的 `state` 字段清零会静默地禁用 NMI 采样。 - 从跟踪点的 `funcs` 数组中移除 SPiCa 的程序指针会静默地分离 `sched_switch`。 - 提前移动环形缓冲区消费者指针会在用户态读取事件之前将其丢弃。 这三种操作都会产生相同的可观察症状:一个通道变暗,而另一个通道继续触发。SPiCa 在每个计时周期 (tick) 都会检查这一点。如果其中一个通道在另一个通道保持活跃的情况下,超过 10 秒没有产生任何事件,就会触发 `[SILENT]`。当通道恢复时,警报会重置,因此循环攻击在每个周期都会重新触发警报。 ### /proc 伪造 — [GHOST] 检测 DKOM 将进程从 `/proc` 中隐藏。相反的攻击——为调度器中实际不存在的进程注入虚假的 `/proc/{tgid}` 条目——能通过 DKOM 检查,但无法通过 eBPF 检查。如果一个 TGID 出现在 `/proc` 中,但在正常运行 5 秒后,两个 eBPF 通道都从未观察到它,就会触发 `[GHOST]`。 ### task_struct 伪造 — [DUPE] 检测 rootkit 可以覆盖隐藏进程上的 `task_struct->tgid` 以冒充合法进程。两个 eBPF 通道都会读取伪造的值并报告一个已知且良好的 TGID——没有差异,没有 DKOM 警报。 SPiCa 还会在每次观测时读取 `task_struct->start_time`(任务的单调纳秒级诞生时间戳)。对某个 TGID 的首次真实观测会将其 `start_time` 锚定到已见 map 中。如果后续事件报告相同的 TGID 但具有不同的 `start_time`,则说明两个结构上完全不同的进程正在声称拥有相同的身份——此时会立即触发 `[DUPE]`。 如果一个 rootkit 同时修补 `tgid` 和 `start_time` 以完美复制一个合法进程,它将产生两个具有相同指纹的事件,这同样是不可能的,并且也会触发 `[DUPE]`。 ## 已知的剩余攻击面 SPiCa 并未声称自己是不可战胜的。以下已知绕过方法目前尚未得到缓解: - **IDT/NMI 处理程序挂钩:** 修补 NMI 调度路径以抑制特定任务的 eBPF 执行。极高的不稳定性风险——一旦出错几乎必然导致内核崩溃。属于攻击者的最后手段。 - **双通道同时压制:** 同时压制两个通道。继承了 NMI 挂钩的不稳定性,并且需要对两个物理上完全不同的机制发起独立攻击。在架构上是彻底的绕过,在实际中却是一颗随时会爆炸的炸弹。 ## 计划中:spica-network ("1/6 out of gravity") 未来的 `spica-network` 工作区成员将把双星原理应用于网络流量:XDP (NIC 驱动层) 与 TC (网络协议栈层) 数据包吞吐量比较,以捕获仅隐藏在某一层但未隐藏在另一层的 C2 通信。相同的编排器,独立的 crate。 ## 许可证 **SPiCa 引擎许可证:** 该项目的源代码(Rust、eBPF 和 C 组件)采用 [GNU General Public License v2.0](LICENSE) (GPLv2) 授权。 **角色归属:** “Hatsune Miku” 及相关角色艺术作品为 Crypton Future Media, INC. (www.piapro.net) 的版权财产。 该项目是一个独立的非商业研究工具,与 Crypton Future Media 没有官方附属关系。该角色的使用遵循 [Piapro Character License (PCL)](https://piapro.jp/license/pcl/summary) 指南。 SPiCa 项目名称的灵感来源于 Toku-P 的原曲。
标签:0day挖掘, AMSI绕过, BTF, CO-RE, Docker镜像, EDR, NMI, Rust, SPiCa, Vocaloid, 交叉视图分析, 内核安全, 初音未来, 可视化界面, 威胁检测, 子域名枚举, 硬件性能计数器, 系统安全, 网络安全, 网络流量审计, 脆弱性评估, 进程完整性, 通知系统, 防御规避检测, 防病毒, 隐私保护