裸金属物理机安全加固实战:BMC 虚拟介质、Secure Boot Setup Mode 与 644 的 MOK 私钥,三道绕过 OS 层加固的口子

先说一个真实场景。红队打进内网后,目标机的 22 端口做了源 IP 白名单,业务网段进不去。但 nmap -sU -p623 一扫,同网段几十台机器的 IPMI 全开着,其中一台还能用 cipher 0 空认证进去。接下来的步骤没有任何"漏洞利用":Redfish 挂载一个定制的 rescue ISO,重启进救援系统,chroot 进原系统,把 /boot/initramfs-*.img 里塞进一段读取 initramfs 阶段 LUKS 密码的逻辑,写回盘里。下次运维正常重启,LUKS 密码就明文送到攻击者手里了。

整个过程里 SELinux 是 enforcing 的,auditd 规则一条不少,AIDE 基线也是干净的——因为它们都还没启动。

所以裸金属的加固顺序必须是从带外往下打,而不是从 /etc/ssh/sshd_config 往上看。下面按我实际落地的顺序,把每一层能查、能验、能踩的地方写清楚。

第 1 步:先收掉 BMC 的三扇门

带外管理面基本是裸金属独有的攻击面,也是最容易被"我不动它就没事"这句话耽误的一层。需要处理三件事:IPMI 认证强度、Redfish 会话、虚拟介质。

先确认 cipher 0 能否直接过认证。IPMI 2.0 的 RAKP 在 cipher suite 0 下认证算法是 None,只要用户名存在就能建会话:

# 从业务网段扫,别只在管理网扫
nmap -sU -p623 --script ipmi-version,ipmi-cipher-zero 10.10.10.0/24

# 证明:不提供密码也能拿到会话
ipmitool -I lanplus -C 0 -H 10.10.10.21 -U ADMIN -P x mc info
# 返回 Manufacturer Name / Firmware Revision 就说明中招

如果返回了信息,说明这台机的 BMC 允许 cipher 0。大部分厂商的修复方式是固件升级后在 Web 里禁用 cipher suite 0,或者把 IPMI over LAN 整个关掉,只留 Redfish over HTTPS。关掉之后用 ipmitool lan print 1 确认 IP Address Source 和网口模式,超微的板子可以额外打一条 ipmitool raw 0x30 0x70 0x0c 0 0 看是 Dedicated 还是 Shared——Shared 意味着 BMC 和业务网共享物理口,业务网被打穿后 BMC 是同一跳,这个必须改。

第二扇门是虚拟介质。这是"物理接触"能力的远程版本,一旦能被调起来,前面所有 OS 层加固都可以跳过。用 Redfish 直接把它弹掉,并且关掉写保护之外的挂载能力:

# 查当前挂载状态
curl -sk -u root:xxx https://bmc/redfish/v1/Managers/1/VirtualMedia/CD \
  | jq '.Image, .Inserted, .WriteProtected'

# 弹出
curl -sk -u root:xxx -X POST \
  https://bmc/redfish/v1/Managers/1/VirtualMedia/CD/Actions/VirtualMedia.EjectMedia \
  -H 'Content-Type: application/json' -d '{}'

# 断掉镜像引用,防止某些固件"弹出后仍保留 Image 字段可被重新 Insert"
curl -sk -u root:xxx -X PATCH \
  https://bmc/redfish/v1/Managers/1/VirtualMedia/CD \
  -H 'Content-Type: application/json' -d '{"Image":null,"Inserted":false}'

第三扇门是引导顺序。BIOS 里把网络引导(PXE/HTTP Boot)从 BootOrder 里删掉,只留本地盘;同时确认 bootctl status 里 Boot into FW 是 allowed 还是 supported,如果 BMC 有 "Front Panel Lockout" 之类的选项一并打开。

这一整套做完,BMC 才算从"运维便利入口"降级成"需要独立管理网 + 强认证"的设备。我们后来把这套流程固化成装机后自动跑的 playbook,机型选择上也开始卡这一条——管理口和业务口必须物理分离、Redfish 的 VirtualMedia 接口能通过 API 关掉而不是只能点 Web,轻云互联那批裸金属的 Redfish 是标准实现,脚本能直接跑通,省了不少手工进 KVM 的活。

第 2 步:Secure Boot 到底是 enabled (user) 还是 disabled (setup)

这一层最容易出的问题是:很多人看 BIOS 里 Secure Boot 是 "Enabled" 就以为生效了,实际上机器处于 Setup Mode,PK 为空,任何人可以自己 enroll 一套密钥,Secure Boot 等于形同虚设。

bootctl status | grep -E 'Secure Boot|Firmware'
# 可信状态:  Secure Boot: enabled (user)
# 假绿状态:  Secure Boot: disabled (setup)
#             或 enabled 但下面 efi-readvar 的 PK 是空的

