
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触发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一个合约非方法的四字节地址时,那么就会直接给其转账。那么贴图看下字节码的执行。

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