如何利用C2PA破解时间
C2PA凭证旨在证明图像的来源和制作时间。David Buchanan展示了如何让凭证携带一个与照片拍摄时间毫无关系的时间戳,并追问该格式能否得到修复。
中文
复制

Welcome to my
::'########::'##::::::::'#######:::'######:::
:: ##.... ##: ##:::::::'##.... ##:'##... ##::
:: ##:::: ##: ##::::::: ##:::: ##: ##:::..:::
:: ########:: ##::::::: ##:::: ##: ##::'####:
:: ##.... ##: ##::::::: ##:::: ##: ##::: ##::
:: ##:::: ##: ##::::::: ##:::: ##: ##::: ##::
:: ########:: ########:. #######::. ######:::
::........:::........:::.......::::......::::
Blog Index
作者:David Buchanan(又名 retr0id),2026 年 10 月 2 日

影史上最唬人的黑客桥段出自 Kung Fury(2015),里面的 Hackerman 直接黑进了时间本身,用这份能力去纠正历史上的恶行。但要换成我拿到黑时间的本事,会干点什么?我这人更怕蝴蝶效应,所以顶多往回倒几个小时,告诉自己中奖的彩票号码。
巧的是,这正是我干过的事——证据就是这张图里密码学上无法伪造的 C2PA 元数据:
没错,一眼就能看出是 P 的。我并不是真想搞彩票诈骗。
你可以在 https://verify.contentauthenticity.org/ 验证这段 C2PA 元数据,包括时间戳(如果你是在未来读到这篇文章,也许那时已经有缓解措施了)。
你也可以在 https://www.euro-millions.com/results/28-08-2026. 确认这些确实是中奖号码,而且是在开奖时间之前几个小时就显示出来的。
背景
一份典型的 C2PA manifest 里有两个签名。第一个是「claim」签名。以相机 App 为例,claim 的内容大致是「这是一张拍摄的照片,拍摄于这个 GPS 坐标、这个时间」(当然,按 C2PA 规范会有更正式的表述)。
在上一篇文章里我说明了 claim 签名基本一文不值,想签什么 claim 就能签什么(至少对 Google Pixel Camera App 这类旗舰级 C2PA 实现来说是这样)。
但通常还有第二个签名,来自时间戳机构(TSA),这个就有意思多了。我们不信任设备自报的时间,于是改用 RFC 3161 协议去问一台远程服务器。服务器返回一个签名,声明「是的,我在这个时间戳看到了这个哈希」,这个响应会被嵌进 C2PA 元数据。只要相信服务器没有乱来,它就能证明被签名的数据在该时间戳之前(或当时)已经存在。TSA 扮演的是一个独立见证人的角色。
对 Pixel 10,Google 决定设备其实可以被信任来自己报时。我还没评估他们的“设备端可信时间戳”实现,但对此高度怀疑。
尽管如此,本文不会攻击 TSA:我们假设它按宣传的那样工作。
该黑时间了
如果 TSA 机制是安全的,我们怎么黑时间?
我们要用我最喜欢的一类漏洞:规范自摆乌龙。
乌龙是这样的:C2PA 允许任意“排除项”。这些是文件内被排除在签名计算之外的字节范围。没错。真的。规范里就是这么写的,但不知为何至今没人管这事。
事实上,Neal Krawertz 博士在 2025 年 6 月发表的 C2PA 缺陷“大列表”中明确指出了这一点:
- 排除范围过大。 manifest 通常会把非常大的字节范围排除在签名之外。任何被排除的字节都可以被改动而不被发现。
这里唯一的新角度是:恶意签名者可以故意使用大排除范围,而不只是顺带这么做。我们可以把整个文件排除掉,从而对一个空字符串生成完全有效的签名。这样一来,文件事后被篡改,签名依然有效,而且 TSA 的时间戳证明也依然有效。
时间状态:已黑
所以我确实拍了一张彩票的照片,并附上了有效的 C2PA 签名和有效的可信时间戳。但我构造 manifest 时排除了整个文件,让我能在号码公布后(拙劣地)PS 它,而不破坏任何签名。
用 c2patool -d 导出我 PoC 文件的 manifest,可以看到关键部分:
"c2pa.hash.data": { "exclusions": [ { "start": 0, "length": 3995383 } ], "name": "jumbf manifest", "alg": "sha256", "hash": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", "pad": []},
3995383 是整个文件的长度,47DE...uFU= 是空字符串的哈希:
$ openssl sha256 -binary /dev/null | base64
47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
claim 签名针对的是这个哈希,timestamp 签名针对的又是 claim 签名。实际上,我们签的等于什么都没有,但直到今天,我能找到的所有 C2PA 验证工具都没有把这种情况标记为异常。
能修吗?
没那么容易!检测出整个文件被排除并报告为无效,这当然很简单,可要是只排除了一小部分呢?你怎么判断它是无害的,还是那种一旦被改动就能彻底改变图像外观的东西?(后者的例子见 MD5 哈希碰撞 PoC)。
这篇文章的初稿到这里就结束了,写的是「我的建议是把 exclusion 这个特性从 C2PA 规范里排除掉」,但事情没这么简单!
exclusion 之所以存在,主要原因是某些文件格式确实需要它。比如 PNG 文件里每个 chunk 都带一个 CRC32 校验和,签名嵌入文件之后,这个校验和必须重新计算。这就形成了循环依赖,除非把 CRC32 排除在签名覆盖范围之外(那些能避免 CRC 失效的巧妙数学技巧暂且不论)。
所以我认为正确的做法是:针对每一种支持的文件格式,仔细而明确地规定文件的哪些部分允许被排除,并要求验证器强制执行这些约束。
除另有说明外,本博客全部内容由 thinking meat 生成。
