内核模块安全:可加载内核模块与 LSM 框架

Ring 0 的权重:为什么内核模块是最高危的攻击面

内核态与用户态的分界是整个 Linux 安全模型的地基:用户态进程权限再大(哪怕 root 加全部 capability),闯祸的上限也被内核约束在系统调用面之内;而内核模块运行在 Ring 0,与内核本体同等权限——它能遍历任意物理内存、改写系统调用表、注册协议处理钩子、拦截任何一条网络数据。一个恶意模块就是一次「内核被替换」:历史上的 LKM rootkit 用 getdents 钩子隐藏进程与文件、用协议钩子窃听流量、改写 credential 结构把任意进程提权成 root——此时用户态的一切检测工具(ps、ls、netstat、审计日志)都跑在被污染的内核之上,检测结果本身不可信。合法模块的风险同样真实:out-of-tree 模块的任何一个空指针解引用就是整机 panic;第 6 篇讲过 capability 把「全权的 root」拆成离散权限,而 CAP_SYS_MODULE 加上有权限的模块文件,几乎等价于「可以往内核注入任意代码」。所以模块面的防御目标有两层:不让没被信任链覆盖的代码进内核(签名体系),以及运行期把加载通道收紧(加载控制与白名单审计)。容器会把这两层的后果放大——所有容器共享同一个内核,宿主机上一个坏模块影响的是整机全部租户,这篇最后一节展开。

flowchart TD
    A["模块进入内核的四条路"] --> B["发行版仓库的签名模块<br/>受 CONFIG_MODULE_SIG 保护"]
    A --> C["第三方 out-of-tree 模块<br/>DKMS 编译后用 MOK 签名"]
    A --> D["管理员手工 insmod<br/>强制模式下验签失败被拒"]
    A --> E["容器进程请求加载<br/>需要 CAP_SYS_MODULE"]
    E --> F{"容器是否持有<br/>CAP_SYS_MODULE?"}
    F -- "默认不持有 → 加载失败" --> G["容器边界仍然有效"]
    F -- "--privileged 或显式授予<br/>→ 模块进入宿主内核" --> H["影响所有容器与宿主机<br/>内核 panic = 全部容器同时死"]
    B --> I["Ring 0:内核唯一执行环境"]
    C --> I
    D --> I
    H --> I

模块签名:CONFIG_MODULE_SIG、Secure Boot 与 DKMS

发行版内核的签名链路是这样工作的:编译内核时开启 CONFIG_MODULE_SIG,构建系统生成一把 X.509 私钥、把公钥编入内核信任环,modules_install 时用私钥给每个 .ko 追加 PKCS#7 签名段,加载时内核用信任环里的公钥验签。配套的 CONFIG_MODULE_SIG_FORCE(或启动参数 module.sig_enforce=1)决定验签失败是直接拒绝还是仅告警并给内核打 taint 标记:

$ grep -E 'CONFIG_MODULE_SIG(_FORCE)?=' /boot/config-$(uname -r)
CONFIG_MODULE_SIG=y
CONFIG_MODULE_SIG_FORCE=y
$ modinfo overlay | grep -E 'signer|sig_hashalgo'
signer:         Debian Secure Boot CA
sig_key:        5C:49:24:...
sig_hashalgo:   sha256

强制模式下,无签名或密钥不在信任环的模块会被直接拒绝,错误码是 EKEYREJECTED:

$ sudo insmod ./demo.ko
insmod: ERROR: could not insert module demo.ko: Key was rejected by service
$ sudo dmesg | tail -1
[ 8812.442981] Loading of module with unavailable key is rejected

Secure Boot 把这条链延伸到固件:UEFI 固件只引导被 db 白名单密钥签名的 shim 与 bootloader,shim 再验证内核签名,内核在 lockdown 模式下自动收紧——未签名模块、hibernation、/dev/mem 直写统统拒绝,形成「固件 → 引导 → 内核 → 模块」的完整信任链。自编第三方模块(NVIDIA 闭源驱动、ZFS、GPU 厂商驱动)走 DKMS:每次内核升级自动重编译,并用 MOK(Machine Owner Key)签名——把 MOK 公钥经 mokutil --import 登记进固件的 MokList(重启后在蓝色 MOK 管理界面确认),DKMS 用配套私钥签名后的模块即可在强制模式下加载。运维上记住两点:模块签名私钥必须与内核镜像同等保管——拿到它等于拿到「给这台机器内核注入代码」的通行证;内核升级后旧签名不迁移,DKMS 的价值正是把「重编译 + 重签名」自动化。

