动态反调试技术

530 阅读5分钟

一.查看结构体数据

PEB是在三环中存储了进程的信息,每一个进程都对应了一个PEB结构体,我们可以通过查看PEB结构体的成员来获得调试信息。

对于PEB结构体的位置,我们可以通过TEB获得,它的地址位于TEB的0x30偏移处

nt!_TEB

+0x030 ProcessEnvironmentBlock

而对于TEB,我们可以通过fs段寄存器来定位,fs段寄存器存储了选择子,在GDT表中查找到相应的段描述符,其中的base基址就是TEB的地址,fs:[0]也就是[fs.base+0],三环中,fs:[0]一直指向了当前正在运行的线程的基址

1. BeingDebugged

       nt!_PEB

           +0x002 BeingDebugged    : UChar

通过BeingDebugged成员,我们可以获得目标进程是否处于调试状态

	_asm
	{
		mov eax, fs: [0x30]
		mov al, BYTE ptr [eax+2]
		mov result, al
	}

由于这种方法很简单,一般的调试器都会将BeingDebugged清除

2. NtGlobalFlag

       nt!_PEB

           +0x068 NtGlobalFlag 

NtGlobalFlag位于PEB的0x68偏移处,它会根据BeingDebugged的值来设置,当BeingDebugged位true时,NtGlobalFlag的值会被设置为0x70

        //NtGlobalFlags
	_asm
   {
   	mov eax,fs:[0x30]
   	mov eax ,[eax+0x68]
   	and eax,0x70
   	cmp eax,0x70
   }
   

想要绕过检查只需要手动修改相应位置的值

3. HeapFlags

HeapFlags包含两个根据NtGlobalFlag进行初始化的标志,Flags和ForceFlags,在windows xp中,Flags位于堆的0xC偏移处,而ForceFlags位于堆的0x10处

    nt!_HEAP

       +0x00c Flags            : Uint4B
   
       +0x010 ForceFlags       : Uint4B

正常情况下,Flags的值为2,ForceFlags的值为0

堆的位置可以通过PEB来获得

    nt!_PEB

        +0x018 ProcessHeap
	//Flags
       _asm
   {
   	mov eax, fs: [0x30]  //eax->PEB
   	mov eax, [eax + 0x18]  //eax->heap
   	mov eax, [eax + 0xc]  //eax = Flags
   	cmp eax,0x2
   }
	//ForceFlags
   _asm
   {
   	mov eax, fs: [0x30] //eax->PEB
   	mov eax, [eax + 0x18] //eax->heap
   	mov eax, [eax + 0x10] //eax = ForceFlags
   	cmp eax, 0
   }

二.使用Windwos API

1. IsDebuggerPresent

     BOOL IsDebuggerPresent();
 

这个函数的实现,本质上是查看PEB.BeingDebugged

2. CheckRemoteDebuggerPresent

    BOOL CheckRemoteDebuggerPresent( [in] HANDLE hProcess, [in, out] PBOOL pbDebuggerPresent );

        typedef BOOL(WINAPI* CHECK_REMOTE_DEBUGGER_PRESENT)(
	HANDLE hProcess, 
	PBOOL pbDebuggerPresent);
        
        

	HMODULE hMod;//模块句柄
	HANDLE hProcess;//进程句柄
	CHECK_REMOTE_DEBUGGER_PRESENT CheckRemoteDebuggerPresent = nullptr;
	BOOL bDebuggerPresent;//返回值

	hMod = LoadLibrary(L"kernel32.dll");//加载kernel32.dll
	CheckRemoteDebuggerPresent = (CHECK_REMOTE_DEBUGGER_PRESENT)GetProcAddress(hMod, "CheckRemoteDebuggerPresent");//获得导出函数地址

	hProcess = GetCurrentProcess();

	CheckRemoteDebuggerPresent(hProcess, &bDebuggerPresent);//调用函数
  

3. NtQueryInformationProcess

    _ NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, //目标进程句柄

                                      [in] PROCESSINFOCLASS ProcessInformationClass,//查看信息的类型
                                             
                                      [out] PVOID ProcessInformation,  // 接收传出的数据
                                             
                                      [in] ULONG ProcessInformationLength,//查找数据的长度
                                             
                                      [out, optional] PULONG ReturnLength//(可选)接收的数据的长度 );

NtQueryInformationProcess()可以用来获得进程的信息,它从零环的EPROCESS结构体中取出数据,然后传回到三环中,我们无法在应用程序中直接获得零环的数据,所以需要使用windows提供的API来实现零环和三环的通信

