JavaScript al dente. Fuzzing JS engines with Fuzzilli

Date: 14/03/2025

Hey guys! Today, pasta is on the menu! You will learn how to identify vulnerabilities in JavaScript engines using the Fuzzilli fuzzer. After a brief theoretical introduction, you’ll jump directly to practice. Let’s assemble the required tools and start fuzzing.

In the past, it was really difficult to fuzz JavaScript engines (those that enable you to add Snowfall to your browser or develop Node.js backends). Mutations in JS code caused syntax errors, which slowed down the process significantly. Samples were discarded by the engine, and you had to generate new ones. Over time, grammar-based fuzzers came to help, but they are not easy-to-use either.

In 2019, security researcher saelo unveiled his project – Fuzzilli. The idea of this fuzzer was to generate bytecode-like output (instead of JavaScript) that would be easier to mutate. The fuzzer name refers to a pasta variety, but, in fact, it originates from FuzzIL (Fuzzing Intermediate Language).

Fuzzilli
Fuzzilli

Test system

First of all, you’ll need a Linux virtual machine. A ready-made VM with your favorite distribution can be downloaded from osboxes.org. In my tests, I will use Ubuntu 22.

The more resources you allocate to the test VM, the better. The fuzzer shows code coverage, and, depending on the computation power, fuzzing the entire JS engine can take from a single day to several weeks.

The accessory tools include Git, the Swift programming language (not to be confused with the songstress), and the toolchain required to build JS engines (to be addressed in more detail later).

Start the VM and enter your password.

Password=osboxes.org
echo $Password | sudo -S apt update
sudo apt upgrade -y
Update & upgrade
Update & upgrade

Fuzzers

Fuzzing is a testing technique: incorrect, unexpected, or randomized data are fed to a software product as input, and the fuzzer monitors it for crashes, assertion triggers, and memory leaks.

An important fuzzing parameter is code coverage. In essence, this is the proportion of program code covered in the course of a certain set of tests.

Fuzzing can be either ‘dumb’ (or unstructured) or ‘smart’ (i.e. structured). If the fuzzer has no idea about the structure of input data expected by the tested program, this is dumb fuzzing. If it knows this structure, this is smart fuzzing.

Input data can be created using generation or mutation. Generation creates data from scratch; while mutation modifies existing data.

Depending on your knowledge about the source code, fuzzers can be divided into black-box and white-box ones.

Black-box fuzzing implies that you have no information about the program structure; accordingly, the fuzzer creates randomized input data.

White-box fuzzing involves program analysis with the purpose to improve code coverage (e.g. symbolic execution to bypass various parts of the program). However, program analysis takes longer than black-box fuzzing.

In addition, the tested ‘box’ can be gray. In such a case, code instrumentation is used instead of program analysis. This makes it possible to collect information about the program without analysis (i.e. something between a white and a black box). Input data are generated faster, and concurrently you receive code coverage information.

Fuzzilli belongs to the third type. In terms of input data creation, it combines generation and mutation. In terms of fuzzing technology, it’s rather ‘smart’.

JavaScript engines

The main components of a JavaScript engine are parser, interpreter, and compiler.

JS pipeline
JS pipeline

First, JavaScript source code is parsed. An abstract syntax tree (AST) is built, and bytecode is created on its basis. Then the interpreter executes the bytecode.

During the execution, various information is recorded (data profiling) and subsequently used when the bytecode is compiled into machine code. This operation is performed by the compiler.

Machine code is generated in situations when some code section is used frequently (e.g. a function is executed in a loop). Usually, it makes sense to spend time on its compilation and subsequently gain in execution time (since interpretation is always slower).

Typically, several compilers are used, and the optimal one is selected depending on the code optimization level.

www

More information on JS engines is available in the presentation “JavaScript engines: The Good Parts” (PDF, WebArchive).

Preparing fuzzer

Fuzzilli is available in the form of source code written in Swift.

