Linux内核模块签名与吊销:从Secure Boot到运行时模块验证全栈实践
内核模块是Linux可扩展性的核心机制,但不受限制的模块加载也意味着系统安全的巨大敞口。本文从
CONFIG_MODULE_SIG的签名格式解析出发,深入讲解内核模块签名验证的底层原理、与UEFI Secure Boot的信任链构建,以及如何实现模块吊销机制,覆盖从开发签名到生产部署的完整路径。
一、为什么内核模块需要签名?
Linux允许通过insmod/modprobe在运行时加载内核模块(.ko文件),这段代码以内核态(ring 0)运行,拥有完整的系统权限。如果没有验证机制,攻击者可以通过以下方式植入恶意模块:
- 利用内核漏洞(如堆溢出)加载内核态Rootkit(ldiska / adore-ng变种)
- 替换
/lib/modules/目录下的合法模块文件 - 通过用户态漏洞获得root权限后加载特权模块
内核模块签名机制通过在模块二进制上附加数字签名,确保任何加载的模块都经过开发者或组织授权,且未被篡改。它构成了可信计算基(TCB)中内核完整性的最后一道防线。
二、模块签名格式详解
内核模块签名采用追加式签名——签名数据不是嵌入ELF结构内部,而是附加在.ko文件的尾部。这种设计保持了ELF加载器的二进制兼容性,签名验证在模块加载前单独执行。
2.1 签名数据结构
┌─────────────────────────────────────┐
│ Standard .ko ELF Data │
├─────────────────────────────────────┤
│ PKCS#7 Signed Data Structure │
│ ┌───────────────────────────────┐ │
│ │ Signer's CN (e.g. "Linux") │ │
│ │ Key ID (SHA-256 fingerprint) │ │
│ │ Signature (RSA/ECDSA) │ │
│ └───────────────────────────────┘ │
├─────────────────────────────────────┤
│ Magic: "~Module signature~\n" │
└─────────────────────────────────────┘
核心代码位于kernel/module_signature.c:
struct module_signature {
u8 algo; // Hash algorithm: 0x04=SHA256, 0x0b=SHA512
u8 hash; // Signature scheme
u8 id_type; // 0=PGP, 1=X509
u8 signer_len; // Signer CN length
u8 key_id_len; // Key ID length
u8 __pad[3];
u32 sig_len; // Signature length (big-endian)
// Followed by: signer + key_id + signature
};
2.2 签名生成流程
构建内核时,scripts/sign-file工具对每个.ko进行签名:
# 签名命令格式
scripts/sign-file <hash_algo> <private_key> <x509_cert> <module.ko>
# 示例:使用SHA-512和自签名证书签名
scripts/sign-file sha512 signing_key.pem signing_key.x509 my_driver.ko
签名过程分为三步:
- 计算哈希:对ELF文件内容(不含尾部签名区域)计算SHA-512哈希
- PKCS#7封装:使用私钥对哈希签名,封装为PKCS#7格式,附带签名者DN和密钥ID
- 写入尾部:将签名结构体以
~Module signature appended~\n魔术字结尾追加到文件 - 签名缺失或损坏:模块根本没有签名或签名格式错误
- 密钥不匹配:签名使用的公钥不在当前系统的信任密钥环中
- 哈希算法不匹配:内核配置要求SHA-512但模块使用SHA-256签名
三、运行时签名验证路径
模块加载时,签名验证发生在load_module() → module_sig_check()调用链中:
// kernel/module/main.c (简化流程)
static int load_module(struct load_info *info, const char *uargs, int flags)
{
// 1. 解析签名尾部
err = module_sig_check(info, flags);
if (err)
goto free_copy;
// 2. 完整的模块加载流程
err = layout_and_allocate(info, mod);
err = complete_formation(mod, info);
err = prepare_binary(info, mod);
err = finalize(mod, info);
return err;
}
module_sig_check()的核心逻辑:
static int module_sig_check(struct load_info *info, int flags)
{
int err = -ENODATA;
bool sig_enforce = sig_enforce_kernel();
// 查找尾部魔术字 "~Module signature appended~\n"
sig = (void *)(info->hdr + info->len) - sizeof(magic);
if (memcmp(sig, MODULE_SIG_STRING, sizeof(magic) - 1))
goto no_signature;
// 解析签名头
err = mod_verify_sig(info, &ms);
if (err)
return err;
// 建立签名者身份与密钥的绑定关系
err = verify_pkcs7_signature(pks7, key);
if (err) {
if (sig_enforce) {
pr_err("Module signature verification failed\n");
return err; // 拒绝加载
}
// 非强制模式:标记-module但允许加载
mark_unsupported_module(info->mod);
}
return 0;
}
强制模式生效时,任何签名验证失败都会导致模块加载返回-EKEYREJECTED("Required key not available")。
四、密钥管理与配置
4.1 密钥类型
内核维护三组密钥环:
| 密钥环 | 用途 | 配置 |
|---|---|---|
| Built-in Trusted Keys | 内核编译时内置的签名公钥 | CONFIG_SYSTEM_TRUSTED_KEYS |
| Platform Key Ring | 平台级密钥(UEFI Secure Boot db/KEK) | CONFIG_LOAD_UEFI_KEYS |
| Imported/MOK Key Ring | 从MOK(Machine Owner Key)导入 | CONFIG_SECONDARY_TRUSTSOURCE |
4.2 Keyctl 实用操作
# 查看系统信任密钥环
keyctl show %:.system_keyring
# 查看模块签名密钥
keyctl list %:.builtin_trusted_keys
# 验证已签名模块
modinfo my_driver.ko | grep sig_
# 输出: signer: Linux Kernel Key
# sig_key: AB:CD:EF:...:12
# sig_hashalgo: sha512
4.3 构建配置
CONFIG_MODULE_SIG=y # 启用签名验证
CONFIG_MODULE_SIG_FORCE=y # 强制模式(无签名=拒绝加载)
CONFIG_MODULE_SIG_ALL=y # 构建时自动签名所有模块
CONFIG_MODULE_SIG_SHA512=y # 使用SHA-512哈希
CONFIG_MODULE_SIG_KEY="certs/signing_key.pem" # 签名私钥路径
CONFIG_SYSTEM_TRUSTED_KEYRING=y # 启用信任密钥环
CONFIG_SYSTEM_EXTRA_CERTIFICATE=y # 额外X.509证书
五、模块吊销机制
密钥吊销是PKI体系中最棘手的环节。内核从5.2版本开始逐步完善了吊销支持。
5.1 黑名单密钥环(Blacklist Keyring)
CONFIG_SYSTEM_BLACKLIST_KEYRING提供了一组"不信任"的密钥/哈希列表。任何签名密钥ID匹配黑名单的模块,无论签名是否有效,都会被拒绝加载:
// certs/blacklist.c
static struct key *blacklist_keyring;
static int blacklist_init(void)
{
// 加载 compiled-in 黑名单
const u8 *p = blacklist_hashes;
const u8 *end = p + blacklist_hashes_size;
while (p < end) {
xas_for_each_mark(&xa, entry, ULONG_MAX) {
// 每个条目格式: <desc_len><desc><hash_algo><hash>
keyring_instantiate(" blacklist_keyring",
"asymmetric: blacklisted key",
hash, hash_len, KEY_POS_VIEW);
}
}
}
5.2 模块完整性黑名单
除了密钥级吊销,还可以按模块的预期哈希值进行"精准打击":
# 生成模块的预期SHA-256哈希
sha256sum /lib/modules/$(uname -r)/my_driver.ko
# 将哈希添加到内核命令行(需在编译时启用CONFIG_MODULE_SIG_BLACKLIST)
module.sig_enforce=1
module_blacklist=<sha256_hash>
5.3 与dm-verity的联动
在生产环境中,模块吊销通常与dm-verity(设备映射器完整性校验)协同:
启动链验证:
┌─────────────────┐
│ UEFI Secure Boot │ → 验证bootloader
├─────────────────┤
│ Kernel Image │ → 验证内核映像
├─────────────────┤
│ Initramfs │ → 验证初始文件系统
├─────────────────┤
│ dm-verity Rootfs │ → 验证根文件系统完整性
├─────────────────┤
│ Module Signing │ → 验证内核加载的模块
└─────────────────┘
这一系列机制构成了从固件到应用的完整信任链。
六、自定义签名实战
以下演示使用自签名证书为外部模块(如DKMS驱动)签名的完整流程:
步骤1:创建签名密钥对
# 生成X.509自签名证书
openssl req -new -x509 -newkey rsa:4096 \
-keyout MOK.priv -outform DER -out MOK.der \
-nodes -days 36500 -subj "/CN=MyOrganization Module Signing Key/"
# 转换为PKCS#12(用于后续MOK导入)
openssl pkcs12 -export -in MOK.der -inkey MOK.priv \
-out MOK.p12 -name "MyOrg Module Signing"
步骤2:导入MOK(Machine Owner Key)
# 通过 mokutil 启动 UEFI MOK 注册
mokutil --import MOK.der
# 输入密码后重启,在UEFI MOK Manager蓝色界面确认注册
# 验证导入
mokutil --list-enrolled
步骤3:构建并签名外部模块
# 假设一个DKMS模块已经安装
cd /var/lib/dkms/my-driver/
# 使用MOK密钥签名所有已编译的模块
find . -name "*.ko" -exec \
/lib/modules/$(uname -r)/build/scripts/sign-file \
sha512 /root/MOK.priv /root/MOK.der {} \;
# 验证签名
modinfo /var/lib/dkms/my-driver/1.0/$(uname -r)/x86_64/module/my-driver.ko | grep signer
步骤4:配置吊销
# 假设需要将某个泄露的密钥哈希加入黑名单
# 编辑 certs/blacklist.conf
echo "<key_id>" >> certs/blacklist.conf
# 重新编译内核,黑名单将被嵌入内核映像
CONFIG_SYSTEM_BLACKLIST_KEYRING=y
CONFIG_SYSTEM_BLACKLIST_HASH_LIST="certs/blacklist.conf"
七、常见陷阱与排错
7.1 "Required key not available" 错误
% modprobe my_module
modprobe: ERROR: could not insert 'my_module': Required key not available
原因排查:
# 获取详细的调试信息
dmesg | tail -20 | grep -i "module\|signature\|key"
# 查看模块签名元数据
modinfo -F signer -F sig_key -F sig_hashalgo <module>.ko
7.2 Secure Boot导致的"Lockdown"状态
当UEFI Secure Boot启用时,内核进入Lockdown模式,这会限制一系列特权操作:
lockdown=integrity: 禁止用户空间直接读写物理内存
lockdown=confidentiality: 禁止I/O端口访问、禁止kexec
在Lockdown模式下,无法加载未签名模块,即使CONFIG_MODULE_SIG_FORCE未启用。
7.3 DKMS与DKMS自动签名
DKMS(Dynamic Kernel Module Support)未原生集成签名。需要借助dkms-signer工具或手动钩子:
# /etc/dkms/template-dkms-makepkg.conf
POST_BUILD="../dkms-signing.sh $kernelver $arch"
八、前沿发展与替代方案
8.1 eBPF作为模块的替代方案
eBPF通过内核内省验证器(Verifier)从根本上拒绝危险操作,在安全性上远超传统模块。但eBPF无法直接替代需要访问私有内核数据结构的驱动场景。
8.2 Rust for Linux
Rust模块借助语言级别的内存安全保证,减少了触发签名验证失败的内部路径。签名是信任链的最后一道,Rust是减少需要信任的攻击面的第一道。
8.3 综合能力对比
| 机制 | 保护范围 | 实现复杂度 | 灵活性 |
|---|---|---|---|
| Module Signing | 模块完整性/来源 | 中 | 高(支持多种用途) |
| dm-verity | 文件系统完整性 | 高 | 低(只读) |
| Lockdown | 运行时特权边界 | 低 | 中 |
| eBPF Verifier | 程序安全性 | 高 | 高 |
总结
Linux内核模块签名是生产环境中保障内核安全的重要基础设施。完整的实施路径包括:正确配置构建系统(CONFIG_MODULE_SIG_FORCE + SHA-512)、与Secure Boot建立信任链、在密钥泄露时通过黑名单密钥环快速吊销、并与dm-verity等机制协同构建纵深防御。随着Rust和eBPF的逐步成熟,未来的内核安全将是语言安全、运行时验证和签名加密的多层融合体系。

发表评论 取消回复