Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第九章 · IMA:把度量延伸到运行时

前面几章的可信启动讲到内核为止:固件度量 bootloader,bootloader 度量内核,然后信任链交接。但系统跑起来之后呢?每次执行的程序、加载的动态库、读入的配置,谁来度量?

答案是 IMA(Integrity Measurement Architecture,完整性度量架构):让内核自己接过这个角色,把度量延伸到运行时。

先说句实话:这一章是纯概念章,没有动手实验。原因很简单——Arch 官方内核没编译 CONFIG_IMA。我实测过 linuxlinux-hardened 都没有,检查方法:

zcat /proc/config.gz | grep CONFIG_IMA
# CONFIG_IMA is not set

Fedora / RHEL / openSUSE 默认是开的,想实操的读者本章末尾有路线。但 IMA 的思想价值不依赖动手:它是“信任链最后一环“的标准答案,也是面试和架构设计里绕不开的话题,值得讲透。

本章目标

  • 理解 IMA 在整条信任链中的位置:启动度量之后,运行时谁来接着度量
  • 掌握 IMA 的三要素:钩子点、策略、模板
  • 理解 boot_aggregate 如何把启动度量和运行时度量焊进同一条证据链
  • 从“记录“讲到“强制“:Appraisal 的三种模式和部署闭环
  • 了解 EVM 如何保护 IMA 的元数据,以及 IMA 在远程证明里的真正舞台
  • 正视 IMA 的局限性:它防什么、不防什么

定位:信任链的最后一环

回忆一下我们已经走过的链:

固件(度量)→ bootloader(度量)→ 内核 → ???

内核启动之后,这个链条就断了。系统接下来要跑成千上万个程序:init、登录管理器、shell、你敲下的每一条命令——它们都不在 PCR 0–9 的视野里。启动时一切正常,不代表运行时跑的东西也可信。

IMA 就是内核给出的答案:由内核自己在文件被使用的瞬间度量它,把结果 extend 进 PCR 10,并写进一份运行时度量日志(runtime measurement list)。

PCR 10 就是 IMA 的专用寄存器。第五章你在日志里见过它,现在可以补上它背后的故事了。

补上之后,整条信任链就完整了:

固件 ──度量──→ bootloader ──度量──→ 内核 ──度量(IMA)──→ 运行时的每个文件
PCR 0-9         PCR 4/11              PCR 10

注意角色变化:前两级是“上一级度量下一级“,到了 IMA 这里变成“自己度量自己管辖的东西“。内核不再是“被度量的对象“,而是“度量的执行者“。这个交接能成立,前提是内核本身可信——又回到了启动链:IMA 的一切结论,都建立在“内核是被可信启动链带上来的“这个前提上。链是环环相扣的,任何一环断了,下游全部作废。

度量机制三要素

要素一:钩子点(hook)

IMA 不是扫描磁盘,而是挂在内核的关键路径上,在文件“被使用“的那一刻触发度量。主要钩子点:

钩子触发时机说明
BPRM_CHECKexecve 执行程序每跑一个可执行文件就度量一次
MMAP_CHECK以可执行权限 mmap 文件动态库就是这么被抓到的
MODULE_CHECK加载内核模块
FIRMWARE_CHECK加载固件文件
KEXEC_KERNEL_CHECKkexec 加载新内核
FILE_CHECK文件被读取最重,什么文件都管,慎用
POLICY_CHECKIMA 策略本身变化度量“度量规则“的变化

注意 MMAP_CHECK 的设计:动态链接库里没有 execve,程序是靠 mmap(PROT_EXEC) 把 .so 映射进来的。如果只钩 execve,换掉一个 libc 就能神不知鬼不觉地污染所有程序——这个钩子堵的就是这条路。

要素二:策略(policy)

不是每个钩子、每个文件都度量,量什么由策略决定。策略是一组规则,写在 /sys/kernel/security/ima/policy

内核内置了几套策略,用内核参数选择,比如:

ima_policy=tcb

tcb(Trusted Computing Base,可信计算基)大致相当于:所有 exec + 所有可执行 mmap + 模块 + firmware。这是个合理的默认覆盖面。

自定义策略长这样:

measure func=BPRM_CHECK
measure func=MMAP_CHECK
appraise func=BPRM_CHECK fowner=0

