p0's blog | 破 关注网络安全
DDCTF一个关于区块链的题目
发表于: | 分类: 技术分享,CTF | 评论:0 | 阅读: 438

前段时间也在看区块链的相关资料,但是没有代码实例,正好这次DDCTF的一个题目,非常好的一个代码实例。由于本人对区块链了解甚少,下面用词不当的地方还请指出

0x00 题目代码

# -*- encoding: utf-8 -*-
# written in python 2.7

import hashlib, json, rsa, uuid
from flask import Flask, session, redirect, url_for, escape, request

app = Flask(__name__)
app.secret_key = '*********************'
url_prefix = '/b942f830cf97e'

def FLAG():
    return 'Here is your flag: DDCTF{******************}'

def hash(x):
    return hashlib.sha256(hashlib.md5(x).digest()).hexdigest()
    
def hash_reducer(x, y):
    return hash(hash(x)+hash(y))
    
def has_attrs(d, attrs):
    if type(d) != type({}): raise Exception("Input should be a dict/JSON")
    for attr in attrs:
        if attr not in d:
            raise Exception("{} should be presented in the input".format(attr))

EMPTY_HASH = '0'*64

def addr_to_pubkey(address):
    return rsa.PublicKey(int(address, 16), 65537)
    
def pubkey_to_address(pubkey):
    assert pubkey.e == 65537
    hexed = hex(pubkey.n)
    if hexed.endswith('L'): hexed = hexed[:-1]
    if hexed.startswith('0x'): hexed = hexed[2:]
    return hexed
    
def gen_addr_key_pair():
    pubkey, privkey = rsa.newkeys(384)
    return pubkey_to_address(pubkey), privkey

bank_address, bank_privkey = gen_addr_key_pair()
hacker_address, hacker_privkey = gen_addr_key_pair()
shop_address, shop_privkey = gen_addr_key_pair()
shop_wallet_address, shop_wallet_privkey = gen_addr_key_pair()

def sign_input_utxo(input_utxo_id, privkey):
    return rsa.sign(input_utxo_id, privkey, 'SHA-1').encode('hex')
    
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'] = hash_utxo(utxo)
    return utxo
    
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 create_tx(input_utxo_ids, output_utxo, privkey_from=None):
    tx = {'input': input_utxo_ids, 'signature': [sign_input_utxo(id, privkey_from) for id in input_utxo_ids], 'output': output_utxo}
    tx['hash'] = hash_tx(tx)
    return tx
    
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 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
    
def find_blockchain_tail():
    return max(session['blocks'].values(), key=lambda block: block['height'])
    
def calculate_utxo(blockchain_tail):
    curr_block = blockchain_tail
    blockchain = [curr_block]
    while curr_block['hash'] != session['genesis_block_hash']:
        curr_block = session['blocks'][curr_block['prev']]
        blockchain.append(curr_block)
    blockchain = blockchain[::-1]
    utxos = {}
    for block in blockchain:
        for tx in block['transactions']:
            for input_utxo_id in tx['input']:
                del utxos[input_utxo_id]
            for utxo in tx['output']:
                utxos[utxo['id']] = utxo
    return utxos
        
def calculate_balance(utxos):
    balance = {bank_address: 0, hacker_address: 0, shop_address: 0}
    for utxo in utxos.values():
        if utxo['addr'] not in balance:
            balance[utxo['addr']] = 0
        balance[utxo['addr']] += utxo['amount']
    return balance

def verify_utxo_signature(address, utxo_id, signature):
    try:
        return rsa.verify(utxo_id, signature.decode('hex'), addr_to_pubkey(address))
    except:
        return False