# 需要 efitools 包
efi-readvar -v PK
# 输出 "Variable PK has no entries"  → Setup Mode,Secure Boot 没锁

efi-readvar -v db
efi-readvar -v dbx   # dbx 为空说明没有吊销列表,老 shim 照样能过

确认是 user mode 之后,再查一件事:MOK 里到底进了多少第三方证书。

mokutil --list-enrolled
# 典型输出里会混进 NVIDIA 闭源驱动、ZFS、VirtualBox 的 MOK

重点在这里——为了让这些 DKMS 模块能加载,签名私钥通常就躺在系统里:

ls -l /var/lib/shim-signed/mok/MOK.priv
# Ubuntu 上经常是 -rw-------  root root  → 合格
# 但不少自动化脚本为了"让非 root 的 dkms 也能签",把它 chmod 成了 0644

# 一旦是 0644,任意本地用户都能:
#   sign-file sha256 MOK.priv MOK.der evil.ko evil.ko.signed
# 然后用 insmod 加载任意内核模块,Secure Boot 全链路失效

这个点比想象中普遍。要么把权限收回 0600 并让 dkms 走 sudo,要么干脆用 mokutil --revoke-import 把不需要的 MOK 清掉,只保留必须的那一个。清理完记得 mokutil --list-enrolled 复查一遍。

还有一层容易漏:Secure Boot 只保证"从 firmware 到 kernel 的链是签过名的",不保证 kernel 之后的 initramfs 被验签。如果用的是分离的 /boot 分区加未签名 initramfs,攻击者仍然可以替换 initramfs(就是我开头那个故事)。要么上 UKI(Unified Kernel Image,把 kernel + initramfs + cmdline 一起签名),要么至少保证 /boot 在 LUKS 里面。

第 3 步:TPM2 + LUKS,PCR 别乱绑

有了 Secure Boot 的 user mode,TPM 绑定才有意义,因为 PCR 7 记录的就是 Secure Boot 的策略状态。

# 先确认 TPM 是真的,不是 Intel PTT / AMD fTPM 的伪装(能用但状态会丢)
cat /sys/class/tpm/tpm0/tpm_version_major

# 绑定 PCR 7(Secure Boot policy)+ 14(MOK 证书)
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7+14 /dev/nvme0n1p2

# 需要防"整机搬走 + TPM 在旁路"的场景,加 PIN
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7+14 \
  --tpm2-with-pin=yes /dev/nvme0n1p2

cryptsetup luksDump /dev/nvme0n1p2 | sed -n '/Tokens:/,/^$/p'
# 应该看到 systemd-tpm2 token 和它绑定的 Keyslot

几个必须知道的坑:

  • 别绑 PCR 4。PCR 4 是 Boot Manager Code,内核升级、grub 配置变更都会让它变,绑上去就是每次升级都进不去系统。
  • 一定保留 password keyslot。BIOS 固件升级、dbx 更新、MOK 证书变更都会改 PCR 7,TPM 解不开。每台机器至少留一个独立保管的口令槽,并在交付文档里写清楚"固件升级前先 systemd-cryptenroll --wipe-slot=tpm2"。
  • fTPM 不是 TPM。AMD fTPM 和 Intel PTT 在 S3/固件更新后可能重置 sealed blob,尤其老固件。关键业务机建议上独立 TPM 2.0 模块(dTPM)。
  • 盘被拔走:绑了 TPM 之后拔盘确实解不开,但如果盘上还有 password keyslot 且口令是弱口令,整个 TPM 绑定就白做了。cryptsetup luksDump 里看 keyslot 数量,交付前把临时 keyslot 全部 wipe 掉。

第 4 步:运行态 —— lockdown、kexec、IOMMU/ACS

Secure Boot 打开后,内核的 lockdown 通常不会自动进入 confidentiality 模式,需要显式指定。先在 cmdline 加:

# /etc/default/grub 或 UKI 的 cmdline
GRUB_CMDLINE_LINUX="... lockdown=confidentiality intel_iommu=on iommu=pt iommu.strict=1 audit=1 audit_backlog_limit=8192"

# 重启后确认
cat /sys/kernel/security/lockdown
# none [confidentiality] integrity

confidentiality 才是真正禁掉 kexec 的那个级别。如果只需要完整性,integrity 模式仍然允许 kexec 加载未签名内核,攻击者可以 kexec 一个自己的内核绕开所有运行时防护。额外再加一道:

echo 'kernel.kexec_load_disabled=1' > /etc/sysctl.d/99-kexec.conf
sysctl --system
# 验证:kexec_load 返回 EPERM 就算生效
kexec -l /boot/vmlinuz-$(uname -r) ; echo $?

