2018 DDCTF mini blockchain(区块链) writeup

mini blockchain(区块链)

  滴滴的这道blockchain真的让人耳目一新,同时感觉也预兆了以后区块链技术出现在CTF上可能会成为一个常态,所以非常有必要去认真的了解一下这个东西。以下关于区块链的表述仅以我两天的学习结果,,,所以路过的大佬发现有表述不对的地方,劳烦指正。

  题目描述:

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

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

  首先先介绍下这道题的解法,先从整体切入,这样可能方便理解后面的操作。下面先清晰几个概念:

  • 1、这道题整的解法是 51% (双花)攻击。
  • 2、请于正常的区块链区分开来,题目环境中只有你一个玩家,并没有人与你竞争(挖矿)。
  • 3、商店交易采用0确认,而不是现实中的6确认。
  • 4、当出现分叉时,区块链的规则认最长的分链为主链,并舍去原有的链。
  • 5、区块链允许添加空块

51%(双花)攻击

  51%(双花)攻击可以达到的目的就是使攻击的交易作废,这里的前不一定是前一个,而是很大程度上取决于你的算力的。让之前的交易作废有什么好处呢?这里我们就要考虑0确认6确认的区别了。

  先看看6确认

当产生一笔交易时,区块链的P2P网络会广播这笔交易,这笔交易会被一个挖矿节点收到,并验证,如果这个挖矿节点挖到区块(生成的hash满足条件)后,并且这笔交易的手续费足够吸引这个节点去打包进区块,那这笔交易就会被打包进区块。因此就得到了一个确认,这个矿工也拿走了相应的手续费。 这个挖矿节点打包后,会把区块广播给其他节点。其他节点验证并广播这个区块。 如果这个区块得到更多的挖矿节点的验证确认,那就得到了更多的确认。这样这笔交易就被记录到了比特币区块链,并成为了比特币账本的一部分。如果得到6个确认后,我们就认为它永远不可变了。

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

  可以看到对0确认6确认进行51%(双花)攻击的难度是不一样的,6确认需要的算力明显要大,因为他要多比其他人生成6个区块。(应该可以这样理解吧,可能我还需要继续学习,如上,如有不对可以联系我(jay80#protonmail.com)改正,在这也谢谢各位大佬了。)好在,题目并不是采用6确认

  然后再看看这里的51% 攻击,其实这里说的51%是指算力,也就是这种攻击需要攻击者具备全网51%的算力,因为这样才有机会使自己生成(挖出)区块的速度超过其他人,然后按区块链的规则:当出现分叉时,区块链的规则认最长的分链为主链,并舍去原有的链,就达到了撤销原来链上已经存在的交易,拿回该交易使用了的的目的,这里我的另一个理解就是可以使交易回滚,从而追回被盗的钱。

  对攻击的原理有了简单的理解后,我们就来看看这道题从原理上应该怎么做。先放两张自己画的图:






实际构造

  原理上明白了以后,我们就开始从代码上进行实际攻击。首先我们先看一下一个标准的区块是咋样的,下面其实就是黑客盗取银行的区块:

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
{
"nonce": "HAHA, I AM THE BANK NOW!",
"prev": "5bc355ab21fd7e07040e2882f36ff8fba90809cbaa27b80bc1439a6e85beec25",
"hash": "e31e1a9a8797d464304c34f215b65edf510bd0dd251fd5d23f9a41017aaba205",
"transactions": [
{
"input": [
"e95c5a89-3f0e-4bd6-a4bc-8ff006fa2a42"
],
"signature": [
"8cf74260504449ce72c537b587b534c7f93e459d97898faea8a3a68622bbe01f2117fba4cfd3cff69f12e209d74cf87c"
],
"hash": "a314b20a66ab94c735f0f82c47ea679869980eb98f0d937a27531f328374119c",
"output": [
{
"amount": 999999,
"hash": "513c7eb598f25efb6201f5f2df66842fc92a3890b6927d1b5563ab88ef87eeba",
"addr": "955c823ea45e97e128bd2c64d139b3625afb3b19c37da9972548f3d28ed584b24f5ea49a17ecbe60e9a0a717b834b131",
"id": "467e55e7-95a9-4551-b2f8-2d1321468fd4"
},
{
"amount": 1,
"hash": "748ac974d0cc1dbff6a19778e4e7c145e3cd569b26a872132ff7ca4ccab067fb",
"addr": "b2b69bf382659fd193d40f3905eda4cb91a2af16d719b6f9b74b3a20ad7a19e4de41e5b7e78c8efd60a32f9701a13985",
"id": "42155d27-4934-49d6-acc4-4a299cebe63f"
}
]
}
],
"height": 1 //这个由系统生成,我们不用管
}




  按照流程,我们应该构造一个转钱给商店的区块。但通过代码,我们可以发现转账的时候是需要私钥签名的,也就是这个signature段。




  做题的时候也卡着这,想着是不是能拿到银行的私钥。但通过看writeup发现,这些信息我们可以通过黑客留下的signature直接绕过,并且上一步的input也可以从黑客的区块中得到。所以我们就可以直接构造转账给商店的区块了,并且通过51%攻击使黑客转走的钱追回。

  下面直接放出完整的payload脚本,需要特别提醒的是要注意每个区块的prev

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
# -*- coding: utf-8 -*-
import json, uuid, hashlib
import random,string

EMPTY_HASH = '0' * 64
DIFFICULTY = int('00000' + 'f' * 59, 16)

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

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

# 对 output 进行hash
def hash_utxo(utxo):
return reduce(hash_reducer, [utxo['id'], utxo['addr'], str(utxo['amount'])])

def create_output_utxo(addr_to, amount):
utxo = {'id': str(uuid.uuid4()), 'addr': addr_to, 'amount': amount}
utxo['hash'] = str(hash_utxo(utxo))
return utxo

# 对 transactions 进行hash
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)
])