To download the source code, you’ll need Git; to build Fuzzilli, GCC and Binutils packages (and, of course, the fuzzer source code). Install the dependencies and clone the Fuzzilli repository.

Password=osboxes.org
echo $Password | sudo -S apt update
sudo apt install git binutils gcc -y
git clone https://github.com/googleprojectzero/fuzzilli
Cloning repository
Cloning repository

Now go to the Swift website and look for a release suitable for your distribution in the Download section. For Ubuntu 22, download the release Ubuntu 22.04 x86_64.

Swift
Swift

Unpack the archive and copy the usr folder to install Swift. After that, make sure everything is configured correctly.

Below is a mini-script for lazy hackers. If you are reading this article many years later, replace the SwiftUrl variable with the respective URL from the Swift page.

# Installing Swift
Password=osboxes.org
SwiftUrl=https://download.swift.org/swift-5.8.1-release/ubuntu2204/swift-5.8.1-RELEASE/swift-5.8.1-RELEASE-ubuntu22.04.tar.gz
SwiftTar=$(echo $SwiftUrl | sed 's:.*/::')
SwiftFolder=${SwiftTar%.tar.gz}
# Go to HOME directory
cd $HOME
# Download archive
wget $SwiftUrl
# Extract
tar -xzf $SwiftTar
# Install
echo $Password | sudo -S cp -r $SwiftFolder/usr /
# Delete archive
rm $SwiftTar
# Delete folder
rm -rf $SwiftFolder
# Test run
swift --version
Installing Swift
Installing Swift

Now go to the Fuzzilli folder and build the fuzzer.

cd fuzzilli && swift build -c release
Building Fuzzilli
Building Fuzzilli

The fuzzer is ready. You can read its Help section if necessary.

swift run -c release FuzzilliCli --help

Time to prepare JS engines. The instruction on the repository main page says: “Download the source code for one of the supported JavaScript engines. See the Targets/ directory for the list of supported JavaScript engines”. For each engine, there is a separate folder containing an instruction that specifies how to build this particular engine for fuzzing.

Building and fuzzing V8

Theory

Let’s start with the Google Chrome browser engine called V8. It’s a JavaScript and WebAssembly open-source engine developed by Google and written in C++. It’s used in Chrome, Node.js, and numerous Chrome derivatives.

This engine was developed in Aarhus, Denmark, and its lead developer is Lars Bak. Bak was also involved in the development of the Self language and HotSpot Java virtual machine. Therefore, many of the Self developments migrated to V8 (e.g. JIT compilation and object maps).

www

For more detail, see the paper that formulates the basis of Self: “An Efficient Implementation of SELF, a Dynamically-Typed Object-Oriented Language Based on Prototypes” (PDF, WebArchive).

The engine includes the following components: Ignition interpreter, non-optimizing JavaScript compiler, and TurboFan optimizing compiler.

V8 pipeline
V8 pipeline

Building V8

The explanatory note in the Targets/V8 folder suggests to follow the instructions provided at https://v8.dev/docs/build. But I compiled all the required steps into a mini-script.

# Preparing and building V8
Password=osboxes.org
echo $Password | sudo -S apt update
# Go to HOME directory
cd $HOME
# Download depot_tools repository
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
# Add to PATH
echo "PATH=$HOME/depot_tools:$PATH" >> ~/.bashrc
source ~/.bashrc
# Download source code
gclient
fetch v8
cd v8/
gclient sync
# Install build dependencies (in fact, update since the script asks to enter password)
echo $Password | sudo -S apt update
./build/install-build-deps.sh
# Build V8 using script in the fuzzer folder
$HOME/fuzzilli/Targets/V8/fuzzbuild.sh
Building V8
Building V8

The script goes to the user home directory, installs depot_tools (to download and build V8), and downloads and compiles the V8 source code. This process can take some time depending on your PC’s horsepower.

Fuzzing

To start fuzzing, run the following command:

