WIndows下的反调试技术

前引

  反调试是一门艺术,我们选择不同的识别思路,也就产生了不同的方法。

   一个进程无论是被调试器启动的,还是被调试器接管的,它的执行环境都会发生变化,也就是PEB的某些字段会跟着发生变化。

一、基于BeingDebugged标志的反调试

1.通过BOOL IsDebuggerPresent函数

返回值 true/false,本质是读取PEB中的BeingDebugged标志。

绕过方法:修改返回值/nop掉部分指令

引:

 

                               PEB结构(进程控制块)

 

                            TIB/TEB(线程信息块)

        PEB的地址通常存在线程环境块TEB中。Windows在调入进程或线程时,操作系统会为每一个线程分配TEB,而FS:[0]存储的是TEB的,所以我们通过FS:[0x30]来访问PEB。

x86下使用FS寄存器:

 

 

 x64下使用GS寄存器:

2.通过BOOL CheckRemoteDebuggerPresent函数

根据bDebuggerPresent值判断 :true/false,本质是通过NtQueryInformationProcess函数查询是否使用调试端口。

绕过方法:修改bDebuggerPresent/nop掉部分指令

3.通过NTSTATUS ZwQueryInformationProcess/NtQueryInformationProcess函数

当第二个参数为ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)时与反调试有关。第三个参数根据第二个参数的值返回对应查询的内容。

int debugPort = 0;  
HMODULE hModule = LoadLibrary("Ntdll.dll");  
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");  
NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL); 

也可以结合SetUnhandleExceptionFilter实现反调试,安全性更高。

 ProcessInformationClass 参数的取值及其对应的十六进制值

 

 

实现方式:

通过设置SetUnhandledExceptionFilter(*pfunc)使程序将捕获的异常交给设置的函数处理。如果程序没有收到异常,则退出程序。如果收到异常,则先调用ZwQueryInformationProcess查看是否调试,调试(返回-1)则退出程序。

异常时系统的处理顺序:

  1. 系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统 
        挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息。
  2. 如果程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否设置了相关的异常处理例程,如果 
        设置了相关的异常处理例程,系统就把异常发送给程序seh处理例程,交由其处理。
  3. 每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,
            可交由链起来的其他例程处理。
  4. 如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知调试器。
  5. 如果程序未处于被调试状态或者调试器没有能够处理,并且你调用SetUnhandledExceptionFilter设置了最后异 
        常处理例程UnhandleExceptionFilter的话,系统转向对它的调用。
  6. 如果你没有设置最后异常处理例程UnhandleExceptionFilter或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框,
        用户可以选择关闭或者最后将其附加到调试器上的调试按钮。如果没有调试器能被附加于其上或者调试器也处理不了,系统 
        就调用ExitProcess终结程序。
  7. 不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会。

 二、基于NtGlobalFlag的识别

  在 32 位机器上, NtGlobalFlag字段位于PEB的0x68偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0。 当调试器正在运行时,该字段会被设置为一个特定的值.。

  由调试器创建的进程会设置以下标志位:

FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

所以有:

Windows x32的:

 Windows x64的:

PS:更多可以参考https://ctf-wiki.org/reverse/windows/anti-debug/ntglobalflag/

 

三、基于PEB中ProcessHeap标志位的检查

32 位环境下ProcessHeap标志位位于PEB 中的偏移量 0x18处,可以通过以下方式检查

MOV EAX, DWORD PTR FS:[30]
MOV EAX, DWORD PTR DS:[EAX+18]

对于 64 位环境下ProcessHeap标志位位于PEB 中的偏移量 0x30处,可以通过以下方式检查

MOV RAX,QWORD PTR GS:[60]
MOV RAX,QWORD PTR DS:[RAX+30]

该标志常见值有:

HEAP_GROWABLE (2)
HEAP_TAIL_CHECKING_ENABLED (0x20)//处于被调试状态
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

所以当ProcessHeap & 0x20等于0x20时,进程处于调试状态。

 

四、基于硬件的检查