def append_block(block, difficulty=int('f'*64, 16)):
    has_attrs(block, ['prev', 'nonce', 'transactions'])
    
    if type(block['prev']) == type(u''): block['prev'] = str(block['prev'])
    if type(block['nonce']) == type(u''): block['nonce'] = str(block['nonce'])
    if block['prev'] not in session['blocks']: raise Exception("unknown parent block")
    tail = session['blocks'][block['prev']]
    utxos = calculate_utxo(tail)
    
    if type(block['transactions']) != type([]): raise Exception('Please put a transaction array in the block')
    new_utxo_ids = set()
    for tx in block['transactions']:
        has_attrs(tx, ['input', 'output', 'signature'])
        
        for utxo in tx['output']:
            has_attrs(utxo, ['amount', 'addr', 'id'])
            if type(utxo['id']) == type(u''): utxo['id'] = str(utxo['id'])
            if type(utxo['addr']) == type(u''): utxo['addr'] = str(utxo['addr'])
            if type(utxo['id']) != type(''): raise Exception("unknown type of id of output utxo")
            if utxo['id'] in new_utxo_ids: raise Exception("output utxo of same id({}) already exists.".format(utxo['id']))
            new_utxo_ids.add(utxo['id'])
            if type(utxo['amount']) != type(1): raise Exception("unknown type of amount of output utxo")
            if utxo['amount'] <= 0: raise Exception("invalid amount of output utxo")
            if type(utxo['addr']) != type(''): raise Exception("unknown type of address of output utxo")
            try:
                addr_to_pubkey(utxo['addr'])
            except:
                raise Exception("invalid type of address({})".format(utxo['addr']))
            utxo['hash'] = hash_utxo(utxo)
        tot_output = sum([utxo['amount'] for utxo in tx['output']])
        
        if type(tx['input']) != type([]): raise Exception("type of input utxo ids in tx should be array")
        if type(tx['signature']) != type([]): raise Exception("type of input utxo signatures in tx should be array")
        if len(tx['input']) != len(tx['signature']): raise Exception("lengths of arrays of ids and signatures of input utxos should be the same")
        tot_input = 0
        tx['input'] = [str(i) if type(i) == type(u'') else i for i in tx['input']]
        tx['signature'] = [str(i) if type(i) == type(u'') else i for i in tx['signature']]
        for utxo_id, signature in zip(tx['input'], tx['signature']):
            if type(utxo_id) != type(''): raise Exception("unknown type of id of input utxo")
            if utxo_id not in utxos: raise Exception("invalid id of input utxo. Input utxo({}) does not exist or it has been consumed.".format(utxo_id))
            utxo = utxos[utxo_id]
            if type(signature) != type(''): raise Exception("unknown type of signature of input utxo")
            if not verify_utxo_signature(utxo['addr'], utxo_id, signature):
                raise Exception("Signature of input utxo is not valid. You are not the owner of this input utxo({})!".format(utxo_id))
            tot_input += utxo['amount']
            del utxos[utxo_id]
        if tot_output > tot_input:
            raise Exception("You don't have enough amount of DDCoins in the input utxo! {}/{}".format(tot_input, tot_output))
        tx['hash'] = hash_tx(tx)
    
    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['height'] = tail['height']+1
    if len(session['blocks']) > 50: raise Exception('The blockchain is too long. Use ./reset to reset the blockchain')
    if block['hash'] in session['blocks']: raise Exception('A same block is already in the blockchain')
    session['blocks'][block['hash']] = block
    session.modified = True
    
def init():
    if 'blocks' not in session:
        session['blocks'] = {}
        session['your_diamonds'] = 0
    
        # First, the bank issued some DDCoins ...
        total_currency_issued = create_output_utxo(bank_address, 1000000)
        genesis_transaction = create_tx([], [total_currency_issued]) # create DDCoins from nothing
        genesis_block = create_block(EMPTY_HASH, 'The Times 03/Jan/2009 Chancellor on brink of second bailout for bank', [genesis_transaction])
        session['genesis_block_hash'] = genesis_block['hash']
        genesis_block['height'] = 0
        session['blocks'][genesis_block['hash']] = genesis_block
        
        # Then, the bank was hacked by the hacker ...
        handout = create_output_utxo(hacker_address, 999999)
        reserved = create_output_utxo(bank_address, 1)
        transferred = create_tx([total_currency_issued['id']], [handout, reserved], bank_privkey)
        second_block = create_block(genesis_block['hash'], 'HAHA, I AM THE BANK NOW!', [transferred])
        append_block(second_block)
        
        # Can you buy 2 diamonds using all DDCoins?
        third_block = create_block(second_block['hash'], 'a empty block', [])
        append_block(third_block)
        
def get_balance_of_all():
    init()
    tail = find_blockchain_tail()
    utxos = calculate_utxo(tail)
    return calculate_balance(utxos), utxos, tail
    
