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如上的合约即可成功调用。