语法很直白:measure(度量)或 appraise(校验),加触发条件,可加过滤(比如只管 root 拥有的文件 fowner=0)。上面这三行的意思是:所有被执行的程序都度量,所有可执行 mmap 都度量,但只有 root 拥有的程序在执行时才做签名校验——普通用户跑自己的脚本不拦。

策略还支持更细的过滤,比如按文件系统类型排除(fsuuid)、按 UID 圈定范围、用 dont_measure / dont_appraise 开白名单。调试 IMA 部署时很大一块工作就是打磨这些规则:覆盖面小了漏,大了性能吃不消还容易误伤。

注意:IMA 策略每次开机只能下发一次——写入 /sys/kernel/security/ima/policy 之后即锁定,直到下次重启。这个设计的用意是防止运行中的 root 篡改度量范围(把某类文件悄悄排除出去)。但它同时暴露了一个信任假设:IMA 防的是“文件被篡改“,防不了“root 在策略下发前抢跑“。所以正经部署里,策略必须在启动早期由 initramfs 下发,让前面几章讲的启动信任链把“策略下发“这一步也罩住。

要素三:模板(template)

每次度量记成日志里的一行,行的格式由模板决定:

  • ima:老式模板,md5 哈希,已过时
  • ima-ng:现代默认,sha256 + 文件路径
  • ima-sig:在 ima-ng 基础上附带文件签名

一条真实的 ima-ng 日志行长这样:

10  d879a1f8... ima-ng  sha256:9f2c3b...  /usr/bin/cmatrix

逐字段拆解:

字段含义
10PCR 编号,这条度量 extend 进了 PCR 10
d879a1f8...模板哈希:整条记录内容的哈希,extend 进 PCR 的就是它
ima-ng模板名
sha256:9f2c3b...文件内容哈希
/usr/bin/cmatrix文件路径

:第二列不是文件哈希!它是“模板哈希“——把模板名、文件哈希、路径等整条记录打包算的哈希。PCR 10 里链的是第二列。所以重放 IMA 日志(第五章练过的手艺在这里原样适用)时,你要链的是每行的第二列,不是文件哈希。

日志本身在 securityfs 里,用 root 直接看:

sudo head -5 /sys/kernel/security/ima/ascii_runtime_measurements
10 d879... ima-ng sha256:9f2c... boot_aggregate
10 3b1a... ima-ng sha256:44e0... /usr/lib/systemd/systemd
10 7c52... ima-ng sha256:1a9d... /usr/lib/ld-linux-x86-64.so.2
10 90ef... ima-ng sha256:c331... /usr/lib/libc.so.6
...

第一行永远是 boot_aggregate(下面单独讲),往后依次是 init 程序、动态链接器、libc……系统刚刚启动时最早被执行和映射的文件。光是这前几行,你就能看见 MMAP_CHECK 钩子在起作用:ld-linux 和 libc 不是被 exec 的,是被 mmap 进来的,照样被抓。

重放的思路和第五章一模一样:从全零开始,把每行第二列依次 extend,最后得到的值应该等于 tpm2_pcrread sha256:10 的读数。对得上,说明这份日志没被增删改过——哈希链又一次充当了“日志完整性校验和“。

boot_aggregate:两条链的焊缝

IMA 日志的第一条记录很特殊,叫 boot_aggregate(启动聚合)。它把 PCR 0 / 2 / 4 / 7 等启动期 PCR 的值聚合成一个哈希,作为运行时日志的第一环。

为什么要这么做?因为没有它,IMA 日志就是一份悬空的清单:它可以来自任何一次启动、任何一台机器。有了 boot_aggregate,运行时日志就锚定在了这次具体的启动上——验证方能确认“这份运行时日志确实接在这台机器的这次启动之后“。启动度量和运行时度量,从此焊在同一条证据链里。

这也回答了一个你可能会有的疑问:为什么 IMA 日志不能只靠 PCR 10 验证?因为 PCR 10 是日志的链上校验和,而 boot_aggregate 把日志的头接进了启动链。两者合起来,证据才完整。

Appraisal:从记录到强制

到目前为止 IMA 只做了“记录“——还记得第八章那句话吗,度量只是记录,不是拒绝。IMA 的 appraise(校验评估)功能是把记录变成强制的部分。

