区块链桥安全 - 第 4 部分

本文是桥接安全系列的第四篇文章,主要讨论了桥接合约中存在的三个安全问题:链ID欺骗、哈希碰撞以及缺乏签名过期检查。通过具体的代码示例和测试用例,深入分析了这些漏洞的原理和潜在危害,并提出了相应的安全建议,例如在签名数据中包含过期时间戳,以防止恶意重放攻击。


这是 Bridge 安全系列的第 4 篇文章。请参阅第一部分第二部分第三部分

本文解释了:

  1. Chain id 欺骗
  2. 哈希碰撞
  3. 缺少签名过期检查

1. Chain id 欺骗

此示例中使用的合约是 BridgeSpoofChainId.sol。 此合约有一个名为 setAllowedSrcChain() 的附加函数,它允许设置 chain id(实际上,它将布尔值映射到数字),然后在 executeMessage() 中的 require 语句中用于检查输入的 transaction.srcChainId 是否被允许。

虽然在此示例中实际上不需要此 require 语句。 在此示例中添加它是为了解释如何欺骗 chain id。

测试 test_chain_id_validation() 表明,当 srcChainId 设置为合约不允许的 chain id 数字时,交易会恢复。 在这种情况下,交易失败是预期的。

test_chain_id_spoofing() 显示了 chain id 欺骗。 在这里,仅举例来说,owner 使用 destBridge.setAllowedSrcChain()56 设置为 true

然后创建 transaction 结构体。 如在 data 字段中可以看到的那样,该交易用于在目标链上将 1e18 个 ERC20tokens 转移给 user。 有趣的是,transaction.srcChainId 设置为 56

然后,它使用 bridge.sendMsgPermit() 在源链 bridge 上签名并发送交易。

然后它执行 destBridge.executeMessage()。 由于在 BridgeSpoofChainId.sol#L147-L148 上,if{} 都为真,因此它执行内部 _transfer() 以将解码后的 value(即 1e18)从 transaction.from 转移到已解码的 to 地址(即 user 地址)。

事务执行成功。 在测试中的这些行中,可以看到该事务已成功处理,user 地址从 Alice(在目标链上)收到了 1e18 个 token,并且 Alice 的 余额变为 0

因此,这里可能存在不同的可能性,例如,目标 bridge(为了本示例的目的)不支持用户当前所在的 chain id,但支持 chain id 56。 在这种情况下,用户没有足够的 token 从 chain id 56 进行转移。 因此,他欺骗了 chain id,将其设置为 56(即使他不在 chain id 56 上)。 并且由于 sendMsgPermit()(或 sendMsg())无法设置 transaction.srcChainId = block.chainid,因此用户能够欺骗逻辑并且事务成功。

你可能已经注意到,我们不需要此 require 检查来显示此漏洞,但将其添加到示例中是为了显示如何可能进行 chain 欺骗; 从当前源链欺骗 chain id。

2. 哈希碰撞

test_sendMsg_hash_collision() 显示了逻辑中的哈希碰撞以及它如何影响合约的运作。 此示例中使用的合约是 BridgeHashCollision.sol

该测试使用 for{} 循环迭代 12 次。 可以看到,对于每次迭代,它都会根据 i 设置 value。 如果 i12,则对于该迭代,它将 value 设置为 20000000000000000(0.02e18)。 否则,value 始终为 120000000000000000(0.12e18)。

然后,它创建 transaction 结构体,用于将 eth 的 value 转移到目标链上的 user 地址。 然后,它使用 bridge.sendMsg() 发送消息。

然后,使用 vm.chainId(2),它将 chain id 设置为 2。 为什么? 因为在 transaction 结构体中设置的目标 chain id ( dstChainId) 是 2,因此在调用 destBridge.executeMessage() 时,如果 block.chainid 不是 2,则由于验证,事务将恢复。

最后,chain id 在 BridgeHashCollision.t.sol#L61 上设置回 transaction.srcChainId,其背后的原因是,在 BridgeHashCollision.t.sol#L45 上,它被设置为 2,以便如上所述成功执行 destBridge.executeMessage(),现在由于所有函数调用都在循环中工作,因此将 chain id 再次设置回原来的值非常重要。 在创建结构体时,transaction.srcChainId 设置为原始 chain id,因此我们使用该值将 chain id 设置回 BridgeHashCollision.t.sol#L61 上的原始值。

了解哈希碰撞导致的实际问题:

BridgeHashCollision.sol 中,每次计算 messageHash 时,都需要在哈希它们之前对所有变量(函数参数)进行编码。 有趣的是,它使用 abi.encodePacked 进行编码。

现在的第一个印象是,由于 sendMsg()sendMsgPermit() 总是递增 messageId ( 其转换为 idString,用于查找 messageHash)**,它应该防止哈希碰撞。 但这种假设是错误的。

BridgeHashCollision.sol#L115 中,messageHash 的计算可能会发生哈希碰撞。 在此示例中,发生碰撞是因为我们使 transaction 结构体中的大多数值与以前相同。 即使我们假设唯一的 id 可以防止碰撞,但与此假设相反的是 abi.encodePacked 的使用。

如果我们再次检查测试,它会期望 for{} 循环的第 12 次迭代在 BridgeHashCollision.t.sol#L50 上恢复为“ Bridge: message already processed”,即,它期望事务已被处理(由于哈希碰撞)。

这种哈希碰撞是由于使用非标准编码来创建 messageHash 而发生的。

因此,在本测试中使用的示例中,当 id 为 111 时,数据的编码将完全相同。 如下所示,对于 id 1,使用的 value 是 120000000000000000(0.12e18),对于 id 11,使用的 value 是 20000000000000000(0.02e18)。 因此,最终,非填充编码(使用 abi.encodePacked)会给出相同的结果。 并且由于我们测试中所有迭代传递的其他变量都相同。 messageHash 相同。 因此,为相同的 messageHash 执行 executeMessage() 将恢复,因为 bridge 假定事务已执行。

下图显示了 messageHash 中碰撞的样子。

由于未填充编码导致哈希碰撞的值。

在当前示例中,它创建的问题是拒绝服务 (DoS),但此问题可能导致任何其他严重漏洞。 由于 bridge 实现可能多次包含消息/结构中正在转发的多个变量,并且可能包含哈希的使用(因为存储了 bridge 的唯一证明等)。 检查 bridge 逻辑是否容易受到这些类型的场景及其创建的问题的影响至关重要。

3. 缺少签名过期检查

处理签名时,还需要注意一件事。 在签名者签署签名的结构体/数据中使用过期时间戳是个好主意。

由于过期时间将是结构体/数据的一部分,因此可以在验证签名的函数中进行检查。 在代码级别,可以检查当前时间戳(即 block.timestamp)是否应小于(或根据要求等于)date/结构体中提到的过期时间戳。

这将确保签名在一段时间内或特定时间内未执行,稍后将被视为无效签名。 其背后的简单原因可能是签名者不希望允许使用该签名的任何情况。 例如,在这种情况下,如果签名者签署了用于转移一定数量 token 的数据,则由于许多可能的原因,该签名者可能不愿意在 6 个月或一年后执行该签名。

重要的是要考虑可能有专门的中继器用于执行/提交这些已签名的签名。 一些项目可能不需要签名过期检查,因为他们确信签名肯定会在一定时间内执行。

除了本系列中讨论的常见漏洞外,根据智能合约和业务逻辑,可能还存在其他独特的漏洞。

  • 原文链接: calibersec.com/blockchai...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
calibersec
calibersec
江湖只有他的大名,没有他的介绍。