众所周知模糊测试是发掘软件 Bug 最有用的方法之一,AFL 就是在这个背景下发展起来的。从原理上来说 AFL 通过变异软件输入的数据来进行软件 Bug 挖掘,与一些模糊测试器不同的是 AFL 变异数据的方法是通过覆盖率算法来实现的,而不是通过格式,虽然前者效率高但是也有缺点,详情可以看看 Black Hat 2019 超越覆盖率指导的模糊测试。
对图片解析库进行模糊测试的前提是找到一款可以读取图片格式的软件,百度搜索了一下觉得 ABC 这个软件不错,因为它能读取很多格式的图片,包括 TIFF 图片,但是测试时并不需要这么多,因为根本没那么多时间(说实话它的官网有点搞笑)。
下载完成之后转到这个看图软件的根目录,由于尚未进行 RCE,所以只能从名称和日志信息上猜测各个模块的功能,等到以后再验证。比如 skinbox 模块可能是换肤功能,KTUpdateDownlaoder 模块可能是下载更新功能,PhotoManage 打开之后是一个路径下寻找图片的功能,PhotoViewer 不同说了肯定是查看图片格式的,Service 模块可能是获取服务器信息的,以及其他一些杂七杂八的。对于动态链接库来说 FreeImage 模块可能是读取图片格式的,Free 说明它是免费的,极有可能是一个公开的库,Imgdecoder-gdip 貌似是解码库或者图形库,ShellExt 应该与执行命令有关,有意思的是这个软件还用到了 sqlite3 的内置小型数据库,不知道它要储存什么。
从 exe 后缀的模块来看,该软件的功能还是十分的多的,甚至包括了 Pdf 的阅读功能。
使用搜索引擎查了一番之后发现 FreeImage 库果然是一个公开的库,专门用来处理图片的,并且有五年的历史。下面主要的逆向工程工作就放在这个上面就行了。
既然你是一个公开的库并且版本不是最新的 3.18.0,那么肯定要查一下漏洞库有没有记录,假如一个漏洞都没有的话那么就没有对它进行模糊测试的必要了。搜索了一下发现全是洞,以堆栈溢出为主。估计下面进行模糊测试的时候会找出一些旧 Bug。
下面对 FreeImage 库进行逆向分析:首先拖入 IDA,发现这个库导出的函数巨多,但是并不是所有函数都会被 PhotoViewer 调用到,并且有些导出函数中会调用一堆导出函数(名称相似的情况下参数少的调用参数多的,当然这个只是经验判断,有时候并不一定是这样)。
从 Get 函数上来看,许多都与获取图片信息有关,比如 GetColorType 获取图片颜色类型,GetFileType 获取图片格式,GetInfo 获取图片格式信息等等。
继续在导出函数中查找,发现了一些重要的信息:FreeImage_Convert* 应该是负责数据位数转换的,FreeImage_Allocate* 表示进行了内存操作,FreeImage_Load* 载入数据,FreeImage_*Tag* 负责标签操作。
傻瓜式的静态分析到此结束,接着使用动态分析,在此之前我已经在网络上收集了各式各样的图片,包括 BMP、JPG、TIFF。使用 DynamoRIO 的 drcov 覆盖率查询工具扫一下,看看调用了 FreeImage 的哪些函数。命令如下:
DynamoRIO-7.0.0\bin32> drrun,exe -t drcov -- [程序/模块名] [参数]
从被执行的函数中可以看出调用了内存操作函数、数据格式转换函数、拷贝函数、初始化函数、标签操作函数以及加载函数等。
从覆盖率上看 ABC 看图软件调用了约 4.26% 的 FreeImage 库函数代码,说明 FreeImage 可以解析的图片格式还是非常的多。
用 windbg 看一下调用函数的顺序,以便编写测试文件用来进行模糊测试。读取的图片为 jpg 格式,命令如下:
.logopen E:\log.txt
lmm FreeImage
x FreeImage*
bm FreeImge!FreeImage* ".echo; kb 1; g"
通过 python 脚本筛选出如下结果,总体可以概括为使用 Initialise 函数初始化库,使用一系列的 Get 函数获取图片格式信息,使用 Load 函数加载图片,对图片进行拷贝和获取详细图片信息操作,卸载图片并进行标签操作。至于参数分析就不写了,这个过程比较繁琐,具体可以概括为大部分函数的参数都传入了加载图片函数的返回值(^ _ ^)。
经过动态和静态的简单分析后,决定对 FreeImage 库的载入函数、格式转换函数、获取图片信息函数、图片格式判断函数进行模糊测试。至于为什么不包括标签和内存操作函数,因为这个过程属于设置而不是获取,并且是在 PhotoViewer 模块中实现的,而非 FreeImage 模块。那么首先编写的是加载图片和获取图片信息的功能,测试代码如下:
/* test.exe */ #define _CRT_SECURE_NO_WARNINGS #include <windows.h> #include <tchar.h> #include <iostream> using namespace std; extern "C" __declspec(dllexport) int main(int argc, char** argv); void FreeImage_test(HINSTANCE hinstLib, wchar_t* pathfile); wchar_t* charToWChar(const char* text); // 加载卸载函数 typedef DWORD(__stdcall *FreeImage_GetFileTypeU)(const wchar_t* lpszPathName, int flag); typedef DWORD(__stdcall *FreeImage_Initialise)(BOOL load_local_plugins_only); typedef DWORD(__stdcall *FreeImage_DeInitialise)(); typedef DWORD(__stdcall *FreeImage_LoadU)(DWORD format, const wchar_t* lpszPathName, int flag); typedef DWORD(__stdcall *FreeImage_UnLoad)(DWORD dib); // 获取信息函数 typedef DWORD(__stdcall *FreeImage_GetFIFFromFilenameU)(const wchar_t* lpszPathName); FreeImage_GetFIFFromFilenameU GetFIFFromFilenameU; typedef DWORD(__stdcall *FreeImage_GetDotsPerMeterX)(DWORD dib); FreeImage_GetDotsPerMeterX GetDotsPerMeterX; typedef DWORD(__stdcall *FreeImage_GetDotsPerMeterY)(DWORD dib); FreeImage_GetDotsPerMeterY GetDotsPerMeterY; typedef DWORD(__stdcall *FreeImage_GetTransparencyCount)(DWORD dib); FreeImage_GetTransparencyCount GetTransparencyCount; typedef DWORD(__stdcall *FreeImage_GetTransparencyTable)(DWORD dib); FreeImage_GetTransparencyTable GetTransparencyTable; typedef DWORD(__stdcall *FreeImage_GetBlueMask)(DWORD dib); FreeImage_GetBlueMask GetBlueMask; typedef DWORD(__stdcall *FreeImage_GetRedMask)(DWORD dib); FreeImage_GetRedMask GetRedMask; typedef DWORD(__stdcall *FreeImage_GetThumbnail)(DWORD dib); FreeImage_GetThumbnail GetThumbnail; typedef DWORD(__stdcall *FreeImage_GetPalette)(DWORD dib); FreeImage_GetPalette GetPalette; typedef DWORD(__stdcall *FreeImage_GetGreenMask)(DWORD dib); FreeImage_GetGreenMask GetGreenMask; typedef DWORD(__stdcall *FreeImage_GetImageType)(DWORD dib); FreeImage_GetImageType GetImageType; typedef DWORD(__stdcall *FreeImage_GetICCProfile)(DWORD dib); FreeImage_GetICCProfile GetICCProfile; typedef DWORD(__stdcall *FreeImage_GetBackgroundColor)(DWORD dib); FreeImage_GetBackgroundColor GetBackgroundColor; typedef DWORD(__stdcall *FreeImage_GetInfo)(DWORD dib); FreeImage_GetInfo GetInfo; // 不确定的 typedef DWORD(__stdcall *FreeImage_GetFormatFromFIF)(DWORD dib); FreeImage_GetFormatFromFIF GetFormatFromFIF; typedef DWORD(__stdcall *FreeImage_GetFIFExtensionList)(DWORD dib); FreeImage_GetFIFExtensionList GetFIFExtensionList; typedef DWORD(__stdcall *FreeImage_GetMetadataCount)(DWORD format, DWORD dib); FreeImage_GetMetadataCount GetMetadataCount; FreeImage_Initialise Initialise; FreeImage_GetFileTypeU LoadFileType; FreeImage_LoadU LoadU; DWORD load; FreeImage_UnLoad UnLoad; FreeImage_DeInitialise DeInitialise; int main(int argc, char** argv) { if (argc < 2) { printf("Usage: %s <xml file>\n", argv[0]); return 0; } wchar_t* PathName = charToWChar(argv[1]); HINSTANCE hinstLib; BOOL fFreeResult, fRunTimeLinkSuccess = FALSE; DWORD Error = NULL; hinstLib = LoadLibrary(TEXT("E:\\FreeImage.dll")); if (hinstLib != NULL) { fRunTimeLinkSuccess = TRUE; Initialise = (FreeImage_Initialise)GetProcAddress(hinstLib, (LPCSTR)163); // 初始化 FreeImage 库 LoadFileType = (FreeImage_GetFileTypeU)GetProcAddress(hinstLib, (LPCSTR)126);// 获取位图文件类型 LoadU = (FreeImage_LoadU)GetProcAddress(hinstLib, (LPCSTR)181); // 加载位图 UnLoad = (FreeImage_UnLoad)GetProcAddress(hinstLib, (LPCSTR)242);// 卸载位图 DeInitialise = (FreeImage_DeInitialise)GetProcAddress(hinstLib, (LPCSTR)83);//卸载 FreeImage 库 // 获取信息函数 LoadFileType = (FreeImage_GetFileTypeU)GetProcAddress(hinstLib, (LPCSTR)126); GetFIFFromFilenameU = (FreeImage_GetFIFFromFilenameU)GetProcAddress(hinstLib, (LPCSTR)118); GetDotsPerMeterX = (FreeImage_GetDotsPerMeterX)GetProcAddress(hinstLib, (LPCSTR)112); GetDotsPerMeterY = (FreeImage_GetDotsPerMeterY)GetProcAddress(hinstLib, (LPCSTR)113); GetTransparencyCount = (FreeImage_GetTransparencyCount)GetProcAddress(hinstLib, (LPCSTR)155); GetTransparencyTable = (FreeImage_GetTransparencyTable)GetProcAddress(hinstLib, (LPCSTR)156); GetBlueMask = (FreeImage_GetBlueMask)GetProcAddress(hinstLib, (LPCSTR)105); GetRedMask = (FreeImage_GetRedMask)GetProcAddress(hinstLib, (LPCSTR)145); GetGreenMask = (FreeImage_GetGreenMask)GetProcAddress(hinstLib, (LPCSTR)128); GetThumbnail = (FreeImage_GetThumbnail)GetProcAddress(hinstLib, (LPCSTR)154); GetPalette = (FreeImage_GetPalette)GetProcAddress(hinstLib, (LPCSTR)141); GetImageType = (FreeImage_GetImageType)GetProcAddress(hinstLib, (LPCSTR)132); GetICCProfile = (FreeImage_GetICCProfile)GetProcAddress(hinstLib, (LPCSTR)131); GetBackgroundColor = (FreeImage_GetBackgroundColor)GetProcAddress(hinstLib, (LPCSTR)103); GetInfo = (FreeImage_GetInfo)GetProcAddress(hinstLib, (LPCSTR)133); GetFormatFromFIF = (FreeImage_GetFormatFromFIF)GetProcAddress(hinstLib, (LPCSTR)127); GetFIFExtensionList = (FreeImage_GetFIFExtensionList)GetProcAddress(hinstLib, (LPCSTR)116); GetMetadataCount = (FreeImage_GetMetadataCount)GetProcAddress(hinstLib, (LPCSTR)139); (Initialise)(FALSE); FreeImage_test(hinstLib, PathName); (DeInitialise)(); fFreeResult = FreeLibrary(hinstLib); } if (!fRunTimeLinkSuccess) cout << "加载函数失败, Error: " << Error << endl; return 0; } void FreeImage_test(HINSTANCE hinstLib, wchar_t* pathfile) { // 加载函数 DWORD FileType = (LoadFileType)(pathfile, 0); load = (LoadU)(FileType, pathfile, 0); // 获取信息函数 GetFIFFromFilenameU(pathfile); GetDotsPerMeterX(load); GetDotsPerMeterY(load); GetTransparencyCount(load); GetTransparencyTable(load); GetBlueMask(load); GetRedMask(load); GetGreenMask(load); GetThumbnail(load); GetPalette(load); GetImageType(load); GetICCProfile(load); GetInfo(load); GetFormatFromFIF(load); GetFIFExtensionList(load); // 卸载函数 (UnLoad)(load); } wchar_t* charToWChar(const char* text) { size_t size = strlen(text) + 1; wchar_t* wa = new wchar_t[size]; mbstowcs(wa, text, size); return wa; }
测试代码编写完之后使用 WinAFL 的案例最小化功能筛选案例,这一步主要是提高代码运行时的覆盖率。命令如下:
winafl\python winafl-cmin.py --working-dir E:\winafl\bin32 -D E:\DynamoRIO-7.0.0\bin32 -t 100000 -i [案例路径] -o [输出案例路径] -coverage_module FreeImage.dll -target_module test.exe -target_method main -nargs 2 -- E:\test.exe @@ // 表示引用
筛选过的图片储存在 MIX_* 文件夹中,并且保留了不大于 4KB 的图片,因为大于 4KB 的图片 WinAFL 读起来会非常的慢,而这次测试的时间计划为 6h 左右:
筛选完图片之后就可以开始模糊测试了,这里规定每个图片格式测试时间为 1h,命令如下:
winafl\bin32\afl-fuzz.exe -i [输入案例路径] -o [结果输出路径] -D E:\DynamoRIO-7.0.0\bi3n2 -t 20000 -- -coverage_module FreeImage.dll -fuzz_iterations 5000 -target_module test.exe -target_offset [函数偏移] // 这里进行模糊测试的函数为 FreeImage_test() -nargs 2 -- E:\test.exe @@
如下图所示 WinAFL 已经开始正常工作了,但是否运行的良好呢,首先来看一下运行速度 stage progress->exec speed,在 WinAFL 中速度分为三挡:极慢(zzzz....)、慢(slow)、正常,此时的速度为 2352 次每秒属于正常速度;其次看一下 Fuzzing strategy yields,这个板块表示样本变异的程度,数字越大样本越模糊,测试效率越高;之后看一下 stage progress -> now trying && stage execs,now trying 表示目前执行的任务,如图所示目前正在执行算数变异(arith),而 stage execs 表示任务执行的进度,用百分率表示。如果按上面的标准来看,下图算是一个良好的测试开始。
如果想在测试过程中查询 WinAFL 的运行状态,可以使用 winafl/winafl-plot.py 脚本进行查询。命令如下:
经过几个小时的模糊测试后,图片格式为 BMP、ICO、TAG 的 crash 目录中均发现了 Bug,包括重复的;但是 TIFF、JPG、PNG 格式则没有测试出 Bug,一方面可能是因为测试的时间有点短(只有 1 个小时),另一方面可能删除了稍大一点的图片导致测试的不是很充分。 python winafl-plot.py sync_dir out_dir
# sync_dir 是 afl-fuzz -o 参数所指定的路径,路径下应该包括 fuzzer_stats 文件;out_dir 目录表示结果输出目录,输出的是 index.html 文件,直接拉到浏览器中查看即可
测试完图片信息获取函数和加载函数之后,使用 BMP 格式测试一下图片格式判断函数和转换函数。
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <tchar.h>
#include <iostream>
using namespace std;
extern "C" __declspec(dllexport) int main(int argc, char** argv);
void FreeImage_test(HINSTANCE hinstLib, wchar_t* pathfile);
wchar_t* charToWChar(const char* text);
// 加载卸载函数
typedef DWORD(__stdcall *FreeImage_GetFileTypeU)(const wchar_t* lpszPathName, int flag);
typedef DWORD(__stdcall *FreeImage_Initialise)(BOOL load_local_plugins_only);
typedef DWORD(__stdcall *FreeImage_DeInitialise)();
typedef DWORD(__stdcall *FreeImage_LoadU)(DWORD format, const wchar_t* lpszPathName, int flag);
typedef DWORD(__stdcall *FreeImage_UnLoad)(DWORD dib);
// 图片格式判断函数
typedef DWORD(__stdcall *FreeImage_HasBackgroundColor)(DWORD dib); FreeImage_HasBackgroundColor HasBackgroundColor;
typedef DWORD(__stdcall *FreeImage_HasPixels)(DWORD dib); FreeImage_HasPixels HasPixels;
typedef DWORD(__stdcall *FreeImage_HasRGBMasks)(DWORD dib); FreeImage_HasRGBMasks HasRGBMasks;
typedef DWORD(__stdcall *FreeImage_IsTransparent)(DWORD dib); FreeImage_IsTransparent IsTransparent;
// 转换函数
typedef DWORD(__stdcall *FreeImage_ConvertTo24Bits)(DWORD dib); FreeImage_ConvertTo24Bits ConvertTo24Bits;
typedef DWORD(__stdcall *FreeImage_ConvertTo32Bits)(DWORD dib); FreeImage_ConvertTo32Bits ConvertTo32Bits;
FreeImage_Initialise Initialise;
FreeImage_GetFileTypeU LoadFileType;
FreeImage_LoadU LoadU; DWORD load;
FreeImage_UnLoad UnLoad;
FreeImage_DeInitialise DeInitialise;
int main(int argc, char** argv)
{
if (argc < 2) {
printf("Usage: %s <xml file>\n", argv[0]);
return 0;
}
wchar_t* PathName = charToWChar(argv[1]);
HINSTANCE hinstLib; BOOL fFreeResult, fRunTimeLinkSuccess = FALSE; DWORD Error = NULL;
hinstLib = LoadLibrary(TEXT("E:\\FreeImage.dll"));
if (hinstLib != NULL)
{
fRunTimeLinkSuccess = TRUE;
Initialise = (FreeImage_Initialise)GetProcAddress(hinstLib, (LPCSTR)163); // 初始化 FreeImage 库
LoadFileType = (FreeImage_GetFileTypeU)GetProcAddress(hinstLib, (LPCSTR)126);// 获取位图文件类型
LoadU = (FreeImage_LoadU)GetProcAddress(hinstLib, (LPCSTR)181); // 加载位图
UnLoad = (FreeImage_UnLoad)GetProcAddress(hinstLib, (LPCSTR)242);// 卸载位图
DeInitialise = (FreeImage_DeInitialise)GetProcAddress(hinstLib, (LPCSTR)83);//卸载 FreeImage 库
//图片格式加载函数
HasBackgroundColor = (FreeImage_HasBackgroundColor)GetProcAddress(hinstLib, (LPCSTR)160);
HasPixels = (FreeImage_HasPixels)GetProcAddress(hinstLib, (LPCSTR)161);
HasRGBMasks = (FreeImage_HasRGBMasks)GetProcAddress(hinstLib, (LPCSTR)162);
IsTransparent = (FreeImage_IsTransparent)GetProcAddress(hinstLib, (LPCSTR)168);
//转换函数
ConvertTo24Bits = (FreeImage_ConvertTo24Bits)GetProcAddress(hinstLib, (LPCSTR)65);
ConvertTo32Bits = (FreeImage_ConvertTo32Bits)GetProcAddress(hinstLib, (LPCSTR)66);
(Initialise)(FALSE);
FreeImage_test(hinstLib, PathName);
(DeInitialise)();
fFreeResult = FreeLibrary(hinstLib);
}
if (!fRunTimeLinkSuccess)
cout << "加载函数失败, Error: " << Error << endl;
return 0;
}
void FreeImage_test(HINSTANCE hinstLib, wchar_t* pathfile)
{
// 加载函数
DWORD FileType = (LoadFileType)(pathfile, 0);
load = (LoadU)(FileType, pathfile, 0);
//图片格式加载函数
HasBackgroundColor(load);
HasPixels(load);
HasRGBMasks(load);
IsTransparent(load);
//转换函数
ConvertTo24Bits(load);
ConvertTo32Bits(load);
(UnLoad)(load);
}
wchar_t* charToWChar(const char* text)
{
size_t size = strlen(text) + 1;
wchar_t* wa = new wchar_t[size];
mbstowcs(wa, text, size);
return wa;
}
等所有测试结束之后就可以开始整理一下被模糊测试找出的 Bug,调试一下出现 Crash 的原因,以及是否可以被利用。当然这个过程比较繁琐,利用 BugID 可以帮助你进行 Bug 分类,或者编写 Windbg 脚本。筛选之后如图下图所示:
首先第一个 Bug 是读写未初始化的堆,通过查看函数调用发现是 FreeImage_GetImageType 出的问题。
通过 !heap -p -a 和 !address 可以看出非法访问未初始化的堆。
样本信息如下:
第二个 Bug 是读写未分配的堆,其实和上面差不多,但是读写的内存地址不一样。如下图所示出问题的函数为 FreeImage_GetFIFFromFilenameU。
从内存信息上来看访问了保留的内存地址。
样本信息如下:
第三个 Bug 是堆溢出,出问题的函数是 FreeImage_OutputMessageProc,父函数为 FreeImage_LoadU,这里函数调用关系可能没有 IDA 的准确。
edi = 0x11eadd08,esi = 0x11ea6000,0x11eadd08 + 0x2f3 - 11eae000 = -5,从堆空间 +5 的地方开始溢出。
最后一个是堆越界写入,出问题的函数为 FreeImage_OutputMessageProc,从 +1 的位置开始写入,写入的了 1 个 DWORD 大小的字节。
样本如下图所示:
对比漏洞库的漏洞看看是否发现了旧的 Bug,通过仔细的分析相关 POC 后发现只有堆溢出被记录在 CVE 中。
本次测试只是简单的 Fuzzing 了一下 ABC 看图软件的加载图片功能,但是效果并不是很理想。由于时间比较紧张,错误在所难免,请勿喷