区块链又3道题目分析(主过程)

 

RCTF2020 roiscoin

题目给了源码

Resource

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

给了源码可以说好分析的多。 查看payforflag的条件是balanceof[msg.sender]>=2000 还有就是调用者必须为owner.
然后查看这里的balance 如何来加, 通过赌注,但是这里赌注的随机数无法预测但是只有0和1,还是可以爆破的。首先讲非预期。

非预期:

由于beOwner中的 address(this).balance在计算时算了msg.value。
所以只要原合约的初始为0,那么我们转账>0就可以拿到BeOwner 然后在暴力猜数字2次成功就可以payforflag了。

预期:

我们可以看到在battle里面,如果猜错这里用了一个在这里定义的结构体。而结构体的内存这里没有声明使用memory而是使用了stroage ,这里便引起了变量覆盖。
这里的failedlog未初始化造成了storage的任意写从而我们可以来覆写我们的codex的数组长度。 数组长度任意写之后,我们下一步就是想把owner写成我们自己。 数组任意写,对长度有一定要求,利用msg.owner覆盖了数组的高20字节。
那么我们就考虑这个codex[] 他的长度codex.length在storage[5] 他的计算是从

keccak256(5)+var0 var0可控。 如果我们在这里 x=keccak256(5) 那么传入

2^256+6-x 我们就可以任意写storage[6] 也就是owner 。这一段如果不太理解最好是对着反汇编看。因为这里源代码反而没有那么直观。

PS:这里为什么+2^256,因为不能传入负数。

写完storage[6]后,只需要满足猜两次就够了。

他用的是未来随机数,不过他就需要猜对2次,就蒙就可以了。
这里还是不放 exp,建议师傅们自己来尝试一下。并且RCTF的wp中也有完整的exp。大家都可以去学习。

 

华为鸿蒙场区块链

华为鸿蒙场的区块链,比赛在考试,现在来复现下,题目没有给出源码。但是已经找不到复现了。应该是pikachu师傅用他的docker出的。这里我自己部署了下原合约。然后重新逆向一次。
经过逆向以及

Resource

pragma solidity ^0.4.23;

contract ContractGame {

    event SendFlag(address addr);

    mapping(address => bool) internal authPlayer;
    uint private blocknumber;
    uint private gameFunds;
    uint private cost;
    bool private gameStopped = false;
    address public owner;
    bytes4 private winningTicket;
    uint randomNumber = 0;
    mapping(address=>bool) private potentialWinner;
    mapping(address=>uint256) private rewards;
    mapping(address=>bytes4) private ticketNumbers;

    constructor() public payable {
        gameFunds = add(gameFunds, msg.value);
        cost = div(gameFunds, 10);
        owner = msg.sender;
        rewards[address(this)] = msg.value;
    }

    modifier auth() {
        require(authPlayer[msg.sender], "you are not authorized!");
        _;
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a);
        uint256 c = a - b;
        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }
        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0);
        uint256 c = a / b;
        return c;
    }

    function BetGame(bool mark) external payable {
        require(msg.value == cost);
        require(gameFunds >= div(cost, 2));
        bytes32 entropy = blockhash(block.number-1);
        bytes1 coinFlip = entropy[10] & 1;
        if ((coinFlip == 1 && mark) || (coinFlip == 0 && !mark)) {
            gameFunds = sub(gameFunds, div(msg.value, 2));
            msg.sender.transfer(div(mul(msg.value, 3), 2));
        } else {
            gameFunds = add(gameFunds, msg.value);
        }

        if (address(this).balance==0) {
            winningTicket = bytes4(0);
            blocknumber = block.number + 1;
            gameStopped = false;
            potentialWinner[msg.sender] = true;
            rewards[msg.sender] += msg.value;
            ticketNumbers[msg.sender] = bytes4((msg.value - cost)/10**8);
        }
    }

    function closeGame() external auth {
        require(!gameStopped);
        require(blocknumber != 0);
        require(winningTicket == bytes4(0));
        require(block.number > blocknumber);
        require(msg.sender == owner || rewards[msg.sender] > 0);
        winningTicket = bytes4(blockhash(blocknumber));
        potentialWinner[msg.sender] = false;
        gameStopped = true;
    }

    function winGame() external auth {
        require(gameStopped);
        require(potentialWinner[msg.sender]);
        if(winningTicket == ticketNumbers[msg.sender]){
            emit SendFlag(msg.sender);
        }
        selfdestruct(msg.sender);
    }

    function AddAuth(address addr) external {
        authPlayer[addr] = true;
    }

    function() public payable auth{
        if(msg.value == 0) {
            this.closeGame();
        } else {
            this.winGame();
        }
    }
}

题目不难,但是逻辑比较多,比较符合pikachu师傅出题的规律非常有学习代表性。首先是在functon中自写了4种运算规则,类似safemath库。

这里剩下可调用的函数采用了external auth等函数声明方法,经过查询也是public的 是可以被外部调用的。主要是可以大量减少在外部传入大数组时的合约交互的gas。

 function() public payable auth{
        if(msg.value == 0) {
            this.closeGame();
        } else {
            this.winGame();
        }
    }

这里是一个fallback是非常有应用价值的。
后面几个函数也都来分析下。

function winGame() external auth {
        require(gameStopped);
        require(potentialWinner[msg.sender]);
        if(winningTicket == ticketNumbers[msg.sender]){
            emit SendFlag(msg.sender);
        }
        selfdestruct(msg.sender);
    }

Wingame中,需要game已经停止, 并且需要potentialWinner[msg.sender]为1,并且如果winningticket == ticketNumbers[msg.sender]就会触发flag了。

 function closeGame() external auth {
        require(!gameStopped);
        require(blocknumber != 0);
        require(winningTicket == bytes4(0));
        require(block.number > blocknumber);
        require(msg.sender == owner || rewards[msg.sender] > 0);
        winningTicket = bytes4(blockhash(blocknumber));
        potentialWinner[msg.sender] = false;
        gameStopped = true;
    }

这里主要进行了closegame 也就是gamestop赋值。这里需要的是game还没stop且blocknumber!=0,并且winningticket=bytes4(0) 且block.number>blocknumber 以及msg.sender已经变成owner,且rewards[msg.sender]

那么这里就会赋值potentialWinner[msg.sender]=false gamestopped=true。这里成功满足了wingame的第一个但是没有满足第二个。

那么现在接着看构造函数。

 constructor() public payable {
        gameFunds = add(gameFunds, msg.value);
        cost = div(gameFunds, 10);
        owner = msg.sender;
        rewards[address(this)] = msg.value;
    }

创建的时候,直接会让gameFunds=gameFunds+msg.value传入值。

cost= gamefunds/10

owner就变成了msg.sender.

且rewards[address(this)]=msg.value

还有一个Bet函数

function BetGame(bool mark) external payable {
        require(msg.value == cost);
        require(gameFunds >= div(cost, 2));
        bytes32 entropy = blockhash(block.number-1);
        bytes1 coinFlip = entropy[10] & 1;
        if ((coinFlip == 1 && mark) || (coinFlip == 0 && !mark)) {
            gameFunds = sub(gameFunds, div(msg.value, 2));
            msg.sender.transfer(div(mul(msg.value, 3), 2));
        } else {
            gameFunds = add(gameFunds, msg.value);
        }

        if (address(this).balance==0) {
            winningTicket = bytes4(0);
            blocknumber = block.number + 1;
            gameStopped = false;
            potentialWinner[msg.sender] = true;
            rewards[msg.sender] += msg.value;
            ticketNumbers[msg.sender] = bytes4((msg.value - cost)/10**8);
        }
    }

这里先要求cost 也就是创建时候的msg.value/10 == 当前传入的msg.value

并且gamefunds >= cost/2

然后是经典的随机数预测。 攻击合约一模一样 写就可以得到相同的结果。

然后写了个巨奇怪的if

其实就是coinFlip==mark。猜对了的话 GameFunds+=msg.value/2

msg.sender.transfer(msg.value*1.5)

要不然就GameFunds +=msg.value

这里进行完事之后 如果合约的balance==0了

那么winningTicket=bytes(4) blocknumber+=1

gameStopped=0 potentialWinner[msg.sender]=1

rewards[msg.sender]+=msg.value

TicketNumbers[msg.sender]=bytes4((msg.value-cost)/10^8)

这里的条件直接基本把closegame这里的要求全满足了。

然后我们首先就是要开始进行题目了。 首先我们给两个ether,相当于让他创建一个有2eth 的游戏。 每次他会输出来0.1eth ,我们进行20次就够了。

然后先call AddAuth题目的合约地址,再call Addauth 外部账户地址,再CallAddauth 攻击合约的地址。
PS:这里ADDAUTH相当于给我们调用函数的权限

最后利用题目合约的fallback调用closegame防止他把我们的
potentialWinner 给改了。
那么现在就满足了所有条件
直接winGame就可以了。
贴下pikachu师傅的exp
modifier是为了允许我们的这些地址可以调用这些函数。
所以都要加到Addauth里面。
那么攻击步骤我这里重新列出