@app.route(url_prefix+'/')
def homepage():
    balance, utxos, _ = get_balance_of_all()
    genesis_block_info = 'hash of genesis block: ' + session['genesis_block_hash']
    addr_info = 'the bank\'s addr: ' + bank_address + ', the hacker\'s addr: ' + hacker_address + ', the shop\'s addr: ' + shop_address
    balance_info = 'Balance of all addresses: ' + json.dumps(balance)
    utxo_info = 'All utxos: ' + json.dumps(utxos)
    blockchain_info = 'Blockchain Explorer: ' + json.dumps(session['blocks'])
    view_source_code_link = "<a href='source_code'>View source code</a>"
    return '<br /><br />\r\n\r\n'.join([view_source_code_link, genesis_block_info, addr_info, balance_info, utxo_info, blockchain_info])
    
    
@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
    
def find_enough_utxos(utxos, addr_from, amount):
    collected = []
    for utxo in utxos.values():
        if utxo['addr'] == addr_from:
            amount -= utxo['amount']
            collected.append(utxo['id'])
        if amount <= 0: return collected, -amount
    raise Exception('no enough DDCoins in ' + addr_from)
    
def transfer(utxos, addr_from, addr_to, amount, privkey):
    input_utxo_ids, the_change = find_enough_utxos(utxos, addr_from, amount)
    outputs = [create_output_utxo(addr_to, amount)]
    if the_change != 0:
        outputs.append(create_output_utxo(addr_from, the_change))
    return create_tx(input_utxo_ids, outputs, privkey)
    
@app.route(url_prefix+'/5ecr3t_free_D1diCoin_b@ckD00r/<string:address>')
def free_ddcoin(address):
    balance, utxos, tail = get_balance_of_all()
    if balance[bank_address] == 0: return 'The bank has no money now.'
    try:
        address = str(address)
        addr_to_pubkey(address) # to check if it is a valid address
        transferred = transfer(utxos, bank_address, address, balance[bank_address], bank_privkey)
        new_block = create_block(tail['hash'], 'b@cKd00R tr1993ReD', [transferred])
        append_block(new_block)
        return str(balance[bank_address]) + ' DDCoins are successfully sent to ' + address
    except Exception, e:
        return 'ERROR: ' + str(e)

DIFFICULTY = int('00000' + 'f' * 59, 16)
@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
    
        
# if you mess up the blockchain, use this to reset the blockchain.
@app.route(url_prefix+'/reset')
def reset_blockchain():
    if 'blocks' in session: del session['blocks']
    if 'genesis_block_hash' in session: del session['genesis_block_hash']
    return 'reset.'
    
@app.route(url_prefix+'/source_code')
def show_source_code():
    source = open('serve.py', 'r')
    html = ''
    for line in source:
        html += line.replace('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').replace('\n', '<br />')
    source.close()
    return html
    
if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

0x01 代码大体逻辑

看init()初始化操作,初始了三个区块。bank_address钱包默认有1000000币,第185-190直接转到hacker_address钱包999999币,bank_address仅剩一个币,然后创建了一个没有任何信息的空区块。这三个区块形成一个简单的链。

create_output_utxo()根据钱包地址和交易后的币做获取一个唯一的uid,然后做个hash(钱包每次交易该钱包id就会更新)。返回格式为:

{
    "amount": 1000000,
    "hash": "8610e45433e89caf747789bf513a03470b14f7ca0c18d440fb36003931976c9f",
    "addr": "92ef90cec7aa96a01ee1d1e8c2febd7714ba2fd4779074a88af16e75b3b6ff71fe3020de090b38bec1b33c638c92ab29",
    "id": "c9c198fe-1cf3-4f15-bc4c-5cc8ab6d35b2"
}

create_tx()参数为上个函数的返回值,使用被转走的币的钱包的当前uid,和私钥做个签名和hash。返回格式为:

