本文是桥接安全系列的第四篇文章,主要讨论了桥接合约中存在的三个安全问题:链ID欺骗、哈希碰撞以及缺乏签名过期检查。通过具体的代码示例和测试用例,深入分析了这些漏洞的原理和潜在危害,并提出了相应的安全建议,例如在签名数据中包含过期时间戳,以防止恶意重放攻击。
这是 Bridge 安全系列的第 4 篇文章。请参阅第一部分,第二部分,第三部分。
本文解释了:
此示例中使用的合约是 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。
test_sendMsg_hash_collision() 显示了逻辑中的哈希碰撞以及它如何影响合约的运作。 此示例中使用的合约是 BridgeHashCollision.sol
该测试使用 for{} 循环迭代 12 次。 可以看到,对于每次迭代,它都会根据 i 设置 value。 如果 i 为 12,则对于该迭代,它将 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 为 1 和 11 时,数据的编码将完全相同。 如下所示,对于 id 1,使用的 value 是 120000000000000000(0.12e18),对于 id 11,使用的 value 是 20000000000000000(0.02e18)。 因此,最终,非填充编码(使用 abi.encodePacked)会给出相同的结果。 并且由于我们测试中所有迭代传递的其他变量都相同。 messageHash 相同。 因此,为相同的 messageHash 执行 executeMessage() 将恢复,因为 bridge 假定事务已执行。
下图显示了 messageHash 中碰撞的样子。
由于未填充编码导致哈希碰撞的值。
在当前示例中,它创建的问题是拒绝服务 (DoS),但此问题可能导致任何其他严重漏洞。 由于 bridge 实现可能多次包含消息/结构中正在转发的多个变量,并且可能包含哈希的使用(因为存储了 bridge 的唯一证明等)。 检查 bridge 逻辑是否容易受到这些类型的场景及其创建的问题的影响至关重要。
处理签名时,还需要注意一件事。 在签名者签署签名的结构体/数据中使用过期时间戳是个好主意。
由于过期时间将是结构体/数据的一部分,因此可以在验证签名的函数中进行检查。 在代码级别,可以检查当前时间戳(即 block.timestamp)是否应小于(或根据要求等于)date/结构体中提到的过期时间戳。
这将确保签名在一段时间内或特定时间内未执行,稍后将被视为无效签名。 其背后的简单原因可能是签名者不希望允许使用该签名的任何情况。 例如,在这种情况下,如果签名者签署了用于转移一定数量 token 的数据,则由于许多可能的原因,该签名者可能不愿意在 6 个月或一年后执行该签名。
重要的是要考虑可能有专门的中继器用于执行/提交这些已签名的签名。 一些项目可能不需要签名过期检查,因为他们确信签名肯定会在一定时间内执行。
除了本系列中讨论的常见漏洞外,根据智能合约和业务逻辑,可能还存在其他独特的漏洞。
- 原文链接: calibersec.com/blockchai...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!