# Linux 内核模块分支(.ko / LKM / 运行态内核)
**三个不要**:
① 不要把 `.ko` 当普通 ELF 程序(它是 ET_REL,relocation 才是主线);
② 不要把**磁盘上的代码**当运行态绝对真值(relocation、KASLR、alternatives、jump labels、ftrace/livepatch/paravirt patching、init 段释放都会让两者不同);
③ 怀疑 rootkit 之后,**不要相信由那个内核自己产生的单一枚举结果**。
Linux 比 seL4 更麻烦的地方恰在这里:**大量"看起来像被篡改"的现象其实是内核合法的动态 patching 机制**。不知道 `__ex_table`、jump labels、livepatch、ftrace、BTF、MODVERSIONS、init 释放这些东西,会产生大量事实性误判。
## 一、先判断你拿到的到底是什么
| 目标 | 性质与处理 |
|---|---|
| `vmlinux` | 完整 ELF(最好分析;符号/类型/节齐全) |
| `bzImage` / `Image` / `zImage` | **boot image,不等于可直接分析的 ELF** |
| `*.ko` | 可加载模块,通常 **ET_REL** |
| `*.ko.xz` / `*.ko.zst` | 压缩模块(loader 有解压阶段,不是壳) |
| built-in driver | 根本没有独立运行时 `.ko` |
| livepatch module | 特殊 ELF(专用节,见 §14) |
| eBPF object | 看起来也是 ELF,**模型完全不同** |
| runtime memory image | 运行态(relocation 已应用、init 段已释放) |
- x86 的 **bzImage 本身是压缩 boot image**:拖进 IDA/Ghidra 会看到 boot stub、大量 16-bit code、后面识别不了——**这不是"内核做了混淆"**。正常路径:识别 architecture/boot format → **提取真正的 kernel image** → 得到 vmlinux-style ELF 或 raw Image → 再恢复符号/类型
- 现代内核支持模块解压(`.ko.xz`/`.ko.zst`):loader 流程里 `kernel_read_file → 模块解压 → ELF 处理` 是**独立阶段**;系统报 `failed_decompress` 先查压缩算法/文件损坏,而不是去查符号依赖
## 二、`.ko` 不是共享库:relocation 优先
- `.ko` 的核心性质是 **ELF ET_REL**(不是 ET_EXEC / ET_DYN)——**relocation 是逆向主线**
- stripped 模块的信息价值排序(不是"没符号只能硬读汇编"):
```
relocations / imported symbols → modinfo → BTF/DWARF → 内核 API 调用模式 → 裸反汇编
```
- 例子:反编译器没恢复出函数名,但 `.rela.text` 里 `offset 0x183 / R_X86_64_PLT32 / symbol = pci_register_driver` ——**这条边实际上已经恢复了**。一个 stripped 驱动只要还剩 `pci_register_driver`、`request_irq`、`dma_alloc_coherent`、`copy_from_user`、`misc_register` 这类导入,功能骨架就出来了
## 三、先吃 `.modinfo`(便宜得离谱,却常被跳过)
- 字段:`license` / `author` / `description` / `alias` / `depends` / `vermagic` / `srcversion` / `parm` / `firmware` / `retpoline` / `intree` / `livepatch`
- **`alias` 直接说明设备匹配**:`alias: pci:v00008086d0000....` → 这是给什么设备用的;大量 `alias: usb:...` → 优先沿 `usb_driver` / `probe` / `disconnect` / `usb_control_msg` / `urb` 方向恢复
- **built-in 也有 modinfo**:`modules.builtin.modinfo`(字段带模块名前缀);较新的构建还生成 `modules.builtin.ranges`(`CONFIG_BUILTIN_MODULE_RANGES`)——按 ELF 节给出 built-in 模块的地址偏移范围,配合 `System.map` 可把符号关联回模块名
## 四、"找不到这个 .ko" ≠ 没有这个逻辑
- 可能是 `CONFIG_FOO=y`(链进 vmlinux)而不是 `=m`
- 查证:`modules.builtin` / `modules.builtin.modinfo` / kernel config / `System.map` / `vmlinux`
- built-in 驱动甚至可接受 **kernel command line 参数**(`foo.option=value`)
## 五、磁盘有代码、内存里找不到 → 先想 `__init` 释放
- 初始化完成后内核**释放 init-only 内存**(`__init` / `__initdata` / `.init.text` / `.init.data`);模块 init 成功后同样丢弃相应 initialization memory
- 所以运行态 dump 里找不到 `foo_init` **可能是正常现象**
- 对内存取证尤其重要:rootkit 的关键 setup(`install_hook()` / `patch_table()` / `register_callback()`)若全在 `module_init()` 路径,加载完成后 init code 自身已不存在,**但它制造出的 callback / hook / 改过的指针 / 持久对象仍在**
- 所以运行态逆向要问的不是"恶意 init 函数在哪",而是"**init 最终留下了什么状态改变**"
## 六、模块加载失败:按阶段诊断,不要乱 patch
loader 自己把失败分成阶段(官方 `CONFIG_MODULE_STATS` 的计数器就是按此划分):
```
read(kernel_read_file_from_fd)
→ decompress(vmap 解压缓冲)
→ 早期校验:module_sig_check(签名)/ elf_validity_cache_copy(ELF 校验)
/ early_mod_check(blacklist、节头重写、vermagic、livepatch 要求、已加载)
→ layout_and_allocate(最终落位)
→ 符号解析 → init
```
- 对应计数:`failed_kreads` / `failed_decompress`(官方注释:**除非压缩/解压坏了否则不该出现**)/ `failed_becoming`(读过之后、落位之前失败)/ `failed_load_modules`;debugfs 的 `stats` 文件直接给 "Mods failed on kread / decompress / becoming / load"
- 所以 `modprobe foo → Invalid module format` **不要一看到就去改 vermagic**——可能是 ELF 错、签名错、MODVERSIONS 错、架构错、livepatch 格式错;结合 `dmesg` + `modinfo` + `readelf` 判断
## 七、vermagic 相同 ≠ ABI 相同
- `CONFIG_MODVERSIONS=y` 时,内核把导出符号原型压成 **CRC**;CRC 不匹配即使 `uname -r`/vermagic 看起来一致也会拒绝加载
- 要匹配的是:**release + config + build flags + `Module.symvers` + symbol CRC**(`Module.symvers` 记录全部导出符号与 CRC)
- **格式有两代,别死写旧 parser**:
- basic(`CONFIG_BASIC_MODVERSIONS`):导入符号的 name+CRC 存在模块的 `__versions` 节,符号名上限 64 字节
- extended(`CONFIG_EXTENDED_MODVERSIONS`):名字在 `__version_ext_names`(连续 NUL 结尾字符串),CRC 在 `__version_ext_crcs`,支持长符号名;加载时校验两者**同时存在或同时缺失**,不匹配报 `-ENOEXEC`
- 导出侧:`__ksymtab` / `__ksymtab_gpl`(名字在 `__ksymtab_strings`),CRC 在 `__kcrctab` / `__kcrctab_gpl`
## 八、文件末尾"不是 ELF 的数据":先查模块签名
- Linux 模块签名**直接附加在 ELF 之后**:`ELF` + signature blob + `~Module signature appended~.`
- 见到"节表已结束但文件还有数据"**不要自动判** overlay / packer / 恶意附加数据——先检查 module signature
- **签完名以后再 strip 会破坏签名**(官方称这种签名为 brittle:整个模块文件都是签名 payload)→ 要分析 signed module:**保留原件 → copy → 只对副本做 strip/解压/patch**
## 九、stripped 但有 `.BTF` / `.BTF.ext` —— 可能反而好逆
- BTF 给:struct / union / enum / typedef / 函数原型 / member offset
- **`.BTF.ext`** 还含 function info、line info 与 **CO-RE relocation**
- 所以 `.symtab` 被 strip 不等于类型信息全没:先查 `.BTF` / `.BTF.ext`,再决定怎么恢复类型
## 十、`.BTF` 里 type ID 全错 → 先查 split BTF
- 模块 BTF 可以是 **split BTF**:`vmlinux BTF` 为 base,模块只存新增类型,type ID 可引用 base
- 单独解析模块 `.BTF` 报"type 38291 不存在"**不是损坏**
- 新版 external module 可能带 **`.BTF.base`**:用于 base BTF 改变后重新定位 split BTF 引用(`btf__relocate()` 的用途)
```
.BTF
├── standalone → 直接解析
└── split → 找对应 vmlinux BTF
└── 若有 .BTF.base → relocate 后再解析
```
## 十一、struct offset 与源码不符:按顺序排查
① exact kernel build? ② BTF 是否对应? ③ CONFIG 选项是否相同? ④ **结构布局随机化**(`CONFIG_GCC_PLUGIN_RANDSTRUCT`,此类内核带 taint `T`)⑤ vendor patch?
**运行态 kernel 自带 BTF/DWARF 的可信度高于"网上同版本源码"的 offset**——不是 IDA 错,也不是算错。
## 十二、磁盘代码 ≠ 内存指令:先排除**合法动态 patching**(本分支最重要的一条)
以下机制都会让运行态字节与磁盘不一致,**全部合法**:
| 机制 | 行为 |
|---|---|
| **static keys / jump labels** | `nop` ↔ `jmp target` 运行时改写(几乎零成本分支) |
| **alternatives** | 启动时按 CPU 特性/勘误替换指令 |
| **paravirt** | `pv_ops` 启动时 binary patching,用 native/hypervisor 实现替换通用路径 |
| **ftrace** | 函数跟踪本身修改执行路径;function graph tracer 还处理函数返回路径 |
| **livepatch** | 真正把执行流换到修补函数(symbol lookup + relocation + 注册 patched functions) |
| **kprobes / fprobe** | 动态插桩 |
判定链:
```
runtime bytes != disk bytes
→ 先排除 alternatives / static keys / ftrace / livepatch / paravirt / breakpoints
→ 剩余无法解释的修改
→ 才提高 rootkit 嫌疑
```
## 十三、看到 `.klp.*` 不要判 ELF 损坏(livepatch)
- 标志:`modinfo` 里 `livepatch: Y`;节 `.klp.rela.