原理分析见原创文章《把.o变为.ko》系列,以下是速通版:
知己知彼,方能百战不殆 —— 当我们使用insmod安装内核模块时,linux内核到底校验了什么?
这个问题只能从内核源码得到详细答案,由于分析过长,这里只给出摘要。
内核到底期望什么二进制?可执行、动态库 or 对象文件?
这里让我们来看.ko加载时期待的section:
毫无疑问,最接近这种格式要求的ELF文件格式就是.o对象文件,但是二者存在差异:
为了解决这个问题,我们引入一阶段修补工程 —— KPatcher,它负责(KPatcher.cpp:46-136):
注:namespace — Linux 把内核导出符号分了"房间",普通驱动不能随便访问内部符号。-N VFS_internal_... 就是告诉内核"我获得了这个房间的访问授权"。
这是fixup需要填充的第一个地方,fixup根据真机上已经存在的.ko文件,提取vermagic,回填到KPatcher留下的位置(vermagic占位符)。
注:其实android内核不是很在意内核版本,比如5.10.198-android12-9-g12345678 SMP preempt mod_unload aarch64,android内核实际关注的是SMP preempt mod_unload aarch64,内核版本5.10.198-android12-9-g12345678则无所谓。猜测这与android的内核映像和设备高度绑定不能更新,但是系统可以更新有关。
即使内核版本相同,编译选项不同也会导致 struct module 内部的字段偏移不同。内核用 __versions 里的 CRC 校验这个。fixup将从真机上的.ko提取其CRC值并填入__versions。
为了解决2、3,我们引入了fixup负责通过真机上的.ko文件修补KPatcher生成的模块.ko:
Android 内核开启了 CFI(Control Flow Integrity,控制流完整性),在真的执行你的代码之前,验证你的函数指针没有被篡改。它的逻辑很简单:
但是5.x内核和6.x内核的具体检验策略是不同的,想象机场安检:
技术上:
关键区别:
内核调用 init_module() 时会被拦下,然后调用模块注册的 __cfi_check 问"这个函数安全吗?"。这里我们手工构造函数:
翻译成人话:"不管来什么 hash,只要目标地址是我认识的,就通过。"
跳转表结构:
但跳转表结构不只是给 5.x 用的。那 4 字节 hash 同时服务了 6.x。
6.x 不用跳转表了,它直接读函数地址前 4 字节。问题是:这 4 字节必须是 clang 亲自生成的,不能自己编。于是有了偷 hash 的流水线。
生成汇编文件:
linker_dual.lds 把前两步产物按精确顺序排进 .text section。内存布局最终变成:
6.x 内核的 kCFI 检查读 cleanup_module - 4 = 跳转表 hash(0xA540670C),匹配,通过。
注:细看会发现 kcfi_prefix.cleanup 里的 hash 和跳转表里的 hash 完全一样。但内核只读跳转表 hash。原因是 kcfi_prefix.cleanup 紧贴在 cleanup_module.cfi 这个符号前面——而内核从不间接调用 cleanup_module.cfi,它只被 cleanup_module: 以 b 指令直接跳转。直接跳转不触发 kCFI。所以这个 hash 实际上从未被读取——它是第一版开发时留下的冗余保险,属于不影响功能的历史残留。
前面解决的只是 loader.ko 自身通过 insmod 时的 CFI 检查。但 loader 加载的 KPM(内核补丁模块)是用普通 clang 编译的,没有任何 CFI 保护。loader 通过函数指针调用 KPM 代码时,还是会撞 CFI。
于是 loader 在内核内部做了一次"安检豁免权申请"——hook 内核的 CFI 基础设施:
同时把 CFI 失败的处理从 crash 降级为 warn:
这样一来 loader.ko 自身的 insmod 走编译时伪造的跳转表 / kCFI hash,而它加载的 KPM 走运行时的 check 豁免,两套机制各司其职。
我也不知道这个技术有什么用,也许可以用于RootKit,目前只在5.10.198和6.13.4内核进行过测试。
下面介绍实践的成品:
项目地址:KPatcher
他负责将NDK编译生成的.o文件修补为待填充的.ko文件。
项目地址:NDK_Kernel_Module
相当于工程模板,很多构建配置已经设置了好了,也提供了有用的头文件,loader文件夹内是一个使用这套工程模板的实例。
在设备上使用时需要配合fixup_ko,用于获取设备上正常内核模块来完成对于待填充的.ko文件的最后修补(注:如果设备不支持kprobe,每次重启需要未修补的Loader.ko使用fixup_ko重新修补)。
loader本身是使用它构建的kpm加载器(但是兼容不全),依靠448号syscall与用户空间通信,且带有根据文件中配置的uid来关闭指定app的seccomp的功能(不然没办法用448号syscall)。
项目地址:NDK_SIMPLE_KPM
loader支持的kpm构建模板,已装好构建配置和头文件。
目前src目录下是我修改的pte无痕hook实现作为使用示例,可以改成别的东西。
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!
最后于 2天前
被孤木落编辑
,原因: