前言
2022 年 1 月 18 日, 不知道据哪里的消息,Anyswap 说它自己的桥有些问题,其中部分代币存在被黑的风险,劝用户尽快取消授权。早上看到消息的时候还以为是中继器的问题,就没怎么管,下午看到twitter消息风向不对,已经出现利用了。赶紧去分析了一下是什么问题,发现原来是合约的问题,那既然是合约的问题,那就和我有关系了。话不多说,直接开始分析。
技术分析
本次攻击交易可以参考 https://etherscan.io/tx/0xd07c0f40eec44f7674dddf617cbdec4758f258b531e99b18b8ee3b3b95885e7d,这里的攻击的是官方提示的六种代币中的 WETH 代币。按照惯例,首先先把交易丢进 ethtx.info 中进行分析,看看情况
从图上来看,不难发现攻击者是直接批量调用了 AnyswapV4Route
的 anySwapOutUnderlyingWithPermit
函数,然后通过函数直接转移了对应存在授权用户的资产。那既然问题出在 anySwapOutUnderlyingWithPermit
函数上,我们就需要对对应函数分析就好了,下面来看 anySwapOutUnderlyingWithPermit
的代码
function anySwapOutUnderlyingWithPermit(
address from,
address token,
address to,
uint amount,
uint deadline,
uint8 v,
bytes32 r,
bytes32 s,
uint toChainID
) external {
address _underlying = AnyswapV1ERC20(token).underlying();
IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s);
TransferHelper.safeTransferFrom(_underlying, from, token, amount);
AnyswapV1ERC20(token).depositVault(amount, from);
_anySwapOut(from, token, to, amount, toChainID);
}
整个函数看下来其实逻辑非常简单,该函数会获取用户指定的 token
地址返回的 underlying
,然后调用该 underlying
的 permit
函数,附带上对应的签名对合约进行授权,然后会将对应的 underlying
转入本合约中,最后充值到 Vault
中。然后在 _anySwapOut
函数中燃烧掉对应的 token
并声明一个事件,该事件将用于跨链。这个流程对于正常的 AnyswapERC20Token
是没有问题的。但是这个流程应用到了 WETH
就出现了问题,为什么呢?我们回顾上图,不难发现 anySwapOutUnderlyingWithPermit
函数中的 underlying()
、deppsitVault
和 burn
函数调用的都不是 AnyswapV1ERC20Token
本身,而是攻击者自己部署的合约 0xb4f89d6a8c113b4232485568e542e646d93cfab1
。这说明什么呢?说明攻击者在调用 anySwapOutUnderlyingWithPermit
函数时传入的 token
参数不是正常的 AnyswapERC20Token
,而是一个自定义的恶意代币,但是对应的 underlying
函数的返回值却是 WETH
,并且由于 anySwapOutUnderlyingWithPermit
函数调用 AnyswapV1ERC20Token
的方式是 interface
的方式,自然对应的 underlying()
、deppsitVault
和 burn
函数调用其实都是在调用攻击者自己的合约。
可是问题似乎并不是在这里,从 ethtx.info
展现的流程图中,攻击者是直接将授权用户的 WETH
直接转移到自己部署的恶意合约中的,通过回顾函数逻辑,不难发现,从函数的实现上,是要有一步 WETH
对 AnySwapRouter
合约的一次授权,才会有接下来的 transferFrom
操作。而且这个授权是需要对应的签名的,难道用户拿到了所有授权用户的私钥吗?其实并不是,我们看下调用 permit
函数所传入的对应的参数
从图中,不难发现传入的用于验证签名的 v
、r
和 s
都是一个0值,意味着其实 permit
这个函数其实根本没有起作用,也就是说 WETH.permit()
这个流程被绕过了。分析到这里,我在想难道是因为 WETH
的 permit
函数用了 if-else
的形式来判断签名吗,所以就算签名不通过,也不会报错。为了验证这个想法,就需要去看 WETH
的 permit
函数的实现。但是通过查询,AnyswapRouter
使用的 WETH9
合约根本没有 permit()
函数。
那没有这个函数的话,这行调用是怎么通过的呢?这里就有一个很古老的 trick
了,我们知道,如果一个函数在合约中不存在,那么对应的调用就会去调用合约的 fallback
函数,而从上图,不难发现 WETH
确实是实现了 fallback
函数,而这个 fallback
函数调用的是 deposit
函数,这波就舒服了,我们知道 WETH
的 deposit
函数是允许 msg.value
为 0 的情况进行充值来兑换 WETH
的,也就是说在 AnyswapRouter
这里,这个 permit
的调用其实是调用了 WETH
的 deposit
函数,充值数量为0 :D。那么这个调用显然是可以通过的,所以剩下的条件就只剩下用户授权了。用户一旦授权,任何人都可以通过 anySwapOutUnderlyingWithPermit
来进行窃取受影响的资产。
为了好理解一点,我画了个图,大家可以将就看看
总结
本次的问题在于和 WETH
的兼容性的问题,如果WETH
的permit
函数本身存在并正确实现,或者如果 Anyswap
团队意识到这个兼容性的问题,那么这次事故就不会发生。
原文始发于微信公众号(蛋蛋的区块链笔记):如果不能在一起,就学会分开 — AnySwap 漏洞分析