diff --git a/.gitignore b/.gitignore index cf62bec..c4db936 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,24 @@ -build32 -build64 +build32 +build64 +bin/ +x64/ +x86/ +CMakeFiles/ +*.dir/ +CMakeCache.txt +cmake_install.cmake +*.vcxproj +*.vcxproj.filters +*.sln +*.recipe +*.tlog +*.obj +*.exe +*.dll +*.lib +*.exp +*.log +*.pdb +*.user +.vs/ +.vscode/ diff --git a/CMakeLists.txt b/CMakeLists.txt index da9ac89..c0d4dc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,18 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.18) # Upgrade for CMAKE_CUDA_ARCHITECTURES + +project(WinAFL_CUDA LANGUAGES C CXX) +set(CMAKE_CUDA_ARCHITECTURES native) + +# CUDA Support - Contributed by Elias Ibrahim (Feb 2026) +enable_language(CUDA) + +# Check if CUDA is found (enable_language should handle this, but explicit check is good) +if(NOT CMAKE_CUDA_COMPILER) + message(WARNING "CUDA compiler not found! GPU acceleration will be disabled.") +else() + message(STATUS "CUDA compiler found: ${CMAKE_CUDA_COMPILER}") + add_definitions(-DHAVE_CUDA) +endif() if (${USE_COLOR}) add_definitions(-DUSE_COLOR) @@ -12,6 +26,15 @@ if (${INTELPT}) add_definitions(-DINTELPT) endif() +# CUDA Support +# CUDA Support - Handled by enable_language(CUDA) +if (CMAKE_CUDA_COMPILER) + add_definitions(-DHAVE_CUDA) + include_directories(${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) +else() + message(WARNING "CUDA not found, GPU acceleration will be disabled.") +endif() + if (${INTELPT}) add_subdirectory(third_party/processor-trace) endif() @@ -73,10 +96,6 @@ add_executable(afl-showmap afl-showmap.c ) -if (${TINYINST}) - target_link_libraries(afl-showmap winafl_tinyinst) -endif() - project(afl-tmin) add_executable(afl-tmin @@ -144,6 +163,16 @@ project(afl-fuzz) SET(AFL_FUZZ_SOURCES afl-fuzz.c) +if(CMAKE_CUDA_COMPILER) + SET(AFL_FUZZ_SOURCES ${AFL_FUZZ_SOURCES} afl-fuzz-gpu.cu) +endif() + +if(DEFINED GPU_SANDBOX_DIR) + add_subdirectory(${GPU_SANDBOX_DIR} gpu_sandbox_build) + include_directories(${GPU_SANDBOX_DIR}/include) + add_definitions(-DUSE_GPU_SANDBOX) +endif() + if (${INTELPT}) include_directories(third_party/winipt/inc) @@ -158,6 +187,14 @@ add_executable(afl-fuzz ${AFL_FUZZ_SOURCES} ) +if(DEFINED GPU_SANDBOX_DIR) + target_link_libraries(afl-fuzz gpu_sandbox) +endif() + +# if (CMAKE_CUDA_COMPILER) +# Target linking is automatic for CUDA sources +# endif() + if (${INTELPT}) target_link_libraries(afl-fuzz winipt) diff --git a/ChangeLog b/ChangeLog index eb71224..484e913 100755 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,25 @@ ChangeLog ========= +---------------------------------- +Version 1.17 (Feb 2026, based on afl 2.43b): +---------------------------------- + + - CUDA GPU Acceleration support (Tier 1 GPU Havoc Mutator). + Offloads seed havoc mutations (bitflips, byte arithmetic, block mutations, + and dictionary splicing) to NVIDIA GPU CUDA cores using double-buffered + asynchronous memory streams and hardware CuRand entropy. + Contributed by Elias Ibrahim + + - Added -G / /gpu command-line flags to enable GPU acceleration in afl-fuzz. + + - Added AFL_GPU_BATCH_SIZE environment variable to configure GPU thread batch allocations. + + - Added experimental Tier 2 GPU target execution harness template (experimental/gpu_harness_template.cu). + + - Added target analysis & harness generation utilities (winafl-target-finder.py, + winafl-harness-builder.py, ghidra_bridge.py). + ----------------------------------- Version 1.16b (based on afl 2.43b): ----------------------------------- diff --git a/FUZZING_GUIDE.md b/FUZZING_GUIDE.md new file mode 100644 index 0000000..3f98ec5 --- /dev/null +++ b/FUZZING_GUIDE.md @@ -0,0 +1,130 @@ +# The Complete Fuzzer's Guide to GPU-Accelerated WinAFL + +*Contributed by Elias Ibrahim (Feb 2026)* + +This guide provides a comprehensive, end-to-end walkthrough on how to conduct a professional fuzzing campaign using this GPU-accelerated fork of WinAFL. It covers everything from selecting a vulnerable target to weaponizing the crashes you find. + +--- + +## 1. Finding a Suitable Target + +Not all applications are good candidates for fuzzing. WinAFL relies on repeatedly feeding mutated data into a target function. + +### What makes a good target? +- **File Parsers:** Applications that take complex, structured files as input (e.g., PDF readers, Image viewers, Video players, Office suites, CAD software). +- **Network Services:** Custom protocols, HTTP servers, or clients that process external data packets. +- **Stateless/Determinism:** The target function should ideally produce the same output/behavior given the same input, without relying heavily on external state (like databases or internet requests). +- **Speed:** The target function needs to execute quickly. The faster the initialization and teardown, the more executions per second (execs/sec) WinAFL can achieve. + +### Finding the Attack Surface +Look for applications handling untrusted user input: +1. **Libraries:** Often the best targets. Look for `dll` files responsible for parsing (e.g., `libpng.dll`, `avcodec.dll`). +2. **Command Line Utilities:** Tools that process a file passed via argument (e.g., `magick.exe image.jpg new.png`). +3. **GUI Applications:** You will need to find the specific function within the GUI app that handles the "File -> Open" or parsing logic. + +--- + +## 2. Harnessing the Target + +WinAFL uses **DynamoRIO** (or TinyInst/Intel PT) to instrument closed-source Windows binaries dynamically. To do this, WinAFL needs to know exactly *where* to start fuzzing. + +### Identifying the Target Function +You cannot simply fuzz `main()`. You need to isolate the function that actually parses the data. +1. Open the target binary or DLL in a disassembler like **IDA Pro**, **Ghidra**, or **Binary Ninja**. +2. Trace the execution flow from the point a file is opened (e.g., `CreateFileW`, `fopen`) to where the data is actually processed. +3. Identify the function offset (e.g., `0x12A40`). + +### Requirements for the Target Function: +- It must open the input file. +- It must parse the file data. +- It must close the file handle (crucial, otherwise WinAFL cannot overwrite the file for the next mutation). +- It must return normally (it cannot call `ExitProcess`). + +### Verifying the Harness +Before fuzzing, test your target function using DynamoRIO's standalone tool to ensure it executes correctly and is caught by the instrumentation: +```cmd +path\to\DynamoRIO\bin64\drrun.exe -c winafl.dll -debug -target_module target.exe -target_offset 0x12A40 -fuzz_iterations 10 -nargs 2 -- target.exe input.txt +``` +Check the generated `afl.log` to ensure it successfully captured the iterations without crashing prematurely. + +--- + +## 3. Preparing the Seed Corpus + +The quality of your initial inputs (seeds) determines the success of the campaign. WinAFL uses these initial files and mutates them on the GPU to discover new code paths. + +### Building the Corpus +1. Gather valid files of the target format (e.g., 50 different `.pdf` files). +2. Ensure they are small. Fuzzing a 10MB PDF is incredibly slow. Aim for files under 10KB. +3. Ensure they hit different features of the parser (e.g., one PDF with images, one with an encrypted stream, one with forms). + +### Minimizing the Corpus +Running WinAFL with redundant seeds wastes time. Use `afl-tmin` to minimize the size of individual files, and then use `winafl-cmin.py` to remove seeds that trigger the exact same code paths. + +To minimize a single file: +```cmd +bin\Release\afl-tmin.exe -D \path\to\DynamoRIO\bin64 -t 1000 -- target.exe @@ +``` + +--- + +## 4. Running the GPU-Accelerated Fuzzer + +With your target harnessed and your seeds ready (`in/` folder), it's time to unleash the GPU. + +The `-G` (or `/gpu`) flag tells WinAFL to offload the heavy "Havoc" mutation stage to the NVIDIA CUDA architecture, multiplying your mutation throughput. + +### The Launch Command +```cmd +bin\Release\afl-fuzz.exe -G -i in_dir -o out_dir -t 2000 -D \path\to\DynamoRIO\bin64 -w bin\Release\winafl.dll -target_module target.exe -target_offset 0x12A40 -coverage_module target.exe -fuzz_iterations 5000 -- target.exe @@ +``` + +### Understanding the Flags: +- `-G` (or `/gpu`): Enables the high-throughput VRAM mutation engine. +- `-i` / `-o`: Input seed directory and output findings directory. +- `-t 2000`: Timeout in milliseconds. If the target hangs for 2 seconds, it counts as a crash/hang. +- `-target_module` / `-target_offset`: The exact DLL/EXE and function address you identified in step 2. +- `-coverage_module`: The specific module you want to record code coverage for (keeps instrumentation fast). +- `@@`: WinAFL replaces this with the path to the mutated file it generates for each execution. + +### Monitoring the Campaign +Watch the `afl-fuzz` UI. +- **exec speed**: This is your executions per second. The higher, the better. (Aim for >50 exec/s for heavy targets, >1000 for light targets). +- **uniq crashes**: The holy grail. If this number increases, WinAFL found an input that caused a memory access violation or fatal exception. +- **gpu batches**: Indicates successful offloading to the CUDA streams. + +--- + +## 5. Triaging Crashes + +When WinAFL finds a crash, it saves the mutated input file that caused it into the `out_dir/crashes/` folder. + +1. **Reproduce:** Take a crashing file and run it manually against the target application to ensure it consistently crashes. +2. **Analyze the Exception:** Open the target in a native debugger like **WinDbg** or **x64dbg**. +3. Run the application with the crashing file: + ```cmd + windbg.exe target.exe out_dir\crashes\id_000000_... + ``` +4. When the debugger catches the exception, use commands like `!analyze -v` in WinDbg. + +### Identifying the Bug Class +You are looking for specific violations: +- **Access Violation (0xC0000005):** The program tried to read or write to unallocated memory. + - *Read Access Violation:* Often an Out-Of-Bounds (OOB) Read. Can lead to Information Leakage. + - *Write Access Violation:* Often a Buffer Overflow, Use-After-Free (UAF), or Type Confusion. These are prime candidates for Remote Code Execution (RCE). +- **Stack Exhaustion / Recursion:** Usually Denial of Service (DoS), harder to exploit for RCE. +- **Divide by Zero:** Usually DoS. + +--- + +## 6. Exploitation (Weaponization) + +Once you've triaged a Write Access Violation or an exploitable Read, you transition from Fuzzing to Exploit Development. + +1. **Root Cause Analysis:** Use your disassembler (IDA/Ghidra) alongside WinDbg. Look at the assembly instructions surrounding the crash. *Why* did it crash? Was a size header parsed incorrectly? Did an integer overflow occur during allocation (`malloc(size * count)`)? +2. **Controlling the Instruction Pointer (RIP/EIP):** If it's a buffer overflow, can you overwrite the return address on the stack, or a function pointer in the heap? You need to trace the exact offset in your mutated file that corresponds to the overwritten memory. +3. **Bypassing Mitigations:** Modern Windows binaries have defenses: + - **ASLR (Address Space Layout Randomization):** You will likely need an Information Leak (an OOB Read bug) to find the base addresses of `ntdll.dll` or your target module to bypass ASLR. + - **DEP (Data Execution Prevention):** You cannot just execute shellcode on the stack. You must use **ROP (Return-Oriented Programming)** to chain together existing snippets of code ("gadgets") to call functions like `VirtualProtect` or `WinExec`. + - **CFG (Control Flow Guard):** If enabled, you must find ways around indirect call validation. +4. **The Final Exploit:** You write a Python script that perfectly crafts a malicious file (e.g., a `.pdf`). It triggers the integer overflow, overwrites a vtable pointer, uses a ROP chain to bypass DEP, and executes your custom shellcode (like popping `calc.exe` or opening a reverse shell). diff --git a/GHIDRA_INTEGRATION.md b/GHIDRA_INTEGRATION.md new file mode 100644 index 0000000..6292814 --- /dev/null +++ b/GHIDRA_INTEGRATION.md @@ -0,0 +1,524 @@ +# Ghidra Integration - Setup, Usage & Troubleshooting + +*Contributed by Elias Ibrahim (Feb 2026)* + +Automated reverse engineering for WinAFL harness generation via Ghidra's **REST API**. +This guide covers both **Windows** and **Linux** environments. + +> [!IMPORTANT] +> **No AI, no MCP protocol, no extra SDKs.** Our scripts talk directly to Ghidra's +> HTTP REST API using plain Python `urllib`. The Ghidra plugin simply exposes an HTTP +> server on localhost — our tools query it like any other REST API. + +--- + +## Overview + +The Ghidra integration adds automated RE analysis to the fuzzing pipeline: + +``` +┌──────────────┐ HTTP REST API ┌──────────────┐ ┌──────────────────────────┐ +│ │ ◀────────────── │ │ │ │ +│ Ghidra + │ Decompile │ ghidra_ │ ──▶ │ winafl-harness-builder │ +│ GhydraMCP │ Call graph │ bridge.py │ │ analyze / generate / │ +│ Plugin │ XRefs │ (urllib) │ │ auto / full │ +│ port 8192 │ Signatures │ │ │ │ +└──────────────┘ └──────────────┘ └──────────────────────────┘ + ↑ + No AI. No MCP SDK. + Just HTTP GET/POST. +``` + +**Recommended plugin:** [GhydraMCP](https://github.com/starsong-consulting/GhydraMCP) (port 8192) + +The bridge also auto-detects [GhidraMCP](https://github.com/LaurieWired/GhidraMCP) (port 8080) +and [GhidrAssistMCP](https://github.com/jtang613/GhidrAssistMCP) (port 8080) if you happen +to have one of those installed instead, but **you only need one**. + +--- + +## Prerequisites + +### Windows + +1. **Python 3.6+** — `python --version` +2. **Ghidra 11.x** — Download from [https://ghidra-sre.org](https://ghidra-sre.org) +3. **Java JDK 17+** — Required by Ghidra. [Adoptium](https://adoptium.net/) +4. **Visual Studio Build Tools** — For `dumpbin.exe` and `cl.exe` (fallback when Ghidra is unavailable) + +### Linux + +1. **Python 3.6+** — `python3 --version` +2. **Ghidra 11.x** — Download and extract to e.g. `/opt/ghidra` +3. **Java JDK 17+** — `sudo apt install openjdk-17-jdk` (Debian/Ubuntu) or `sudo dnf install java-17-openjdk` (Fedora) +4. **GCC or Clang** — For compiling generated harnesses + +> [!NOTE] +> The `ghidra_bridge.py` module uses only Python stdlib (`urllib`, `json`) — **no pip dependencies required**. + +--- + +## Installation + +### Step 1: Install the GhydraMCP Ghidra Plugin + +The plugin adds an HTTP REST server to Ghidra. No Python, no AI, no MCP SDK needed on the Ghidra side. + +**Download:** [github.com/starsong-consulting/GhydraMCP/releases](https://github.com/starsong-consulting/GhydraMCP/releases) + +> [!IMPORTANT] +> The release contains **two zip files**: an outer "Complete" archive and the actual +> plugin zip inside it. You must unpack the outer one first. + +**Windows & Linux:** +```bash +# 1. Download the latest release (e.g., GhydraMCP-Complete-2.2.0.zip) +# 2. UNPACK the outer zip — inside you'll find: +# GhydraMCP-2.2.0.zip ← This is the actual Ghidra plugin +# bridge_mcp_ghidra.py ← MCP bridge (we DON'T need this) +# +# 3. In Ghidra: +# File → Install Extensions → Click + → Select GhydraMCP-2.2.0.zip +# (NOT the Complete zip, the inner plugin zip) +# 4. Restart Ghidra +# 5. File → Configure → Developer → Ensure GhydraMCPPlugin is checked +``` + +If Ghidra **still rejects the zip**, there are two alternative install methods: + +```bash +# Alternative A: Manual install (copy to extensions directory) +# 1. Find your Ghidra extensions folder: +# Windows: %USERPROFILE%\.ghidra\\Extensions\ +# Linux: ~/.ghidra//Extensions/ +# 2. Unzip GhydraMCP-2.2.0.zip directly into that folder +# 3. You should have: Extensions/GhydraMCP/lib/GhydraMCP.jar +# Extensions/GhydraMCP/extension.properties +# 4. Restart Ghidra + +# Alternative B: Build from source (requires Maven + JDK 17+) +git clone https://github.com/starsong-consulting/GhydraMCP.git +cd GhydraMCP +mvn clean package -P plugin-only +# Output: target/GhydraMCP-.zip → install this in Ghidra +``` + +**Verify it's working:** +```bash +# After opening a binary in Ghidra's CodeBrowser, check the console: +# Click the computer icon in bottom-right of project window → "Open Console" +# Look for: "HydraMCP HTTP server started on port 8192" + +# Or test from command line: +curl http://localhost:8192/api/instances # Linux +python ghidra_bridge.py --test localhost:8192 # Windows or Linux +``` + +### Step 2: Verify Files Are in Place + +Ensure these files are in your WinAFL directory: + +``` +z:\WinAFL CUDA2\ +├── ghidra_bridge.py # HTTP client for Ghidra REST API +├── winafl-harness-builder.py # Main harness tool +└── winafl-target-finder.py # Target discovery tool +``` + +--- + +## Usage + +### Quick Start — Fully Automated (Ghidra Required) + +```bash +# 1. Open your target binary in Ghidra and let auto-analysis complete +# 2. Run: +python winafl-harness-builder.py auto target.dll --ghidra localhost:8192 --out harness/ +``` + +This will: +1. Connect to Ghidra's analysis engine +2. Discover all exported functions + file I/O callers via cross-references +3. Score and rank fuzzing candidates +4. Decompile the top candidates +5. Auto-detect harness style from real parameter types +6. Generate ready-to-compile C harness source files + +### Manual Pipeline — With Ghidra Enhancement + +```bash +# Analyze a specific function with Ghidra decompilation +python winafl-harness-builder.py analyze target.dll --offset 0x41040 --ghidra localhost:8192 + +# Generate with Ghidra-derived function signature +python winafl-harness-builder.py generate target.dll --offset 0x41040 --ghidra localhost:8192 + +# Full pipeline (analyze → generate → validate) with Ghidra +python winafl-harness-builder.py full target.dll --offset 0x41040 --ghidra localhost:8192 --out harness/ +``` + +### Without Ghidra (Fallback to dumpbin) + +All commands work without `--ghidra` — they use `dumpbin.exe` for disassembly instead: + +```bash +python winafl-harness-builder.py full target.dll --offset 0x41040 --out harness/ +``` + +### Standalone Ghidra Bridge CLI + +```bash +# Test connection +python ghidra_bridge.py --test localhost:8192 + +# List all functions +python ghidra_bridge.py --list-functions localhost:8192 + +# Filter functions by name +python ghidra_bridge.py --list-functions localhost:8192 --filter CreateBitmap + +# Decompile a function +python ghidra_bridge.py --decompile GdipLoadImageFromFile localhost:8192 + +# Get call graph (depth 3) +python ghidra_bridge.py --callgraph main localhost:8192 --depth 3 + +# Find cross-references to an address (who calls this function?) +python ghidra_bridge.py --xrefs-to 0x00401000 localhost:8192 + +# Auto-discover fuzzing targets +python ghidra_bridge.py --find-fuzz-targets localhost:8192 + +# Get C function signature (for manual typedef) +python ghidra_bridge.py --signature GdipLoadImageFromFile localhost:8192 + +# Get function variables (parameter names and types) +python ghidra_bridge.py --variables GdipLoadImageFromFile localhost:8192 + +# JSON output (for scripting/piping) +python ghidra_bridge.py --list-exports localhost:8192 --json +``` + +--- + +## What Ghidra Enhancement Provides + +| Feature | Without Ghidra (dumpbin) | With Ghidra | +|---------|------------------------|-------------| +| Disassembly | Raw x86/x64 instructions | Full C pseudocode decompilation | +| Arg count | Heuristic from prologue registers | Exact from function prototype | +| Arg types | Unknown (manual typedef needed) | Real types auto-filled in typedef | +| Call graph | None | Full call tree with configurable depth | +| File I/O detection | Pattern match in disasm | Cross-reference analysis of import table | +| Harness style | Manual or basic auto-detect | Intelligent from parameter types | +| Coverage | Single function body | Nested calls traced via call graph | + +--- + +## Command Reference + +### Global Options + +| Flag | Description | +|------|-------------| +| `--ghidra HOST:PORT` | Connect to Ghidra's REST API (e.g. `localhost:8192`) | +| `--arch {x64,x86}` | Target architecture (default: x64) | +| `--json-out` | Output analysis results as JSON | + +### Commands + +| Command | Description | Requires Ghidra? | +|---------|-------------|:---:| +| `analyze` | Deep-analyze a target function | Optional | +| `generate` | Generate harness C source code | Optional | +| `validate` | Pre-flight safety checks | No | +| `full` | analyze → generate → validate | Optional | +| `pipe` | Process piped JSON from target-finder | No | +| `auto` | Fully automated discovery + generation | **Yes** | +| `styles` | List available harness templates | No | + +### Harness Styles + +| Style | When to Use | +|-------|------------| +| `file_parser` | Function takes a file path (`LPCWSTR filename`) | +| `stream_parser` | Function takes a pre-opened handle (`HANDLE hFile`) | +| `buffer_parser` | Function takes `(BYTE* buf, size_t len)` | +| `dll_export` | Generic DLL export via `LoadLibrary` + offset | +| `com_interface` | COM object method fuzzing | +| `custom` | Minimal skeleton for manual coding | +| `auto` | Auto-detect from analysis/Ghidra params (default) | + +--- + +## Linux-Specific Notes + +### Running WinAFL Harnesses on Linux + +WinAFL is Windows-only, but the **analysis and harness generation tools work on Linux**: + +```bash +# Analysis on Linux (Ghidra runs natively on Linux) +python3 ghidra_bridge.py --test localhost:8192 +python3 winafl-harness-builder.py auto target.dll --ghidra localhost:8192 --out harness/ + +# Cross-compile harnesses for Windows (using MinGW) +x86_64-w64-mingw32-gcc -o harness.exe harness/harness_target_41040.c -lole32 +``` + +### Ghidra Headless Mode (Linux) + +For CI/CD or batch processing, use Ghidra's headless analyzer: + +```bash +# Import binary into a Ghidra project (headless) +/opt/ghidra/support/analyzeHeadless /tmp/ghidra_project MyProject \ + -import /path/to/target.dll -scriptPath /path/to/ghydramcp/scripts + +# Start the GhydraMCP HTTP server in headless mode +# (See GhydraMCP documentation for headless setup) +``` + +### Python Path Setup (Linux) + +```bash +# Ensure both scripts can find each other +export PYTHONPATH="/path/to/winafl:$PYTHONPATH" + +# Or just run from the WinAFL directory +cd /path/to/winafl +python3 winafl-harness-builder.py auto target.dll --ghidra localhost:8192 +``` + +--- + +## Troubleshooting + +### Connection Issues + +#### "Cannot connect to Ghidra at localhost:8192" + +**Cause:** Ghidra is not running, plugin is not enabled, or wrong port. + +**Fix:** +```bash +# 1. Verify Ghidra is running with the plugin +# In Ghidra: File → Configure → Developer → Check GhydraMCP is enabled + +# 2. Check the port +# GhydraMCP default: 8192 +# GhidraMCP default: 8080 +# Try both: +python ghidra_bridge.py --test localhost:8192 +python ghidra_bridge.py --test localhost:8080 + +# 3. Check if the HTTP server is listening (Windows) +netstat -ano | findstr :8192 + +# 3. Check if the HTTP server is listening (Linux) +ss -tlnp | grep 8192 +# or +curl -s http://localhost:8192/api/instances + +# 4. Check Ghidra console for errors +# Window → Scripting → Console Log +``` + +#### "Could not connect — falling back to dumpbin" + +**Cause:** `--ghidra` was specified but connection failed. The tool continues with dumpbin fallback. + +**Fix:** This is non-fatal. The tool will still work, just without Ghidra's deep analysis. Fix the connection to get the enhanced results. + +### Import/Module Issues + +#### "ghidra_bridge not found" or "HAS_GHIDRA_BRIDGE is False" + +**Cause:** `ghidra_bridge.py` is not in the same directory as `winafl-harness-builder.py`. + +**Fix:** +```bash +# Ensure both files are in the same directory +ls ghidra_bridge.py winafl-harness-builder.py # Linux +dir ghidra_bridge.py winafl-harness-builder.py # Windows + +# Or add to PYTHONPATH +export PYTHONPATH="/path/to/winafl:$PYTHONPATH" # Linux +set PYTHONPATH=z:\WinAFL CUDA2;%PYTHONPATH% # Windows +``` + +#### "I/O operation on closed file" (Windows only) + +**Cause:** Previous terminal session had a broken stdout/stderr wrapper. + +**Fix:** Close and reopen the terminal (cmd.exe or PowerShell), then retry. + +### Analysis Issues + +#### "No candidates found" in auto mode + +**Cause:** The binary may not export parser-like functions or import file I/O APIs. + +**Fix:** +```bash +# 1. Check what functions Ghidra sees +python ghidra_bridge.py --list-functions localhost:8192 --limit 100 + +# 2. Check imports +python ghidra_bridge.py --list-imports localhost:8192 + +# 3. Try manual analysis with a known offset +python winafl-harness-builder.py analyze target.dll --offset 0x1000 --ghidra localhost:8192 +``` + +#### "Function body is very small — may be a thunk/stub" + +**Cause:** The function at the specified offset is a thin wrapper that jumps to another function. + +**Fix:** +```bash +# Use Ghidra to decompile and find the real implementation +python ghidra_bridge.py --decompile 0x41040 localhost:8192 + +# Look at the call graph to find the actual parsing function +python ghidra_bridge.py --callgraph 0x41040 localhost:8192 --depth 3 +``` + +#### "FATAL: Function calls ExitProcess" + +**Cause:** The function terminates the process, making it incompatible with WinAFL's loop mechanism. + +**Fix:** Choose a different function that returns normally. Use the `auto` command to find safe candidates, or manually pick a function deeper in the call chain that does the parsing without exiting. + +### Compilation Issues + +#### "cl.exe not found" (Windows) + +**Fix:** +```cmd +REM Open a Developer Command Prompt for VS, or: +"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" +cl.exe /nologo /W3 /O2 harness\harness_target_41040.c +``` + +#### Cross-compiling on Linux + +```bash +# Install MinGW +sudo apt install gcc-mingw-w64-x86-64 # Debian/Ubuntu +sudo dnf install mingw64-gcc # Fedora + +# Compile +x86_64-w64-mingw32-gcc -o harness.exe harness/harness_target_41040.c \ + -lkernel32 -lole32 -lgdi32 +``` + +### Ghidra Plugin Issues + +#### Ghidra rejects the .zip file ("Not a valid extension") + +**Cause:** You're trying to install the outer "Complete" zip instead of the inner plugin zip. + +**Fix:** +``` +1. Unpack GhydraMCP-Complete-2.2.0.zip first +2. Inside you'll find GhydraMCP-2.2.0.zip (the actual plugin) +3. Install THAT file via File → Install Extensions → + +``` + +If it still doesn't work (version mismatch with your Ghidra): +```bash +# Manual install: +# Windows: +cd %USERPROFILE%\.ghidra\ghidra_11.3_PUBLIC\Extensions +mkdir GhydraMCP +# Extract GhydraMCP-2.2.0.zip contents here + +# Linux: +cd ~/.ghidra/ghidra_11.3_PUBLIC/Extensions +mkdir GhydraMCP +unzip GhydraMCP-2.2.0.zip -d GhydraMCP/ + +# Or build from source (always matches your Ghidra): +git clone https://github.com/starsong-consulting/GhydraMCP.git +cd GhydraMCP +export GHIDRA_INSTALL_DIR=/path/to/ghidra +mvn clean package -P plugin-only +``` + +#### Plugin not showing in Ghidra Configure menu + +**Fix:** +``` +1. Ensure you installed the correct version for your Ghidra version +2. Check: File → Install Extensions → Verify the plugin is listed +3. Restart Ghidra completely (close ALL Ghidra windows) +4. File → Configure → Developer → Check the plugin checkbox +5. A tool restart is usually needed after enabling +``` + +#### Ghidra analysis hasn't completed + +**Cause:** Auto-analysis may take several minutes for large binaries. + +**Fix:** +```bash +# Check analysis status +python ghidra_bridge.py --test localhost:8192 + +# In Ghidra: Wait for the progress bar at bottom-right to finish +# Or trigger analysis manually: Analysis → Auto Analyze +``` + +--- + +## Architecture + +### Files + +| File | Purpose | +|------|---------| +| `ghidra_bridge.py` | Protocol-agnostic Ghidra HTTP client | +| `winafl-harness-builder.py` | Harness generation + analysis pipeline | +| `winafl-target-finder.py` | Target discovery + scoring | + +### Data Flow + +``` +ghidra_bridge.py + GhidraClient + ├── connect() → Auto-detect server type + ├── decompile() → C pseudocode + ├── get_callgraph() → Function call tree + ├── get_xrefs() → Cross-references + ├── get_function() → Signature + parameters + ├── list_exports() → Exported functions + ├── list_imports() → Imported APIs + ├── find_fuzz_candidates() → Automated scoring + └── get_function_signature_c() → Harness typedef + +winafl-harness-builder.py + DeepAnalyzer + ├── _ghidra_enhance() → Uses GhidraClient when available + └── _disassemble() → Uses dumpbin when Ghidra unavailable + HarnessGenerator + ├── _ghidra_typedef() → Real params from Ghidra + └── auto-detect style → From Ghidra parameter types +``` + +### Supported REST API Endpoints + +The bridge auto-detects the server type and adapts its HTTP calls: + +| Endpoint | GhydraMCP | GhidraMCP | GhidrAssistMCP | +|----------|:---------:|:---------:|:--------------:| +| `/api/functions` | ✅ | ❌ | ❌ | +| `/api/functions/{id}/decompile` | ✅ | ❌ | ❌ | +| `/api/analysis/callgraph` | ✅ | ❌ | ❌ | +| `/api/analysis/dataflow` | ✅ | ❌ | ❌ | +| `/api/xrefs` | ✅ | ❌ | ❌ | +| `/methods` | ❌ | ✅ | ❌ | +| `/decompile/{name}` | ❌ | ✅ | ❌ | +| `/imports` | ❌ | ✅ | ✅ | +| `/exports` | ❌ | ✅ | ✅ | diff --git a/GHIDRA_MCP_INTEGRATION_GUIDE.md b/GHIDRA_MCP_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..6b39cbf --- /dev/null +++ b/GHIDRA_MCP_INTEGRATION_GUIDE.md @@ -0,0 +1,108 @@ +# Ghidra MCP Integration Guide + +*Contributed by Elias Ibrahim (Feb 2026)* + +This guide provides end-to-end instructions for connecting **WinAFL AI Agents** to **Ghidra** via Model Context Protocol (MCP) servers. + +While our Python utilities (like `winafl-target-finder.py`) use `dumpbin.exe` out of the box to quickly discover parsing functions, things get a lot more powerful when you plug **Ghidra** into the pipeline. + +By running Ghidra v11 with an active Model Context Protocol (MCP) extension, you upgrade the automated script from a basic "offset finder" into a deep-learning reverse engineer capable of understanding code context. + +Here is the setup, how to use it, and exactly what it does for your fuzzing campaigns. + +--- + +## 1. Setup & Installation + +### The Prerequisites +* **Ghidra v11+**: Ensure you have a recent version of Ghidra running. +* **Python 3.6+**: You must have Python installed and added to your system PATH. +* **Ghidra Bridge / MCP Server**: You need an RPC bridge running inside Ghidra that allows external Python scripts to query its decompiler. + * **Install the Python package**: + 1. Open your Command Prompt (`cmd.exe`). + 2. Verify Python is working by typing `python --version`. + 3. Type: `pip install ghidra_bridge` and press Enter. + 4. Wait for the "Successfully installed ghidra_bridge..." message. + * **Install the Ghidra Server Plugin:** + 1. Open your target binary in the Ghidra CodeBrowser. + 2. If you are using the `bridge_mcp_hydra.py` MCP extension, it communicates with Ghidra's REST API. You must install the corresponding Ghidra server plugin (usually an extension zip file) into Ghidra and enable it. + 3. Start the plugin server within Ghidra so it listens on the default port (8192). + * **Run the MCP Python Bridge:** + 1. Open your Command Prompt (`cmd.exe`). + 2. Navigate to where you saved the bridge script: `cd d:\hacking\tools` + 3. Run the script: `python bridge_mcp_hydra.py` + 4. The script will automatically discover your running Ghidra instance on port 8192 and provide the MCP interface. + +### The Connection +When `winafl-harness-builder.py` starts up, it automatically attempts to import `ghidra_bridge`. +If the import succeeds, the script is now in **Enhanced Ghidra Mode**. + +To use this mode, your Ghidra client **must be open**, the target binary must be loaded/analyzed in the CodeBrowser, and the RPC server script (e.g., `GhidraBridgeServer.py` or the MCP server) must be running. + +--- + +## 2. How to Use It in the Pipeline + +Using Ghidra doesn't change *how* you use the Python scripts; it just makes them dramatically smarter. + +Normally, you would use dumpbin to find an offset, and then tell the compiler to build a generic harness: +```cmd +python winafl-harness-builder.py generate target.dll --offset 0x41040 +``` + +**With Ghidra Running:** +You simply include Ghidra in your workflow like normal, it happens automatically behind the scenes! +Here is the exact step-by-step flow: +1. Double-click your target `.dll` or `.exe` and load it into your Ghidra Project. +2. Open it in the **CodeBrowser** and let auto-analysis finish (the progress bar in the bottom right). +3. Ensure you have started the `ghidra_bridge_server.py` script from the Script Manager as described in Section 1. +4. Leave Ghidra open on your screen. +5. In your Command Prompt window, run the harness generator tool just like you would without Ghidra: + ```cmd + python winafl-harness-builder.py generate target.dll --offset 0x41040 + ``` +6. The python script will notice the Bridge Server is listening, log "Ghidra Bridge connected", and bypass dumpbin entirely to extract its data directly from Ghidra! + +--- + +## 3. What Does Ghidra Actually Do? + +When Ghidra is connected to the harness builder, it performs **High-Fidelity Analysis** rather than simple assembly parsing. Here is exactly what the MCP integration unlocks: + +### A. Perfectly Accurate Function Signatures +Without Ghidra, the script guesses how many arguments a function takes by counting assembly registers. +**With Ghidra**, the script queries the exact decompiled signature. +* *Example Dumpbin:* `typedef int (*TargetFunc)(void*, void*);` +* *Example Ghidra:* `typedef HRESULT (WINAPI *TargetFunc)(HANDLE hFile, LPCWSTR pwszFormat, DWORD dwFlags);` + +This means the generated C code in `harness.c` is guaranteed to compile and successfully pass data into the target without crashing the stack. + +### B. Auto-Detecting the Harness Style +WinAFL harnesses need to wrap the target function differently depending on what the target expects. +* **File Parser Style:** If Ghidra detects the target's arguments are `const char* filename` or `LPCWSTR path`, the script automatically generates a harness that passes the mutated fuzzer file path. +* **Stream Style:** If Ghidra sees the target asks for a `HANDLE` or `FILE*`, the script generates a harness that *opens the file for you*, passes the handle, and strictly closes it afterward (crucial to avoid WinAFL handle exhaustion). +* **Buffer Style:** If Ghidra sees `uint8_t* buffer` and `size_t length`, the script automatically builds a harness that allocates memory, reads the mutated file into RAM, and passes the buffer. + +### C. Deep Behavioral Call Graphs +Instead of scanning 50 lines of blind assembly for a `CreateFileA` call, Ghidra builds a complete call graph. +The script queries Ghidra to see if the target function *eventually* calls a file read or memory allocation downstream. +It analyzes the **Decompiled C Code** directly for high-risk behaviors: +* Does it allocate memory and fail to free it? (The script warns you to add cleanup logic to the harness). +* Does it call `ExitProcess` or `abort`? (The script throws a fatal error because WinAFL cannot fuzz functions that self-terminate the application). + +### D. Finding Hidden Parsing Logic +Instead of just looking at exported function names, the MCP extension allows the `winafl-target-finder.py` to scan the internal decompiler logic for string references. +If the script finds a function manipulating strings like `%PDF-1.4` or `PK\x03\x04` (ZIP headers), it immediately flags that offset as a high-value parsing target, even if the function is completely unnamed! + +--- + +## 4. Example: The Ghidra-Powered Workflow + +1. You load a massive DLL like `libmedia.dll` into Ghidra and let it auto-analyze. +2. You start the Ghidra RPC Bridge script. +3. You run `python winafl-target-finder.py analyze libmedia.dll` in your terminal. +4. The Python script reaches into Ghidra, scans the decompiler for everything mentioning `fopen`, calculates the exact parameters of the 10 best un-exported parsing functions, and scores them. +5. You pipe the best offset into the harness builder. +6. The harness builder queries Ghidra, realizes the target needs a wide-string path and an initialization flag, writes the C code, compiles it, and launches the fuzzer. + +By combining the speed of the CLI Python tools with the brain of Ghidra, you automate the entire reverse engineering and harnessing bottleneck of Windows fuzzing! diff --git a/GPU_Changes.md b/GPU_Changes.md new file mode 100644 index 0000000..0d53bee --- /dev/null +++ b/GPU_Changes.md @@ -0,0 +1,108 @@ +# WinAFL CUDA GPU Acceleration - Technical Architecture & Change Summary + +**Author:** Elias Ibrahim `` +**Date:** February 2026 +**Target Repository:** `google/winafl` +**Feature Title:** Hardware-Accelerated GPU Havoc Mutation Engine (Tier 1 & Tier 2) + +--- + +## 1. Executive Summary & Rationale + +WinAFL traditional fuzzing spends significant CPU cycles sequentially generating mutations in the `havoc_stage` loop (`fuzz_one`) on a single CPU core before invoking target execution. + +This contribution introduces **NVIDIA CUDA GPU Acceleration** to WinAFL: +- **Tier 1 (GPU Havoc Mutator):** Seed mutations (bitflips, byte additions/subtractions, block mutations, dictionary splicing) are offloaded to thousands of parallel CUDA threads on the GPU. +- **Asynchronous Double-Buffering:** Uses DMA page-locked host memory (`cudaMallocHost`) and dual CUDA streams (`cudaMemcpyAsync`) so the CPU evaluates testcases from Batch $N$ while the GPU concurrently mutates Batch $N+1$. +- **Hardware Entropy (`cuRand`):** Integrates per-thread CUDA `curandState` generators for lock-free parallel entropy. +- **Coalesced VRAM Access:** Transposes the global mutation matrix to guarantee 100% warp memory coalescing. +- **Tier 2 (GPU Target Execution Harness Template):** Included in `experimental/gpu_harness_template.cu` for pure-logic targets running entirely in VRAM with `atomicOr()` shared memory coverage. + +--- + +## 2. Architecture & Performance Pipeline + +### Asynchronous Memory Pipeline (Double Buffering) +``` + CPU Execution Loop (Batch N) + ┌────────────────────────────────────────────────────────┐ + │ common_fuzz_stuff() -> Target Execution (DynamoRIO) │ + └────────────────────────────────────────────────────────┘ + │ + (Concurrent Execution) + ▼ + GPU CUDA Pipeline (Batch N+1) + ┌────────────────────────────────────────────────────────┐ + │ 1. cudaMemcpyAsync (Host Pinned -> Device VRAM) │ + │ 2. mutation_kernel<<>> │ + │ 3. cudaMemcpyAsync (Device VRAM -> Host Pinned) │ + └────────────────────────────────────────────────────────┘ +``` + +### Key Optimizations Implemented: +1. **DMA Page-Locked Memory (`cudaMallocHost`):** Prevents double-bouncing through page-table lookups, achieving maximum PCIe bus throughput. +2. **CUDA Streams (`cudaStream_t`):** Dual stream execution allows overlapping host CPU target evaluation with GPU VRAM mutation generation. +3. **Warp Memory Coalescing:** Memory is structured as `[mutation_index * batch_capacity + thread_id]` so adjacent threads write to adjacent bytes in VRAM. +4. **Singleton Manager (`afl-fuzz-gpu.cu`):** Encapsulates all CUDA context and stream allocations cleanly without cluttering existing AFL C structures. +5. **Graceful Degradation:** If compiled without CUDA support or executed on hardware without an NVIDIA GPU, WinAFL falls back to standard CPU fuzzing without breaking existing DynamoRIO, Intel PT, or TinyInst workflows. + +--- + +## 3. Command Line & Configuration Options + +### Command Line Flags +| Flag | Short | Description | +| :--- | :--- | :--- | +| `-G` | `-G` / `/gpu` | Enables GPU mutation offloading in `afl-fuzz.exe`. | + +*Note: For backward compatibility with scripts, `/gpu` and `-gpu` are automatically mapped to `-G` internally.* + +### Environment Variables +| Variable | Default | Description | +| :--- | :--- | :--- | +| `AFL_GPU_BATCH_SIZE` | `10000` | Sets the number of parallel GPU thread allocations per batch buffer. | + +--- + +## 4. Itemized File Manifest + +### Added Core CUDA Feature Files +- **`afl-fuzz-gpu.cu`**: CUDA device kernel implementations, double-buffer allocation, `cuRand` initialization, memory transposition. +- **`afl-fuzz-gpu.h`**: Header declaring C interface (`gpu_init()`, `gpu_mutate_batch_async()`, `gpu_sync()`, `gpu_get_mutation()`). +- **`experimental/gpu_harness_template.cu`**: Tier 2 GPU-native target execution template with `atomicOr()` shared coverage bitmap. + +### Added Documentation & Tooling +- **`SETUP_AND_RUN.md`**: Environment setup guide for CUDA 11+/12+/13+, CMake 3.18+, and Visual Studio 2019/2022. +- **`FUZZING_GUIDE.md`**: End-to-end fuzzing campaign guide. +- **`winafl-target-finder.py`**: Automated target binary scanning & harness launch configuration generator. +- **`winafl-harness-builder.py`**: Automated C/C++ fuzzer harness code generator. +- **`ghidra_bridge.py`**: Protocol-agnostic Ghidra MCP server bridge client. +- **`GHIDRA_INTEGRATION.md`** & **`GHIDRA_MCP_INTEGRATION_GUIDE.md`**: Ghidra integration guides. +- **`WIN_AFL_ZERO_TO_HERO_GUIDE.md`**: Fuzzing campaign guide. + +### Modified Files in Upstream WinAFL +- **`CMakeLists.txt`**: Upgraded CMake to 3.18+, added `check_language(CUDA)` & `enable_language(CUDA)` check, defined `-DHAVE_CUDA`, and conditionally appended `afl-fuzz-gpu.cu` to `AFL_FUZZ_SOURCES`. +- **`afl-fuzz.c`**: Added `-G` / `/gpu` CLI flags, `AFL_GPU_BATCH_SIZE` env var, async havoc stage loop in `fuzz_one()`, and GPU status UI (`gpu batches`, `gpu execs`). +- **`README.md`**: Added GPU Acceleration usage section, flag details, and tool descriptions. +- **`ChangeLog`**: Added Version 1.17 (Feb 2026) release entry with author attribution. + +--- + +## 5. Build & Verification Instructions + +### Requirements +- Windows 10/11 x64 +- Visual Studio 2019 or 2022 (Desktop C++ Workload) +- NVIDIA CUDA Toolkit 11.0 or higher +- CMake 3.18+ + +### Build Commands +```cmd +cmake -G "Visual Studio 17 2022" -A x64 . +cmake --build . --config Release +``` + +### Execution Example +```cmd +bin\Release\afl-fuzz.exe -G -i in -o out -t 1000 -D C:\path\to\DynamoRIO\bin64 -- target.exe @@ +``` diff --git a/README.md b/README.md index d774fc2..6b00018 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Windows fork written and maintained by Ivan Fratric + GPU acceleration added by Elias Ibrahim (Feb 2026) + Copyright 2016 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); @@ -71,7 +73,7 @@ WinAFL has been successfully used to identify bugs in Windows software, such as | Microsoft | [CVE-2018-8464](https://cpr-zero.checkpoint.com/vulns/cprid-2098/) | Yoav Alon ([@yoavalon](https://twitter.com/yoavalon)) and Netanel Ben-Simon ([@NetanelBenSimon](https://twitter.com/netanelbensimon)) of Check Point Research | Microsoft | [CVE-2019-0538](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0538), [CVE-2019-0576](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0576), [CVE-2019-0577](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0577), [CVE-2019-0579](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0579), [CVE-2019-0580](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0580), [CVE-2019-0879](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0879), [CVE-2019-0889](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0889), [CVE-2019-0891](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0891), [CVE-2019-0899](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0899), [CVE-2019-0902](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-0902), [CVE-2019-1243](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-1243), [CVE-2019-1250](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2019-1250), [CVE-2020-0687](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-0687), [CVE-2020-0744](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-0744), [CVE-2020-0879](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-0879), [CVE-2020-0964](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-0964), [CVE-2020-0995](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-0995), [CVE-2020-1141](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1141), [CVE-2020-1145](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1145), [CVE-2020-1160](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1160), [CVE-2020-1179](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1179), [CVE-2021-1665](https://www.mcafee.com/blogs/other-blogs/mcafee-labs/analyzing-cve-2021-1665-remote-code-execution-vulnerability-in-windows-gdi/) | Hardik Shah ([@hardik05](https://twitter.com/hardik05)) of McAfee | Microsoft | [CVE-2021-42276](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-42276), [CVE-2021-28350](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-28350), [CVE-2021-28349](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-28349), [CVE-2021-28348](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-28348) | Simon Barsky ([expend20](https://twitter.com/expend20)) -| Microsoft | [CVE-2022-21903](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21903), [CVE-2022-21904](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_setdibitstodevice-record/), [CVE-2022-21915](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_stretchdibits-record/), [CVE-2022-26934](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_createdibpatternbrushpt-record/), [CVE-2022-29112](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_bitblt-record/), [CVE-2022-35837](https://www.seljan.hu/posts/arbitrary-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_startdoc-record/), [CVE-2022-34728](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_setpixelv-record/), [CVE-2022-38006](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_stretchdibits-record-again/), [CVE-2025-30388](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-30388), [CVE-2025-47984](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-47984), [CVE-2025-53766](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53766) | [Gábor Selján](https://twitter.com/GaborSeljan) +| Microsoft | [CVE-2022-21903](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21903), [CVE-2022-21904](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_setdibitstodevice-record/), [CVE-2022-21915](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_stretchdibits-record/), [CVE-2022-26934](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_createdibpatternbrushpt-record/), [CVE-2022-29112](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_bitblt-record/), [CVE-2022-35837](https://www.seljan.hu/posts/arbitrary-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_startdoc-record/), [CVE-2022-34728](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_setpixelv-record/), [CVE-2022-38006](https://www.seljan.hu/posts/out-of-bounds-read-information-disclosure-vulnerability-in-microsoft-windows-gdi-emr_stretchdibits-record-again/) | [Gábor Selján](https://twitter.com/GaborSeljan) | Microsoft | [CVE-2021-38665](https://thalium.github.io/blog/posts/leaking-aslr-through-rdp-printer-cache-registry/), [CVE-2021-38666](https://thalium.github.io/blog/posts/deserialization-bug-through-rdp-smart-card-extension/) | Valentino Ricotta with Thalium | Microsoft | [CVE-2022-26929](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-26929), [CVE-2022-30130](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-30130) | Eran Zimmerman Gonen ([@3r4nz](https://twitter.com/3r4nz)) | FreeRDP | [CVE-2021-37594](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-37594), [CVE-2021-37595](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-37595) | Valentino Ricotta with Thalium @@ -82,7 +84,6 @@ WinAFL has been successfully used to identify bugs in Windows software, such as | XnView | [CVE-2019-13083](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x384e2a.md), [CVE-2019-13084](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x26b739.md), [CVE-2019-13085](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x30ecfa.md), [CVE-2019-13253](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x0000000000385474.md), [CVE-2019-13254](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x000000000032e808.md), [CVE-2019-13255](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x0000000000327464.md), [CVE-2019-13256](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x000000000032e849.md), [CVE-2019-13257](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x00000000003273aa.md), [CVE-2019-13258](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x0000000000328165.md), [CVE-2019-13259](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x000000000032e566.md), [CVE-2019-13260](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x0000000000327a07.md), [CVE-2019-13261](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x0000000000328384.md), [CVE-2019-13262](https://github.com/apriorit/pentesting/blob/master/bugs/xnview/0x00000000003283eb.md) | [@expend20](https://twitter.com/expend20) and Anton Kukoba of Apriorit | IrfanView | [CVE-2019-13242](https://github.com/apriorit/pentesting/blob/master/bugs/irfanview/0x0000000000013a98.md), [CVE-2019-13243](https://github.com/apriorit/pentesting/blob/master/bugs/irfanview/0x00000000000249c6.md) | [@expend20](https://twitter.com/expend20) and Anton Kukoba of Apriorit | FastStone | [CVE-2019-13244](https://github.com/apriorit/pentesting/blob/master/bugs/fsview/0x0000000000002d7d.md), [CVE-2019-13245](https://github.com/apriorit/pentesting/blob/master/bugs/fsview/0x00000000001a95b1.md), [CVE-2019-13246](https://github.com/apriorit/pentesting/blob/master/bugs/fsview/0x00000000001a9601.md) | [@expend20](https://twitter.com/expend20) and Anton Kukoba of Apriorit -| FastStone | [CVE-2024-9112](https://www.zerodayinitiative.com/advisories/ZDI-24-1273/), [CVE-2024-9113](https://www.zerodayinitiative.com/advisories/ZDI-24-1274/), [CVE-2024-9114](https://www.zerodayinitiative.com/advisories/ZDI-24-1275/) | [@zeze7w](https://x.com/zeze7w) with TeamT5 | ACDSee | [CVE-2019-13247](https://github.com/apriorit/pentesting/blob/master/bugs/acdsee/0x00000000000024ed.md), [CVE-2019-13248](https://github.com/apriorit/pentesting/blob/master/bugs/acdsee/0x0000000000002450.md), [CVE-2019-13249](https://github.com/apriorit/pentesting/blob/master/bugs/acdsee/0x00000000000b9e7a.md), [CVE-2019-13250](https://github.com/apriorit/pentesting/blob/master/bugs/acdsee/0x00000000000b9c2f.md), [CVE-2019-13251](https://github.com/apriorit/pentesting/blob/master/bugs/acdsee/0x00000000000c47ff.md), [CVE-2019-13252](https://github.com/apriorit/pentesting/blob/master/bugs/acdsee/0x00000000001172b0.md), [CVE-2019-15293](https://www.apriorit.com/dev-blog/640-qa-fuzzing-for-closed-source-windows-software) | [@expend20](https://twitter.com/expend20) and Anton Kukoba of Apriorit | Foxit | [CVE-2019-13330](https://www.zerodayinitiative.com/advisories/ZDI-19-853/), [CVE-2019-13331](https://www.zerodayinitiative.com/advisories/ZDI-19-854/), [CVE-2020-8844](https://www.zerodayinitiative.com/advisories/ZDI-20-200/) | Natnael Samson ([@NattiSamson](https://twitter.com/NattiSamson)) | Rockwell Automation | [CVE-2020-12034, CVE-2020-12038](https://www.us-cert.gov/ics/advisories/icsa-20-140-01) | [Sharon Brizinov](https://sharonbrizinov.com/) and Amir Preminger of Claroty @@ -232,6 +233,23 @@ Before using WinAFL for the first time, you should read the documentation for the specific instrumentation mode you are interested in. These also contain usage examples. +## GPU Acceleration (CUDA) + +WinAFL includes high-performance NVIDIA CUDA GPU Acceleration for offloading seed havoc mutations. When activated with `-G` (or `/gpu`), WinAFL offloads heavy mutation generation across parallel GPU CUDA threads using double-buffered asynchronous memory streams. + +- **Enable GPU Mode:** Pass the `-G` (or `/gpu`) flag to `afl-fuzz.exe`. +- **Batch Size:** Configure parallel GPU thread allocation via `AFL_GPU_BATCH_SIZE` environment variable (default: 10,000 threads). +- **Double-Buffered Asynchronous Memory Streams:** Uses page-locked DMA memory (`cudaMallocHost`) and `cudaMemcpyAsync` so the CPU evaluates testcases from Batch $N$ while the GPU concurrently mutates Batch $N+1$. +- **NVIDIA CuRand Integration:** Hardware-native random generation per CUDA thread (`curandState`). +- **Memory Coalescing:** Transposed VRAM matrix for 100% memory bus efficiency across warps. + +For detailed setup, compilation, and campaign workflows, see [SETUP_AND_RUN.md](file:///d:/Dev/winafl/SETUP_AND_RUN.md), [FUZZING_GUIDE.md](file:///d:/Dev/winafl/FUZZING_GUIDE.md), and [GPU_Changes.md](file:///d:/Dev/winafl/GPU_Changes.md). + +### Automated Target Analysis & Harness Generation Utilities +- `winafl-target-finder.py`: Automated scanning of Windows executables/DLLs to identify target functions, build seed corpora, and generate launch configurations. +- `winafl-harness-builder.py`: Automated C/C++ fuzzer harness code generator. +- `ghidra_bridge.py`: Protocol-agnostic client for Ghidra MCP servers to perform automated disassembling and RVA offset discovery. See [GHIDRA_INTEGRATION.md](file:///d:/Dev/winafl/GHIDRA_INTEGRATION.md). + ## Attaching to a running process The DynamoRIO instrumentation mode supports dynamically attaching to running processes. This option can be used to fuzz processes that cannot be directly launched by WinAFL, such as system services. diff --git a/SETUP_AND_RUN.md b/SETUP_AND_RUN.md new file mode 100644 index 0000000..8c99cef --- /dev/null +++ b/SETUP_AND_RUN.md @@ -0,0 +1,50 @@ +# WinAFL CUDA - Environment Setup & Execution + +*Contributed by Elias Ibrahim (Feb 2026)* + +Use this guide to set up build tools, IDEs, and run GPU-accelerated WinAFL. + +## 1. Prerequisites +- **OS:** Windows 10 or Windows 11 (x64) +- **Compiler:** [Visual Studio 2019 or 2022](https://visualstudio.microsoft.com/) + - Ensure you install the **"Desktop development with C++"** workload. +- **CUDA:** [NVIDIA CUDA Toolkit 11.0+](https://developer.nvidia.com/cuda-downloads) (e.g. 11.x, 12.x, or 13.x). +- **CMake:** [CMake 3.18+](https://cmake.org/download/) (Ensure it is added to your system `PATH`). + +## 2. Setting Up & Building + +### Option A: Visual Studio Code (Recommended) +1. Install [VS Code](https://code.visualstudio.com/). +2. Install the **C/C++ Extension Pack** and **CMake Tools** extensions. +3. Open the `winafl` folder in VS Code. +4. VS Code will prompt you to select a "Compiler Kit". Select the 64-bit version (e.g., `Visual Studio Community 2022 Release - amd64`). +5. Open the Command Palette (`Ctrl+Shift+P`), type `CMake: Configure` and select it. +6. Once configured, type `CMake: Build` to build the `afl-fuzz` executable. + +### Option B: Visual Studio Command Prompt +1. Open the **"x64 Native Tools Command Prompt for VS 2019/2022"**. +2. Navigate to the `winafl` source directory. +3. Run the following commands to configure and build: + ```cmd + cmake -G "Visual Studio 17 2022" -A x64 . + cmake --build . --config Release + ``` +4. The compiled executables will be written to `bin\Release\`. + +## 3. Running the Fuzzer with GPU Acceleration + +1. Ensure you have an `in` folder containing at least one valid starting seed (e.g., `in/seed.txt`). +2. Have a target executable compiled and ready (e.g., `bin\Release\test.exe`). +3. Start the fuzzer by appending the `-G` (or `/gpu`) flag: + ```cmd + bin\Release\afl-fuzz.exe -G -i in -o out -t 1000 -D C:\path\to\DynamoRIO\bin64 -- bin\Release\test.exe @@ + ``` + +### Verification +- On launch, the console outputs GPU initialization details: + ```text + [*] Initializing GPU acceleration (Batch Size: 10000)... + [+] GPU initialized successfully + ``` +- On the `afl-fuzz` TUI dashboard, the status line will display: + `gpu status : ONLINE batches=12 execs=120000` diff --git a/WIN_AFL_ZERO_TO_HERO_GUIDE.md b/WIN_AFL_ZERO_TO_HERO_GUIDE.md new file mode 100644 index 0000000..8bab0a5 --- /dev/null +++ b/WIN_AFL_ZERO_TO_HERO_GUIDE.md @@ -0,0 +1,138 @@ +# WinAFL GPU: Zero to Hero Fuzzing Guide + +*Contributed by Elias Ibrahim (Feb 2026)* + +Welcome to the definitive guide for harnessing closed-source Windows binaries with **WinAFL GPU**. If you've never fuzzed a program before, this guide is designed exactly for you. + +Traditionally, fuzzing a closed-source Windows program (where you don't have the original source code) was an incredibly painful process. You had to reverse-engineer the binary, manually hunt for internal "parsing" functions, figure out how to call them safely, write C++ wrappings from scratch, and manually compile corpora. + +To solve this, we've built a suite of **intelligent Python utilities** that completely automate the hardest parts of the pipeline: finding the target, writing the code, and optimizing the fuzzing loop. + +Let's go from zero to crashing a target in 5 phases using the WinAFL Tool Suite. + +--- + +## What is Fuzzing? + +**Fuzzing** is simply feeding a computer program mangled, mutated, or randomized data (the "fuzz") and seeing if it crashes. If the program crashes, you've likely found a memory corruption bug (like a buffer overflow) that a hacker could turn into a security exploit. + +To fuzz a program fast (millions of times a minute), we don't open its GUI and click buttons. Instead, we write a **Harness**. A harness is a tiny snippet of code that loads the specific parsing function of the target program (e.g., `LoadImage_Internal()`), passes it mutated bytes, and closes it, looping endlessly. + +Here is how our tools build that harness for you. + +--- + +## Phase 1: Finding a Target (`winafl-target-finder.py`) + +You have a folder full of `.dll` and `.exe` files, but you don't know which ones are vulnerable or even capable of being fuzzed. + +**The Tool:** `winafl-target-finder.py scan` +This tool mathematically scores Windows binaries from 0 to 100 on their "fuzzability". It looks for binaries that read files, allocate memory, and manipulate strings (the holy trinity of software bugs). + +**The Command:** +```cmd +python winafl-target-finder.py scan "C:\Program Files\VulnerableApp" +``` + +**What happens?** +The tool will scan the directory and print a ranked leaderboard. It might tell you that `image_decoder.dll` scored a 95/100 because it exports a function named `parse_file` and calls `fread()` and `VirtualAlloc()`. You have your target! + +--- + +## Phase 2: Generating the Harness (`winafl-harness-builder.py`) + +Now that you know `image_decoder.dll` is your target, you need a C++ wrapper to loop its parsing function. + +**The Tool:** `winafl-harness-builder.py` +This tool acts as a robotic reverse-engineer. It disassembles the target, counts its arguments, figures out if it wants a file path or a memory buffer, checks for safety (does it call `ExitProcess`?), and generates the perfect C harness file automatically. + +**The Command:** +Use the pipeline to feed the finder directly into the builder: +```cmd +python winafl-target-finder.py analyze image_decoder.dll --offset 0x1A40 | python winafl-harness-builder.py --pipe > harness.c +``` +*(You can get the offset of interesting functions from the Phase 1 target finder's deep analysis).* + +**What happens?** +You will instantly get a `harness.c` file written for you. It automatically contains the code to `LoadLibrary` your DLL, resolve the function, and safely execute it in a `__try/__except` block. + +**Compile it:** +Open a Visual Studio developer command prompt: +```cmd +cl.exe /nologo /W3 /O2 harness.c /link /OUT:harness.exe +``` + +--- + +## Phase 3: Building a Quality Seed Corpus (`seeds` & `winafl-cmin.py`) + +A fuzzer needs examples of *valid* files to start with. If it's fuzzing an image decoder, it needs valid images to mutate. A folder of these starting files is called the **Corpus**. + +**The Tool:** `winafl-target-finder.py seeds` +This command automatically rummages through your Windows system looking for tiny, valid files of the format you request. + +**The Command:** +```cmd +python winafl-target-finder.py seeds png .\corpus_in +``` + +### Minimizing the Corpus +If you feed the fuzzer 50 images that are essentially identical (e.g., just different colors), the fuzzer wastes time. We want a small corpus where *every file triggers different code logic*. + +**The Tool:** `winafl-cmin.py` (Corpus Minimizer) +This runs every seed through the target using DynamoRIO to map its execution path. If two seeds hit the exact same lines of code, one is deleted. + +**The Command:** +```cmd +python winafl-cmin.py -D C:\DynamoRIO\bin32 -t 10000 -i .\corpus_in -o .\corpus_minimized -target_module harness.exe -target_method fuzz_target -nargs 2 -- harness.exe @@ +``` +Now you have a hyper-optimized, deduplicated starting point in `.\corpus_minimized`. + +--- + +## Phase 4: Launching the Campaign + +It's time to unleash WinAFL. + +Usually, the WinAFL command line is horribly complex to type by hand. Let's ask our tool to generate the exact command string for us! + +**The Tool:** `winafl-target-finder.py generate` + +**The Command:** +```cmd +python winafl-target-finder.py generate image_decoder.dll 0x1A40 -i .\corpus_minimized -o .\fuzz_out -D C:\DynamoRIO\bin32 +``` + +Copy the command it spits out and run it! The WinAFL dashboard will appear, and your executions-per-second counter will start flying as it mutates files and executes `harness.exe`. + +--- + +## Phase 5: Monitoring Success (`whatsup` & `plot`) + +If you leave your fuzzer running overnight, you want to know exactly what it achieved without deciphering raw logs. + +**The Tool:** `winafl-whatsup.py` +This prints a clean summary of your fuzzing campaign, including total runtime, average speeds, and how many unique crashes it discovered. + +**The Command:** +```cmd +python winafl-whatsup.py .\fuzz_out +``` + +**The Tool:** `winafl-plot.py` +If you want to show off your progress in a report, this tool generates beautiful HTML/Gnuplot graphs showing coverage growth, execution speeds, and crash frequency over time. + +**The Command:** +```cmd +python winafl-plot.py .\fuzz_out .\graphs_dir +``` + +--- + +## Summary of the Automatic Workflow +1. **Find Target**: `winafl-target-finder.py scan` +2. **Build Harness**: `... target-finder.py analyze ... | winafl-harness-builder.py` -> `cl.exe harness.c` +3. **Gather Seeds**: `winafl-target-finder.py seeds png in_dir` +4. **Minimize**: `winafl-cmin.py -i in_dir -o min_dir ...` +5. **Fuzz**: Execute the generated `afl-fuzz.exe` command! +6. **Profit**: Check `winafl-whatsup.py` for crashes and `winafl-plot.py` for charts. diff --git a/afl-fuzz-gpu.cu b/afl-fuzz-gpu.cu new file mode 100644 index 0000000..820ed8e --- /dev/null +++ b/afl-fuzz-gpu.cu @@ -0,0 +1,305 @@ +/* + WinAFL - CUDA GPU Mutation Offloading Core Implementation + -------------------------------------------------------- + + Original AFL code written by Michal Zalewski + Windows fork written and maintained by Ivan Fratric + CUDA GPU acceleration written and contributed by Elias Ibrahim (Feb 2026) + + Copyright 2016, 2026 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include +#include +#include + +#include "afl-fuzz-gpu.h" + +typedef struct { + u8* d_seed; + u8* d_output; + u8* h_seed; + u8* h_output; + cudaStream_t stream; + u32 current_seed_len; + u32 current_batch_size; +} GPUBatch; + +typedef struct { + int initialized; + size_t max_file_sz; + size_t batch_capacity; + GPUBatch buffers[2]; + curandState* d_curand_states[2]; +} GPUContext; + +static GPUContext g_ctx = {0}; + +__global__ void setup_kernel(curandState* state, unsigned long seed, int max_threads) { + int id = threadIdx.x + blockIdx.x * blockDim.x; + if (id < max_threads) { + curand_init(seed, id, 0, &state[id]); + } +} + +#define GET_BYTE(out, batch_idx, batch_sz, byte_idx) (out[(byte_idx) * (batch_sz) + (batch_idx)]) + +// Simple mutation kernel +__global__ void mutate_kernel(u8* seed, u32 seed_len, u8* output_batch, size_t max_file_sz, u32 batch_size, curandState* global_state) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= batch_size) return; + + curandState local_state = global_state[idx]; + + // Copy seed directly to transposed layout + for (int i = 0; i < seed_len; i++) { + GET_BYTE(output_batch, idx, batch_size, i) = seed[i]; + } + + // Mutate + // 1. Bit flip + int num_flips = (curand(&local_state) % 4) + 1; + for(int i=0; i 4 && curand(&local_state) % 5 == 0) { + u32 del_len = (curand(&local_state) % (seed_len / 2)) + 1; + u32 del_pos = curand(&local_state) % (seed_len - del_len); + for (int i = del_pos; i < seed_len - del_len; i++) { + GET_BYTE(output_batch, idx, batch_size, i) = GET_BYTE(output_batch, idx, batch_size, i + del_len); + } + for(int i = seed_len - del_len; i < seed_len; i++) { + GET_BYTE(output_batch, idx, batch_size, i) = 0; + } + } + + // 4. Byte overlap + if (seed_len > 2 && curand(&local_state) % 4 == 0) { + u32 copy_from = curand(&local_state) % seed_len; + u32 copy_to = curand(&local_state) % seed_len; + u32 copy_len = (curand(&local_state) % 4) + 1; + if (copy_from + copy_len <= seed_len && copy_to + copy_len <= seed_len) { + for(int i=0; i= 0 && parsed_device < count) { + gpu_device = parsed_device; + } + } + + GPU_SAFE(cudaSetDevice(gpu_device)); + + // Default max file size for GPU buffers. This can be overridden. + size_t seed_buf_sz = 64 * 1024; // 64KB default (much safer pinned-memory footprint) + char* max_file_env = getenv("AFL_GPU_MAX_FILE_SIZE"); + if (max_file_env) { + size_t parsed_max = (size_t)atoll(max_file_env); + if (parsed_max >= 4096) { + seed_buf_sz = parsed_max; + } + } + g_ctx.max_file_sz = seed_buf_sz; + + // Dynamic batching by VRAM with an explicit pinned-memory budget cap. + cudaDeviceProp prop; + GPU_SAFE(cudaGetDeviceProperties(&prop, gpu_device)); + size_t vram = prop.totalGlobalMem; + size_t dyn_batch = (size_t)(0.25 * vram) / (2 * g_ctx.max_file_sz + 256); + + size_t pinned_budget_mb = 256; + char* pinned_budget_env = getenv("AFL_GPU_PINNED_MB"); + if (pinned_budget_env) { + size_t parsed_budget = (size_t)atoll(pinned_budget_env); + if (parsed_budget >= 32) { + pinned_budget_mb = parsed_budget; + } + } + size_t capacity_by_budget = ((pinned_budget_mb * 1024ULL * 1024ULL) / g_ctx.max_file_sz); + + g_ctx.batch_capacity = dyn_batch; + if (capacity_by_budget > 0 && g_ctx.batch_capacity > capacity_by_budget) { + g_ctx.batch_capacity = capacity_by_budget; + } + if (g_ctx.batch_capacity < 1) g_ctx.batch_capacity = 1; + + // Optional explicit init capacity cap. + char* init_cap_env = getenv("AFL_GPU_INIT_CAPACITY"); + if (init_cap_env) { + size_t parsed_cap = (size_t)atoll(init_cap_env); + if (parsed_cap >= 1 && g_ctx.batch_capacity > parsed_cap) { + g_ctx.batch_capacity = parsed_cap; + } + } + + // Final hard safety cap. + if (g_ctx.batch_capacity > 131072) g_ctx.batch_capacity = 131072; + + size_t total_sz = g_ctx.max_file_sz * g_ctx.batch_capacity; + + for (int i = 0; i < 2; i++) { + // Allocate streams + GPU_SAFE(cudaStreamCreate(&g_ctx.buffers[i].stream)); + + // Allocate device memory + GPU_SAFE(cudaMalloc(&g_ctx.buffers[i].d_seed, seed_buf_sz)); + GPU_SAFE(cudaMalloc(&g_ctx.buffers[i].d_output, total_sz)); + + // Allocate pinned host memory + GPU_SAFE(cudaMallocHost(&g_ctx.buffers[i].h_seed, seed_buf_sz)); + GPU_SAFE(cudaMallocHost(&g_ctx.buffers[i].h_output, total_sz)); + } + + g_ctx.initialized = 1; + + // Allocate curand states per buffer to avoid race conditions + for (int i = 0; i < 2; i++) { + GPU_SAFE(cudaMalloc(&g_ctx.d_curand_states[i], g_ctx.batch_capacity * sizeof(curandState))); + + int threads = 256; + int blocks = (g_ctx.batch_capacity + threads - 1) / threads; + setup_kernel<<>>(g_ctx.d_curand_states[i], time(NULL) + i, g_ctx.batch_capacity); + } + GPU_SAFE(cudaDeviceSynchronize()); + + fprintf(stderr, "[GPU] Initialized Double Buffering & CuRand.\n[GPU] Device: %d\n[GPU] Hardware: %s (VRAM: %zu MB)\n[GPU] Max seed size: %zu bytes\n[GPU] Dynamic Batch Capacity: %zu per buffer\n", + gpu_device, prop.name, vram / (1024 * 1024), g_ctx.max_file_sz, g_ctx.batch_capacity); + return 0; +} + +extern "C" int gpu_is_available() { + return g_ctx.initialized; +} + +extern "C" int gpu_mutate_batch_async(int buffer_id, u8* seed, u32 seed_len, u32 batch_size) { + if (!g_ctx.initialized || buffer_id < 0 || buffer_id > 1) return 1; + if (seed_len == 0 || seed_len > g_ctx.max_file_sz) { + fprintf(stderr, "[GPU] Seed too large for configured GPU buffer (%u > %zu). Increase AFL_GPU_MAX_FILE_SIZE.\n", seed_len, g_ctx.max_file_sz); + return 1; + } + if (batch_size > g_ctx.batch_capacity) batch_size = g_ctx.batch_capacity; + + GPUBatch* b = &g_ctx.buffers[buffer_id]; + b->current_seed_len = seed_len; + b->current_batch_size = batch_size; + + // Copy seed to pinned host memory (fast CPU copy) + for (size_t i = 0; i < seed_len; ++i) { + b->h_seed[i] = seed[i]; + } + + // Async copy seed to device + GPU_SAFE(cudaMemcpyAsync(b->d_seed, b->h_seed, seed_len, cudaMemcpyHostToDevice, b->stream)); + + // Launch kernel async with per-buffer curand states + int threads = 256; + int blocks = (batch_size + threads - 1) / threads; + mutate_kernel<<stream>>>(b->d_seed, seed_len, b->d_output, g_ctx.max_file_sz, batch_size, g_ctx.d_curand_states[buffer_id]); + + // Async copy only the bytes we actually need (transposed layout: seed_len * batch_size) + size_t copy_size = (size_t)seed_len * batch_size; + GPU_SAFE(cudaMemcpyAsync(b->h_output, b->d_output, copy_size, cudaMemcpyDeviceToHost, b->stream)); + + cudaError_t err = cudaGetLastError(); + if(err != cudaSuccess) { + fprintf(stderr, "[GPU] Kernel Launch FAIL: %s\n", cudaGetErrorString(err)); + return 1; + } + return 0; +} + +extern "C" int gpu_sync(int buffer_id) { + if (!g_ctx.initialized || buffer_id < 0 || buffer_id > 1) return 1; + GPUBatch* b = &g_ctx.buffers[buffer_id]; + GPU_SAFE(cudaStreamSynchronize(b->stream)); + return 0; +} + +extern "C" size_t gpu_get_mutation(int buffer_id, u32 index, u8* out_buf, u32 max_len) { + if (!g_ctx.initialized || buffer_id < 0 || buffer_id > 1) return 0; + GPUBatch* b = &g_ctx.buffers[buffer_id]; + + // We assume gpu_sync(buffer_id) has been called beforehand. + size_t len_to_copy = b->current_seed_len; + if (len_to_copy > max_len) len_to_copy = max_len; + + u8* host_ptr = b->h_output; + for (size_t i = 0; i < len_to_copy; ++i) { + out_buf[i] = host_ptr[i * b->current_batch_size + index]; + } + + return len_to_copy; +} + +extern "C" void gpu_cleanup() { + if (!g_ctx.initialized) return; + + for (int i = 0; i < 2; i++) { + if (g_ctx.buffers[i].d_seed) cudaFree(g_ctx.buffers[i].d_seed); + if (g_ctx.buffers[i].d_output) cudaFree(g_ctx.buffers[i].d_output); + if (g_ctx.buffers[i].h_seed) cudaFreeHost(g_ctx.buffers[i].h_seed); + if (g_ctx.buffers[i].h_output) cudaFreeHost(g_ctx.buffers[i].h_output); + cudaStreamDestroy(g_ctx.buffers[i].stream); + if (g_ctx.d_curand_states[i]) cudaFree(g_ctx.d_curand_states[i]); + } + + g_ctx.initialized = 0; + fprintf(stderr, "[GPU] Resources cleaned up.\n"); +} diff --git a/afl-fuzz-gpu.h b/afl-fuzz-gpu.h new file mode 100644 index 0000000..8202c07 --- /dev/null +++ b/afl-fuzz-gpu.h @@ -0,0 +1,56 @@ +/* + WinAFL - CUDA GPU Acceleration Header + ------------------------------------- + + Original AFL code written by Michal Zalewski + Windows fork written and maintained by Ivan Fratric + CUDA GPU acceleration written and contributed by Elias Ibrahim (Feb 2026) + + Copyright 2016, 2026 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef AFL_FUZZ_GPU_H +#define AFL_FUZZ_GPU_H + +#include "types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Initialize GPU (allocating pinned memory and streams). +int gpu_init(); + +// Check if GPU is ready/available. +int gpu_is_available(); + +// Launch mutation kernel asynchronously. +// buffer_id should be 0 or 1 for double buffering. +int gpu_mutate_batch_async(int buffer_id, u8* seed, u32 seed_len, u32 batch_size); + +// Synchronize a specific buffer's stream, ensuring GPU to Host memory transfer is finished. +int gpu_sync(int buffer_id); + +// Retrieve a specific mutated testcase from the batch (reads from pinned host memory). +size_t gpu_get_mutation(int buffer_id, u32 index, u8* out_buf, u32 max_len); + +// Release all GPU resources (VRAM, streams, curand states). +void gpu_cleanup(); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/afl-fuzz.c b/afl-fuzz.c index 88db5d5..59737ad 100644 --- a/afl-fuzz.c +++ b/afl-fuzz.c @@ -6,6 +6,8 @@ Windows fork written and maintained by Ivan Fratric + CUDA GPU acceleration contributed by Elias Ibrahim (Feb 2026) + Copyright 2016 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,6 +59,16 @@ #include #include #include +#ifdef HAVE_CUDA +/* CUDA GPU Acceleration support contributed by Elias Ibrahim (Feb 2026) */ +#include "afl-fuzz-gpu.h" +#endif + +#ifdef USE_GPU_SANDBOX +#include "winafl_integration.h" +#endif + + #include #include #include @@ -78,6 +90,18 @@ void tinyinst_killtarget(); /* Lots of globals, but mostly for the status UI and other things where it really makes no sense to haul them around as function parameters. */ BOOL use_sample_shared_memory = FALSE; +#ifdef HAVE_CUDA +u8 use_gpu = 0; +// GPUBatch gpu_batch - Removed, using singleton +u32 gpu_batch_size = 10000; +u64 gpu_batches_submitted = 0; +u64 gpu_total_execs = 0; /* Total mutations generated by GPU */ +#endif + +#ifdef USE_GPU_SANDBOX +u8 use_gpu_sandbox = 0; +#endif + static u8 *in_dir, /* Input directory with test cases */ *out_file, /* File to fuzz, if any */ *out_dir, /* Working & output directory */ @@ -4662,6 +4686,17 @@ static void show_stats(void) { SAYF(bSTG bV bSTOP " total paths : " cRST "%-4s " bSTG bV "\n", DI(queued_paths)); +#ifdef HAVE_CUDA + if (use_gpu) { + SAYF(bSTG bV bSTOP " gpu status : " cLGN "ONLINE" cRST + " batches=%-6s execs=%-10s " bSTG bV "\n", + DI(gpu_batches_submitted), DI(gpu_total_execs)); + } else { + SAYF(bSTG bV bSTOP " gpu status : " cLRD "OFFLINE" cRST + " (add -G flag to enable GPU mutations) " bSTG bV "\n"); + } +#endif + /* Highlight crashes in red if found, denote going over the KEEP_UNIQUE_CRASH limit with a '+' appended to the count. */ @@ -6659,6 +6694,9 @@ static u8 fuzz_one(char** argv) { stage_cur_byte = -1; + /* GPU havoc block removed: was using stage_max before it was set */ + + /* The havoc stage mutation code is also invoked when splicing files; if the splice_cycle variable is set, generate different descriptions and such. */ @@ -6692,7 +6730,51 @@ static u8 fuzz_one(char** argv) { /* We essentially just do several thousand runs (depending on perf_score) where we take the input file and make random stacked tweaks. */ - for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { + stage_cur = 0; +#ifdef HAVE_CUDA + if (use_gpu && gpu_is_available() && stage_max > 200) { + u32 gpu_ops = stage_max; + size_t total_computed = 0; + int current_buf = 0; + u32 batch_sz = (gpu_ops > gpu_batch_size) ? gpu_batch_size : gpu_ops; + + // Kick off first batch on buffer 0 + if (gpu_mutate_batch_async(current_buf, out_buf, len, batch_sz) == 0) { + + while (total_computed < gpu_ops) { + u32 remaining = gpu_ops - (u32)total_computed; + u32 current_batch = (remaining > gpu_batch_size) ? gpu_batch_size : remaining; + + u32 next_remaining = remaining - current_batch; + u32 next_batch = (next_remaining > gpu_batch_size) ? gpu_batch_size : next_remaining; + + int next_buf = 1 - current_buf; + if (next_batch > 0) { + gpu_mutate_batch_async(next_buf, out_buf, len, next_batch); + } + + // Wait for current buffer's GPU->Host transfer + gpu_sync(current_buf); + + for (u32 k = 0; k < current_batch; k++) { + size_t m_len = gpu_get_mutation(current_buf, k, out_buf, MAX_FILE); + if (m_len > 0) { + if (common_fuzz_stuff(argv, out_buf, m_len)) goto abandon_entry; + } + stage_cur++; + } + + total_computed += current_batch; + gpu_batches_submitted++; + gpu_total_execs += current_batch; + current_buf = next_buf; + } + memcpy(out_buf, in_buf, temp_len); + } + } +#endif + + for (; stage_cur < stage_max; stage_cur++) { u32 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2)); @@ -7497,7 +7579,8 @@ static void usage(u8* argv0) { " -f file - location read by the fuzzed program (stdin)\n" " -m limit - memory limit for the target process\n" " -p - persist DynamoRIO cache across target process restarts\n" - " -c cpu - the CPU to run the fuzzed program\n\n" + " -c cpu - the CPU to run the fuzzed program\n" + " -G - enable GPU acceleration (WinAFL CUDA)\n\n" "Fuzzing behavior settings:\n\n" @@ -8192,7 +8275,14 @@ int main(int argc, char** argv) { client_params = NULL; winafl_dll_path = NULL; - while ((opt = getopt(argc, argv, "+i:o:f:m:t:I:T:sdyYnCB:S:M:x:QD:b:l:pPc:w:A:eV")) > 0) + /* Rewrite -gpu or /gpu to -G to support the requested flag style. */ + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-gpu") || !strcmp(argv[i], "/gpu")) { + argv[i] = "-G"; + } + } + + while ((opt = getopt(argc, argv, "+i:o:f:m:t:I:T:sdyYnCB:S:M:x:QD:b:l:pPc:w:A:eVGX")) > 0) switch (opt) { case 's': @@ -8202,6 +8292,23 @@ int main(int argc, char** argv) { ACTF("using shared memory mode..."); break; + case 'G': +#ifdef HAVE_CUDA + use_gpu = 1; + break; +#else + FATAL("System not configured for GPU acceleration"); +#endif + + case 'X': +#ifdef USE_GPU_SANDBOX + use_gpu_sandbox = 1; + break; +#else + FATAL("System not configured for GPU Sandbox"); +#endif + + case 'i': if (in_dir) FATAL("Multiple -i options not supported"); @@ -8464,7 +8571,11 @@ int main(int argc, char** argv) { } +#ifdef USE_GPU_SANDBOX + if (!in_dir || !out_dir || !timeout_given || (!drioless && !dynamorio_dir && !use_intelpt && !use_tinyinst && !use_gpu_sandbox)) usage(argv[0]); +#else if (!in_dir || !out_dir || !timeout_given || (!drioless && !dynamorio_dir && !use_intelpt && !use_tinyinst)) usage(argv[0]); +#endif if (!winafl_dll_path) { winafl_dll_path = "winafl.dll"; @@ -8475,6 +8586,23 @@ int main(int argc, char** argv) { setup_signal_handlers(); check_asan_opts(); +#ifdef HAVE_CUDA + if (use_gpu) { + char* gpu_batch_env = getenv("AFL_GPU_BATCH_SIZE"); + if (gpu_batch_env) { + gpu_batch_size = atoi(gpu_batch_env); + if (gpu_batch_size < 1) gpu_batch_size = 10000; + } + ACTF("Initializing GPU acceleration (Batch Size: %u)...", gpu_batch_size); + ACTF("GPU support added by Elias Ibrahim "); + if (gpu_init() != 0) { + FATAL("GPU initialization failed"); + } + OKF("GPU initialized successfully"); + } +#endif + + if (sync_id) fix_up_sync(); if (use_intelpt) { @@ -8492,7 +8620,13 @@ int main(int argc, char** argv) { optind += tinyinst_options; #endif } else { +#ifdef USE_GPU_SANDBOX + if (!use_gpu_sandbox) { + extract_client_params(argc, argv); + } +#else extract_client_params(argc, argv); +#endif } optind++; @@ -8561,6 +8695,8 @@ int main(int argc, char** argv) { read_testcases(); load_auto(); + /* GPU init moved to guarded block above (only when -G is specified) */ + pivot_inputs(); if (extras_dir) load_extras(extras_dir); @@ -8580,9 +8716,17 @@ int main(int argc, char** argv) { else use_argv = argv + optind; +#ifdef USE_GPU_SANDBOX + if (!use_gpu_sandbox) { + perform_dry_run(use_argv); + + cull_queue(); + } +#else perform_dry_run(use_argv); cull_queue(); +#endif show_init_stats(); @@ -8597,6 +8741,12 @@ int main(int argc, char** argv) { /* Woop woop woop */ +#ifdef USE_GPU_SANDBOX + if (use_gpu_sandbox) { + ACTF("Entering GPU Sandbox mode..."); + sandbox_fuzz_loop(in_dir, out_dir, NULL); + } else { +#endif while (1) { u8 skipped_fuzz; @@ -8661,6 +8811,10 @@ int main(int argc, char** argv) { write_stats_file(0, 0, 0); save_auto(); +#ifdef USE_GPU_SANDBOX + } /* end else (use_gpu_sandbox) */ +#endif + stop_fuzzing: SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing %s +++\n" cRST, diff --git a/experimental/gpu_harness_template.cu b/experimental/gpu_harness_template.cu new file mode 100644 index 0000000..f08dbe5 --- /dev/null +++ b/experimental/gpu_harness_template.cu @@ -0,0 +1,102 @@ +/* + * gpu_harness_template.cu + * + * WinAFL - Tier 2 GPU Fuzzing Template + * Original AFL code written by Michal Zalewski + * Windows fork written and maintained by Ivan Fratric + * CUDA GPU acceleration written and contributed by Elias Ibrahim (Feb 2026) + * Copyright 2016, 2026 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * GOAL: + * Instead of running the target function on the CPU (one by one), we run thousands + * of instances of the target function in parallel on the GPU. + * + * REQUIREMENTS: + * 1. The target logic must be "pure" (no syscalls, no file I/O, no network). + * 2. The target must be recompilable with `nvcc`. + * + * USAGE: + * 1. Copy your target function logic into `target_func_gpu`. + * 2. Use this as a standalone high-speed fuzzer interacting with AFL via shared mem or pipes. + */ + +#include +#include + +#define MAX_INPUT_LEN 1024 +#define MAP_SIZE_BITS 65536 +#define MAP_SIZE_INTS (MAP_SIZE_BITS / 32) + +// -------------------------------------------------------------------------------- +// USER TARGET LOGIC HERE +// -------------------------------------------------------------------------------- + +__device__ void record_cov(unsigned int* shared_map, u32 edge_id) { + // Highly efficient atomic OR into block-level shared memory + atomicOr(&shared_map[edge_id / 32], 1 << (edge_id % 32)); +} + +// Example generic target function: parses a "packet" +__device__ void target_func_gpu(u8* buf, int len, unsigned int* shared_map, u32* crash_flag) { + // Simple state machine or parser logic + if (len < 4) return; + + // Check magic bytes + if (buf[0] == 'P' && buf[1] == 'K') { + record_cov(shared_map, 0); + + if (buf[2] == 0x01) { + record_cov(shared_map, 1); + + if (buf[3] == 0xFF) { + // CRASH! (simulated) + atomicExch(crash_flag, 1); // Report crash immediately + } + } + } +} + +// -------------------------------------------------------------------------------- +// KERNEL +// -------------------------------------------------------------------------------- + +__global__ void fuzz_kernel(u8* inputs, int* lengths, u32 batch_size, unsigned int* global_coverage_map, u32* global_crash_flag) { + // Allocate highly efficient shared memory for the coverage map + // All threads in the block will contribute to this shared map before persisting to VRAM + __shared__ unsigned int shared_bitmap[MAP_SIZE_INTS]; + + // Initialize shared bitmap to 0 + for(int i = threadIdx.x; i < MAP_SIZE_INTS; i += blockDim.x) { + shared_bitmap[i] = 0; + } + __syncthreads(); + + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < batch_size) { + u8* my_input = &inputs[idx * MAX_INPUT_LEN]; + int my_len = lengths[idx]; + + target_func_gpu(my_input, my_len, shared_bitmap, global_crash_flag); + } + + __syncthreads(); + + // Merge block's shared bitmap into the global device bitmap + for(int i = threadIdx.x; i < MAP_SIZE_INTS; i += blockDim.x) { + if (shared_bitmap[i] != 0) { + atomicOr(&global_coverage_map[i], shared_bitmap[i]); + } + } +} + +// -------------------------------------------------------------------------------- +// HOST RUNNER (Example) +// -------------------------------------------------------------------------------- + +int main() { + printf("[GPU Fuzzer] This is a template. Integrate this logic into your specific harness.\n"); + return 0; +} diff --git a/ghidra_bridge.py b/ghidra_bridge.py new file mode 100644 index 0000000..75704db --- /dev/null +++ b/ghidra_bridge.py @@ -0,0 +1,753 @@ +#!/usr/bin/env python3 +""" +Ghidra Bridge — Protocol-agnostic client for Ghidra MCP servers. +Contributed by Elias Ibrahim (Feb 2026) + +Supports: + - GhydraMCP (starsong-consulting) — HATEOAS REST API [PRIMARY] + - GhidraMCP (LaurieWired) — HTTP API + - GhidrAssistMCP (jtang613) — HTTP API + +Usage: + from ghidra_bridge import GhidraClient + + ghidra = GhidraClient("localhost", 8192) + if ghidra.connect(): + funcs = ghidra.list_functions(limit=50) + code = ghidra.decompile("GdipLoadImageFromFile") + graph = ghidra.get_callgraph("main", depth=3) + +CLI test: + python ghidra_bridge.py --test localhost:8192 + python ghidra_bridge.py --list-functions localhost:8192 + python ghidra_bridge.py --decompile main localhost:8192 +""" + +import sys +import io +import json +import argparse +import textwrap +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from urllib.parse import quote, urlencode + +# Force UTF-8 on Windows (guard against double-wrapping when imported) +if sys.platform == "win32": + if not isinstance(sys.stdout, io.TextIOWrapper) or sys.stdout.encoding.lower() != 'utf-8': + try: + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + except (AttributeError, ValueError): + pass + if not isinstance(sys.stderr, io.TextIOWrapper) or sys.stderr.encoding.lower() != 'utf-8': + try: + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + except (AttributeError, ValueError): + pass + + +# ============================================================================ +# Data Classes +# ============================================================================ + +class FunctionInfo: + """Represents a function from Ghidra analysis.""" + __slots__ = ("name", "address", "signature", "size", "entry_point", + "calling_convention", "return_type", "parameters", "is_thunk") + + def __init__(self, **kwargs): + for k in self.__slots__: + setattr(self, k, kwargs.get(k)) + + def __repr__(self): + return f"Function({self.name} @ 0x{self.address or '?'}, sig={self.signature})" + + def to_dict(self): + return {k: getattr(self, k) for k in self.__slots__} + + +class XRef: + """Cross-reference.""" + __slots__ = ("from_addr", "to_addr", "ref_type", "from_func", "to_func") + + def __init__(self, **kwargs): + for k in self.__slots__: + setattr(self, k, kwargs.get(k)) + + def to_dict(self): + return {k: getattr(self, k) for k in self.__slots__} + + +class Variable: + """Function variable.""" + __slots__ = ("name", "data_type", "storage", "size", "is_parameter") + + def __init__(self, **kwargs): + for k in self.__slots__: + setattr(self, k, kwargs.get(k)) + + def to_dict(self): + return {k: getattr(self, k) for k in self.__slots__} + + +# ============================================================================ +# Ghidra HTTP Client (GhydraMCP REST API) +# ============================================================================ + +class GhidraClient: + """ + Protocol-agnostic Ghidra client. + Primary backend: GhydraMCP HATEOAS REST API. + Fallback: raw HTTP to GhidraMCP or GhidrAssistMCP. + """ + + def __init__(self, host="localhost", port=8192, timeout=30): + self.host = host + self.port = port + self.timeout = timeout + self.base_url = f"http://{host}:{port}" + self.connected = False + self.server_type = None # "ghydra", "ghidramcp", "ghidrassist" + self.program_name = None + + # ---------------------------------------------------------------- + # HTTP helpers + # ---------------------------------------------------------------- + + def _get(self, path, params=None): + """Send GET request, return parsed JSON or None.""" + url = f"{self.base_url}{path}" + if params: + url += "?" + urlencode(params) + try: + req = Request(url, headers={"Accept": "application/json"}) + with urlopen(req, timeout=self.timeout) as resp: + data = resp.read().decode("utf-8") + return json.loads(data) + except (URLError, HTTPError, json.JSONDecodeError, TimeoutError) as e: + return None + + def _post(self, path, body=None): + """Send POST request with JSON body.""" + url = f"{self.base_url}{path}" + data = json.dumps(body or {}).encode("utf-8") + try: + req = Request(url, data=data, method="POST", + headers={"Content-Type": "application/json", + "Accept": "application/json"}) + with urlopen(req, timeout=self.timeout) as resp: + return json.loads(resp.read().decode("utf-8")) + except (URLError, HTTPError, json.JSONDecodeError) as e: + return None + + def _extract_result(self, response): + """Extract the 'result' field from a GhydraMCP HATEOAS response.""" + if response is None: + return None + if isinstance(response, dict): + # GhydraMCP format: {"success": true, "result": ...} + if "result" in response: + return response["result"] + # GhidraMCP format: direct result + return response + return response + + # ---------------------------------------------------------------- + # Connection & Discovery + # ---------------------------------------------------------------- + + def connect(self): + """Auto-detect server type and verify connection.""" + # Try GhydraMCP first (has /api/instances) + resp = self._get("/api/instances") + if resp is not None: + self.server_type = "ghydra" + self.connected = True + result = self._extract_result(resp) + if isinstance(result, list) and result: + self.program_name = result[0].get("program", None) + return True + + # Try GhidraMCP / GhidrAssistMCP (simpler APIs) + resp = self._get("/methods") + if resp is not None: + self.server_type = "ghidramcp" + self.connected = True + return True + + # Try GhidrAssistMCP + resp = self._get("/") + if resp is not None: + self.server_type = "ghidrassist" + self.connected = True + return True + + return False + + def list_instances(self): + """List active Ghidra instances (GhydraMCP only).""" + resp = self._get("/api/instances") + return self._extract_result(resp) or [] + + def get_program_info(self): + """Get info about the currently loaded program.""" + if self.server_type == "ghydra": + resp = self._get("/api/project") + return self._extract_result(resp) + else: + resp = self._get("/program/info") + return resp + + # ---------------------------------------------------------------- + # Function Analysis + # ---------------------------------------------------------------- + + def list_functions(self, offset=0, limit=100, filter_name=None): + """List functions in the binary.""" + if self.server_type == "ghydra": + params = {"offset": offset, "limit": limit} + resp = self._get("/api/functions", params) + result = self._extract_result(resp) + else: + resp = self._get("/methods") + result = resp + + if not result: + return [] + + funcs = [] + if isinstance(result, list): + for item in result: + if isinstance(item, dict): + f = FunctionInfo( + name=item.get("name", ""), + address=item.get("address", item.get("entryPoint", "")), + signature=item.get("signature", item.get("prototype", "")), + size=item.get("size", 0), + entry_point=item.get("entryPoint", item.get("address", "")), + is_thunk=item.get("isThunk", False), + ) + if filter_name and filter_name.lower() not in (f.name or "").lower(): + continue + funcs.append(f) + elif isinstance(item, str): + # GhidraMCP returns list of strings + f = FunctionInfo(name=item) + if filter_name and filter_name.lower() not in item.lower(): + continue + funcs.append(f) + + return funcs + + def get_function(self, name_or_addr): + """Get detailed info about a single function.""" + if self.server_type == "ghydra": + resp = self._get(f"/api/functions/{quote(str(name_or_addr))}") + result = self._extract_result(resp) + if result and isinstance(result, dict): + return FunctionInfo( + name=result.get("name", ""), + address=result.get("address", result.get("entryPoint", "")), + signature=result.get("signature", result.get("prototype", "")), + size=result.get("size", 0), + entry_point=result.get("entryPoint", ""), + calling_convention=result.get("callingConvention", ""), + return_type=result.get("returnType", ""), + parameters=result.get("parameters", []), + is_thunk=result.get("isThunk", False), + ) + return None + + def decompile(self, name_or_addr, style="default", syntax_tree=False): + """Decompile a function to C pseudocode.""" + if self.server_type == "ghydra": + params = {} + if style != "default": + params["style"] = style + if syntax_tree: + params["syntax_tree"] = "true" + resp = self._get(f"/api/functions/{quote(str(name_or_addr))}/decompile", params) + result = self._extract_result(resp) + if isinstance(result, dict): + return result.get("c_code", result.get("decompilation", str(result))) + return result + else: + # GhidraMCP + resp = self._get(f"/decompile/{quote(str(name_or_addr))}") + if isinstance(resp, dict): + return resp.get("decompilation", resp.get("c_code", str(resp))) + return resp + + def disassemble(self, name_or_addr): + """Get disassembly of a function.""" + if self.server_type == "ghydra": + resp = self._get(f"/api/functions/{quote(str(name_or_addr))}/disassemble") + return self._extract_result(resp) + else: + resp = self._get(f"/disassemble/{quote(str(name_or_addr))}") + return resp + + def get_function_variables(self, name_or_addr): + """Get function parameters and local variables.""" + if self.server_type == "ghydra": + resp = self._get(f"/api/functions/{quote(str(name_or_addr))}/variables") + result = self._extract_result(resp) + if not result: + return [] + variables = [] + if isinstance(result, list): + for v in result: + variables.append(Variable( + name=v.get("name", ""), + data_type=v.get("dataType", v.get("type", "")), + storage=v.get("storage", ""), + size=v.get("size", 0), + is_parameter=v.get("isParameter", False), + )) + return variables + return [] + + # ---------------------------------------------------------------- + # Analysis — Call Graph, Data Flow, XRefs + # ---------------------------------------------------------------- + + def get_callgraph(self, name_or_addr, max_depth=3): + """Get call graph for a function.""" + if self.server_type == "ghydra": + params = {"max_depth": max_depth} + if name_or_addr.startswith("0x") or name_or_addr.startswith("0X"): + params["address"] = name_or_addr + else: + params["name"] = name_or_addr + resp = self._get("/api/analysis/callgraph", params) + return self._extract_result(resp) + return None + + def get_dataflow(self, address, direction="forward", max_steps=50): + """Perform data flow analysis (GhydraMCP only).""" + if self.server_type == "ghydra": + params = {"address": address, "direction": direction, "max_steps": max_steps} + resp = self._get("/api/analysis/dataflow", params) + return self._extract_result(resp) + return None + + def get_xrefs(self, to_addr=None, from_addr=None, ref_type=None, limit=100): + """Get cross-references.""" + if self.server_type == "ghydra": + params = {"limit": limit} + if to_addr: + params["to_addr"] = to_addr + if from_addr: + params["from_addr"] = from_addr + if ref_type: + params["type"] = ref_type + resp = self._get("/api/xrefs", params) + result = self._extract_result(resp) + if not result: + return [] + xrefs = [] + if isinstance(result, list): + for x in result: + xrefs.append(XRef( + from_addr=x.get("fromAddress", ""), + to_addr=x.get("toAddress", ""), + ref_type=x.get("refType", x.get("type", "")), + from_func=x.get("fromFunction", ""), + to_func=x.get("toFunction", ""), + )) + return xrefs + return [] + + # ---------------------------------------------------------------- + # Imports, Exports, Strings + # ---------------------------------------------------------------- + + def list_imports(self): + """List imported functions.""" + if self.server_type == "ghydra": + all_imports = [] + offset = 0 + while True: + resp = self._get("/api/imports", {"offset": offset, "limit": 200}) + result = self._extract_result(resp) + if not result or not isinstance(result, list): + break + all_imports.extend(result) + if len(result) < 200: + break + offset += 200 + return all_imports + else: + resp = self._get("/imports") + return resp if isinstance(resp, list) else [] + + def list_exports(self): + """List exported functions.""" + if self.server_type == "ghydra": + all_exports = [] + offset = 0 + while True: + resp = self._get("/api/exports", {"offset": offset, "limit": 200}) + result = self._extract_result(resp) + if not result or not isinstance(result, list): + break + all_exports.extend(result) + if len(result) < 200: + break + offset += 200 + return all_exports + else: + resp = self._get("/exports") + return resp if isinstance(resp, list) else [] + + def list_strings(self, filter_text=None, limit=200): + """List defined strings in the binary.""" + if self.server_type == "ghydra": + params = {"limit": limit} + if filter_text: + params["filter"] = filter_text + resp = self._get("/api/data/strings", params) + return self._extract_result(resp) or [] + return [] + + # ---------------------------------------------------------------- + # Convenience — Fuzzing-Specific Queries + # ---------------------------------------------------------------- + + def find_file_io_callers(self): + """ + Find all functions that call file I/O APIs (CreateFileW, fopen, etc.). + Returns a list of (function_name, function_addr, api_called) tuples. + """ + file_apis = [ + "CreateFileA", "CreateFileW", "CreateFile2", + "fopen", "_wfopen", "_open", + "ReadFile", "fread", + ] + + callers = [] + imports = self.list_imports() + if not imports: + return callers + + for imp in imports: + imp_name = imp.get("name", "") if isinstance(imp, dict) else str(imp) + if imp_name not in file_apis: + continue + + imp_addr = imp.get("address", "") if isinstance(imp, dict) else "" + if imp_addr: + xrefs = self.get_xrefs(to_addr=imp_addr) + for xref in xrefs: + callers.append({ + "function": xref.from_func, + "address": xref.from_addr, + "api_called": imp_name, + }) + + return callers + + def find_fuzz_candidates(self, max_depth=2): + """ + Automated fuzzing target discovery using Ghidra analysis. + Combines export analysis, file I/O xrefs, and call graph tracing. + """ + candidates = [] + seen = set() + + # Strategy 1: Exported functions with parser-like names + exports = self.list_exports() + parser_kw = ["parse", "read", "load", "open", "decode", "process", + "import", "extract", "from_file", "fromfile", "from_stream", + "fromstream", "create_from", "createfrom", "init_from"] + for exp in exports: + name = exp.get("name", "") if isinstance(exp, dict) else str(exp) + addr = exp.get("address", "") if isinstance(exp, dict) else "" + name_lower = name.lower() + for kw in parser_kw: + if kw in name_lower: + if addr not in seen: + seen.add(addr) + candidates.append({ + "name": name, + "address": addr, + "score": 30, + "reasons": [f"Export name contains '{kw}'"], + "source": "ghidra_export", + }) + break + + # Strategy 2: Functions that call file I/O + callers = self.find_file_io_callers() + for c in callers: + addr = c["address"] + if addr not in seen: + seen.add(addr) + candidates.append({ + "name": c["function"], + "address": addr, + "score": 40, + "reasons": [f"Calls {c['api_called']} (file I/O)"], + "source": "ghidra_xref", + }) + else: + # Boost score for already-seen + for cand in candidates: + if cand["address"] == addr: + cand["score"] += 10 + cand["reasons"].append(f"Also calls {c['api_called']}") + break + + # Strategy 3: Deep decompile top candidates to check for CloseHandle + for cand in sorted(candidates, key=lambda x: x["score"], reverse=True)[:10]: + name = cand["name"] + if not name: + continue + code = self.decompile(name) + if code: + if "CloseHandle" in code or "fclose" in code: + cand["score"] += 20 + cand["reasons"].append("Closes file handles (WinAFL compatible)") + if "ExitProcess" in code or "TerminateProcess" in code: + cand["score"] -= 50 + cand["reasons"].append("FATAL: Calls ExitProcess") + if "malloc" in code or "HeapAlloc" in code: + cand["reasons"].append("Allocates memory") + + candidates.sort(key=lambda x: x["score"], reverse=True) + return candidates + + def get_function_signature_c(self, name_or_addr): + """ + Get the C function signature (return type + params) for harness typedef. + Returns a string like 'int __stdcall FuncName(LPCWSTR param1, int param2)'. + """ + func = self.get_function(name_or_addr) + if func and func.signature: + return func.signature + + # Fallback: try decompilation and extract first line + code = self.decompile(name_or_addr) + if code: + for line in code.splitlines(): + line = line.strip() + if line and not line.startswith("/*") and not line.startswith("//"): + if "(" in line and ")" in line: + return line.rstrip("{").strip() + break + return None + + +# ============================================================================ +# CLI +# ============================================================================ + +def main(): + parser = argparse.ArgumentParser( + prog="ghidra_bridge", + description="Ghidra Bridge — Test connection and query Ghidra MCP servers", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent("""\ + Examples: + %(prog)s --test localhost:8192 + %(prog)s --list-functions localhost:8192 + %(prog)s --list-functions localhost:8192 --filter CreateFile + %(prog)s --decompile GdipLoadImageFromFile localhost:8192 + %(prog)s --callgraph main localhost:8192 --depth 3 + %(prog)s --xrefs-to 0x00401000 localhost:8192 + %(prog)s --find-fuzz-targets localhost:8192 + %(prog)s --signature GdipLoadImageFromFile localhost:8192 + """) + ) + + parser.add_argument("server", nargs="?", default="localhost:8192", + help="Ghidra server host:port (default: localhost:8192)") + + group = parser.add_mutually_exclusive_group() + group.add_argument("--test", action="store_true", help="Test connection") + group.add_argument("--list-functions", action="store_true", help="List functions") + group.add_argument("--list-exports", action="store_true", help="List exports") + group.add_argument("--list-imports", action="store_true", help="List imports") + group.add_argument("--decompile", metavar="FUNC", help="Decompile a function") + group.add_argument("--disassemble", metavar="FUNC", help="Disassemble a function") + group.add_argument("--callgraph", metavar="FUNC", help="Get call graph") + group.add_argument("--xrefs-to", metavar="ADDR", help="Get xrefs TO an address") + group.add_argument("--xrefs-from", metavar="ADDR", help="Get xrefs FROM an address") + group.add_argument("--find-fuzz-targets", action="store_true", + help="Auto-discover fuzzing targets") + group.add_argument("--signature", metavar="FUNC", help="Get C function signature") + group.add_argument("--variables", metavar="FUNC", help="Get function variables") + + parser.add_argument("--filter", help="Filter pattern for list operations") + parser.add_argument("--depth", type=int, default=3, help="Call graph depth") + parser.add_argument("--limit", type=int, default=50, help="Result limit") + parser.add_argument("--json", action="store_true", help="Output as JSON") + + args = parser.parse_args() + + # Parse server address + if ":" in args.server: + host, port_str = args.server.rsplit(":", 1) + port = int(port_str) + else: + host, port = args.server, 8192 + + client = GhidraClient(host, port) + + # Test connection + if args.test or not any([args.list_functions, args.list_exports, args.list_imports, + args.decompile, args.disassemble, args.callgraph, + args.xrefs_to, args.xrefs_from, args.find_fuzz_targets, + args.signature, args.variables]): + print(f"\n Ghidra Bridge — Connection Test") + print(f" Server: {host}:{port}\n") + + if client.connect(): + print(f" [OK] Connected to {client.server_type} server") + info = client.get_program_info() + if info: + print(f" [OK] Program info: {json.dumps(info, indent=2)[:200]}") + instances = client.list_instances() + if instances: + print(f" [OK] Active instances: {len(instances)}") + funcs = client.list_functions(limit=5) + print(f" [OK] Sample functions: {len(funcs)}") + for f in funcs[:3]: + print(f" {f}") + else: + print(f" [!!] Cannot connect to Ghidra at {host}:{port}") + print(f" Ensure Ghidra is running with a GhydraMCP/GhidraMCP plugin.") + print(f" Default ports: GhydraMCP=8192, GhidraMCP=8080") + print() + if args.test: + return + + if not client.connected: + if not client.connect(): + print(f" [!!] Cannot connect to Ghidra at {host}:{port}") + return + + # Commands + if args.list_functions: + funcs = client.list_functions(limit=args.limit, filter_name=args.filter) + if args.json: + print(json.dumps([f.to_dict() for f in funcs], indent=2)) + else: + print(f"\n Functions ({len(funcs)}):\n") + for f in funcs: + print(f" {f.address or '?':>16s} {f.name}") + print() + + elif args.list_exports: + exports = client.list_exports() + if args.json: + print(json.dumps(exports, indent=2)) + else: + print(f"\n Exports ({len(exports)}):\n") + for exp in exports[:args.limit]: + name = exp.get("name", "") if isinstance(exp, dict) else str(exp) + addr = exp.get("address", "") if isinstance(exp, dict) else "" + print(f" {addr:>16s} {name}") + print() + + elif args.list_imports: + imports = client.list_imports() + if args.json: + print(json.dumps(imports, indent=2)) + else: + print(f"\n Imports ({len(imports)}):\n") + for imp in imports[:args.limit]: + name = imp.get("name", "") if isinstance(imp, dict) else str(imp) + print(f" {name}") + print() + + elif args.decompile: + code = client.decompile(args.decompile) + if code: + if args.json: + print(json.dumps({"function": args.decompile, "code": code})) + else: + print(f"\n Decompilation: {args.decompile}\n") + print(code) + else: + print(f" [!!] Could not decompile '{args.decompile}'") + + elif args.disassemble: + asm = client.disassemble(args.disassemble) + if asm: + if args.json: + print(json.dumps({"function": args.disassemble, "disassembly": asm})) + else: + print(f"\n Disassembly: {args.disassemble}\n") + if isinstance(asm, list): + for line in asm: + print(f" {line}") + else: + print(asm) + else: + print(f" [!!] Could not disassemble '{args.disassemble}'") + + elif args.callgraph: + graph = client.get_callgraph(args.callgraph, max_depth=args.depth) + if graph: + if args.json: + print(json.dumps(graph, indent=2)) + else: + print(f"\n Call Graph: {args.callgraph} (depth={args.depth})\n") + print(json.dumps(graph, indent=2)) + else: + print(f" [!!] Could not get call graph for '{args.callgraph}'") + + elif args.xrefs_to: + xrefs = client.get_xrefs(to_addr=args.xrefs_to, limit=args.limit) + if args.json: + print(json.dumps([x.to_dict() for x in xrefs], indent=2)) + else: + print(f"\n XRefs TO {args.xrefs_to} ({len(xrefs)}):\n") + for x in xrefs: + print(f" {x.from_addr} ({x.from_func or '?'}) -> {x.to_addr} [{x.ref_type}]") + print() + + elif args.xrefs_from: + xrefs = client.get_xrefs(from_addr=args.xrefs_from, limit=args.limit) + if args.json: + print(json.dumps([x.to_dict() for x in xrefs], indent=2)) + else: + print(f"\n XRefs FROM {args.xrefs_from} ({len(xrefs)}):\n") + for x in xrefs: + print(f" {x.from_addr} -> {x.to_addr} ({x.to_func or '?'}) [{x.ref_type}]") + print() + + elif args.find_fuzz_targets: + print(f"\n Auto-discovering fuzzing targets via Ghidra analysis...\n") + candidates = client.find_fuzz_candidates() + if args.json: + print(json.dumps(candidates, indent=2)) + else: + for i, c in enumerate(candidates[:20]): + print(f" #{i+1:2d} [Score: {c['score']:3d}] {c['address']:>16s} {c['name']}") + for r in c["reasons"]: + print(f" + {r}") + print() + + elif args.signature: + sig = client.get_function_signature_c(args.signature) + if sig: + print(f"\n Signature: {sig}\n") + else: + print(f" [!!] Could not get signature for '{args.signature}'") + + elif args.variables: + variables = client.get_function_variables(args.variables) + if args.json: + print(json.dumps([v.to_dict() for v in variables], indent=2)) + else: + print(f"\n Variables for {args.variables} ({len(variables)}):\n") + for v in variables: + param_tag = " [PARAM]" if v.is_parameter else "" + print(f" {v.data_type or '?':>20s} {v.name}{param_tag}") + print() + + +if __name__ == "__main__": + main() diff --git a/harness/harness_gdiplus.cpp b/harness/harness_gdiplus.cpp new file mode 100644 index 0000000..b969fce --- /dev/null +++ b/harness/harness_gdiplus.cpp @@ -0,0 +1,59 @@ +// WinAFL GDI+ Image Parser Harness +// Build: +// cl.exe /nologo /W3 /O2 /EHsc harness\harness_gdiplus.cpp /link gdiplus.lib /OUT:harness.exe +// Fuzz: +// build64\bin\Release\afl-fuzz.exe -i in -o out -D "d:\hacking\tools\DynamoRIO-Windows-11.3.0-1\bin64" +// -t 5000 -- -coverage_module gdiplus.dll -target_module harness.exe +// -target_method fuzz_target -fuzz_iterations 5000 -nargs 2 -- harness.exe @@ + +// Correct include order — objidl.h MUST come before gdiplus.h +#include +#include +#include +#pragma comment(lib, "gdiplus.lib") + +using namespace Gdiplus; + +static ULONG_PTR g_token; + +// GDI+ lifetime: initialized once per process, WinAFL loops fuzz_target not main +static void gdip_init() { + if (!g_token) { + GdiplusStartupInput si; + GdiplusStartup(&g_token, &si, NULL); + } +} + +extern "C" __declspec(dllexport) int fuzz_target(int argc, char** argv) { + if (argc < 2) return 1; + + // argv[1] is the mutated input file path + int n = MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, NULL, 0); + WCHAR* wp = (WCHAR*)HeapAlloc(GetProcessHeap(), 0, n * sizeof(WCHAR)); + if (!wp) return 1; + MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, wp, n); + + // Parse the image — target for fuzzing + Bitmap* bmp = new Bitmap(wp); + if (bmp) { + if (bmp->GetLastStatus() == Ok) { + // Force full decode: read pixel data + UINT w = bmp->GetWidth(), h = bmp->GetHeight(); + if (w > 0 && h > 0) { + BitmapData bd; + Rect r(0, 0, (INT)w, (INT)h); + if (bmp->LockBits(&r, ImageLockModeRead, PixelFormat32bppARGB, &bd) == Ok) + bmp->UnlockBits(&bd); + } + } + delete bmp; + } + + HeapFree(GetProcessHeap(), 0, wp); + return 0; +} + +int main(int argc, char** argv) { + gdip_init(); + return fuzz_target(argc, argv); +} diff --git a/harness/harness_gdiplus_41040.c b/harness/harness_gdiplus_41040.c new file mode 100644 index 0000000..15a5feb --- /dev/null +++ b/harness/harness_gdiplus_41040.c @@ -0,0 +1,70 @@ +/* + * WinAFL Harness Auto-generated by winafl-harness-builder + * + * Target: gdiplus.dll + * Offset: 0x41040 + * Style: dll_export + * Args: 2 + * + * Build: + * cl.exe /nologo /W3 /O2 harness.c /link /OUT:harness.exe + * + * Verify: + * drrun.exe -c winafl.dll -debug + * -target_module harness.exe -target_offset 0x + * -fuzz_iterations 10 -nargs 2 + * -- harness.exe input.bin + * + * Fuzz: + * afl-fuzz.exe -G -i in -o out -t 2000 + * -D -- + * -target_module harness.exe -target_offset 0x + * -coverage_module gdiplus.dll + * -fuzz_iterations 5000 -nargs 2 + * -- harness.exe @@ + */ +#include +#include + +#define TARGET_DLL "gdiplus.dll" + +/* Adjust the typedef to match the exported function's real signature. + * Use: dumpbin /exports gdiplus.dll + * Then look up the function in IDA/Ghidra to confirm args. */ +typedef int (WINAPI *TargetFunc)(const wchar_t* input_path); + +int fuzz_target(int argc, char** argv) { + if (argc < 2) return 1; + + static HMODULE hMod = NULL; + static TargetFunc pTarget = NULL; + + if (!hMod) { + hMod = LoadLibraryA(TARGET_DLL); + if (!hMod) { + fprintf(stderr, "[-] LoadLibrary failed: %lu\n", GetLastError()); + return 1; + } + pTarget = (TargetFunc)((BYTE*)hMod + 0x41040); + if (!pTarget) { + fprintf(stderr, "[-] Could not resolve target function\n"); + return 1; + } + fprintf(stderr, "[+] Target resolved at %p\n", pTarget); + } + + wchar_t wpath[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, argv[1], -1, wpath, MAX_PATH); + + __try { + pTarget(wpath); + } __except(EXCEPTION_EXECUTE_HANDLER) { + /* swallow */ + } + + return 0; +} + +int main(int argc, char** argv) { + return fuzz_target(argc, argv); +} diff --git a/winafl-harness-builder.py b/winafl-harness-builder.py new file mode 100644 index 0000000..8161a0e --- /dev/null +++ b/winafl-harness-builder.py @@ -0,0 +1,1486 @@ +#!/usr/bin/env python3 +""" +WinAFL Harness Builder - Automated Harness Generation & Deep Analysis Pipeline +Contributed by Elias Ibrahim (Feb 2026) + +Accepts piped JSON from winafl-target-finder.py or direct CLI arguments. +Performs deep binary analysis and generates ready-to-compile WinAFL harness code. + +Pipeline usage: + python winafl-target-finder.py harness target.dll | python winafl-harness-builder.py --pipe + python winafl-target-finder.py harness target.dll --json | python winafl-harness-builder.py --pipe + +Direct usage: + python winafl-harness-builder.py analyze target.dll --offset 0x41040 + python winafl-harness-builder.py generate target.dll --offset 0x41040 --style file_parser + python winafl-harness-builder.py validate target.dll --offset 0x41040 --drio C:\\DynamoRIO + python winafl-harness-builder.py full target.dll --offset 0x41040 --drio C:\\DynamoRIO --out harness\\ + +Requirements: + - Python 3.6+ + - dumpbin.exe (Visual Studio Build Tools) + - cl.exe (for --compile flag) + - DynamoRIO (for validate command) +""" + +import os +import sys +import re +import io +import json +import struct +import subprocess +import argparse +import glob +import textwrap +from pathlib import Path +from collections import defaultdict + +# Force UTF-8 on Windows (guard against double-wrapping when imported) +if sys.platform == "win32": + if not isinstance(sys.stdout, io.TextIOWrapper) or sys.stdout.encoding.lower() != 'utf-8': + try: + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + except (AttributeError, ValueError): + pass + if not isinstance(sys.stderr, io.TextIOWrapper) or sys.stderr.encoding.lower() != 'utf-8': + try: + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + except (AttributeError, ValueError): + pass + +# Ghidra bridge (optional — enhanced analysis when Ghidra MCP is available) +try: + from ghidra_bridge import GhidraClient + HAS_GHIDRA_BRIDGE = True +except ImportError: + HAS_GHIDRA_BRIDGE = False + GhidraClient = None + +# ============================================================================ +# Tool Resolution +# ============================================================================ + +def find_tool(name, extra_globs=None): + """Locate a build tool on the system.""" + try: + r = subprocess.run(["where", name], capture_output=True, text=True, timeout=5) + if r.returncode == 0: + return r.stdout.strip().splitlines()[0] + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + for pattern in (extra_globs or []): + hits = glob.glob(pattern) + if hits: + return hits[0] + return None + +DUMPBIN = find_tool("dumpbin", [ + r"C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe" +]) +CL = find_tool("cl", [ + r"C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC\*\bin\Hostx64\x64\cl.exe" +]) + + +# ============================================================================ +# Deep Binary Analyzer +# ============================================================================ + +class DeepAnalyzer: + """ + Performs deeper-than-export analysis on a target function: + - Argument count inference from prologue + - Call graph construction (what APIs does the function call?) + - Safety validation for WinAFL compatibility + - Calling convention detection + """ + + def __init__(self, binary_path, offset, arch="x64"): + self.binary = binary_path + self.offset = offset.lstrip("0x").lstrip("0X").upper() + self.arch = arch + self.module = os.path.basename(binary_path) + + # Analysis results + self.disasm_lines = [] + self.called_apis = [] + self.arg_count = 0 + self.calling_convention = "fastcall" if arch == "x64" else "stdcall" + self.opens_file = False + self.closes_file = False + self.calls_exit = False + self.reads_data = False + self.allocates_memory = False + self.function_size = 0 + self.is_safe = True + self.safety_warnings = [] + self.safety_errors = [] + + def run_all(self, max_instructions=500, ghidra=None): + """Run all analysis passes. If ghidra client is provided, use it.""" + if ghidra and ghidra.connected: + self._ghidra_enhance(ghidra) + else: + self._disassemble(max_instructions) + self._infer_args() + self._trace_calls() + self._validate_safety() + return self.to_dict() + + def _ghidra_enhance(self, ghidra): + """Use Ghidra for high-fidelity analysis instead of dumpbin.""" + addr = f"0x{self.offset}" + + # Get function info (signature, params) + func = ghidra.get_function(addr) + if func: + if func.signature: + self.ghidra_signature = func.signature + if func.calling_convention: + self.calling_convention = func.calling_convention + if func.parameters and isinstance(func.parameters, list): + self.arg_count = len(func.parameters) + self.ghidra_params = func.parameters + if func.size: + self.function_size = func.size + + # Decompile — much richer than dumpbin disassembly + code = ghidra.decompile(addr) + if code: + self.ghidra_decompiled = code + # Parse decompiled C for behavioral flags + for api in ["CreateFileA", "CreateFileW", "CreateFile2", "fopen", "_wfopen"]: + if api in code: + self.opens_file = True + self.called_apis.append({"name": api, "category": "file_open"}) + for api in ["ReadFile", "fread", "_read", "fgets"]: + if api in code: + self.reads_data = True + self.called_apis.append({"name": api, "category": "file_read"}) + for api in ["CloseHandle", "fclose", "_close"]: + if api in code: + self.closes_file = True + self.called_apis.append({"name": api, "category": "file_close"}) + for api in ["malloc", "calloc", "HeapAlloc", "VirtualAlloc"]: + if api in code: + self.allocates_memory = True + self.called_apis.append({"name": api, "category": "memory_alloc"}) + for api in ["ExitProcess", "TerminateProcess", "abort"]: + if api in code: + self.calls_exit = True + self.called_apis.append({"name": api, "category": "process_exit"}) + + # Call graph — trace deeper than single function + graph = ghidra.get_callgraph(addr, max_depth=2) + if graph: + self.ghidra_callgraph = graph + + # Get variables for parameter type info + variables = ghidra.get_function_variables(addr) + if variables: + self.ghidra_variables = variables + params = [v for v in variables if v.is_parameter] + if params: + self.arg_count = len(params) + self.ghidra_params = [ + {"name": v.name, "type": v.data_type, "storage": v.storage} + for v in params + ] + + def _disassemble(self, max_instructions): + """Extract disassembly around the target offset.""" + if not DUMPBIN: + self.safety_warnings.append("dumpbin not found — skipping disassembly analysis") + return + + try: + result = subprocess.run( + [DUMPBIN, "/disasm", self.binary], + capture_output=True, text=True, timeout=120, + creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0) + ) + if result.returncode != 0: + return + + # Find our offset in the disassembly + lines = result.stdout.splitlines() + recording = False + count = 0 + ret_count = 0 + + for line in lines: + stripped = line.strip() + if not stripped: + continue + + # Match address lines + m = re.match(r'^\s*([0-9A-Fa-f]{8,16}):', stripped) + if m: + addr = m.group(1).upper() + # Convert to RVA + try: + addr_int = int(addr, 16) + if self.arch == "x64" and addr_int > 0x140000000: + rva = format(addr_int - 0x140000000, 'X') + elif self.arch != "x64" and addr_int > 0x10000000: + rva = format(addr_int - 0x10000000, 'X') + else: + rva = addr + + if rva.lstrip('0') == self.offset.lstrip('0') or rva == self.offset: + recording = True + except ValueError: + pass + + if recording: + self.disasm_lines.append(stripped) + count += 1 + + # Stop at function epilogue (ret instruction) + if re.search(r'\bret\b', stripped, re.IGNORECASE): + ret_count += 1 + if ret_count >= 2 or count > 50: + break + + if count >= max_instructions: + break + + self.function_size = count + + except (subprocess.TimeoutExpired, MemoryError): + self.safety_warnings.append("Disassembly timed out on large binary") + + def _infer_args(self): + """Infer argument count from function prologue.""" + if not self.disasm_lines: + self.arg_count = 2 # Default assumption + return + + # x64 Windows fastcall: RCX, RDX, R8, R9, then stack + # Look for register saves and stack parameter references + arg_regs_used = set() + x64_arg_regs = {"rcx": 1, "rdx": 2, "r8": 3, "r9": 4, "r8d": 3, "r9d": 4, "ecx": 1, "edx": 2} + + # Only scan prologue (first ~20 instructions) + prologue = self.disasm_lines[:20] + for line in prologue: + lower = line.lower() + for reg, idx in x64_arg_regs.items(): + if reg in lower: + arg_regs_used.add(idx) + + # Stack parameter references: [rsp+28h], [rsp+30h], etc. + stack_match = re.search(r'\[rsp\+([0-9A-Fa-f]+)h?\]', lower) + if stack_match: + stack_off = int(stack_match.group(1), 16) + if stack_off >= 0x28: # 5th arg starts at rsp+28h (after shadow space) + arg_num = 5 + (stack_off - 0x28) // 8 + arg_regs_used.add(arg_num) + + if arg_regs_used: + self.arg_count = max(arg_regs_used) + else: + self.arg_count = 2 # Safe default + + def _trace_calls(self): + """Extract API calls from the function body.""" + api_categories = { + "file_open": ["CreateFileA", "CreateFileW", "CreateFile2", "fopen", "_wfopen", "_open"], + "file_read": ["ReadFile", "ReadFileEx", "fread", "_read", "fgets", "fgetc"], + "file_close": ["CloseHandle", "fclose", "_close"], + "file_seek": ["SetFilePointer", "SetFilePointerEx", "fseek", "_lseek"], + "file_map": ["CreateFileMappingW", "CreateFileMappingA", "MapViewOfFile", "UnmapViewOfFile"], + "memory_alloc": ["malloc", "calloc", "realloc", "HeapAlloc", "VirtualAlloc", "LocalAlloc", "GlobalAlloc"], + "memory_free": ["free", "HeapFree", "VirtualFree", "LocalFree", "GlobalFree"], + "memory_copy": ["memcpy", "memmove", "memset", "RtlCopyMemory", "RtlMoveMemory"], + "string_ops": ["strcpy", "strncpy", "wcsncpy", "lstrcpyW", "sprintf", "swprintf"], + "process_exit": ["ExitProcess", "TerminateProcess", "abort", "exit", "_exit"], + "exception": ["RaiseException", "FatalAppExitA", "FatalAppExitW"], + } + + for line in self.disasm_lines: + if "call" not in line.lower(): + continue + for category, apis in api_categories.items(): + for api in apis: + if api in line: + self.called_apis.append({"name": api, "category": category}) + if category == "file_open": + self.opens_file = True + elif category == "file_close": + self.closes_file = True + elif category == "file_read": + self.reads_data = True + elif category == "memory_alloc": + self.allocates_memory = True + elif category in ("process_exit", "exception"): + self.calls_exit = True + + def _validate_safety(self): + """Validate whether this function is safe to fuzz with WinAFL.""" + # Critical errors (will not work) + if self.calls_exit: + self.safety_errors.append( + "FATAL: Function calls ExitProcess/abort — WinAFL cannot loop it. " + "Choose a different target function.") + self.is_safe = False + + if not self.opens_file and not self.reads_data: + self.safety_warnings.append( + "WARNING: No file open/read calls detected in visible code. " + "The function may delegate to a sub-call or accept pre-opened handles.") + + if self.opens_file and not self.closes_file: + self.safety_warnings.append( + "WARNING: Opens files but no close detected — WinAFL needs the file " + "handle closed between iterations to overwrite the input file.") + + # Memory safety notes + if self.allocates_memory: + self.safety_warnings.append( + "NOTE: Function allocates memory. Ensure it frees properly to " + "avoid per-iteration leaks during long fuzzing runs.") + + # Size check + if self.function_size < 5: + self.safety_warnings.append( + "WARNING: Function body is very small — may be a thunk/stub. " + "The actual parsing may happen in a called function.") + + def to_dict(self): + d = { + "binary": self.binary, + "module": self.module, + "offset": self.offset, + "arch": self.arch, + "arg_count": self.arg_count, + "calling_convention": self.calling_convention, + "function_size": self.function_size, + "opens_file": self.opens_file, + "closes_file": self.closes_file, + "reads_data": self.reads_data, + "calls_exit": self.calls_exit, + "allocates_memory": self.allocates_memory, + "called_apis": self.called_apis, + "is_safe": self.is_safe, + "safety_warnings": self.safety_warnings, + "safety_errors": self.safety_errors, + } + # Add Ghidra-specific fields if present + if hasattr(self, 'ghidra_signature'): + d["ghidra_signature"] = self.ghidra_signature + if hasattr(self, 'ghidra_params'): + d["ghidra_params"] = self.ghidra_params + if hasattr(self, 'ghidra_decompiled'): + d["ghidra_decompiled"] = self.ghidra_decompiled + return d + + def print_report(self): + """Print a human-readable analysis report.""" + print(f"\n{'='*70}") + print(f" Deep Analysis Report") + print(f" {self.module} + 0x{self.offset}") + print(f"{'='*70}\n") + + print(f" Architecture: {self.arch}") + print(f" Calling convention: {self.calling_convention}") + print(f" Inferred args: {self.arg_count}") + print(f" Function size: ~{self.function_size} instructions") + print() + + # API call summary + if self.called_apis: + print(f" API Calls Detected:") + by_cat = defaultdict(list) + for api in self.called_apis: + by_cat[api["category"]].append(api["name"]) + for cat, names in sorted(by_cat.items()): + print(f" [{cat}] {', '.join(set(names))}") + print() + + # Behavioral flags + print(f" Behavioral Flags:") + flags = [ + ("Opens file", self.opens_file), + ("Reads data", self.reads_data), + ("Closes handle", self.closes_file), + ("Allocates memory", self.allocates_memory), + ("Calls exit", self.calls_exit), + ] + for label, val in flags: + icon = "[Y]" if val else "[ ]" + print(f" {icon} {label}") + print() + + # Safety assessment + safe_label = "SAFE" if self.is_safe else "UNSAFE" + print(f" Safety: {safe_label}") + for err in self.safety_errors: + print(f" [X] {err}") + for warn in self.safety_warnings: + print(f" [!] {warn}") + print() + + +# ============================================================================ +# Harness Code Generator +# ============================================================================ + +class HarnessGenerator: + """ + Generates C source code for WinAFL harnesses. + Multiple styles for different target patterns. + """ + + STYLES = { + "file_parser": "Target function opens, reads, and closes a file path argument", + "stream_parser": "Target function accepts a pre-opened stream/handle", + "buffer_parser": "Target function accepts a buffer pointer + length", + "dll_export": "Fuzzing a DLL exported function via LoadLibrary + GetProcAddress", + "com_interface": "Fuzzing a COM interface method", + "custom": "Minimal skeleton — fill in your own logic", + } + + def __init__(self, analysis, style="auto", **kwargs): + self.a = analysis # DeepAnalyzer results dict + self.style = style + self.opts = kwargs + + # Auto-detect style from analysis / Ghidra params + if style == "auto": + ghidra_params = analysis.get("ghidra_params", []) + if ghidra_params: + # Inspect parameter types from Ghidra + param_types = [p.get("type", "").lower() if isinstance(p, dict) else "" for p in ghidra_params] + type_str = " ".join(param_types) + if any(t in type_str for t in ["handle", "hfile", "stream"]): + self.style = "stream_parser" + elif any(t in type_str for t in ["byte *", "char *", "uchar *", "void *", "pbyte"]): + # Check if there's also a length param + if any(t in type_str for t in ["size_t", "uint", "int", "dword", "ulong"]): + self.style = "buffer_parser" + else: + self.style = "file_parser" + elif any(t in type_str for t in ["wchar", "lpcwstr", "lpwstr", "lpcstr"]): + self.style = "file_parser" + else: + self.style = "dll_export" if Path(analysis.get("binary", "")).suffix.lower() == ".dll" else "file_parser" + elif analysis.get("opens_file") and analysis.get("closes_file"): + self.style = "file_parser" + elif analysis.get("reads_data") and not analysis.get("opens_file"): + self.style = "buffer_parser" + elif Path(analysis.get("binary", "")).suffix.lower() == ".dll": + self.style = "dll_export" + else: + self.style = "file_parser" + + def generate(self): + """Generate the harness C source code.""" + method = getattr(self, f"_gen_{self.style}", self._gen_custom) + return method() + + def _ghidra_typedef(self): + """Generate typedef from Ghidra signature if available.""" + sig = self.a.get("ghidra_signature", "") + params = self.a.get("ghidra_params", []) + + if params and isinstance(params, list) and isinstance(params[0], dict): + # Build typedef from Ghidra parameter info + param_strs = [] + for p in params: + ptype = p.get("type", "void *") + pname = p.get("name", "param") + param_strs.append(f"{ptype} {pname}") + params_c = ", ".join(param_strs) if param_strs else "void" + return f"/* Ghidra-derived signature */\ntypedef int (WINAPI *TargetFunc)({params_c});" + + if sig: + # Try to convert Ghidra signature to typedef + return f"/* Ghidra signature: {sig} */\ntypedef int (WINAPI *TargetFunc)(const wchar_t* input_path);" + + # Fallback + return "typedef int (WINAPI *TargetFunc)(const wchar_t* input_path);" + + def _header(self): + """Common header for all harnesses.""" + return textwrap.dedent(f"""\ + /* + * WinAFL Harness — Auto-generated by winafl-harness-builder + * + * Target: {self.a['module']} + * Offset: 0x{self.a['offset']} + * Style: {self.style} + * Args: {self.a['arg_count']} + * + * Build: + * cl.exe /nologo /W3 /O2 harness.c /link /OUT:harness.exe + * + * Verify: + * drrun.exe -c winafl.dll -debug + * -target_module harness.exe -target_offset 0x + * -fuzz_iterations 10 -nargs 2 + * -- harness.exe input.bin + * + * Fuzz: + * afl-fuzz.exe -G -i in -o out -t 2000 + * -D -- + * -target_module harness.exe -target_offset 0x + * -coverage_module {self.a['module']} + * -fuzz_iterations 5000 -nargs 2 + * -- harness.exe @@ + */ + """) + + def _gen_file_parser(self): + module = self.a["module"] + offset = self.a["offset"] + nargs = self.a["arg_count"] + is_dll = module.lower().endswith(".dll") + + if is_dll: + return self._header() + textwrap.dedent(f"""\ + #include + #include + + /* ---- Configuration ---- */ + #define TARGET_DLL "{module}" + #define TARGET_OFFSET 0x{offset} + + /* Typedef for the target function. + * Adjust return type and parameters to match the real signature. + * Use IDA/Ghidra to confirm the prototype. */ + {self._ghidra_typedef()} + + /* ---- WinAFL fuzz target ---- */ + /* This is the function WinAFL will loop on. + * -target_module harness.exe -target_offset + * -nargs 2 (argc, argv — WinAFL passes during iteration) */ + int fuzz_target(int argc, char** argv) {{ + if (argc < 2) {{ + fprintf(stderr, "Usage: harness.exe \\n"); + return 1; + }} + + /* Convert input path to wide string */ + wchar_t wpath[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, argv[1], -1, wpath, MAX_PATH); + + /* Load the target DLL and resolve the function */ + static HMODULE hMod = NULL; + static TargetFunc pTarget = NULL; + + if (!hMod) {{ + hMod = LoadLibraryA(TARGET_DLL); + if (!hMod) {{ + fprintf(stderr, "[-] Failed to load %s (err=%lu)\\n", + TARGET_DLL, GetLastError()); + return 1; + }} + + /* Calculate function address from base + offset */ + pTarget = (TargetFunc)((BYTE*)hMod + TARGET_OFFSET); + fprintf(stderr, "[+] Loaded %s, target at %p\\n", + TARGET_DLL, pTarget); + }} + + /* Call the target — WinAFL will mutate argv[1] between iterations */ + __try {{ + pTarget(wpath); + }} __except(EXCEPTION_EXECUTE_HANDLER) {{ + /* Swallow crashes — WinAFL sees them via debug events */ + }} + + return 0; + }} + + int main(int argc, char** argv) {{ + return fuzz_target(argc, argv); + }} + """) + else: + # EXE target — harness wraps the target offset directly + return self._header() + textwrap.dedent(f"""\ + #include + #include + + /* ---- Configuration ---- */ + #define TARGET_EXE "{module}" + #define TARGET_OFFSET 0x{offset} + + /* Typedef — adjust to match the real function signature */ + typedef int (*TargetFunc)(const char* filename); + + int fuzz_target(int argc, char** argv) {{ + if (argc < 2) {{ + fprintf(stderr, "Usage: harness.exe \\n"); + return 1; + }} + + /* The target is in the same process — resolve once */ + static TargetFunc pTarget = NULL; + if (!pTarget) {{ + HMODULE hMod = GetModuleHandleA(NULL); + pTarget = (TargetFunc)((BYTE*)hMod + TARGET_OFFSET); + }} + + __try {{ + pTarget(argv[1]); + }} __except(EXCEPTION_EXECUTE_HANDLER) {{ + /* Swallow */ + }} + + return 0; + }} + + int main(int argc, char** argv) {{ + return fuzz_target(argc, argv); + }} + """) + + def _gen_stream_parser(self): + module = self.a["module"] + offset = self.a["offset"] + return self._header() + textwrap.dedent(f"""\ + #include + #include + + #define TARGET_DLL "{module}" + #define TARGET_OFFSET 0x{offset} + + /* Target accepts a pre-opened HANDLE */ + typedef int (WINAPI *TargetFunc)(HANDLE hFile); + + int fuzz_target(int argc, char** argv) {{ + if (argc < 2) return 1; + + static HMODULE hMod = NULL; + static TargetFunc pTarget = NULL; + + if (!hMod) {{ + hMod = LoadLibraryA(TARGET_DLL); + if (!hMod) return 1; + pTarget = (TargetFunc)((BYTE*)hMod + TARGET_OFFSET); + }} + + /* Open the file, pass handle, close — WinAFL needs the close */ + HANDLE hFile = CreateFileA( + argv[1], GENERIC_READ, FILE_SHARE_READ, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + + if (hFile == INVALID_HANDLE_VALUE) return 1; + + __try {{ + pTarget(hFile); + }} __except(EXCEPTION_EXECUTE_HANDLER) {{ + /* swallow */ + }} + + CloseHandle(hFile); + return 0; + }} + + int main(int argc, char** argv) {{ + return fuzz_target(argc, argv); + }} + """) + + def _gen_buffer_parser(self): + module = self.a["module"] + offset = self.a["offset"] + return self._header() + textwrap.dedent(f"""\ + #include + #include + #include + + #define TARGET_DLL "{module}" + #define TARGET_OFFSET 0x{offset} + + /* Target accepts a buffer + size */ + typedef int (WINAPI *TargetFunc)(const unsigned char* buf, size_t len); + + int fuzz_target(int argc, char** argv) {{ + if (argc < 2) return 1; + + static HMODULE hMod = NULL; + static TargetFunc pTarget = NULL; + + if (!hMod) {{ + hMod = LoadLibraryA(TARGET_DLL); + if (!hMod) return 1; + pTarget = (TargetFunc)((BYTE*)hMod + TARGET_OFFSET); + }} + + /* Read entire file into memory */ + FILE* f = fopen(argv[1], "rb"); + if (!f) return 1; + + fseek(f, 0, SEEK_END); + long file_size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (file_size <= 0 || file_size > 10 * 1024 * 1024) {{ + fclose(f); + return 1; /* Skip files > 10MB */ + }} + + unsigned char* buf = (unsigned char*)malloc(file_size); + if (!buf) {{ fclose(f); return 1; }} + + fread(buf, 1, file_size, f); + fclose(f); + + __try {{ + pTarget(buf, (size_t)file_size); + }} __except(EXCEPTION_EXECUTE_HANDLER) {{ + /* swallow */ + }} + + free(buf); + return 0; + }} + + int main(int argc, char** argv) {{ + return fuzz_target(argc, argv); + }} + """) + + def _gen_dll_export(self): + module = self.a["module"] + offset = self.a["offset"] + # Try to find export name for this offset + export_name = self.opts.get("export_name", None) + if export_name: + resolve_code = f'pTarget = (TargetFunc)GetProcAddress(hMod, "{export_name}");' + else: + resolve_code = f"pTarget = (TargetFunc)((BYTE*)hMod + 0x{offset});" + + return self._header() + textwrap.dedent(f"""\ + #include + #include + + #define TARGET_DLL "{module}" + + /* Adjust the typedef to match the exported function's real signature. + * Use: dumpbin /exports {module} + * Then look up the function in IDA/Ghidra to confirm args. */ + typedef int (WINAPI *TargetFunc)(const wchar_t* input_path); + + int fuzz_target(int argc, char** argv) {{ + if (argc < 2) return 1; + + static HMODULE hMod = NULL; + static TargetFunc pTarget = NULL; + + if (!hMod) {{ + hMod = LoadLibraryA(TARGET_DLL); + if (!hMod) {{ + fprintf(stderr, "[-] LoadLibrary failed: %lu\\n", GetLastError()); + return 1; + }} + {resolve_code} + if (!pTarget) {{ + fprintf(stderr, "[-] Could not resolve target function\\n"); + return 1; + }} + fprintf(stderr, "[+] Target resolved at %p\\n", pTarget); + }} + + wchar_t wpath[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, argv[1], -1, wpath, MAX_PATH); + + __try {{ + pTarget(wpath); + }} __except(EXCEPTION_EXECUTE_HANDLER) {{ + /* swallow */ + }} + + return 0; + }} + + int main(int argc, char** argv) {{ + return fuzz_target(argc, argv); + }} + """) + + def _gen_com_interface(self): + module = self.a["module"] + offset = self.a["offset"] + return self._header() + textwrap.dedent(f"""\ + #include + #include + #include + + /* ---- COM Configuration ---- + * Set the CLSID and IID for your target COM object. + * Find these in the registry or with OleView. */ + // DEFINE_GUID(CLSID_Target, 0x..., 0x..., 0x..., ...); + // DEFINE_GUID(IID_ITarget, 0x..., 0x..., 0x..., ...); + + int fuzz_target(int argc, char** argv) {{ + if (argc < 2) return 1; + + static int com_initialized = 0; + if (!com_initialized) {{ + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + com_initialized = 1; + }} + + wchar_t wpath[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, argv[1], -1, wpath, MAX_PATH); + + /* TODO: Create COM object and call the target method + * + * IUnknown* pUnk = NULL; + * HRESULT hr = CoCreateInstance(&CLSID_Target, NULL, + * CLSCTX_INPROC_SERVER, &IID_ITarget, (void**)&pUnk); + * + * if (SUCCEEDED(hr)) {{ + * // Call method with wpath + * pUnk->lpVtbl->Release(pUnk); + * }} + */ + + fprintf(stderr, "[!] COM harness template — fill in CLSID/IID and method call\\n"); + return 0; + }} + + int main(int argc, char** argv) {{ + return fuzz_target(argc, argv); + }} + """) + + def _gen_custom(self): + module = self.a["module"] + offset = self.a["offset"] + return self._header() + textwrap.dedent(f"""\ + #include + #include + + #define TARGET_MODULE "{module}" + #define TARGET_OFFSET 0x{offset} + + /* + * Custom harness skeleton. + * Fill in your own target function call logic below. + * + * Analysis detected {self.a['arg_count']} arguments. + * Called APIs: {', '.join(a['name'] for a in self.a.get('called_apis', [])) or 'none detected'} + */ + + int fuzz_target(int argc, char** argv) {{ + if (argc < 2) {{ + fprintf(stderr, "Usage: harness.exe \\n"); + return 1; + }} + + /* TODO: Your target call here */ + fprintf(stderr, "Input: %s\\n", argv[1]); + + return 0; + }} + + int main(int argc, char** argv) {{ + return fuzz_target(argc, argv); + }} + """) + + +# ============================================================================ +# Harness Validator +# ============================================================================ + +class HarnessValidator: + """ + Pre-flight safety checks before running a harness. + Validates without executing anything dangerous. + """ + + def __init__(self, binary, offset, drio_dir=None, harness_exe=None): + self.binary = binary + self.offset = offset + self.drio_dir = drio_dir + self.harness_exe = harness_exe + self.checks = [] + + def validate_all(self): + """Run all validation checks.""" + self._check_binary_exists() + self._check_binary_bitness() + self._check_offset_in_range() + self._check_drio() + self._check_harness() + return self.checks + + def _check_binary_exists(self): + exists = os.path.exists(self.binary) + self.checks.append({ + "name": "Target binary exists", + "ok": exists, + "detail": self.binary if exists else f"NOT FOUND: {self.binary}" + }) + + def _check_binary_bitness(self): + try: + with open(self.binary, "rb") as f: + dos_sig = f.read(2) + if dos_sig != b"MZ": + self.checks.append({"name": "Valid PE", "ok": False, "detail": "Not a PE file"}) + return + f.seek(0x3C) + pe_offset = struct.unpack("='} file size {file_size:,d}" + }) + except Exception: + self.checks.append({"name": "Offset check", "ok": False, "detail": "Invalid offset"}) + + def _check_drio(self): + if not self.drio_dir: + self.checks.append({"name": "DynamoRIO", "ok": False, "detail": "Not specified (--drio)"}) + return + drrun = os.path.join(self.drio_dir, "bin64", "drrun.exe") + exists = os.path.exists(drrun) + self.checks.append({ + "name": "DynamoRIO drrun.exe", + "ok": exists, + "detail": drrun if exists else f"NOT FOUND: {drrun}" + }) + + def _check_harness(self): + if self.harness_exe: + exists = os.path.exists(self.harness_exe) + self.checks.append({ + "name": "Harness executable", + "ok": exists, + "detail": self.harness_exe if exists else f"NOT FOUND: {self.harness_exe}" + }) + + def print_report(self): + print(f"\n{'='*70}") + print(f" Pre-Flight Validation") + print(f"{'='*70}\n") + + all_ok = True + for c in self.checks: + icon = "[OK]" if c["ok"] else "[!!]" + print(f" {icon} {c['name']}: {c['detail']}") + if not c["ok"]: + all_ok = False + + print() + if all_ok: + print(f" All checks passed — safe to proceed.") + else: + print(f" Some checks failed — resolve issues before fuzzing.") + print() + return all_ok + + +# ============================================================================ +# Pipeline Integration +# ============================================================================ + +def read_pipe_input(): + """Read JSON from stdin (piped from winafl-target-finder.py harness --json).""" + if sys.stdin.isatty(): + return None + + try: + raw = sys.stdin.read() + data = json.loads(raw) + return data + except (json.JSONDecodeError, ValueError): + # Try line-by-line parsing (the harness command doesn't output JSON yet, + # so also try parsing the structured text output) + return None + + +# ============================================================================ +# Ghidra Connection Helper +# ============================================================================ + +def _connect_ghidra(args): + """Try to connect to Ghidra if --ghidra flag is set.""" + ghidra_addr = getattr(args, 'ghidra', None) + if not ghidra_addr or not HAS_GHIDRA_BRIDGE: + return None + + if ":" in ghidra_addr: + host, port_str = ghidra_addr.rsplit(":", 1) + port = int(port_str) + else: + host, port = ghidra_addr, 8192 + + client = GhidraClient(host, port) + if client.connect(): + print(f" [GHIDRA] Connected to {client.server_type} at {host}:{port}") + return client + else: + print(f" [GHIDRA] Could not connect to {host}:{port} — falling back to dumpbin") + return None + + +# ============================================================================ +# CLI Commands +# ============================================================================ + +def cmd_analyze(args): + """Deep-analyze a target function.""" + offset = args.offset.lstrip("0x").lstrip("0X") + ghidra = _connect_ghidra(args) + analyzer = DeepAnalyzer(args.binary, offset, arch=args.arch) + results = analyzer.run_all(ghidra=ghidra) + analyzer.print_report() + + # Show decompiled code if available from Ghidra + if hasattr(analyzer, 'ghidra_decompiled') and analyzer.ghidra_decompiled: + print(f" Decompiled Code (via Ghidra):") + print(f" {'-'*60}") + for line in analyzer.ghidra_decompiled.splitlines()[:40]: + print(f" {line}") + print(f" {'-'*60}\n") + + if args.json_out: + print(json.dumps(results, indent=2)) + + return results + + +def cmd_generate(args): + """Generate a harness source file.""" + offset = args.offset.lstrip("0x").lstrip("0X") + + # Run analysis first (with Ghidra if available) + ghidra = _connect_ghidra(args) + analyzer = DeepAnalyzer(args.binary, offset, arch=args.arch) + results = analyzer.run_all(ghidra=ghidra) + + # Generate harness + gen = HarnessGenerator( + results, + style=args.style, + export_name=getattr(args, 'export_name', None), + ) + source = gen.generate() + + # Output + out_dir = args.out or "." + os.makedirs(out_dir, exist_ok=True) + + module_base = Path(args.binary).stem + out_file = os.path.join(out_dir, f"harness_{module_base}_{offset}.c") + + with open(out_file, "w") as f: + f.write(source) + + print(f"\n{'='*70}") + print(f" Harness Generated") + print(f"{'='*70}\n") + print(f" Style: {gen.style}") + print(f" Output: {out_file}") + print(f" Args: {results['arg_count']}") + + if results['safety_errors']: + print(f"\n SAFETY ERRORS:") + for e in results['safety_errors']: + print(f" [X] {e}") + + if results['safety_warnings']: + print(f"\n WARNINGS:") + for w in results['safety_warnings']: + print(f" [!] {w}") + + # Compile if requested + if args.compile: + print(f"\n Compiling...") + out_exe = out_file.replace(".c", ".exe") + cl = CL or "cl" + cmd = f'"{cl}" /nologo /W3 /O2 "{out_file}" /link /OUT:"{out_exe}"' + try: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) + if result.returncode == 0: + print(f" [OK] Compiled: {out_exe}") + else: + print(f" [!!] Compilation failed:") + print(f" {result.stderr[:300]}") + except subprocess.TimeoutExpired: + print(f" [!!] Compilation timed out") + + print(f"\n Next steps:") + print(f" 1. Review and adjust the typedef in {out_file}") + print(f" 2. Compile: cl.exe /nologo /W3 /O2 {out_file}") + print(f" 3. Verify: python winafl-target-finder.py verify harness.exe ") + print() + + return out_file + + +def cmd_validate(args): + """Pre-flight validation.""" + offset = args.offset.lstrip("0x").lstrip("0X") + validator = HarnessValidator( + args.binary, offset, + drio_dir=args.drio, + harness_exe=getattr(args, 'harness_exe', None), + ) + checks = validator.validate_all() + validator.print_report() + + # Also run deep analysis + analyzer = DeepAnalyzer(args.binary, offset, arch=args.arch) + analyzer.run_all() + analyzer.print_report() + + +def cmd_full(args): + """Full pipeline: analyze → generate → validate.""" + offset = args.offset.lstrip("0x").lstrip("0X") + + print(f"\n{'='*70}") + print(f" Full Pipeline: {args.binary} + 0x{offset}") + print(f"{'='*70}") + + # Step 1: Deep analysis + ghidra = _connect_ghidra(args) + ghidra_label = " + Ghidra" if ghidra else "" + print(f"\n [1/3] Deep Analysis{ghidra_label}...") + analyzer = DeepAnalyzer(args.binary, offset, arch=args.arch) + results = analyzer.run_all(ghidra=ghidra) + analyzer.print_report() + + if not results["is_safe"]: + print(f" [!!] Target is NOT SAFE for WinAFL. Aborting.") + print(f" Fix the safety errors above or choose a different offset.") + return + + # Step 2: Generate harness + print(f"\n [2/3] Generating Harness...") + gen = HarnessGenerator(results, style=args.style) + source = gen.generate() + + out_dir = args.out or "harness" + os.makedirs(out_dir, exist_ok=True) + module_base = Path(args.binary).stem + out_file = os.path.join(out_dir, f"harness_{module_base}_{offset}.c") + + with open(out_file, "w") as f: + f.write(source) + print(f" [OK] Harness written to: {out_file}") + print(f" [OK] Style: {gen.style}") + + # Step 3: Validate + print(f"\n [3/3] Pre-Flight Validation...") + validator = HarnessValidator( + args.binary, offset, + drio_dir=args.drio, + ) + validator.validate_all() + validator.print_report() + + # Summary + print(f" {'='*60}") + print(f" PIPELINE COMPLETE") + print(f" {'='*60}\n") + print(f" Harness: {out_file}") + print(f" Style: {gen.style}") + print(f" Args: {results['arg_count']}") + print() + print(f" Next steps:") + print(f" 1. Review {out_file} — adjust the typedef to match the real signature") + print(f" 2. Compile: cl.exe /nologo /W3 /O2 \"{out_file}\"") + if args.drio: + harness_exe = out_file.replace(".c", ".exe") + print(f" 3. Verify: drrun.exe -c winafl.dll -debug " + f"-target_module {os.path.basename(harness_exe)} " + f"-target_offset -fuzz_iterations 10 -nargs 2 " + f"-- {harness_exe} input.bin") + print() + + +def cmd_auto(args): + """Fully automated: discover targets via Ghidra + generate harnesses.""" + ghidra = _connect_ghidra(args) + if not ghidra: + print(f"\n [!!] The 'auto' command requires a Ghidra MCP connection.") + print(f" Start Ghidra with the GhydraMCP plugin, load your binary, then run:") + print(f" python winafl-harness-builder.py auto {args.binary} --ghidra localhost:8192") + if not HAS_GHIDRA_BRIDGE: + print(f"\n Also ensure ghidra_bridge.py is in the same directory.") + return + + print(f"\n{'='*70}") + print(f" Automated Harness Discovery + Generation") + print(f" Target: {args.binary}") + print(f" Ghidra: {args.ghidra}") + print(f"{'='*70}") + + # Step 1: Auto-discover fuzz candidates via Ghidra + print(f"\n [1/4] Discovering fuzzing candidates via Ghidra...") + candidates = ghidra.find_fuzz_candidates() + + if not candidates: + print(f" [!!] No candidates found. The binary may not have relevant exports") + print(f" or file I/O imports. Try manual analysis with:") + print(f" python winafl-harness-builder.py analyze {args.binary} --offset --ghidra {args.ghidra}") + return + + limit = args.limit or 5 + print(f" [OK] Found {len(candidates)} candidates, processing top {limit}\n") + + for i, cand in enumerate(candidates[:limit]): + name = cand.get("name", "unknown") + addr = cand.get("address", "0") + score = cand.get("score", 0) + print(f" #{i+1:2d} [Score: {score:3d}] {addr} {name}") + for r in cand.get("reasons", []): + print(f" + {r}") + + # Step 2: Deep-analyze the top candidates + print(f"\n [2/4] Deep-analyzing top candidates via Ghidra decompilation...") + viable = [] + for cand in candidates[:limit]: + addr = cand.get("address", "") + name = cand.get("name", "unknown") + if not addr: + continue + + offset = addr.lstrip("0x").lstrip("0X").upper() + analyzer = DeepAnalyzer(args.binary, offset, arch=args.arch) + results = analyzer.run_all(ghidra=ghidra) + + if results["is_safe"]: + viable.append({"candidate": cand, "analysis": results, "offset": offset}) + sig_info = f" | sig: {results.get('ghidra_signature', 'unknown')}" if results.get("ghidra_signature") else "" + print(f" [OK] {name} — {results['arg_count']} args, safe{sig_info}") + else: + print(f" [XX] {name} — UNSAFE: {results['safety_errors'][0][:60]}") + + if not viable: + print(f"\n [!!] No viable candidates passed safety analysis.") + return + + # Step 3: Generate harnesses + print(f"\n [3/4] Generating harnesses for {len(viable)} viable candidates...") + out_dir = args.out or "harness" + os.makedirs(out_dir, exist_ok=True) + + generated = [] + for v in viable: + results = v["analysis"] + offset = v["offset"] + name = v["candidate"].get("name", "func") + + gen = HarnessGenerator(results, style=args.style, export_name=name) + source = gen.generate() + + safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', name) + out_file = os.path.join(out_dir, f"harness_{safe_name}_{offset}.c") + with open(out_file, "w") as f: + f.write(source) + + generated.append({"file": out_file, "name": name, "offset": offset, "style": gen.style}) + print(f" [OK] {out_file} (style: {gen.style})") + + # Step 4: Summary + print(f"\n [4/4] Validation Summary...") + validator = HarnessValidator(args.binary, viable[0]["offset"], drio_dir=getattr(args, 'drio', None)) + validator.validate_all() + validator.print_report() + + print(f" {'='*60}") + print(f" AUTO-GENERATION COMPLETE") + print(f" {'='*60}\n") + print(f" Generated {len(generated)} harness(es) in {out_dir}/\n") + + for g in generated: + print(f" {g['file']}") + print(f" Target: {g['name']} @ 0x{g['offset']}, Style: {g['style']}") + + print(f"\n Next steps:") + print(f" 1. Review the generated .c files — adjust typedefs if needed") + print(f" 2. Compile: cl.exe /nologo /W3 /O2 harness\\harness_*.c") + if getattr(args, 'drio', None): + print(f" 3. Verify: drrun.exe -c winafl.dll -debug -target_module harness.exe ...") + print() + + +def cmd_styles(args): + """List available harness styles.""" + print(f"\n Available Harness Styles:\n") + for name, desc in HarnessGenerator.STYLES.items(): + print(f" {name:16s} {desc}") + print(f"\n auto Auto-detect from analysis results (default)\n") + + +def cmd_pipe(args): + """Process piped JSON input from winafl-target-finder.py.""" + data = read_pipe_input() + if not data: + print(" [!] No valid JSON on stdin.") + print(" Usage: python winafl-target-finder.py harness target.dll --json | python winafl-harness-builder.py pipe") + return + + # data could be a list of candidates or a single candidate + candidates = data if isinstance(data, list) else [data] + + print(f"\n Received {len(candidates)} candidates from pipeline.\n") + + # Process top N candidates + limit = args.limit or 3 + for i, cand in enumerate(candidates[:limit]): + offset = cand.get("offset", "0") + binary = cand.get("binary", args.binary or "") + name = cand.get("name", "unknown") + + if offset == "MANUAL" or not binary: + continue + + print(f" --- Candidate {i+1}: {name} (0x{offset}) ---") + + analyzer = DeepAnalyzer(binary, offset, arch=args.arch) + results = analyzer.run_all() + + if results["is_safe"]: + gen = HarnessGenerator(results, style=args.style) + source = gen.generate() + + out_dir = args.out or "harness" + os.makedirs(out_dir, exist_ok=True) + out_file = os.path.join(out_dir, f"harness_{name}_{offset}.c") + + with open(out_file, "w") as f: + f.write(source) + print(f" [OK] Generated: {out_file} (style: {gen.style})") + else: + print(f" [SKIP] Not safe: {results['safety_errors']}") + print() + + +def main(): + parser = argparse.ArgumentParser( + prog="winafl-harness-builder", + description="Automated harness generation & deep analysis for WinAFL", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent("""\ + Pipeline Examples: + # Full automated pipeline + %(prog)s full target.dll --offset 0x41040 --drio C:\\DynamoRIO --out harness\\ + + # Just analyze a function + %(prog)s analyze target.dll --offset 0x41040 + + # Generate a specific harness style + %(prog)s generate target.dll --offset 0x41040 --style buffer_parser + + # Pipe from target-finder + python winafl-target-finder.py harness target.dll --json | %(prog)s pipe --binary target.dll + + # List available styles + %(prog)s styles + """) + ) + + # Global options + parser.add_argument("--arch", choices=["x64", "x86"], default="x64", + help="Target architecture (default: x64)") + parser.add_argument("--json-out", action="store_true", + help="Also output analysis as JSON") + parser.add_argument("--ghidra", metavar="HOST:PORT", + help="Connect to Ghidra MCP server (e.g. localhost:8192)") + + sub = parser.add_subparsers(dest="command") + + # analyze + p_analyze = sub.add_parser("analyze", help="Deep-analyze a target function") + p_analyze.add_argument("binary", help="Target binary (EXE or DLL)") + p_analyze.add_argument("--offset", required=True, help="Function offset (hex)") + + # generate + p_gen = sub.add_parser("generate", help="Generate harness source code") + p_gen.add_argument("binary", help="Target binary") + p_gen.add_argument("--offset", required=True, help="Function offset (hex)") + p_gen.add_argument("--style", default="auto", + choices=list(HarnessGenerator.STYLES.keys()) + ["auto"], + help="Harness style (default: auto-detect)") + p_gen.add_argument("--out", help="Output directory (default: current dir)") + p_gen.add_argument("--export-name", help="Export function name (for dll_export style)") + p_gen.add_argument("--compile", action="store_true", help="Compile after generating") + + # validate + p_val = sub.add_parser("validate", help="Pre-flight validation checks") + p_val.add_argument("binary", help="Target binary") + p_val.add_argument("--offset", required=True, help="Function offset (hex)") + p_val.add_argument("--drio", help="DynamoRIO directory") + p_val.add_argument("--harness-exe", help="Path to compiled harness") + + # full + p_full = sub.add_parser("full", help="Full pipeline: analyze + generate + validate") + p_full.add_argument("binary", help="Target binary") + p_full.add_argument("--offset", required=True, help="Function offset (hex)") + p_full.add_argument("--style", default="auto", + choices=list(HarnessGenerator.STYLES.keys()) + ["auto"]) + p_full.add_argument("--drio", help="DynamoRIO directory") + p_full.add_argument("--out", help="Output directory (default: harness/)") + + # pipe + p_pipe = sub.add_parser("pipe", help="Process piped input from winafl-target-finder.py") + p_pipe.add_argument("--binary", help="Target binary (if not in piped data)") + p_pipe.add_argument("--style", default="auto", + choices=list(HarnessGenerator.STYLES.keys()) + ["auto"]) + p_pipe.add_argument("--out", help="Output directory") + p_pipe.add_argument("--limit", type=int, default=3, + help="Max candidates to process (default: 3)") + + # styles + sub.add_parser("styles", help="List available harness styles") + + # auto (Ghidra-powered full automation) + p_auto = sub.add_parser("auto", help="Fully automated: discover targets via Ghidra + generate harnesses") + p_auto.add_argument("binary", help="Target binary") + p_auto.add_argument("--style", default="auto", + choices=list(HarnessGenerator.STYLES.keys()) + ["auto"]) + p_auto.add_argument("--out", help="Output directory (default: harness/)") + p_auto.add_argument("--limit", type=int, default=5, + help="Max candidates to process (default: 5)") + p_auto.add_argument("--drio", help="DynamoRIO directory") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + commands = { + "analyze": cmd_analyze, + "generate": cmd_generate, + "validate": cmd_validate, + "full": cmd_full, + "pipe": cmd_pipe, + "styles": cmd_styles, + "auto": cmd_auto, + } + + commands[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/winafl-target-finder.py b/winafl-target-finder.py new file mode 100644 index 0000000..5130dc7 --- /dev/null +++ b/winafl-target-finder.py @@ -0,0 +1,1392 @@ +#!/usr/bin/env python3 +""" +WinAFL Target Finder - Automated Fuzzing Campaign Bootstrap Utility +Contributed by Elias Ibrahim (Feb 2026) + +Scans Windows executables and DLLs to identify promising fuzzing targets, +builds seed corpora, verifies DynamoRIO harnesses, and generates ready-to-run +afl-fuzz command lines with GPU acceleration. + +Usage: + python winafl-target-finder.py scan Scan directory for fuzzable targets + python winafl-target-finder.py analyze Deep-analyze a single binary + python winafl-target-finder.py seeds Build a minimal seed corpus + python winafl-target-finder.py verify Test a harness + python winafl-target-finder.py generate [options] Generate launch cmd + +Requirements: + - Python 3.6+ + - dumpbin.exe (ships with Visual Studio) + - DynamoRIO (for verify/generate commands) +""" + +import os +import sys +import re +import subprocess +import struct +import argparse +import json +import glob +import io +from pathlib import Path +from collections import defaultdict + +# Force UTF-8 output on Windows consoles +if sys.platform == "win32": + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + +# ============================================================================ +# Configuration +# ============================================================================ + +# Imports that indicate file parsing behavior (high-value targets) +FILE_PARSING_IMPORTS = { + "CreateFileA", "CreateFileW", "ReadFile", "ReadFileEx", + "MapViewOfFile", "CreateFileMappingA", "CreateFileMappingW", + "fopen", "fread", "fgets", "fclose", "fseek", "ftell", + "_wfopen", "_read", "_open", + "MultiByteToWideChar", # often in parsers +} + +# Imports that indicate complex data processing (memory manipulation = bugs) +DATA_PROCESSING_IMPORTS = { + "malloc", "calloc", "realloc", "free", + "HeapAlloc", "HeapReAlloc", "HeapFree", + "VirtualAlloc", "VirtualFree", + "memcpy", "memmove", "memset", "memcmp", + "strcpy", "strncpy", "wcsncpy", "lstrcpyW", + "sprintf", "swprintf", "vsprintf", +} + +# Imports that indicate network activity (network fuzzing targets) +NETWORK_IMPORTS = { + "recv", "recvfrom", "WSARecv", + "send", "sendto", "WSASend", + "accept", "listen", "bind", "connect", + "socket", "WSASocketW", +} + +# Known parser library patterns +PARSER_DLL_PATTERNS = [ + r"lib.*\.(dll|exe)$", # libpng, libjpeg, libxml2, etc. + r".*codec.*\.(dll|exe)$", # avcodec, etc. + r".*parse.*\.(dll|exe)$", # parsers + r".*decode.*\.(dll|exe)$", # decoders + r".*render.*\.(dll|exe)$", # renderers + r".*image.*\.(dll|exe)$", # image processing + r".*font.*\.(dll|exe)$", # font parsers + r".*pdf.*\.(dll|exe)$", # PDF processing + r".*xml.*\.(dll|exe)$", # XML parsers + r".*json.*\.(dll|exe)$", # JSON parsers + r".*zip.*\.(dll|exe)$", # archive handlers + r".*compress.*\.(dll|exe)$", # compression + r".*crypt.*\.(dll|exe)$", # crypto + r".*media.*\.(dll|exe)$", # media handling +] + +# File format to seed search patterns +SEED_SOURCES = { + "pdf": {"ext": ".pdf", "dirs": [r"C:\Users\Public\Documents"], "min_size": 100, "max_size": 50000}, + "png": {"ext": ".png", "dirs": [r"C:\Windows\Web", r"C:\Windows\Resources"], "min_size": 50, "max_size": 10000}, + "jpg": {"ext": ".jpg", "dirs": [r"C:\Windows\Web", r"C:\Users\Public\Pictures"], "min_size": 50, "max_size": 10000}, + "jpeg": {"ext": ".jpeg", "dirs": [r"C:\Windows\Web", r"C:\Users\Public\Pictures"], "min_size": 50, "max_size": 10000}, + "bmp": {"ext": ".bmp", "dirs": [r"C:\Windows\Web", r"C:\Windows"], "min_size": 50, "max_size": 10000}, + "gif": {"ext": ".gif", "dirs": [r"C:\Windows\Web"], "min_size": 50, "max_size": 10000}, + "tiff": {"ext": ".tiff", "dirs": [r"C:\Windows\Web"], "min_size": 50, "max_size": 10000}, + "xml": {"ext": ".xml", "dirs": [r"C:\Windows\System32"], "min_size": 30, "max_size": 5000}, + "zip": {"ext": ".zip", "dirs": [r"C:\Users\Public"], "min_size": 50, "max_size": 20000}, + "mp3": {"ext": ".mp3", "dirs": [r"C:\Windows\Media"], "min_size": 100, "max_size": 50000}, + "wav": {"ext": ".wav", "dirs": [r"C:\Windows\Media"], "min_size": 100, "max_size": 50000}, + "avi": {"ext": ".avi", "dirs": [r"C:\Users\Public\Videos"], "min_size": 100, "max_size": 100000}, + "doc": {"ext": ".doc", "dirs": [r"C:\Users\Public\Documents"], "min_size": 100, "max_size": 50000}, + "ttf": {"ext": ".ttf", "dirs": [r"C:\Windows\Fonts"], "min_size": 1000, "max_size": 100000}, + "otf": {"ext": ".otf", "dirs": [r"C:\Windows\Fonts"], "min_size": 1000, "max_size": 100000}, +} + + +# ============================================================================ +# PE Header Analysis (no external deps) +# ============================================================================ + +class PEAnalyzer: + """Lightweight PE parser to extract imports, exports, and metadata.""" + + def __init__(self, filepath): + self.filepath = filepath + self.imports = defaultdict(list) + self.exports = [] + self.is_64bit = False + self.is_dll = False + self.has_aslr = False + self.has_dep = False + self.has_cfg = False + self.sections = [] + self._parse() + + def _parse(self): + try: + with open(self.filepath, "rb") as f: + # DOS Header + dos_sig = f.read(2) + if dos_sig != b"MZ": + return + + f.seek(0x3C) + pe_offset = struct.unpack("= 2: + func_name = parts[-1] + if func_name.isidentifier(): + self.imports[current_dll].append(func_name) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return self.imports + + def get_exports_via_dumpbin(self): + """Use dumpbin to get exports.""" + try: + dumpbin = DUMPBIN_PATH or "dumpbin" + result = subprocess.run( + [dumpbin, "/exports", self.filepath], + capture_output=True, text=True, timeout=15, + creationflags=subprocess.CREATE_NO_WINDOW + ) + if result.returncode == 0: + in_exports = False + for line in result.stdout.splitlines(): + line = line.strip() + if "ordinal" in line.lower() and "name" in line.lower(): + in_exports = True + continue + if in_exports and line: + parts = line.split() + if len(parts) >= 4: + self.exports.append({ + "ordinal": parts[0], + "rva": parts[2], + "name": parts[3] if len(parts) > 3 else f"ordinal_{parts[0]}" + }) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return self.exports + + +# ============================================================================ +# Target Scoring +# ============================================================================ + +def score_target(pe, all_imports_flat): + """Score a binary for fuzzing suitability (0-100).""" + score = 0 + reasons = [] + + # File parsing imports + file_hits = FILE_PARSING_IMPORTS.intersection(all_imports_flat) + if file_hits: + score += min(len(file_hits) * 5, 30) + reasons.append(f"File I/O: {', '.join(sorted(file_hits)[:5])}") + + # Data processing imports (memory ops = bugs) + data_hits = DATA_PROCESSING_IMPORTS.intersection(all_imports_flat) + if data_hits: + score += min(len(data_hits) * 3, 20) + reasons.append(f"Memory ops: {len(data_hits)} functions") + + # Network imports + net_hits = NETWORK_IMPORTS.intersection(all_imports_flat) + if net_hits: + score += min(len(net_hits) * 4, 15) + reasons.append(f"Network: {', '.join(sorted(net_hits)[:3])}") + + # DLL bonus (easier to harness) + if pe.is_dll: + score += 10 + reasons.append("DLL (easy to harness)") + + # Exports bonus for DLLs (more attack surface) + if pe.exports: + export_score = min(len(pe.exports) * 1, 10) + score += export_score + reasons.append(f"{len(pe.exports)} exports") + + # Name pattern matching + basename = os.path.basename(pe.filepath).lower() + for pattern in PARSER_DLL_PATTERNS: + if re.match(pattern, basename): + score += 10 + reasons.append(f"Name match: parser pattern") + break + + # Penalty for mitigations (harder but still fuzzable) + mitigations = [] + if pe.has_aslr: + mitigations.append("ASLR") + if pe.has_dep: + mitigations.append("DEP") + if pe.has_cfg: + mitigations.append("CFG") + score -= 5 # CFG makes exploitation harder + + if mitigations: + reasons.append(f"Mitigations: {', '.join(mitigations)}") + + # Size of .text section (larger = more code = more bugs) + for sec in pe.sections: + if sec["name"] == ".text": + text_kb = sec["virtual_size"] / 1024 + if text_kb > 500: + score += 5 + reasons.append(f".text: {text_kb:.0f} KB") + break + + return min(score, 100), reasons + + +# ============================================================================ +# Target Scanning +# ============================================================================ + +def find_dumpbin(): + """Try to locate dumpbin.exe in the system.""" + try: + result = subprocess.run(["where", "dumpbin"], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + return result.stdout.strip().splitlines()[0] + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + # Search common VS paths + vs_paths = glob.glob(r"C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe") + if vs_paths: + return vs_paths[0] + + return None + +# Resolve dumpbin once at module load +DUMPBIN_PATH = find_dumpbin() + + +def scan_directory(scan_path, max_depth=3): + """Scan a directory for fuzzable targets.""" + # Strip trailing quotes/backslashes (Windows cmd.exe escapes \" in "path\") + scan_path = Path(str(scan_path).rstrip('"').rstrip('\\').rstrip('"')) + + if not scan_path.exists(): + print(f"\n [-] Path does not exist: {scan_path}") + return + + # If user passed a single file, scan its parent directory + if scan_path.is_file(): + print(f"\n [*] '{scan_path.name}' is a file — scanning parent directory: {scan_path.parent}") + scan_path = scan_path.parent + + print(f"\n{'='*70}") + print(f" WinAFL Target Finder - Scanning: {scan_path}") + print(f"{'='*70}\n") + + dumpbin = find_dumpbin() + if dumpbin: + print(f" [+] dumpbin located: {dumpbin}") + else: + print(f" [!] dumpbin not found (install VS Build Tools for deeper analysis)") + print(f" Falling back to PE header analysis only.\n") + + targets = [] + + # Collect binaries + binaries = [] + for ext in ("*.exe", "*.dll"): + for f in scan_path.rglob(ext): + # Respect max depth + try: + depth = len(f.relative_to(scan_path).parts) - 1 + if depth <= max_depth: + binaries.append(f) + except ValueError: + continue + + print(f" [*] Found {len(binaries)} binaries to analyze...\n") + + for i, binary in enumerate(binaries): + try: + pe = PEAnalyzer(str(binary)) + + # Get imports (use dumpbin if available, else just PE header data) + all_imports_flat = set() + if dumpbin: + imports = pe.get_imports_via_dumpbin() + for dll_imports in imports.values(): + all_imports_flat.update(dll_imports) + + score, reasons = score_target(pe, all_imports_flat) + + if score >= 15: # Only show interesting targets + targets.append({ + "path": str(binary), + "name": binary.name, + "score": score, + "reasons": reasons, + "is_64bit": pe.is_64bit, + "is_dll": pe.is_dll, + "exports_count": len(pe.exports) if pe.exports else 0, + "mitigations": { + "aslr": pe.has_aslr, + "dep": pe.has_dep, + "cfg": pe.has_cfg, + } + }) + except Exception as e: + continue + + # Progress + if (i + 1) % 50 == 0: + print(f" [*] Analyzed {i + 1}/{len(binaries)}...") + + # Sort by score + targets.sort(key=lambda x: x["score"], reverse=True) + + # Display results + print(f"\n{'='*70}") + print(f" TOP FUZZING TARGETS (scored by attack surface)") + print(f"{'='*70}\n") + + if not targets: + print(" No suitable targets found in this directory.\n") + return + + for i, t in enumerate(targets[:25]): + arch = "x64" if t["is_64bit"] else "x86" + kind = "DLL" if t["is_dll"] else "EXE" + bar = "█" * (t["score"] // 5) + "░" * (20 - t["score"] // 5) + + print(f" #{i+1:2d} [{t['score']:3d}/100] {bar} {t['name']}") + print(f" {kind} | {arch} | {t['path']}") + for r in t["reasons"]: + print(f" • {r}") + print() + + # Save results + results_file = os.path.join(str(scan_path), "target_scan_results.json") + try: + with open(results_file, "w") as f: + json.dump(targets, f, indent=2) + print(f" [+] Full results saved to: {results_file}") + except IOError: + results_file = "target_scan_results.json" + with open(results_file, "w") as f: + json.dump(targets, f, indent=2) + print(f" [+] Full results saved to: {results_file}") + + print(f" [+] Total targets scored ≥15: {len(targets)}") + print() + + return targets + + +def analyze_binary(binary_path): + """Deep analysis of a single binary.""" + print(f"\n{'='*70}") + print(f" WinAFL Target Finder - Deep Analysis") + print(f" Binary: {binary_path}") + print(f"{'='*70}\n") + + if not os.path.exists(binary_path): + print(f" [-] File not found: {binary_path}") + return + + pe = PEAnalyzer(binary_path) + + print(f" Architecture: {'x64' if pe.is_64bit else 'x86'}") + print(f" Type: {'DLL' if pe.is_dll else 'EXE'}") + print(f" ASLR: {'Yes' if pe.has_aslr else 'No'}") + print(f" DEP/NX: {'Yes' if pe.has_dep else 'No'}") + print(f" CFG: {'Yes' if pe.has_cfg else 'No'}") + print() + + # Sections + print(f" Sections:") + for sec in pe.sections: + print(f" {sec['name']:10s} {sec['virtual_size']:>10,d} bytes") + print() + + # Imports + imports = pe.get_imports_via_dumpbin() + if imports: + all_flat = set() + for dll, funcs in imports.items(): + all_flat.update(funcs) + + print(f" Imported DLLs: {len(imports)}") + print(f" Total imports: {len(all_flat)}") + print() + + # Categorize + file_hits = FILE_PARSING_IMPORTS.intersection(all_flat) + data_hits = DATA_PROCESSING_IMPORTS.intersection(all_flat) + net_hits = NETWORK_IMPORTS.intersection(all_flat) + + if file_hits: + print(f" 📂 File I/O Functions ({len(file_hits)}):") + for f in sorted(file_hits): + print(f" {f}") + print() + + if data_hits: + print(f" 🧠 Memory/Data Functions ({len(data_hits)}):") + for f in sorted(data_hits): + print(f" {f}") + print() + + if net_hits: + print(f" 🌐 Network Functions ({len(net_hits)}):") + for f in sorted(net_hits): + print(f" {f}") + print() + + # Exports + exports = pe.get_exports_via_dumpbin() + if exports: + print(f" Exported Functions ({len(exports)}):") + # Highlight functions that look like parsers + parser_keywords = ["parse", "read", "load", "decode", "open", "process", "import", "extract", "init"] + highlighted = [] + normal = [] + for exp in exports: + name = exp.get("name", "") + if any(kw in name.lower() for kw in parser_keywords): + highlighted.append(exp) + else: + normal.append(exp) + + if highlighted: + print(f"\n ⭐ HIGH-VALUE EXPORTS (potential target functions):") + for exp in highlighted: + print(f" 0x{exp['rva']} {exp['name']}") + + if normal and len(normal) <= 30: + print(f"\n Other exports:") + for exp in normal: + print(f" 0x{exp['rva']} {exp['name']}") + elif normal: + print(f"\n ... and {len(normal)} other exports (use dumpbin /exports for full list)") + + # Score + all_flat = set() + for funcs in imports.values(): + all_flat.update(funcs) + score, reasons = score_target(pe, all_flat) + + print(f"\n {'='*50}") + print(f" FUZZ SCORE: {score}/100") + for r in reasons: + print(f" • {r}") + print(f" {'='*50}\n") + + +# ============================================================================ +# Seed Corpus Builder +# ============================================================================ + +def build_seed_corpus(fmt, output_dir, afl_tmin_path=None, drio_dir=None, target_cmd=None): + """Build and optionally minimize a seed corpus for a given file format.""" + fmt = fmt.lower().lstrip(".") + + print(f"\n{'='*70}") + print(f" WinAFL Target Finder - Seed Corpus Builder") + print(f" Format: .{fmt}") + print(f"{'='*70}\n") + + if fmt not in SEED_SOURCES: + print(f" [-] Unknown format '.{fmt}'. Supported formats:") + for k in sorted(SEED_SOURCES.keys()): + print(f" .{k}") + print() + print(f" Tip: Manually create a folder with small sample files of your target format.") + return + + config = SEED_SOURCES[fmt] + os.makedirs(output_dir, exist_ok=True) + + found_files = [] + for search_dir in config["dirs"]: + if not os.path.exists(search_dir): + continue + for root, dirs, files in os.walk(search_dir): + # Don't recurse too deep + depth = root.replace(search_dir, "").count(os.sep) + if depth > 3: + continue + for f in files: + if f.lower().endswith(config["ext"]): + full = os.path.join(root, f) + try: + sz = os.path.getsize(full) + if config["min_size"] <= sz <= config["max_size"]: + found_files.append((full, sz)) + except OSError: + continue + + print(f" [*] Searched {len(config['dirs'])} system directories") + print(f" [+] Found {len(found_files)} candidate files (size {config['min_size']}-{config['max_size']} bytes)") + print() + + if not found_files: + print(f" [!] No suitable files found. Creating a minimal synthetic seed...") + # Create a minimal valid-ish file + seed_path = os.path.join(output_dir, f"minimal.{fmt}") + _create_minimal_seed(fmt, seed_path) + print(f" [+] Created: {seed_path}") + return + + # Sort by size (prefer smaller) and take top 20 + found_files.sort(key=lambda x: x[1]) + selected = found_files[:20] + + print(f" [*] Copying {len(selected)} smallest files to {output_dir}...") + for i, (src, sz) in enumerate(selected): + dst = os.path.join(output_dir, f"seed_{i:03d}{config['ext']}") + try: + with open(src, "rb") as fin, open(dst, "wb") as fout: + fout.write(fin.read()) + print(f" {dst} ({sz:,d} bytes)") + except IOError as e: + print(f" [!] Failed to copy {src}: {e}") + + # Minimize with afl-tmin if available + if afl_tmin_path and drio_dir and target_cmd: + print(f"\n [*] Minimizing seeds with afl-tmin...") + min_dir = output_dir + "_minimized" + os.makedirs(min_dir, exist_ok=True) + + for seed_file in os.listdir(output_dir): + seed_path = os.path.join(output_dir, seed_file) + min_path = os.path.join(min_dir, seed_file) + cmd = f'"{afl_tmin_path}" -D "{drio_dir}" -t 5000 -i "{seed_path}" -o "{min_path}" -- {target_cmd}' + print(f" Minimizing: {seed_file}") + try: + subprocess.run(cmd, shell=True, timeout=30, capture_output=True) + except subprocess.TimeoutExpired: + print(f" [!] Timeout on {seed_file}, keeping original") + import shutil + shutil.copy2(seed_path, min_path) + + print(f"\n [+] Minimized corpus: {min_dir}") + else: + print(f"\n Tip: To minimize seeds, provide --afl-tmin, --drio-dir, and --target-cmd") + + print(f"\n [+] Seed corpus ready: {output_dir}") + print(f" [+] Total seeds: {len(selected)}\n") + + +def _create_minimal_seed(fmt, path): + """Create a minimal but structurally valid seed file.""" + seeds = { + "pdf": b"%PDF-1.0\n1 0 obj<>endobj\n2 0 obj<>endobj\n3 0 obj<>endobj\nxref\n0 4\ntrailer<>\nstartxref\n0\n%%EOF", + "png": bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, # PNG signature + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, # IHDR chunk + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, # 1x1 + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, + 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, + 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC, + 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, + 0x44, 0xAE, 0x42, 0x60, 0x82]), + "bmp": (b"BM" + struct.pack("\ndata', + "json": b'{"key": "value", "num": 42, "arr": [1, 2, 3]}', + "zip": bytes([0x50, 0x4B, 0x05, 0x06] + [0x00] * 18), # Empty ZIP end-of-central-directory + "wav": b"RIFF\x24\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\x44\xAC\x00\x00\x88\x58\x01\x00\x02\x00\x10\x00data\x00\x00\x00\x00", + } + + data = seeds.get(fmt, b"FUZZ" + bytes(range(256))) + with open(path, "wb") as f: + f.write(data) + + +# ============================================================================ +# Harness Discovery (automated target function identification) +# ============================================================================ + +# Functions that indicate a candidate is a file-parsing entry point +ENTRY_POINT_CALLS = { + # File open/read functions — the candidate should call these + "CreateFileA", "CreateFileW", "CreateFileExW", + "ReadFile", "ReadFileEx", + "MapViewOfFile", "CreateFileMappingW", "CreateFileMappingA", + "fopen", "_wfopen", "fread", "_read", "fgets", + "fclose", "CloseHandle", # must close handle for WinAFL +} + +# Functions that DISQUALIFY a candidate (won't return normally) +DISQUALIFYING_CALLS = { + "ExitProcess", "TerminateProcess", "abort", "exit", "_exit", + "FatalAppExitA", "FatalAppExitW", "RaiseException", +} + +# Keywords in export names that suggest parsing/processing +PARSER_EXPORT_KEYWORDS = [ + "parse", "read", "load", "open", "decode", "import", "extract", + "process", "handle", "analyze", "convert", "transform", "render", + "inflate", "decompress", "unpack", "deserialize", "unmarshal", + "scan", "lex", "tokenize", "eval", "execute", "interpret", + "create_from", "init_from", "from_file", "from_stream", "from_buffer", + "read_file", "load_file", "open_file", "parse_file", + "readimage", "loadimage", "openimage", "decodeimage", + "input", "ingest", "consume", "accept", +] + + +def find_harness_candidates(binary_path, drio_dir=None): + """Identify candidate target functions and offsets for WinAFL harnessing.""" + print(f"\n{'='*70}") + print(f" WinAFL Target Finder - Harness Discovery") + print(f" Binary: {binary_path}") + print(f"{'='*70}\n") + + if not os.path.exists(binary_path): + print(f" [-] File not found: {binary_path}") + return + + pe = PEAnalyzer(binary_path) + module_name = os.path.basename(binary_path) + is_dll = pe.is_dll + + print(f" Architecture: {'x64' if pe.is_64bit else 'x86'}") + print(f" Type: {'DLL' if is_dll else 'EXE'}") + print() + + candidates = [] + + # ---- Strategy 1: Export analysis (DLLs) ---- + if is_dll: + print(f" [*] Strategy 1: Analyzing exported functions...") + exports = pe.get_exports_via_dumpbin() + if exports: + for exp in exports: + name = exp.get("name", "") + rva = exp.get("rva", "0") + score = 0 + reasons = [] + + # Score by name matching — two tiers + name_lower = name.lower() + + # Tier 1: High-value file I/O entry points (best WinAFL targets) + tier1_keywords = [ + "fromfile", "from_file", "fromstream", "from_stream", + "loadimage", "load_image", "readfile", "read_file", + "openfile", "open_file", "parsefile", "parse_file", + "frombuffer", "from_buffer", "loadfrom", "readfrom", + "createfrom", "create_from", "initfrom", "init_from", + ] + tier1_hit = False + for kw in tier1_keywords: + if kw in name_lower: + score += 25 + reasons.append(f"HIGH: File I/O entry ('{kw}')") + tier1_hit = True + break + + # Tier 2: General parser/processing keywords + if not tier1_hit: + for kw in PARSER_EXPORT_KEYWORDS: + if kw in name_lower: + score += 15 + reasons.append(f"Name contains '{kw}'") + break + + # Penalize internal/helper-looking functions + if name.startswith("_") and not name.startswith("__"): + score -= 5 + if any(x in name_lower for x in ["internal", "private", "helper", "util", "debug", "test", "log"]): + score -= 10 + + # Bonus for functions that look like entry points + if any(x in name_lower for x in ["main", "entry", "start", "run", "exec"]): + score += 5 + reasons.append("Entry-point pattern") + + if score > 0: + candidates.append({ + "name": name, + "offset": rva, + "score": score, + "reasons": reasons, + "source": "export", + }) + print(f" Found {len(exports)} exports, {len(candidates)} look promising") + else: + print(f" No exports found (or dumpbin unavailable)") + print() + + # ---- Strategy 2: Disassembly analysis ---- + print(f" [*] Strategy 2: Disassembly analysis (tracing file I/O call sites)...") + call_site_candidates = _find_call_sites(binary_path, pe.is_64bit) + candidates.extend(call_site_candidates) + print(f" Found {len(call_site_candidates)} functions referencing file I/O") + print() + + # ---- Strategy 3: String references ---- + print(f" [*] Strategy 3: Scanning for file-format string references...") + string_candidates = _find_string_refs(binary_path) + candidates.extend(string_candidates) + print(f" Found {len(string_candidates)} functions near format-related strings") + print() + + # Deduplicate by offset — take max score, merge unique reasons + seen_offsets = {} + for c in candidates: + key = c["offset"] + if key not in seen_offsets: + seen_offsets[key] = c + else: + existing = seen_offsets[key] + existing["score"] = max(existing["score"], c["score"]) + for r in c["reasons"]: + if r not in existing["reasons"]: + existing["reasons"].append(r) + if c["name"] and not existing["name"]: + existing["name"] = c["name"] + candidates = list(seen_offsets.values()) + + # Sort by score + candidates.sort(key=lambda x: x["score"], reverse=True) + + # Display + print(f" {'='*60}") + print(f" CANDIDATE TARGET FUNCTIONS (ranked by suitability)") + print(f" {'='*60}\n") + + if not candidates: + print(" No candidates found. Try manual analysis with IDA/Ghidra.") + print(" Look for functions that:") + print(" 1. Open a file (CreateFileW, fopen)") + print(" 2. Read and process data (ReadFile, fread)") + print(" 3. Close the handle (CloseHandle, fclose)") + print(" 4. Return normally (no ExitProcess)") + return + + top = candidates[:15] + for i, c in enumerate(top): + bar = "#" * min(c["score"] // 3, 20) + name_str = c["name"] if c["name"] else f"sub_{c['offset']}" + print(f" #{i+1:2d} [Score: {c['score']:3d}] 0x{c['offset']} {name_str}") + for r in c["reasons"]: + print(f" + {r}") + print() + + # Generate verify/generate commands for top candidate + best = top[0] + offset = best["offset"] + print(f" {'='*60}") + print(f" RECOMMENDED NEXT STEPS") + print(f" {'='*60}\n") + print(f" Best candidate: 0x{offset} ({best['name'] or 'unnamed'})\n") + + if drio_dir: + print(f" 1. Verify the harness:") + print(f" python winafl-target-finder.py verify \"{binary_path}\" {offset} \"{drio_dir}\"\n") + print(f" 2. If verification passes, generate the launch command:") + print(f" python winafl-target-finder.py generate \"{binary_path}\" {offset} --drio \"{drio_dir}\"\n") + else: + print(f" 1. Verify the harness:") + print(f" python winafl-target-finder.py verify \"{binary_path}\" {offset} \n") + print(f" 2. Generate the launch command:") + print(f" python winafl-target-finder.py generate \"{binary_path}\" {offset} --drio \n") + + print(f" If the top candidate doesn't work, try the next ones in the list.\n") + + return candidates + + +def _find_call_sites(binary_path, is_64bit): + """Use dumpbin /disasm to find functions that reference file I/O APIs.""" + candidates = [] + + try: + dumpbin = DUMPBIN_PATH or "dumpbin" + # Use dumpbin /disasm — this can be large, so we limit output + result = subprocess.run( + [dumpbin, "/disasm", binary_path], + capture_output=True, text=True, timeout=60, + creationflags=subprocess.CREATE_NO_WINDOW + ) + if result.returncode != 0: + return candidates + + lines = result.stdout.splitlines() + except (FileNotFoundError, subprocess.TimeoutExpired, MemoryError): + return candidates + + # Parse disassembly to find functions that call file I/O + current_func_offset = None + current_func_name = None + file_io_calls = set() + disqualified = False + close_calls = False + + for line in lines: + stripped = line.strip() + + # Function header: " 0000000140001000: 48 89 5C 24 08 mov qword ptr [rsp+8],rbx" + # or label: "Module!FuncName:" + # New function detected by address gap or label + func_match = re.match(r'^\s*([0-9A-Fa-f]{8,16}):', stripped) + label_match = re.match(r'^(\w+!)?(\w+):$', stripped) + + if label_match: + # Save previous function + if current_func_offset and file_io_calls and not disqualified: + score = len(file_io_calls) * 10 + if close_calls: + score += 15 # Bonus: closes handles (required for WinAFL) + reasons = [f"Calls: {', '.join(sorted(file_io_calls))}"] + if close_calls: + reasons.append("Closes file handles (WinAFL compatible)") + candidates.append({ + "name": current_func_name or "", + "offset": current_func_offset, + "score": score, + "reasons": reasons, + "source": "disasm", + }) + + current_func_name = label_match.group(2) + current_func_offset = None + file_io_calls = set() + disqualified = False + close_calls = False + + elif func_match and current_func_offset is None: + # Capture the first address as the function offset + addr = func_match.group(1) + # Convert to RVA (strip image base) + try: + addr_int = int(addr, 16) + if is_64bit and addr_int > 0x140000000: + current_func_offset = format(addr_int - 0x140000000, 'X') + elif not is_64bit and addr_int > 0x10000000: + current_func_offset = format(addr_int - 0x10000000, 'X') + else: + current_func_offset = addr + except ValueError: + current_func_offset = addr + + # Check for CALL instructions to file I/O functions + if "call" in stripped.lower(): + for api in ENTRY_POINT_CALLS: + if api in stripped: + file_io_calls.add(api) + break + for api in DISQUALIFYING_CALLS: + if api in stripped: + disqualified = True + break + if "CloseHandle" in stripped or "fclose" in stripped: + close_calls = True + + # Don't forget the last function + if current_func_offset and file_io_calls and not disqualified: + score = len(file_io_calls) * 10 + if close_calls: + score += 15 + reasons = [f"Calls: {', '.join(sorted(file_io_calls))}"] + if close_calls: + reasons.append("Closes file handles (WinAFL compatible)") + candidates.append({ + "name": current_func_name or "", + "offset": current_func_offset, + "score": score, + "reasons": reasons, + "source": "disasm", + }) + + return candidates + + +def _find_string_refs(binary_path): + """Scan for format-related strings that suggest parsing logic nearby.""" + candidates = [] + + # Common format magic / header strings + format_strings = [ + b"%PDF", b"PNG", b"JFIF", b"GIF8", b"RIFF", b"BM", + b"PK\x03\x04", # ZIP + b" 0, + f" ✅ Seeds found: {len(seeds)} files", + f" ❌ No seeds! Add sample files to {input_dir}/") + ) + + all_ok = True + for ok, good_msg, bad_msg in checks: + print(good_msg if ok else bad_msg) + if not ok: + all_ok = False + + print() + + if all_ok: + print(f" 🚀 Ready to fuzz! Copy and run the command above.") + else: + print(f" ⚠️ Fix the issues above before fuzzing.") + + print() + return cmd + + +# ============================================================================ +# CLI +# ============================================================================ + +def main(): + parser = argparse.ArgumentParser( + prog="winafl-target-finder", + description="Automated fuzzing campaign bootstrap utility for WinAFL + GPU", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s scan "C:\\Program Files\\SomeApp" + %(prog)s analyze target.dll + %(prog)s seeds png ./in + %(prog)s verify target.exe 12A40 C:\\DynamoRIO + %(prog)s generate target.exe 12A40 --drio C:\\DynamoRIO --gpu + """ + ) + + subparsers = parser.add_subparsers(dest="command", help="Command to run") + + # scan + sp_scan = subparsers.add_parser("scan", help="Scan directory for fuzzable targets") + sp_scan.add_argument("path", help="Directory to scan") + sp_scan.add_argument("--depth", type=int, default=3, help="Max recursion depth") + + # analyze + sp_analyze = subparsers.add_parser("analyze", help="Deep-analyze a single binary") + sp_analyze.add_argument("binary", help="Path to EXE or DLL") + + # seeds + sp_seeds = subparsers.add_parser("seeds", help="Build a minimal seed corpus") + sp_seeds.add_argument("format", help="File format (e.g., png, pdf, xml)") + sp_seeds.add_argument("output", help="Output directory for seeds") + sp_seeds.add_argument("--afl-tmin", help="Path to afl-tmin.exe for minimization") + sp_seeds.add_argument("--drio-dir", help="DynamoRIO directory (for afl-tmin)") + sp_seeds.add_argument("--target-cmd", help="Target command line (for afl-tmin)") + + # harness + sp_harness = subparsers.add_parser("harness", help="Find candidate target functions for WinAFL harnessing") + sp_harness.add_argument("binary", help="Target EXE or DLL to analyze") + sp_harness.add_argument("--drio", help="DynamoRIO directory (for generated verify commands)") + + # verify + sp_verify = subparsers.add_parser("verify", help="Verify a DynamoRIO harness") + sp_verify.add_argument("binary", help="Target binary") + sp_verify.add_argument("offset", help="Target function offset (hex, no 0x prefix)") + sp_verify.add_argument("drio_dir", help="DynamoRIO directory") + sp_verify.add_argument("--nargs", type=int, default=2, help="Number of arguments") + sp_verify.add_argument("--iterations", type=int, default=10, help="Test iterations") + + # generate + sp_gen = subparsers.add_parser("generate", help="Generate afl-fuzz launch command") + sp_gen.add_argument("binary", help="Target binary") + sp_gen.add_argument("offset", help="Target function offset (hex, no 0x prefix)") + sp_gen.add_argument("--drio", help="DynamoRIO directory") + sp_gen.add_argument("--input", default="in", help="Input seed directory") + sp_gen.add_argument("--output", default="out", help="Output directory") + sp_gen.add_argument("--timeout", type=int, default=2000, help="Timeout in ms") + sp_gen.add_argument("--gpu", action="store_true", default=True, help="Enable GPU (default)") + sp_gen.add_argument("--no-gpu", action="store_true", help="Disable GPU") + sp_gen.add_argument("--coverage-module", help="Module to collect coverage for") + sp_gen.add_argument("--winafl-dll", help="Path to winafl.dll") + sp_gen.add_argument("--fuzz-iterations", type=int, default=5000) + + # all + sp_all = subparsers.add_parser("all", help="Run the entire 4-step fuzzing pipeline automatically") + sp_all.add_argument("binary", help="Target EXE or DLL to fuzz") + sp_all.add_argument("format", help="Input format for seeds (e.g., pdf, png)") + sp_all.add_argument("--drio", help="DynamoRIO directory", required=True) + sp_all.add_argument("--out", default="out", help="Output directory") + sp_all.add_argument("--gen-harness", action="store_true", help="Also generate harness C code") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + if args.command == "scan": + scan_directory(args.path, args.depth) + + elif args.command == "analyze": + analyze_binary(args.binary) + + elif args.command == "harness": + find_harness_candidates(args.binary, drio_dir=args.drio) + + elif args.command == "seeds": + build_seed_corpus( + args.format, args.output, + afl_tmin_path=getattr(args, 'afl_tmin', None), + drio_dir=getattr(args, 'drio_dir', None), + target_cmd=getattr(args, 'target_cmd', None), + ) + + elif args.command == "verify": + verify_harness(args.binary, args.offset, args.drio_dir, + nargs=args.nargs, iterations=args.iterations) + + elif args.command == "generate": + generate_launch_command( + args.binary, args.offset, + drio_dir=args.drio, + input_dir=args.input, + output_dir=args.output, + timeout=args.timeout, + gpu=not args.no_gpu, + coverage_module=args.coverage_module, + fuzz_iterations=args.fuzz_iterations, + winafl_dll=args.winafl_dll, + ) + + elif args.command == "all": + print(f"\n======================================================================") + print(f" WinAFL Target Finder - AUTO-PILOT MODE") + print(f"======================================================================\n") + + # Step 1: Find best offset + print(f"[*] STEP 1: Analyzing {args.binary} for the best target offset...") + candidates = find_harness_candidates(args.binary, drio_dir=args.drio) + if not candidates: + print(f"[-] No suitable targets found. Aborting.") + return + + best_candidate = candidates[0] + best_offset = best_candidate.get("offset") + if best_offset == "MANUAL": + best_offset = candidates[1].get("offset") if len(candidates) > 1 else None + + if not best_offset: + print(f"[-] Could not determine an exact offset automatically. Aborting.") + return + + print(f"[+] Selected best target: {best_candidate.get('name')} at offset 0x{best_offset}\n") + + # Step 2: Generate Harness Code + print(f"[*] STEP 2: Writing Harness C Code...") + harness_builder = "winafl-harness-builder.py" + if os.path.exists(harness_builder) or os.path.exists(os.path.join(os.path.dirname(__file__), harness_builder)): + harness_path = harness_builder if os.path.exists(harness_builder) else os.path.join(os.path.dirname(__file__), harness_builder) + cmd = f'python "{harness_path}" generate "{args.binary}" --offset {best_offset}' + print(f" Running: {cmd}") + # Try to pipe the JSON exactly as they would manually + try: + # Capture just the code to disk + import tempfile + proc = subprocess.run(f'python "{harness_path}" generate "{args.binary}" --offset {best_offset} > auto_harness.c', shell=True) + print(f"[+] Wrote auto_harness.c to current directory.\n") + except Exception as e: + print(f"[-] Failed to invoke harness builder: {e}\n") + else: + print(f"[-] winafl-harness-builder.py not found. Skipping harness generation.\n") + + # Step 3: Build Seed Corpus + print(f"[*] STEP 3: Building seed corpus for '{args.format}'...") + in_dir = os.path.join(args.out, "in_dir") + build_seed_corpus(args.format, in_dir) + + # Step 4: Generate Launch Command + out_dir = os.path.join(args.out, "out_dir") + print(f"[*] STEP 4: Generating Fuzzer Launch Command...\n") + generate_launch_command( + args.binary, best_offset, + drio_dir=args.drio, + input_dir=in_dir, + output_dir=out_dir, + timeout=2000, + gpu=True, + coverage_module=None, + fuzz_iterations=5000, + winafl_dll=None, + ) + print(f"[+] Auto-Pilot complete. Compile auto_harness.c and run the command above!") + + +if __name__ == "__main__": + main()