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

《区块链又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相当于给我们调用函数的权限

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

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

1. 首先建立攻击合约,并且打2 ether过去。2. Addauth 使我们的题目合约,攻击合约,以及我们的外部账户都有权限调用函数。3. 通过外部合约转账调用delegatecall触发closegame4. 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一个合约非方法的四字节地址时,那么就会直接给其转账。那么贴图看下字节码的执行。

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

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

点赞