文章目录
  1. mini blockchain
    1. 双花攻击
    2. 为何可以直接append?
    3. 如果可以任意append,为什么不直接给shop转200w?
    4. 51% attack和算力有什么关系?

DDCTF2018 mini blockchain writeup

mini blockchain

居然出了一个区块链题目也是非常的厉害,新知识学起来。对区块链不是很懂,可能下面的描述存在误差,如果有问题还请大神指出。

DD自己的商店用的是“区块链”货币,并不是真正的区块链,因为链只保存在服务器的session中,不过这不影响我们做题。

来看一张交易模型img

关于UTXO的详解看http://8btc.com/article-4381-1.html

区块链就是区块和链。在这道题目中,看上面的图片,一个方框就是一个区块(block)。一个区块包含了一次交易(tx),一个交易包含了交易的输入(input)和交易的输出(output),其中输出又称为UTXO。每一次交易的输入和输出必须相同,如果输入10块钱花了2块给便利店,那么2块输出给便利店,8块输出给自己。也可以把UTXO理解为余额,因为每次能花多少,都是要取决于之前的UTXO向自己输出了多少。

这道题目的背景是银行在某天发行了100w个DDB(对应上图第一个方块),这时黑客出现了,他在第一个block后面append了一个区块,把银行的99w9999转给了自己,把1转给银行。这样银行就只剩下一块钱了,黑客还得意的喊:second_block = create_block(genesis_block['hash'], 'HAHA, I AM THE BANK NOW!', [transferred]) 题目的要求是获得两个钻石,获得钻石的方法是商店有100w元。一个钻石的价格是100w,就是说我们得有200w才可以得到两个钻石,可银行只发行了100w,该怎么办?

双花攻击

双花攻击是同一笔UTXO在不同交易中的花费,双花不会产生新的货币,只能把自己花出去的钱重新拿回来。

这个攻击方法给了我灵感,实际上这道题就是使用双花攻击中的51% attack。51% attack指的是攻击者如果可以拥有超过全网50%的算力,就可以创造一条高度大于原始链的链,攻击者可以发送一个新的块到这条链上。(如果有对比特币进行51% attack成功的案例,最大的危害在于人们对比特币的信心受损导致的比特币大跌而不是51% attack本身带来的危害)

如何进行51% attack攻击?在这道题中,就是创造一条超过原始链的长度。为了在后续讲解中方便,先写出题目给出的几个块,主链上块前有*

*块1(创世区块):银行发行100w币
*块2(1):黑客转走99w9999,银行留1
*块3(1):空块(什么都没操作)

具体操作就是从块1之后append一个块,把银行的100w转到shop中

*块1(创世区块):银行发行100w币
*块2(1):黑客转走99w9999,银行留1 块2(2)–shop转走100w
*块3(1):空块(什么都没操作)

(还可以随意转钱?就是有这种操作23333)

下一步,在自己append的块后append一个空块

*块1(创世区块):银行发行100w币
*块2(1):黑客转走99w9999,银行留1 块2(2)–shop转走100w
*块3(1):空块(什么都不发生) 块3(2)–空块(什么都不发生)

再来一次同样的操作

*块1(创世区块):银行发行100w币
块2(1):黑客转走99w9999,银行留1 *块2(2)–shop转走100w
块3(1):空块(什么都没操作) *块3(2)–空块(什么都不发生)
*块4(1)–空块(什么都不发生)

此时最长的链为块1-块2(2)-块3(1)-块4(1)。这样,我们就构造了一个比题目给我们还要长的链,区块链这套逻辑会把最长的链当做主链,主链从块2(2)处分叉,块2(1)失效了,shop账户中多了100w,我们获得一个钻石。接下来系统在购买钻石的块3(2)块后添加一个块,转走商店中的100w到商店钱包。

*块1(创世区块):银行发行100w币
块2(1):黑客转走99w9999,银行留1 *块2(2)–shop转走100w
*块3(2)–空块(什么都不发生)
*块4(1)–空块(什么都不发生)
*块5(1)–把100w转到shop_wallet_address

