Nebulas – Using WebAssembly To Bypass Gas Counter

By art_of_bug | art_of_bug | 21 Jun 2020


Welcome to our next episode. Today we open Nebulas. Similarly to IOST, this blockchain project uses Google's V8 JavaScript engine in order to allow smart contracts to be written in JavaScript.

Speaking of IOST, after the initial disappointment due to impossibility of contacting their team, we have been contacted back after we published our first finding in IOST. Since then we have been cooperating with IOST in order to improve the security of their product.

The issue we have presented previously for IOST have initially been fixed improperly (please let us know in the comments if you can see why this was broken), but after we explained the problem with this fix to IOST team, it has been fixed properly. We will cover more of IOST in our future posts.

Now back to Nebulas. Sadly again, it took us enormous effort to get response from the team. We have tried to contact them through 6 different channels for almost 4 weeks. After that we have received very short answer that Nebulas is not interested in talking to us and that the only way to proceed is to submit our findings to their very poorly designed bug bounty program via an online Google form and wait. This obviously is not an option for us, therefore we have a bunch of critical findings for Nebulas that are now available. Today we present one of these findings and we may publish more of them in the future. Please contact us if you're interested in them.

Today's vulnerability has not been fixed yet, hence it does not need an additional proof of knowledge.

Using WebAssembly To Paralyze Network

Bug type: DoS
Bug severity:
6/10

Scenario 1

Attacker cost: low

In this scenario, the goal of the attacker is to disrupt the normal operation of the network such that no normal transactions are processed. As this happens, the primary purpose of the network is violated. In order to do that, the attacker creates special transactions and propagates them to the network. When miners attempt to put these transactions to new blocks, they will fail and only create empty blocks. Next time the miners attempt to create new blocks the situation repeats and the miners again create just empty blocks.

Description

The codebase state at the time of writing can be seen here.

Nebulas implements smart contract functionality leveraging V8 JavaScript engine from Google. This allows users of Nebulas to deploy smart contracts written in JavaScript. However, V8 by itself is not suitable for consensus critical operation. This is why Nebulas implemented several restrictions on the top of V8. One of the common things that have to be prevented is an infinite loop execution. For this, Nebulas implements a gas counter, which is injected into the source code of every contact, and which is responsible for halting the execution once the contract call goes out of gas. Another countermeasure is a time restriction of V8 execution. When the timeout is reached, V8 execution of the contract is stopped.

One problem with Nebulas is that the used V8 engine supports WebAssembly, which can be invoked from the JavaScript contract. When this happens, the gas counter is only injected into the JavaScript code, but it is not injected to the WebAssembly code. Therefore, it is possible to create a contract, whose gas usage will only account for the JavaScript part but not the WebAssembly part.

When a block producer is creating a new block in Nebulas, there is 5 seconds timeout for the construction of the entire block. Actually, the timeout can be anything between 2 to 5 seconds, but it is safe for us to only consider the 5 seconds case because that is the most common case and attacks against the 5 seconds value also work against lower values but not vice versa. During the construction of a new block, the block producer is trying to include transactions from the mempool to the block. Every such transaction must be executed and this execution is limited by 15 seconds timeout.

Therefore, if the attacker wants to make a heavy computation without being charged for the used gas, she must make sure that the computation is finished within 5 seconds, otherwise the block production deadline will trigger and the transaction won't be included into the block.

If a transaction processing takes more than 15 seconds, the transaction is discarded completely. However, if the transaction takes less than 15 seconds but more than the block production timeout – usually 5 seconds, the transaction is put back into the mempool. Importantly, the order of obtaining transactions from the mempool is predictable. Specifically, the miner first takes transactions with higher gas price.

Malicious Contact Code

Let's consider the following contract code:

"use strict";

class BaseContract {

    constructor() {
        this.__contractName = "doomish";
    }

    init() {
    }

    doom() {
        var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,138,128,128,128,0,2,96,0,1,127,96,1,127,1,127,3,132,128,128,128,0,3,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,170,128,128,128,0,4,6,109,101,109,111,114,121,2,0,11,103,101,116,95,114,97,110,100,95,101,120,0,0,7,103,101,116,67,104,97,114,0,1,5,104,101,108,108,111,0,2,10,211,129,128,128,0,3,169,128,128,128,0,1,1,127,65,0,65,0,40,2,12,65,235,139,2,108,65,141,129,2,106,65,3,118,65,255,255,3,113,34,0,54,2,12,32,0,65,229,250,0,112,11,138,128,128,128,0,0,32,0,65,16,106,45,0,0,11,144,129,128,128,0,1,3,127,65,0,40,2,12,33,1,65,128,214,194,140,4,33,2,3,64,32,1,65,235,139,2,108,65,141,129,2,106,65,3,118,65,255,255,3,113,34,1,65,229,250,0,112,65,16,106,32,1,65,235,139,2,108,65,141,129,2,106,65,3,118,65,255,255,3,113,34,1,65,229,250,0,112,58,0,0,32,2,65,127,106,34,2,13,0,11,65,0,32,1,54,2,12,65,155,133,127,33,1,65,0,33,2,3,64,32,2,32,1,65,245,250,0,106,45,0,0,65,0,71,106,33,2,32,1,65,1,106,34,0,33,1,32,0,13,0,11,32,2,11,11,138,128,128,128,0,1,0,65,12,11,4,135,53,8,0]);
        var m = new WebAssembly.Instance(new WebAssembly.Module(wasmCode));
        var p = m.exports.hello();
        return "result:" + p;
    }
}

