BSC攻击事件分析 这是一起发生在2024年5月8日币安智能链(BSC)上的闪贷攻击事件,影响的是GPU代币合约。该合约存在自转账漏洞,每次自转账都会导致资产翻倍。黑客利用该漏洞,通过DODO协议以闪贷的方式借入BUSD,在PancakeSwap上兑换成GPU代币,再利用漏洞进行多次自转账,导致其GPU代币余额增加了100亿倍,达到28,070,259,409,924枚代币。最后,黑客将GPU代币卖出换成BUSD,用于偿还闪贷本息,最终窃取约3.2万美元资金。
首先攻击者从DODO借贷协议中发起闪电贷,借出BUSD到攻击合约地址,攻击者合约在调用PancakeSwap协议,将借出的BUSD兑换成GPU代币,利用自我转账漏洞,凭空产生CPU代币,攻击者合约自己给自己转账,每转账一次,GPU资产就会翻倍,攻击者合约将获取到的GPU代币发送到PancakeSwap V2协议中,换取BUSD。然后攻击者从PancakeSwap V2协议中获取的BUSD,将其中的一部分用于返还DODO借贷协议的本金和利息,剩下的都是攻击者所获得的。
完整的调用信息看这里
下面是DODO协议中借贷协议中的借贷函数flashLoan 的部分信息,用于从DODO协议资金池中借贷指定数量的资产,这里的参数baseAmount为零,而quoteAmount为226007,表示借贷这么多个BUSD,assetTo为攻击者合约地址,data则在回调函数中使用到 简单的讲该函数分为3个部分,第一个部分讲资产发送到目标地址,第二部分执行目标地址的回调函数,通常用于实现套利逻辑,第三部分校验是否归还本利息
function flashLoan( uint256 baseAmount, uint256 quoteAmount, address assetTo, bytes calldata data ) external preventReentrant { // step1 transfer token to target address _transferBaseOut(assetTo, baseAmount); _transferQuoteOut(assetTo, quoteAmount); // step2 callback function if (data.length > 0) IDODOCallee(assetTo).DPPFlashLoanCall(msg.sender, baseAmount, quoteAmount, data); uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)); uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)); // step3 check balance require( baseBalance >= _BASE_RESERVE_ || quoteBalance >= _QUOTE_RESERVE_, "FLASH_LOAN_FAILED" ); // ...; }
PancakeRouter 合约,该合约主要用于BUSD和GPU代币的兑换,由于GPU合约会在兑换环节中收取手续费,兑换的另一个代币数量可能无法提前准确计算,因此这里使用了下面的swapExactTokenForTokensSupportingFeeOnTransferTokends函数 ,参数amountln 表示往池子里存入的代币数量,而参数amountOutMin 表示从池子里取出来另一种代币的最小数量,path 代表代币的兑换路径,如 [address(A),address(B)]表示A代币兑换成B代币,参数 to 表示兑换出来的代币转入目标地址,deadline 为兑换有效日期 该函数简单分为以下几个部分,第一部分把A代币转入相应的池子中,第二部分通过**_swapSupportingFeeONTransferTokens函数计算B代币的数量并转给 to地址,第三部分判断得到的B代币数量是否满足 zamountOutMin**的要求
function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) { // step1 transfer A token to pool TransferHelper.safeTransferFrom( path[0], msg.sender, PancakeLibrary.pairFor(factory, path[0], path[1]), amountIn ); uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); // step2 calculate and transfer B token _swapSupportingFeeOnTransferTokens(path, to); // step3 check B token amount require( IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, 'PancakeRouter: INSUFFICIENT_OUTPUT_AMOUNT' ); }
GPU是一个普通的ERC20代币,他的转账函数如下,这里并没有进行from 和to 的是否相等的判断,实际的转账逻辑正发生在父合约中,正是如此,这个漏洞很少被发现
function _transfer( address from, address to, uint256 amount) internal override { require(from != address(0),"ERC20: transfer from the zero address"); require(to != address(0),"ERC20: transfer to the zero address"); require(amount>0); } if(_isExcludedFromFeesVip[from] || _isExcludedFromFeesVip[tol]){ super._transfer(from, to, amount); You, 2 days ago " add demo return; if(super.balance0f(address(this)) >super.balance0f(uniswapV2Pair).div(2000)){ if (_isExcludedFromFees[from] || _isExcludedFromFees[tol]) {} else { if(_isPairs[from]){ require(startTime < block.timestamp,"startTime"); if(startTime.add(18 * 30 *86400) > block. timestamp){ super._transfer(from, _destroyAddress, amount.div(100).mul (2)); super._transfer(from, address(this),amount.div(100).mul(1)); amount = amount.div(100).mul(97); } }
而父合约的_tranfer 函数,也没有进行from 和to 是否相等的检查,
function _transfer(address sender,address recipient,uint256 amount )internal virtual { (requirelsender != address(0), "ERC20: transfer from the zero address"); (require(recipient != address(0),"ERC20: transfer to the zero address"); uint256 senderAmount = _balances [sender]; uint256 recipientAmount = _balances [recipient]; (requirelsenderAmount >= amount,"ERC20:transfer amount exceeds balance"); _balances [sender]= senderAmount. sub(amount); _balances[recipient] = recipientAmount.add(amount); emit Transfer(sender, recipient, amount); }
从以上代码我们可以看见,,在进行 transfer 时,先保存 from 和 to 的 balance 到变量 senderAmount 和 recipientAmount,随后计算转账后的 senderAmount 和 recipientAmount 的值,最后在更新到 balances 中。但是,当 from 和 to 为同一地址时,先更新 from 再更新 to 其实就是给 to 凭空添加了 amount 数量的 token。所以,攻击者通过持续给自己转账从而让自己的GPU Token凭空增多。
这个攻击成功就是利用了自我转账的漏洞,我认为要防御的话加个对转账地址和被转账地址是否相等的条件