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代币,他的转账函数如下,这里并没有进行fromto的是否相等的判断,实际的转账逻辑正发生在父合约中,正是如此,这个漏洞很少被发现

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函数,也没有进行fromto 是否相等的检查,

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凭空增多。

这个攻击成功就是利用了自我转账的漏洞,我认为要防御的话加个对转账地址和被转账地址是否相等的条件