0x01 PEB相关标志位反调试
简单的获取当前进程的PEB结构体地址的方法:
1 2 3 4 5
| #ifndef _WIN64 PPEB pPeb = (PPEB)__readfsdword(0x30); #else PPEB pPeb = (PPEB)__readgsqword(0x60); #endif
|
1.1 IsDebuggerPresent
底层原理就是返回PEB结构中BeingDebugged位的值,当有调试器附加的时候BeingDebugged位被置为1。
1.2 NtGlobalFlag
附加一个调试器并不改变NtGlobalFlag的值。但是如果进程是由调试器创建的,则将设置以下标志:
1 2 3 4 5 6
| FLG_HEAP_ENABLE_TAIL_CHECK (0x10) FLG_HEAP_ENABLE_FREE_CHECK (0x20) FLG_HEAP_VALIDATE_PARAMETERS (0x40)
NtGlobalFlag值: 0000 0000 0111 0000
|
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #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 if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED) do something...
|
NtGlobalFlag
的汇编代码如下,如果返回值为0x70
则程序处于调试状态
1 2 3 4 5
| mov eax, fs:[30h] ;Process Environment Block mov al, [eax+68h] ;NtGlobalFlag and al, 70h cmp al, 70h je being_debugged
|
相应函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| bool CheckNtGlobalFlag() { BOOL IsDebug = FALSE;
DWORD NtGlobalFlag = 0; __asm { mov eax, fs:[0x30] mov eax, [eax + 0x68] mov NtGlobalFlag, eax }
if (NtGlobalFlag == 0x70) { IsDebug = TRUE; }
return IsDebug; }
|
1.3 Heap Flag
在PEB的ProcessHeap位指向_HEAP结构体,该结构体中有俩个字段会受到调试器的影响,具体如何影响,取决于Windows的版本,主要是修改原始的内容,这两个字段是Flags和ForceFlags。
Flags and ForceFlags
的值通常分别设置为HEAP_GROWABLE
和0
。
在 64 位 Windows XP 和 Windows Vista 及更高版本上,如果存在调试器,则 Flags 字段将设置为以下标志的组合:
HEAP_GROWABLE (2)
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)
当存在调试器时,ForceFlags 字段设置为以下标志的组合:
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)
例子: Win10 x86环境 1 2 3 4 5 6 7 8 9 10 11 12 13
| 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); return (*pdwHeapFlags & ~HEAP_GROWABLE) || (*pdwHeapForceFlags != 0); }
|
1.4 堆保护
当进程被调试器调试时该进程堆会被一些特殊的标志填充,这些特殊标记分别是0xABABABAB , 0xFEEEFEEE。
在调试模式下, NtGlobalFlag的HEAP_TAIL_CHECKING_ENABLED 标志将被默认设置,堆内存分配会在末尾追加 0xABABABAB标志进行安全检查,如果NtGlobalFlag设置了HEAP_FREE_CHECKING_ENABLED标志,那么当需要额外的字节来填充堆块尾部时, 就会使用0xFEEEFEEE(或一部分) 来填充。
微软PROCESS_HEAP_ENTRY结构文档
1 2 3 4 5 6 7 8 9 10 11 12
| 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线程属性反调试
ThreadHideFromDebugger
是线程的一个属性值:
当线程具备该特性时则该线程对“调试器”隐藏(一般是主线程),能够使得主线程无法继续接受该线程的调试事件。
如果线程设置了ThreadHideFromDebugger那么当断点等调试事件触发时调试器表现为卡死。
线程启动后可以通过ZwSetInformationThread()
函数来设置,该函数动态加载自ntdll.dll
文件,以下是函数声明:
1 2 3 4 5 6
| NTSYSAPI NTSTATUS ZwSetInformationThread( [in] HANDLE ThreadHandle, [in] THREADINFOCLASS ThreadInformationClass, [in] PVOID ThreadInformation, [in] ULONG ThreadInformationLength );
|
官方文档
该函数用于设置线程的优先级,ThreadInfomationClass的值可以搜索
原理:
将HideFromDebugger的值置1,从而实现反调试。
别人的反反调试:
把对应位置置0.
调用例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <Windows.h> #include <stdio.h>
typedef DWORD(WINAPI* ZW_SET_INFORMATION_THREAD) (HANDLE, DWORD, PVOID, ULONG); #define ThreadHideFromDebugger 0x11 VOID DisableDebugEvent(VOID) { HINSTANCE hModule; ZW_SET_INFORMATION_THREAD ZwSetInformationThread; hModule = GetModuleHandleA("Ntdll"); ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule, "ZwSetInformationThread"); ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0); }
int main() { printf("Begin\n"); DisableDebugEvent(); printf("End\n"); return 0; }
|
2.2 NtCreateThreadEx反调试
Windows Vista新引入了NtCreateThreadEx函数,以下是函数声明:
在32位下和64位下函数原型不一致
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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可以使用如下标志:
1 2 3 4 5 6
| #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
|
如果新线程设置了THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
标志,那么在创建时就可以向调试器隐藏该线程信息,这与NtSetInformationThread
函数设置的ThreadHideFromDebugger
相同。负责安全任务的代码可以在设置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
标志的线程中执行。
NtQueryInformationProcess函数是操作系统中十分有用的一个关键函数,可以用来查找进程的很多相关信息。以下是函数声明:
1 2 3 4 5 6 7
| 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是一个枚举类型,能查询近百种信息,其中以下四种信息是最常见可用于检测调试器的存在。
1 2 3 4
| ProcessDebugPort ProcessDebugFlags ProcessDebugObjectHandle ProcessBasicInformation
|
3.1 CheckRemoteDebuggerPresent函数
ProcessDebugPort端口是Windows调试子系统依赖的一个数据结构,可以通过检测调试端口的方式来检测进程是否被调试。
在CheckRemoteDebuggerPresent函数内部就是调用NtQueryInformationProcess函数查询ProcessDebugPort信息,判断目标进程是否在调试状态,没有调试器的时候值为0。
3.2 ProcessDebugObjectHandle
ProcessDebugObjectHandle的内容为调试对象的句柄,没有调试器的时候值为0,以下为示例代码:
1 2 3 4 5 6 7 8
| DWORD bDebugger = -1; NTSTATUS status = NtQueryInformationProcess( GetCurrentProcess(), 0x1E, &bDebugger, sizeof(DWORD), NULL );
|
3.3 ProcessDebugFlags反调试
进程调试标志位,当程序处于调试状态的时候ProcessDebugFlags = 0,以下为示例代码:
1 2 3 4 5 6 7 8
| DWORD bDebugger = 0; NTSTATUS status = NtQueryInformationProcess( GetCurrentProcess(), 0x1F, &bDebugger, sizeof(DWORD), NULL );
|
当使用ProcessBasicInformation标志调用NtQueryInformationProcess函数时,将返回PROCESS_BASIC_INFORMATION结构,以下是在官方定义的基础上进行完整化的结构信息:
1 2 3 4 5 6 7 8 9
| typedef struct _PROCESS_BASIC_INFORMATION { DWORD ExitStatus; DWORD PebBaseAddress; DWORD AffinityMask; DWORD BasePriority; ULONG UniqueProcessId; ULONG InheritedFromUniqueProcessId; } PROCESS_BASIC_INFORMATION;
|
通过这个标志我们就可以获得父进程ID(Reserved3),后续在通过OpenProcess获取父进程句柄,调用GetProcessImageFileName获得父进程名进行比较即可判断是否被调试。
0x04 基于时间的反调试
在现代计算机中一段代码的执行通常不会消耗很多时间,但是如果程序处于调试状态,则他的执行时间就不可控了。这种检测本质就是通过获取时间的函数比较时间差进程检测。
0x05 基于异常处理的反调试
Windows的异常处理流程大致为:
1 2 3 4 5 6
| 1. 交给调试器(进程必须被调试) 2. 执行VEH 3. 执行SEH 4. TopLevelEH(进程被调试时不会被执行) 5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器) 6. 调用异常端口通知csrss.exe
|
未完待续
https://www.anquanke.com/post/id/179709
https://www.anquanke.com/post/id/179710