我们会用到的的DebugPort也是在存储在EPROCESS中,DebugPort是进程用来和调试器通信的,当进程处于调试状态时,DebugPort会被设置为非零的值,因此可以获得进程的DebugPort来检测调试器

     nt!_EPROCESS

         +0x0ec DebugPort        : Ptr32 Void
    typedef DWORD(WINAPI* NT_QUERY_INFORMATION_PROCESS)(
   HANDLE hProcess,
   DWORD ProcessInformationClass,
   PVOID ProcessInformation,
   ULONG ProcessInformationLength,
   PULONG ReturnLength
   );
       
       
       
       HMODULE hMod = nullptr;
   HANDLE hProcess = nullptr;
   NT_QUERY_INFORMATION_PROCESS NtQueryInformationProcess = nullptr;
   DWORD dwProcessInformation = 0;

   hMod = GetModuleHandle(L"ntdll.dll");
   NtQueryInformationProcess = (NT_QUERY_INFORMATION_PROCESS)GetProcAddress(hMod, "NtQueryInformationProcess");
   hProcess = GetCurrentProcess();

   NtQueryInformationProcess(hProcess,
                                 7, //7为宏,这里代表了DebugPort(0x7)
                                 &dwProcessInformation, 
                                 4, 
                                 nullptr);

   if (dwProcessInformation != 0)
   	printf("Find Debug!\n");

4. NtSetInformationThread

    NTSYSCALLAPI NTSTATUS NtSetInformationThread(
        [in] HANDLE          ThreadHandle,//目标线程的句柄
        [in] THREADINFOCLASS ThreadInformationClass,//查看数据的类型,同样也是宏
        [in] PVOID           ThreadInformation,//要传递的数据
        [in] ULONG           ThreadInformationLength//要传递数据的长度
    );

NtSetInformationThread()可以设置线程的信息,线程和进程一样,在零环中也有一个结构体ETHEAD存储了自身的信息,但我们在应用程序中无法直接得到和修改,所以也要利用Windwos提供的API

    nt!_ETHREAD

        +0x280 HideFromDebugger

这里我们会用到的宏是ThreadHideFormDebug(17),它可以将ETHERAD的HideFormDebugger设置为TRUE,它会禁止对应的线程产生调试事件

调用ZwSetInformationThread()前后对比

image.png

image.png

调试器与进程无法通信,这与直接将DebugPort置零类似

三.其他技巧

1. 查找调试器进程

我们可以便利进程链表,去查看是否有调试器的存在,这种方法很容易跳过,只需要修改进程名称或者删除进程链表中的对应项即可

    	HANDLE hProcessSnap = nullptr;//进程快照句柄
	PROCESSENTRY32 pe32 = { 0 };//进程结构体
	pe32.dwSize = sizeof(PROCESSENTRY32);//初始化

	hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);//从第一个进程开始遍历

	if (!Process32First(hProcessSnap, &pe32))//将第一个进程的信息写入pe32
		return false;

	do//继续向下遍历
	{
		if (!_wcsicmp(pe32.szExeFile, L"x64dbg.exe")|| !_wcsicmp(pe32.szExeFile, L"x32dbg.exe")|| !_wcsicmp(pe32.szExeFile, L"ollydbg.exe"))
		{
			printf("Find Debug!\n");
			break;
		}
	}while (Process32Next(hProcessSnap, &pe32));

2. 时间差

一般来说,处于调试状态的进程运行的时间远长于正常状态的进程,因此我们可以获得指令前后的时钟周期数,如果前后两次时钟计数的差超过正常的范围,那么就可以确定存在调试器

- rdtsc指令

该指令可以获得CPU自开机运行到目前的时钟计数,值为64位,存于edx:eax中

	DWORD time1, time2;
	_asm
	{
		rdtsc
		mov time1 ,eax
		rdtsc
		mov time2,eax
	}

	if (time2 - time1 > 0xff)
		printf("Find Debug!\n");

- _rdtsc()

Windows提供了_rdtsc(),这个函数本质上就是rdtsc指令

	time1 = __rdtsc();
	time2 = __rdtsc();

	if (time2 - time1 > 0xff)
		printf("Find Debug\n");

- GetTickCout()

	time1 = GetTickCount();
	time2 = GetTickCount();

	if (time2 - time1 > 0xff)
		printf("Find Debug\n");

3. 父进程检测

我们所使用的进程一般都是由explorer.exe调用CreatProcess()创建的,但处于调试状态的进程却是由调试器创建的,我们可以通过查看目标进程的父进程是否是explorer.exe,即两者的PID是否相同,来实现判断

	HANDLE hProcessSnap = nullptr;
	HANDLE hProcess = nullptr;
	PROCESSENTRY32 pe32 = { 0 };
	pe32.dwSize = sizeof(PROCESSENTRY32);//初始化
	DWORD dwPID;
	DWORD dwParentID;

	dwPID = GetCurrentProcessId();//获得当前进程ID

	hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, dwPID);//创建进程快照
	if (!Process32First(hProcessSnap, &pe32))//获得当前进程的信息
		return false;
	dwParentID = pe32.th32ParentProcessID;//获得当前进程的父进程ID


	hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (!hProcessSnap)
		return false;

	if (!Process32First(hProcessSnap, &pe32))//获得第一个进程的信息
		return false;

	do//遍历进程,查找explorer.exe
	{
		if (!_wcsicmp(pe32.szExeFile, L"explorer.exe"))
			if (dwParentID != pe32.th32ParentProcessID)//判断ID是否相同
				printf("Find Debug!\n");
                                break;
	}while (Process32Next(hProcessSnap, &pe32));

后续掌握了其他的方法会继续添加的