那么另一个钻石该怎么获得呢?继续利用50% attack攻击,从块4(1)分叉,添加空块

*块1(创世区块):银行发行100w币
块2(1):黑客转走99w9999,银行留1 *块2(2)–shop转走100w
*块3(2)–空块(什么都不发生)
*块4(1)–空块(什么都不发生)
*块5(1)–把100w转到shop_wallet_address 块5(2)–空块(什么都不发生)

再append一个空块

*块1(创世区块):银行发行100w币
块2(1):黑客转走99w9999,银行留1 *块2(2)–shop转走100w
*块3(2)–空块(什么都不发生)
*块4(1)–空块(什么都不发生)
块5(1)–把100w转到shop_wallet_address *块5(2)–空块(什么都不发生)
*块6(1)–空块(什么都不发生)

主链变为块1-块2(2)-块3(1)-块4(1)-块5(2)-块6(1),块5(1)失效,shop拥有100w,钻石+1,得到flag。

为何可以直接append?

在这道题目中,给了一个append块的方法,可以将post请求当做块append到某个块后面,这个是一个正常的功能。在生成sign的时候没有将使用签名的交易hash计算进去,导致在验证的时候没有验证sign和交易hash的对应,所以只要有一个sign,就可以不断的利用这个sign append区块。

如果可以任意append,为什么不直接给shop转200w?

首先,所有的append都必须在创世block后。其次,系统会验证append块中的sign。还会验证prev值,是否为某个已存在的block的hash。(block的hash是将block的每个参数打包后进行hash)无法知道某个block的hash就无法在block后append一个block。最后,转出的钱,必须是之前的UTXO,题目中UTXO总量为100w,无法创造200w的UTXO。

51% attack和算力有什么关系?