#对整个块 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)])

prev = "5bc355ab21fd7e07040e2882f36ff8fba90809cbaa27b80bc1439a6e85beec25"
input = ["e95c5a89-3f0e-4bd6-a4bc-8ff006fa2a42"]
signature = ['8cf74260504449ce72c537b587b534c7f93e459d97898faea8a3a68622bbe01f2117fba4cfd3cff69f12e209d74cf87c']


address = 'b81ff6d961082076f3801190a731958aec88053e8191258b0ad9399eeecd8306924d2d2a047b5ec1ed8332bf7a53e735'
output = [create_output_utxo(address,1000000)]

transactions = {
"input":input,
"signature":signature,
"output":output
}

# 对 transactions 进行签名
hash_transactions = hash_tx(transactions)
transactions['hash'] = str(hash_transactions)
# 爆破(挖矿,找到满足条件的hash)
def fuzz(block, size=20):
CHARS = string.letters + string.digits
while True:
rnds = ''.join(random.choice(CHARS) for _ in range(size))
block['nonce'] = rnds
block_hash = str(hash_block(block))
# 转换成 16 进制
tmp_hash = int(block_hash, 16)
# POW 验证工作
if tmp_hash < DIFFICULTY:
block['hash'] = block_hash
return block

# 创建符合条件的块
block = {
"prev":prev,
"transactions":[transactions]
}
ok_block = fuzz(block)
print(json.dumps(ok_block))
# 创建一个空块
empty_tmp = {
"prev" : ok_block['hash'],
"transactions" : []
}
empty_block1 = fuzz(empty_tmp)
print(json.dumps(empty_block1))

empty_tmp = {
"prev" : empty_block1['hash'],
"transactions" : []
}
empty_block2 = fuzz(empty_tmp)
print(json.dumps(empty_block2))

empty_tmp = {
"prev" : empty_block2['hash'],
"transactions" : []
}
empty_block3 = fuzz(empty_tmp)
print(json.dumps(empty_block3))

empty_tmp = {
"prev" : empty_block3['hash'],
"transactions" : []
}
empty_block4 = fuzz(empty_tmp)
print(json.dumps(empty_block4))

  运行后会得到5个区块,然后依次post就可以得到flag。




  post第三块的时候会得到一个钻石。




  post第5块的时候会得到第二个钻石。




  然后访问/flag,从而得到flag。




  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

评论

Your browser is out-of-date!

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

×