反调试技术基础

虽然现在的保护主要是注册码机制,但反调试仍然是很有趣的值得学习的技术。从最简单的WIN32 API反调试到ring0的各种HOOK,Anti和Anti-Anti技术互相促进,很有意思。

BeingDebugged标志

IsDebuggerPresent

IsDebuggerPresent函数是Win32 API,用于检测当前是否被调试。它的实现很简单,读取当前进程PEB,然后返回BeingDebugged标志。


lkd> dt _peb
nt!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar


在ring3获取PEB地址有很多种方法,这里使用线程环境块TEB。


lkd> dt _teb -r1
nt!_TEB
   +0x000 NtTib            : _NT_TIB
      +0x000 ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD
      +0x004 StackBase        : Ptr32 Void
      +0x008 StackLimit       : Ptr32 Void
      +0x00c SubSystemTib     : Ptr32 Void
      +0x010 FiberData        : Ptr32 Void
      +0x010 Version          : Uint4B
      +0x014 ArbitraryUserPointer : Ptr32 Void
      +0x018 Self             : Ptr32 _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
      +0x000 UniqueProcess    : Ptr32 Void
      +0x004 UniqueThread     : Ptr32 Void
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB


FS:0总是指向当前线程TEB,而TEB的30h偏倚处是PEB,所以自己实现的IsDebuggerPresent可以是这样:


__asm
{
    mov eax, fs:[0x30]
    movzx eax, byte ptr [eax+2]
}


要获取任意进程的PEB,可以使用GetThreadContext和GetThreadSelectorEntry函数获得FS指向的地址,然后使用上面相似的步骤检测其他进程是否被调试。

可以看到这是很孱弱的反调试技术,很容易就可以绕过,所以我们需要发掘更深层次的被调试程序与正常运行程序的不同之处。

NtGlobalFlag

创 建进程时会调用LdrpInitialize函数,该函数会对进程的PEB结构中的NtGlobalFlag进行设置,所以这里也可以检测是否被调试。如 果PEB的BeingDebugged被设置,LdrpInitialize会将NTGlobalFlag的 FLG_HEAP_ENABLE_FREE_CHECK等和堆相关的位也会被设置。这也是调试堆和直接运行堆有所不同的原因。

如果HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options中创建名为进程,值为空的子健,就可以绕过针对NtGlobalFlag进行的反调试。

由于这些标志会对堆产生比较大的影响,所以下面看一下通过堆来反调试。

Heap Image

调试堆会有HEAP_TAIL_CHECKING_ENABLED和HEAP_FREE_CHECKING_ENABLED标志,从而在堆头等地方添加特殊字符,如BABA等,所以可以通过堆是否有特殊标志来检测是否被调试。


lkd> dt _peb
nt!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 SpareBool        : UChar
   +0x004 Mutant           : Ptr32 Void
   +0x008 ImageBaseAddress : Ptr32 Void
   +0x00c Ldr              : Ptr32 _PEB_LDR_DATA
   +0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS
   +0x014 SubSystemData    : Ptr32 Void
   +0x018 ProcessHeap      : Ptr32 Void
lkd> dt _heap
nt!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : Uint4B
   +0x00c Flags            : Uint4B
   +0x010 ForceFlags       : Uint4B


PEB的0x18偏倚处是ProcessHeap,而Heap结构的的Flags成员在被调试时不等于2,ForceFlags被调试时不等于0,所以又有了可以检测的方法。不过这个同样可以用添加注册表的方法绕过。

Native检测

CheckRemoteDebuggerPresent是另一个可以检测是否被调试的API。它并不是检测PEB的BeingDebugger标志,而是调用ZwQueryInformationProcess查看ProcessDebugPort信息。所以简单的修改标志并不能绕过检测,可以通过ring0 HOOK等方式修改返回值达到Anti-Anti的目的。

另一个可以防止被调试的函数是ZwSetInformationThread,直接调用它并传入ThreadHideFromDebugger参数可以禁止线程产生调试事件,彻底根绝了被调试。

调试对象

调试器挂接进程之后,会创建一个调试器对象DebugObject,所以可以通过ZwQueryObject查询是否有调试类型对象,禁止任何调试器。

大杂烩函数ZwQuerySystemInformation也可以用来反调试。SystemKernelDebuggerInformation可以查找是否存在系统调试器,如是否被Windbg调试。

