04.airdrop-hunting @fake3d
2023-07-13 16:27:32 # 02.ChainflagCTF

airdrop-hunting(fake3d)

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
/**
*Submitted for verification at Etherscan.io on 2018-11-27
*/

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 WinnerList{
address owner;
struct Richman{
address who;
uint balance;
}

function note(address _addr, uint _value) public{
Richman rm;
rm.who = _addr;
rm.balance = _value;
}

}

contract Fake3D {
using SafeMath for *;

mapping(address => uint256) public balance;
uint public totalSupply = 10**18;
WinnerList wlist;

event FLAG(string b64email, string slogan);

constructor(address _addr) public{
wlist = WinnerList(_addr);
}

modifier turingTest() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
_;
}

function transfer(address _to, uint256 _amount) public{
require(balance[msg.sender] >= _amount);
balance[msg.sender] = balance[msg.sender].sub(_amount);
balance[_to] = balance[_to].add(_amount);
}


function airDrop() public turingTest returns (bool) {
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number))));

if((seed - ((seed / 1000) * 1000)) < 288){
balance[tx.origin] = balance[tx.origin].add(10);
totalSupply = totalSupply.sub(10);
return true;
}else{
return false;
}
}

function CaptureTheFlag(string b64email) public{
require (balance[msg.sender] > 8888);
wlist.note(msg.sender,balance[msg.sender]);
emit FLAG(b64email, "Congratulations to capture the flag?");
}

}

analyses

This level is about airdrop, and it is funny, let’s look at the airdrop() function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function airDrop() public turingTest returns (bool) {
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number))));

if((seed - ((seed / 1000) * 1000)) < 288){
balance[tx.origin] = balance[tx.origin].add(10);
totalSupply = totalSupply.sub(10);
return true;
}else{
return false;
}
}

seed - ((seed / 1000) * 1000)) < 288:The obtained seed needs to meet certain conditions: less than 288, which means a probability of success of 288/1000.

Because the information that determines the seed is in the block, the caller cannot control it (miners can choose), so we can only try it randomly until we get 8888 money(only get 10 if we guess successfully:( so it will take many times to get 8888 money). Fortunately, this method only consumes GAS instead of ETH.

And then let’s look at the turingTest(), it means we can use an EOA account to call airdrop() or a contract that only has a constructor() but no function(). So in this level, we can use EOA account to do or create a contract to do. You can create a specific account in the site.

1
2
3
4
5
6
7
modifier turingTest() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
_;
}

But when we are resolving it, something wrong happened…. And this is the funny part, let’s look at the note() function:

1
2
3
4
5
function note(address _addr, uint _value) public{
Richman rm;
rm.who = _addr;
rm.balance = _value;
}

It is so normal, right? but why our tx fail? Because the puzzle provider us a wrong contract! The WinnerList contract is a fake contract! I search the WinnerList tx in the blockchain browser, found the address of WinnerList and decompile it, the output is as following:

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
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;

if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;

if (var0 != 0x03b6eb88) { revert(memory[0x00:0x00]); }

var var1 = msg.value;

if (var1) { revert(memory[0x00:0x00]); }

var1 = 0x0091;
var var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
var var3 = msg.data[0x24:0x44];
func_0093(var2, var3);
stop();
}

function func_0093(var arg0, var arg1) {
var var0 = 0x00;
storage[var0] = (arg0 & 0xffffffffffffffffffffffffffffffffffffffff) | (storage[var0] & ~0xffffffffffffffffffffffffffffffffffffffff);
storage[var0 + 0x01] = arg1;
var var1 = ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & 0x0100000000000000000000000000000000000000000000000000000000000000 * 0xb1;
var var2 = tx.origin * 0x01000000000000000000000000;
var var3 = 0x12;

if (var3 >= 0x14) { assert(); }

var temp0 = byte(var2, var3) * 0x0100000000000000000000000000000000000000000000000000000000000000 & ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff != var1;
var1 = temp0;

if (!var1) {
label_023F:

if (!var1) { return; }
else { revert(memory[0x00:0x00]); }
} else {
var1 = ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & 0x0100000000000000000000000000000000000000000000000000000000000000 * 0x43;
var2 = tx.origin * 0x01000000000000000000000000;
var3 = 0x13;

if (var3 >= 0x14) { assert(); }

var1 = byte(var2, var3) * 0x0100000000000000000000000000000000000000000000000000000000000000 & ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff != var1;
goto label_023F;
}
}
}

Obviously, the real contract has a lot of code that proves the contract given us is fake. But how to read it? I can’t understand the decompile contract :( Don’t worry, let’s analysis it step by step.

(1) We can easily infer the func_0093() function is note()

(2) address _addr ===> var arg0, uint _value ===> var arg1

(3)

https://moe.photo/images/2023/05/21/_20230521191426.png

(4) byte(x,y): nth byte of x, where the most significant byte is the 0th byte. In this case, the penultimate byte of tx.origin is 0xb1.

(5) if the penultimate byte is not 0xb1, 0x43 in the last byte can be ok.

solve

All in all, we can solve this level like this:

  1. get a specific account that pass the function note(), you have 2 choice:
    • EOA account: get a specific EOA in this site
    • create a contract with opcode CREATE2, while it only has a constructor.
  2. call airDrop() again and again until we get 8888 money

All in all, the puzzle provider is tricky, he gave us a wrong ABI(WinnerList), and we can only decompile its bytecode and analyses diffucultly.