LegendSaber 发表于 2021-10-6 17:33

用户层反调试技术总结

本帖最后由 LegendSaber 于 2021-10-20 17:56 编辑

一.通过PEB来实现反调试
         PEB结构体中包含了进程的许多信息,其中有不少信息可以用来帮助我们判断程序是否运行在调试环境下。因为FS段寄存器所指的便是TEB结构体,而TEB中偏移0x30的地方便是PEB结构体的地址,所以我们可以使用FS:来获取PEB结构体的地址。
   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
   +0x01c FastPebLock      : Ptr32 _RTL_CRITICAL_SECTION
   +0x020 FastPebLockRoutine : Ptr32 Void
   +0x024 FastPebUnlockRoutine : Ptr32 Void
   +0x028 EnvironmentUpdateCount : Uint4B
   +0x02c KernelCallbackTable : Ptr32 Void
   +0x030 SystemReserved   : Uint4B
   +0x034 AtlThunkSListPtr32 : Uint4B
   +0x038 FreeList         : Ptr32 _PEB_FREE_BLOCK
   +0x03c TlsExpansionCounter : Uint4B
   +0x040 TlsBitmap      : Ptr32 Void
   +0x044 TlsBitmapBits    : Uint4B
   +0x04c ReadOnlySharedMemoryBase : Ptr32 Void
   +0x050 ReadOnlySharedMemoryHeap : Ptr32 Void
   +0x054 ReadOnlyStaticServerData : Ptr32 Ptr32 Void
   +0x058 AnsiCodePageData : Ptr32 Void
   +0x05c OemCodePageData: Ptr32 Void
   +0x060 UnicodeCaseTableData : Ptr32 Void
   +0x064 NumberOfProcessors : Uint4B
   +0x068 NtGlobalFlag   : Uint4B
   +0x070 CriticalSectionTimeout : _LARGE_INTEGER
   +0x078 HeapSegmentReserve : Uint4B
   +0x07c HeapSegmentCommit : Uint4B
   +0x080 HeapDeCommitTotalFreeThreshold : Uint4B
   +0x084 HeapDeCommitFreeBlockThreshold : Uint4B
   +0x088 NumberOfHeaps    : Uint4B
   +0x08c MaximumNumberOfHeaps : Uint4B
   +0x090 ProcessHeaps   : Ptr32 Ptr32 Void
   +0x094 GdiSharedHandleTable : Ptr32 Void
   +0x098 ProcessStarterHelper : Ptr32 Void
   +0x09c GdiDCAttributeList : Uint4B
   +0x0a0 LoaderLock       : Ptr32 Void
   +0x0a4 OSMajorVersion   : Uint4B
   +0x0a8 OSMinorVersion   : Uint4B
   +0x0ac OSBuildNumber    : Uint2B
   +0x0ae OSCSDVersion   : Uint2B
   +0x0b0 OSPlatformId   : Uint4B
   +0x0b4 ImageSubsystem   : Uint4B
   +0x0b8 ImageSubsystemMajorVersion : Uint4B
   +0x0bc ImageSubsystemMinorVersion : Uint4B
   +0x0c0 ImageProcessAffinityMask : Uint4B
   +0x0c4 GdiHandleBuffer: Uint4B
   +0x14c PostProcessInitRoutine : Ptr32   void
   +0x150 TlsExpansionBitmap : Ptr32 Void
   +0x154 TlsExpansionBitmapBits : Uint4B
   +0x1d4 SessionId      : Uint4B
   +0x1d8 AppCompatFlags   : _ULARGE_INTEGER
   +0x1e0 AppCompatFlagsUser : _ULARGE_INTEGER
   +0x1e8 pShimData      : Ptr32 Void
   +0x1ec AppCompatInfo    : Ptr32 Void
   +0x1f0 CSDVersion       : _UNICODE_STRING
   +0x1f8 ActivationContextData : Ptr32 Void
   +0x1fc ProcessAssemblyStorageMap : Ptr32 Void
   +0x200 SystemDefaultActivationContextData : Ptr32 Void
   +0x204 SystemAssemblyStorageMap : Ptr32 Void
   +0x208 MinimumStackCommit : Uint4B
      首先就是PEB中偏移0x2的BeingDebugged字段,当我们的程序在运行在调试器中的时候,这个字段将为1,否则为0,所以可以通过检查这个字段的值来判断是否运行在调试环境下。

      __asm
      {
                push eax
                push ecx

                xor eax, eax
                xor ecx, ecx
                mov eax, DWORD PTR fs:
                movzx ecx, BYTE PTR ds:
                mov isDebugger, ecx

                pop ecx
                pop eax
      }

       事实上被用来检测调试器的函数IsDebuggerPresent就是通过查询BeingDebugged字段来实现的。以下就是是他函数的具体实现
         
      其次是位于0x18的ProcessHeap指针,它指向了一个HEAP结构体,HEAP结构体中一部分的成员如下
      
      当程序在非调试状态运行的时候0xC的Flags的值就是0x2,0x10的ForceFlags字段的值就是0x0。否则就是在调试状态下运行,所以可以用如下代码来检测

      __asm
      {
                push eax
                push ecx

                xor ecx, ecx
                xor eax, eax
                mov eax, fs:
                mov eax, ds:
               
                mov ecx, ds:
                cmp ecx, 0x2
                je NotDebugger

                mov ecx, ds:
                cmp ecx, 0x0
                je NotDebugger

                mov ecx, 1
                mov isDebugger, ecx

      NotDebugger:
                pop ecx
                pop eax
      }