# Run Fuzzilli
# Disable core dumps
echo $Password | sudo -S sysctl -w 'kernel.core_pattern=|/bin/false'
# Go to Fuzzilli folder
cd $HOME/fuzzilli
# Runs
swift run -c release FuzzilliCli --profile=v8 --resume --storagePath=$HOME/fuzzilli-storage-v8 $HOME/v8/out/fuzzbuild/d8
Fuzzing V8
Fuzzing V8

The main settings are as follows: engine profile (--profile=v8), resume the previous fuzzing session (--resume), and path to the storage where the fuzzer will save its data (--storagePath): found crashes, sample corpus, and other information.

Fuzzing statistics will be displayed on a regular basis. The most interesting parameters are Crashes Found and Coverage.

Fuzzer statistics
Fuzzer statistics

Building and fuzzing SpiderMonkey

Theory

Let’s move on to Firefox JavaScript engine.

SpiderMonkey originates from the world’s first JavaScript engine released back in 1995. Originally developed by Brendan Eich at Netscape, its first versions were written in C, but later the code was rewritten in C++.

The SpiderMonkey structure is shown below. The parser produces bytecode. The JavaScript interpreter executes this bytecode. The baseline interpreter creates inline cache. The baseline compiler creates non-optimized machine code, and WarpMonkey creates optimized machine code.

SpiderMonkey pipeline
SpiderMonkey pipeline

Building SpiderMonkey

The instruction suggests to clone the Gecko repository and run the fuzzbuild.sh script.

But in fact, you have to apply patches from the Patches folder first – and only then build the engine. Also, fuzzbuild should be run not from js/src, but from the gecko-dev root.

To build the engine, you’ll need curl and the Rust compiler.

