Skip to content
On this page

深入揭秘:DVT 代币桥的“安全漏洞”与你的守护使命

想象一下,在一个去中心化金融(DeFi)的世界里,你扮演着一个至关重要的角色——代币桥的守护者。你的任务是保护一座连接 L2 和 L1 的数字桥梁,确保上面价值百万的 DVT(Damn Valuable Tokens)代币安全无虞。然而,一个潜在的危险正悄然逼近,一场针对这座桥梁的“盗窃”正在酝酿。

故事背景:一条看似安全的代币桥

我们拥有一座 L2 到 L1 的代币桥,它目前储备着高达一百万枚 DVT 代币。这座 L1 侧的桥梁设计允许任何人,在满足特定条件的情况下,可以完成代币的提现。这些条件包括:

  1. 延迟期已过: 提现请求必须经过 L2 的一个标准延迟期,在 L1 才能被最终确认。
  2. 有效的 Merkle 证明: 提现者需要提供一个有效的 Merkle 证明,这个证明必须对应着由桥梁所有者设定的、最新的提现根(root)。

潜藏的危机:一份可疑的提现记录

你手中掌握着一份包含 4 笔 L2 发起的提现事件的 JSON 文件。理论上,这些提现都可以在 7 天的延迟期后在 L1 执行。然而,你敏锐的直觉告诉你,其中一定隐藏着一个“可疑分子”。

你的特殊使命:成为桥梁的守护者

作为这座桥梁的桥梁操作员,你拥有一种特殊的权力。你的目标是:

  • 保护所有提现: 确保所有合法的提现都能顺利完成。
  • 阻止可疑提现: 识别并阻止那个“破坏者”的非法提现。
  • 保护代币总量: 在完成以上目标的同时,避免耗尽桥梁的所有 DVT 代币。

代码解析:漏洞的蛛丝马迹

通过分析提供的智能合约代码,我们可以找到问题的关键点:

  1. L1Gateway.sol - finalizeWithdrawal 函数:

    • 这个函数负责最终确定提现。
    • 它首先检查 timestamp + DELAY > block.timestamp,如果提现时间未到,则会 revert。
    • 关键点: if (!isOperator) 块中的 Merkle 证明验证。如果调用者不是操作员(operator),就必须提供有效的 Merkle 证明。
    • 漏洞猜测: 如果“可疑提现”者拥有一个伪造的 Merkle 证明,或者找到了绕过 Merkle 证明验证的方法,就可能导致非法提现。
  2. L1Forwarder.sol - forwardMessage 函数:

    • 这个函数负责将 L2 的消息转发到 L1。
    • 它通过 gateway.xSender() == l2Handler 来判断消息是否来自 L2 Handler。
    • 关键点: require(failedMessages[messageId]);require(!failedMessages[messageId]); 的不同条件。
    • 漏洞猜测: L1GatewayfinalizeWithdrawal 函数中,当 isOperatortrue 时,并未验证 Merkle 证明。这意味着,作为操作员,你可以直接调用 finalizeWithdrawal,绕过 Merkle 证明的验证。
  3. TokenBridge.sol - executeTokenWithdrawal 函数:

    • 这个函数处理实际的代币转移。
    • 它检查 msg.sender == address(l1Forwarder) || l1Forwarder.getSender() == otherBridge
    • 关键点: l1Forwarder.getSender() == otherBridge 这个条件。
    • 漏洞猜测: 如果我们能够让 l1Forwarder.getSender() 返回一个otherBridge 的值,那么 TokenBridge.executeTokenWithdrawal 就可以被调用,从而执行代币的转移。

策略制定:成为真正的守护者

既然你拥有操作员的特殊权限,就可以利用这一点来扭转乾坤。

  1. 利用操作员身份: 作为操作员,你可以绕过 L1Gateway 中的 Merkle 证明验证。这意味着,你可以主动调用 finalizeWithdrawal 函数,为所有提现(包括那个可疑的)提供一个“最终确认”。
  2. 篡改 L2 Sender: 重点在于 L1Forwarder.forwardMessage 函数中的 gateway.xSender() == l2Handler 逻辑。如果 gateway.xSender() 的值可以被控制,并且不等于 address(l2Handler),那么 L1Forwarder 就会执行 require(failedMessages[messageId]);
    • 关键思路:L1Gateway.finalizeWithdrawal 中,xSender 的值会被设置为 l2Sender。如果我们将一个l2Handlerl2Sender 传递给 finalizeWithdrawal,那么当 L1Forwarder 收到这个消息时,gateway.xSender() 就会是这个非 l2Handler 的值。
    • 攻击方式: 我们可以构造一个“可疑”的提现,其 l2Sender 被设置为一个已知的值,但这个值不是 l2Handler。当我们调用 finalizeWithdrawal 时,l1Gateway.xSender 会被设置为这个可疑的 l2Sender
    • 阻止可疑提现:L1Forwarder.forwardMessage 被调用时,它会看到 gateway.xSender() (即我们设置的可疑 l2Sender) 不等于 l2Handler,因此会执行 require(failedMessages[messageId]);。由于该消息 ID 尚未被标记为失败,这将导致 forwardMessage 失败,从而阻止了可疑提现的执行。

解决方案的核心:

  1. 跳过延迟期: 利用 vm.skip 来模拟 7 天的延迟期过去。
  2. 模拟提现数据: 准备好所有 4 个提现的数据,包括 nonce, l2Sender, target, timestamp, message 以及 空 Merkle 证明(因为我们是操作员)。
  3. 构造 Payload:
    • 首先,你需要调用 TokenBridge.executeTokenWithdrawal,将代币转移到你指定的接收者。你需要构建 message 参数,指向 TokenBridge.executeTokenWithdrawal 函数,并包含接收者地址和金额。
    • 然后,你需要构建 forwardMessagemessage 参数,它将包含上述的 TokenBridge.executeTokenWithdrawal 的调用。
  4. 调用 finalizeWithdrawal
    • 对于前 3 个合法的提现: 调用 l1Gateway.finalizeWithdrawal,并使用真实的 l2Sender
    • 对于第 4 个可疑提现: 这是关键!调用 l1Gateway.finalizeWithdrawal,但l2Sender 设置为一个我们知道的、但不是 l2Handler 的地址。这样,当 L1Forwarder 尝试处理这个消息时,由于 gateway.xSender() 不匹配,forwardMessage 函数将执行 require(failedMessages[messageId]);,导致该消息被标记为失败,从而阻止了可疑提现。
  5. 最后一步: 由于我们已经通过 finalizeWithdrawal 模拟了所有提现的执行,并且阻止了可疑提现。最后,再执行一个简单的代币转移,确保代币总数不会被完全耗尽,满足“不耗尽所有资金”的要求。

总结:

这个挑战巧妙地利用了 L1GatewayL1Forwarder 之间的交互逻辑。作为拥有特殊权限的操作员,你不仅可以确保所有合法提现的执行,更可以通过操纵 l2Sender 的值,制造一个“假象”,让 L1Forwarder 认为可疑提现来自一个未知的 L2 Handler,从而触发失败逻辑,成功阻止了这次潜在的资产盗窃。你成功地完成了守护者的使命,保护了桥梁的安全!

Built with AiAda