运行时加载控制:blacklist、modules_disabled 与白名单审计

签名解决「谁被信任」,加载控制解决「信任的代码什么时候允许进来」。三道常用闸门。第一道是 modprobe 黑名单——/etc/modprobe.d/blacklist.conf 里写 blacklist usb-storage 再补一行 install usb-storage /bin/false:前者阻止按别名自动加载,后者把手动加载也变成假命令直接失败;它防的是「插入 U 盘自动挂载体」这类意外,不是对抗蓄意越权(root 仍可绕过 kmod 封装)。第二道是运行时总闸 kernel.modules_disabled:

$ sudo sysctl -w kernel.modules_disabled=1
kernel.modules_disabled = 1
$ sudo modprobe dummy
modprobe: ERROR: could not insert 'dummy': Operation not permitted
$ sudo sysctl -w kernel.modules_disabled=0
sysctl: setting key "kernel.modules_disabled" failed, ignoring: Operation not permitted

设为 1 后任何模块加载(含已加载模块的重新加载)都被拒绝,且不可逆——只能重启恢复。它的正确用法是「固定负载定型」:业务所需模块(网卡、文件系统、容器运行时要用的 overlay 与 br_netfilter)全部就位并验证后,再把总闸拉下。第三道是白名单审计:给生产基线固化一份 lsmod 清单,用 cron 做漂移对比:

# audit-modules.sh —— 与基线对比,发现不在清单内的已加载模块
lsmod | awk 'NR>1 {print $1}' | sort > /tmp/loaded.txt
comm -13 /etc/security/modules.baseline /tmp/loaded.txt \
  | grep -v '^$' && logger -t modaudit "发现未登记模块,请人工核查"

再配一条内核审计规则(auditctl -w /sys/module/ -p r)把「谁在查询模块状态」也留痕。三道闸门的共同前提要说清楚:它们防意外与纵深突破,不防已拿到 root 的对手绕路——登录与运维通道本身的管控是这一切的前置条件。

内核的 taint 标记与模块卸载的实况

lsmod 只是第一层信息,模块面的健康状态还要会读内核的 taint 标记——它告诉你「这个内核的信任纯度」有没有被打破:

$ cat /proc/sys/kernel/tainted
12288
$ cat /proc/modules | head -2
overlay 155648 2 - Live 0xffffa1c040000000 (O)
nvidia_drm 69632 5 - Live 0xffffa1c040020000 (OE)
$ dmesg | grep -i taint | tail -1
[   12.345678] overlay: module verification failed: signature and/or required key missing - tainting kernel

/proc/modules 里每个模块行尾括号内的字母是逐模块的 taint 标记:P 表示无签名或验签失败,O 表示 out-of-tree,E 表示在强制验签下未经许可被加载,M 表示在强制模式下加载,另有 G(GPL 许可)等;/proc/sys/kernel/tainted 的数字是全局位图的十进制值。生产节点的巡检标准可以定得很机械:模块清单无漂移、taint 标记无 P——出现 P 标记意味着签名闸门没有真正闭合,要么是有人手工塞了模块,要么是部署链路上某一步漏签了。模块卸载侧的实况也要心里有数:lsmod 第三列是被引用计数,模块被进程或其他模块引用时 rmmod 会以 is in use 拒绝,modprobe -r 才会按依赖关系逆序尝试连卸;内核不保证卸载模块后归还全部内存(存在已知泄漏的模块干脆以「禁止卸载」回避问题),所以「定期 rmmod 再 insmod 释放内存」的老经验在现代内核上不可靠,真正的内存问题应回到第 2 篇的内存回收体系去定位,而不是靠卸载模块碰运气。这两件事合起来的结论:把「taint 无 P、清单无漂移、无异常加载日志」三项写进节点巡检,比事后从碎片日志里抢救线索便宜得多。

LSM 框架:钩子机制与三种实现的选择

模块签名管「代码怎么进来」,LSM(Linux Security Modules)管「进来之后的内核操作能不能被策略拦截」——第 15 篇已详细写过 SELinux 与 AppArmor 的策略语法与排错,这里补齐框架视角。LSM 不是某个具体产品,而是内核在关键对象操作路径(打开文件、bind 端口、fork、挂载、ptrace、创建 user namespace……)预置的数百个钩子点,各安全模块向钩子注册裁决函数;内核按启动参数 lsm= 的顺序依次调用,全部放行才放行系统调用。机器上当前生效的模块列表在 /sys/kernel/security/lsm:

$ cat /sys/kernel/security/lsm
landlock,lockdown,yama,integrity,apparmor,bpf

这一行能读出关键事实:capability、yama、lockdown、landlock、bpf 这类非独占模块可以共存叠加,但「主策略模块」只有一个坑——SELinux/AppArmor/Smack/Tomoyo 互斥,不能同时启用。三种主流选择对应三种运维画像:SELinux(RHEL 系默认)标签世界加默认拒绝,强度最高、策略工程成本也最高,适合合规强约束环境;AppArmor(Ubuntu/Debian/SUSE 默认)按程序路径画像,人类可读、逐程序白名单,强度与成本都居中,是多数 Web 服务的合理默认;BPF LSM(内核 5.7+,需 CONFIG_BPF_LSM 且 lsm= 列表里含 bpf)用 eBPF 程序实现裁决逻辑——策略即代码,能做「只禁止容器进程创建 user namespace」这类传统策略语言写不出的上下文判断,代价是策略代码的正确性由你负责(一个逻辑错的 BPF 裁决函数可以拒绝全系统的一切操作),适合有平台工程团队把安全策略产品化的场景。选择建议浓缩成一句:没有专职安全团队就用发行版默认,有就叠加 BPF LSM 做精细补刀,不要幻想「同时上两个主模块」。BPF LSM 的策略程序骨架长这样:

/* deny_userns.bpf.c —— BPF LSM 示例:拒绝在宿主初始 user ns 下创建新 userns */
SEC("lsm/userns_create")
int BPF_PROG(deny_userns_create, const struct cred *cred)
{
    if (cred != current_cred())          /* 示意:可叠加任意上下文判断 */
        return 0;
    return -EPERM;                       /* 裁决函数返回负值 = 拒绝该操作 */
}

容器与宿主机模块:一荣俱荣、一损俱损

关键事实先摆出来:容器没有自己的内核。容器内 uname -r 与宿主机相同,容器进程通过系统调用请求内核服务,而模块加载(init_module/finit_module)作用于唯一的那颗内核。默认 Docker capability 集合不含 CAP_SYS_MODULE,容器里 modprobe 会以 Operation not permitted 失败——这是容器边界正常工作的表现;但只要一个容器拿到 --privileged(或显式授予 CAP_SYS_MODULE 且能读到模块文件),它就能往宿主内核注入模块,隔离的意义随之蒸发——这正是 privileged 容器被列为高危配置的根本原因之一。反向依赖同样真实:容器运行时依赖宿主机内核模块——overlay2 需要 overlay、Kubernetes ipvs 模式需要 ip_vs 系列、常见 CNI 需要 br_netfilter——节点镜像必须把这些模块预置就位(systemd-modules-load 预加载),避免首个 Pod 调度时才触发自动加载。由此得出容器环境的模块纪律:宿主机模块就是全体容器的内核依赖——镜像构建 CI 不装内核头文件、运行配置不给 CAP_SYS_MODULE、节点加固用「签名强制 + modules_disabled 定型」双保险、BPF LSM 拦截容器进程的 userns_create 企图——把「容器 → 内核」这条唯一真正穿透边界的路锁死。

为什么你的应用会受这个影响

四条可落地准则。其一,接手节点先看三个文件:/boot/config 里的 MODULE_SIG(_FORCE)、/sys/kernel/security/lsm、/proc/sys/kernel/modules_disabled——三行输出就是这台机器模块面的防御姿态,比任何口头承诺都可信。其二,业务容器永远不要 privileged:排障确需特权,用 --cap-add 逐项加并限时回收;CAP_SYS_MODULE/CAP_SYS_RAWIO 这类「内核注入级」能力不该出现在任何业务镜像的启动参数里。其三,把模块白名单与漂移告警纳入节点基线:新模块出现往往是安全事件的第一信号,审计脚本十行的成本远低于一次入侵的复盘成本。其四,自研与第三方模块一律走 DKMS 加 MOK 签名链,禁止把无签名 .ko 散落各节点手工加载;要下线的模块走正式流程从基线移除,而不是「先删了再说」。模块安全没有银弹,它是「信任链覆盖代码、运行期收紧通道、策略层兜底裁决」三层叠加的结果——每一层都便宜,缺任何一层都可能让前面几层白做。

小结

内核模块的安全是 Linux 安全的最后一道防线——签名验证+模块白名单+LSM 策略的三层防御——能不加载模块就不加载——是最安全的策略。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?