IOMMU 部分,很多人只写了 intel_iommu=on 就以为有 DMA 隔离了。实际上还要看 ACS:

dmesg | grep -i -E 'DMAR|IOMMU enabled'

lspci -vvv | grep -A2 'Access Control Services' | grep ACSCtl
# 关键看 ACSCtl 这一行里 SrcValid 后面是 + 还是 -
# ACSCtl: SrcValid+ TransBlk+ ReqRedir+ ...   → 隔离有效
# ACSCtl: SrcValid- TransBlk- ...             → 同 root complex 下设备可互相 DMA

如果 PCH 下同时挂着 NVMe 和网卡,且网卡不支持 ACS,那么网卡控制器一旦被攻破,就能直接读 NVMe 的 DMA 内存——绕过所有 OS 层的文件权限和 SELinux。缓解方式是把敏感存储挪到 CPU 直连的 PCIe lane,或者退一步,用 iommu.strict=1 关掉 DMA 重映射的缓存窗口(性能有损,但禁止了利用时序窗口的 DMA 攻击)。不要用 pcie_acs_override=downstream 去"假装"支持 ACS,那是把隔离度往下调,方向和加固相反。

Thunderbolt/PCIe 热插拔设备是这类攻击的标准载体,服务器上如果是外置 PCIe 扩展箱,确认 Thunderbolt 域的安全策略:

cat /sys/bus/thunderbolt/devices/domain0/security
# none    → 任何设备插入即授权,最危险
# user    → 需要用户确认
# secure  → 需要质询-响应(推荐)
# dponly  → 只走 DP 通道,不给 PCIe

# 已接入设备的授权位,逐台核对
for d in /sys/bus/thunderbolt/devices/0-*; do
  echo "$d authorized=$(cat $d/authorized 2>/dev/null)"
done

第 5 步:auditd 的规则,究竟加载了没有

加固做完之后最怕的是"以为在审计"。auditd 有三处常见断链:

# 1) 当前内核里实际生效的规则
auditctl -l

# 对比磁盘上的规则文件
ls /etc/audit/rules.d/*.rules

# 如果不一致:改了 rules.d 但没生成 audit.rules,auditd 启动读的是
# /etc/audit/audit.rules,rules.d 只是 augenrules 的输入
augenrules --load
auditctl -l   # 复查

# 2) 看是否在丢事件
auditctl -s
# enabled 1 / failure 1 / lost 0
# lost 非 0 说明 backlog 不够,cmdline 里的 audit_backlog_limit 要往上调

第三个坑更隐蔽:-w /etc/shadow -p wa 这类文件监视规则只覆盖 VFS 层的写路径,绕过方式多到没必要列举。真正的关键操作应该挂 syscall 规则,并且注意 SELinux 的 dontaudit 规则会把一部分 AVC denial 静默掉,排查期需要 semodule -DB 打开。

还有一条:cmdline 里如果残留 audit=0(有些镜像模板带的),auditd 用户态进程会启动、规则会加载、auditctl -l 会显示正常,但内核压根不产生事件。cat /proc/cmdline | grep audit 看一眼,两秒钟的事。

收尾:开机 60 秒自检清单

把这套东西固化成一个开机自检脚本,每次变更/重启后跑一遍,比什么文档都管用:

#!/bin/bash
echo "== Secure Boot =="
bootctl status | grep 'Secure Boot'
efi-readvar -v PK | head -1
echo "== MOK 私钥权限 =="
stat -c '%a %U %n' /var/lib/shim-signed/mok/MOK.priv 2>/dev/null
echo "== TPM keyslot =="
cryptsetup luksDump "$(findmnt -no SOURCE /)" | grep -c systemd-tpm2
echo "== Lockdown / kexec =="
cat /sys/kernel/security/lockdown
sysctl -n kernel.kexec_load_disabled
echo "== IOMMU =="
dmesg | grep -ci 'IOMMU enabled'
lspci -vvv | grep -c 'ACSCtl:.*SrcValid+'
echo "== audit =="
grep -o 'audit=[01]' /proc/cmdline
auditctl -s | grep -E 'enabled|lost'
echo "== 623/udp =="
ss -lunp | grep -c ':623 '

加固这件事在裸金属上有个特点:OS 层的每一道防护,都有一个"还没启动"的时间窗。BMC 的虚拟介质、Secure Boot 的 Setup Mode、那颗 644 的 MOK 私钥、以及没人复核过的 auditd 规则,本质上是同一个问题——你验证的是配置文件,而不是运行时状态。把这四处的运行时验证补齐,剩下的才轮到 sshd_config 和 sysctl 那些老生常谈。