-
Notifications
You must be signed in to change notification settings - Fork 749
Fuzzing
Qiling Framework is an excellent engine for building coverage-guided fuzzers. Its ability to provide fine-grained control over the execution environment and its snapshot feature make it highly suitable for fuzzing applications, especially for complex, stateful targets.
- Cross-Platform: Fuzz binaries from different architectures (ARM, MIPS, x86) and operating systems (Linux, Windows, macOS) on a single machine.
- Snapshot Engine: Qiling's built-in snapshot feature allows you to save and restore the entire machine state (CPU, memory, registers) very quickly. This is crucial for high-performance fuzzing, as you can reset the state for each new input without restarting the entire process.
- Fine-grained Control: Use hooks to precisely control the program's execution, patch instructions on the fly, and model complex hardware interactions.
- No Source Code Required: Fuzz closed-source binaries and libraries.
A typical fuzzing loop with Qiling looks like this:
- Initialization: Load the target binary and run it until it reaches a point where it's ready to process input (the "fuzzing harness").
- Snapshot: Save a snapshot of the machine state at this point.
- Fuzzing Loop: a. Restore: Restore the snapshot. b. Input Injection: Get a new input from the fuzzer (e.g., AFL++, libFuzzer) and place it into the emulated program's memory (e.g., in a buffer, file, or network socket). c. Emulation: Resume emulation. d. Monitoring: Monitor for crashes (e.g., memory access violations) or other interesting behavior. e. Coverage: Collect code coverage information (which basic blocks were executed) and report it back to the fuzzer.
Here is a conceptual example of a fuzzing harness that uses the snapshot feature.
from qiling import Qiling
from qiling.const import QL_VERBOSE
# A simple function to fuzz
# It crashes if the input is "CRASH"
def harness_func(ql, input_data):
# Find the address of the input buffer in the emulated memory
input_buffer_addr = 0x100000 # Assume this is where the program expects input
# Write the fuzzer-generated input into the buffer
ql.mem.write(input_buffer_addr, input_data)
# Set the length of the input in a register (e.g., RDI)
ql.reg.rdi = len(input_data)
# Resume execution of the target function
target_function_addr = 0x401122
ql.run(begin=target_function_addr)
if __name__ == "__main__":
ql = Qiling(['my_app'], 'path/to/rootfs/x8664_linux', verbose=QL_VERBOSE.OFF)
# 1. Run to the point right before the target function is called
# This is where we want to start each fuzzing iteration
snapshot_point = 0x401120
ql.run(end=snapshot_point)
# 2. Save the snapshot
ql.save()
print("Snapshot taken. Starting fuzzing loop...")
# 3. Fuzzing loop (in a real scenario, this would be driven by a fuzzer)
test_inputs = [
b"good input",
b"another good one",
b"CRASH", # This will cause a crash
b"some other data"
]
for i, test_input in enumerate(test_inputs):
print(f"\n--- Iteration {i+1}: Input = {test_input} ---")
try:
# a. Restore the state
ql.restore()
# b. Inject input and run
harness_func(ql, test_input)
print("Execution finished normally.")
except Exception as e:
# d. Monitor for crashes
print(f"Crash detected! Exception: {e}")
# Here you would save the crashing input
To build a proper coverage-guided fuzzer, you need to integrate this harness with a fuzzing engine like AFL++.
-
AFL++ Qiling Mode: The AFL++ project has native support for Qiling as a fuzzing backend (
qiling_mode
). This is the recommended way to build a high-performance fuzzer with Qiling. - Coverage Tracking: The integration handles the complex parts, such as collecting basic block coverage from Unicorn Engine and passing it to the AFL++ engine in the required format.
A standout application of Qiling is fuzzing low-level firmware, such as UEFI applications. This is traditionally very difficult because the hardware environment is complex and not easily reproducible.
The Challenge: UEFI applications run in a special environment before the main operating system boots. They interact with hardware and UEFI-specific services, not standard OS syscalls.
The Qiling Solution (efi_fuzz
):
The efi_fuzz project is a great example of how to solve this. The approach is:
-
Emulate the UEFI Environment: Instead of a standard OS
rootfs
, the fuzzer sets up a memory layout that mimics a real UEFI environment. - Model UEFI Services: When the firmware application calls a UEFI service (e.g., to allocate memory or read a file), Qiling intercepts this call. Instead of a syscall handler, a custom Python function is executed to model the behavior of that UEFI service.
- Snapshot-Based Fuzzing: The fuzzer runs the UEFI application to a point where it is ready to accept input, takes a snapshot, and then uses that snapshot to run thousands of fuzzing iterations, just like in a standard fuzzing setup.
This approach allows for the fuzzing of firmware components in a completely isolated, scriptable, and scalable way, without needing physical hardware for every test case.
Using Qiling for fuzzing opens up possibilities for analyzing and finding vulnerabilities in a wide variety of targets that are difficult to fuzz with traditional tools.
- Home
- Getting Started
- Core Concepts
- Usage
- Features
- Tutorials
- Development
- Resources