反调试技术
调试技术可以直接探查出程序运行的机制,程序作者为了隐藏程序底层机制而通过反调试技术来反制。
对于一般的程序需要防止核心代码被调试逆向,软件或专利技术被破解;而病毒或恶意代码会隐藏自己的恶意行为防止被跟踪。
所以对于反调试技术我们是有了解的必要的。
Windows 前置知识
以下存在未公开结构体 (地址为 32 位,具体请参考微软官方文档)
TEB 线程环境块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| typedef struct _TEB { NT_TIB Tib; PVOID EnvironmentPointer; CLIENT_ID Cid; PVOID ActiveRpcHandle; PVOID ThreadLocalStoragePointer; struct _PEB *ProcessEnvironmentBlock; ULONG LastErrorValue; ULONG CountOfOwnedCriticalSections; PVOID CsrClientThread; struct _W32THREAD* Win32ThreadInfo; ULONG User32Reserved[0x1A]; ULONG UserReserved[5]; PVOID WOW32Reserved; LCID CurrentLocale; ULONG FpSoftwareStatusRegister; PVOID SystemReserved1[0x36]; LONG ExceptionCode; struct _ACTIVATION_CONTEXT_STACK *ActivationContextStackPointer; UCHAR SpareBytes1[0x28]; GDI_TEB_BATCH GdiTebBatch; CLIENT_ID RealClientId; PVOID GdiCachedProcessHandle; ULONG GdiClientPID; ULONG GdiClientTID; PVOID GdiThreadLocalInfo; ULONG Win32ClientInfo[62]; PVOID glDispatchTable[0xE9]; ULONG glReserved1[0x1D]; PVOID glReserved2; PVOID glSectionInfo; PVOID glSection; PVOID glTable; PVOID glCurrentRC; PVOID glContext; NTSTATUS LastStatusValue; UNICODE_STRING StaticUnicodeString; WCHAR StaticUnicodeBuffer[0x105]; PVOID DeallocationStack; PVOID TlsSlots[0x40]; LIST_ENTRY TlsLinks; PVOID Vdm; PVOID ReservedForNtRpc; PVOID DbgSsReserved[0x2]; ULONG HardErrorDisabled; PVOID Instrumentation[14]; PVOID SubProcessTag; PVOID EtwTraceData; PVOID WinSockData; ULONG GdiBatchCount; BOOLEAN InDbgPrint; BOOLEAN FreeStackOnTermination; BOOLEAN HasFiberData; UCHAR IdealProcessor; ULONG GuaranteedStackBytes; PVOID ReservedForPerf; PVOID ReservedForOle; ULONG WaitingOnLoaderLock; ULONG SparePointer1; ULONG SoftPatchPtr1; ULONG SoftPatchPtr2; PVOID *TlsExpansionSlots; ULONG ImpersionationLocale; ULONG IsImpersonating; PVOID NlsCache; PVOID pShimData; ULONG HeapVirualAffinity; PVOID CurrentTransactionHandle; PTEB_ACTIVE_FRAME ActiveFrame; PVOID FlsData; UCHAR SafeThunkCall; UCHAR BooleanSpare[3]; } TEB, *PTEB;
|
PEB 进程环境块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| typedef struct _PEB { UCHAR InheritedAddressSpace; UCHAR ReadImageFileExecOptions; UCHAR BeingDebugged; UCHAR Spare; PVOID Mutant; PVOID ImageBaseAddress; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID SubSystemData; PVOID ProcessHeap; PVOID FastPebLock; PPEBLOCKROUTINE FastPebLockRoutine; PPEBLOCKROUTINE FastPebUnlockRoutine; ULONG EnvironmentUpdateCount; PVOID* KernelCallbackTable; PVOID EventLogSection; PVOID EventLog; PPEB_FREE_BLOCK FreeList; ULONG TlsExpansionCounter; PVOID TlsBitmap; ULONG TlsBitmapBits[0x2]; PVOID ReadOnlySharedMemoryBase; PVOID ReadOnlySharedMemoryHeap; PVOID* ReadOnlyStaticServerData; PVOID AnsiCodePageData; PVOID OemCodePageData; PVOID UnicodeCaseTableData; ULONG NumberOfProcessors; ULONG NtGlobalFlag; UCHAR Spare2[0x4]; LARGE_INTEGER CriticalSectionTimeout; ULONG HeapSegmentReserve; ULONG HeapSegmentCommit; ULONG HeapDeCommitTotalFreeThreshold; ULONG HeapDeCommitFreeBlockThreshold; ULONG NumberOfHeaps; ULONG MaximumNumberOfHeaps; PVOID** ProcessHeaps; PVOID GdiSharedHandleTable; PVOID ProcessStarterHelper; PVOID GdiDCAttributeList; PVOID LoaderLock; ULONG OSMajorVersion; ULONG OSMinorVersion; ULONG OSBuildNumber; ULONG OSPlatformId; ULONG ImageSubSystem; ULONG ImageSubSystemMajorVersion; ULONG ImageSubSystemMinorVersion; ULONG GdiHandleBuffer[0x22]; PVOID ProcessWindowStation; } PEB, *PPEB;
|
TLS 线程局部存储
线程局部存储用来将数据与一个正在执行的指定线程关联起来。
它主要是为了避免多个线程同时访存同一全局变量或者静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。为了解决这个问题,我们可以通过TLS机制,为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。而从全局变量的角度上来看,就好像一个全局变量被克隆成了多份副本,而每一份副本都可以被一个线程独立地改变。
WinAPI 反调试
在头文件 windows.h
中存在函数
IsDebuggerPresent()
当函数返回 true
代表检测到调试器,false
则代表未检测到调试器
相同功能的函数还有 CheckRemoteDebuggerPresent()
同时还可以通过
FindWindow
、EnumWindows
或是GetForegroundWindow
粗暴查询主流调试器窗口名
又或者通过
CreateToolhelp32Snapshot
、Process32First
和Process32Next
查询所有的进程来排查主流调试器进程是否存在
甚至可以通过判断父进程是否为explorer.exe
来检查是否被调试,因为一般来说双击点开的程序的父进程都是explorer.exe
利用 PEB 标志位
IsDebuggerPresent()
实际上也是取 PEB
结构体中的 BeingDebugged
标志位
当调试器开始调试应用程序的时候,操作系统会将目标应用程序的
PEB
结构体中的 BeingDebugged
标志位置为 1
而 FS[]
寄存器指向 TEB
结构体
1 2 3
| mov eax, fs:[30h] ; 取 PEB 首地址 mov eax, [eax+2] ; 取 PEB 首地址偏移 2 的位置 and eax, 0xff ; 只需要低 2 位的 PEB->BeingDebugged
|
或者 (NtGlobalFlag
默认为 0 )
1 2
| mov eax, fs:[30h] ; 取 PEB 首地址 mov eax, [eax+68h] ; 取 PEB 首地址偏移 68h 的位置,即 PEB->NtGlobalFlag
|
在 C++ 中可写为 (仅为示例)
1 2 3 4 5 6 7 8 9 10 11
| bool isDebugged() { unsigned int flag = false; __asm { mov eax, fs:[30h] mov eax, [eax+2] and eax, 0xff mov flag, eax } return flag; }
|
利用堆标志位
不同系统、不同版本可能不太一样,具体请查询微软官方文档。
在Windows
XP系统中,ForceFlags
属性位于堆头部偏移量0x10处;在Windows
7系统中,对于32位的应用程序来说ForceFlags
属性位于堆头部偏移量
0x44 处。
利用 PEB->ProcessHeap->Flags
1 2 3
| mov eax, fs:[30h] ; 取 PEB 首地址 mov eax, [eax + 18h] ; 取 PEB 首地址偏移 18h 的位置,即 PEB->ProcessHeap mov eax, [eax + 0ch] ; 取 PEB->ProcessHeap 首地址偏移 0ch 的位置,即 PEB->ProcessHeap->Flags (默认为 2)
|
利用 PEB->ProcessHeap->ForceFlags
1 2 3
| mov eax, fs:[30h] ; 取 PEB 首地址 mov eax, [eax + 18h] ; 取 PEB 首地址偏移 18h 的位置,即 PEB->ProcessHeap mov eax, [eax + 10h] ; 取 PEB->ProcessHeap 首地址偏移 0ch 的位置,即 PEB->ProcessHeap->ForceFlags
|
使用 ntdll 函数
无论是 kernel32.dll
还是
user32.dll
,始终会通过 ntdll.dll
中的函数与驱动层取得联系
其中 ntdll.dll
存在一个未导出函数
NtQueryInformationProcess()
可供查询调试端口
定义如下:
1
| extern "C" DWORD NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG length, PULONG returnLen);
|
当我们需要使用时,以下 MSVC 代码可供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| extern "C" DWORD NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG length, PULONG returnLen); #pragma comment(lib, "ntdll.lib")
bool isDebugged() { HANDLE hDebugPort = NULL; if (NtQueryInformationProcess((HANDLE)-1, 7, &hDebugPort, sizeof(HANDLE), NULL)) { return hDebugPort; } return false; }
|
CheckRemoteDebuggerPresent()
实际上内部也调用了此函数
时间检测
利用时间差判断是否代码停留时间过长,可判断是否在被动态调试,适合放在关键代码处运行,从某种意义上可以反制一定的调试手段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| bool isDebugged() { DWORD start, end; __asm { rdtsc mov start, eax rdtsc mov end, eax } if (end - start > 21) { return true; } return false; }
|
或者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void keyFunction() { DWORD start, end; start = GetTickCount();
end = GetTickCount(); if (end - start > 100) { } else {
} }
|
硬件断点检测
获取线程上下文,检测硬件断点,查看是否占用
以下 MSVC 代码可供参考:
1 2 3 4 5 6 7 8 9 10 11 12
| bool isDebugged() { CONTEXT ctx; ctx.ContextFlags = CONTEXT_ALL; GetThreadContext((HANDLE)-1, &ctx);
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) { return true; } return false; }
|
异常检测
但当调试器捕获异常时,大多数调试器并不会立即将异常传递给被调试程序处理或是直接不传递。我们便可以将这种机制用来反调试。
但以下代码不一定运行成功——时代在发展,科技在进步。
一个字节的 Interrupt 3 中断
在所有会被调试器处理的异常中,interrupt 3
中断算是一个,它会生成一个单字节的断点。
1 2 3 4 5 6 7 8 9 10 11 12
| bool isDebugged_Int3() { __try { __asm int 3 } __except (1) { return false; } return true; }
|
两个字节的 Interrupt 3 中断
使用 MSVC 内联汇编器的 _emit
伪指令可以生成一个两字节的interrupt 3指令。在测试的所有调试器中,只有
OnllyDbg 调试器识别这个异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| bool isDebugged_Int3_2Bytes() { __try { __asm { __emit 0xCD __emit 0x03 } } __except (1) { return false; } return true; }
|
Interrupt 0x2D 中断
如果执行了interrupt 0x2D,Windows将抛出一个断点异常
1 2 3 4 5 6 7 8 9 10 11 12
| bool isDebugged_Int2d() { __try { __asm int 0x2d } __except (1) { return false; } return true; }
|
GetLastError 检测
当出现异常时,winapi 往往会通过 GetLastError()
函数获取错误原因,而我们也可以通过 SetLastError()
来报错
其中 OutputDebugString()
函数用于在调试器中显示字符串,同时可以用来探测调试器是否存在。
我们可以通过将错误码重置,当使用 OutputDebugString()
函数时,若不存在调试器,则会发生错误,错误码被重新设置;若存在调试器,则之后的错误码则会跟我们重置的错误码相同,以此便可以判断调试器存在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| bool isDebugged() { DWORD dwCode = 10086; SetLastError(dwCode); OutputDebugString(L"hacker!"); if (GetLastError() == dwCode) { return true; } else { return false; } }
|
利用 TLS 回调
以下 MSVC 代码可供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| void NTAPI tls_callback(PVOID h, DWORD reason, PVOID pv) { if (IsDebuggerPresent()) { MessageBoxA(NULL, "hacker!", 0, 0); } }
#ifdef _M_IX86 #pragma comment(linker, "/INCLUDE:__tls_used") #pragma comment(linker, "/INCLUDE:__tls_callback") #else #pragma comment(linker, "/INCLUDE:_tls_used") #pragma comment(linker, "/INCLUDE:_tls_callback") #endif
EXTERN_C #ifdef _M_X64 #pragma const_seg (".CRT$XLB") #else #pragma data_seg (".CRT$XLB") #endif PIMAGE_TLS_CALLBACK _tls_callback[] = { tls_callback, 0 };
EXTERN_C #ifdef _M_X64 #pragma const_seg (".CRT$XLB") #else #pragma data_seg (".CRT$XLB") #endif
|
我们可以在 TLS 回调函数中,进行我们的反调试手段,如加壳、加密等
内存校验
将内存值进行 hash 得到哈希值 (如 CRC
算法等),当内存区段数据被调试,或者是被补丁后,内存值经过同一哈希函数得到的哈希值会发生改变,可以此来判断是否被调试。
注册表查询检测
以下为调试器在注册表中的一个常用位置。 32位系统:
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
64位系统:
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug
该注册表项指定当应用程序发生错误时,触发哪一个调试器。默认情况下,它被设置为Dr.Watson
。如果该这册表的键值被修改为OllyDbg
或是其他调试器名,则代码就可以确定它正在被调试。
对数据加密
在程序开始前,不将正确的数据显露出来
当用以上技术并未检测到程序正在调试时,再将相应的数据进行解密
可结合 TLS 回调函数等技术
以下代码仅供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| char secret[256] = { 0x54, 0x65, 0x6a, 0x7f, 0x65, 0x4f, 0x21, 0x63, 0x4f, 0x78, 0x24, 0x7e, 0x74, 0x63, 0x20, 0x7d, 0x75 };
void init() { if (IsDebuggerPresent()) { memcpy(secret, "hacker!", 8); } else { for (size_t i = 0; i < 17; ++i) { secret[i] ^= 0x10; } } }
int main() { init(); puts(secret);
return 0; }
|
自修改代码
将想要加密的 .text
代码区段进行加密
注意,需要使用 PE Editor
等软件将软件的
.text
代码区段改为可写
当程序运行时再进行解密,可以在一定程度上反动态调试
可结合 TLS 回调函数等技术
以下代码仅供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| void functionA() { ; }
void functionB() { ; }
void init() { if (IsDebuggerPresent()) {
} else { LPBYTE start = (LPBYTE)functionA; LPBYTE end = (LPBYTE)functionB; while (start != end) { *start ^= 0x66; } } }
int main() { init(); functionA(); functionB();
return 0; }
|
攻击调试器漏洞
与所有软件一样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试,会攻击这些漏洞。