论坛首页 逆向工程技术区 阅读主题

[原创]把 .o 变成 .ko(三):自己写 Loader 才知道的事

179 浏览 7 回复
#1 楼主 2026-06-01 21:09:09
本文是系列第三篇,也是收官之作。前两篇讲述了如何通过 ELF 格式转换,将用户空间编译产物变成 .ko,并踩平了 Android GKI 设备上的安全特性陷阱,包括 SELinux、vermagic、BTI、PAC、CFI 和厂商驱动对 ELF section 布局的敏感问题。当基础格式和安全机制都打通后,我们面临一个更根本的问题:是否必须依赖内核原生的模块加载器?如果希望在目标设备运行时动态接收、解析、重定位并执行受控二进制,就必须自己实现一个运行在内核空间的迷你加载器。本篇记录自研内核模块加载器 KPM Loader 从设计到落地过程中遇到的关键问题。原始调试过程中一共记录了第 21 到第 39 个坑,经过复盘后,将其中被后续方案完全覆盖的临时坑合并,最终整理为第 21 到第 34 个核心坑。KPatcher 的离线转换方案解决了“生成合法 .ko”的问题,但它有一个根本局限:所有 ELF 转换、section 修补、符号处理和重定位修复都必须在开发机上完成。如果希望目标设备在运行时直接接收二进制、完成解析、重定位和执行,就需要一个运行在内核空间的加载器。KPM Loader 应运而生。它是一个独立内核模块,通过 /proc 接口接收一种自定义格式的二进制,称为 KPM,并在内核空间完成:本质上,这就是一个迷你的内核模块加载器。内核原生加载器走过的每一步:我们都要自己实现一遍。而那些在内核加载器里被成熟代码处理好的细节,自己动手时全都变成了坑。每次都按流程获取 kallsyms_lookup_name 地址:模块可以加载成功。但有时前一秒还能正常工作的模块,下一秒就崩溃。崩溃类型是:也就是 Instruction Abort。崩溃点恰好发生在调用 kallsyms_lookup_name() 的位置。Android GKI 内核启用了 KASLR:每次设备重启后,内核符号的虚拟地址都会重新随机化。典型错误流程如下:两个 adb 会话之间设备恰好发生了重启,符号地址已经变化,但 loader 仍在使用旧地址。始终在同一次启动周期内重新获取地址,并立即加载模块。示例:KASLR 地址只在单次启动周期内有效。任何崩溃、重启、软重启之后,都必须重新获取所有 kallsyms 地址。这是动态分析内核环境的基础纪律。通过 /proc/kpm_loader 写入了正确的 kallsyms_lookup_name 地址:但 dmesg 显示:后续所有符号解析全部失败。自写的 hex_to_ulong() 函数没有处理 0x 前缀。有 bug 的解析逻辑类似这样:也就是说:被错误解析成:而且函数还返回“成功”。在解析前显式跳过 0x 或 0X 前缀:十六进制解析函数必须显式处理 0x 前缀。内核日志、/proc/kallsyms 输出、用户空间脚本提取出来的地址,几乎都会保留这个前缀。如果解析器不处理它,就会解析出 0,而且很可能没有任何错误提示。KPM 加载完成后调用入口函数:结果触发:也就是 Instruction Abort。崩溃地址正好落在 KPM 的 .text 段中。这说明代码所在内存不可执行。在 ARM64 GKI 上,module_alloc() 返回的内存属性通常是:也就是:在 ARM64 上,不可执行由 PXN 控制:因此,module_alloc() 得到的内存默认不是可执行内存。这和很多 x86_64 环境不同。x86 上开发 loader 时,这类问题经常不会暴露;但在 ARM64 Android GKI 上,PXN 是硬件强制的。不能一分配完就直接执行。正确流程应该是:示例流程:ARM64 的 PXN 是硬件级约束。不能假设 module_alloc() 返回的内存默认可执行。所有代码写入完成后,必须显式切换为可执行。重定位计算完全正确,但写入指令后读回仍是旧值。表现像是写操作失败,但没有明确报错。部分 ARM64 硬件实现支持 WXN:也就是页面不能同时拥有写权限和执行权限。错误流程如下:问题在于:在 relocation patching 之前,页面已经被设置成可执行。此时如果硬件或内核策略不允许可执行页继续写入,那么后续对 .text 的写操作可能失败或行为异常。必须严格遵循:也就是:ARM64 上写权限和执行权限必须分阶段管理。只要 .text 进入可执行阶段,就不应该再继续 patch 它。编译阶段一切正常,但运行时:返回:类似的问题还出现在:Android GKI 严格限制导出符号列表。某个符号即使存在于 /proc/kallsyms 中,也不代表普通模块可以直接引用。常见情况是:KPM Loader 又恰好需要这些不导出的核心函数。在 loader 初始化阶段,通过传入的 kallsyms_lookup_name 地址统一解析,并缓存所需符号。示例结构:初始化时统一解析:不要假设某个内核符号在 GKI 上一定导出。写内核 loader 需要的关键函数,往往恰好不在 GKI 对外导出列表中。KPM 中的:输出了乱码,而不是正常字符串。进一步调试发现,ADRP 指令编码异常:对比:但重定位数学本身是对的:理论上 ADRP 应该被编码成:但实际却变成了:loader 中定义的 AArch64 relocation type 常量,从 MOVW_PREL_G0 开始整体偏移了一位。错误表大致如下:这导致真实的 ADRP relocation 被错误匹配到了 MOVW relocation 分支。最终出现这样的污染路径:于是:严格按照 ARM ELF 规范修正 relocation 常量:修复后:ELF relocation type 常量不能凭记忆手写。必须和权威来源交叉验证,例如:一个常量错位,会导致后续多个 relocation 类型互相顶替,症状非常隐蔽。重定位引擎处理 .rela.text 等 relocation section 时,需要访问:但对于:这些非 SHF_ALLOC 段,sh_addr 仍然是 0。结果访问空地址,触发内核 Oops。对于 ET_REL 文件,section header 中的 sh_addr 通常是 0。因为它还没有被最终链接,也没有运行时地址。内核原生 module loader 会在加载过程中设置 section 的运行时地址。但自己写 loader 时,这一步需要手动完成。尤其要注意:重定位处理不仅需要访问 SHF_ALLOC 段,也需要访问非 SHF_ALLOC 的符号表、字符串表和 relocation 表。例如:如果这些 sh_addr 没有设置,就会访问 NULL。在 loader setup 阶段,对所有非 SHF_ALLOC 段设置 sh_addr,让它们指向文件缓冲区中的原始位置:ET_REL 文件中的 sh_addr 不能直接相信。自己写 loader 时,非 ALLOC 段也必须拥有一个可访问的内存地址。否则符号表、字符串表、relocation 表都会在运行时访问失败。KPM 加载成功,但 dmesg 显示:模块名和版本号都是空字符串。但检查 KPM 文件中的 .kpm.info 段,字符串明明存在。在

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291097.htm
#2 2026-06-01 21:09:09
这三篇文章写完之后,实现的成果主要有两个:
1. 一套只使用ndk的跨版本兼容内核模块开发框架
2. 一个可跨版本兼容的kpm加载器(作为kernel su元模块使用),不需要patch内核,可在越狱模式的设备上使用
#3 2026-06-01 21:09:09
本篇中加载器设计主要参考安卓内核自己的模块加载器和kernel patch的kpm加载器
坑点全部出现在仿制加载器的过程之中
#4 2026-06-01 21:09:09
谢谢分享
#5 2026-06-01 21:09:09
tql
#6 2026-06-01 21:09:09
mark
#7 2026-06-01 21:09:09
牛啊牛啊
#8 2026-06-01 21:09:09
谢谢分享

请登录后参与讨论

立即登录 注册账号