在默认的Ubuntu服务器或桌面安装中,内核会加载大量通用模块以兼容各种硬件和软件场景。这些自动加载的模块中,很多在你的实际工作负载中根本用不到。每一个闲置的内核模块都是一个潜在的攻击入口,内核漏洞一旦被利用,攻击者就能获得最高权限。减小内核攻击面的核心逻辑不是修补漏洞,而是让攻击者根本找不到可用的代码路径。禁用不必要的内核模块,就是物理上移除这些代码路径。
理解内核模块的加载机制Linux内核模块加载主要依赖两个层面。第一个是硬件探测,udev通过设备ID匹配内核中的模块别名表,自动加载对应驱动。第二个是功能依赖,当某个子系统被调用时,内核会尝试加载相关的功能模块。比如挂载一个ext4分区,如果ext4模块未加载,内核会自动加载。这两个机制共同导致了一个结果:你插上一个U盘,可能同时加载了usb_storage、uas、以及文件系统驱动等多个模块。即使你从未使用过某些功能,只要内核配置中编译了这些模块,它们就可能在某些条件下被触发加载。Ubuntu的默认内核配置非常宽泛,几乎把所有能编译成模块的驱动都编译了,这是为了方便普通用户,但对安全加固来说却是负担。
第一步:摸清当前已加载的模块加固之前,必须清楚系统当前的状态。使用lsmod命令可以列出所有已加载的内核模块及其依赖关系。输出第一列是模块名,第二列是占用内存大小,第三列是依赖该模块的其他模块数量。
lsmod | head -20
这个命令会显示模块名称、大小和使用计数。重点关注使用计数为0的模块,它们没有被其他模块依赖,理论上可以直接卸载。但要小心,有些模块虽然计数为0,却是某些用户空间进程正在使用的硬件驱动,比如显卡驱动。更深入的方法是查看每个模块的具体信息:
modinfo floppy
这个命令会显示floppy模块的描述、作者、依赖、别名等信息。描述字段能帮你判断这个模块的用途,别名字段显示了哪些硬件ID会触发这个模块的自动加载。如果你看到一堆PCI或USB设备ID,而你的服务器根本没有这些物理接口,那这个模块就是明显的冗余目标。
第二步:识别可禁用的目标模块以下是在Ubuntu服务器上几乎总是可以安全禁用的模块类别。第一类是传统硬件驱动,包括软盘驱动floppy、并行端口parport和parport_pc、PS/2鼠标和键盘驱动psmouse和serio_raw、以及游戏端口joydev。现代服务器通常通过IPMI或USB连接控制台,这些老旧接口的驱动完全没有存在的必要。第二类是文件系统模块,如果你的服务器只使用ext4和xfs,那么vfat、msdos、ntfs、hfs、hfsplus、udf、isofs这些模块都可以移除。特别是vfat和ntfs,它们经常被用于USB攻击,一个恶意构造的文件系统镜像可能触发内核漏洞。第三类是网络协议模块,比如appletalk、ipx、decnet、rds、tipc、dccp、sctp。这些协议在现代数据中心几乎绝迹,但内核仍然默认提供支持。第四类是红外和蓝牙模块,服务器上出现irda、bluetooth、btusb这些模块本身就是不正常的。第五类是摄像头和音频驱动,uvcvideo、snd_hda_intel等模块在服务器上完全多余。
对于桌面用户,禁用范围需要更谨慎,但仍有大量优化空间。比如你不会同时使用所有文件系统类型,不会同时使用蓝牙和Wi-Fi的所有驱动变体。关键是实事求是地根据你的硬件清单和工作负载来决策,而不是盲目套用别人的黑名单。
第三步:立即卸载并验证影响在永久禁用之前,先尝试手动卸载模块,观察系统是否出现异常。使用modprobe -r命令卸载模块:
sudo modprobe -r floppy
如果模块正在被使用,这个命令会报错。如果卸载成功,运行dmesg查看内核日志尾部,确认没有驱动报错。然后继续正常使用系统几分钟,观察网络、存储、外设是否工作正常。如果卸载后系统出现异常,立即用modprobe重新加载:
sudo modprobe floppy
这个测试阶段非常重要,因为有些依赖关系是隐式的。比如卸载一个看似无关的i2c控制器驱动,可能导致温度传感器失效,进而影响风扇控制。不要跳过这一步,尤其是在生产环境操作时。
第四步:永久禁止模块加载验证通过后,通过内核模块黑名单实现永久禁用。在/etc/modprobe.d/目录下创建一个配置文件,比如blacklist-security.conf:
sudo nano /etc/modprobe.d/blacklist-security.conf
文件内容格式为每行一个blacklist指令:
# 禁用传统硬件驱动 blacklist floppy blacklist parport blacklist parport_pc blacklist psmouse blacklist serio_raw blacklist joydev # 禁用不必要的文件系统 blacklist vfat blacklist msdos blacklist ntfs blacklist hfs blacklist hfsplus blacklist udf blacklist isofs # 禁用不必要的网络协议 blacklist appletalk blacklist ipx blacklist decnet blacklist rds blacklist tipc blacklist dccp blacklist sctp # 禁用红外和蓝牙 blacklist irda blacklist bluetooth blacklist btusb
黑名单只是阻止模块自动加载,但并不能阻止手动加载或依赖加载。如果某个模块被其他模块依赖,内核仍然会加载它。要实现彻底禁止,需要使用install指令将模块的加载操作重定向到/bin/false:
# 彻底禁止模块加载 install floppy /bin/false install vfat /bin/false install ntfs /bin/false
install指令告诉modprobe,当有人请求加载这个模块时,执行/bin/false而不是真正的加载操作。这比blacklist更强硬,连手动加载都能阻止。配置完成后,更新initramfs以确保这些规则在引导早期就生效:
sudo update-initramfs -u
这一步至关重要,因为有些模块在initramfs阶段就被加载了,如果只修改/etc/modprobe.d而不更新initramfs,这些模块在下次重启时仍然会出现。
第五步:内核命令行参数加固除了模块黑名单,内核命令行参数提供了更深层的控制。编辑/etc/default/grub文件,在GRUB_CMDLINE_LINUX_DEFAULT变量中添加安全相关参数:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash modprobe.blacklist=floppy,parport,parport_pc modprobe.blacklist=vfat,msdos,ntfs modprobe.blacklist=appletalk,ipx,dccp,sctp"
modprobe.blacklist参数在内核启动早期就生效,比用户空间的modprobe配置更早。你还可以添加slab_nomerge参数防止内核合并相似的slab缓存,这能增加某些堆溢出漏洞的利用难度。module.sig_enforce=1参数强制内核只加载签名模块,虽然Ubuntu默认不启用模块签名强制,但在高安全环境中可以考虑配合自定义签名使用。修改grub配置后必须更新:
sudo update-grub
重启后,检查内核命令行是否生效:
cat /proc/cmdline第六步:编译自定义内核作为终极方案
对于安全要求极高的环境,模块黑名单仍然不够彻底。模块文件本身还存在于/lib/modules/目录中,如果攻击者获得了root权限,完全可以删除黑名单配置然后手动insmod加载模块。最彻底的方法是从内核编译阶段就移除不需要的模块。Ubuntu提供了相对简便的内核编译流程。首先安装编译依赖:
sudo apt build-dep linux
然后获取内核源码,使用make localmodconfig命令可以基于当前已加载的模块生成最小化配置。这个命令会分析lsmod的输出,只保留当前正在使用的模块,其他全部禁用。但要注意,在执行localmodconfig之前,确保你已经插入了所有可能使用的设备,比如USB存储、网络适配器等,否则它们的驱动会被意外排除。更可控的方法是手动执行make menuconfig,逐项审查内核配置。在设备驱动、文件系统、网络协议等菜单中,将不需要的项目设置为关闭状态,而不是编译为模块。这样生成的vmlinuz镜像本身就小很多,攻击面也从根源上被削减了。
第七步:建立持续监控和审计机制安全加固不是一次性工作。内核模块的加载情况可能因为软件更新、硬件更换、配置变更而发生变化。建立一个简单的审计脚本,定期检查已加载模块列表与预期基线之间的差异:
#!/bin/bash
BASELINE="/etc/modules-baseline.txt"
CURRENT=$(lsmod | awk '{print $1}' | sort)
if [ ! -f "$BASELINE" ]; then
echo "$CURRENT" > "$BASELINE"
echo "基线已创建"
exit 0
fi
DIFF=$(diff <(cat "$BASELINE") <(echo "$CURRENT"))
if [ -n "$DIFF" ]; then
echo "警告:内核模块列表发生变化"
echo "$DIFF"
fi
将这个脚本加入crontab,每小时或每天执行一次。同时配置auditd审计modprobe和insmod的调用,记录所有模块加载操作。在/etc/audit/rules.d/目录中添加规则:
-w /sbin/modprobe -p x -k module_load -w /sbin/insmod -p x -k module_load -w /sbin/rmmod -p x -k module_load
这样任何模块的加载和卸载操作都会被记录到审计日志中,便于事后追溯。
常见陷阱和注意事项在禁用模块的过程中,有几个容易踩的坑需要特别注意。第一个是USB键盘鼠标在服务器上的依赖。很多服务器虽然通过IPMI或远程桌面管理,但在机房现场操作时可能插USB键盘。如果你禁用了usbhid模块,USB键盘将无法工作。第二个是文件系统模块的误伤。即使你的根分区使用ext4,你仍然可能挂载ISO镜像,这需要isofs和loop模块。如果你做容器化部署,overlay模块是Docker运行的必要条件。第三个是虚拟化环境中的特殊驱动。在云服务器或虚拟机中运行Ubuntu时,磁盘通常是virtio_blk驱动,网卡是virtio_net驱动。这些驱动不能禁用,否则系统无法启动。第四个是某些安全软件和监控代理会依赖特定的内核模块,比如auditd需要audit模块,部分EDR产品需要加载自己的内核驱动。在禁用之前,务必检查这些依赖关系。
还有一个容易被忽略的点是内核模块的参数。有些模块即使加载了,通过参数可以限制其功能,从而减小攻击面。比如usb_storage模块可以通过参数限制只读挂载,蓝牙模块可以禁用某些不安全的协议版本。在/etc/modprobe.d/中可以使用options指令设置模块参数:
options usb_storage delay_use=0
这比完全禁用更灵活,适用于那些你需要保留基础功能但想限制其攻击面的场景。
效果验证和量化评估加固完成后,用具体数据衡量效果。对比加固前后的lsmod输出行数,一个典型的Ubuntu Server 22.04默认安装后可能有120到150个已加载模块,经过合理精简后可以降到60到80个。每个被移除的模块都意味着对应的内核代码不再驻留在内存中。使用kallsyms可以查看内核符号表的大小,模块越少,可用的内核函数指针就越少,漏洞利用的难度就越大。还可以使用内核自带的漏洞缓解统计来评估:
cat /sys/kernel/security/lsm
结合AppArmor或SELinux的强制访问控制,模块减少带来的攻击面缩减效果会被进一步放大。安全是一个纵深防御体系,内核模块精简是这个体系中基础但关键的一环。