机制:文件的“身份证明“存在扩展属性(xattr)security.ima 里,有两种形式:

  • imahash:直接存文件哈希。不需要密钥体系,但 xattr 谁都能改——防君子不防小人,只适合做基线核对
  • imasig:存私钥对文件哈希的签名。校验时需要公钥在内核的 .ima keyring(密钥环)里

appraise 有三种模式,用内核参数 ima_appraise= 选择:

模式行为用途
fix校验失败不拦截,缺签名的就地补签初次部署,全系统“上户口“
log校验照做,失败只记日志灰度观察,看会误伤谁
enforce校验失败拒绝访问(EACCES正式运行,真拦

标准部署闭环是这样的:

  1. fix 模式全系统跑一遍,给所有合法文件补签上户口
  2. log 观察一阵,确认没有误伤
  3. enforce——此后任何被篡改的、未授权的可执行文件,执行时直接被拒

这就把第八章的教训在运行时层面补上了:不只是“记下谁跑了“,而是“没户口的不许跑“。

代价也很现实:升级软件要重新签名。这就是发行版需要官方签名体系的原因——你用 fix 模式给系统上户口,本质上是在扮演一个迷你的发行版签名方。另外注意一个细节:.ima keyring 只接受被内核内置信任链签过的 CA 下发的公钥,root 不能随手往里塞公钥。为什么?否则攻击者拿到 root 后塞一把自己的公钥进去,给自己篡改的文件签名,appraise 就形同虚设了。这和第八章 Secure Boot 密钥库的思路一脉相承:验证规则的变更,必须比被验证的对象更难伪造。

签名操作本身不神秘,ima-evm-utils 包里的 evmctl 就是干这个的:

# 生成签名密钥对(私钥离线保管,公钥放进内核 .ima keyring)
evmctl ima_sign --key /path/to/privkey.pem /usr/bin/myapp

# 检查文件当前的 security.ima xattr
getfattr -m security.ima -d /usr/bin/myapp

真正难的不是签一个文件,而是围绕签名建立整套流程:私钥怎么保管、CI 里哪一步签、升级包怎么带上新签名、keyring 里的公钥怎么轮换。技术上一下午能跑通,工程上是一个体系。

EVM:保护 xattr 本身

appraise 的信任根基是 security.ima 这个 xattr。问题来了:如果攻击者连 xattr 一起改呢?离线挂载磁盘,把文件和它的签名一起换掉——签名是对的,文件是坏的。

EVM(Extended Verification Module,扩展验证模块)堵的是这一层:把 security.ima、SELinux 标签等一组安全相关 xattr 打包,算一个 HMAC 或签名,存进另一个 xattr security.evm。改任何一个字段,校验就对不上。

EVM 也分两种口味,和 security.ima 的 imahash / imasig 对应:

  • HMAC 模式:用一把对称密钥算 HMAC。密钥不落地,启动早期由加密扇区或 TPM 解封出来,直接装进内核密钥环——这就是所谓 encrypted / trusted key 的用法
  • 签名模式:用私钥签名、公钥校验,适合需要跨机器验证的场景

EVM 密钥的来源值得再强调一遍:它由 TPM 解封(或从加密扇区读出),而解封条件可以绑定 PCR。闭环就此完成:篡改 xattr 也要先过“启动环境正确“这一关——而那一关,正是第四到第八章搭起来的东西。启动可信 → EVM 密钥可得 → xattr 可信 → IMA 签名校验可信 → 文件执行可信,一环扣一环。

IMA 与远程证明:真正的舞台

说句可能泼冷水的话:单机视角下,IMA 的价值有限。本机 root 能看日志,也就能骗日志——日志在内核内存里,而内核已经是攻击者的了,你读到的是攻击者想让你读到的。

IMA 的真正舞台是远程证明(Remote Attestation)。流程大致是:

  1. 验证方向目标机器发起挑战,附带一个一次性随机数(nonce,防重放——否则攻击者可以把上次“健康“时的 quote 录下来反复用)
  2. 目标机器的 TPM 把 nonce 和 PCR 值(包括 PCR 10)一起签名,产出 quote(引用)
  3. 验证方拿到 quote + IMA 日志,先重放日志验证链对得上 quote 里的 PCR 10
  4. 然后逐条审查:“这台远程机器自可信启动以来跑过的每一个文件,是否都在我的白名单里?”

注意第 3 步为什么可信:quote 是 TPM 用芯片内私钥签的,本机 root 伪造不了——这正是第一章说的“TPM 的价值在于你无法绕过它“。日志可以编,但编出来的日志重放后对不上 TPM 签出来的 PCR 值,谎言当场穿帮。root 控制得了内核内存里的日志,控制不了 TPM 芯片里的签名私钥,这个不对称就是远程证明的根基。

开源项目 Keylime 做的核心工作就是这个。所以在很多文档里,IMA 日志也被称为 runtime measurement list(运行时度量清单),它是远程证明的标准输入。

局限性:设计和面试都要知道

IMA 不是银弹,几个局限要心里有数:

  • 只管执行,不管数据。 BPRM_CHECK 管的是可执行文件。配置文件被改了?不在视野里。程序行为很大程度由配置驱动,这是个实打实的盲区——一个干净的 nginx 配上被改过的 nginx.conf,IMA 看到的全是绿灯。
  • 解释器绕过。 python evil.py 度量的是 /usr/bin/python,不是 evil.py。解释器本身是干净的,脚本是恶意的,IMA 一无所觉。要堵这个得靠 FILE_CHECK 之类更重的钩子把脚本也纳入度量,或者干脆让解释器自己在打开脚本时走 IMA 路径(一些加固发行版确实给解释器打过这类补丁),代价都是性能和复杂度。
  • 性能开销。 文件首次使用时算哈希,冷启动阶段大量文件集中度量,感知是有的。热缓存之后影响小,但“无感“谈不上。
  • 部署复杂度。 签名体系、keyring 管理、升级重签、策略下发时序……每一项都是运维负担。这是 IMA 在桌面端普及慢的根本原因,也是为什么它主要活在服务器和嵌入式这些有专职运维的场景。

这些局限放在一起,勾勒出 IMA 的真实定位:它不是“系统完整性“的完整答案,而是“可执行内容完整性“的扎实一步。设计系统时把它当成纵深的一层,而不是最后的一层。

想实操怎么办

本书的 Arch 环境跑不了 IMA,两条路:

  1. 换一个默认开启 IMA 的发行版。 Fedora 或 RHEL 的虚拟机,开箱就有 CONFIG_IMA=y,内核参数加 ima_policy=tcb 就能看到 /sys/kernel/security/ima/ascii_runtime_measurements 里滚滚而来的度量日志。跑起来之后第一件事建议做重放:从全零开始把日志第二列依次 extend,和 tpm2_pcrread sha256:10 对一遍——把第五章的手艺在新场景里再过一遍,IMA 就内化成你自己的东西了。
  2. 自编译内核。 Arch 的 linux 包有现成 PKGBUILD,改 config 打开 CONFIG_IMA 及相关的 appraise / EVM 选项,用 make localmodconfig 只编译当前加载的模块,能把编译时间从几小时压到一顿饭的工夫。走通这条路,你对内核构建的理解也会涨一截——算是买一送一。

无论走哪条路,记得还是在虚拟机里玩。ima_appraise=enforce 一旦配错(比如 keyring 里没装公钥),系统会在启动早期拒绝执行一切未签名文件——包括 init。这种把自己锁死的体验,留在虚拟机里就好。

小结

  • IMA 让内核接过信任链的最后一环:文件被使用的瞬间度量它,extend 进 PCR 10
  • 三要素:钩子点(在哪触发)、策略(量什么,每次开机只下发一次)、模板(记成什么样)
  • 日志第二列是模板哈希不是文件哈希,重放时链它;boot_aggregate 把运行时日志锚定到具体某次启动
  • Appraisal 把记录升级为强制:fix 上户口 → log 灰度 → enforce 真拦;代价是升级要重签,.ima keyring 受内核信任链约束
  • EVM 用 HMAC 保护 xattr 元数据,密钥由 TPM / 加密扇区解封,把防篡改闭环焊死
  • 单机上 root 能骗日志,IMA 的真正舞台是远程证明(quote + 日志重放 + 白名单审查),Keylime 是代表实现
  • 局限:不管数据文件、解释器绕过、有性能开销、部署复杂——知道这些,才算真的懂了 IMA

到这里,从芯片到固件、从固件到内核、从内核到运行时,整条信任链我们走完了。下一章收个尾,给你一张进阶地图:这本书没展开的路,各自通向哪里。