攻击过程分析2023年1月12日下午14:22:39,CirculateBUSD项目跑路,损失金额227万美金。
2023年1月12日下午 14:22:39 ,CirculateBUSD项目跑路,损失金额227万美金。我将跟随教程分析这一事件,这个攻击总体来说并不复杂,我会将分析思路详细地记录下来。
攻击基本信息
@KeyInfo - Total Lost : ~ 2 270 000 US$
Attack Tx: https://bscscan.com/tx/0x3475278b4264d4263309020060a1af28d7be02963feaf1a1e97e9830c68834b3
Attacker Address(EOA): 0x5695Ef5f2E997B2e142B38837132a6c3Ddc463b7
Vulnerable Address: 0x9639d76092b2ae074a7e2d13ac030b4b6a0313ff
@Analysis
Blocksec : https://twitter.com/BlockSecTeam/status/1556483435388350464
首先,我们对此次攻击交易进行函数追踪,通过 phalcon 来追踪函数,结果如下,
整个攻击过程非常简单,Attacker 调用CirculateBUSD.startTrading
,可以看到Attacker输入了3个参数,分别是 _trader = Attacker, _borrowAmount, _swappedToken=WBNB, 进入debug模式,不难看出startTrading
是抵押借款的函数,该函数已开源,我们来看看,
function startTrading(address _trader, uint256 _borrowAmount, address _swappedToken) public {
require( msg.sender == _trader || msg.sender == AutoStartOperator, "You don't have permission" );
if(lastStartMID[_swappedToken] != currentMinuteID())
lastStartMBorrowAmount[_swappedToken] = 0;
(,, uint256 MinCollateralLimit,,,uint256 DailyLoanInterestRate,,) = ISwapHelper(SwapHelper).TradingInfo( BUSDContract,_swappedToken );
uint256 _borrowableAmount = getBorrowableAmount(_swappedToken, _trader); // 获取可借款金额
require( _borrowAmount <= _borrowableAmount, "Over full borrowable amount limit" );
require( debtors[_trader].collateralAmount >= MinCollateralLimit, "Cannot trade without depositing collateral funds more than MinCollateralLimit" );
require( debtors[_trader].tradingState == false, "Trading has already started" );
totalTradingAmount[_swappedToken] += _borrowAmount;
lastStartMBorrowAmount[_swappedToken] += _borrowAmount;
IERC20(BUSDContract).safeApprove(SwapHelper, _borrowAmount);
uint256 swapOutAmount = ISwapHelper(SwapHelper).swaptoToken( BUSDContract,_swappedToken, _borrowAmount);
debtors[_trader].tradingState = true;
debtors[_trader].debtAmount = _borrowAmount;
debtors[_trader].swappedAmount = swapOutAmount;
debtors[_trader].swappedToken = _swappedToken;
debtors[_trader].startTime = block.timestamp;
debtors[_trader].withdrawableAmount = 0;
addTrader(_trader);
lastStartMID[_swappedToken] = currentMinuteID();
unsetAutoStartTrading(_trader);
// calculate the total profit
if(tradeInfo[_swappedToken].startstate==false){
tradeInfo[_swappedToken].startstate = true;
tradeInfo[_swappedToken].lastTradeTime = block.timestamp;
}
tradeInfo[_swappedToken].totalTradeProfit += (tradeInfo[_swappedToken].totalTradeAmount * ( block.timestamp - tradeInfo[_swappedToken].lastTradeTime ) * DailyLoanInterestRate).div(percentRate * rewardPeriod);
tradeInfo[_swappedToken].totalTradeAmount += _borrowAmount;
tradeInfo[_swappedToken].lastTradeTime = block.timestamp;
}
function getBorrowableAmount(address _toToken, address _trader) public view returns(uint256 _amount){
(uint256 MaxStartMLimit,,, uint256 MaxLoanLimit, uint256 MaxTotalTradingLimit,,,uint256 LoanDivCollateral) = ISwapHelper(SwapHelper).TradingInfo( BUSDContract,_toToken );
if(lastStartMID[_toToken] != currentMinuteID())
_amount = MaxStartMLimit;
else
_amount = MaxStartMLimit.sub(lastStartMBorrowAmount[_toToken]);
if(_amount > MaxLoanLimit)
_amount = MaxLoanLimit;
if(MaxTotalTradingLimit > totalTradingAmount[_toToken]){
if(_amount > MaxTotalTradingLimit - totalTradingAmount[_toToken])
_amount = MaxTotalTradingLimit - totalTradingAmount[_toToken];
}
else
_amount = 0;
if(_amount>debtors[_trader].collateralAmount * LoanDivCollateral)
_amount = debtors[_trader].collateralAmount * LoanDivCollateral;
uint256 _bal = getBalance();
if(_amount > _bal){
_amount = _bal;
}
}
这个函数是抵押借款的,首先需要满足以下条件:
MinCollateralLimit
的抵押资金lastStartMID
是否不等于当前的分钟ID,如果是,则将lastStartMBorrowAmount
设置为0。getBorrowableAmount
函数进行计算的,
ISwapHelper(SwapHelper).swaptoToken( BUSDContract,_swappedToken, _borrowAmount)
lastStartMID
被设置为当前的分钟ID,并且为交易者取消AutoStartTrading
标志。tradeInfo
结构体中的总交易利润、总交易金额和上次交易时间。看起来似乎都是正常的抵押借款流程,每一步都似乎是正常的,有什么问题呢?
在整个过程里,都是使用了ISwapHelper(SwapHelper)
,一个是利用TradingInfo
获取合约借款信息的来源,二是通过swaptoToken
来转账,在bsc浏览器上查看并没有开源,我们来尝试利用深入了解一下ISwapHelper(SwapHelper),利用dedaub进行反编译,结果如下
function 0x598cd567(address varg0, address varg1) public payable {
require(4 + (msg.data.length - 4) - 4 >= 64);
v0 = 0x4e0(varg1, varg0);
require(v0, Error('unable to swap')); // 判断借款合约和代币是否合理
v1 = v2 = stor_a;
v3 = v4 = stor_b;
v5 = v6 = stor_c;
v7 = v8 = stor_d;
v9 = v10 = stor_e;
v11 = v12 = varg0 == stor_3_0_19;
if (v12) {
v11 = varg1 == _bUSDContract;
}
if (v11) {
v1 = v13 = _SafeDiv(stor_a, 200);
v3 = _SafeDiv(stor_b, 200);
v5 = _SafeDiv(stor_c, 200);
v7 = _SafeDiv(stor_d, 200);
v9 = _SafeDiv(stor_e, 200);
}
// 如果varg0是_bUSDContract,varg1是stor_3_0_19,将a,b,c,d,e直接返回
// 如果varg1是_bUSDContract,判断varg0是不是stor_3_0_19,如果二者都符合,将a,b,c,d,e除以200,并返回信息
return v1, v3, v5, v7, v9, stor_f, stor_10, stor_11;
}
function 0x4e0(address varg0, address varg1) private { // 判断借款合约和代币是否合理
v0 = v1 = 0;
v2 = v3 = varg1 == _bUSDContract;
if (v3) {
v2 = v4 = varg0 == stor_3_0_19;
}
if (v2) {
v0 = v5 = 1;
} // 如果varg1是_bUSDContract,判断varg0是不是stor_3_0_19,如果二者都符合,v0为真
v6 = v7 = varg1 == stor_3_0_19;
if (v7) {
v6 = v8 = varg0 == _bUSDContract;
}
if (v6) {
v0 = v9 = 1;
}// 如果varg1是stor_3_0_19,判断varg0是不是_bUSDContract,如果二者都符合,v0为真
return v0;
}
一开始猜测会不会因为参数顺序写反了导致出现问题,但根据交易回显来看,参数顺序是正确的。我们接着来看一下swaptoToken
,函数选择器是 0x63437561,这个函数是比较长的,我们首先根据transfer
来定位看看能不能找到漏洞,
function 0x63437561(address varg0, address varg1, uint256 varg2) public payable {
require(4 + (msg.data.length - 4) - 4 >= 96);
0x1a3d(varg2);
require(stor_1 != 2, Error('ReentrancyGuard: reentrant call'));
stor_1 = 2;
v0 = 0x4e0(varg1, varg0);
require(v0, Error('unable to swap'));
if (varg2 != 0) {
v1, /* uint256 */ v2 = varg0.allowance(address(this), stor_4_0_19).gas(msg.gas);
require(bool(v1), 0, RETURNDATASIZE()); // checks call status, propagates error data on error
MEM[64] = MEM[64] + (RETURNDATASIZE() + 31 & ~0x1f);
require(MEM[64] + RETURNDATASIZE() - MEM[64] >= 32);
0x1a3d(v2);
if (v2 == 0) {
....
}
if (this.balance >= 0) { // 合约balance不为0,执行下面逻辑
...
if (varg2 != stor_7) {
....
v43, /* uint256 */ v44 = _getSwapOut.swapExactTokensForTokens(varg2, 0, v36, msg.sender, block.timestamp, v20, varg0).gas(msg.gas);
....
} else { // varg2 == stor_7
v49 = _SafeSub(_percentRate, stor_8);
require(!(bool(varg2) & (v49 > uint256.max / varg2)), Panic(17)); // arithmetic overflow or underflow
v50 = _SafeDiv(varg2 * v49, _percentRate);
v51 = _SafeSub(varg2, v50);
if (this.balance >= 0) {
if (varg0.code.size > 0) {
v52 = v53 = 0;
while (v52 < 68) {
MEM[MEM[64] + v52] = MEM[MEM[64] + 32 + v52];
v52 = v52 + 32;
}
if (v52 > 68) {
MEM[MEM[64] + 68] = 0;
}
@ >>> v54, /* uint256 */ v55, /* uint256 */ v56, /* uint256 */ v57 = varg0.transfer(stor_6_0_19, v51).gas(msg.gas);
if (RETURNDATASIZE() == 0) {
v58 = v59 = 96;
} else {
v58 = v60 = new bytes[](RETURNDATASIZE());
RETURNDATACOPY(v60.data, 0, RETURNDATASIZE());
}
...
}
}
}
}
}
}
我们追踪到transfer
函数,为了判断此函数位置是否是漏洞,我们使用 cast storage
读取 stor_6_0_19 的值(uint256 stor_6_0_19; // STORAGE[0x6] bytes 0 to 19
),
cast storage 0x112f8834cd3db8d2dded90be6ba924a88f56eb4b 6 --rpc-url $BSC
0x0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7
发现其值为0x5695ef5f2e997b2e142b38837132a6c3ddc463b7
,也就是Attacker EOA的地址,看来出现问题的就是这。我们接着来往上看看执行到 transfer
需要满足什么条件,如果是正常转账应该通过_getSwapOut.swapExactTokensForTokens进行转账,通过 cast storage 读取,其值为0x10ed43c718714eb63d5aa57b78b54704e256024e,在bscScan上可以看到这是个开源的 pancakerouter 。
如果 varg2 == stor_7 就会转到后门函数进行转账,通过 cast run 读取slot_7的值为 0x000000000000000000000000000000000000000000000000010168ada6bd50d4, 这和调用swapToken的参数不一致,我们来看看 stor_7 是否能够被修改.
function 0x4b2d25ef(address varg0, uint256 varg1, uint256 varg2) public payable {
require(4 + (msg.data.length - 4) - 4 >= 96);
0x1a3d(varg1);
0x1a3d(varg2);
@>> require(_owner == msg.sender, Error('Ownable: caller is not the owner'));
stor_6_0_19 = varg0;
@> stor_7 = varg1;
stor_8 = varg2;
}
stor_7的确可以被 owner 修改,由此判断此次事件是 项目方作恶 。
用户在投资任何项目之前,需要关注项目方的更新和公告,请确保项目方已经对合约进行了充分的安全审计。
这是因为在进行项目审计时,审计公司会关注:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!