Unicorn Mode for Emulation-Based Fuzzing
Relevant source files
The following files were used as context for generating this wiki page:Unicorn Mode provides emulation-based fuzzing capabilities for AFL++ by leveraging the Unicorn Engine. This mode allows for the instrumentation and fuzzing of black-box, closed-source binary code snippets that cannot be compiled with afl-cc or easily run in QEMU mode. It enables AFL++ to gain coverage feedback from arbitrary binary code by emulating it.
While Unicorn Mode is a powerful tool for binary-only targets, it comes with a significant performance penalty compared to native, compile-time instrumentation. The effectiveness of this mode is highly dependent on a well-crafted harness that accurately sets up the execution environment for the target code.
Sources: unicorn_mode/README.md:14-22
Setup and Build
Before using Unicorn Mode, it must be built. The build process is handled by a Python script that prepares the Unicorn Engine with AFL++ support.
Build Process
- Build AFL++: Ensure that AFL++ is compiled successfully.
- Run Build Script: Navigate to the
unicorn_modedirectory and execute the build script:cd unicorn_mode ./build_unicorn_support.py
The build_unicorn_support.py script automates several steps:
- It checks for dependencies like
cargoand a sufficiently newrustc. - It checks out the correct version of the
unicornaflsubmodule. - It builds and installs the
unicornaflPython bindings, preferably into a local Python virtual environment (.venv). - It compiles the C/C++ static and dynamic libraries (
libunicornafl.a,libunicornafl.so/.dylib) and places them inunicorn_mode/lib. - Header files are copied to
unicorn_mode/include. - Finally, it runs a simple test using
afl-showmapto verify that the Python bindings are working correctly.
Sources: unicorn_mode/README.md:27-33, unicorn_mode/build_unicorn_support.py:36-121
The following diagram illustrates the build process initiated by build_unicorn_support.py.
Fuzzing with Unicorn Mode
To fuzz a target with Unicorn Mode, you must provide a custom harness and launch afl-fuzz with the -U flag.
Fuzzing Workflow
The general workflow for a fuzzing campaign is as follows:
- Harness Creation: Write a harness in C, Python, or Rust that prepares the memory map, loads the binary, sets up the initial state (registers, memory), and starts the emulation.
- Input Corpus: Create a folder with initial seed inputs for the fuzzer.
- Launch Fuzzer: Run
afl-fuzzwith the-Uflag, pointing it to the harness and input/output directories.
A typical command line looks like this:
afl-fuzz -U -m none -i /path/to/inputs -o /path/to/results -- ./test_harness @@
The -U flag tells afl-fuzz to operate in Unicorn Mode. The rest of the command line arguments follow the standard afl-fuzz format.
Sources: src/afl-fuzz.c:1160-1166, unicorn_mode/README.md:35-64
The diagram below shows the high-level architecture of the fuzzing loop in Unicorn Mode.
This diagram illustrates the interaction between afl-fuzz, the user-provided harness, and the Unicorn Engine during a fuzzing iteration.
Sources: unicorn_mode/samples/c/harness.c, unicorn_mode/samples/python_simple/simple_test_harness.py
Configuration
Unicorn Mode can be configured via command-line options and environment variables.
| Option | Description | Source File |
|---|---|---|
-U | Enables Unicorn Mode in afl-fuzz. | src/afl-fuzz.c:1160-1166 |
UNICORN_AFL_CMPCOV=1 | An environment variable that enables sub-instruction instrumentation for multi-byte comparisons, similar to laf-intel. This helps the fuzzer solve complex checks byte by byte. | unicorn_mode/README.md:81-85 |
Sources: src/afl-fuzz.c:1160-1166, unicorn_mode/README.md:81-85, test/test-unicorn-mode.sh:49
Harness Development
A harness is a program that uses the unicornafl library to load and execute a binary blob under emulation. Harnesses can be written in C, Python, or Rust.
Key Responsibilities of a Harness
- Initialize Unicorn Engine: Create a
uc_engineinstance for the correct architecture. - Memory Mapping: Map memory regions for the code, stack, and any data buffers, including the area for the fuzzer's input.
- Load Binary: Read the target binary from a file and write it into its mapped memory region.
- Set Initial State: Configure CPU registers (e.g., stack pointer, program counter, function arguments) to the state expected at the start of the code snippet to be fuzzed.
- Implement Input Placement: Create a callback function that takes the mutated input from
afl-fuzzand places it into the emulated memory. - Start Fuzzing: Call the main fuzzing loop function (
uc_afl_fuzzoruc_afl_fuzz_custom) which handles the forkserver and communication with AFL++.
Sources: unicorn_mode/README.md:46-60, unicorn_mode/samples/c/harness.c, unicorn_mode/samples/python_simple/simple_test_harness.py
C Harness Example
The C harness uses the unicornafl/unicornafl.h header. The core of the harness is the call to uc_afl_fuzz.
// unicorn_mode/samples/persistent/harness.c:203-214
uc_afl_ret afl_ret = uc_afl_fuzz(
uc, // The unicorn instance we prepared
filename, // Filename of the input to process.
place_input_callback, // Callback that places the input
&end_address, // Where to exit (this is an array)
1, // Count of end addresses
NULL, // Optional callback to run after each exec
false, // true, if the optional callback should be run also for non-crashes
1000, // For persistent mode: How many rounds to run
NULL // additional data pointer
);
The place_input_callback is critical for feeding data to the target. It receives the input buffer from AFL++ and must write it to the correct location in the emulated memory.
// unicorn_mode/samples/persistent/harness.c:113-140
static bool place_input_callback(
uc_engine *uc,
char *input,
size_t input_len,
uint32_t persistent_round,
void *data
){
if (input_len < 1 || input_len >= INPUT_SIZE_MAX - INPUT_OFFSET) {
// Test input too short or too long, ignore this testcase
return false;
}
// For persistent mode, we have to set up stack and memory each time.
uc_reg_write(uc, UC_X86_REG_RIP, &CODE_ADDRESS);
uc_reg_write(uc, UC_X86_REG_RSI, &INPUT_LOCATION);
uc_reg_write(uc, UC_X86_REG_RDI, &EMULATED_ARGC);
// Write the testcase to unicorn.
uc_mem_write(uc, INPUT_LOCATION + INPUT_OFFSET, input, input_len);
// store input_len for the faux strlen hook
current_input_len = input_len;
return true;
}
Python Harness Example
The Python harness uses the unicornafl package. The structure is similar to the C harness.
# unicorn_mode/samples/python_simple/simple_test_harness.py:146-152
# Start the fuzzer.
uc_afl_fuzz(
uc=uc,
input_file=args.input_file,
place_input_callback=place_input_callback,
exits=[end_address]
)
The place_input_callback in Python serves the same purpose.
# unicorn_mode/samples/python_simple/simple_test_harness.py:136-143
def place_input_callback(uc, input, persistent_round, data):
# Apply constraints to the mutated input
if len(input) > DATA_SIZE_MAX:
# print("Test input is too long (> {} bytes)")
return False
# Write the mutated command into the data buffer
uc.mem_write(DATA_ADDRESS, input)
API Comparison
The following table compares some of the key functions between the C and Python unicornafl bindings.
| Feature | C API (unicornafl.h) | Python API (unicornafl) |
|---|---|---|
| Initialization | uc_open(arch, mode, &uc) | uc = Uc(arch, mode) |
| Memory Map | uc_mem_map(uc, addr, size, perms) | uc.mem_map(addr, size, perms) |
| Memory Write | uc_mem_write(uc, addr, bytes, size) | uc.mem_write(addr, bytes) |
| Register Write | uc_reg_write(uc, regid, &value) | uc.reg_write(regid, value) |
| Add Hook | uc_hook_add(uc, &hook, type, ...) | uc.hook_add(type, callback) |
| Start Fuzzing | uc_afl_fuzz(uc, ...) | uc_afl_fuzz(uc, ...) |
Sources: unicorn_mode/samples/c/harness.c, unicorn_mode/samples/python_simple/simple_test_harness.py
Persistent Mode
For faster fuzzing, Unicorn Mode supports a persistent mode, which avoids the overhead of re-initializing the emulator for each input. In persistent mode, the harness is responsible for resetting the target's state between fuzzing iterations. This is typically done inside the place_input_callback. The number of iterations to run in persistent mode is specified as an argument to uc_afl_fuzz. A value greater than 1 enables persistent mode.
The C sample in unicorn_mode/samples/persistent/ demonstrates this by resetting the instruction pointer and function arguments at the beginning of each place_input_callback.
Sources: unicorn_mode/samples/persistent/harness.c:113-140, unicorn_mode/README.md:101-103
Provided Examples
The unicorn_mode/samples/ directory contains several examples to help users build their own harnesses.
| Example Directory | Language | Key Feature Demonstrated |
|---|---|---|
c | C | Basic usage of the C bindings. |
compcov_x64 | Python | Using UNICORN_AFL_CMPCOV to solve complex comparisons. |
persistent | C | Using persistent mode for higher fuzzing speeds. |
python_simple | Python | Basic usage of the Python bindings. |
speedtest/c | C | A harness for performance comparison against other languages. |
speedtest/rust | Rust | A harness in Rust for the same target, for performance comparison. |
Sources: unicorn_mode/README.md:95-110
Summary
Unicorn Mode is an essential feature of AFL++ for fuzzing binary-only targets where source code is unavailable and other binary instrumentation modes like QEMU are not suitable. It operates by emulating code snippets within the Unicorn Engine and feeding coverage information back to afl-fuzz. While it introduces performance overhead, its flexibility allows fuzzing a wide range of otherwise inaccessible targets. Success with Unicorn Mode heavily relies on creating a correct and efficient harness in C, Python, or Rust to accurately simulate the target's execution environment.