Linux内核从3.7版本开始引入了模块签名验证机制,但Debian稳定版在默认安装下,这一关键安全防线往往是静默关闭的。你可以运行 cat /proc/sys/kernel/modules_disabled 查看返回值,如果是0,意味着系统允许加载模块;接着运行 cat /sys/module/module/parameters/sig_enforce,如果返回N,说明内核虽然可能检查签名,但不会强制拒绝未签名或签名无效的模块。这种配置让rootkit开发者有了可乘之机,他们只需编译一个无签名的恶意内核模块,就能绕过大多数完整性检查,直接在内核空间执行任意代码。

问题的核心在于Debian的默认内核策略倾向于兼容性。官方编译的内核虽然开启了 CONFIG_MODULE_SIG 选项,但通常没有开启 CONFIG_MODULE_SIG_FORCE。这意味着内核会在模块加载时检查签名,如果签名缺失或无效,内核只会记录一条日志并标记模块为“污染”,但依然允许加载。对于生产服务器或高安全需求的环境,这种“只警告不阻止”的策略形同虚设。我们需要主动改变这一现状,将内核置于强制验证模式,并建立一套完整的本地签名体系。

理解内核模块签名链的底层逻辑

内核模块签名验证基于非对称加密。在内核编译阶段,会生成一对公私钥。私钥用于在编译模块时对模块的哈希值进行签名,签名信息附加在模块文件的尾部。公钥则被编译进内核镜像。当模块加载时,内核用内置的公钥解密签名,并与重新计算的模块哈希进行比对。如果匹配,说明模块自签名后未被篡改,且确实来自可信的构建源。

Debian的问题在于,它提供了一个预编译的内核和一套预生成的密钥,但这些密钥主要用于官方模块包的构建。对于你自行编译的第三方模块(如某些硬件驱动、虚拟化组件或安全软件),它们显然没有Debian官方的签名。如果你直接开启强制验证,这些合法的第三方模块将无法加载。因此,我们的任务不是简单地打开一个开关,而是要在本地建立一套属于你自己的签名密钥体系,并用这套密钥为所有需要加载的模块重新签名。

第一步:准备编译环境和内核源码

要进行密钥生成和模块签名,你不需要重新编译整个内核,但需要安装对应内核版本的头文件包和必要的编译工具。首先同步软件源并安装基础依赖:

sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev linux-headers-$(uname -r)

这里的 linux-headers-$(uname -r) 确保你安装的头文件与当前运行的内核版本严格一致。内核模块编译依赖于头文件中的数据结构定义,版本不匹配会导致签名工具或模块编译失败。安装完成后,进入内核头文件目录,我们将在这里进行密钥生成操作。

第二步:生成属于你自己的内核签名密钥

内核源码树中提供了自动生成密钥的脚本,但我们需要手动执行以确保证书配置符合你的组织信息。进入头文件目录下的certs子目录:

cd /usr/src/linux-headers-$(uname -r)/certs

在这个目录下,你需要创建一个X.509证书配置文件。用文本编辑器创建一个名为 x509.genkey 的文件,内容如下:

[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
prompt = no
string_mask = utf8only
x509_extensions = myexts

[ req_distinguished_name ]
O = YourOrganizationName
CN = YourHostName Module Signing Key
emailAddress = admin@yourdomain.com

[ myexts ]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid

请务必将 O 和 CN 字段修改为你自己的组织名和主机名。这不仅是良好的安全实践,也能让你在日后审计时,通过 modinfo 命令一眼识别出模块是由你的私钥签名的。配置完成后,使用openssl生成私钥和自签名证书:

sudo openssl req -new -nodes -utf8 -sha256 -days 36500 -batch -x509 -config x509.genkey -outform PEM -out signing_key.pem -keyout signing_key.pem

这条命令生成了一个有效期长达100年的自签名证书,私钥和证书存储在同一个 signing_key.pem 文件中。权限控制至关重要,必须将私钥的读取权限限制在root用户:

sudo chmod 600 signing_key.pem
第三步:将公钥注册到内核信任链

仅仅生成密钥还不够,内核必须识别并信任这个公钥。Debian系统使用 MOK(Machine Owner Key)机制来管理启动时的密钥信任链。你需要使用 mokutil 工具将刚刚生成的公钥注册为机器所有者密钥。

首先提取公钥的DER格式:

sudo openssl x509 -in signing_key.pem -outform DER -out signing_key.der

然后请求注册此密钥到MOK列表:

sudo mokutil --import signing_key.der

系统会提示你设置一个一次性密码。这个密码不是存储在你的用户账户中,而是用于在下次重启时验证你对MOK管理界面的物理访问权限。请务必记住这个密码。完成导入后,重启系统:

sudo reboot

在系统启动过程中,shim引导加载程序会检测到有待处理的MOK请求,并自动进入蓝色的MOK管理界面。选择“Enroll MOK”,然后选择“Continue”,接着选择“Yes”确认注册,最后输入你刚刚设置的一次性密码。系统会将你的公钥写入NVRAM的MOKList变量中,之后内核启动时会自动信任由对应私钥签名的模块。这个过程只需执行一次,除非你更换了密钥。

第四步:编写自动化签名脚本并处理现有模块

有了受信任的私钥,你就可以对模块进行签名了。内核源码树提供了一个签名工具 sign-file,它通常位于 /usr/src/linux-headers-$(uname -r)/scripts/ 目录下。你需要先编译它:

cd /usr/src/linux-headers-$(uname -r)/scripts
sudo gcc -o sign-file sign-file.c -lcrypto

编译完成后,你可以手动对单个模块签名,但更高效的做法是编写一个脚本,批量处理所有需要签名的第三方模块。创建一个脚本文件 /usr/local/bin/sign-modules.sh:

#!/bin/bash

KERNEL_VER=$(uname -r)
KEY_DIR="/usr/src/linux-headers-${KERNEL_VER}/certs"
SIGN_TOOL="/usr/src/linux-headers-${KERNEL_VER}/scripts/sign-file"
KEY_FILE="${KEY_DIR}/signing_key.pem"

if [ ! -f "$SIGN_TOOL" ]; then
    echo "Error: sign-file tool not found. Please compile it first."
    exit 1
fi

if [ ! -f "$KEY_FILE" ]; then
    echo "Error: Signing key not found at $KEY_FILE"
    exit 1
fi

# 查找所有未签名的内核模块
for module in $(find /lib/modules/${KERNEL_VER} -name "*.ko" -type f); do
    # 检查模块是否已经包含签名
    if ! modinfo "$module" | grep -q "signer:"; then
        echo "Signing unsigned module: $module"
        sudo ${SIGN_TOOL} sha256 ${KEY_FILE} ${KEY_FILE} ${module}
    else
        # 可选:强制重新签名所有模块,覆盖已有签名
        # echo "Re-signing module: $module"
        # sudo ${SIGN_TOOL} sha256 ${KEY_FILE} ${KEY_FILE} ${module}
        :
    fi
done

赋予脚本执行权限:

sudo chmod +x /usr/local/bin/sign-modules.sh

运行脚本前,你需要明确哪些模块需要签名。如果你安装了NVIDIA驱动、VirtualBox内核模块、ZFS文件系统或者任何通过DKMS(动态内核模块支持)编译的模块,它们默认都是未签名的。执行脚本:

sudo /usr/local/bin/sign-modules.sh

脚本会遍历模块目录,对每一个没有签名的模块使用你的私钥进行SHA-256签名。签名信息会直接追加到模块文件的ELF结构末尾,不会影响模块的正常功能。

第五步:处理DKMS模块的自动签名

DKMS是Debian系统中管理动态内核模块的常用框架。每当内核版本更新时,DKMS会自动重新编译这些模块。如果你只手动签名一次,内核升级后新编译的模块又会变成未签名状态,导致系统启动时驱动加载失败。你需要配置DKMS在编译完成后自动调用签名工具。

创建一个DKMS签名钩子脚本 /etc/dkms/sign_module.sh:

#!/bin/bash

KERNEL_VER=$1
MODULE_PATH=$2

KEY_DIR="/usr/src/linux-headers-${KERNEL_VER}/certs"
SIGN_TOOL="/usr/src/linux-headers-${KERNEL_VER}/scripts/sign-file"
KEY_FILE="${KEY_DIR}/signing_key.pem"

if [ -f "$SIGN_TOOL" ] && [ -f "$KEY_FILE" ]; then
    echo "DKMS: Signing module $MODULE_PATH"
    ${SIGN_TOOL} sha256 ${KEY_FILE} ${KEY_FILE} ${MODULE_PATH}
else
    echo "DKMS: Signing tools or key not found, module not signed."
fi

赋予执行权限:

sudo chmod +x /etc/dkms/sign_module.sh

然后配置DKMS在构建后调用此脚本。编辑 /etc/dkms/framework.conf 文件,添加或修改以下行:

POST_BUILD="/etc/dkms/sign_module.sh $kernelver $dkms_module_path"

这样,每次DKMS完成模块编译后,都会自动使用你的私钥对新模块进行签名,确保在内核强制验证模式下这些模块依然可以正常加载。

第六步:开启内核强制签名验证

完成所有模块的签名并验证它们都能正常加载后,就可以开启强制验证了。最直接的方法是修改内核引导参数。编辑 /etc/default/grub 文件,找到 GRUB_CMDLINE_LINUX_DEFAULT 这一行,在其中添加 module.sig_enforce=1 参数:

GRUB_CMDLINE_LINUX_DEFAULT="quiet module.sig_enforce=1"

如果你希望更加激进,可以同时锁定模块加载器,防止在运行时通过任何方式加载新模块:

GRUB_CMDLINE_LINUX_DEFAULT="quiet module.sig_enforce=1 modules_disabled=1"

注意:设置 modules_disabled=1 后,即使是root用户也无法在系统运行期间加载或卸载任何内核模块。这意味着像USB设备热插拔驱动、某些按需加载的文件系统模块都将无法工作。除非你完全清楚系统的硬件环境和运行需求,否则不建议在生产环境中锁定模块加载器。

更新GRUB配置并重启:

sudo update-grub
sudo reboot

重启后,验证配置是否生效:

cat /sys/module/module/parameters/sig_enforce

如果返回Y,说明内核强制签名验证已成功启用。你可以尝试加载一个未签名的测试模块,系统将直接拒绝并记录一条“Module verification failed”的错误日志。

深度防御:结合Secure Boot与完整性监控

内核模块签名验证只是内核自我保护的一环。如果你的硬件支持UEFI,应当同步开启Secure Boot。Debian的shim引导加载程序会验证GRUB和内核的签名,而MOK机制则负责内核模块的签名信任链。两者结合,可以构建从固件到内核模块的完整信任链。任何在启动前或运行时被篡改的模块都无法通过验证。

此外,你应当配置审计系统来监控模块加载事件。安装auditd并添加规则:

sudo auditctl -a always,exit -F arch=b64 -S init_module -S finit_module -k module_load

这条规则会记录所有成功和失败的模块加载尝试。结合日志分析工具,你可以实时发现攻击者试图加载恶意模块的行为。即使攻击者获得了root权限,只要他没有你的MOK私钥,就无法编译出能通过验证的模块,而强制加载的尝试会被审计系统完整记录下来。

维护与故障排查要点

在日常维护中,如果你需要安装新的内核模块,必须先用你的私钥对其进行签名。如果你丢失了 signing_key.pem 文件,将无法为任何新模块签名,除非你重新生成密钥并再次通过MOK注册流程。因此,务必将私钥备份到离线加密存储介质中。

如果系统在开启强制验证后无法启动,通常是因为某个关键驱动模块未签名。你可以在GRUB启动菜单中按e键临时编辑引导参数,删除 module.sig_enforce=1,然后启动系统进行修复。使用 dmesg | grep "module verification failed" 命令可以快速定位是哪个模块签名失败。

对于使用自定义内核的场景,你可以在编译内核时直接设置 CONFIG_MODULE_SIG_FORCE=y,并将你的公钥哈希直接嵌入内核镜像,这样无需依赖MOK机制。但对于大多数使用Debian预编译内核的用户,本文描述的MOK注册加手动签名的方案是最灵活且不破坏系统完整性的选择。