本文是桥安全系列文章的第三篇,介绍了由于桥合约允许任意调用其他合约而导致的任意调用执行漏洞。文章通过两个具体的PoC示例,展示了攻击者如何利用该漏洞窃取以太币或欺骗铸币逻辑,在源链不销毁token的情况下,在目标链mint token。文章最后提出了避免此类漏洞的建议,例如限制调用到可信地址,并检查可信地址是否存在漏洞。
这是 Bridge 安全系列的第三篇文章。查看第一部分和第二部分
本文借助两个特定于实现的示例解释了任意调用执行:
此漏洞是可能发生的,因为本例中的桥合约允许任意调用任何其他合约。
有两个 PoC 用于解释与当前实现相关的内容。两者都使用嵌套消息:
任意调用执行的可视化表示。
阅读时请参考该图,以便更直观地理解。
首先,它创建一个 maliciousTransaction 结构体类型的变量,它将成为在目标链上将 1 ether 转移到 user 地址的交易。
然后,它创建一个名为 transaction 的结构体,其中它将 to 地址作为 signalProcessor 的地址传递。数据将是 sendTx(bytes32) 的 calldata,其中 _msgHash 参数作为 messageHash 传递。messageHash 是 maliciousTransaction 的哈希值。参见BridgeSignatureReplay.t.sol#L35-L36。
然后,测试在 BridgeSignatureReplay.t.sol#L50 上使用 sendMsg() 将消息跨链发送。
在 BridgeSignatureReplay.t.sol#L56 上,destBridge.executeMessage() 在目标链上执行交易。第一次调用 executeMessage() 执行第一个交易,这将调用信号处理器来存储恶意交易哈希。发生这种情况是因为在 BridgeSignatureReplay.sol#L129 中的 else{} 在 executeMessage() 中执行。因为 BridgeSignatureReplay.sol#L120 上的 if{} 条件失败,因为 transaction.to 是 signalProcessor 地址,所以 else{} 执行。如 BridgeSignatureReplay.sol#L130 所示,它执行恶意调用,即调用 signalProcessor。sendTx() 存储恶意数据哈希:
(bool success,) = recipient.call{value: amount}(transaction.data);
最后,在 BridgeSignatureReplay.t.sol#L59 上,测试执行另一个 destBridge.executeMessage()。这次,攻击者实际上将 maliciousTransaction 作为 destBridge.executeMessage() 的参数传递。在 BridgeSignatureReplay.sol#L117 上,它能够验证消息。为什么?因为攻击者在 BridgeSignatureReplay.t.sol#L56 上,通过第一次调用 destBridge.executeMessage() 以及看起来无辜的 transaction 存储了恶意消息。
第二次调用 destBridge.executeMessage() 中发生的事情是,BridgeSignatureReplay.sol#L120上的 if{} 失败,因为 transaction.to 不是 address(0) 或 address(this),所以 else{} 执行,它将对 recipient 地址(即 user 地址)执行 call()。这就是合约转移 amount 值的的地方,而 amount 实际上是 maliciousTransaction 结构体中声明的 1 ether 的 transaction.value。
这就是攻击者如何在不首先从源链发送的情况下,在目标链上获得 1 ether 的方法。
它显示了与 test_sendMsg_nested() 中显示的类似的漏洞,但在这里,攻击者试图在目标链上铸造代币,而无需在源链上销毁他的代币。
与之前的测试类似,它首先创建 maliciousTransaction,其中 to 是 address(0),data 是 mint(address,uint256) 的 calldata。它还有一个看起来无辜的名为 transaction 的交易,它将 to 分配为 signalProcessor,data 是 sendTx(bytes32) 的 calldata,其中 _msgHash 参数作为 messageHash 传递。messageHash 是 maliciousTransaction 的哈希值。
与之前的测试类似,它首先执行 bridge.sendMsg() 以跨链发送消息。
如果你看到 sendMsg() 函数。在 BridgeSignatureReplay.sol#L61 上,如果 transaction.to 等于 address(0) 或 address(this) 并且 transaction.data 不等于 this.transfer.selector,它将从 transaction.from(在本例中为 maliciousTransaction.from)销毁 valueToTransfer。当在 BridgeSignatureReplay.t.sol#L93 上执行测试中的 bridge.sendMsg() 时,transaction.to 是 signalProcessor,因此 BridgeSignatureReplay.sol#L56 上的 if{} 永远不会执行。因此,没有代币从源链上的 transaction.from(即 msg.sender 或说攻击者)销毁。
然后,它以看起来无辜的 transaction 结构体作为参数执行 destBridge.executeMessage()。如上面的示例中所述,它将调用信号处理器来存储恶意交易哈希。
最后,它使用 maliciousTransaction 再次调用 destBridge.executeMessage()。由于 maliciousTransaction 结构体的 to 字段为 address(0),因此将执行 BridgeSignatureReplay.sol#L120 上的 if{}。然后,由于 maliciousTransaction.data 是 mint() 的 calldata 而不是 transfer() 的,因此 BridgeSignatureReplay.sol#L121 上的 if{} 将失败,并且 BridgeSignatureReplay.sol#L124 上的 else{} 将解码后的 value(9999 以太币 ) 铸造到 transaction.from(在本例中为 maliciousTransaction.from。Alice 的地址)。
问题是这些 9999e18 ERC20 代币来自哪里。答案是无处可来。因为用户在执行 sendMsg() 函数时从未在源链上销毁他的代币,如上所述。
在 BridgeSignatureReplay.test_sendMsg_nested() 和 BridgeSignatureReplay.test_sendMsg_nested_transfer() 中,漏洞是可能发生的,因为逻辑无法保护桥合约免受任何人使用桥本身对其他地址执行任意调用的影响。在本例中,这个任意地址是 signalProcessor。
为了避免这种情况,通常一个好主意是将调用限制为仅信任的地址,即使允许调用信任的地址,也要检查它是否容易受到任何漏洞的影响(就像本例中一样)。此外,根据逻辑,最好不要有一个可以用来调用任意地址的函数。
下一部分(第 4 部分)展示:
- 原文链接: calibersec.com/blockchai...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!