Listen, fuzzing is a whole other story. It all sounds great in theory: you take a program, run the fuzzer, and in a couple of hours it's found a bunch of bugs. In practice, you sit there and curse at the pitfalls. Especially when it comes to WinAFL, a fork of the famous AFL for Windows. There's supposedly documentation, but the actual path from downloading to working fuzzing is a quest.
I was recently messing around with an old program, and I've put it all together so you don't make the same mistakes I did.
---
What is WinAFL and why is it needed?
AFL (American Fuzzy Lop) was originally written for Linux. WinAFL is a Windows port that can fuzz closed-source programs. It can collect code coverage information in three ways:
· Via DynamoRIO (dynamic instrumentation)
· Via Syzygy (static instrumentation)
· Via IntelPT (tracing)
In practice, DynamoRIO is easiest to use—it installs, works, and doesn't require rebuilding.
Building WinAFL from Source
There are ready-made binaries in the repository, but they didn't work for me. So, we'll compile them ourselves. It's easier than it looks.
You'll need:
· Visual Studio 2019 Community Edition (select "Desktop C++ Development" during installation)
· The latest release of DynamoRIO
· WinAFL source code from GitHub
After installing Visual Studio, shortcuts to the command prompt appear in the Start menu: x86 Native Tools Command Prompt for 32-bit programs and x64 for 64-bit programs.
Open the desired file and navigate to the WinAFL source folder.
For the 32-bit version:
Bash:
mkdir build32 cd build32 cmake -G"Visual Studio 16 2019" -A Win32 .. -DDynamoRIO_DIR=C:\winafl_build\DynamoRIO-Windows-8.0.18915\cmake -DINTELPT=0 -DUSE_COLOR=1 cmake--build . --config Release
For 64-bit:
Bash:
mkdir build64 cd build64 cmake -G"Visual Studio 16 2019" -A x64 .. -DDynamoRIO_DIR=C:\winafl_build\DynamoRIO-Windows-8.0.18915\cmake -DINTELPT=0 -DUSE_COLOR=1 cmake--build . --config Release
After compilation, WinAFL binaries will appear in the build32/bin/Release or build64/bin/Release folder. Copy them, along with the DynamoRIO folder, to the virtual machine where we'll be fuzzing.
--
How to find a suitable target for fuzzing
WinAFL is easiest to use for programs that work with files. Network programs are also possible, but they require wrappers written using custom_net_fuzzer.dll, which is a real challenge.
We need a function that:
· Opens the input file
· Parses its contents
· Closes the file gracefully and cleans up after itself
· Exits without side effects
In reality, ideal goals are almost never achieved, but you can find close ones.
---
Looking for a function within a program
I took an old program, built statically, with an 8MB main EXE. It can open files, so it's a promising target.
First, I load the binary into IDA Pro. I search for CreateFileA in the imports and look for cross-references. There are several of them; I need to figure out which of these functions actually parses the file.
I run the program in x64dbg and pass it a test file (I opened the program manually, pressed all the buttons, saved the document, and used this file as a test).
In the Symbols tab, I select kernelbase.dll and set breakpoints on CreateFileA and CreateFileW. Why in the DLL and not in the EXE itself? Because that way there's less chance of missing a call via GetProcAddress, and it's clear where the calls are actually coming from.
After launching, the program crashes on the first CreateFileA. I look at the arguments—it opens some kind of service file, not ours. I press F9 until the path to my test file appears in the arguments.
Now I look at the Call Stack. The call isn't coming directly from the EXE, but through the CFile::Open function from mfc42.dll. I climb the stack, copy the return address, and open IDA.
I find the function that calls CFile::Open. In IDA, I see that it takes two arguments—file paths. It looks like one is an input, the other is temporary.
I set breaks at the beginning and end of this function. I run the program again. At the beginning, both arguments are paths. At the end, the temporary file is empty, and the input file is unchanged. This means this function isn't parsing the file; it's doing something with it, but not changing it.
I continue execution. The next call to CreateFileA shows a different stack trace. This function unzips the file. The program works with both compressed and uncompressed files. Uncompressed files are easier to fuzz—the code coverage is higher.
Now I'm looking for a function that works with an already uncompressed file. The easiest way is to find the one that first interacts with the input file and follow the stack trace to the function that takes the file path as an argument.
I check that this function terminates correctly. I break at the end, press F9, and everything runs to completion. I check in Process Explorer to see if the files are being closed—the handle to my test file isn't listed. This means the function is cleaning up after itself.
Great, target found.
---
WinAFL Arguments and Pitfalls
My WinAFL arguments look like this:
Bash:
afl-fuzz.exe -i c:\inputs -o c:\winafl_build\out-plain -D C:\winafl_build\DynamoRIO-Windows-8.0.18915\bin32 -t 40000 -x C:\winafl_build\test.dict -f tes
I was recently messing around with an old program, and I've put it all together so you don't make the same mistakes I did.
---
What is WinAFL and why is it needed?
AFL (American Fuzzy Lop) was originally written for Linux. WinAFL is a Windows port that can fuzz closed-source programs. It can collect code coverage information in three ways:
· Via DynamoRIO (dynamic instrumentation)
· Via Syzygy (static instrumentation)
· Via IntelPT (tracing)
In practice, DynamoRIO is easiest to use—it installs, works, and doesn't require rebuilding.
Building WinAFL from Source
There are ready-made binaries in the repository, but they didn't work for me. So, we'll compile them ourselves. It's easier than it looks.
You'll need:
· Visual Studio 2019 Community Edition (select "Desktop C++ Development" during installation)
· The latest release of DynamoRIO
· WinAFL source code from GitHub
After installing Visual Studio, shortcuts to the command prompt appear in the Start menu: x86 Native Tools Command Prompt for 32-bit programs and x64 for 64-bit programs.
Open the desired file and navigate to the WinAFL source folder.
For the 32-bit version:
Bash:
mkdir build32 cd build32 cmake -G"Visual Studio 16 2019" -A Win32 .. -DDynamoRIO_DIR=C:\winafl_build\DynamoRIO-Windows-8.0.18915\cmake -DINTELPT=0 -DUSE_COLOR=1 cmake--build . --config Release
For 64-bit:
Bash:
mkdir build64 cd build64 cmake -G"Visual Studio 16 2019" -A x64 .. -DDynamoRIO_DIR=C:\winafl_build\DynamoRIO-Windows-8.0.18915\cmake -DINTELPT=0 -DUSE_COLOR=1 cmake--build . --config Release
After compilation, WinAFL binaries will appear in the build32/bin/Release or build64/bin/Release folder. Copy them, along with the DynamoRIO folder, to the virtual machine where we'll be fuzzing.
--
How to find a suitable target for fuzzing
WinAFL is easiest to use for programs that work with files. Network programs are also possible, but they require wrappers written using custom_net_fuzzer.dll, which is a real challenge.
We need a function that:
· Opens the input file
· Parses its contents
· Closes the file gracefully and cleans up after itself
· Exits without side effects
In reality, ideal goals are almost never achieved, but you can find close ones.
---
Looking for a function within a program
I took an old program, built statically, with an 8MB main EXE. It can open files, so it's a promising target.
First, I load the binary into IDA Pro. I search for CreateFileA in the imports and look for cross-references. There are several of them; I need to figure out which of these functions actually parses the file.
I run the program in x64dbg and pass it a test file (I opened the program manually, pressed all the buttons, saved the document, and used this file as a test).
In the Symbols tab, I select kernelbase.dll and set breakpoints on CreateFileA and CreateFileW. Why in the DLL and not in the EXE itself? Because that way there's less chance of missing a call via GetProcAddress, and it's clear where the calls are actually coming from.
After launching, the program crashes on the first CreateFileA. I look at the arguments—it opens some kind of service file, not ours. I press F9 until the path to my test file appears in the arguments.
Now I look at the Call Stack. The call isn't coming directly from the EXE, but through the CFile::Open function from mfc42.dll. I climb the stack, copy the return address, and open IDA.
I find the function that calls CFile::Open. In IDA, I see that it takes two arguments—file paths. It looks like one is an input, the other is temporary.
I set breaks at the beginning and end of this function. I run the program again. At the beginning, both arguments are paths. At the end, the temporary file is empty, and the input file is unchanged. This means this function isn't parsing the file; it's doing something with it, but not changing it.
I continue execution. The next call to CreateFileA shows a different stack trace. This function unzips the file. The program works with both compressed and uncompressed files. Uncompressed files are easier to fuzz—the code coverage is higher.
Now I'm looking for a function that works with an already uncompressed file. The easiest way is to find the one that first interacts with the input file and follow the stack trace to the function that takes the file path as an argument.
I check that this function terminates correctly. I break at the end, press F9, and everything runs to completion. I check in Process Explorer to see if the files are being closed—the handle to my test file isn't listed. This means the function is cleaning up after itself.
Great, target found.
---
WinAFL Arguments and Pitfalls
My WinAFL arguments look like this:
Bash:
afl-fuzz.exe -i c:\inputs -o c:\winafl_build\out-plain -D C:\winafl_build\DynamoRIO-Windows-8.0.18915\bin32 -t 40000 -x C:\winafl_build\test.dict -f tes