# Preparing and building SpiderMonkey
Password=osboxes.org
cd $HOME
# Install curl
echo $Password | sudo -S apt install curl -y
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o install.sh
sh ./install.sh -y
source "$HOME/.cargo/env"
# Clone repository
git clone https://github.com/mozilla/gecko-dev.git
# Go to folder
cd gecko-dev/js/src
# Apply patch
git apply $HOME/fuzzilli/Targets/Spidermonkey/Patches/*
cd $HOME/gecko-dev
# Build
$HOME/fuzzilli/Targets/Spidermonkey/fuzzbuild.sh
Rust installed
Rust installed
Building SpiderMonkey
Building SpiderMonkey

Fuzzing

After building the engine, you can start fuzzing it from the fuzzilli folder. The startup commands and fuzzer parameters are the same, except for the profile and executable file.

# Starting Fuzzilli
# Disable core dumps
echo $Password | sudo -S sysctl -w 'kernel.core_pattern=|/bin/false'
# Go to Fuzzilli folder
cd $HOME/fuzzilli
# Run
swift run -c release FuzzilliCli --profile=spidermonkey --resume --storagePath=$HOME/fuzzilli-storage-sm $HOME/gecko-dev/obj-fuzzbuild/dist/bin/js
Fuzzing SpiderMonkey
Fuzzing SpiderMonkey

Building and fuzzing JavaScriptCore

And finally, the JavaScriptCore engine that powers the Safari browser.

Theory

JavaScriptCore originates from the KJS JavaScript engine used in the Konqueror browser, which is part of KDE. The WebKit project started in 2001 as a fork of KHTML and KJS.

It features the most complex four-level pipeline:

  • LLInt (Low Level Interpreter) interprets bytecode generated based on the JavaScript source code;
  • Then slightly optimizing Baseline compiler goes into play. Both of them collect information required for further machine code optimizations.
  • The next component is DFG JIT (Data Flow Graph Just In Time). It enhances machine code optimization; and 
  • Finally, FTL JIT (Faster Than Light) generates the most optimized machine code.
KJS Pipeline
KJS Pipeline

Building JavaScriptCore

Let’s see what the instruction in the Targets folder says about JavaScriptCore.

You have to clone the code from the WebKit mirror, apply patches, and run the fuzzbuild.sh script. Also, you need to install Clang and dependencies. The Tools folder contains ready-made scripts for this. Download and install all the required stuff, apply patches, and build the engine. As usual, below is the script:

# Preparing and building JavaScriptCore
Password=osboxes.org
cd $HOME
# Download source code
git clone https://github.com/WebKit/WebKit.git
# Install dependencies
cd WebKit
echo $Password | sudo -S apt update
sudo apt install clang -y
Tools/gtk/install-dependencies
# Apply patches
git apply ../fuzzilli/Targets/JavaScriptCore/Patches/*
# Build JSC
$HOME/fuzzilli/Targets/JavaScriptCore/fuzzbuild.sh
Building JSC
Building JSC

Fuzzing

After building the engine, you can proceed to fuzzing.

# Running Fuzzilli
# Disable core dumps
echo $Password | sudo -S sysctl -w 'kernel.core_pattern=|/bin/false'
# Go to Fuzzilli folder
cd $HOME/fuzzilli
# Start fuzzing
swift run -c release FuzzilliCli --profile=jsc --resume --storagePath=$HOME/fuzzilli-storage-jsc $HOME/WebKit/FuzzBuild/Debug/bin/jsc
Fuzzing JavaScriptCore
Fuzzing JavaScriptCore

Conclusions

Now you are familiar with the fuzzing theory and have a fuzzing platform suitable for the three main JavaScript engines. However, Fuzzilli supports other engines as well (just check the Targets folder), including:

  • JerryScript;
  • QuickJS;
  • Qt QJSEngine;
  • XS; and 
  • duktape.

Your newly acquired knowledge and skills enable you to fuzz them on your own.

Good luck!

Useful links

Related posts:
2022.02.15 — First contact: How hackers steal money from bank cards

Network fraudsters and carders continuously invent new ways to steal money from cardholders and card accounts. This article discusses techniques used by criminals to bypass security…

Full article →
2022.01.13 — Bug in Laravel. Disassembling an exploit that allows RCE in a popular PHP framework

Bad news: the Ignition library shipped with the Laravel PHP web framework contains a vulnerability. The bug enables unauthorized users to execute arbitrary code. This article examines…

Full article →
2023.07.07 — Evil Ethernet. BadUSB-ETH attack in detail

If you have a chance to plug a specially crafted device to a USB port of the target computer, you can completely intercept its traffic, collect cookies…

Full article →
2022.06.01 — Routing nightmare. How to pentest OSPF and EIGRP dynamic routing protocols

The magic and charm of dynamic routing protocols can be deceptive: admins trust them implicitly and often forget to properly configure security systems embedded in these protocols. In this…

Full article →
2023.02.13 — First Contact: Attacks on Google Pay, Samsung Pay, and Apple Pay

Electronic wallets, such as Google Pay, Samsung Pay, and Apple Pay, are considered the most advanced and secure payment tools. However, these systems are also…

Full article →
2023.02.12 — Gateway Bleeding. Pentesting FHRP systems and hijacking network traffic

There are many ways to increase fault tolerance and reliability of corporate networks. Among other things, First Hop Redundancy Protocols (FHRP) are used for this…

Full article →
2023.07.29 — Invisible device. Penetrating into a local network with an 'undetectable' hacker gadget

Unauthorized access to someone else's device can be gained not only through a USB port, but also via an Ethernet connection - after all, Ethernet sockets…

Full article →
2022.01.12 — First contact. Attacks against contactless cards

Contactless payment cards are very convenient: you just tap the terminal with your card, and a few seconds later, your phone rings indicating that…

Full article →
2023.02.21 — SIGMAlarity jump. How to use Sigma rules in Timesketch

Information security specialists use multiple tools to detect and track system events. In 2016, a new utility called Sigma appeared in their arsenal. Its numerous functions will…

Full article →
2022.04.04 — Elephants and their vulnerabilities. Most epic CVEs in PostgreSQL

Once a quarter, PostgreSQL publishes minor releases containing vulnerabilities. Sometimes, such bugs make it possible to make an unprivileged user a local king superuser. To fix them,…

Full article →