二.通过NtQueryInformationProcess实现反调试      
NtQueryInformationProcess函数可以被用来查询与进程调试相关的信息,该函数在文档中的定义如下

NTSTATUS
WINAPI
NtQueryInformationProcess(__in HANDLE ProcessHandle,
                        __in PROCESSINFOCLASS ProcessInformationClass,
                        __out PVOID ProcessInformation,
                        __in ULONG ProcessInformationLength,
                        __out_opt PULONG ReturnLength);
参数ProcessHandle是我们要查询的进程的句柄。
参数ProcessInformationClass是一个枚举类型,通过传入特定的值,系统就会把相关的信息保存在ProcessInformation中。
参数ProcessInformation就是我们用来保存查询结果的缓冲区。
参数ProcessInformationLength是我们缓冲区的长度。
参数ReturnLength是调用函数后最终返回的数据长度。      
其中PROCESSINFORCLASS的定义如下

typedef enum _PROCESSINFOCLASS
{
      ProcessBasicInformation, // 0x0
      ProcessQuotaLimits,
      ProcessIoCounters,
      ProcessVmCounters,
      ProcessTimes,
      ProcessBasePriority,
      ProcessRaisePriority,
      ProcessDebugPort, // 0x7
      ProcessExceptionPort,
      ProcessAccessToken,
      ProcessLdtInformation,
      ProcessLdtSize,
      ProcessDefaultHardErrorMode,
      ProcessIoPortHandlers,
      ProcessPooledUsageAndLimits,
      ProcessWorkingSetWatch,
      ProcessUserModeIOPL,
      ProcessEnableAlignmentFaultFixup,
      ProcessPriorityClass,
      ProcessWx86Information,
      ProcessHandleCount,
      ProcessAffinityMask,
      ProcessPriorityBoost,
      ProcessDeviceMap,
      ProcessSessionInformation,
      ProcessForegroundInformation,
      ProcessWow64Information, // 0x1A
      ProcessImageFileName, // 0x1B
      ProcessLUIDDeviceMapsEnabled,
      ProcessBreakOnTermination,
      ProcessDebugObjectHandle, // 0x1E
      ProcessDebugFlags // 0x1F
} PROCESSINFOCLASS;

       如果进程处于调试状态,那么系统就会进程分配一个调试端口。这个时候,我们调用这个函数,第二个参数传入的是ProcessDebugPort(0x7)的时候,那么第三个参数就会返回调试端口。如果进程处于非调试状态,那么第三个参数返回0,否则就会返回0xFFFFFFFF(-1)。所以可以用如下代码检测调试器