1. 首先建立攻击合约,并且打2 ether过去。
2. Addauth 使我们的题目合约,攻击合约,以及我们的外部账户都有权限调用函数。
3. 通过外部合约转账调用delegatecall触发closegame
4. call wingame()

这样就可以成功拿到flag了。

 

*CTF2021 Starndbox

六星战队在分站赛出的题,非常不错。
考察的点和2020qwb 的ezsandbox很像。 利用可用字节码清空合约余额即成功。
给出了以下源码

pragma solidity ^0.5.11;

library Math {
    function invMod(int256 _x, int256 _pp) internal pure returns (int) {
        int u3 = _x;
        int v3 = _pp;
        int u1 = 1;
        int v1 = 0;
        int q = 0;
        while (v3 > 0){
            q = u3/v3;
            u1= v1;
            v1 = u1 - v1*q;
            u3 = v3;
            v3 = u3 - v3*q;
        }
        while (u1<0){
            u1 += _pp;
        }
        return u1;
    }

    function expMod(int base, int pow,int mod) internal pure returns (int res){
        res = 1;
        if(mod > 0){
            base = base % mod;
            for (; pow != 0; pow >>= 1) {
                if (pow & 1 == 1) {
                    res = (base * res) % mod;
                }
                base = (base * base) % mod;
            }
        }
        return res;
    }
    function pow_mod(int base, int pow, int mod) internal pure returns (int res) {
        if (pow >= 0) {
            return expMod(base,pow,mod);
        }
        else {
            int inv = invMod(base,mod);
            return expMod(inv,abs(pow),mod);
        }
    }

    function isPrime(int n) internal pure returns (bool) {
        if (n == 2 ||n == 3 || n == 5) {
            return true;
        } else if (n % 2 ==0 && n > 1 ){
            return false;
        } else {
            int d = n - 1;
            int s = 0;
            while (d & 1 != 1 && d != 0) {
                d >>= 1;
                ++s;
            }
            int a=2;
            int xPre;
            int j;
            int x = pow_mod(a, d, n);
            if (x == 1 || x == (n - 1)) {
                return true;
            } else {
                for (j = 0; j < s; ++j) {
                    xPre = x;
                    x = pow_mod(x, 2, n);
                    if (x == n-1){
                        return true;
                    }else if(x == 1){
                        return false;
                    }
                }
            }
            return false;
        }
    }

    function gcd(int a, int b) internal pure returns (int) {
        int t = 0;
        if (a < b) {
            t = a;
            a = b;
            b = t;
        }
        while (b != 0) {
            t = b;
            b = a % b;
            a = t;
        }
        return a;
    }
    function abs(int num) internal pure returns (int) {
        if (num >= 0) {
            return num;
        } else {
            return (0 - num);
        }
    }

}

contract StArNDBOX{
    using Math for int;
    constructor()public payable{
    }
    modifier StAr() {
        require(msg.sender != tx.origin);
        _;
    }
    function StArNDBoX(address _addr) public payable{

        uint256 size;
        bytes memory code;
        int res;

        assembly{
            size := extcodesize(_addr)
            code := mload(0x40)
            mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            mstore(code, size)
            extcodecopy(_addr, add(code, 0x20), 0, size)
        }
        for(uint256 i = 0; i < code.length; i++) {
            res = int(uint8(code[i]));
            require(res.isPrime() == true);
        }
        bool success;
        bytes memory _;
        (success, _) = _addr.delegatecall("");
        require(success);
    }
}

上面的数学方法以2为基来算素数在0-255区间内,除了0是没有问题的,所以我们想到的就是用0来绕过它对字节码仅能为素数的限制。
给了delegatecall。
合约里面只有100wei,我们可以通过call(0xf1素数)方法来将余额清空。
比赛时候是利用强大的黑暗力量做的。因为题目部署合约100wei在Rinkedby测试链属实很少见,随便翻了翻就可以找到其中队伍做出的合约。
给出赛时exp(题目代码就不贴了)。

contract exp{
    constructor()public{}
    address ss=0xb3879a53b3964494a149BcC1863dD262C35a64aE;
    address target=0x8748ec747eB7af0B7c4e82357AAA9de00d32264a;
    StArNDBOX a=StArNDBOX(target);
    function step()external{
        a.StArNDBoX(ss);
    }
}

call的其他是没有问题的,当call一个合约非方法的四字节地址时,那么就会直接给其转账。那么贴图看下字节码的执行。

如此一来就没有质数。部署一个bytecode如上的合约即可成功调用。

(完)