附录 C · 本书用到的脚本
这里收录两个完整跑过的脚本,附上逐段讲解。它们都不长,没有任何第三方依赖——这是刻意为之:重放验证这件事,理解每一步字节从哪来、到哪去,比直接调库重要得多。
replay_pcrs.py:重放固件启动度量日志
功能:读取内核暴露的二进制 BIOS 度量日志,逐条重放哈希链,算出每个 PCR 的理论值,再和 TPM 芯片里的真实值逐一比对。第五章实验 C 的核心脚本。
使用方法:
python3 replay_pcrs.py
# 需要能读 /sys/kernel/security/tpm0/binary_bios_measurements(一般 root 或 securityfs 已挂载)
# 脚本内部的 tpm2_pcrread 通过 sudo 调用
完整代码:
#!/usr/bin/env python3
"""重放 TPM 启动度量日志,验证重算值与 TPM 芯片中的真实 PCR 一致"""
import hashlib, struct, subprocess
LOG = "/sys/kernel/security/tpm0/binary_bios_measurements"
ALG_NAME = {0x0004: "sha1", 0x000B: "sha256", 0x000C: "sha384", 0x000D: "sha512"}
data = open(LOG, "rb").read()
u16 = lambda b, o: struct.unpack_from("<H", b, o)[0]
u32 = lambda b, o: struct.unpack_from("<I", b, o)[0]
esize = u32(data, 28)
event = data[32:32 + esize]
assert event[:16] == b"Spec ID Event03\x00", f"不支持的日志格式: {event[:16]!r}"
nalg = u32(event, 24)
alg_size, o = {}, 28
for _ in range(nalg):
alg_size[u16(event, o)] = u16(event, o + 2)
o += 4
pos = 32 + esize
pcrs = {}
while pos < len(data):
pcr, _, ndig = u32(data, pos), u32(data, pos + 4), u32(data, pos + 8)
o = pos + 12
digests = []
for _ in range(ndig):
alg = u16(data, o); o += 2
digests.append((alg, data[o:o + alg_size[alg]])); o += alg_size[alg]
evsz = u32(data, o)
o += 4 + evsz
for alg, dg in digests:
old = pcrs.get((alg, pcr), b"\x00" * alg_size[alg])
h = hashlib.new(ALG_NAME[alg]); h.update(old); h.update(dg)
pcrs[(alg, pcr)] = h.digest()
pos = o
out = subprocess.run(["sudo", "tpm2_pcrread"], capture_output=True, text=True).stdout
actual, bank = {}, None
for line in out.splitlines():
line = line.strip()
if line.endswith(":"):
bank = line[:-1]
elif bank and ":" in line:
idx, val = line.split(":", 1)
actual[(bank, int(idx.strip()))] = val.strip().lower().replace("0x", "")
print(f"{'bank':8} {'PCR':>3} {'日志重放计算值':<64} {'TPM 实际值':<64} 一致")
for (alg, pcr), val in sorted(pcrs.items(), key=lambda kv: (kv[0][1], kv[0][0])):
name = ALG_NAME[alg]
got = actual.get((name, pcr), "")
print(f"{name:8} {pcr:>3} {val.hex():<64} {got:<64} {'✓' if got == val.hex() else '✗'}")
关键实现点:
- 日志开头是一条特殊的 “Spec ID Event03” 事件(
assert处)。它的存在标志着这是 crypto-agile 格式——每条事件可以携带多个哈希算法的摘要。老格式(每条只带 SHA-1)的日志结构不同,本脚本会直接拒绝。 - 第一条事件里还声明了日志用到的算法 ID 和对应摘要长度(
nalg循环)。ALG_NAME把 TCG 的算法 ID(如0x000B)映射成 hashlib 认识的名字。 - 主循环逐条解析事件:PCR 编号、摘要列表、事件数据长度。事件数据本身(如命令行字符串)不参与哈希——参与的是固件当年算好的摘要,脚本只是把它“滚动“进去。
- 滚动的公式就是第三章亲手验证过的那条:
PCR_new = hash(PCR_old || digest),old不存在时按全零初始化——这正是 PCR 上电初值。 - 后半段解析
tpm2_pcrread的文本输出做比对。这块是“够用就行“的文本解析,换个 tpm2-tools 版本输出格式变了就得跟着调,这是它的脆弱点。
已知边界:
- 只支持 crypto-agile 格式(Spec ID Event03)。老机器、老固件的 SHA-1 旧格式日志不支持。
- 比对的是“日志能滚出的值“和“PCR 当前值“。如果开机后还有组件往这些 PCR 里 extend 过东西而没记进日志,会对不上——这不是脚本错了,是发现了日志没记录的度量,值得追下去。
replay_ima.py:重放 IMA 运行时度量日志
功能:重放 IMA(Integrity Measurement Architecture)的 ASCII 度量日志,验证重算值与 PCR 10 一致。第九章配套脚本——但注意,只有内核编了 CONFIG_IMA 的环境才能用(Arch 官方内核没有,见附录 A)。
使用方法:
python3 replay_ima.py
# 需要 /sys/kernel/security/ima/ascii_runtime_measurements 存在
完整代码:
#!/usr/bin/env python3
"""重放 IMA 运行时度量日志,验证与 PCR 10 一致"""
import hashlib, subprocess
pcr = bytes(32)
with open("/sys/kernel/security/ima/ascii_runtime_measurements") as f:
n = 0
for line in f:
h = line.split()[1]
if h == "0" * 64:
continue
pcr = hashlib.sha256(pcr + bytes.fromhex(h)).digest()
n += 1
out = subprocess.run(["sudo", "tpm2_pcrread", "sha256:10"],
capture_output=True, text=True).stdout
actual = out.split("0x")[-1].strip().lower()
print(f"重放了 {n} 条度量记录")
print(f"重放计算值: {pcr.hex()}")
print(f"TPM 实际值: {actual}")
print("一致 ✓" if pcr.hex() == actual else "不一致 ✗(日志在开机后仍在增长属正常)")
关键实现点:
- IMA 日志是 ASCII 文本,每行一条度量记录,第二个字段就是 extend 进 PCR 10 的 SHA-256 摘要。所以不需要 struct 解二进制,
split()就够了。 - 滚动公式和固件日志完全一样:
PCR_new = sha256(PCR_old || digest),初值全零。学透一次,到处能用。 - 跳过全零摘要的行:那是占位/无效的度量记录,不参与真实哈希链。
- 实现刻意简化:只处理 sha256 bank、直接用
split("0x")抠 PCR 值。作为验证工具够用,别当库抄。
已知边界:
- 活日志问题:IMA 日志是持续增长的——你读到一半,系统里一个新文件被访问,IMA 又记了一条、又 extend 了一次。这种情况下重放值和 PCR 值对不上,不是篡改,是日志在你读它的时候还在动。
- 这也是为什么正规的远程证明系统(如第十章提到的 Keylime)抓日志和取 quote 必须原子进行:先让 TPM 用 quote 锁定某一时刻的 PCR 值,再验证日志能滚出这个值,中间不能允许新的度量插进来,否则永远对不齐。
- 日常手动验证时,对不上先看末尾几行日志的时间戳,多半是新度量进来了;重跑一次往往就一致了。
思考 两个脚本加起来不到一百行,但它们做的事情——独立重放、独立验证——就是整个可信计算最核心的动作。商业远程证明系统做的事本质上一样,只是多了签名、nonce 和网络传输。