文章目录
开篇简述
昨天发布了二进制漏洞挖掘(AFL Fuzzing)使用篇阅读量今日竟然达到了400+阅读私信的朋友也有在问Windows下如何使用AFL,本篇文章就重点将Windows下的AFL,就是WinAFL的安装与使用。
WinAFL编译与安装
1.AFL与WinAFL的区别
之前发布的二进制漏洞挖掘(AFL Fuzzing)使用篇使用的就是AFL与AFL++它们是专门针对于Linux环境下的支持源码插桩和QEMU动态插桩,WinAFL只支持DynamoRIO动态插桩不过也有一些文章配置可以将WinAFL进行源码插桩不过本篇文章不打算深入其中,只介绍使用最常用的WinAFL中的DynamoRIO动态插桩技术。
AFL维护是Michał Zalewski(Google)→ 现已无人维护。
AFL++维护是Marc "van Hauser" Heuse 等社区开发者。
WinAFL维护是Google Project Zero(GPZ) 是 Google 旗下的安全研究团队。
2.WinAFL与DynamoRIO准备
3.WinAFL编译
下载好上述的源码之后就是安装编译环境需要Visual Studio和CMake这里我已经安装好了然后就是编译命令。
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build --config Release
bash
#第一步:配置项目(生成 Visual Studio 工程)
#编译的时候需要指定DynamoRIO
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DDynamoRIO_DIR="C:\Users\admin\Documents\WinAFL\DynamoRIO-Windows-11.3.0\DynamoRIO-Windows-11.3.0-1\cmake"
#第二步:编译项目
cmake --build build --config Release
这里编译遇到一个坑点2026年2月5日还在那就是跟DynamoRIO不兼容修复可以参考
https://github.com/googleprojectzero/winafl/issues/479
编译成功后有如下文件:


4.DynamoRIO安装
访问之后下载对应的releases

下载zip解压之后是下面的目录
DynamoRIO-Windows-11.3.0-1/
├── ACKNOWLEDGEMENTS # 致谢名单
├── LICENSE.txt # 许可证
├── README # 简要说明
├── bin32/ # 32位核心工具
├── bin64/ # 64位核心工具 ⭐WinAFL主要使用
├── cmake/ # CMake配置文件
├── docs/ # 官方文档
├── drmemory/ # Dr.Memory工具
├── dynamorio/ # 运行时核心文件
├── ext/ # 扩展库
├── include/ # 开发头文件
├── lib32/ # 32位静态库
├── lib64/ # 64位静态库
├── logs/ # 日志输出
├── samples/ # 示例代码
└── tools/ # 额外工具

