71-反调试 - 多种技术
简介
安全研究人员和恶意软件分析人员会使用调试来增强对恶意软件样本的了解。这使他们能够针对这些样本编写更好的检测规则。作为一个恶意软件开发人员,您应该始终采用反调试技术来让分析人员花费更多的时间在分析上。
本篇模块将讨论几种反调试技术。
使用 IsDebuggerPresent 检测调试器
最简单的反调试技术之一是使用 WinAPI IsDebuggerPresent。此函数如果检测到调试器附加到调用进程,则返回 TRUE
;如果未检出调试器,则返回 FALSE
。以下代码片段展示了检测调试器的函数。
if (IsDebuggerPresent()) {
printf("[i] IsDebuggerPresent 检测到调试器 \n");
// 运行无害代码。
}
IsDebuggerPresent 替代方法 (1)
调用 IsDebuggerPresent
WinAPI 可疑,即使它通过 API 哈希进行了良好的隐藏。该 WinAPI 被认为是检测调试程序的一种非常基础的方法,可以使用 ScyllaHide 等工具来绕过它,这是一个适用于 xdbg 的反反调试插件。
更好的方法是创建 IsDebuggerPresent
WinAPI 的自定义版本。回想一下 Windows 进程 - 初学者模块 展示了具有 BeingDebugged
成员的 PEB 结构,当进程正在被调试时将该成员设置为 1。一个简单的 IsDebuggerPresent
WinAPI 替换涉及检查 BeingDebugged
值,如下面的自定义函数所示。
如果 BeingDebugged
元素被设置为 1,则 IsDebuggerPresent2
函数返回 TRUE
。
BOOL IsDebuggerPresent2() {
// 获取 PEB 结构
#ifdef _WIN64
PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif
// 检查 'BeingDebugged' 元素
if (pPeb->BeingDebugged == 1)
return TRUE;
return FALSE;
}
替代 IsDebuggerPresent 方法(2)
另一种自定义 IsDebuggerPresent
WinAPI 版本的方法是利用未记录的 NtGlobalFlag 标志,它也位于 PEB 结构中。如果进程正在被调试,则 NtGlobalFlag
成员被设为十六进制 0x70
,否则为 0。需要注意的是,只有当进程由调试器创建时,NtGlobalFlag
元素才被设为 0x70
。因此,如果在执行之后附加了调试器,此方法将无法检测到调试器。
值 0x70
来源于以下标志的组合:
FLG_HEAP_ENABLE_TAIL_CHECK
-0x10
FLG_HEAP_ENABLE_FREE_CHECK
-0x20
FLG_HEAP_VALIDATE_PARAMETERS
-0x40
如果 NtGlobalFlag
元素被设为 0x70
,则 IsDebuggerPresent3
函数返回 TRUE
。
#define FLG_HEAP_ENABLE_TAIL_CHECK 0x10
#define FLG_HEAP_ENABLE_FREE_CHECK 0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
BOOL IsDebuggerPresent3() {
// 获取 PEB 结构
#ifdef _WIN64
PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif
// 检查 'NtGlobalFlag' 元素
if (pPeb->NtGlobalFlag == (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS))
return TRUE;
return FALSE;
}
通过 NtQueryInformationProcess 检测调试器
NtQueryInformationProcess
系统调用将使用两个标志位 ProcessDebugPort
和 ProcessDebugObjectHandle
来检测调试器。
回想一下 NtQueryInformationProcess
类似于以下内容:
NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle, // 要检索信息的进程句柄。
IN PROCESSINFOCLASS ProcessInformationClass, // 要检索的进程信息类型
OUT PVOID ProcessInformation, // 函数将请求的信息写入的缓冲区的指针
IN ULONG ProcessInformationLength, // 'ProcessInformation' 参数所指缓冲区的大小
OUT PULONG ReturnLength // 函数返回请求的信息大小的变量指针
);
ProcessDebugPort 标志
Microsoft 关于 ProcessDebugPort
标志的文档指出以下内容:
获取一个 DWORD_PTR 值,该值是进程调试器的端口号。非零值表示该进程正在环 3 调试器的控制下运行
换句话说,如果 NtQueryInformationProcess
返回 ProcessInformation
参数接收到的非零值,则该进程正在被主动调试。
ProcessDebugObjectHandle 标志
未公开的标志 ProcessDebugObjectHandle
的工作原理类似于之前的 ProcessDebugPort
标志,用于获取当前进程的调试对象句柄,当进程正在进行调试时会创建该句柄。通过 NtQueryInformationProcess
获得的 ProcessInformation
参数中的非零值表示对进程进行了主动调试。
如果 NtQueryInformationProcess
无法检索调试对象句柄,这意味着它未检测到调试器,并将返回错误代码 0xC0000353
。根据 Microsoft 在 NTSTATUS 值 上的文档,该错误代码等效于 STATUS_PORT_NOT_SET
。
利用 NtQueryInformationProcess 检测调试器
NtQIPDebuggerCheck
函数同时使用ProcessInformation
和ProcessDebugObjectHandle
来检测调试器。如果 NtQueryInformationProcess
使用 ProcessDebugPort
和 ProcessDebugObjectHandle
标志返回一个有效句柄,则该函数返回 TRUE
。
BOOL NtQIPDebuggerCheck() {
NTSTATUS STATUS = NULL;
auto pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(
GetModuleHandle(TEXT("NTDLL.DLL")), "NtQueryInformationProcess");
if (pNtQueryInformationProcess == NULL) {
printf("\t[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return FALSE;
}
DWORD64 dwIsDebuggerPresent = NULL;
DWORD64 hProcessDebugObject = NULL;
// 使用'ProcessDebugPort'标志调用NtQueryInformationProcess
STATUS = pNtQueryInformationProcess(
GetCurrentProcess(), ProcessDebugPort, &dwIsDebuggerPresent,
sizeof(DWORD64), NULL);
if (STATUS != 0x0) {
printf("\t[!] NtQueryInformationProcess [1] Failed With Status : 0x%0.8X \n",
STATUS);
return FALSE;
}
// 如果NtQueryInformationProcess返回非零值,则该句柄是有效的,这意味着我们正在被调试
if (dwIsDebuggerPresent != NULL) {
// 检测到调试器
return TRUE;
}
//使用'ProcessDebugObjectHandle'标志调用NtQueryInformationProcess
STATUS = pNtQueryInformationProcess(
GetCurrentProcess(), ProcessDebugObjectHandle, &hProcessDebugObject,
sizeof(DWORD64), NULL);
// 如果STATUS不为0且不为0xC0000353(即'STATUS_PORT_NOT_SET')
if (STATUS != 0x0 && STATUS != 0xC0000353) {
printf("\t[!] NtQueryInformationProcess [2] Failed With Status : 0x%0.8X \n",
STATUS);
return FALSE;
}
// 如果NtQueryInformationProcess返回非零值,则该句柄是有效的,这意味着我们正在被调试
if (hProcessDebugObject != NULL) {
// 检测到调试器
return TRUE;
}
return FALSE;
}
通过硬件断点检测调试器
此方法仅在调试期间设置硬件断点时有效。硬件断点(也称为硬件调试寄存器)是现代微处理器的特性,它在触发特定内存地址或事件时暂停进程的执行。硬件断点在处理器中实现,因此比普通软件断点更快、更高效。普通软件断点依赖于操作系统或调试器定期检查程序执行。
设置硬件断点后,特定寄存器的值会发生改变。这些寄存器的值可用于确定是否将调试器附加到进程。如果寄存器 Dr0
、Dr1
、Dr2
和 Dr3
包含非零值,则设置了硬件断点。以下示例使用 xdbg 调试器对 NtAllocateVirtualMemory
系统调用放置了一个硬件断点。请注意,Dr0
的值已从零更改为 NtAllocateVirtualMemory
的地址。
![图片](https://maldevacademy.s3.amazonaws.com/images/Intermediate/anti-debugging-115282576-1557ca5f-2841-4a0f-ad73-63c30e03c843.png)
![图片](https://maldevacademy.s3.amazonaws.com/images/Intermediate/anti-debugging-215283166-37faff36-628c-43e4-aaf1-e41ad6310dd9.png)
![图片](https://maldevacademy.s3.amazonaws.com/images/Intermediate/anti-debugging-315282633-6d0bf541-7327-42b9-af79-0b9f9489cd68.png)
获取寄存器值
要获取 Dr
寄存器值,可以使用 GetThreadContext
WinAPI。在_线程劫持_模块中,GetThreadContext
用于检索指定线程的上下文。上下文以 CONTEXT
结构返回。此结构还包括 Dr0
、Dr1
、Dr2
和 Dr3
寄存器值。
HardwareBpCheck
函数通过检查上述寄存器值来检测调试器的存在。如果检测到调试器,该函数返回 TRUE
。
BOOL HardwareBpCheck() {
CONTEXT Ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
if (!GetThreadContext(GetCurrentThread(), &Ctx)) {
printf("\t[!] GetThreadContext 失败。错误代码:%d \n", GetLastError());
return FALSE;
}
if (Ctx.Dr0 != NULL || Ctx.Dr1 != NULL || Ctx.Dr2 != NULL || Ctx.Dr3 != NULL)
return TRUE; // 检测到调试器
return FALSE;
}
通过黑名单数组检测调试器
检测调试进程的另一种方法是检查当前运行进程的名称,并与已知的调试器名称列表进行对比。此名称的“黑名单”存储在硬编码的数组中。如果进程名称与黑名单匹配,则表明系统上正在运行一个调试器应用程序。
可以从前面讨论过的任何技术中枚举计算机上正在运行的进程。对于此方案,将使用 CreateToolhelp32Snapshot
进程枚举技术。
黑名单数组表示如下:
#define BLACKLISTARRAY_SIZE 5 // 数组中元素的数量
WCHAR* g_BlackListedDebuggers[BLACKLISTARRAY_SIZE] = {
L"x64dbg.exe", // xdbg 调试器
L"ida.exe", // IDA 反汇编器
L"ida64.exe", // IDA 反汇编器
L"VsDebugConsole.exe", // Visual Studio 调试器
L"msvsmon.exe" // Visual Studio 调试器
};
黑名单数组应尽可能包含更多调试器名称,以便检测更多种类的调试器。此外,应使用字符串哈希处理字符串,因为二进制文件中的调试器名称可用作 IOC(入侵指标)。
BlackListedProcessesCheck
函数使用 g_BlackListedDebuggers
数组作为黑名单进程数组。如果进程名称与 g_BlackListedDebuggers
的元素匹配,它将返回 TRUE
:
BOOL BlackListedProcessesCheck() {
HANDLE hSnapShot = NULL;
PROCESSENTRY32W ProcEntry = { .dwSize = sizeof(PROCESSENTRY32W) };
BOOL bSTATE = FALSE;
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE) {
printf("\t[!] CreateToolhelp32Snapshot 失败,错误代码: %d \n", GetLastError());
goto _EndOfFunction;
}
if (!Process32FirstW(hSnapShot, &ProcEntry)) {
printf("\t[!] Process32FirstW 失败,错误代码: %d \n", GetLastError());
goto _EndOfFunction;
}
do {
// 遍历 `g_BlackListedDebuggers` 数组并将其每个元素与
// 从快照中获取的当前进程名称进行比较
for (int i = 0; i < BLACKLISTARRAY_SIZE; i++) {
if (wcscmp(ProcEntry.szExeFile, g_BlackListedDebuggers[i]) == 0) {
// 检测到调试器
wprintf(L"\t[i] 找到了 Pid 为 %d 的 \"%s\" \n", ProcEntry.szExeFile, ProcEntry.th32ProcessID);
bSTATE = TRUE;
break;
}
}
} while (Process32Next(hSnapShot, &ProcEntry));
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
return bSTATE;
}
通过 GetTickCount64 检测断点
断点用于在程序运行的特定点暂停执行,以供分析内存、寄存器状态、变量等。
可以通过使用 GetTickCount64 WinAPI 检测执行暂停。此函数会检索自系统启动以来经过的毫秒数。通过分析处理器在两次 GetTickCount64
调用之间花费的时间,可以指示恶意软件是否正在调试。如果花费的时间超出了预期,则可以安全地假定恶意软件正在被调试。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/anti-debugging-415305654-6593a2cd-5fc1-4f8c-b4dc-9f4eb55c47b6.png)
识别延迟
可以通过计算 T1 - T0 的平均值并将其存储为硬编码值来检测断点。当 T1 - T0 的输出超过此值时,延迟可能是由于断点造成的。例如,如果 T1 - T0 主机上的输出为 20 秒,但在运行时输出大于该值,则 T1 - T0 之间的延迟很可能是由于断点引起的。应略微增加原始值以考虑可能会更慢的处理器。
GetTickCount64反调试代码
TimeTickCheck1
函数使用相应方法检测断点。如果dwTime2 - dwTime1
超过中间执行代码的平均值(即50),则该函数将返回TRUE
。
BOOL TimeTickCheck1() {
DWORD dwTime1 = NULL,
dwTime2 = NULL;
dwTime1 = GetTickCount64();
/*
其它代码
*/
dwTime2 = GetTickCount64();
printf("\t[i] (dwTime2 - dwTime1) : %d \n", (dwTime2 - dwTime1));
if ((dwTime2 - dwTime1) > 50) {
return TRUE;
}
return FALSE;
}
通过 QueryPerformanceCounter 检测断点
QueryPerformanceCounter WinAPI 与先前显示的 GetTickCount64
WinAPI 相同。区别在于,QueryPerformanceCounter
使用硬件提供的具有高解析度性能计数器,该计数器可以以纳秒为增量测量时间,而 GetTickCount64
使用每毫秒增量的时间计数器。请注意,QueryPerformanceCounter
以计数形式而不是毫秒形式检索性能计数器值。
TimeTickCheck2
函数使用 QueryPerformanceCounter
WinAPI 检测断点。如果 Time2.QuadPart - Time1.QuadPart
超过中间执行代码的平均值(100000 个计数),则返回 TRUE。
BOOL TimeTickCheck2() {
LARGE_INTEGER Time1 = { 0 },
Time2 = { 0 };
if (!QueryPerformanceCounter(&Time1)) {
printf("\t[!] QueryPerformanceCounter [1] Failed With Error : %d \n", GetLastError());
return FALSE;
}
/*
其他代码
*/
if (!QueryPerformanceCounter(&Time2)) {
printf("\t[!] QueryPerformanceCounter [2] Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("\t[i] (Time2.QuadPart - Time1.QuadPart) : %d \n", (Time2.QuadPart - Time1.QuadPart));
if ((Time2.QuadPart - Time1.QuadPart) > 100000) {
return TRUE;
}
return FALSE;
}
通过 DebugBreak 检测调试器
DebugBreak 会导致在当前进程中发生断点异常 EXCEPTION_BREAKPOINT
。如果调试器已附加到当前进程,则该异常应由调试器处理。该技术的原理是触发异常并查看调试器是否尝试处理此异常。
我们将使用 __try
和 __except
代码块来处理来自 DebugBreak
调用的异常,并使用 GetExceptionCode 调用来获取在这种情况下生成的异常代码,这有两种可能的情况:
如果获取的异常为
EXCEPTION_BREAKPOINT
,则执行EXCEPTION_EXECUTE_HANDLER
,这表示该异常未被调试器处理。如果异常不是
EXCEPTION_BREAKPOINT
,则表示调试器处理了引发的异常(而不是我们的 try-except 代码块),然后执行EXCEPTION_CONTINUE_SEARCH
,这会强制调试器负责处理引发的异常。
以下 DebugBreakCheck
函数如果 WinAPI DebugBreak
成功执行且异常未被调试器捕获/处理,而是由我们的 try-except 代码块处理,则返回 FALSE
,表示当前进程未附加调试器。
BOOL DebugBreakCheck() {
__try {
DebugBreak();
}
__except (GetExceptionCode() == EXCEPTION_BREAKPOINT ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
// 如果异常等于 EXCEPTION_BREAKPOINT,则执行 EXCEPTION_EXECUTE_HANDLER 并且函数返回 FALSE
return FALSE;
}
// 如果异常不等于 EXCEPTION_BREAKPOINT,则执行 EXCEPTION_CONTINUE_SEARCH 并且函数返回 TRUE
return TRUE;
}
通过 OutputDebugString 检测调试器
另一种可用于检测调试器的 WinAPI 是 OutputDebugString。此函数用于向调试器发送要显示的字符串。如果存在调试器,则 OutputDebugString
将成功执行其任务。
可以运行 OutputDebugString
并使用 GetLastError
检查其是否失败;如果失败,则 GetLastError
将返回一个非零错误代码。在这种情况下,非零错误代码等同于没有调试器正在运行。如果 GetLastError
返回零,则 OutputDebugString
成功向调试器发送了一条字符串。
OutputDebugStringCheck
函数使用上述逻辑,如果 OutputDebugStringW
成功,则返回 TRUE
。此外,它使用 SetLastError 将最后一个错误值设置为 1。这仅仅是为了确保在调用 OutputDebugString
之前它是一个非零值,以减少误报。
BOOL OutputDebugStringCheck() {
SetLastError(1);
OutputDebugStringW(L"MalDev Academy");
// 如果 GetLastError 为 0,则 OutputDebugStringW 成功了
if (GetLastError() == 0) {
return TRUE;
}
return FALSE;
}