4、Bootloader安全:GRUB2与shim,MOK管理,内核完整性校验
好,我们继续往下走。上一章聊了UEFI安全启动的硬件根基,那层信任锚点算是扎下去了。但说实话,光有固件层面的校验还不够——你想想看,固件验完Bootloader,Bootloader还得去加载内核,这中间要是被人动了手脚,前面全白干。
这一章,我们就来拆解Bootloader这一环。我个人习惯把Bootloader比作「信任传递的桥梁」——它得接住固件递过来的信任,再稳稳地交到内核手里。桥断了,整个启动链就塌了。
4.1 GRUB2:不只是个菜单
很多人对GRUB2的印象就是那个选系统的菜单界面。嗯,确实,它最常露脸的就是那个界面。但GRUB2真正的核心能力,是它作为第二阶段Bootloader,负责加载内核和initramfs,并且——关键来了——它自己也得被验过。
在UEFI安全启动的环境下,GRUB2的二进制文件(通常是grubx64.efi)必须经过签名。固件在启动它之前,会检查这个签名是否在DB(允许数据库)里。如果签名不对,或者压根没签名,机器直接拒绝启动。
核心要点:GRUB2本身是UEFI安全启动链上的第一个「可执行代码」环节。它必须被签名,且签名必须可信。
我在项目中遇到过一种情况:某团队自己编译了GRUB2,加了定制模块,结果忘了签名。刷到机器上,重启——黑屏,连错误提示都没有。后来查了半天,才发现是安全启动把未签名的grubx64.efi给拦了。从那以后,我每次编译完GRUB2,第一件事就是检查签名状态。
4.2 shim:那个「中间人」
这里有个现实问题:GRUB2是开源软件,社区维护,版本迭代快。如果每次GRUB2更新,都得让微软给它签一遍,那效率太低了。而且,有些Linux发行版有自己的签名证书,但微软的DB里不一定有。
于是就有了shim。shim是一个极简的Bootloader,它的唯一使命就是——被微软签名,然后去验证并加载GRUB2。
说白了,shim就是个「中间人」。它本身很小,代码量极少,审查起来容易。微软只给shim签名,然后shim再去验GRUB2的签名。GRUB2的签名可以用发行版自己的证书,只要这个证书的哈希被记录在shim的MOK(Machine Owner Key)列表里就行。
| 组件 | 签名方 | 验证方 | 说明 |
|---|---|---|---|
| shim.efi | 微软 | UEFI固件 | 被固件直接验签,加载后接管控制权 |
| GRUB2 (grubx64.efi) | 发行版/自签名 | shim | shim验其签名,通过后才加载 |
| 内核 (vmlinuz) | 发行版/自签名 | GRUB2 | GRUB2验内核签名,通过后才启动 |
这个链条你看明白了吗?信任从固件到shim,从shim到GRUB2,再从GRUB2到内核。每一环都验上一环的签名,环环相扣。
4.3 MOK管理:你自己的信任锚
MOK,全称Machine Owner Key。这玩意儿是干嘛的?简单说,它允许你自己管理哪些证书和哈希是可信的,而不需要去改UEFI固件里的DB。
为什么要搞这么一套?因为改DB太麻烦了。很多机器的固件设置界面里,安全启动相关的选项藏得很深,甚至有些OEM直接锁死了DB,不让你改。MOK就绕过了这个限制——它在shim层面维护了一个独立的信任列表。
MOK的管理工具是mokutil。我常用的几个命令:
# 查看当前已注册的MOK列表
mokutil --list-enrolled
# 查看待注册的MOK(新添加的还没生效)
mokutil --list-new
# 导入一个新MOK(需要重启后通过shim界面确认)
mokutil --import /path/to/my.der
# 删除一个MOK
mokutil --delete <hash-or-description>
小技巧:用mokutil --import添加MOK后,重启时会自动进入shim的MOK管理界面(蓝底白字)。你需要在那里手动确认「Enroll MOK」,输入密码,才能生效。这一步很多人会忽略,以为命令执行完就完了。
我曾经帮一个客户排查问题,他编译了自定义内核,也签了名,但死活启动不了。远程一看,MOK根本没注册成功——他以为mokutil --import完事大吉,结果重启后没去shim界面确认。嗯,这种坑我踩过一次就记住了。
4.4 内核完整性校验:最后一关
GRUB2加载内核之前,会做最后一件事:校验内核的签名。这一步用的是PE签名验证——因为Linux内核在UEFI环境下,本质上也是一个PE格式的EFI应用程序(vmlinuz.efi)。
GRUB2会检查内核文件的签名是否匹配它信任的证书。这个证书可以是MOK里的,也可以是DB里的。如果签名无效,GRUB2会拒绝加载内核,直接报错。
注意:内核完整性校验不光是验签名。GRUB2还会校验内核的测量值(如果启用了TPM),把内核的哈希值扩展到PCR寄存器里。这样,即使攻击者绕过了签名校验(比如利用了某些漏洞),TPM也能感知到内核被篡改过。
这里有个细节:GRUB2的verification模块负责签名校验。你可以通过check_signatures和enforce_signatures这两个变量来控制行为:
# 在GRUB2配置文件中设置
set check_signatures=enforce
# 或者更严格
set check_signatures=force
enforce模式下,所有未签名的文件都会被拒绝加载。force模式更狠,连已经加载的模块都得重新验一遍。我一般建议生产环境用enforce,开发环境可以暂时关掉,但上线前必须打开。
4.5 完整流程:一张图说清楚
说了这么多,不如一张图来得直观。下面这张SVG图,把整个Bootloader安全启动链画清楚了:
你看,从UEFI固件到shim,再到GRUB2,再到内核,最后到操作系统——每一层都做了签名验证。这就是所谓的「信任链传递」。任何一环断了,启动就会失败。这不是bug,这是feature——它保证了只有你信任的代码才能在你的机器上运行。
4.6 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 签名算法不匹配:有些老版本的shim只支持SHA-256哈希,你用SHA-512签的GRUB2,shim认不出来。我建议统一用SHA-256,兼容性最好。
- MOK证书格式:shim只认DER格式的X.509证书。别拿PEM格式往里塞,会报错。用
openssl x509 -in cert.pem -outform DER -out cert.der转一下就行。 - GRUB2模块签名:如果你加载了额外的GRUB2模块(比如
lvm.mod、luks.mod),这些模块也得签名。否则在enforce模式下,GRUB2会拒绝加载它们。 - 内核签名后别改:内核文件签名之后,哪怕你只改了一个字节,签名就失效了。我曾经手贱用
strip命令去掉了内核的调试符号,结果启动失败——因为签名校验没通过。
我的习惯:每次更新内核或GRUB2之后,我都会用sbverify --list /boot/vmlinuz-xxx和pesign -S -i /boot/efi/EFI/grub/grubx64.efi检查一下签名状态。确认没问题了,再重启。这个习惯救过我很多次。
好了,Bootloader安全这块就聊到这儿。记住一句话:信任链的每一环,都得自己亲手验过。别偷懒,别侥幸。