4. 内核模块签名机制概述

内核模块是 Linux 内核在运行时动态扩展功能的基本单元,它运行在最高特权级(Ring 0 / EL0 内核态),一旦被加载,就拥有与内核本体几乎相同的权限。正因如此,内核模块既是驱动生态的基石,也是攻击者最想利用的入口之一。内核模块签名机制的核心目标,就是回答一个问题:这段即将进入内核的二进制代码,是否来自可信来源,并且在传输与存储过程中没有被篡改?

一、为什么需要签名:从信任边界说起

在没有签名校验的系统中,任何具备 root 权限的进程都可以通过 insmod 或 init_module 系统调用把任意 .ko 文件塞进内核。这意味着一次提权成功后,攻击者可以植入 rootkit、挂钩系统调用、隐藏进程与文件,且这种修改在内核内存中运行,常规的用户态检测手段很难发现。签名机制在“模块二进制”与“内核加载器”之间建立了一道密码学信任边界。

具体而言,签名解决三类问题:

  • 来源真实性:确认模块由持有私钥的可信方(厂商、OEM、SoC 供应商)构建,而非第三方伪造。
  • 完整性:确认模块在分发、OTA、刷机过程中未被篡改,哪怕改动一个字节也会导致校验失败。
  • 策略可控性:让内核能够按策略决定“允许加载 / 拒绝加载 / 仅告警”,从而支撑安全启动链的完整性。
签名并不加密模块内容,它只提供真实性与完整性保证。模块本身仍是明文,可以被读取和分析,但无法被悄悄修改后继续加载。

二、签名验证流程:从 .ko 到内核信任链

内核模块签名采用“分离签名”的方式:签名数据并不嵌入模块代码段,而是附加在模块文件末尾,形成 module_signature 结构。加载时,内核先解析模块 ELF,再定位签名块,最后用内置公钥验证。

典型流程可以概括为以下步骤:

  1. 构建阶段:对模块的规范化内容计算摘要(通常是 SHA-256 或 SHA-512)。
  2. 签名阶段:用私钥对摘要进行签名,生成 RSA/ECDSA 签名值。
  3. 附加阶段:通过 scripts/sign-file 工具把算法 ID、密钥 ID、签名值写入模块尾部。
  4. 加载阶段:内核读取模块,提取签名块,用密钥环中的公钥验证签名。
  5. 策略判定:根据 CONFIG_MODULE_SIG_FORCE 等配置决定强制拒绝还是仅记录告警。

下面是一个典型的签名与查看命令:

# 使用私钥对模块签名(SHA-256 + RSA)
scripts/sign-file sha256 signing_key.pem signing_cert.pem my_driver.ko

# 查看模块尾部签名信息
tail -c 200 my_driver.ko | hexdump -C

# 加载时观察内核日志
dmesg | grep -i "module verification"
# 输出示例:
# my_driver: module verification failed: signature and/or required key missing
# my_driver: loading out-of-tree module taints kernel

验证失败时,内核会给出明确提示。若启用了强制模式,加载会直接返回 -EKEYREJECTED;若未强制,模块仍可加载,但内核会被标记为 tainted,影响后续问题定位与厂商支持。

内核模块签名验证流程与信任链 构建与签名侧 my_driver.ko SHA-256 摘要 私钥签名 → 附加尾部 .ko 文件结构 ELF 代码与数据 module_signature 算法ID + 密钥ID + 签名值 内核加载器 解析 ELF 提取签名块 公钥验证 内核内置公钥 / 密钥环 .builtin_trusted_keys · .secondary_trusted_keys 验证通过 → 加载模块 进入内核地址空间运行 验证失败 → 拒绝/告警 -EKEYREJECTED 或 taint

三、Android 与标准 Linux 的差异

标准 Linux 发行版通常把模块签名作为“可选加固项”,很多发行版默认不强制,甚至允许用户自行签名。Android 则把模块签名嵌入到整机安全启动链中,要求更严格、更系统化。

对比维度 标准 Linux Android
强制策略 默认宽松,CONFIG_MODULE_SIG_FORCE 可选 通常强制校验,与 Verified Boot 绑定
信任根 内核内置公钥或用户密钥环 设备厂商 / SoC 供应商密钥,逐级签名
模块来源 发行版仓库、第三方 DKMS 多为 GKI 与 vendor 模块,来源受控
加载限制 root 可加载未签名模块 未签名模块通常被拒绝,需解锁或关闭校验
与启动链关系 相对独立 boot / vendor_boot / dtbo 逐级验证,模块签名是其中一环

Android 的另一个显著差异是 GKI(Generic Kernel Image) 与 vendor 模块的分离。Google 提供通用内核镜像,厂商通过 vendor_boot 与 vendor_dlkm 分区加载自己的模块。这些模块必须使用厂商密钥签名,并且其公钥需要被内核信任,否则在强制校验下无法加载。

四、常见误区与排障复盘

误区一:签名后模块内容被加密。签名只保证真实性与完整性,模块仍是明文,可被反汇编分析。

误区二:只要签名就能加载。若公钥未被内核信任,或签名算法不被支持,验证仍会失败。

误区三:关闭校验没有代价。关闭 CONFIG_MODULE_SIG_FORCE 会降低安全启动链强度,并可能触发 taint,影响厂商支持与 OTA 兼容性。

排障时建议按以下顺序检查:

  • 确认模块尾部是否存在 module_signature 结构;
  • 确认签名使用的算法与内核配置一致(如 SHA-256 + RSA-2048);
  • 确认对应公钥已编译进内核或加入可信密钥环;
  • 查看 dmesg 中的 module verification failed 具体原因;
  • 检查是否因 CONFIG_MODULE_SIG_FORCE 导致直接拒绝。

理解签名机制,是后续掌握 Android 模块加载策略、GKI 兼容性与安全启动链的基础。签名不是一道孤立的校验,而是从构建、分发到加载的完整信任传递过程。