[INSHack2017]boulicoin 題解

涉及的密碼學原理很簡單,但是題目看了很久纔看懂

題目代碼分析

題目要求我們挖一個boulicoin出來

實際上boulicoin就是一個flag

server.py

from flask import Flask
from flask import jsonify
from flask import request
from Crypto.Cipher import AES
import base64
import boulicoin
import json

app = Flask(__name__)

def pad(data):
    length = 16 - (len(data) % 16)
    data += chr(length)*length
    return data

def unpad(data):
    assert data[-ord(data[-1]):]==ord(data[-1])*data[-1]
    return data[:-ord(data[-1])]

def encrypt(data):
    aes=AES.new(boulicoin.SECRET_KEY, AES.MODE_CBC, boulicoin.AES_IV)
    return base64.b64encode(aes.encrypt(pad(str(data))))

def decrypt(data):
    aes=AES.new(boulicoin.SECRET_KEY, AES.MODE_CBC, boulicoin.AES_IV)
    return unpad(aes.decrypt(base64.b64decode(data)))

''' AES-CBC,密鑰和IV值未知,Padding方式爲PKCS5 '''
''' 注意這裏可以看出密鑰和IV值是永遠不變的。因爲是用的boulicoin這個包裏的變量,沒有對他進行過操作,因此值肯定是固定的'''

#若請求訪問url: host/task,則執行getTask()
@app.route('/task')
def getTask():
    '''
    要求客戶端(我們)解決完全揹包問題:容量爲size的揹包,裝len(items)種物品,第i種物品價值爲item[i][0],物品重量爲item[i][1]。   
	向請求方發送 items、size、scoreMin的明文和簽名,簽名算法使用前面的AES '''
    items,size,scoreMin = boulicoin.generateTask()
    itemsEnc = encrypt(items)
    sizeEnc = encrypt(size)
    scoreMinEnc = encrypt(scoreMin)
    return jsonify({'items':items,
                    'size':size,
                    'scoreMin':scoreMin,
                    'itemsEnc':itemsEnc,
                    'sizeEnc':sizeEnc,
                    'scoreMinEnc':scoreMinEnc})


#若使用POST方法請求url: host/coin 則執行getCoin()
@app.route('/coin', methods=['POST'])
def getCoin():
    '''
    驗證請求方是否解決了揹包問題,並驗證明文及其簽名是否對應,
    以防止我們自己僞造items size scoreMin這些參數。
    '''
    req=request.json
    if ('itemsEnc' not in req) or ('sizeEnc' not in req) or ('scoreMinEnc' not in req) or ('selected' not in req):
        return jsonify({'status':'error','message':'JSON attributes missing'})
    items=[]
    size=-1
    try:
        items=json.loads(decrypt(req['itemsEnc']))
        size=int(decrypt(req['sizeEnc']))
        scoreMin=int(decrypt(req['scoreMinEnc']))
        selected=req['selected']
        assert type(selected)==type([])
        assert len(set(selected))==len(selected)
    except:
        return jsonify({'status':'error','message':'Failed to decode data.'})

    score=0
    weight=0
    try:
        for it in selected:
            if it<0 or it>=len(items):
                return jsonify({'status':'error','message':'Invalid item index.'})
            score+=items[it][0]      # score = ∑items[i][0]
            weight+=items[it][1]     # weight = ∑items[i][1]
    except:
        return jsonify({'status':'error','message':'Invalid proof of work.'})

    # 要求score >= scoreMin   weight <= size
    if score>=scoreMin and weight<=size:
        coin=boulicoin.generateCoin()
        assert coin.startswith('INSA{')
        return jsonify({'status':'success','boulicoin':coin})
    return jsonify({'status':'error','message':'Invalid proof of work.'})
    
    
if __name__ == '__main__':
    app.run(debug=True)

client.py

import requests
import random
import sys


def solve(items, scoreMin, size):
    """
    求解服務器發給我們的揹包問題
    """
    bestScore = -1
    while True:
        selected = []
        score = 0
        occupiedSize = 0
        #  瞎蒙法? 不是很理解
        for i in range(len(items)):
            if random.getrandbits(1):
                selected.append(i)
        for item in selected:
            score += items[item][0]
            occupiedSize += items[item][1]

        if occupiedSize <= size and score > bestScore:
            bestScore = score
            print "%s/%s" % (score, scoreMin)
            if score >= scoreMin:
                return selected


