Welcome to our first blog post. We hope you will enjoy our content. Today, we start with a vulnerability in Neblio project. We made several attempts to contact the Neblio team in April, but all our attempts failed. It seemed that they just refused to communicate with us while they were online and active in public channels. We interpret such hesitancy to communicate as lack of interest in the security of their product.
Our vulnerability reports will always include possible attack scenarios and optionally, a detailed description of the vulnerability itself. The description is not going to be provided if we agree with the vendor that it should not be disclosed.
Currently we are aware of several problems in Neblio and today we are only presenting the first one. As the Neblio team is unreachable, we are open to take offers for the second vulnerability, which attack scenario you can find below. But first, let's start with the disclosure of the first bug.
VerifyInputsUnspent Denial of Service
Bug type: DoS
Bug severity: 5/10
Attacker cost: negligible
In this scenario the attacker targets one or more specific nodes that the attacker wants to disable. The attacker needs a direct connection to the target node, so we expect the target node to be operating on publicly accessible network interface – i.e. public IP address and open firewall port. Eventually, the attacker is able to create connection to every public node in the network and disable all such nodes. The network will become dysfunctional.
Attacker cost: medium
In this scenario, the attacker buys nontrivial amount of coins and it also performs a sybil attack against the network – this means it will create a large number of fully operational public nodes. Then it will perform scenario one attack against all other public nodes in order to disable them. This will cause that all the block propagation in the network will only have to go through the public nodes of the attacker. This allows the attacker to censor any transaction as well as censor any block, which further allows the attacker to create a longest chain with just fractional stake, compared to the original staking power of the network. With just fraction of the network staking power, the difficulty will go down very quickly because the blocks will not be produced within the expected timeframe. Eventually the difficulty will decrease enough for the attacker to produce a chain that looks healthy and it is fully controlled by the attacker. This allows the attacker to perform additional attacks such as double spending.
ProcessMessage function in main.cpp processes incoming messages from the network. Importantly, ProcessMessage is called with cs_main locked. When a "block" message is received, ProcessBlock is called directly. This is done regardless of whether the block was previously announced or not.
Besides other things, this function performs validation of a block. Most of the validation is implemented inside of CheckBlock, which is called first, and AcceptBlock functions. Importantly, an attacker can construct a block that passes all checks inside CheckBlock and all other checks up to the point where AcceptBlock is called without having any stake in the network. So the only cost for an attacker to do that is their ability to connect to a node on the network, no coins are required.
Early in AcceptBlock, it calls VerifyInputsUnspent function, which is a new function implemented by Neblio in response to fake stake vulnerability. This vulnerability allowed an attacker to use already spent coins for creation of seemingly valid coinstake transaction and thus seemingly valid block. Such a block would only be found invalid if it was part of the best chain and was about to be connected. Final part of the validation in the block connection process would reveal that the used coins were already spent. However, the attacker could create such blocks on chain branches with lower total work, hence the connection process would never occur, but the node would store helper structures for the invalid blocks in the memory and on the disk, which would eventually cause the node to crash. In order to fix this denial of service vulnerability, Neblio implemented additional validation checks inside of VerifyInputsUnspent.
The design of this fix is flawed as it creates a new security hole which can be exploited even easier than the original vulnerability. In short, the idea of this fix is to rewind the blocks from the main chain in memory and to reconnect blocks of the branch on which the new block appears, also in memory, without changing the database. With this in-memory state, it becomes possible to check whether the used coins are still unspent.
However, nothing prevents an attacker to create a block which forks the main chain very early – for example within the first thousand of blocks. In construction of this block the attacker does not need to own any coins and it still can create a proof of stake block that would pass all the checks before or inside of VerifyInputsUnspent. Such a block would then cause VerifyInputsUnspent to load all blocks in the blockchain to the memory from the database. At the time of writing there are more than 750k blocks in the Neblio main chain. Because most of the blocks are empty, the whole chain is quite small, so this approach would cause problems with memory only for nodes running on very restricted hardware. However, loading so many blocks from disk and processing them takes a lot of time. We have tested exploitation of this bug with the target node running on a dedicated low-end machine. Processing each such a block took about five minutes. Thus we expect that processing such blocks can take between 2 to 10 minutes depending on the hardware the node is running on.
Moreover, the block can be constructed in a way that fully passes checks in VerifyInputsUnspent, but it fails one of the very next checks in AcceptBlock in a way the offending node is not banned or disconnected, only the invalid block is rejected. This allows the attacker to perform denial of service attack against the node for as long as it is connected, extending the time of node doing nothing to hours or days. We have successfully confirmed this during our test.
The attacker can deliver a large number of block messages in a row in very short time and this will cause that all these messages will be processed in a row. This means that the node is unable to do any other work before it processes the whole bunch of blocks received from the attacker. By holding the important cs_main lock during block message processing, the effects of the attack are devastating and the node is basically completely disabled while processing attacker's messages.
The following code is the main part of our exploit. It replaces the original SignBlock function in the flow of StakeMiner function. One more change we made in the flow is that the creation of the block using CreateNewBlock was changed in order not to use the best block as the previous block, but instead we used block number 200 on the main chain.
bool CBlock::SignBlockEx(CBlockIndex& pindexPrev, CWallet& wallet, int64_t nFees)
txCoinStake.nTime = pindexPrev.nTime - 1000;
uint256 hashTx = block.vtx.GetHash();
CPubKey pubKey = wallet.GenerateNewKey();
scriptPubKeyOut << key.GetPubKey() << OP_CHECKSIG;
vtx.nTime = nTime = txCoinStake.nTime;
vtx.insert(vtx.begin() + 1, txCoinStake);
hashMerkleRoot = BuildMerkleTree();
return key.Sign(GetHash(), vchBlockSig);
Second Vulnerability – Cheap 51% Attack
Bug type: 51% attack
Bug severity: 8/10
Attacker cost: high
With a medium-sized stake (well below 10% of the network staking power) in the network, the attacker is able to perform 51% attack and thus produce the longest chain without attacking any other nodes in the network. This allows the attacker to double spend or censor transaction.
In the original description of the second vulnerability, we have wrongly claimed the attacker costs were medium with estimated cost around 32000 USD at the time of writing. This was miscalculation on our side and we corrected the claim to high cost for the attacker. Also, after publishing of the first vulnerability, Neblio team started to communicate with us and we have disclosed the second vulnerability to them, so it is no longer for sale.