1.基于时间检查的反调试

  原理:程序被调试时运行的速度要远比正常运行慢,存在时间差,当时间差超出一定界限时可以认定程序被调试。

  我们可以设定一个检查区域,在检查区域故意触发一个异常,在边界上比对时间差。

法一:基于系统时间比较

使用QueryPerformanceCounter或GetTickCount获取计数器。

 

法二:基于时间戳计数器

RDTSC这条汇编指令,用来将时间戳计数读入EDX:EAX(高位32位保存到EDX寄存器,低32位保存到EAX寄存器)。

通过比较前后时间戳计数差距范围得出判断。

RDTSC
...
RDTSC
...
cmp
jx
...

2.基于陷阱标志的反调试

类1:通过将陷阱标志位TF置1导致触发单步执行异常(触发后会置0),而我们事先设置好的异常处理函数会修改eip跳到正确的代码处,调试时则不会。其实与SetUnhandledExceptionFilter的反调一个原理。

类2:int 0x2d 内核模式下触发中断的指令,也可以在用户模式下触发。但是,程序处于被调试状态时不会触发,只是忽略。

         特性:

  1. 执行后,下一条指令的第一个字节被忽略,不当作指令的部分进行解析,起到混淆的作用。
  2. 执行后后面的执行部分无法通过F7或F8进行单步执行指令,会像按了F9一样执行到底,除非下面设置了F2中断。(破解方法:在执行int 0x2d时将TF标志位置1。这样第一个字节也不会被忽略,还可以继续F7、F8。)

四、基于DbgBreakPoint / DbgUserBreakPoint或注入

先说检测调试思路,我们可以通过hook DbgBreakPoint函数,在其中加入提示代码来检测程序是否有调试进程对进程进行调试。

反调试思路:进程主动破坏掉这两个函数,比如写入垃圾字节覆盖掉原指令。在进程初始化结束后将代码段权限修改为不可写,我们先保存各页面的权限信息,然后启动一个线程定时检测页面权限,若某页突然获得写入以及执行权限,则检测出该进程已被注入或调试。

五、其他反调试技术

1.双进程保护(Debug Blocker技术)

通过父进程创建子进程并调试实现反调试。

破解方法:修改父进程代码,使子进程可以独立运行。

2. 0xCC探测

原理:调试器通过将指令的第一个字节修改为0xcc实现软件中断,也就是会导致与原程序代码不同。

法一:在执行的过程中检查程序中是否有int 3(0xcc)指令。

法二:在执行过程中计算某一部分代码的校验值,进行比对。

3. 检测父进程

explorer.exe是Windows程序管理器或者文件资源管理器,一般双击运行的进程的父进程都是它,但是如果进程被调试父进程则是调试器进程。

#include <Windows.h>
#include <Winternl.h>
#include <TlHelp32.h>

typedef NTSTATUS (WINAPI *NTQUERYINFORMATIONPROCESS)(HANDLE ProcessHandle,PROCESSINFOCLASS ProcessInformationClass,PVOID ProcessInformation,ULONG ProcessInformationLength,PULONG ReturnLength);