def work(host):

    req = requests.get('https://' + host + '/task').json()
    selected = solve(req['items'], req['scoreMin'], req['size'])
    req['selected'] = selected
    coin = requests.post('https://' + host + '/coin', json=req).json()
    if coin['status'] != 'success':
        print 'Something wrong happened'
        sys.exit(1)
    return coin['boulicoin']


if __name__ == '__main__':
    if len(sys.argv) != 2:
        print 'Usage: python2 client.py host'
        sys.exit(1)
    wallet = []
    coin = work(sys.argv[1])
    wallet.append(coin)
    print 'New coin added : ' + coin

如果能挖出來,用wireshark一看就能獲得flag。當然服務器肯定不會給我們一個好求的揹包問題,讓我們直接用client.py就能跑出來。(其實根本就是無解的,而不是不好求)那麼我們考慮服務器端存在的問題

漏洞分析

參考以下writeup
https://github.com/HugoDelval/inshack-2017/blob/master/challenges/crypto/boulicoin-225/writeup.md

首先經過測試發現,每一次給你的完全揹包問題(以下簡稱task)都是不一樣的。也就是每次規定的 items、size、scoreMin都是不一樣的,我們能獲得它們和它們的簽名。其中 items 是物品的重量和價值,size 是揹包的容量,scoreMin 是我們揹包裏要達到的價值。

首先你並不能自己製造 size 等參數,因爲服務器使用 m||Ek(m) 來防止你僞造 。但注意到 m || Ek(m) 每次的k都一樣。那麼雖然我們不能隨便自己造一個參數,但我們可以做一個重放攻擊(其實這個Ek(m)可以認爲是一個對稱密碼實現的簽名)

那麼重放有什麼用?由於server對於client發回來的數據,僅僅是分別驗證task中的每一個參數是否僞造,而不驗證task 跟之前下發的task是否每個參數都相同,因此我們就可以從服務器的多個task中抽取一些我們想要的參數和MAC值重新構造出一個簡單的task來讓我們得以求解,再發回去。

比如現在服務器給你 task1(size1 = 400),task2(size2 = 1000)。 這兩個原問題哪個都不能求解,但是如果你把task1中的 size1 改成 size2,那這個問題就好求多了。

參考github上的exp

參(照)考(搬)以下腳本
https://github.com/HugoDelval/inshack-2017/blob/master/challenges/crypto/boulicoin-225/exploit/exploit.py

#!/usr/bin/python3

import requests

url = "http://52.82.121.166:28978"

def alive():
    return 'status' in requests.post(url + '/coin',json={}).json()

def exploitable():
    req=requests.get(url + '/task').json()
    # 對應的scoreMin很小,多跑幾次一定能跑出scoreMin和scoreMinEnc對應
    req['scoreMinEnc']='/eBOj8K3tGkk2olT/yGbRw=='
    # 直接選0,1。沒想到這都行 
    req['selected']=[0,1] 
    return requests.post(url + '/coin',json=req).json()['boulicoin'].startswith('flag{')

if __name__ == "__main__":
	print(alive() and exploitable())
	#用wireshark看http,輸出True之後就能在包裏找到flag。

題目涉及的其他知識

Flask相關內容

內容可能不嚴謹,僅個人理解。有錯請指出

Flask是一個web應用框架,以一個應用爲例:

# hello.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

其中app = Flask(__name__) 是相當於開一個容器,跑一個web應用。這裏 傳參__name__ 是因爲這個應用只有這一個模塊(文件)

下面這段話摘自:https://dormousehole.readthedocs.io/en/latest/quickstart.html#id2

如果你使用 一個單一模塊(就像本例),那麼應當使用 name ,因爲名稱會根據這個 模塊是按應用方式使用還是作爲一個模塊導入而發生變化(可能是 ‘main’ , 也可能是實際導入的名稱)。

@app.route('/')表示 如果有請求訪問url ‘/’,那麼就執行下面這一段的邏輯。比如應用跑在 127.0.0.1:5000 ,訪問 http://127.0.0.1:5000/ 就會觸發hello_world()的執行

def hello_world():
    return 'Hello, World!'

json相關內容

大概就是應用層解析器,把發過來的包的應用層(字節流)進行解析,方便得到其中的那些字段。具體的我並不太懂。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章