第三章 · 实验 A:亲手验证 PCR 哈希链
本章目标
读概念的时候,PCR(平台配置寄存器,Platform Configuration Register)最容易让人产生“听懂了但不知道它到底是什么“的错觉。这一章我们直接上手:手动往一个 PCR 里写数据,然后用纯 shell 命令把 TPM 内部的计算过程一步步复算出来,和芯片给出的结果逐字节比对。
做完这个实验,你应该能建立起三个直觉:
- PCR 不是普通内存,不能被直接写入,只能被 extend(扩展)
- extend 的本质就是一次哈希:
新值 = SHA256(旧值 ‖ 事件哈希) - 这个结构让“历史“变得不可伪造——你没法悄悄改掉过去的某一步
背景知识:extend 到底在干什么
PCR 是一组存放在 TPM 芯片内部的寄存器,每个银行(bank)通常有 24 个。以 SHA-256 银行为例,每个 PCR 就是一个 32 字节的值,复位后全为零。
普通寄存器可以被覆写,PCR 不行。你唯一能对它做的写操作叫 extend:
PCR_new = Hash(PCR_old ‖ digest)
注意是拼接(‖)后再哈希,不是替换。这个设计有三个后果:
- 不可逆:知道了新值,倒推不出旧值。
- 顺序敏感:先 extend A 再 extend B,和先 B 再 A,结果完全不同。
- 历史包含一切:当前值是之前每一次 extend 的函数,缺一不可。
真实启动时,固件把引导加载器的哈希 extend 进 PCR,引导加载器再把内核的哈希 extend 进去——一条哈希链就这么串起来了。而我们这一章要做的,就是手动扮演一次“固件“。
思考:为什么 TPM 要设计成“只能 extend 不能写“?如果允许直接写,任何能访问 PCR 的程序(包括恶意软件)都能把寄存器改回“干净“的值,度量就失去了意义。extend-only 是整件事的地基。
准备工作
实验环境是第二章搭好的虚拟机(Arch 系发行版 + swtpm)。装一下工具:
sudo pacman -S --needed tpm2-tools
确认系统能看到 TPM:
ls /dev/tpm*
预期输出:
/dev/tpm0 /dev/tpmrm0
tpm0 是字符设备,tpmrm0 是内核的资源管理器(resource manager),tpm2-tools 默认走后者。能看到这两个文件,说明 swtpm 已经正确挂上了。
选一个安全的 PCR
TPM 2.0 规范给 PCR 分配了用途:0–7 归固件和启动链用,8–15 归操作系统用,16–23 是调试用途,系统不会动它们。我们选 PCR 16 当试验田,随便折腾不影响系统。
先读一下当前值:
sudo tpm2_pcrread sha256:16
预期输出:
sha256:
16 : 0x0000000000000000000000000000000000000000000000000000000000000000
全零。这是一张白纸。
第一次 extend
先算事件哈希。假设我们“度量“的对象是字符串 hello:
echo -n "hello" | sha256sum
输出:
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -
坑:
echo默认会在末尾加一个换行符,哈希出来的就不是你以为的东西了。echo -n这个-n不能省。真实启动链里度量的是二进制文件,没有这个问题,但我们手工模拟时必须小心。
把这个哈希 extend 进 PCR 16:
sudo tpm2_pcrextend 16:sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
没有任何输出就是成功。再读一次:
sudo tpm2_pcrread sha256:16
sha256:
16 : 0x9851312028952521510E8EAAB5BE94E7DC24B5FC292B2E9781173CF11FFA9878
值变了。这个值既不是 hello 的哈希,也不是全零——它是两者的某种组合。什么组合?下一节见分晓。
核心环节:手工复算 TPM 的算法
这是我们整章的重头戏。TPM 内部做的计算,规范里写得很清楚:
PCR_new = SHA256(PCR_old ‖ digest)
旧值是全零(32 个零字节),事件哈希我们已经有了。用纯 shell 把这个拼接做出来:
{ head -c 32 /dev/zero; echo "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" | xxd -r -p; } | sha256sum
拆解一下这条命令:
head -c 32 /dev/zero:产出 32 个零字节——这就是 PCR 的旧值;echo "..." | xxd -r -p:把十六进制字符串转成原始字节(哈希计算作用在字节上,不是作用在十六进制字符上,这一步少了结果就全错);- 大括号把两段输出拼在一起,一共 64 字节,送进
sha256sum。
输出:
9851312028952521510e8eaab5be94e7dc24b5fc292b2e9781173cf11ffa9878 -
把它和上面 tpm2_pcrread 读到的 0x9851312028...ffa9878 对比:一字不差。
这一刻值得停下来体会一下。我们手里没有任何 TPM 的私钥、没有调用任何特权接口,就用 /dev/zero 和 sha256sum,完整复现了芯片内部的运算。PCR 的机制没有任何魔法——它就是一条公开的、可验证的哈希链。TPM 的价值不在于计算本身(这个谁都能算),而在于芯片内部的那个中间值你无法绕过、无法篡改、无法伪造。
第二次 extend:感受“链“
再 extend 一个 world,看看链式结构:
W=$(echo -n "world" | sha256sum | cut -d' ' -f1)
sudo tpm2_pcrextend 16:sha256=$W
复算这一次的新值,注意这次的旧值不再是零,而是上一步的结果:
P=9851312028952521510e8eaab5be94e7dc24b5fc292b2e9781173cf11ffa9878
{ echo "$P" | xxd -r -p; echo "$W" | xxd -r -p; } | sha256sum
输出:
98d128df384d428ffe76af3c0198ff1e8945ef71e741ba440bafff0510da8f22 -
读 PCR 验证:
sudo tpm2_pcrread sha256:16
sha256:
16 : 0x98D128DF384D428FFE76AF3C0198FF1E8945EF71E741BA440BAFFF0510DA8F22
又对上了。现在 PCR 16 的值同时“包含“了 hello 和 world 两次度量,而且顺序锁死在里面:如果你反过来先 extend world 再 extend hello,得到的会是完全不同的值。可以自己试一下——顺序错一位,结果面目全非。
重置:有的 PCR 可以,有的不行
PCR 16 是调试寄存器,允许在运行时复位:
sudo tpm2_pcrreset 16
sudo tpm2_pcrread sha256:16
sha256:
16 : 0x0000000000000000000000000000000000000000000000000000000000000000
回到全零了。现在试试 PCR 0:
sudo tpm2_pcrreset 0
ERROR: Tss2_Sys_PCR_Reset(0x184) - tpm:error(2.0): locality is not the correct locality to perform the command
被拒绝。错误信息里的 locality(局部性)先不用深究,结论记住就行:PCR 0–15 只允许在平台复位(重启)时清零,运行中的操作系统——哪怕是 root——也无权重置它们。
这说明什么
这个实验动的是无关紧要的调试寄存器,但它演示的机制就是真实可信启动的全部核心:
第一,历史不可伪造。 想篡改 PCR 的当前值,只有两条路:extend 一个能“凑“出目标值的新哈希(哈希原像攻击,计算上不可行),或者重置重来(对 PCR 0–15 做不到)。改历史这条路被从物理上堵死了。
第二,真实启动时固件做的事,和你手动做的事一模一样。 开机后固件度量下一级组件,算出哈希,extend 进 PCR 0;引导加载器度量内核,extend 进 PCR 4……全是同一套 新值 = Hash(旧值 ‖ 摘要)。等你登录系统后 tpm2_pcrread 看到的 PCR 0–9 那些非零值,就是这么一步步长出来的。没有任何神秘环节。
第三,root 也清不掉 PCR 0–7。 这意味着哪怕攻击者拿到了最高权限,也无法抹除启动链留下的度量痕迹。唯一能改变这些 PCR 的办法是重启、重走一遍启动链——而重启后的每一次度量又会留下新的痕迹。篡改不是“会不会被发现“的问题,是“必然留下证据“的问题。
小结
- PCR 只能 extend,不能直接写;
新值 = Hash(旧值 ‖ 事件哈希) - 我们用
/dev/zero+xxd+sha256sum手工复算了 TPM 的内部运算,逐字节吻合 - 链式结构让顺序敏感、历史不可伪造
- PCR 16 可复位,适合当试验田;PCR 0–15 运行时不可复位,root 也不行
读到这里你可能会问:既然 PCR 记录的是启动历史,那它除了“看看“还有什么用?问得好——光记录不拦人确实没用。下一章我们就把 PCR 变成一把锁。