0x01.环境准备
WinAFL 是一个模糊测试工具,基于 AFL (American Fuzzy Lop) 的一个变种,专为 Windows 平台设计。模糊测试(Fuzzing)是一种自动化软件测试技术,通过向目标程序提供大量随机或伪随机数据来发现程序中的漏洞和错误。WinAFL 利用了 DynamoRIO 这一动态插桩工具,可以在程序运行时插入自定义代码,跟踪和记录程序执行路径,以便更有效地发现漏洞。
下载 WinAFL 和 DynamoRIO:
•WinAFL:WinAFL GitHub仓库
•DynamoRIO:DynamoRIO 下载
编译构建:
32位版本:
mkdir build32
cd build32
cmake -G"Visual Studio 16 2019" -A Win32 .. -DDynamoRIO_DIR=C:\path\to\DynamoRIO\cmake -DINTELPT=1
cmake --build . --config Release
64位版本:
mkdir build64
cd build64
cmake -G"Visual Studio 16 2019" -A x64 .. -DDynamoRIO_DIR=C:\path\to\DynamoRIO\cmake -DINTELPT=1
cmake --build . --config Release
0x02.WinAFL 的工作原理
1.初始种子输入:
用户提供一个或多个初始输入文件,作为模糊测试的起点。
2.变异输入:
WinAFL 使用 AFL 的高效变异策略,生成大量变异输入文件。
3.执行目标程序:
通过 DynamoRIO 监控程序执行,收集覆盖率信息。
4.分析结果:
根据覆盖率信息评估变异输入的有效性,并优先选择覆盖新的代码路径的输入。
5.发现漏洞:
记录所有导致程序崩溃或异常行为的输入,以便进一步分析和修复。
大多数Win应用程序都是基于GUI的,对于 GUI 程序进行 Fuzzing(模糊测试),尤其是闭源程序,通常需要编写一个 harness 来控制程序的输入和执行路径。
在进行 Fuzzing 之前,需要对目标程序有一定的理解,包括它的输入点、关键函数以及用户交互模式。以下工具可以帮助你分析闭源程序:
•反编译工具:如 IDA Pro、Ghidra,用于逆向工程目标程序,找到关键函数和输入点。
•调试器:如 OllyDbg、x64dbg,用于动态分析程序的执行流,找出关键代码路径和函数。
这里重点关注的是如何选取目标函数(target function),比如一个查看各类文件程序,我们想fuzz文件解析函数,那么如何找到target function,target function需要满足什么条件呢?
target function执行周期内需满足以下几个条件:
1.打开输入文件
2.解析这个文件
3.关闭输入文件。如果文件未关闭,Winafl将无法对其重写
4.正常返回。WInafl会捕获此标志用于目标重定向,如果函数执行了ExitProcess(),Winafl将无法正常工作。
target function如何运行:
1.目标程序正常执行,直到到达target function为止
2.Winafl保存修改寄存器环境,开始插桩,并记录覆盖率、程序执行状态等
3.运行到函数return
4.Winafl统计覆盖率,重新写入输入文件,恢复寄存器环境,继续执行target function
5.目标函数运行指定的迭代次数后,目标进程将被终止并重新启动。目标函数返回后的代码将永远不会被执行
0x03.WinAFL 实战案例
某工业软件有个图片加载功能,现在想要用winafl针对这个图片解析功能Fuzzing,首先我们需要找到target function。
在软件加载图片的过程中,通过Process Monitor监控进程,我们观察ReadFile调用堆栈可以看出应该是在cximage.dll里面解析的图片。
用IDA打开cximage.dll并定位到解析函数
通过分析发现CxImage::Decode内部对不同格式的图片进行分类解析,比如对于BMP格式的图片,首先调用了一个构造函数构造了一个CxImage对象给v48,v48前四个字节指向CxImageBMP对象的虚表,然后调用CxImage::CopyInfo拷贝一些信息,最后调用sub_10001BD0来解析BMP图片。所以要fuzz该模块对bmp图片格式的解析选取sub_10001BD0最合适不过。
目标函数的参数构造:
sub_10001BD0,第一个参数指向CxImage结构体,上面已经给出了CxImage的构造函数、虚表偏移。通过调用构造函数和虚表偏移值可以完成前两步赋值操作。另外就是关于CxImage::CopyInfo(&48,v3),通过对该位置下断点,用Xdbg动态调试,通过比较该函数执行完后v48指向内存数据的变化,发现只是在偏移0x40处改变了一个字节,并且该字节的数值和构造函数的第二个参数有关。所以只需要在调用构造函数时第二个参数传0即可。
对于第二个参数a2,实际上是一个指向CxFile结构体的指针
CxFile结构体如下
struct CxFile
{
void ** pvtable;
FILE * phfile;
};
至此,我们分析了用于fuzz的目标函数sub_10001BD0,并解决了该函数的参数构造问题。
一个简化版的harness关键内容:
//定义bmp解析函数的函数指针
typedef char (__thiscall *pfun)(void* pthis, void* pcxfile);
pfun BmpFun = 0;
//定义CxImage结构体构造函数的函数指针
...
...
int praseFile(wchar_t * path,void* pcximage)
{
FILE* hfile = WfopenFun(path, L"rb");
CxFile cxfile;
cxfile.pvtable = (void**)cxfilevtable;
cxfile.phfile = hfile;
void* pcximage = malloc(0x250);
CxImageFun(pcximage, 0);
BmpFun(pcximage, &cxfile)
CxfDestory(pcximage);
FcloseFun(hfile);
return 1;
}
int main(int argc, char *argv[])
{
if (2 != argc)
{
printf("Parameter Error\n");
}
swprintf(ws, 100, L"%hs", argv[1]);
wprintf(L"%s\n", ws);
hdll = LoadLibraryA("cximage.dll");
if (hdll)
{
printf("load success\n");
}
BmpFun = (pfun)((DWORD)hdll+0x1BD0);
...
...
void* pcximage = malloc(0x1f0);
praseFile(ws,pcximage);
FreeLibrary(hdll);
return 0;
}
检测harness是否正常运行
>C:\my-winafl-fuzz\DynamoRIO-Windows-8.0.0-1\bin32\drrun.exe -c winafl.dll -debug -coverage_module cximage.dll -target_module fuzzcx.exe -target_offset 0x1010 -fuzz_iterations 10 -nargs 1 -- fuzzcx.exe 555.bmp
winafl.dll 常用参数说明。
-debug # debug模式, 它会生成一个log文件
-target_module # 目标程序(只能有一个), 也是target_offset所在的模块
-target_offset # 目标程序偏移,相对于target_module的偏移,在method无法导出的时候使用
-fuzz_iterations # 目标程序重新启动一次内运行目标函数(即target_method)的最大迭代数
-nargs # 目标程序执行所需要的参数个数(包括目标程序本身)
-target_module # 目标函数,需要export或者调试符号(pdb)
-coverage_module # 计算覆盖率的模块,也就是目标程序会调用的模块(dll); (可以有多个)
命令执行完成后,查看生成的log文件,
可以看到程序插桩过程中无异常,指定的函数被成功调用。
检测插桩覆盖率,用IDA打开生成的log文件(IDA安装lighthouse插件)。
C:\my-winafl-fuzz\DynamoRIO-Windows-8.0.0-1\bin32\drrun.exe -t drcov -- fuzzcx.exe 555.bmp
执行下面命令启动winafl进行fuzz
.\afl-fuzz.exe -i in -o out -t 90000 -m none -D C:\my-winafl-fuzz\DynamoRIO-Windows-8.0.0-1\bin32\ -- -coverage_module cximage.dll -target_module fuzzcx.exe -target_offset 0x1010 -fuzz_iterations 50000 -nargs 2 -- fuzzcx.exe @@
经过十几个小时的fuzz,最终发现三十多个crash
加载其中一个crash文件来测试,发现程序进程崩溃,后面就可以通过调试器分析漏洞成因了。
本文作者:qret2libc