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

第三章 · 实验 A:亲手验证 PCR 哈希链

本章目标

读概念的时候,PCR(平台配置寄存器,Platform Configuration Register)最容易让人产生“听懂了但不知道它到底是什么“的错觉。这一章我们直接上手:手动往一个 PCR 里写数据,然后用纯 shell 命令把 TPM 内部的计算过程一步步复算出来,和芯片给出的结果逐字节比对。

做完这个实验,你应该能建立起三个直觉:

  1. PCR 不是普通内存,不能被直接写入,只能被 extend(扩展)
  2. extend 的本质就是一次哈希:新值 = SHA256(旧值 ‖ 事件哈希)
  3. 这个结构让“历史“变得不可伪造——你没法悄悄改掉过去的某一步

背景知识: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/zerosha256sum,完整复现了芯片内部的运算。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 变成一把锁。