WinAFL使用
WinAFL使用-D命令指定DynamoRIO的bin64目录即可,完整的WinAFL命令如下:
bash
afl-fuzz.exe ^
-i in ^
-o out ^
-D "C:\path\to\DynamoRIO\bin64" ^
-t 2000+ ^
--coverage_module target.exe ^
--target_module target.exe ^
--target_offset 0x1000 ^
--fuzz_iterations 5000 ^
-f 1024 ^
-- target.exe @@
| 参数 | 示例值 | 说明 |
|---|---|---|
-i |
in |
输入目录:存放种子文件的文件夹 |
-o |
out |
输出目录:AFL输出结果(崩溃、超时、队列) |
-D |
C:\DynamoRIO\bin64 |
DynamoRIO路径:插桩引擎所在目录 |
--coverage_module |
target.exe |
覆盖率模块:记录代码覆盖率的模块名(可多个,逗号分隔) |
--target_module |
target.exe |
目标模块:包含目标函数的模块(用于计算偏移) |
--target_offset |
0x1000 |
函数偏移:目标函数在模块内的RVA地址(十六进制) |
-- |
-- |
分隔符:区分AFL参数和目标程序参数 |
target.exe |
target.exe |
目标程序:被测试的可执行文件 |
@@ |
@@ |
输入占位符:会被替换为变异后的文件路径 |
可选参数
| 参数 | 示例值 | 说明 |
|---|---|---|
-t |
2000+ |
超时时间 :毫秒,+表示自动调整(默认1000) |
--fuzz_iterations |
5000 |
迭代次数:每次持久模式循环执行次数 |
-f |
1024 |
内存模式 :数据大小(字节),不用@@时用 |
-m |
none |
内存限制 :目标程序内存限制(默认50MB,none不限) |
--target_method |
fuzz |
函数名:用导出函数名代替偏移(优先于offset) |
--nargs |
2 |
参数数量:目标函数的参数个数(默认2) |
-Y |
无 | 启用网络模式:测试网络程序 |
-w |
100 |
网络超时:网络模式的超时时间 |
-p |
1234 |
目标端口:网络模式的目标端口 |
1.WinAFL与Wrapper(包装函数)
通过上面的参数基本了解了WinAFL有哪些功能,WinAFL在Fuzzing的时候只会传入第一个参数进行模糊测试,比如一个函数第一个参数是文件路径第二个参数是大小第三个是类型,那么这个情况下可以直接使用WinAFL进行Fuzzing因为WinAFL默认情况下只会对第一个参数进行种子变异模糊测试,那么如果第一个参数是文件大小第二个参数是文件路径不就不可以了吗,这个时候就出现了Wrapper技术他在Fuzzing是非常常见的,本质上就是在实现一个程序这个程序接受一个参数由WinAFL提供然后自己程序在此调用需要模糊测试的比如某个DLL的导出函数顺序这里自己就可以随意控制了,这就是Wrapper技术。
2.使用Wrapper进行Fuzzing漏洞DLL
这里就要开始实战演示环节了需要用到2个程序分别是Wrapper包装函数用来与WinAFL做沟通,第2个就是要真正测试的程序这里我设计成了一个DLL然后有一个漏洞缺陷的导出函数来演示。
Wrapper程序源码
cpp
// main.c: WinAFL harness wrapper for dlldemo
//
// 演示目的:展示为什么需要
//
// 目标函数需要 3 个参数:
// 1. int size - 数据大小
// 2. char* filepath - 文件路径
// 3. int type - 操作类型
//
// 但 WinAFL fuzzer 只能提供原始字节流,
// wrapper 的作用就是:把原始字节解析成目标函数需要的参数格式
#include "pch.h"
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include "dlldemo.h"
#define MAX_INPUT_SIZE 4096
#define MAX_PATH_SIZE 256
// 函数指针类型
typedef void (*vulnerable_function_t)(int size, char* filepath, int type);
// 从原始字节流解析参数的简易协议:
// [0-3]字节: int size (小端序)
// [4]字节: int type
// [5+]字节: filepath字符串 (以\0结尾)
int parse_input(unsigned char* data, size_t len, int* size, int* type, char** filepath)
{
if (len < 6) {
printf("输入数据太短\n");
return -1;
}
// 解析前4字节为 size
*size = *(int*)data;
// 第5字节为 type
*type = data[4];
// 剩余部分为 filepath
*filepath = (char*)(data + 5);
// 确保 filepath 以 \0 结尾
data[len - 1] = '\0';
printf("[Wrapper解析] size=%d, type=%d, filepath=%s\n", *size, *type, *filepath);
return 0;
}
int main(int argc, char** argv)
{
HMODULE hDll;
vulnerable_function_t vulnerableFunc;
FILE* fp;
unsigned char* buffer = NULL;
size_t fileSize;
int size, type;
char* filepath = NULL;
if (argc < 2) {
printf("用法: %s <输入文件>\n", argv[0]);
printf("WinAFL Wrapper 演示程序\n");
printf("作用:将 fuzzer 生成的原始字节流解析为函数参数\n");
return 1;
}
// 加载 DLL
hDll = LoadLibraryA("dlldemo.dll");
if (hDll == NULL) {
printf("错误:无法加载 dlldemo.dll\n");
return 1;
}
// 获取漏洞函数地址
vulnerableFunc = (vulnerable_function_t)GetProcAddress(hDll, "vulnerable_function");
if (vulnerableFunc == NULL) {
printf("错误:无法获取 vulnerable_function 地址\n");
FreeLibrary(hDll);
return 1;
}
// 读取 fuzzer 生成的输入文件
fopen_s(&fp, argv[1], "rb");
if (fp == NULL) {
printf("错误:无法打开输入文件: %s\n", argv[1]);
FreeLibrary(hDll);
return 1;
}
fseek(fp, 0, SEEK_END);
fileSize = ftell(fp);
fseek(fp, 0, SEEK_SET);
if (fileSize == 0 || fileSize > MAX_INPUT_SIZE) {
printf("错误:输入文件大小无效\n");
fclose(fp);
FreeLibrary(hDll);
return 1;
}
buffer = (unsigned char*)malloc(fileSize);
if (buffer == NULL) {
printf("错误:内存分配失败\n");
fclose(fp);
FreeLibrary(hDll);
return 1;
}
fread(buffer, 1, fileSize, fp);
fclose(fp);
// ==== 关键:Wrapper 的核心作用 ====
// 把 fuzzer 的原始字节流转换成目标函数需要的参数
if (parse_input(buffer, fileSize, &size, &type, &filepath) != 0) {
free(buffer);
FreeLibrary(hDll);
return 1;
}
// 调用 DLL 中的漏洞函数
printf("[Wrapper调用] 正在调用 vulnerable_function(%d, \"%s\", %d)...\n",
size, filepath, type);
vulnerableFunc(size, filepath, type);
printf("[Wrapper] 函数调用完成\n");
// 清理
free(buffer);
FreeLibrary(hDll);
return 0;
}
漏洞演示DLL代码
cpp
#ifndef DLLEMO_H
#define DLLEMO_H
#ifdef DLLEMO_EXPORTS
#define DLLEMO_API __declspec(dllexport)
#else
#define DLLEMO_API __declspec(dllimport)
#endif
#ifdef __cplusplus
extern "C" {
#endif
DLLEMO_API void vulnerable_function(int size, char* filepath, int type);
#ifdef __cplusplus
}
#endif
#endif // DLLEMO_H
cpp
// dllmain.c: DLL application entry point
#include "pch.h"
#include <windows.h>
// 存在缓冲区溢出漏洞的函数
// 参数1: size - 声称的数据大小
// 参数2: filepath - 文件路径字符串
// 参数3: type - 操作类型
DLLEMO_API void vulnerable_function(int size, char* filepath, int type)
{
char buffer[16];
// 漏洞:盲目信任 size 参数,没有验证实际长度
// 当 type == 1 时,使用 strcpy 复制 filepath
// 如果 filepath 实际长度 > 16,就会发生缓冲区溢出
if (type == 1) {
// 危险操作:没有检查 filepath 长度
printf("[类型1] 处理文件: %s, 声称大小: %d\n", filepath, size);
strcpy(buffer, filepath); // 漏洞点!filepath 太长会溢出
printf("缓冲区内容: %s\n", buffer);
}
else if (type == 2) {
// 另一种操作
printf("[类型2] 仅检查文件路径长度\n");
if (strlen(filepath) < 16) {
strcpy(buffer, filepath);
printf("安全复制: %s\n", buffer);
} else {
printf("路径太长,已拒绝\n");
}
}
else {
printf("[类型%d] 未知操作类型\n", type);
}
}
// DLL entry point
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
将上面程序编译好之后就可以使用WinAFL配合DynamoRIO对wrapper进行二进制插桩对DLL导出函数vulnerable_function进行模糊测试。
创建种子:A到seed.txt
cpp
mkdir in out
echo AAAAAAAAAAAAAAAAAAAAAAA > in\seed.txt

