2023年12月8日,OpenZeppelin官方向社区发布了一则重要的安全警报。警报指出,在项目集成中使用ERC-2771标准与类Multicall方式时,可能存在任意地址欺骗攻击的风险。
SharkTeam对此事件第一时间进行了技术分析,并总结了安全防范手段,希望后续项目可以引以为戒,共筑区块链行业的安全防线。
一、攻击交易分析
由于存在一系列与该漏洞相关的攻击交易,我们选择其中一笔攻击交易进行分析。
攻击者地址:0xFDe0d1575Ed8E06FBf36256bcdfA1F359281455A
攻击交易:0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6
攻击流程:
1.首先。攻击者(0xFDe0d157)先利用5枚WETH兑换了约3,455,399,346枚 TIME。
2.随后,攻击者(0xFDe0d157)构建了恶意的calldata参数并调用了[Forwarder].execute函数。
3.在调用[Forwarder].execute函数时,恶意的calldata触发了TIME合约的multicall函数。随后,使用剩余的calldata触发执行TIME合约的burn函数,销毁池中的TIME代币。
二、漏洞分析
首先,此次攻击事件主要涉及几个方面:ERC2771、Multicall、经过精心构造的calldata。我们可以从TIME代币合约中找到相关的继承:
1.ERC2771提供了拥有虚拟的msg.sender的能力,允许用户委托第三方[Forwarder]执行交易,用来降低gas成本。提交交易时,msg.sender地址会被添加到calldata中。
2.TIME 代币合约继承了 ERC2771Context。当[Forwarder]调用合约时,_msgSender() 会检查 calldata 数据,并将其右移,截断最后的 20 个字节作为预期的 msg.sender。
3.Multicall是一种将单个函数调用转变为在同一个合约中按顺序调用多个函数的方法。它接受一个用户编码调用的数组并对其自身合约执行。这个函数遍历调用数组,并对每一个操作执行 delegatecall()。这允许用户组合自己的一系列操作,并在同一笔交易中顺序执行,而无需在协议中预先定义好某些操作组合。它主要目的也是为了节省gas。
4.对于经过精心构造的 calldata,攻击者调用了 [Forwarder].execute 函数,并传入相关参数。
我们对data值进行相应的可读格式化后得出:
攻击者(0xFDe0d157)通过对当前 calldata 的偏移操作获得新的 data 值,并将该值传递给 multicall(bytes[]) 函数。新 data 的前 4 个字节是 burn(uint256) 函数的选择器,amount 参数为 62227259510000000000000000000。
5.在multicall(bytes[])函数中,通过delegatecall调用burn(uint256)函数。在0x20这一行,0x760dc1e043d99394a10605b2fa08f123d60faf84地址是在构造calldata时一开始添加在末尾的。该地址对应Uniswapv2上的TIME-ETH流动性池,即前文提到的预期的msg.sender。
6.刚才提到的msg.sender为何变成TIME-ETH流动性池地址?原因是一开始msg.sender是[Forwarder]合约地址。为了判断是否是可信的[Forwarder],如果是可信的[Forwarder],则将msg.sender设置为calldata的最后20个字节。
三、安全建议
此次攻击事件的根本原因:在ERC-2771中,[Forwarder]并不是专为multicall设计。攻击者将_msgSender()函数中的相关参数添加到multicall的外部调用中,即本次事件的[Forwarder].execute函数。在multicall函数中,一些函数也会附加_msgSender()中的相关参数,从而允许攻击者欺骗_msgSender()。因此,攻击者通过使用multicall调用相关函数,可以模仿任意地址的调用。最终,通过授权销毁池子里的TIME代币。
针对此事件,可采取以下缓解和防范措施:
1.使用修复bug后的新版本,OpenZeppelin新版本的 Multicall 带有 ERC2771context 数据的context后缀长度,用于标识 ERC-2771 预期的context后缀长度。因此,来自可信任[Forwarder]的任何call都将被识别并适应每个子函数call。
以下是bug版本和已更新版本的对比图:
2.禁止任何合约调用multicall来防止[Forwarder]使用它,以ThirdWeb为例,该方法与OpenZeppelin的解决方案相比,OpenZeppelin仍然允许通过合约进行multicall。以下是ThirdWeb的相关bug版本和已更新版本的对比图。