BOOL IsDebugger()
{
    BOOL isDebugger = FALSE;
    DWORD dwDebugPort = 0;
    HMODULE hDll = NULL;
    pNtQueryInformationProcess NtQueryInformationProcess = NULL;

    hDll = LoadLibrary("ntdll.dll");

    if (hDll)
    {
                NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(hDll, "NtQueryInformationProcess");
                if (NtQueryInformationProcess)
                {
                        NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &dwDebugPort, sizeof(dwDebugPort), NULL);
                        if (dwDebugPort != 0) isDebugger = TRUE;
                }
                FreeLibrary(hDll);
    }
      
    return isDebugger;
}
          事实上,CheckRemoteDebuggerPresent函数内部的实现就是通过这个方式来检测,如下是它的实现代码      
             
   可以看到参数的第二个参数入栈的值就是0x7。   
   当调试器调试一个进程的时候,就会生成调试对象。这个时候,第二个参数传入ProcessDebugObjectHandle(0x1E),那么第三个参数就会得到调试对象的句柄。如果这个时候处在调试状态,那么句柄就会存在,否则就是NULL。所以可以用如下代码来检测调试器

BOOL IsDebugger()
{
    BOOL isDebugger = FALSE;
    HANDLE hDebugObj = NULL;
    HMODULE hDll = NULL;
    pNtQueryInformationProcess NtQueryInformationProcess = NULL;

    hDll = LoadLibrary("ntdll.dll");

    if (hDll)
    {
      NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(hDll, "NtQueryInformationProcess");
      if (NtQueryInformationProcess)
      {
                        NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugObjectHandle, &hDebugObj, sizeof(hDebugObj), NULL);
                        if (hDebugObj != NULL) isDebugger = TRUE;
      }
      FreeLibrary(hDll);
    }

    return isDebugger;
}

       当第二个参数传入ProcessDebugFlags(0x1F)的时候,第三个参数就会返回调试标志的值。如果系统处于调试状态,这个值就是0,否则就是1。所以可以用如下代码检测调试器

BOOL IsDebugger()
{
    BOOL isDebugger = FALSE;
    HMODULE hDll = NULL;
    pNtQueryInformationProcess NtQueryInformationProcess = NULL;

    hDll = LoadLibrary("ntdll.dll");

    if (hDll)
    {
      NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(hDll, "NtQueryInformationProcess");
      if (NtQueryInformationProcess)
      {
                        NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugFlags, &isDebugger, sizeof(isDebugger), NULL);
                        isDebugger = !isDebugger;
      }
      FreeLibrary(hDll);
    }

    return isDebugger;
}

三.通过NtQuerySystemInformation实现反调试      
NtQuerySystemInformation是一个可以用来获取多种系统信息的函数。它在文档中的定义如下

NTSTATUS
WINAPI
NtQuerySystemInformation(__in SYSTEM_INFORMATION_CLASS SystemInformationClass,
                         __inout PVOID SystemInformation,
                         __in ULONG SystemInformationLength,
                         __out_opt PULONG ReturnLength);
参数SystemInformationClass是一个枚举类型,用来指定我们要获取的系统的信息类型。
参数SystemInformation是返回值的缓冲区,这个返回值返回的内容是根据第一个参数指定的类型来决定的。
参数SystemInformationLength是返回值缓冲区的长度。
参数ReturnLength是实际返回到缓冲区的长度。      
      其中SysInformationClass的定义如下   

