Fork me on GitHub
pikachu's Blog

Balsn CTF 2020 - Election


前言

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
199
200
201
202
203
204
205
206
207
208
209
210
211
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;
string public override symbol;
uint8 public override decimals;
uint public override totalSupply;
mapping (address => uint) private _balances;
string private constant _tokenFallback = "tokenFallback(address,uint256,bytes)";

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)) {
(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 = 0;
bool public sendFlag = false; //6
address public owner; //6
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;
mapping(address => bool) public revealed;

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));
}

/* custom added functions */
function testdeet(address to, uint value, bytes memory data, string memory customFallback) pure public returns (bytes memory){
return abi.encodeWithSignature(customFallback, to, value, data);
}

function properEncode(address candidate, Proposal memory proposal, address t1, address t2) pure public {

}

function ballotEncode(Ballot[] memory ballots) pure public returns (bytes32){
return keccak256(abi.encode(ballots));
}
}

Analyse

  • 考点是 ERC223customFallback 错误引入、ABI encodingInteger overflow ,关于 ERC223 的官方实现可点击这里 👉 officially recommended implementation 👈
  • 目标成功调用 giveMeFlag() 即可
  • 题目大概是实现了一个投票系统,我们只需要让我们的票数最多即可,包括三个阶段:proposevotereveal

    • propose : 通过 auth 验证便可添加新的 candidates
    • vote : 投票人提交 ballot 投票的 hash 值,投票的数量是任意的,这里没有限制
    • reveal : 计算所有 candidates 的票数情况
  • 56 行的这种调用可以通过 auth 验证,如果控制合理,可以调用自身的其他函数,因为 customFallback 可控,并且参数也都可控,只要我们控制的参数符合 ABI encoding format ,便可调用任何函数,关于 ABI encoding 可参考 官方文档,关于带有 auth 限制的主要有 propose_setStage 函数:

    • 当调用 customFallback 时,三个参数编码如下图左边所示
    • 当调用 propose 时,编码如下图中间所示
    • 当调用 _setStage 时,编码如下图右边所示
    • 下图取自官方 WP ,如果有疑惑,可查看我的另一片博客 👉 Function Selector and Argument Encoding 👈

  • 知道了不同函数调用参数编码,便可根据参数编码后的相应位置进行分析

    • 通过 customFallback 调用 propose,需要满足下面条件: value = 0x40(需要构造,要求 attacker 拥有 0x40token ) ,offset_to_data = 0x60(已满足),length_of_data = 0xa0(需要构造,要求 data 长度是 0xa0,同时为了通过 172 行和 173 行的检查,我们需要对 data 内容进行合理的构造)
    • 通过 customFallback 调用 _setStage,需要满足下面条件:stage 可以是 0123,所以相应只需 msg.sender00010203结尾即可,多余的 value 参数和 data 参数不影响
  • 145 行存在一个 integer overflow ,所以我们可以两个 ballot 投票,票数分别为 2^256-1(投给 attacker ) 和 1 (投给 AliceBob 任意一个),这样利用 145 行的 integer overflow 便可通过 148 行的限制,同时使 attacker 票数为 2^256-1 ,可通过 getWinner() 赢得选票

Solution

  • 本题中的一些账户,下面为了简洁,就使用简称,下面是对应关系
    • 0xef31471E3004a78Ae403858BbcB27D6d1f37791C Election
    • 0x52cC2403764380CCa583633d2523999FE5077113 attacker账户
    • 0xe7F2D75e69a989fb834e302f7caf8e1A9CC34000 00账户
    • 0x40F6dBA38C634C86Ae11D1a775FA2215b7669702 02账户
    • 0x6CDFf92000246705EFc00B625E2e3d6b0C9e7603 03账户
    • 0x0000000000000000000000000000000000009453 Alice
    • 0x0000000000000000000000000000000000009487 Bob
  1. 通过下列脚本,令 times = 64,使 _balances[attacker] = 0x40
1
2
3
4
5
6
7
8
9
10
contract son {
Election target = Election(0xef31471E3004a78Ae403858BbcB27D6d1f37791C);

function getmoney(uint times) public {
for (uint i=0; i<times; i++) {
target.giveMeMoney();
target.transfer(0x52cC2403764380CCa583633d2523999FE5077113, 1, "", "");
}
}
}

  1. 准备两个 ballot,一个投给 attacker,票数为 2^256-1 ;一个投给 Alice,票数为 1
  • 先进行 ballotEncode 计算,调用形式如下
1
ballotEncode([["0x52cC2403764380CCa583633d2523999FE5077113","0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"], ["0x0000000000000000000000000000000000009453","0x1"]])
  • 结果如下图所示

  • 调用 vote(0x741d83d533784642740e64dbea7fa658c082a96b14623cdb1866e58c252f7f23) ,然后查看 voteHashes[0] 成功显示 (使用 attacker账户

  1. 切换到 stage 0 (使用 00账户 ),data 字段随意

  1. attacker 增加到 candidate (使用 attacker账户 )

  • 其中 data 字段拆分如下

  • 查看 proposals 可以看到添加 attackercandidate 成功

  1. 切换到 stage 2 (使用 02账户 ),data 字段随意

  1. 进行 reveal 调用,其中 ballotsstep 2 中的 ballots (使用 attacker账户 ),调用完后查看 voteCount[attacker] 已经是 2^256-1

  2. 切换到 stage 3 (使用 03账户 ),data 字段随意

  1. 调用 giveMeFlag() 即可 (使用 attacker账户 ),撒花完结 🎉🎉🎉🎉🎉🎉

---------------- The End ----------------
谢谢大爷~

Author:pikachu
Link:https://hitcxy.com/2021/balsn2020-election/
Contact:hitcxy.cn@gmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!