module.exports = BaseContract

As you can see, the WASM code is written in a binary array. Its original source code in C follows:

#define M 15717
#define C 1100000000

long get_rand_ex()
{
    static long holdrand = 537991;
    holdrand = ((holdrand * 34283 + 32909) >> 3) & 0xFFFF;
    return holdrand % M;
}

static unsigned char buf[M];

unsigned char getChar(int i)
{
    return buf[i];
}

int hello()
{    
    int a = 0;
    for (int i = 0; i < C; i++)
    {
        long b = get_rand_ex();
       buf[b] = (unsigned char)get_rand_ex();
    }
    
    for (int i = 0; i < M; i++)
    {
        if (buf[i] != 0) a++;
    }

    return a;
}

The WASM code is implemented in order to just consume CPU. In our hardware configuration, calling the doom() method took about 9 seconds. This is just between the required 5 and 15 seconds margins that we have, but in no way the attacker needs to have a single transaction that works for every node hardware configuration. The attacker can easily create a series of transactions like this with different constants causing the CPU consumption to be different for every transaction. With such a series, the attacker can guarantee that no miner in the network will be able to process all of the malicious transactions – i.e. at least one transaction will take between 5 and 15 seconds for each miner.

Getting Network Stuck

In order to cause the network to be stuck, the attacker just needs to set extremely high gas price for her malicious transactions. When the gas price is so high, no normal transaction will be executed unless it has at least the same gas price. But unlike the regular users, setting extremely high gas price is essentially free for the attacker. This is because the malicious transactions will never be included in blocks and therefore they will never be paid for (note that this holds even if the attacker uses a series of different transactions as mentioned above). This is why the attacker can set the gas price to any value of her choice without being subject to economic loss. Hence the attacker can set the gas price to the maximal allowed value of 1,000,000,000,000.

Initially, we have classified this vulnerability as 7/10, but we have re-evaluated that to 6/10 after we calculated that even with the maximal allowed gas price, the actual cost for user, who uses it for its own transaction in order to be on par with malicious transactions, is very low. We argue, however, that the vulnerability is still critical as per 6/10 severity classification. This is because although users may set the maximum gas price for their transactions, they do not have any reason to do so if they are not fully aware of this attack and its details when it's being exploited. From the user's point of view, the user tries to send their transaction with a normal fee and then the user finds out that the transaction is not confirmed. Even if the user examines the block explorer, they will only find empty blocks. There is no indication that the user should increase the fee. And even in case the user attempts to increase the fee of their transaction, it's unlikely that they set it to the maximal allowed gas price value.

And even if users are fully aware of the fact that only the maximal gas price transactions can go through, the attacker can make countermeasures to prevent users transactions to be processed in reasonable time. For example, the attacker can create a large number of transactions using different accounts and fill the mempool with many malicious transactions with the maximal gas price. Remember that the attacker never pays for these transactions, so even large number of transactions can be created for almost no cost. When a new transaction is added to the mempool, a binary search algorithm is performed to find a place in the queue of transactions. So in case that all of the transactions in the mempool have the same maximal allowed gas price, the user's transaction can only skip half of the mempool because it will be inserted in the middle of the mempool's queue. For example, if the attacker creates 10,000 malicious transactions, the user's transaction will be inserted such that it will be processed only after 5,000 malicious transactions are processed. This means the user will have to wait until 5,000 empty blocks are created. And even then the attacker can react and add even more transactions and depending on the actual number of transactions in the queue it may happen that the user's transaction is pushed further down the queue.

This is why in practice we consider this vulnerability almost as severe as if there was no way for users to get their transactions in blocks until the problem is fixed and the fix deployed.

How do you rate this article?

53


art_of_bug
art_of_bug

We are research group with focus to expose bugs in design and implementation of blockchain projects. We only honour responsible disclosure with projects that honour responsible development.


art_of_bug
art_of_bug

We are research group with focus to expose bugs in design and implementation of blockchain projects. We only honour responsible disclosure with projects that honour responsible development.

Send a $0.01 microtip in crypto to the author, and earn yourself as you read!

20% to author / 80% to me.
We pay the tips from our rewards pool.