{
    "input": ["fadfe8eb-6203-4b24-9864-4e6023021f75"],
    "output": [{
        "amount": 999999,
        "hash": "a44b965165b2284d01b57a0ace714e46c25ba05f7d65ff8ea69113d8e105387d",
        "id": "15946ffc-005c-4142-9c35-bb469175193c",
        "addr": "bc6f2cf398993b830a35a75eef04c0f2f5cfc65ed58cda5dea0b40a61e8c891d88ad14074be6c9aa66f16ec5f2ac2465"
    }, {
        "amount": 1,
        "hash": "9124aa4291d103cbf6debcfc493e8c33ef61f536dc0c953bb6145e11beeb81ed",
        "id": "b7036237-d959-455c-bc83-b94a50924d4f",
        "addr": "8dc10fd4d97ee2852509a4973ebcaef31e60028e8839a5120f0bcd37c2dda4acad01860d791e6f8fc14c218cc4ee048b"
    }],
    "hash": "201f35e63e35fe51e30c84fb905d7df31b3f2b555dd09caad04b87eacd0f2f05",
    "signature": ["410881ada35f95a07b15cc96d4b893e19045ba711753f381265e01b9161fbf74b6bfcfd3f241a87f3dcbafddc8e02154"]
}

create_block()创建区块,结合前两个函数可以理解为创建交易订单。返回格式为下面再说一下

append_block()拼接区块,可以理解为进行交易。

free_ddcoin()函数可以将bank_address区块的币转到任意区块,但是前面说了,bank_address的币全被转到hacker_address,这个函数基本没用,误导选手。

create_tx_and_check_shop_balance()函数可以新建一个区块,但是需要计算一个sha256使前五位为0,称为挖矿吧。然后区块新建成功后会判断shop_address钱包的币是否等于1000000,如果是则给一枚钻石,将币转到shop_wallet_address钱包。

现在要做的就是使shop_address包的币等于1000000.

0x02 区块json格式

{
    "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 //距离根区块的距离
}

0x03 实施攻击

创建区块的地方,任何人只要计算出来了符合要求的sha256签名都可以进行创建区块,即创建一个交易,但是币在hacker_address的钱包,并且不知道其私钥,不能将币转出来,但是可以直接在根区块下新建一个链,根据代码逻辑,数据是以链最长的哪个区块链为准的,所以只需创建一个大于原链的链,覆盖即可。

根据

{
    "nonce": "HAHA, I AM THE BANK NOW!",
    "height": 1,
    "prev": "aca684034b165c2fb9726b0bfdac19ca091ae2f0ae9da92a0f13366680639ea4",
    "hash": "247ddb6781955bb6340b9ae606d78cd9bb02e0e27505530dc16185d044a2de7b",
    "transactions": [{
        "input": ["fadfe8eb-6203-4b24-9864-4e6023021f75"],
        "output": [{
            "amount": 999999,
            "hash": "a44b965165b2284d01b57a0ace714e46c25ba05f7d65ff8ea69113d8e105387d",
            "id": "15946ffc-005c-4142-9c35-bb469175193c",
            "addr": "bc6f2cf398993b830a35a75eef04c0f2f5cfc65ed58cda5dea0b40a61e8c891d88ad14074be6c9aa66f16ec5f2ac2465"
        }, {
            "amount": 1,
            "hash": "9124aa4291d103cbf6debcfc493e8c33ef61f536dc0c953bb6145e11beeb81ed",
            "id": "b7036237-d959-455c-bc83-b94a50924d4f",
            "addr": "8dc10fd4d97ee2852509a4973ebcaef31e60028e8839a5120f0bcd37c2dda4acad01860d791e6f8fc14c218cc4ee048b"
        }],
        "hash": "201f35e63e35fe51e30c84fb905d7df31b3f2b555dd09caad04b87eacd0f2f05",
        "signature": ["410881ada35f95a07b15cc96d4b893e19045ba711753f381265e01b9161fbf74b6bfcfd3f241a87f3dcbafddc8e02154"]
    }]
}

这个区块的数据,可以获取到该交易的签名和根区块的id。所以伪造该交易,将币全部转到shop_address,创建一个区块:

便可创建一个区块

然后利用000007e7cdbbd5d7c734f0dfa52062e1ed409694fb0682d507591ef7a28555d2作为父区块,再创建一个区块。

既可以获得一颗钻石

获得第二个的方法一样,就是需要多构造一个区块。 期间遇到了一个坑,导致第二个钻石怎么也加不上,最后才发现cookie的长度与区块的长度有关,火狐有cookie长度限制。。放burpsuite就好了。


著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:p0
链接:http://p0sec.net/index.php/archives/119/
来源:http://p0sec.net/

添加新评论

TOP