我这里将exe的动态基址关闭了方便后面调试,我要进行Fuzzing的位置是wrapper的main位置所以需要知道具体偏移这里是0x1070,没有选择Fuzzing DLL是因为WinAFL只是对一个参数进行Fuzzing,通过上面的代码其实就可以看出了第一个参数是大小而我们是要对第二个参数文件路径来传入我们的畸形数据来测试的,需要注意的是WinAFL支持两种模式传入种子数据分别是文件模式和内存模式,@@参数就是文件模式,内存模式即直接传入缓冲区比如接受参数是byte * buffer这种不需要@@数据直接写内存不需要IO。
文件模式命令:
cpp
afl-fuzz.exe -i in -o out -D "C:\Users\admin\Documents\WinAFL\WinAFL\WinAFL\DynamoRIO-Windows-11.3.0\DynamoRIO-Windows-11.3.0-1\bin64" -t 20000 -- -coverage_module wrapperdemo.exe -target_module wrapperdemo.exe -target_offset 0x1070 -fuzz_iterations 20 -- wrapperdemo.exe @@

这里其实就跟我之前写的AFL一样了找到了一个crashes,后续的调试可以参考我的
二进制漏洞挖掘AFL其实套路是一样的后续只不过从GDB调试改成了XDBG调试来观察崩溃位置。