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

附录 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 和网络传输。