最好的Anti就是自己加载dll,然后查找函数,防止被别人hook。比如自己构造对战,调用ThreadHideFromDebugger,防止被调试。

其他方法

反调试方法有很多种,针对程序本身或者利用调试原理,也有利用调试器漏洞,该怎么防或者绕过,就看自己思路了。

比如被调试时程序有SeDebugPrivilege权限,所以可以通过尝试打开csrss.exe判断是否被调试;也可以通过SetUnhandledExceptionFilter设置异常处理,然后触发异常,如果被调试,那么异常会被调试器接收,这时可能会错过一些程序运行必须的步骤,从而达到反调试的目的;还可以查看父进程ID,看是否是某个诡异进程;也可以查看某条指令运行的时间差或者查看EFLAGS的TF标志是否被设置;针对调试器设计不足的,比如在DR0~DR3中放置程序运行需要的数据,防止下硬件断点等。

当然还有很多技巧。

附上几行代码,涉及到本文提到的一些反调试技术。


#include <windows.h> 
#include <stdio.h> 

#pragma comment(lib, "msvcrt.lib")

typedef DWORD (WINAPI *ZW_SET_INFORMATION_THREAD)(HANDLE, DWORD, PVOID, ULONG);
#define ThreadHideFromDebugger 17

#define SystemKernelDebuggerInformation 35

#pragma pack(4)

typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION 
{ 
	BOOLEAN DebuggerEnabled; 
	BOOLEAN DebuggerNotPresent; 
} SYSTEM_KERNEL_DEBUGGER_INFORMATION, *PSYSTEM_KERNEL_DEBUGGER_INFORMATION;

typedef DWORD (WINAPI *ZW_QUERY_SYSTEM_INFORMATION)(DWORD, PVOID, ULONG, PULONG);


VOID DisableDebugEvent(VOID)
{
	HINSTANCE hModule;
	ZW_SET_INFORMATION_THREAD ZwSetInformationThread;

	hModule = GetModuleHandleA("ntdll.dll");
	ZwSetInformationThread = 
		(ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule,                       "ZwSetInformationThread");
	ZwSetInformationThread(GetCurrentThread(),         ThreadHideFromDebugger, NULL, NULL);
}

BOOL CheckKernelDbgr()
{
	HINSTANCE hModule = GetModuleHandleA("ntdll.dll");
	ZW_QUERY_SYSTEM_INFORMATION ZwQuerySystemInformation = 
		(ZW_QUERY_SYSTEM_INFORMATION)GetProcAddress(hModule,               "ZwQuerySystemInformation");
	SYSTEM_KERNEL_DEBUGGER_INFORMATION Info = {0};

	ZwQuerySystemInformation(
		SystemKernelDebuggerInformation,
		&Info, 
		sizeof(Info),
		NULL);
	return (Info.DebuggerEnabled && !Info.DebuggerNotPresent);
}

void A()
{
	printf("IsDebuggerPresent:\t%d\n", IsDebuggerPresent());
}

void B()
{
	int Ans;
	__asm
	{
		mov eax, fs:[0x30]
		movzx eax, byte ptr [eax+2]
		mov Ans, eax
	}
	printf("MyDebuggerPresent:\t%d\n", Ans);
}

void C()
{
	int Ans;
	__asm
	{
		mov eax, fs:[0x30]
		mov eax, [eax+0x68]
		mov Ans, eax
	}
	printf("NtGlobalForDebug:\t%d\n", (bool)Ans);
}


void E()
{
	int Ans;
	__asm
	{
		mov eax, fs:[0x30]
		mov eax, [eax + 0x18]
		mov ebx, 1
		cmp dword ptr [eax+0x0C], 2
		jne end
		cmp dword ptr [eax+0x10], 0
		jne end
		mov ebx, 0
end:
		mov Ans, ebx
	}
	printf("HeapFlagAndForceFlag:\t%d\n", Ans);
}

void F()
{
	BOOL ans;
	CheckRemoteDebuggerPresent(GetCurrentProcess(), &ans);
	printf("RemoteDebuggerPresent:\t%d\n", ans);
}

void G()
{
	HANDLE k = NULL;
	k = OpenProcess(PROCESS_QUERY_INFORMATION, 0, 620);
	printf("ByDoOpenProcess:\t%d\n", k);
}

int main()
{
	A();
	B();
	C();
	E();
	F();
	G();
	system("pause");
	return 0;
}