初探Windows应用层反调试技术
0x01 PEB相关标志位反调试
PEB是一个非常庞大的数据结构,它是用来存储每个进程的运行时数据。下图为Windows 10的_PEB部分结构。
在VS中获取当前进程PEB结构体地址方法
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif // _WIN64
1.1 IsDebuggerPresent函数反调试
这个API是最经典检测调试器的函数,它底层原理就是返回PEB结构中BeingDebugged位的值,当有调试器附加的时候BeingDebugged位被置为1。
1.2 NtGlobalFlag标志位
PEB的NtGlobalFlag字段(32位Windows的0x68偏移,64位Windows的0xBC)默认为0。
附加一个调试器并不改变NtGlobalFlag的值。但是如果进程是由调试器创建的,则将设置以下标志:
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)
NtGlobalFlag值:
0000 0000 0111 0000
反调试检测:
#define FLG_HEAP_ENABLE_TAIL_CHECK 0x10
#define FLG_HEAP_ENABLE_FREE_CHECK 0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
#define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS)
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#endif // _WIN64
if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)
do something...
1.3 Heap Flags
在PEB的ProcessHeap位指向_HEAP结构体,该结构体中有俩个字段会受到调试器的影响,具体如何影响,取决于Windows的版本,主要是修改原始的内容,这两个字段是Flags和ForceFlags。
Win10 x86环境下检测代码:
BOOL CheckHeapFlagsDebug()
{
PPEB pPeb = (PPEB)__readfsdword(0x30);
PVOID pHeapBase = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18));
DWORD dwHeapFlagsOffset = 0x40;
DWORD dwHeapForceFlagsOffset = 0x44;
PDWORD pdwHeapFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapFlagsOffset);
PDWORD pdwHeapForceFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset);
//HEAP_GROWABLE (2)
return (*pdwHeapFlags & ~HEAP_GROWABLE) || (*pdwHeapForceFlags != 0);
}
1.4 堆Magic标志
当进程被调试器调试时该进程堆会被一些特殊的标志填充,这些特殊标记分别是0xABABABAB , 0xFEEEFEEE。在调试模式下, NtGlobalFlag的HEAP_TAIL_CHECKING_ENABLED 标志将被默认设置,堆内存分配会在末尾追加 0xABABABAB标志进行安全检查,如果NtGlobalFlag设置了HEAP_FREE_CHECKING_ENABLED标志,那么当需要额外的字节来填充堆块尾部时, 就会使用0xFEEEFEEE(或一部分) 来填充。PROCESS_HEAP_ENTRY结构详细介绍
BOOL CheckHeapMagic()
{
PROCESS_HEAP_ENTRY HeapEntry = { 0 };
do
{
if (!HeapWalk(GetProcessHeap(), &HeapEntry))
return false;
} while (HeapEntry.wFlags != PROCESS_HEAP_ENTRY_BUSY);
PVOID pOverlapped = (PBYTE)HeapEntry.lpData + HeapEntry.cbData;
return ((DWORD)(*(PDWORD)pOverlapped) == 0xABABABAB);
}
0x02 ThreadHideFromDebugger线程属性反调试
2.1 ZwSetInformationThread反调试
ThreadHideFromDebugger是线程的一个属性值,当线程具备ThreadHideFromDebugger特性时则该线程(一般是主线程)对“调试器”隐藏,使调试器无法继续接收该线程的调试事件。所以如果线程设置了ThreadHideFromDebugger那么当断点等调试事件触发时调试器表现为卡死。当线程启动后可以通过ZwSetInformationThread函数来设置,该函数需要动态从ntdll.dll中获取,以下是函数声明:
NTSTATUS (NTAPI*) pfnZwSetInformationThread(
HANDLE ThreadHandle,
THREADINFOCLASS ThreadInformationClass,
PVOID ThreadInformation,
ULONG ThreadInformationLength
);
调用方式:
HINSTANCE hModule;
pfnZwSetInformationThread ZwSetInformationThread;
hModule = GetModuleHandleA("Ntdll.dll");
ZwSetInformationThread = (pfnZwSetInformationThread)GetProcAddress(hModule,"ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);
2.2 NtCreateThreadEx反调试
Windows Vista新引入了NtCreateThreadEx函数,以下是函数声明:
#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
#define THREAD_CREATE_FLAGS_HAS_SECURITY_DESCRIPTOR 0x00000010
#define THREAD_CREATE_FLAGS_ACCESS_CHECK_IN_TARGET 0x00000020
#define THREAD_CREATE_FLAGS_INITIAL_THREAD 0x00000080
NTSTATUS NTAPI NtCreateThreadEx (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);
在CreateFlags这个参数中设置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,则线程创建时将对调试器隐藏该线程。这与NtSetInformationThread函数设置的ThreadHideFromDebugger相同。
0x03 NtQueryInformationProcess函数反调试
NtQueryInformationProcess函数是操作系统中十分有用的一个关键函数,可以用来查找进程的很多相关信息。以下是函数声明:
NTSTATUS NTAPI NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess
第一个参数表明待查询的目标进程句柄,而第二个参数则是标明查询的信息种类。PROCESSINFOCALASS是一个枚举类型,能查询近百种信息,其中以下四种信息是最常见可用于检测调试器的存在。
ProcessDebugPort // 0x7
ProcessDebugFlags // 0x1F
ProcessDebugObjectHandle // 0x1E
ProcessBasicInformation // 0x0
3.1 CheckRemoteDebuggerPresent函数
CheckRemoteDebuggerPresent函数是用来检测另一个进程是否处于调试状态,以下是利用该函数进行反调试的示例代码:
BOOL bDebugger = FALSE;
if (CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebugger ))
{
if (bDebugger)
do something...
}
ProcessDebugPort端口是Windows调试子系统依赖的一个数据结构,可以通过检测调试端口的方式来检测进程是否被调试。在CheckRemoteDebuggerPresent函数内部就是调用NtQueryInformationProcess函数查询ProcessDebugPort信息,判断目标进程是否在调试状态,没有调试器的时候值为0。
3.2 ProcessDebugObjectHandle反调试
ProcessDebugObjectHandle的内容为调试对象的句柄,没有调试器的时候值为0,以下为示例代码:
DWORD bDebugger = -1;
NTSTATUS status = NtQueryInformationProcess(
GetCurrentProcess(), // 进程句柄
0x1E, // 要检索的进程信息类型,ProcessDebugObjectHandle
&bDebugger, // 接收进程信息的缓冲区指针
sizeof(DWORD), // 缓冲区大小
NULL // 实际返回进程信息的大小
);
3.3 ProcessDebugFlags反调试
进程调试标志位,当程序处于调试状态的时候ProcessDebugFlags = 0,以下为示例代码:
DWORD bDebugger = 0;
NTSTATUS status = NtQueryInformationProcess(
GetCurrentProcess(), // 进程句柄
0x1F, // 要检索的进程信息类型,ProcessDebugFlags
&bDebugger, // 接收进程信息的缓冲区指针
sizeof(DWORD), // 缓冲区大小
NULL // 实际返回进程信息的大小
);
3.4 ProcessBasicInformation反调试
当使用ProcessBasicInformation标志调用NtQueryInformationProcess函数时,将返回PROCESS_BASIC_INFORMATION结构,以下是在官方定义的基础上进行完整化的结构信息:
typedef struct _PROCESS_BASIC_INFORMATION
{
DWORD ExitStatus; // 接收进程终止状态
DWORD PebBaseAddress; // 接收进程环境块地址(PEB)
DWORD AffinityMask; // 接收进程关联掩码
DWORD BasePriority; // 接收进程的优先级类
ULONG UniqueProcessId; // 接收进程ID
ULONG InheritedFromUniqueProcessId; // 接收父进程ID
} PROCESS_BASIC_INFORMATION;
我们知道一般情况下都是通过Windows的资源管理器来打开程序,所以一般的手动打开的进程父进程都是explorer.exe,如果通过调试器创建进程则父进程就不会是explorer.exe。所以可以通过检测父进程是否是explorer.exe来检测调试器的存在。
通过这个标志我们就可以获得父进程ID(Reserved3),后续在通过OpenProcess获取父进程句柄,调用GetProcessImageFileName获得父进程名进行比较即可判断是否被调试。
0x04 基于时间的反调试
在现代计算机中一段代码的执行通常不会消耗很多时间,但是如果程序处于调试状态,则他的执行时间就不可控了。这种检测本质就是通过获取时间的函数比较时间差进程检测。
4.1 rdtsc函数反调试
原理很直接了就是在代码的俩个地方执行rdtsc函数,然后检测两次执行的时间差。
BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
ULARGE_INTEGER Start, End;
__asm
{
xor ecx, ecx
rdtsc
mov Start.LowPart, eax
mov Start.HighPart, edx
}
// do something...
__asm
{
xor ecx, ecx
rdtsc
mov End.LowPart, eax
mov End.HighPart, edx
}
return (End.QuadPart - Start.QuadPart) > qwNativeElapsed;
}
4.2 GetTickCount函数反调试
BOOL IsDebugged(DWORD dwNativeElapsed)
{
DWORD dwStart = GetTickCount();
// do something...
DWORD dwEnd = GetTickCount();
return (dwEnd - dwStart) > dwNativeElapsed;
}
4.3 GetLocalTime函数反调试
BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
SYSTEMTIME stStart, stEnd;
FILETIME ftStart, ftEnd;
ULARGE_INTEGER uiStart, uiEnd;
GetLocalTime(&stStart);
// do something...
GetLocalTime(&stEnd);
if (!SystemTimeToFileTime(&stStart, &ftStart))
return FALSE;
if (!SystemTimeToFileTime(&stEnd, &ftEnd))
return FALSE;
uiStart.LowPart = ftStart.dwLowDateTime;
uiStart.HighPart = ftStart.dwHighDateTime;
uiEnd.LowPart = ftEnd.dwLowDateTime;
uiEnd.HighPart = ftEnd.dwHighDateTime;
return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed;
}
4.4 GetSystemTime函数反调试
BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
SYSTEMTIME stStart, stEnd;
FILETIME ftStart, ftEnd;
ULARGE_INTEGER uiStart, uiEnd;
GetSystemTime(&stStart);
// do something...
GetSystemTime(&stEnd);
if (!SystemTimeToFileTime(&stStart, &ftStart))
return FALSE;
if (!SystemTimeToFileTime(&stEnd, &ftEnd))
return FALSE;
uiStart.LowPart = ftStart.dwLowDateTime;
uiStart.HighPart = ftStart.dwHighDateTime;
uiEnd.LowPart = ftEnd.dwLowDateTime;
uiEnd.HighPart = ftEnd.dwHighDateTime;
return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed;
}
4.5 QueryPerformanceCounter函数反调试
BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
LARGE_INTEGER liStart, liEnd;
QueryPerformanceCounter(&liStart);
// do something...
QueryPerformanceCounter(&liEnd);
return (liEnd.QuadPart - liStart.QuadPart) > qwNativeElapsed;
}
0x05 基于异常的反调试
异常处理是Windows操作系统中非常重要的一种机制,它包括VEH/SEH等等,异常处理程序通常会脱离原有的程序执行流程,它很多时候可以被用于反调试。若进程在调试运行中发生异常,调试器就会接受处理。利用该特征就可以判断进程使正常运行还是调试运行,然后根据不同的结果执行不同的操作。
在代码编写过程中使用\_\_try/\_\_except 关键字可以设置一个SEH异常处理函数
int main()
{
__try
{
cout << "hello,world" << endl;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
cout << "异常" << endl;
}
return 0;
}
__except中值的含义:
1:处理异常
0:不处理异常交给下一个异常节点去处理
-1:继续执行也就是继续EIP处理执行,但是这里又有异常,所以这里就会一直卡在这里
Windows的异常处理流程大致为:
1. 交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. TopLevelEH(进程被调试时不会被执行)
5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe
也就是说当一个异常被触发最先会到调试器(如果被调试),所以我们可以在__except内部动手脚以实现反调试,示例代码如下:
BOOL IsDebugged()
{
__try{
__asm int 3
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return FALSE;
}
return TRUE;
}
0x06 总结
以上是在此前学习过程中收集的一些应用层反调试原理,其中还有很多比如获取窗口,文件完整性检验,代码重映射,硬件断点检测等由于篇幅的原因无法详细展开,如果文中出现什么错误还望师傅指正。
下一篇打算结合SycllaHide这个开源反调试插件项目探索一下Windows应用层反反调试的原理和实现。