typedef enum _SYSTEM_INFORMATION_CLASS {
      SystemBasicInformation = 0,
      SystemPerformanceInformation = 2,
      SystemTimeOfDayInformation = 3,
      SystemProcessInformation = 5,
      SystemProcessorPerformanceInformation = 8,
      SystemInterruptInformation = 23,
      SystemExceptionInformation = 33,
      SystemKernelDebuggerInformation = 35,
      SystemRegistryQuotaInformation = 37,
      SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

当我们第二个参数传入的是SystemKernelDebuggerInformation(0x23)的时候,第三个参数会返回SYSTEM_KERNEL_DEBUGGER_INFORMATION类型的数据。该结构体定义如下

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

   如果程序处在非调试进程,那么返回值中的这两个成员就会为FALSE,否则就处在调试状态。所以可以用如下代码检测调试器

BOOL IsDebugger()
{
      BOOL isDebugger = FALSE;
      HMODULE hDll = NULL;
      pNtQuerySystemInformation NtQuerySystemInformation = NULL;
      SYSTEM_KERNEL_DEBUGGER_INFORMATION debugInfo = { 0, 0 };
      DWORD ulRetLen = 0;

      hDll = LoadLibrary("ntdll.dll");

      if (hDll)
      {
                        NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(hDll, "NtQuerySystemInformation");
                        if (NtQuerySystemInformation)
                        {
                              NtQuerySystemInformation(SystemKernelDebuggerInformation, &debugInfo, sizeof(debugInfo), &ulRetLen);
                              if (debugInfo.DebuggerEnabled || debugInfo.DebuggerNotPresent) isDebugger = TRUE;
                        }
                        FreeLibrary(hDll);
      }

      return isDebugger;
}

四.通过OutputDebugString来实现反调试      
   OutputDebugString函数是用来在调试器中显示一个字符串。如果程序运行在调试状态,那么这个函数就会成功调用,否则这个函数会调用失败并且重新设置错误码。所以我们可以在调用这个函数前设置一个错误码,在调用完这个函数之后在判断错误码是否被改变来判断程序是否运行在调试状态下,相应代码如下

BOOL IsDebugger()
{
      BOOL isDebugger = FALSE;
      DWORD dwErrValue = 1900;

      SetLastError(dwErrValue);
      OutputDebugString("1900");
      if (GetLastError() == dwErrValue) isDebugger = TRUE;

      return isDebugger;
}

五.通过ZwSetInformationThread来实现反调试      
   ZwSetInformationThread是一个用来设置线程信息的API。该函数文档中的定义如下

NTSTATUS
ZwSetInformationThread(IN HANDLE ThreadHandle,
                     IN THREADINFOCLASS ThreadInformationClass,
                     IN PVOID ThreadInformation,
                     IN ULONG ThreadInformationLength);
参数ThreadHandle用来接受要设置的线程的句柄。
参数ThreadInformationClass是一个枚举类型,定义如下。当我们将它设置为ThreadHideFromDebugger(0x11)的时候,调试进程就会被分离出来。

typedef enum _THREADINFOCLASS {
      ThreadBasicInformation,
      ThreadTimes,
      ThreadPriority,
      ThreadBasePriority,
      ThreadAffinityMask,
      ThreadImpersonationToken,
      ThreadDescriptorTableEntry,
      ThreadEnableAlignmentFaultFixup,
      ThreadEventPair,
      ThreadQuerySetWin32StartAddress,
      ThreadZeroTlsCell,
      ThreadPerformanceCount,
      ThreadAmILastThread,
      ThreadIdealProcessor,
      ThreadPriorityBoost,
      ThreadSetTlsArrayAddress,
      ThreadIsIoPending,
      ThreadHideFromDebugger
}THREADINFOCLASS;

      所以可以用以下的代码实现反调试

VOID IsDebugger()
{
      HMODULE hDll = LoadLibrary("ntdll.dll");
      pZwSetInformationThread ZwSetInformationThread = NULL;

      if (hDll)
      {
                ZwSetInformationThread = (pZwSetInformationThread)GetProcAddress(hDll, "ZwSetInformationThread");

                if (ZwSetInformationThread) ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
      }
}

六.通过时钟来检测来实现反调试   
      因为调试的时候,程序运行的时间会比正常运行时间久,所以我们可以根据运行时间的长短来判断是否运行在调试环境中。      
   rdtsc指令可以返回系统重启以来的时钟数,并且会将它作为一个64位的值放到EDX:EAX中。那我们就可以运行两次rdstc指令,比较两次读取的数值之间的差距来判断是否运行在调试环境下,代码如下

BOOL IsDebugger()
{
      BOOL IsDebugger = FALSE;

      __asm
      {
                push eax
                push ecx

                xor eax, eax
                xor ecx, ecx

                rdtsc
                mov ecx, eax
                rdtsc
                sub eax, ecx
                cmp eax, 0xFFF
                jb NotDebugger
                mov ecx, 1
                mov IsDebugger,ecx
      NotDebugger:
                pop ecx
                pop eax
      }
}

      GetTickCount函数可以返回最近系统重启时间与当前时间相差的毫秒数。同样可以通过两次调用这个函数来计算差值判断是否处于调试环境,代码如下

BOOL IsDebugger()
{
      BOOL bIsDebugger = FALSE;
      DWORD dwFirst = 0, dwSecond = 0;

      dwFirst = GetTickCount();

      dwSecond = GetTickCount();

      if (dwSecond - dwFirst > 0x1A) bIsDebugger = TRUE;

      return bIsDebugger;
}


七.通过int 3来实现反调试      
      当程序运行在调试器中触发int 3异常的时候,调试器会接管异常,否则程序会转向注册的SEH异常程序执行。所以我们可以在程序中注册一个SEH然后触发int 3异常,根据不同的情况实现功能来实现反调试,过程如图            
         
      所以我们可以根据以下代码来实现反调试

      __asm
      {
                push offset NotDebugger
                push dword ptr fs :
                mov dword ptr fs : , esp                //注册SEH异常

                int 0x3                         //触发中断
                push 0xFFFFFFFF               //调试环境下会继续执行,让程序停止
                ret
      NotDebugger:                        //正常情况下会运行到注册的处理函数这里
                mov eax, dword ptr ss : //第三个参数就是CONTEXT
                mov ebx, offset quit
                mov dword ptr ds:, ebx //CONTEXT+0xB8就是EIP的地址,设置为我们要返回的地址
                xor eax, eax                                        //返回值设为0
                ret
      quit:
                pop dword ptr fs :       //卸载SEH
                add esp, 4
      }

      还有其他的方法,比如我们可以使用FindWindows查看窗口是否是调试器;检测代码段是否有0xCC就是int 3断点;计算整个代码段中的CRC,判断程序是否被更改等等来实现反调试,都比较简单,这里不再诉述。       

aonima 发表于 2021-10-6 19:59

收藏了!!!

少年持剑 发表于 2021-10-6 21:12

通过时间来反调试,如果系统或者软件卡到的话,会不会误报。

LegendSaber 发表于 2021-10-6 21:22

少年持剑 发表于 2021-10-6 21:12
通过时间来反调试,如果系统或者软件卡到的话,会不会误报。

哈哈哈哈哈。少侠考虑周全啊。。。会的吧。。但是问题也不大。。?你重启以下程序就好了。

Qq76761043 发表于 2021-10-7 12:24

感谢楼主慷慨分享~

luckysky 发表于 2021-10-7 22:58

其他的都可以在ring3干掉,rdtsc也要上驱动硬刚

LegendSaber 发表于 2021-10-8 08:54

luckysky 发表于 2021-10-7 22:58
其他的都可以在ring3干掉,rdtsc也要上驱动硬刚

花时间的话当然都可以过掉

chenjingyes 发表于 2021-10-8 17:04

:lol还是vt级别的难搞

国际豆哥 发表于 2021-10-8 23:04

搜藏了搜藏了感谢楼主

54mj 发表于 2021-10-9 06:49

谢谢分享,下断好办法
页: [1] 2
查看完整版本: 用户层反调试技术总结