BOOL checkDebug()
{
    int pid = GetCurrentProcessId();

    PROCESS_BASIC_INFORMATION info;
    HMODULE hNtDll = GetModuleHandle("ntdll.dll");
    NTQUERYINFORMATIONPROCESS NtQueryInformationProcess = (NTQUERYINFORMATIONPROCESS)GetProcAddress(hNtDll,"NtQueryInformationProcess");
    NtQueryInformationProcess(GetCurrentProcess(),ProcessBasicInformation,&info,sizeof(PROCESS_BASIC_INFORMATION),NULL);
    //查看进程列表(也就是任务管理器)
    HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
   CloseHandle(hNtDll);
if(hProcessSnap == INVALID_HANDLE_VALUE) { return FALSE; } PROCESSENTRY32 pe32; BOOL bFlag = Process32First(hProcessSnap, &pe32); while(bFlag) { if((DWORD)info.Reserved3 == pe32.th32ProcessID)//父进程id { if(strcmp(pe32.szExeFile,"explorer.exe")==0) { CloseHandle(hProcessSnap); return FALSE; } else { CloseHandle(hProcessSnap); return TRUE; } } bFlag = Process32Next(hProcessSnap, &pe32); } CloseHandle(hProcessSnap); return FALSE; } int main() { if(checkDebug()) return 1; system("pause"); return 0; }

 注:由于NtQueryInformationProcess函数是一个未公开的函数,因此需要使用LoadLibrary和GetProceAddress的方法获取调用地址。

4. TLS回调中反调试

TLS函数会在函数调用代码入口点前中执行,其代码在.tls段中。我们可以将反调试的代码定义在TLS回调函数中。

5. 基于错误码的检测

通过调用SetLastError函数设置错误码,调用OutputDebugStringA函数向调试器发送调试信息。当调试器存在时,错误码不变;当调试器不存在时,会引发错误,错误码被重新设置,即改变。

      SetLastError(0xDEAD);
      OutputDebugStringA(OutputString);
      LastError = GetLastError();
      isDebug = false;
      if ( LastError != 0xDEAD )
          isDebug = true;

6. 主动抛出异常

向CloseHandle函数传入无效handle,有调试器的情况下会抛出异常,继续调试运行程序会崩溃。

CloseHandle(0x232);//乱写一个handle值

 

7. 取消调试器附加

通过调用NtSetInformationThread函数,并将第二个参数指定为0x11(代表ThreadHideFromDebugger),使程序从调试器中脱离出去从而起到反调试的效果。如果正常运行是不会受该函数影响的。

typedef enum _THREADINFOCLASS {
    ThreadBasicInformation          = 0,
    ThreadTimes                     = 1,
    ThreadPriority                  = 2,
    ThreadBasePriority              = 3,
    ThreadAffinityMask              = 4,
    ThreadImpersonationToken        = 5,
    ThreadDescriptorTableEntry      = 6,
    ThreadEnableAlignmentFaultFixup = 7,
    ThreadEventPair_Reusable        = 8,
    ThreadQuerySetWin32StartAddress = 9,
    ThreadZeroTlsCell               = 10,
    ThreadPerformanceCount          = 11,
    ThreadAmILastThread             = 12,
    ThreadIdealProcessor            = 13,
    ThreadPriorityBoost             = 14,
    ThreadSetTlsArrayAddress        = 15,   // Obsolete
    ThreadIsIoPending               = 16,
    ThreadHideFromDebugger          = 17,
    ThreadBreakOnTermination        = 18,
    ThreadSwitchLegacyState         = 19,
    ThreadIsTerminated              = 20,
    ThreadLastSystemCall            = 21,
    ThreadIoPriority                = 22,
    ThreadCycleTime                 = 23,
    ThreadPagePriority              = 24,
    ThreadActualBasePriority        = 25,
    ThreadTebInformation            = 26,
    ThreadCSwitchMon                = 27,   // Obsolete
    ThreadCSwitchPmu                = 28,
    ThreadWow64Context              = 29,
    ThreadGroupInformation          = 30,
    ThreadUmsInformation            = 31,   // UMS
    ThreadCounterProfiling          = 32,
    ThreadIdealProcessorEx          = 33,
    ThreadCpuAccountingInformation  = 34,
    ThreadSuspendCount              = 35,
    ThreadActualGroupAffinity       = 41,
    ThreadDynamicCodePolicyInfo     = 42,
    ThreadSubsystemInformation      = 45,
 
    MaxThreadInfoClass              = 51,
} THREADINFOCLASS;

  示例代码如下:

#include <Windows.h>
#include <Winternl.h>

typedef NTSTATUS (WINAPI *NTSETINFORMATIONTHREAD)(HANDLE ThreadHandle,THREADINFOCLASS ThreadInformationClass,PVOID ThreadInformation,ULONG ThreadInformationLength);


int main()  
{  
    HMODULE hNtDll = GetModuleHandle("ntdll.dll");
    NTSETINFORMATIONTHREAD NtSetInformationThread = (NTSETINFORMATIONTHREAD)GetProcAddress(hNtDll,"NtSetInformationThread");
    NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11, 0, 0);
    system("pause");
    return 0;  
} 
posted @ 2020-05-07 12:03  An2i  阅读(774)  评论(0)    收藏  举报