本文为看雪论坛优秀文章
看雪论坛作者ID:1900
一
模糊测试的基础知识
通过向应用程序提供非预期输入并监控输出中的异常来发现软件中的故障的方法。利用自动化或是半自动化的方法重复地向应用提供输入。
用于模糊测试地模糊测试器分为两类:
采用何种模糊测试方法取决于众对因素。没有所谓的一定正确的模糊测试方法,决定采用何种模糊测试方法完全依赖于被测应用,测试者拥有的技能,以及被进行模糊测试的数据的格式。但是,不论对什么应用进行模糊测试,不论采用何种模糊测试方法,模糊测试执行过程都包含以下几个阶段:
确定测试目标: 只有有了明确的测试目标后,才能决定使用的模糊测试工具或方法。一个程序的供应商曾经出现过的安全漏洞是确定测试目标的一个重要参考。
确定输入向量: 几乎所有可被利用的安全漏洞都是因为应用没有对用户的输入进行校验或进行必要的非法输入处理。从客户端向目标应用发送的任何东西,包括头,文件名,环境变量,注册表键,以及其他信息,都应该被看做输入向量。所有这些输入向量都可能是潜在的模糊测试变量。
生成模糊测试数据: 一旦识别出输入向量,就可以根据输入向量产生模糊测试数据。究竟是使用预先确定的值,使用基于存在的数据通过变异生成的值,还是使用动态生成的值依赖于被测应用及其使用的数据格式。但是,无论选择哪种方式,都应该使用自动化过程来生成数据。
执行模糊测试数据: 在该步骤中,一般会向被测目标发送数据包,打开文件,或是执行被测应用。与上一个步骤一个,该步骤也应该是自动化的。
监视异常: 在进行模糊测试的过程中,如果输入数据触发了漏洞,往往会导致程序崩溃。因此,模糊测试器应当可以捕获到发生的错误,所以,模糊测试器应当具有对异常和错误进行监控的能力。模糊测试需要根据被测应用和所决定采用的模糊测试类型来设置各种形式的监视。
判断发现的漏洞是否可能被利用: 如果在模糊测试中发现了一个错误,依据审计的目的,可能需要判定这个被发现的错误是否是一个可被利用的安全漏洞。这种判定过程是典型的手工过程,需要操者有特定的安全知识。
模糊测试方法可以分为以下的五类:
预生成测试用例: 该方法要求首先研究特定的规约,理解该规约支持的数据结构和可接受的值的范围;然后依据这些理解生成用于测试边界条件或是违反规约的测试用例;接下来使用这些测试用例来测试该规约实现的完备性。创建这些测试用例需要花费很多精力,但这些用例一旦被创建,就很"容易"被复用,用于测试某种协议或文件格式的不同实现。该方法缺乏随机生成,一旦测试用例列表中的用例被执行完,测试只能结束。
随机生成输入: 随机方法是最低效的方法,但是这种方法可以被用来快速地识别目标应用中是否有非常糟糕的代码。随机方法简单地向目标应用发送伪随机数据,希望得到最好或最坏地结果。
手工协议变异测试: 在手工测试中,测试者就是模糊测试器。在加载了目标应用程序后,测试者仅仅通过输入不正确地数据,试图使服务器崩溃,或者诱发一些不正常地行为。
变异或强制性测试: 模糊测试器从一个有效的协议样本或是数据格式样本开始,持续不断的打乱数据包或是文件中的每一个字节(byte),字(word),双字(dword)或是字符串(string)。
自动协议生成测试: 自动协议生成测试是一种更高级的强制性方法。在这种方式中,首先要做的是对被测应用进行研究,理解和解释协议规约或文件定义。然而,这种方法并不基于协议规约或文件定义创建硬编码的测试用例,而是创建一个描述协议规约如何工作的文法。采用这种方式,测试者可以识别出数据包或是文件中的静态部分和动态部分,动态部分就是可以被模糊化变量替代的部分。随后,模糊测试器动态分析包含了静态和动态部分模板,生成模糊测试数据,将结果数据包或是文件发送给被测应用。
根据实现方法的不同,模糊测试器大致可以分为以下两类:
常规模糊测试器: 该方法根据不同的应用程序的输入数据格式不同来构建符合要求的数据后启动程序进行测试,这里的不同可以是不同的应用,不同的协议,不同的文件格式。该方法的缺点是每次都要重新启动程序才能完成测试,性能损耗比较大。
内存模糊测试器: 内存模糊测试器的一种实现方法是对进程执行一次快照,在生成快照后迅速向该进程的输入子例程中注入故障数据。当执行完一个测试用例后,恢复上次的快照并注入新的数据。重复以上过程知道所有测试用例都执行完成。
对不同的应用程序进行探测的时候,会选择不同的数据类别进行探测,主要分为以下几种数据类型:
这类型数据主要用来探测导致输入的整型数据是否存在溢出的可能,因此,0和0xFFFFFFFF应当包含其中。此外,输入数据有可能被程序进行加减乘除的操作,因此也需要将这些可能性加入其中。所以,一个整数测试用例中,应当包含以下用例:
MAX32 - 16 <= MAX32 <= MAX32 + 16
MAX32 / 2 - 16 <= MAX32 <= MAX32 / 2 + 16
MAX32 / 3 - 16 <= MAX32 / 3 <= MAX32 / 3 + 16
MAX32 / 4 - 16 <= MAX32 / 4 <= MAX32 / 4 + 16
MAX16 - 16 <= MAX16 <= MAX16 + 16
MAX16 / 2 - 16 <= MAX16 <= MAX16 / 2 + 16
MAX16 / 3 - 16 <= MAX16 / 3 <= MAX16 / 3 + 16
MAX16 / 4 - 16 <= MAX32 / 4 <= MAX16 / 4 + 16
MAX8 - 16 <= MAX8 <= MAX8 + 16
MAX8 / 2 - 16 <= MAX8 <= MAX8 / 2 + 16
MAX8 / 3 - 16 <= MAX8 / 3 <= MAX8 / 3 + 16
MAX8 / 4 - 16 <= MAX8 / 4 <= MAX8 / 4 + 16
其中MAX32,MAX16,MAX8分别代表最大32位,16位,8位整型。
溢出漏洞的产生往往是由于没有对输入字符串长度进行控制,因此在生成的模糊测试用例中应当要包含不同长度的字符串,且长字符串更为重要。因为,漏洞通常是被长字符串触发。
模糊测试数据集还需要包含非字母数字字符,例如空格和制表符。这些字符经常被用作字符分隔符和终止符。将这些字符随机地放到生成地模糊字符串中能够更好地模拟正在模糊测试地协议,由此可以覆盖更多的代码。
"%s"和"%n"是发现格式字符串漏洞最有效的两个字符串标记。因此,模糊测试器的数据集中应当包含足够多的不同长度的包含"%s"和"%n"字符串标记的字符串。
在字符转换和翻译过程中也会包含安全漏洞,特别是字符扩展。例如,16进制的0xFE和0xFF在UTF-16下被扩展成4个字符,代码中对字符扩展的不适当处理通常会导致安全漏洞。字符转换也可能会被不正确的实现,尤其是在处理少见或是很少被使用的边界值时。
接下来将创建一个简单的包含栈溢出漏洞的程序,针对这个包含漏洞的程序打造一个简易模糊测试器来复习上述内容 根据这个漏洞程序的代码将构建一个常规模糊测试器和内存模糊测试器,模糊测试器的源代码已经上传到github上,代码地址:https://github.com/LegendSaber/Fuzzer
二
包含漏洞的被测程序
以下是被测程序的源代码,可以看到,该程序的vuln函数包含了栈溢出漏洞:
// vuln.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <cstdio>
#include <Windows.h>
void vuln(char *szInput)
{
char szStr[100] = { 0 };
strcpy(szStr, szInput);
printf("Vuln func...\n");
}
void test(char *szInput)
{
vuln(szInput);
printf("test func...\n");
}
void Decrypt(char *szInput)
{
DWORD dwLen = strlen(szInput);
for (DWORD i = 0; i < dwLen; i++)
{
szInput[i] ^= 190;
}
printf("Decrypt func...\n");
}
void Init(char *szInput, char *szInit)
{
if (strlen(szInit) < MAXBYTE)
{
strcpy(szInput, szInit);
printf("Init func...\n");
}
else
{
printf("输入的字符串过长\n");
}
}
int main(int argc, char *argv[])
{
char szInput[MAXBYTE] = { 0 };
Init(szInput, argv[0]);
Decrypt(szInput);
test(szInput);
return 0;
}
三
常规模糊测试器
常规的模糊测试器的实现是通过产生不同的输入数据来启动程序完成测试,实现起来相对简单,只需要不断构建满足条件的输入数据然后启动程序就好。为了捕获到程序发生的异常,还需要在模糊测试器和被测程序中建立调试关系,测试代码如下:
#include "Fuzzy.h"
#include "auxiliary.h"
Fuzzy::Fuzzy(char *szFilePath)
{
if (strlen(szFilePath) < MAX_PATH)
{
strcpy(this->szFilePath, szFilePath);
}
else
{
printf("文件名过长\n");
}
}
char *Fuzzy::GetFilePath()
{
return this->szFilePath;
}
void Fuzzy::BeginFuzzy()
{
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
DEBUG_EVENT DbgEvt = { 0 }; // 保存调试事件的数据结构
bool bContinue = true; // 是否继续
DWORD dwContinueStatus = DBG_CONTINUE; // 恢复继续执行用的状态代码
DWORD dwSize = 4; // 输入数据的大小
char szInput[MAXBYTE] = { 0 }; // 输入数据缓冲区
bool bExit = false; // 被调试进程结束则设为true
printf("Fzzy Begin....\n");
while (dwSize < MAXBYTE)
{
// 随机输入数据
memset(szInput, 0, MAXBYTE);
for (DWORD i = 0; i < dwSize; i++)
{
szInput[i] = rand() % 255 + 1;
}
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
si.cb = sizeof(si);
if (!CreateProcess(this->szFilePath,
szInput,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE |
DEBUG_PROCESS |
DEBUG_ONLY_THIS_PROCESS,
NULL,
NULL,
&si,
&pi))
{
ShowError("CreateProcess");
goto exit;
}
bExit = false;
while (bContinue && !bExit)
{
memset(&DbgEvt, 0, sizeof(DbgEvt));
// 等待调试事件发生
bContinue = WaitForDebugEvent(&DbgEvt, INFINITE);
if (!bContinue)
{
ShowError("WaitForDebugEvent");
break;
}
switch (DbgEvt.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
{
switch (DbgEvt.u.Exception.ExceptionRecord.ExceptionCode)
{
// 捕获到程序异常
case EXCEPTION_ACCESS_VIOLATION:
{
printf(".....Catch Access Violation.....\n");
printf("dwSize is: %d\n", dwSize);
printf("Address is 0x%X\n", DbgEvt.u.Exception.ExceptionRecord.ExceptionAddress);
bExit = true;
break;
}
default:
{
break;
}
}
break;
}
case EXIT_PROCESS_DEBUG_EVENT:
{
bExit = true; // 该进程已退出
break;
}
default:
{
break;
}
}
// 恢复被调试进程继续运行
bContinue = ContinueDebugEvent(DbgEvt.dwProcessId,
DbgEvt.dwThreadId,
dwContinueStatus);
if (!bContinue)
{
ShowError("ContinueDebugEvent");
break;
}
}
// 增加数据大小,供下一次进行测试
dwSize += 4;
if (pi.hProcess) CloseHandle(pi.hProcess);
if (pi.hThread) CloseHandle(pi.hThread);
}
exit:
printf("Fuzzy End...\n");
}
运行结果如下:
可以看到模糊测试器虽然能发现异常,但是性能开销是很大的。因此每次测试都要开启一个新的进程,被测程序每次都要经过Init->Decrypt->test->vuln才可以完成验证。如果这是一个需要大量运算的被测程序,或者是需要经过网络传输的程序,这个开销就会进一步增大。
四
内存模糊测试器构建方法
有以下两种构建内存模糊测试器的方法:
变异循环插入(Mutation Loop Insertion: MLI)
快照恢复变异(Snapshot restoration mutation: SRM)
变异循环插入方法需要首先通过逆向工程,人工定位解析例程的起始和结束地址。一旦定位完成,变异循环插入工具能够向应用程序中插入一个mutation例程(变异例程),变异例程负责修改解析例程拿到的数据。接下来,需要在内存中插入两条无条件跳转指令,这两条指令分别是"从解析例程的结尾跳到变异例程的开始处"和"从变异例程的结尾跳到解析例程的开始处"。
根据上面的被测程序的代码,此时的程序执行就会如下图所示:
此时,当程序运行到test函数末尾的时候,就会跳转到mutate变异例程执行。变异例程会将输入数据经过变异以后在次跳转到test函数起始地址开始执行。这样,围绕解析代码和目标应用创建了一个自给自足的数据变异循环。不需要为每个测试用例都启动一次进程,自然就节省了性能开销。变异例程每次迭代都会向mutate例程传入不同的,可能引发错误的数据。
与变异循环插入方法一样,快照恢复变异方法也需要定位到解析代码的开始和结束位置。在标识出这些位置后,快照变异工具会在到达解析代码的开始位置时为目标进程建立快照。在解析函数执行完成后,快照恢复变异工具恢复进程快照,对原先数据产生变异,并使用变异得到的数据重新执行解析代码。
此时程序的执行流程就会如下图所示:
五
快照恢复变异
快照恢复变异的方法需要在程序的解析例程中找到插入点,这样才可以进行相应的保存快照和恢复快照的操作。在vuln.exe程序中,通过调试器可以定位,test函数的起始地址是0x004010B0,结束位置是0x004010D8。
由于模糊测试器和被测程序处于调试关系中,所以可以通过向test函数的起始和结束位置插入软件断点的方式来实现对被测程序的HOOK操作。在插入软件断点后,当被测程序运行到相应位置,模糊测试器就可以获得控制权,然后在对程序进行下一步操作。
当被测程序在函数起始处中断的时候,首先要判断是不是第一次运行到这个地址,如果是的话就需要保存进程的快照。
而进程快照由两部分构成:
被测进程的线程CONTEXT
被测进程的内存页状态和内容
当被测程序运行到test函数末尾的时候,就需要恢复快照,也就是恢复保存的线程CONTEXT和内存页的状态和内容。这两部分的代码分别在Thread.cpp和Pages.cpp中实现:
#include "Thread.h"
#include "auxiliary.h"
PCONTEXTLIST g_pCtxListHead = NULL; // 头节点,用来连接所有的线程
// 保存线程CONTEXT
BOOL SaveThreadContext(DWORD dwPid)
{
BOOL bRet = TRUE, bContninue = TRUE;
HANDLE hSnap = NULL, hThread = NULL;
THREADENTRY32 te = { 0 };
PCONTEXTLIST pContextList = NULL;
CONTEXT cxt;
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL);
if (!hSnap)
{
ShowError("CreateToolhelp32Snapshot");
bRet = FALSE;
goto exit;
}
te.dwSize = sizeof(te);
bContninue = Thread32First(hSnap, &te);
while (bContninue)
{
// 根据进程PID选择要保存的线程
if (te.th32OwnerProcessID == dwPid)
{
// 申请空间用来保存线程信息
pContextList = (PCONTEXTLIST)VirtualAlloc(NULL,
sizeof(CONTEXTLIST),
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
if (!pContextList)
{
bRet = FALSE;
ShowError("VirtualAlloc");
goto exit;
}
memset(pContextList, 0, sizeof(CONTEXTLIST));
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (!hThread)
{
bRet = FALSE;
ShowError("OpenThread");
goto exit;
}
if (SuspendThread(hThread) == 0xFFFFFFFF)
{
bRet = FALSE;
ShowError("SuspendThread");
goto exit;
}
// 保存线程ID
pContextList->dwThreadId = te.th32ThreadID;
// 获取线程的Context
pContextList->ctx.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(hThread, &(pContextList->ctx)))
{
bRet = FALSE;
ShowError("GetThreadContext");
goto exit;
}
// 将Context挂入pCtxListHead中
pContextList->next = g_pCtxListHead;
g_pCtxListHead = pContextList;
if (ResumeThread(hThread) == 0xFFFFFFFF)
{
bRet = FALSE;
ShowError("SuspendThread");
goto exit;
}
CloseHandle(hThread);
}
bContninue = Thread32Next(hSnap, &te);
}
exit:
return bRet;
}
// 恢复线程CONTEXT
BOOL RestoreThreadContext()
{
BOOL bRet = TRUE;
HANDLE hThread = NULL;
PCONTEXTLIST pContextList = NULL;
pContextList = g_pCtxListHead;
while (pContextList)
{
hThread = OpenThread(THREAD_ALL_ACCESS,
FALSE,
pContextList->dwThreadId);
if (!hThread)
{
bRet = FALSE;
ShowError("OpenThread");
goto exit;
}
if (SuspendThread(hThread) == 0xFFFFFFFF)
{
bRet = FALSE;
ShowError("SuspendThread");
goto exit;
}
if (!SetThreadContext(hThread, &(pContextList->ctx)))
{
bRet = FALSE;
ShowError("SetThreadContext");
goto exit;
}
if (ResumeThread(hThread) == 0xFFFFFFFF)
{
bRet = FALSE;
ShowError("SuspendThread");
goto exit;
}
CloseHandle(hThread);
pContextList = pContextList->next;
}
exit:
return bRet;
}
#include "Pages.h"
PMEMORYBLOCKSLIST g_pMemoryBlocksListHead = NULL;
BOOL SavePages(HANDLE hProcess)
{
BOOL bRet = TRUE, bSaveBlock = TRUE;
MEMORY_BASIC_INFORMATION mbi = { 0 };
DWORD dwCursor = 0, dwQuerySize = 0;
PMEMORYBLOCKSLIST pMemoryBlocksList = NULL;
while (dwCursor < 0xFFFFFFFF)
{
// 查询页属性
memset(&mbi, 0, sizeof(mbi));
dwQuerySize = VirtualQueryEx(hProcess,
(PVOID)dwCursor,
&mbi,
sizeof(mbi));
if (dwQuerySize < sizeof(mbi))
{
break;
}
bSaveBlock = TRUE;
// 判断是否是要保存的页
if (mbi.State != MEM_COMMIT ||
mbi.Type == MEM_IMAGE ||
mbi.Protect & PAGE_READONLY ||
mbi.Protect & PAGE_EXECUTE_READ ||
mbi.Protect & PAGE_GUARD ||
mbi.Protect & PAGE_NOACCESS)
{
bSaveBlock = FALSE;
}
if (bSaveBlock)
{
pMemoryBlocksList = (PMEMORYBLOCKSLIST)VirtualAlloc(NULL,
sizeof(MEMORYBLOCKSLIST),
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!pMemoryBlocksList)
{
bRet = FALSE;
ShowError("VirtualAlloc");
goto exit;
}
memset(pMemoryBlocksList, 0, sizeof(MEMORYBLOCKSLIST));
// 申请用来保存页中的数据
pMemoryBlocksList->read_buf = VirtualAlloc(NULL,
mbi.RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!pMemoryBlocksList->read_buf)
{
bRet = FALSE;
ShowError("VirtualAlloc");
goto exit;
}
// 保存页中的数据
if (!ReadProcessMemory(hProcess,
mbi.BaseAddress,
pMemoryBlocksList->read_buf,
mbi.RegionSize,
NULL))
{
bRet = FALSE;
ShowError("ReadProcessMemory");
goto exit;
}
memcpy(&pMemoryBlocksList->mbi, &mbi, sizeof(mbi));
// 连入链表
pMemoryBlocksList->next = g_pMemoryBlocksListHead;
g_pMemoryBlocksListHead = pMemoryBlocksList;
}
dwCursor += mbi.RegionSize;
}
exit:
return bRet;
}
BOOL RestorePages(HANDLE hProcess)
{
BOOL bRet = TRUE;
PMEMORYBLOCKSLIST pMemoryBlocksListHead = g_pMemoryBlocksListHead;
while (pMemoryBlocksListHead)
{
if (!WriteProcessMemory(hProcess,
pMemoryBlocksListHead->mbi.BaseAddress,
pMemoryBlocksListHead->read_buf,
pMemoryBlocksListHead->mbi.RegionSize,
NULL))
{
bRet = FALSE;
ShowError("WriteProcessMemory");
goto exit;
}
// 恢复页属性
VirtualProtectEx(hProcess,
pMemoryBlocksListHead->mbi.BaseAddress,
pMemoryBlocksListHead->mbi.RegionSize,
pMemoryBlocksListHead->mbi.Protect,
NULL);
// 获取下一个页信息
pMemoryBlocksListHead = pMemoryBlocksListHead->next;
}
exit:
return bRet;
}
而保存的页面应当具有可写的属性,所以具有以下属性的页将会被忽略:
PAGE_READONLY
PAGE_EXECUTE_READ
PAGE_GUARD
PAGE_NOACCESS
现在模糊测试器可以保存和恢复进程快照,剩下的一个问题就是要能在被测程序中申请和是否随机数据。这样才可以对输入数据进行变异,这部分代码在Data.cpp中实现:
#include "Data.h"
#include "auxiliary.h"
// 本进程中申请内存空间用来写入被调试进程
char *g_pData;
PVOID GetData(HANDLE hProcess, DWORD dwSize)
{
PVOID pTarget = NULL;
// 在目标进程申请空间
pTarget = VirtualAllocEx(hProcess,
NULL,
dwSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (pTarget == NULL)
{
ShowError("VirtualAllocEx");
goto exit;
}
// 本进程中申请同样大小的内存空间,并初始化为随机数据
g_pData = (char *)VirtualAlloc(NULL,
dwSize,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
memset(g_pData, 0, dwSize);
// 赋值为1-255大小的随机数
for (DWORD i = 0; i < dwSize; i++)
{
g_pData[i] = rand() % 255 + 1;
}
// 将本进程中初始化的输入写入到目标进程中
if (!WriteProcessMemory(hProcess,
pTarget,
g_pData,
dwSize,
NULL))
{
ShowError("WriteProcessMemory");
goto exit;
}
exit:
return pTarget;
}
BOOL FreeData(HANDLE hProcess, PVOID pBaseAddr, DWORD dwSize)
{
BOOL bRet = TRUE;
// 释放掉目标进程中申请的内存空间
if (!VirtualFreeEx(hProcess,
pBaseAddr,
dwSize,
MEM_DECOMMIT))
{
ShowError("VirtualFreeEx");
bRet = FALSE;
goto exit;
}
// 释放掉本进程中申请的内存空间
if (!VirtualFree(g_pData,
dwSize,
MEM_DECOMMIT))
{
ShowError("VirtualFreeEx");
bRet = FALSE;
g_pData = NULL;
goto exit;
}
exit:
return bRet;
}
有了上面的功能,就可以实现一个实现恢复变异的模糊测试器了,代码如下:
#include "SRMFuzzy.h"
#include "Thread.h"
#include "Pages.h"
SRMFuzzy::SRMFuzzy(char *szFilePath,
DWORD dwFuncBegin,
DWORD dwFuncEnd):Fuzzy(szFilePath)
{
this->dwFuncBegin = dwFuncBegin;
this->dwFuncEnd = dwFuncEnd;
this->bOrgFuncBegin = 0;
this->bOrgFuncEnd = 0;
this->bInt3 = 0xCC;
}
void SRMFuzzy::BeginFuzzy()
{
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
DEBUG_EVENT DbgEvt = { 0 }; // 保存调试事件的数据结构
bool bContinue = true; // 是否继续
DWORD dwContinueStatus = DBG_CONTINUE; // 恢复继续执行用的状态代码
DWORD dwSize = 4; // 要修改的数据大小
PVOID pInputAddr = NULL; // 被被调试进程中申请的数据地址
CONTEXT cxt = { 0 };
bool bFirstPointer = true; // 记录是否是第一次触发函数起始地址的软件断点
si.cb = sizeof(si);
if (!CreateProcess(GetFilePath(),
NULL,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE |
DEBUG_PROCESS |
DEBUG_ONLY_THIS_PROCESS,
NULL,
NULL,
&si,
&pi))
{
ShowError("CreateProcess");
goto exit;
}
printf("Fuzzy Begin...\n");
while (bContinue && dwSize < MAX_PATH)
{
memset(&DbgEvt, 0, sizeof(DbgEvt));
// 等待调试事件发生
bContinue = WaitForDebugEvent(&DbgEvt, INFINITE);
if (!bContinue)
{
ShowError("WaitForDebugEvent");
break;
}
witch (DbgEvt.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
{
switch (DbgEvt.u.Exception.ExceptionRecord.ExceptionCode)
{
// 捕获到异常,打印出来
case EXCEPTION_ACCESS_VIOLATION:
{
printf(".....Catch Access Violation...\n");
printf("Address: 0x%X\n", DbgEvt.u.Exception.ExceptionRecord.ExceptionAddress);
printf("dwSize:%d\n", dwSize);
goto exit;
}
case EXCEPTION_BREAKPOINT:
{
// 触发断点则判断触发int 3断点的位置
DWORD dwPointAddr = (DWORD)DbgEvt.u.Exception.ExceptionRecord.ExceptionAddress;
if (dwPointAddr == this->dwFuncBegin)
{
// 断点位置是函数起始地址
// 是否是第一次触发函数起始处的软件断点
if (bFirstPointer)
{
// 保存主线程的状态
cxt.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(pi.hThread, &cxt))
{
ShowError("GetThreadContext");
goto exit;
}
// 将EIP重新指向函数的起始地址
cxt.Eip -= sizeof(this->bInt3);
// 这一次的保存是为了主线程加入到全局变量中的时候
// EIP可以指向函数起始地址
if (!SetThreadContext(pi.hThread, &cxt))
{
ShowError("SetThreadContext");
goto exit;
}
// 保存线程CONTEXT与内存状态到全局变量中
if (!SaveThreadContext(pi.dwProcessId) ||
!SavePages(pi.hProcess))
{
goto exit;
}
bFirstPointer = false;
}
// 恢复函数起始的字节
if (!WriteProcessMemory(pi.hProcess,
(PVOID)this->dwFuncBegin,
&(this->bOrgFuncBegin),
sizeof(this->bOrgFuncBegin),
NULL))
{
ShowError("WriteProcessMemory");
goto exit;
}
// 函数末尾设置为软件断点
if (!WriteProcessMemory(pi.hProcess,
(PVOID)this->dwFuncEnd,
&(this->bInt3),
sizeof(this->bInt3),
NULL))
{
ShowError("WriteProcessMemory");
goto exit;
}
// 在被调试进程中申请并初始化输入数据
pInputAddr = GetData(pi.hProcess, dwSize);
if (!pInputAddr)
{
goto exit;
}
// 设置输入数据地址为随机产生的数据地址
if (!WriteProcessMemory(pi.hProcess,
(PVOID)(cxt.Esp + 4),
(PVOID)&pInputAddr,
sizeof(pInputAddr),
NULL))
{
ShowError("WriteProcessMemory");
goto exit;
}
// 不是第一次触发断点,此时就要修改主线程CONTEXT的EIP
if (!bFirstPointer)
{
if (!SetThreadContext(pi.hThread, &cxt))
{
ShowError("SetThreadContext");
goto exit;
}
}
}
else if (dwPointAddr == this->dwFuncEnd)
{
// 执行到函数末尾
// 释放掉申请的内存
if (!FreeData(pi.hProcess, pInputAddr, dwSize))
{
goto exit;
}
// 重新设定申请的内存大小
pInputAddr = NULL;
dwSize += 4;
// 恢复线程CONTEXT与内存状态
if (!RestoreThreadContext() ||
!RestorePages(pi.hProcess))
{
goto exit;
}
// 修改函数起始地址中的字节为软件断点
if (!WriteProcessMemory(pi.hProcess,
(PVOID)this->dwFuncBegin,
&(this->bInt3),
sizeof(this->bInt3),
NULL))
{
ShowError("WriteProcessMemory");
goto exit;
}
}
break;
}
default:
{
break;
}
}
break;
}
case CREATE_PROCESS_DEBUG_EVENT:
{
// 被调试进程创建的时候将函数的原始字节读取出来
// 随后写入软件断点
if (DbgEvt.dwProcessId == pi.dwProcessId)
{
if (!ReadProcessMemory(pi.hProcess,
(PVOID)this->dwFuncBegin,
&(this->bOrgFuncBegin),
sizeof(this->bOrgFuncBegin),
NULL))
{
ShowError("ReadProcessMemory");
goto exit;
}
if (!WriteProcessMemory(pi.hProcess,
(PVOID)this->dwFuncBegin,
&(this->bInt3),
sizeof(this->bInt3),
NULL))
{
ShowError("WriteProcessMemory");
goto exit;
}
if (!ReadProcessMemory(pi.hProcess,
(PVOID)this->dwFuncEnd,
&(this->bOrgFuncEnd),
sizeof(this->bOrgFuncEnd),
NULL))
{
ShowError("ReadProcessMemory");
goto exit;
}
if (!WriteProcessMemory(pi.hProcess,
(PVOID)this->dwFuncEnd,
&(this->bInt3),
sizeof(this->bInt3),
NULL))
{
ShowError("WriteProcessMemory");
goto exit;
}
printf("Init BeakPoint ok...\n");
}
break;
}
default:
{
break;
}
}
// 恢复被调试进程继续运行
bContinue = ContinueDebugEvent(DbgEvt.dwProcessId,
DbgEvt.dwThreadId,
dwContinueStatus);;
if (!bContinue)
{
ShowError("ContinueDebugEvent");
break;
}
}
exit:
printf("Fuzzy End...\n");
}
运行结果如下:
模糊测试器成功捕获到错误,而且此时只需要启动一次被测程序就可以完成任务。不需要在反复经历Init->Decrypt->test->Vuln这些流程,降低了性能的开销。
看雪ID:1900
https://bbs.kanxue.com/user-home-835440.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!