litemars/EDRHookDetector
GitHub: litemars/EDRHookDetector
一款多架构 Linux Hook 检测器,通过审计用户态和内核态多种拦截机制来发现 EDR 产品或 Rootkit 的 hook 行为。
Stars: 4 | Forks: 0
# 多架构 EDR 与 Rootkit Hook 检测器
这是一个单一的 Linux 二进制程序,用于审计实时系统中最常见的
EDR 产品(或 rootkit)拦截活动的方式:
| 层级 | 检查内容 | 真值来源 |
|---|---|---|
| 用户态内联补丁 | `libc`, `libssl`, `libcrypto`, `libpthread`, `libdl`, `libpam`, `libaudit` 中一组精选的热点函数 | 磁盘上的 `.so` 字节对比 `/proc/PID/mem` |
| GOT / PLT 劫持 | 同样精选的函数,但通过每个已加载模块的 GOT 导入 —— 即使函数体未被修改也能捕获 | ELF `JUMP_SLOT`/`GLOB_DAT` 重定位对比 `/proc/PID/mem` 中的实时槽值 |
| 环境变量预加载 | `LD_PRELOAD`, `/etc/ld.so.preload` | 文件系统 / 环境变量 |
| eBPF 程序 | 每个具备 hook 能力的 BPF 程序 (KPROBE / TRACING / LSM / TRACEPOINT / PERF_EVENT / SYSCALL) | `bpf()` syscall, BTF, `BPF_TASK_FD_QUERY`, `BPF_LINK_GET_NEXT_ID` |
| 非 BPF kprobes | 每个 kprobe/kretprobe,包括来自 SystemTap 或自定义 LKM 的 | `/sys/kernel/debug/kprobes/list` |
| uprobes | 每个 uprobe,无论其附加方式如何 | `tracefs/uprobe_events` |
| ftrace hook | 通过带有自定义 trampoline 的 ftrace hook 的函数(rootkit 滥用 ftrace)+ 全局 `current_tracer` 状态 | `tracefs/enabled_functions`, `tracefs/current_tracer` |
| VDSO 篡改 | 内容偏离同架构大多数进程的特定进程 `[vdso]` 页(原地 vdso 补丁没有磁盘基线) | 对 `/proc/PID/mem` 中的 `[vdso]` 字节进行跨进程比较 |
| 活动的 LSM | 已加载 LSM 的列表和顺序;未知名称会被标记 | `/sys/kernel/security/lsm` |
| 受污染的内核模块 | 带有 `O` (树外)、`E` (未签名) 或 `F` (强制加载) 标志的已加载模块 | `/proc/modules` |
支持 **ARM64 (AArch64)** 和 **x86-64** 二进制文件。架构是通过每个
ELF 的 `e_machine` 按库检测的,因此单个扫描器二进制文件可以
处理混合环境。CLI 输出人类可读的文本或
单个有效的 JSON 文档以供机器使用。
## 快速开始
```
make
sudo ./edr_hooks_check # full system scan
./edr_hooks_check --self # current process only, no root needed
sudo ./edr_hooks_check --json # machine-readable output
```
### CLI 选项
| 标志 | 含义 |
|---|---|
| `-p, --pid ` | 仅扫描给定的进程 |
| `-l, --lib ` | 将用户态检查限制在特定的库 |
| `-s, --self` | 自扫描(仅当前 PID);无需 root 权限 |
| `-v, --verbose` | 按来源列出详细信息(kprobe 地址、函数名等)。使用两次获取更多细节 |
| `-x, --hexdump` | 显示每个检测到的用户态 hook 的磁盘与内存指令字节对比 |
| `-j, --json` | 将整个报告输出为单个 JSON 对象 |
| `-h, --help` | 显示用法并退出 |
### 退出代码
- `0` — 未检测到用户态 hook 和内核侧 hook 信号
- `1` — 至少检测到以下之一:用户态补丁函数、GOT/PLT 劫持、eBPF hook 程序、kprobe、uprobe、ftrace trampoline 或 VDSO 异常
(未知的 LSM 和受污染的模块会被报告,但它们本身**不会**将退出代码设为 `1` —— 它们在大多数实际系统中仅供参考。)
## 检测机制
### 用户态内联补丁
对于每个被扫描进程加载的受监控库,扫描器会:
1. 读取磁盘上的 `.so` 并解析 ELF 动态符号表,以定位每个受监控函数的虚拟地址。
2. 分别从文件(`pread`)和 `/proc/PID/mem` 中的 `base_addr + (vaddr − preferred_base)` 处读取固定大小的窗口。
- **ARM64**: 8 条定长指令(32 字节)。
- **x86-64 / i386**: 64 字节(≈10–15 条变长指令),由内置的长度解码器解码。
3. 如果字节不同,则运行特定架构的评分启发式算法,过滤已知的良性模式 —— PLT stub、syscall trampoline、尾调用、函数结语、IFUNC 分发、≤ 32 字节的轻量级包装器 —— 并将其余的分类为 **低 / 中 / 高** 置信度。评分系统还能识别以无相对分支开头的全范围 trampoline:x86-64 上的 `push imm; ret` 和 `mov r64, imm64; jmp r64`,以及 ARM64 上的 `movz/movk…; br Xn`。
### GOT / PLT 劫持
内联补丁并不是拦截调用的唯一方式:覆盖全局偏移表(GOT)条目会将每个调用点通过 PLT 重新路由,而不会触碰目标函数的任何一个字节,因此上面的内联检查无法察觉。对于每个加载的模块(主可执行文件和每个 `.so`),扫描器会:
1. 解析模块的 `JUMP_SLOT` / `GLOB_DAT` 重定位(x86-64/ARM64 上为 RELA,i386 上为 REL),并保留符号为受监控函数的那些重定位。
2. 计算每个 GOT slot 的运行时地址(`base + (r_offset − preferred_base)`),并从 `/proc/PID/mem` 读取实时指针。
3. 当该指针**没有**落入合法的可执行映射中时,标记该条目。延迟绑定的槽指向模块自身的 PLT,而真正的插入式拦截器(包括 `LD_PRELOAD`)会解析为磁盘上的 `.so` 文件 —— 这两者都不会触发检查。该信号专门是指向**匿名 / 注入**内存(或未映射地址)的指针,这是 ptrace 注入 trampoline 的特征。
### eBPF 内核 hook
系统级运行,直接与 `bpf()` 通信。对于每个已加载的 BPF 程序:
1. `BPF_PROG_GET_NEXT_ID` 循环直到遇到 `ENOENT`。
2. `BPF_PROG_GET_FD_BY_ID` + `BPF_OBJ_GET_INFO_BY_FD` 检索类型、名称、UID 和 `attach_btf_id`。
3. 附加目标通过三个独立的路径进行解析:
- **vmlinux BTF**(对 ID 1 执行 `BPF_BTF_GET_FD_BY_ID`,解析类型区段)—— 这是 fentry/fexit/LSM 所需的,它们的目标存储为 BTF 类型 ID。
- **`BPF_TASK_FD_QUERY`** 遍历 `/proc/*/fd` —— 捕获 BCC 风格的 perf_event 附加,此时程序可以通过打开的文件描述符访问,但没有 `bpf_link` 对象。
- **`BPF_LINK_GET_NEXT_ID`** + `BPF_OBJ_GET_INFO_BY_FD` —— 捕获被固定的 link(包括具有具体附加点的 raw_tracepoint / perf_event 变体)。
仅报告具备 hook 能力的类型:`KPROBE`, `TRACEPOINT`, `RAW_TRACEPOINT`, `RAW_TP_WRITABLE`, `PERF_EVENT`, `TRACING`, `LSM`, `SYSCALL`。纯网络类型(XDP, SOCKET_FILTER, SCHED_CLS, …)被有意忽略。
### 非 BPF kprobes
`/sys/kernel/debug/kprobes/list` 枚举了内核中每个活动的 kprobe/kretprobe —— 包括由非 BPF 工具(如 SystemTap)或自定义内核模块注册的。每一行都被解析为 `{address, type (k/r/p), symbol+offset, active, optimized, ftrace_based}`。需要 root 权限 + 挂载 debugfs。
### uprobes
`tracefs/uprobe_events` 列出了每个 uprobe,无论它是如何被附加的(BPF、perf、手动写入)。在 `/sys/kernel/tracing/`(现代)和 `/sys/kernel/debug/tracing/`(传统 debugfs 挂载)下都进行了尝试。
### ftrace hook
`tracefs/enabled_functions` 列出了当前通过 ftrace 被 hook 的每个内核函数。包含 `tramp:` 的行表示真正的代码重定向(rootkit 滥用 ftrace 是一种流行的 LKM hooking 技术,因为它绕过了直接的函数修补);没有包含 `tramp:` 的行则是被动的 tracer。只有带有 trampoline 的条目才会计入 hook 总数。
`tracefs/current_tracer` 会单独读取 —— 如果它是 `nop` 以外的任何内容,扫描器就会发出一个警告条目,因为在生产系统上开启全局函数追踪是不寻常的。
### VDSO 篡改
内核将相同的、位置无关的 `[vdso]` 镜像(`gettimeofday`, `clock_gettime`, `getcpu`)映射到每个进程中,因此其字节在给定架构的所有进程中都是相同的。修补某个进程 vdso 的 rootkit 会破坏写时复制 (COW),并使该进程拥有一个私有的、被修改过的页面,该页面**没有磁盘基线**可供对比。扫描器通过 `/proc/PID/mem` 读取每个可读的 `[vdso]`,根据 `(length, FNV-1a hash)` 对它们进行分组,并标记任何在**相同长度**(相同长度 ⇒ 相同架构,因此内容不同即为篡改信号;长度不同仅仅是 32 位与 64 位进程的区别,属于预期情况)的 vdso 中占绝对少数的任何变体。
局限性:在 COW 之前应用的*全局*补丁会同样地改变每个进程,从而不会留下任何少数派供标记 —— 检测该情况需要可信的基线,且超出了本文的讨论范围。CRIU 恢复的进程可以合理地拥有一个存在差异的 vdso 代理,因此应将命中视为“需要调查”,而不是确凿证据。
### 活动的 LSM
`/sys/kernel/security/lsm` 是一个以逗号分隔的已激活 LSM 列表(按加载顺序排列)。已知的良好名称会被静默接受;其他任何内容都会被标记。在 JSON 模式下总是会输出完整的列表,以便机器消费者验证顺序。
### 受污染的内核模块
`/proc/modules` 被解析以获取尾部括号内的污染标志。扫描器将每个模块归类为以下内容的某种组合:
- `O` = 树外模块
- `E` = 未签名
- `F` = 强制加载
- `P` = 专有
树外 + 未签名 + 强制加载的模块会被标记为值得调查(恶意的 LKM 几乎总是同时具备这三者)。仅具有专有标志是信息性的(涵盖了合法的供应商驱动程序)。
## 输出
### 文本模式 (使用 `-v`)
```
========================================================
Multi-Arch EDR Hook Detector (ARM64 + x86 / x86-64)
========================================================
[+] No /etc/ld.so.preload
[*] Scanning eBPF kernel hooks...
security_file_open [TRACING] prog=falco_filopen
do_unlinkat [KPROBE] prog=kp_unlink
2 kernel hook(s) found (out of 38 eBPF program(s) seen)
[*] Scanning kprobes...
ffffffff8108a2c0 k __x64_sys_open+0x0 [FTRACE]
ffffffff810b5450 r __x64_sys_kill+0x0
2 kprobe(s) registered (2 active)
[*] Scanning uprobes...
[+] No uprobes registered
[*] Scanning ftrace hooks...
14 ftrace hook(s), 0 with custom trampoline — run with -v for the list
[*] Active LSMs...
capability
yama
apparmor
bpf
[*] Scanning kernel modules...
nvidia taint=[OE] out-of-tree UNSIGNED
vboxdrv taint=[OE] out-of-tree UNSIGNED
187 module(s) loaded, 2 flagged (out-of-tree / unsigned / forced)
[*] Checking VDSO consistency...
[+] VDSO consistent across 42 process(es) (1 variant)
[*] Scanning GOT/PLT for hijacks...
[+] No GOT/PLT hijacks detected
Scanning processes...
[!] PID 1234 (sshd): 2 hook(s)
========================================================
SUMMARY
========================================================
Processes scanned: 42
Processes w/ userland hooks: 1
Userspace hooks: 2
GOT/PLT hijacks: 0
eBPF kernel hooks: 2
Active kprobes: 2
uprobes: 0
ftrace trampoline hooks: 0
Unknown LSMs: 0
Out-of-tree/unsigned mods: 2
VDSO anomalies: 0
--------------------------------------------------------
Total signals: 8
[!] Suspicious activity found — investigate above
Run with -v for the full per-source listings
Run with -x to see userland instruction hexdumps
========================================================
```
标签:Docker镜像, EDR, Hakrawler, Hook检测, Linux内核, 动态分析, 客户端加密, 脆弱性评估