append的块除了以上要求,还有一个复杂性要求。也就是工作量证明(https://baike.baidu.com/item/%E5%B7%A5%E4%BD%9C%E9%87%8F%E8%AF%81%E6%98%8E/22448498?fr=aladdin)。任意添加一个块的要求是

1
2
3
DIFFICULTY = int('00000' + 'f' * 59, 16)
......
if block_hash > difficulty: raise Exception('Please provide a valid Proof-of-Work')

block的hash要小于系统定义的difficulty。为了使得可以控制hash的大小,一个block中还有一个可以随意定义的nonce,我们可以控制nonce来控制block_hash达到目的。为了满足复杂度要求,必须穷举nonce。init()中的几个块可以使用有语义的nonce是因为在那个阶段DIFFICULTY要求极低。

如果世界上有100个用户在使用这个系统,100个用户都在计算nonce以append自己的block。如果其中一个人计算nonce的速度要超过其余99个的速度,那么他添加新块的速度就会超过其他99个人添加新块的速度,他就可以在随意的一个块开始添加自己的块,使得自己构造的链长度大于其余99个构造的链,成为主链,达成51%攻击。这道题没人和我们比算力,生成一个比最长链长度大一的链即可。

一个block结构是怎样的?块2(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
'nonce': 'HAHA, I AM THE BANK NOW!',
'prev': 'dd04faf20c550cf63ae07504884e1fb673cfefaaac2979dde1ae3cbf95961569',
'hash': '5217b7fa9c1e2296e66202997df0a51b20e58fe921011069535a62cd53518e55',
'transactions': [{
'input': ['9d65e5db-8671-4323-b279-af56963f2565'],
'output': [{
'amount': 999999,
'hash':
'da32c8155ebbec8df888653d4d243698e29c4ea43cc0fa1bff14649e8511416b',
'id': '9dcb9e47-5816-4451-b99e-eb6d729f64b7',
'addr': 'b2a6484625db7305ea7bb1c8a484832ec32686c0f3a3dac5cfe63779ede94494d841f8117fe1dd55c57e23aa61953391'
},
{
'amount': 1,
'hash': '19fa5198bc172d6525976b7f0fb5f0647b96ab6b55bd4eb9033ab158faebb0ad',
'id': '592e27c6-b111-40a7-8b2d-ccefa333e616',
'addr': '99a13a3a21051c8f93c5a87f7f92151b4acfaf01f2e596696e8922e3801278470592cdbc8920f289a1829f726c43a1e9'
}],

'hash': '5815cc2ccf6327396ce5490c39e7c6381f15250fa0ab043eae8096d1a1c44704',
'signature': ['9455298609f042b631f99cb33f3f683f6b3361962df5f1c6f698e03b23d72c7ea42c939999913424e4c424f6b7024514']
}
]}

参数解释,括号内为生成函数

nonce:自定义字符串

prev:上一个块的hash

hash:本个块的hash(hashhash,hash_reducer,hash_block)

transactions:交易(tx)

​ input:之前utxo的id

​ output:UTXO

​ amount:数量

​ hash:UTXO的hash(hash,hash_reducer,hash_utxo)

​ id:这个UTXO的id

​ addr:目标地址

​ hash:交易的hash(hash,hash_reducer,hash_tx)

​ signature:交易签名(sign_input_utxo)

其余的函数:

addr_to_pubkey:检查地址有效性

pubkey_to_address:生成钱包地址

gen_addr_key_pair:生成钱包地址

create_output_utxo:创建一个utxo

create_tx:创建一个tx

create_block:创建一个block

find_blockchain_tail:查询最后一个block

calculate_utxo:得到所有utxo

calculate_balance:计算钱包的余额

verify_utxo_signature:验证utxo签名

append_block:添加块

init:初始化函数

get_balance_of_all:得到所有block,所有地址和utxo

homepage:web主页

getFlag:flag获取页面

EXP: 重命名源代码为btc.py

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
# -*- encoding: utf-8 -*-

import btc, rsa, uuid, json, copy
#创世块的hash
genies_hash = "92875ca628cd0890020f6a74f3011b611db814f30300f729f20b5a88c49e3e44"
#黑客转账999999,所用的input和签名
input,signature = ("9018b356-cb1d-44c9-ab4e-bf15a8b2f95c","161ae7eac89f71d50d1019d21288dce23cae6cbb587998df9010e3ff3c80ee8e4c06bd70555604be85ca0869136b3966")
#商店地址
shop_address = "b81ff6d961082076f3801190a731958aec88053e8191258b0ad9399eeecd8306924d2d2a047b5ec1ed8332bf7a53e735"
txout_id = str(uuid.uuid4())

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

def myprint(b):
print(json.dumps(b))
print(len(json.dumps(b)))

#构造一个空块
def empty_block(msg, prevHash):
b={}
b["prev"] = prevHash
b["transactions"] = []
b = pow(b, btc.DIFFICULTY, msg)
return b

#从创世块开始分叉,给商店转1000000
block1 = {}
block1["prev"] = genies_hash
tx = {"input":[input],"output":[{"amount":1000000, 'id':txout_id,'addr':shop_address}],'signature':[signature]}
tx["output"][0]["hash"] = btc.hash_utxo(tx["output"][0])
tx['hash'] = btc.hash_tx(tx)
block1["transactions"] = [tx]
block1 = pow(block1, btc.DIFFICULTY)
myprint(block1)

#构造空块增加分叉链长度,使分叉链最长,因为max的结果不唯一,少则一次多则两次
block2 = empty_block("myempty1", block1["hash"])
myprint(block2)
block3 = empty_block("myempty2", block2["hash"])
myprint(block3)

#余额更新成功,系统自动添加块,转走商店钱,钻石+1

#从自己的块,即系统转走钱之前的那个块再次分叉,添加空块
block4 = empty_block("myempty3", block3["hash"])
myprint(block4)
block5 = empty_block("myempty4", block4["hash"])
myprint(block5)
#新的分叉链最长,余额更新成功,钻石+1

生成出的四个块按顺序提交,再访问/flag就可以得到flag

最后感谢ACM大佬@M3r0dach师傅的指导,在做这道题的过程中给予了很多帮助。大佬的github:https://github.com/M3r0dach/gobtc

支持一下
扫一扫,支持forsigner