Linux内核为了支持设备驱动和文件系统扩展,允许在运行期间加载内核模块。这个机制虽然灵活,但也打开了注入恶意代码的通道。攻击者一旦拿到root权限,就可以通过加载一个构造好的.ko文件隐藏进程、替换系统调用或植入后门。内核模块签名验证就是针对这一风险设计的防线,它让内核在模块代码进入内核地址空间之前,先确认模块来源可信且内容没有被篡改。

一、从ELF文件到签名验证的完整链路
内核模块本质上是ELF格式的可重定位目标文件。启用模块签名后,编译系统不会直接使用普通的strip流程把文件末尾清理干净,而是在模块文件尾部附加一个签名段。这个段不是ELF标准中的加载段,而是内核约定的一段数据,包含PKCS#7格式的数字签名以及签名者信息。模块加载器在拷贝模块内容之前会检查这段数据,因此签名不会影响模块原有的代码节、数据节和重定位信息。
签名验证的核心不是简单比对哈希值,而是走完整的非对称加密流程。构建阶段使用私钥对模块文件的摘要做签名,公钥或X.509证书则被编译进内核。内核加载模块时,module_sig_check函数会定位签名段,用内嵌的公钥验证PKCS#7签名,再重新计算模块内容的哈希并和签名中的哈希比对。只有验签通过且哈希一致,模块才会被允许继续解析和重定位。正因为公钥在编译时已经进入内核镜像,攻击者即便替换模块文件,也无法伪造出能被内核接受的签名。
可以用modinfo命令查看一个已签名模块的签名信息。签名后的模块通常会包含sig_id、signer、sig_key和sig_hashalgo等字段,这些字段直接来自签名时使用的证书和算法。
modinfo hello.ko | grep -E 'sig_id|signer|sig_key|sig_hashalgo'
输出的sig_id通常为PKCS#7,signer是证书中的CN字段。如果模块没有经过签名,这些字段不会出现。需要说明的是,仅靠modinfo看到签名字段并不能证明签名一定有效,最终结果仍以内核加载时的验签为准。
二、生成密钥并启用内核签名选项
要使用模块签名验证,首先要在内核配置中打开相关选项。CONFIG_MODULE_SIG是总开关,CONFIG_MODULE_SIG_ALL表示构建过程中自动为所有模块签名,CONFIG_MODULE_SIG_FORCE决定是否强制拒绝未签名或签名无效的模块。如果只是希望具备验签能力但允许未签名模块继续加载,可以只打开CONFIG_MODULE_SIG,不打开CONFIG_MODULE_SIG_FORCE。生产环境中为了提高安全性,通常会把强制模式一并打开。
CONFIG_MODULE_SIG=y CONFIG_MODULE_SIG_ALL=y CONFIG_MODULE_SIG_KEY="certs/kernel_key.pem" CONFIG_MODULE_SIG_FORCE=y CONFIG_MODULE_SIG_SHA256=y
签名密钥可以通过OpenSSL生成。密钥类型必须是X.509证书对应的私钥,常见做法是直接生成一个自签名证书和私钥放在同一个PEM文件中。配置项CONFIG_MODULE_SIG_KEY可以指向这个PEM文件。若把密钥放在内核源码的certs/目录下并命名为kernel_key.pem,构建系统会自动使用,不需要额外指定绝对路径。私钥的安全性非常重要,特别是启用了强制验签的内核,拿到私钥就相当于拿到了向该内核注入任意模块的能力。
# x509.genkey 内容示例 [ req ] default_bits = 4096 distinguished_name = req_distinguished_name prompt = no string_mask = utf8only x509_extensions = myexts [ req_distinguished_name ] O = ExampleOrg CN = ExampleOrg kernel signing key emailAddress = admin@ipipp.com [ myexts ] basicConstraints = critical,CA:FALSE keyUsage = digitalSignature subjectKeyIdentifier = hash authorityKeyIdentifier = keyid
上面的x509.genkey文件定义了一个不具CA属性的数字签名证书模板。生成命令中-days 36500表示证书有效期约为100年,避免短期内证书过期导致已签名模块无法加载。生成后可以得到同时包含私钥和证书的kernel_key.pem。
openssl req -new -nodes -utf8 -sha256 -days 36500 -batch -x509 \
-config x509.genkey -outform PEM -out kernel_key.pem \
-keyout kernel_key.pem
openssl x509 -in kernel_key.pem -outform DER -out kernel_key.x509
接下来重新编译内核并安装模块。如果开启了CONFIG_MODULE_SIG_ALL,make modules_install阶段会自动调用scripts/sign-file为每个模块签名。构建完成后可以用modinfo批量检查。CONFIG_MODULE_SIG_KEY指定的私钥不会被安装到目标系统的模块目录中,构建完成后应妥善保存或从构建目录中移除。
三、手动签名、内核验证与强制模式
对于第三方模块或单独编译的模块,构建系统不会自动处理签名,这时需要手动调用内核源码中的sign-file脚本。第一个参数是哈希算法,通常与内核配置的CONFIG_MODULE_SIG_SHA256保持一致;第二个参数是私钥PEM文件;第三个参数是DER格式的证书文件;最后是待签名的.ko文件。
cd /usr/src/linux
scripts/sign-file sha256 \
certs/kernel_key.pem \
certs/kernel_key.x509 \
/lib/modules/$(uname -r)/extra/hello.ko
手动签名后,可以看到模块文件变大,尾部出现签名数据。使用readelf -S查看节区时会发现一个名为.module_sig的节。不同工具链对尾部附加数据的处理方式不同,因此不建议在签名后继续执行strip操作。若必须strip,应使用内核构建流程自带的方式,而不是任意使用strip --strip-debug等命令,否则可能破坏签名导致验签失败。
模块加载时,内核会检查CONFIG_MODULE_SIG_FORCE下的策略。未签名模块、签名哈希不匹配的模块、证书不受信任的模块都会被拒绝。dmesg中常见日志包括PKCS#7 signature not signed with a trusted key和Required key not available。如果内核编译时没有把正确公钥编译进去,即使模块本身签名有效,也会因为证书不在信任链中而加载失败。此时需要重新编译内核,把对应证书或PEM文件作为CONFIG_MODULE_SIG_KEY的输入。
dmesg | grep -Ei 'module|signature|key'
还有一种情况是开启了Secure Boot的机器。UEFI Secure Boot会限制内核和模块的启动链,如果启用内核锁定,模块签名策略会被进一步收紧。此时不仅要通过内核内置公钥验签,还要符合UEFI平台对签名的要求。很多发行版通过shim和MOK机制管理这类密钥,用户自编译内核后如果使用第三方模块,通常需要把证书导入MOK列表,否则模块会被内核锁定策略拦截。
四、生产环境实践与常见误区
模块签名验证不是银弹。它主要防止攻击者在获得root权限后加载任意内核模块,但不能阻止root用户修改内核内存、挂载调试接口或利用已有漏洞。启用强制签名后,系统可用性也会受到影响,例如自行编译的驱动、厂商闭源驱动以及DKMS生成的模块都需要使用同一套签名证书。如果私钥丢失,原先签名的模块仍然可以加载,但后续更新模块需要重新生成密钥并重编译内核,因此证书和私钥管理是运维中必须提前规划的事项。
另一个常见误区是认为只要开启CONFIG_MODULE_SIG就能拦截所有未签名模块。实际上该选项只提供验签能力,是否强制由CONFIG_MODULE_SIG_FORCE和内核启动参数module.sig_enforce共同决定。如果内核配置没有把强制模式编译进去,攻击者仍可能加载未签名模块。使用时可以通过/proc/sys/kernel/modules_disabled进一步限制模块加载,但要注意启用后无法撤销,只能重启。
在企业环境中,模块签名策略通常和完整性度量、安全启动、审计日志一起组成内核安全基线。对于需要发布内核模块的团队,建议使用独立的签名证书,不要与代码签名证书混用;私钥不要进入版本控制,构建机与签名机分离;定期检查已加载模块的签名信息,并监控dmesg中的验签失败日志。这些措施配合强制签名和内核对模块参数的严格限制,能显著提高攻击者植入内核级恶意代码的成本。
内核模块签名验证属于防御纵深中的一环,它解决的是模块来源和完整性问题。理解其证书链、签名段结构和强制策略,不仅有助于在自编译内核时避免模块加载失败,也能在安全事件排查中快速定位是否为签名不匹配或证书不受信任导致的问题。对于运维和内核开发者来说,掌握这套机制的实际操作远比记住配置项名称更加重要。