逆向工程核心原理——反调试技术

反调试技术的分类

反调试技术多种多样,大体上可以分成静态与动态两组反调试。调试运用了静态技术的程序文件时只要在开始破解一次就可以解除全部反调试限制,但运用了动态技术的程序则需要一边调试一边破解。

静态反调试技术

主要用来探测调试器,如果探测到,则使程序无法正常运行。所以一般在调试器中打开应用了静态反调试技术的文件时,文件将无法正常运行。

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   : [1] Uint4B
 +0x034 AtlThunkSListPtr32 : Uint4B
 +0x038 FreeList         : Ptr32 _PEB_FREE_BLOCK
 +0x03c TlsExpansionCounter : Uint4B
 +0x040 TlsBitmap        : Ptr32 Void
 +0x044 TlsBitmapBits    : [2] 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  : [34] Uint4B
 +0x14c PostProcessInitRoutine : Ptr32     void 
 +0x150 TlsExpansionBitmap : Ptr32 Void
 +0x154 TlsExpansionBitmapBits : [32] 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

这个是Windows XP SP3中的PEB结构体成员,与反调试技术密切相关的成员有以下几个:

+0x002  BeingDebugged    : UChar
+0x00c  Ldr              : Ptr32 _PEB_LDR_DATA
+0x018	ProcessHeap      : Ptr32 Void
+0x068 	NtGlobalFlag     : Uint4B

BeingDebugged成员是一个标志,用来表示进程是否处于被调试状态。

Ldr、ProcessHeap、NtGlobalFlag成员与被调试进程的堆内存特性相关。接下来分别讲解以上4个PEB成员

BeingDebugged(+0x2)

当进程处于调试状态时,PEB.BeingDebugged成员(+0x2)的值会被设置为1,进程在非调试状态下运行时,其值被设置为0

IsDebuggerPresent()

IsDebuggerPresent()API获取PEB.BeingDebugged的值来判断进程是否处于被调试状态。

IsDebuggerPresentd的代码非常简单,先找到PEB结构,然后访问PEB.BeingDebugged成员(+0x2)。

破解之法

在调试的时候,借助OD调试器的编辑功能,将PEB.BeingDebugged的值修改成0即可

Ldr(+0xC)

在调试进程中,其堆内存中就会出现一些特殊标识,表示它正处于被调试状态,其中最醒目的就是,未使用的堆内存区域全部填充着0xEEFEEEFE,这证明正在调试进程。利用这一特征可以判断进程是否处于调试状态。

PEB.Ldr成员是一个指向_PEB_LDR_DATA结构体的指针,而 _PEB_LDR_DATA结构体刚好是在堆内存区域中创建的,所以扫描该区域就可以轻松查找到是否存在0xEEFEEEFE区域,如果存在就是调试,不存在就不在调试状态。

破解之法

只要将填充0xEEFEEEFE值的区域全部覆盖成NULL就行了

提示

这个方法仅适用于WindowsXP系统,而在Windows Vista以后的系统中就无法使用了。另外,利用附加功能将运行中的进程附加到调试器上,堆内存不会出现上述情况。

ProcessHeap(+0x18)

在PEB的偏移0x18是指向HEAP结构体的指针

+0x000 Entry 
+0x008 Signature
+0x00c Flags
+0x010 ForceFlags
+0x014 VirtualMemoryThreshold
+0x018 SegmentReserve
+0x01c SegmentCommit
+0x020 DeCommitFreeBlockThreshold

这是HEAP结构体的部分成员,进程处于被调试状态时,Flags与ForceFlags会被设置为特定值

PEB.ProcessHeap()可以从PEB结构体中直接获取HEAP结构体(偏移0x18),也可以通过GetProcessHeap()API获取。

进程HEAP结构体的地址为PEB.ProcessHeap=190000

Flags(+0xC)&Force Flags(+0x10)

进程正常运行时,Flags为0x2,而ForceFlags值为0x0,但是进程处于被调试状态时,这些值就会发生改变

所以,比较这些值就可以判断进程是否处于被调试状态

破解之法

只要将Flags的值重新设置为0x2,ForceFlags的值重新设置成0x0就行了。

提示

这个方法只能在Windows XP系统下有效,Windows7系统会保留Flags和ForceFlags属性。此外,如果运行中的进程附加到调试器中,也不会出现上述特征。

NtGlobalFlag(+0x68)

调试进程时,PEB.NtGlobalFlag成员(+0x68)的值会被设置成0x70,所以,检测该成员的值就可以判断进程是否处于被调试状态

NtGlobalFlag的0x70是下列Flags值进行bit OR(位或)运算的结果

FLG_HEAP_ENABLE_TAIL_CHECK 		(0x10)
FLG_HEAP_ENABLE_FREE_CHECK 		(0x20)
FLG_HEAP_VALIDATE_PARAMETERS	(0x40)
破解之法

将PEB.NtGlobalFlag值改为0即可

NtQueryInformationProcess()

利用这个API可以探测调试器,获取各种与进程调试相关的信息。

__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);

我们给NtQueryInformationProcess的第二个二参数ProcessInformationClass ProcessInformationClass指定特定值并调用该函数,相关的信息就会被设置到第三个参数ProcessInformation中

ProcessBasicInformation             = 0x00,
   ProcessQuotaLimits              = 0x01,
   ProcessIoCounters               = 0x02,
   ProcessVmCounters               = 0x03,
   ProcessTimes                = 0x04,
   ProcessBasePriority             = 0x05,
   ProcessRaisePriority            = 0x06,
   ProcessDebugPort                = 0x07,
   ProcessExceptionPort            = 0x08,
   ProcessAccessToken              = 0x09,
   ProcessLdtInformation               = 0x0A,
   ProcessLdtSize                  = 0x0B,
   ProcessDefaultHardErrorMode         = 0x0C,
   ProcessIoPortHandlers               = 0x0D,
   ProcessPooledUsageAndLimits         = 0x0E,
   ProcessWorkingSetWatch              = 0x0F,
   ProcessUserModeIOPL             = 0x10,
   ProcessEnableAlignmentFaultFixup        = 0x11,
   ProcessPriorityClass            = 0x12,
   ProcessWx86Information              = 0x13,
   ProcessHandleCount              = 0x14,
   ProcessAffinityMask             = 0x15,
   ProcessPriorityBoost            = 0x16,
   ProcessDeviceMap                = 0x17,
   ProcessSessionInformation           = 0x18,
   ProcessForegroundInformation        = 0x19,
   ProcessWow64Information             = 0x1A,
   ProcessImageFileName            = 0x1B,
   ProcessLUIDDeviceMapsEnabled        = 0x1C,
   ProcessBreakOnTermination           = 0x1D,
   ProcessDebugObjectHandle            = 0x1E,
   ProcessDebugFlags               = 0x1F,
   ProcessHandleTracing            = 0x20,
   ProcessIoPriority               = 0x21,
   ProcessExecuteFlags             = 0x22,
   ProcessResourceManagement           = 0x23,
   ProcessCookie                   = 0x24,
   ProcessImageInformation             = 0x25,
   ProcessCycleTime                = 0x26,
   ProcessPagePriority             = 0x27,
   ProcessInstrumentationCallback          = 0x28,
   ProcessThreadStackAllocation        = 0x29,
   ProcessWorkingSetWatchEx            = 0x2A,
   ProcessImageFileNameWin32           = 0x2B,
   ProcessImageFileMapping             = 0x2C,
   ProcessAffinityUpdateMode           = 0x2D,
   ProcessMemoryAllocationMode         = 0x2E,
   ProcessGroupInformation             = 0x2F,
   ProcessTokenVirtualizationEnabled       = 0x30,
   ProcessConsoleHostProcess           = 0x31,
   ProcessWindowInformation            = 0x32,
   ProcessHandleInformation            = 0x33,
   ProcessMitigationPolicy             = 0x34,
   ProcessDynamicFunctionTableInformation      = 0x35,
   ProcessHandleCheckingMode           = 0x36,
   ProcessKeepAliveCount               = 0x37,
   ProcessRevokeFileHandles            = 0x38,
   ProcessWorkingSetControl            = 0x39,
   ProcessHandleTable              = 0x3A,
   ProcessCheckStackExtentsMode        = 0x3B,
   ProcessCommandLineInformation           = 0x3C,
   ProcessProtectionInformation        = 0x3D,
   ProcessMemoryExhaustion             = 0x3E,
   ProcessFaultInformation             = 0x3F,
   ProcessTelemetryIdInformation           = 0x40,
   ProcessCommitReleaseInformation         = 0x41,
   ProcessDefaultCpuSetsInformation        = 0x42,
   ProcessAllowedCpuSetsInformation        = 0x43,
   ProcessSubsystemProcess             = 0x44,
   ProcessJobMemoryInformation         = 0x45,
   ProcessInPrivate                = 0x46,
   ProcessRaiseUMExceptionOnInvalidHandleClose = 0x47,
   ProcessIumChallengeResponse         = 0x48,
   ProcessChildProcessInformation          = 0x49,
   ProcessHighGraphicsPriorityInformation      = 0x4A,
   ProcessSubsystemInformation         = 0x4B,
   ProcessEnergyValues             = 0x4C,
   ProcessActivityThrottleState        = 0x4D,
   ProcessActivityThrottlePolicy           = 0x4E,
   ProcessWin32kSyscallFilterInformation       = 0x4F,
   ProcessDisableSystemAllowedCpuSets      = 0x50,
   ProcessWakeInformation              = 0x51,
   ProcessEnergyTrackingState          = 0x52,
   ProcessManageWritesToExecutableMemory       = 0x53,
   ProcessCaptureTrustletLiveDump          = 0x54,
   ProcessTelemetryCoverage            = 0x55,
   ProcessEnclaveInformation           = 0x56,
   ProcessEnableReadWriteVmLogging         = 0x57,
   ProcessUptimeInformation            = 0x58,
   ProcessImageSection             = 0x59,
   ProcessDebugAuthInformation         = 0x5A,
   ProcessSystemResourceManagement         = 0x5B,
   ProcessSequenceNumber               = 0x5C,
   ProcessLoaderDetour             = 0x5D,
   ProcessSecurityDomainInformation        = 0x5E,
   ProcessCombineSecurityDomainsInformation    = 0x5F,
   ProcessEnableLogging            = 0x60,
   ProcessLeapSecondInformation        = 0x61,
   ProcessFiberShadowStackAllocation       = 0x62,
   ProcessFreeFiberShadowStackAllocation       = 0x63,
   MaxProcessInfoClass             = 0x64

以上与调试器探测有关的成员是ProcessDebugPort(0x7),ProcessDebugObjectHandle(0x1E),ProcessDebugFlags(0x1F)。

ProcessDebugPort(0x7)

进程处于调试状态时,系统会为它分配一个调试端口。ProcessInformation的参数值设置为ProcessDebugPort,调用NtQueryInformationProcess函数就能获取调试端口。如果进程处于非调试状态,则变量dwDebugPort的值设置成0,如果进程处于调试状态,则变量dwDebugPort的值设置成0xFFFFFFFF

DWORD dwDebugPort=0;
pNtQueryInformationProcess(GetCurrentProcess(),ProcessDebugPort,&dwDebugPort,sizeof(dwDebugPort),NULL);
printf("NtQueryInformationProcess(ProcessDebugPort)=0x%X\n",dwDebugPort);
if(dwDebugPort!=0x0) printf("==>Debugging!!\n\n");
else printf("==>Not Debugging\n\n")

ProcessDebugObjectHandle(0x1E)

调试进程时会生成调试对象(Debug Object)。函数的第二个参数值为ProcessDebugObjectHandle时,调用函数后通过第三个参数就能获取调试对象句柄。进程处于调试状态时,调试对象句柄的值就存在;若进程处于非调试状态,则调试对象句柄值为NULL。

HANDLE hDebugObject=NULL;
pNtQueryInformationProcess(GetCurrentProcess(),ProcessDebugObjectHandle,&hDebugObject,sizeof(hDebugObject),NULL);
printf("NtQueryInformationProcess(ProcessDebugObjectHandle)=0x%X\n",hDebugObject);
if(hDebugObject!=0x0) printf("==>Debugging!!\n\n");
else printf("==>Not Debugging\n\n")

ProcessDebugFlags(0x1F)

检测DebugFlags的值也可以判断进程是否处于被调试状态。函数的第二个参数设置为ProcessDebugFlags,调用函数后通过第三个参数即可获取调试标志的值,如果为0,则进程处于被调试状态;若为1,则进程处于非调试状态。

BOOL bDebugFlags=TRUE;
pNtQueryInformationProcess(GetCurrentProcess(),ProcessDebugFlags,&bDebugFlags,sizeof(bDebugFlags),NULL);
printf("NtQueryInformationProcess(ProcessDebugFlags)=0x%X\n",bDebugFlags);
if(bDebugFlags==0x0) printf("==>Debugging!!\n\n");
else printf("==>Not Debugging\n\n")

NtQuerySystemInformation()

介绍一种基于调试环境检测的反调试技术,运用这种反调试技术可以检测当前OS是否在调试模式下运行。

NtQuerySystemInformation是一个系统函数,可以用来获取当前运行的多种OS信息。

__kernel_entry NTSTATUS NtQuerySystemInformation(
  [in]            SYSTEM_INFORMATION_CLASS SystemInformationClass,
  [in, out]       PVOID                    SystemInformation,
  [in]            ULONG                    SystemInformationLength,
  [out, optional] PULONG                   ReturnLength
);

SYSTEM_INFORMATION_CLASS SystemInformationClass是一个枚举类型,传入需要的系统信息类型,将某结构体的地址传给SystemInformation,API被调用后,该结构体中就会填充相关的信息

SYSTEM_INFORMATION_CLASS的枚举类型

typedef enum _SYSTEM_INFORMATION_CALSS{
	SystemBasicInformation=0,
	SystemPerformanceInformation=2,
	SystemProcessInformation=5,
	SystemProcessorPerformanceInformation=8,
	SystemInterruptInformation=23,
	SystemExceptionInformation=33,
	SystemKernelDebuggerInformation=35,
	SystemRegistryQuotaInformation=37,
	SystemLookasideInformation=45
}SYSTEM_INFORMATION_CALSS

SystemKernelDebuggerInformation(0x23)

void MyNtQuerySystemInformation()
{
    typedef NTSTATUS (WINAPI *NTQUERYSYSTEMINFORMATION)(
        ULONG SystemInformationClass,
        PVOID SystemInformation,
        ULONG SystemInformationLength,
        PULONG ReturnLength
    );//函数没有导出,需要我们自己导出

    typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION 
    {
        BOOLEAN DebuggerEnabled;
        BOOLEAN DebuggerNotPresent;
    } SYSTEM_KERNEL_DEBUGGER_INFORMATION, *PSYSTEM_KERNEL_DEBUGGER_INFORMATION;//第二个参数结构体

    NTQUERYSYSTEMINFORMATION NtQuerySystemInformation;//函数指针
  
    NtQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)  
                                GetProcAddress(GetModuleHandle(L"ntdll"), 
                                               "NtQuerySystemInformation");//获取NtQuerySystemInformation的地址

    ULONG SystemKernelDebuggerInformation = 0x23;
    ULONG ulReturnedLength = 0;
    SYSTEM_KERNEL_DEBUGGER_INFORMATION DebuggerInfo = {0,};

    NtQuerySystemInformation(SystemKernelDebuggerInformation, //调用NtQuerySystemInformation,参数是SystemKernelDebuggerInformation
                             (PVOID) &DebuggerInfo, 		  //和DebuggerInfo
                             sizeof(DebuggerInfo),      // 2 bytes
                             &ulReturnedLength);

    printf("NtQuerySystemInformation(SystemKernelDebuggerInformation) = 0x%X 0x%X\n", 
           DebuggerInfo.DebuggerEnabled, DebuggerInfo.DebuggerNotPresent);
    if( DebuggerInfo.DebuggerEnabled )  printf("  => Debugging!!!\n\n");//根据DebuggerEnabled判断是否存在调试
    else                                printf("  => Not debugging...\n\n");
}

在调用NtQuerySystemInformation时,第一个参数是SystemKernelDebuggerInformation,第二个参数是和DebuggerInfo,当函数调用结束后,如果系统处于调试模式下,则DebuggerInfo中的成员DebuggerEnabled将会被设置成为1(DebuggerNotPresent恒为1)。

NtQueryObject()

系统中的某个调试器调试进程时,会创建一个调试对象类型的内核对象,检测该对象是否存在就可以判断是否有进程正在被调试。

NtQueryObject是用来获取系统各种内核对象的信息

__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject(
  [in, optional]  HANDLE                   Handle,
  [in]            OBJECT_INFORMATION_CLASS ObjectInformationClass,
  [out, optional] PVOID                    ObjectInformation,
  [in]            ULONG                    ObjectInformationLength,
  [out, optional] PULONG                   ReturnLength
);

调用NtQueryObject先向OBJECT_INFORMATION_CLASS ObjectInformationClass赋值,然后调用后查询到的内核对象信息就会保存在ObjectInformation中

OBJECT_INFORMATION_CLASS 和之前一样是一个枚举类型

typedef enum _OBJECT_INFORMATION_CLASS{
	ObjectBasicInformation,
	ObjectNameInformation,
	ObjectTypeInformation,
	ObjectAllTypesInformation,
	ObjectHandleInformation
}OBJECT_INFORMATION_CLASS,*POBJECT_INFORMATION_CLASS

首先使用ObjectAllTypesInformation值获取系统所有对象信息,然后从中检测是否存在调试对象。

使用方法

1、获取内核对象信息链表大小
ULONG lsize=0;
pNtQueryObject(NULL,ObjectAllTypesInformation,&lsize,sizeof(ObjectAllTypesInformation),&lsize);
2、分配内存
void *pBuf
pBuf=VirtualAlloc(NULL,lsize,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE);
获取内核对象信息链表
typedef struct _OBJECT_TYPE_INFORMATION {
     UNICODE_STRING TypeName;  //内核对象类型的名称,比如互斥体,事件等等                     
    ULONG TotalNumberOfObjects;//对象的数量
    ULONG TotalNumberOfHandles;
    ULONG TotalPagedPoolUsage;
    ULONG TotalNonPagedPoolUsage;
    ULONG TotalNamePoolUsage;
    ULONG TotalHandleTableUsage;
    ULONG HighWaterNumberOfObjects;
    ULONG HighWaterNumberOfHandles;
    ULONG HighWaterPagedPoolUsage;
    ULONG HighWaterNonPagedPoolUsage;
    ULONG HighWaterNamePoolUsage;
    ULONG HighWaterHandleTableUsage;
    ULONG InvalidAttributes;
    GENERIC_MAPPING GenericMapping;
    ULONG ValidAccessMask;
    BOOLEAN SecurityRequired;
    BOOLEAN MaintainHandleCount;
    ULONG PoolType;
    ULONG DefaultPagedPoolCharge;
    ULONG DefaultNonPagedPoolCharge;
}OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
	
typedef struct _OBJECT_ALL_INFORMATION {
ULONG                   NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;

pNtQueryObject((HANDLE)0xFFFFFFFF,ObjectAllTypesInformation,pBuf,lsize,NULL);
POBJECT_ALL_INFORMATION pObjectAllInfo=(POBJECT_ALL_INFORMATION)pBuf;

调用NtQueryObject后,系统中所有的对象的信息代码以OBJECT_ALL_INFORMATION结构的形式存入了pBuf中,然后将pBuf强转成POBJECT_ALL_INFORMATION,OBJECT_ALL_INFORMATION是由OBJECT_TYPE_INFORMATION结构体数组构成,实际的内核对象类型信息就被存入了OBJECT_TYPE_INFORMATION结构体数组中,通过循环遍历就可以查找是否存在调试对象。

破解之法

我们在调用call esi也就是call ZwQueryObject之前观察堆栈,发现第二个参数是ObjectAllTypesInformation(3),我们将这个值修改成0在执行,这样子就无法读取内核对象了,当然我们也可以直接钩取ZwQueryObject()API。

ZwSetInformationThread()

利用ZwSetInformationThread()API,被调试者可以将自身从调试器中分离出来。

NTSYSAPI NTSTATUS ZwSetInformationThread(
  [in] HANDLE          ThreadHandle,
  [in] THREADINFOCLASS ThreadInformationClass,
  [in] PVOID           ThreadInformation,
  [in] ULONG           ThreadInformationLength
);

ZwSetInformationThread是来为线程设置信息的。该函数拥有两个参数,第一个ThreadHandle用来接收当前线程的句柄,第二个参数ThreadInformationClass表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),则调用该函数后,调试进程就会被分离出来。ZwSetInformationThread不会对正常运行的程序产生任何影响,但是运行的是调试器程序,调用该API就会使调试器终止运行,同时终止自身进程。

当我们继续执行并调用ZwSetInformationThread时候,就会分离出被调试进程并终止运行

破解之法

简单的破解思路就是在调用4001027地址的ZwSetInformationThread前,查找存储在栈中的第二个参数,如果它的值为ThreadHideFromDebugger(0x11),就修改成0继续运行。

原理

ZwSetInformationThread是将线程隐藏起来,调试器就接受不到信息,从而无法调试。另外,Windows XP以后新增了DebugActiveProcessStop()API也可以用来分离调试器与被调试进程,从而停止调试。

TLS回调函数

TLS回调函数是反调试技术中最常用的函数,但我们并不能将TLS回调本身看作一种反调试技术,其回调函数的执行会先于EP代码,所以我们可以在回调函数中加一些骚操作。TLS在前面已经讲过了,这里就不再多描述。

ETC

首先,我们必须明白应用反调试技术的目的在于防止程序遭受逆向分析。不必非得为此费力判断自身进程是否处于调试状态。还有一种比较简单的方法就是判断当前的系统是否为逆向分析专用系统,如果是则停止程序。这样又出现了各种各样的反调试技术,这些技术都能从系统中轻松获取各种信息(进程、文件、窗口、注册表、主机名、计算机名、用户名、环境变量等)

常用的几种反调试技术

这些技术都是借助Win32API获取系统信息来具体实现

1、检测OllyDbg窗口(FindWindow)

2、检测OllyDbg进程(CreateToolhelp32Snapshot)

3、检测计算机名称是否为“TEST”、”ANALYSIS”等(GetComputerName)

4、检测程序运行路径中是否存在“TEST”、”SAMPLE”等(GetCommandLine)

5、检测虚拟机是否处于运行状态

总结

除了这些静态反调试方法,还有很多方法,而且调试过程中还会遇到更多,这是积累经验、不断进步的必经之路。

动态反调试技术

如果在程序文件中应用了动态反调试技术,则很难再使用调试器中的跟踪技术,动态反调试技术会扰乱调试器的跟踪功能。

动态反调试技术的目的

隐藏和保护程序代码与数据,使之无法进行逆向分析。PE保护器中一般会使用大量的动态反调试技术,以保护源程序的核心算法。在调试这些程序时,动态反调试技术就会干扰调试器,使之无法正常跟踪查找源程序的核心代码(OEP)。

异常

异常常用于反调试技术中,正常运行的程序发生异常时,在SEH机制的作用下,OS就会接收异常,然后调用进程中注册的SEH处理,但是,如果被调试的进程在调试状态下发生异常,调试器就会接收处理。利用这个特征就可以判断进程是正常运行还是调试运行。

SEH

EXCEPTION_ACCESS_VIOLATION    	    0xC0000005     程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存。
EXCEPTION_ARRAY_BOUNDS_EXCEEDED     0xC000008C     数组访问越界时引发的异常。
EXCEPTION_BREAKPOINT                0x80000003     触发断点时引发的异常。
EXCEPTION_DATATYPE_MISALIGNMENT     0x80000002     程序读取一个未经对齐的数据时引发的异常。
EXCEPTION_FLT_DENORMAL_OPERAND      0xC000008D     如果浮点数的操作数是非正常的,则引发该异常。所谓非正常即它的值太小以至于不能用标准格式表示出来。
EXCEPTION_FLT_DIVIDE_BY_ZERO        0xC000008E     浮点数除法的除数是0时引发该异常。
EXCEPTION_FLT_INEXACT_RESULT        0xC000008F     浮点数操作的结果不能精确表示成小数时引发该异常。
EXCEPTION_FLT_INVALID_OPERATION     0xC0000090     该异常表示不包括在这个表内的其它浮点数异常。
EXCEPTION_FLT_OVERFLOW              0xC0000091     浮点数的指数超过所能表示的最大值时引发该异常。
EXCEPTION_FLT_STACK_CHECK           0xC0000092     进行浮点数运算时栈发生溢出或下溢时引发该异常。
EXCEPTION_FLT_UNDERFLOW             0xC0000093     浮点数的指数小于所能表示的最小值时引发该异常。
EXCEPTION_ILLEGAL_INSTRUCTION       0xC000001D     程序企图执行一个无效的指令时引发该异常。
EXCEPTION_IN_PAGE_ERROR             0xC0000006     程序要访问的内存页不在物理内存中时引发的异常。
EXCEPTION_INT_DIVIDE_BY_ZERO        0xC0000094     整数除法的除数是0时引发该异常。
EXCEPTION_INT_OVERFLOW              0xC0000095     整数操作的结果溢出时引发该异常。
EXCEPTION_INVALID_DISPOSITION       0xC0000026     异常处理器返回一个无效的处理的时引发该异常。
EXCEPTION_NONCONTINUABLE_EXCEPTION  0xC0000025     发生一个不可继续执行的异常时,如果程序继续执行,则会引发该异常。
EXCEPTION_PRIV_INSTRUCTION          0xC0000096     程序企图执行一条当前CPU模式不允许的指令时引发该异常。
EXCEPTION_SINGLE_STEP               0x80000004     标志寄存器的TF位为1时,每执行一条指令就会引发该异常。主要用于单步调试。
EXCEPTION_STACK_OVERFLOW            0xC00000FD     栈溢出时引发该异常

Windows操作系统中最具代表性的异常时断点异常。BreakPoint指令触发异常时,若程序处于正常运行状态,则自动调用已经注册过的SEH;如果程序处于调试运行状态,则系统会立刻停止运行程序,将控制权转给调试器。

这是基于int 3 异常的反调试代码

程序的执行流程

1、安装SEH
00401011 PUSH 40102C
00401016 PUSH DWORD PTR FS:[0]
0040101D MOV DWORD PTR FS[0],ESP

2、触发INT 3 异常
INT 3
3-1、调试运行—终止进程

如果进程处于调试运行状态,则由调试器处理异常。INT3指令是CPU中断指令,在用户模式的调试器中什么也不做,继续往下执行

00401025 MOV EAX,-1
0040102A JMP EAX

跳转到FFFFFFFF,是一个非法地址,无法继续调试

3-2、正常运行—运行SEH
0040102C MOV EAX,DWORD PTR SS:[ESP+C]
00401031 MOV EBX,401040
00401036 MOV DWORD PTR DS:[EAX+B8],EBX
0040103D XOR EAX,EAX
0040103F RETN	

SS:[ESP+C]是SEH的第三个参数,是CONTEXT *pContext结构体指针,它是一个发生异常的线程CONTEXT结构体。DS:[EAX+B8]就是pContext->EIP,所以401036地址的MOV DWORD PTR DS:[EAX+B8],EBX是将该结构体的EIP修改为401040,然后异常处理器返回0。接下来发生异常的线程再次从修改后的EIP开始运行

删除SEH
00401040 POP DWORD PTR FS:[0]
00401047 ADD ESP,4
破解之法

此时调试器就会忽略被调试进程中发生的INT3异常,而由自身的SEH处理

SetUnhandledExceptionFilter()

进程发异常时,如果SEH未处理或者注册的SEH根本不存在,会发生什么呢?这个时候就会调用执行系统的

kernel32!SetUnhandledExceptionFilter()API,这个函数内部会运行系统的最后一个异常处理器,最后的异常处理器通常会弹出错误消息框,然后终止程序运行。

kernel32!SetUnhandledExceptionFilter()内部调用了ntdll!NtQueryInformationProcess()API,以判断是否正在调试进程,如果进程正常执行,则运行系统最后的异常处理器;如果进程处于调试中,则将异常派送给调试器。我们可以通过kernel32!SetUnhandledExceptionFilter()修改系统最后的异常处理器

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
  [in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

调用该函数修改系统的最后异常处理器时,只要将新的Top Level Exception Filter函数地址传递给函数的lpTopLevelExceptionFilter参数就行了。

破解之法

利用SetUnhandledExceptionFilter()API反调试的技术综合运用了静态和动态技术。。因此,破解时要先使SetUnhandledExceptionFilter()内部调用的ntdll!NtQueryInformationProcess()API失效,然后调用SetUnhandledExceptionFilter跟踪注册的Exception Filter,在正常运行时确定要跳到哪个位置即可

Timing Check

在调试器中逐行跟踪程序代码比程序正常运行耗费的时间多得多,Timing Check技术通过计算运行时间的差异来判断进程是否处于被调试状态。

直接对时间信息进行操作或者对比较时间的语句进行修改就行了,但是在实际操作中,该反调试技术通常与其他反调试技术并用,导致破解过程变得非常困难。

有时候Timing Check也会用在反模拟技术上,程序在模拟器中运行的速度要比正常运行慢很多。

时间间隔测量法

1.Counter based method
RDTSC(Read Time Stamp Counter 读取时间戳计数器)
kernel32!QueryPerformanceCounter()/NtQueryPermanceCounter()
kernel32!GetTickCount()

2.Time based method
timeGetTime()
_ftime()

测量时间间隔的方法大致分为两类,一类是利用CPU的计数器,另一类是利用系统的实际时间(TIME).

提示:

计数器的准确程度由高到低排列如下:

RDSTC>NtQueryPerformanceCounter()>GetTickCount()

NtQueryPerformanceCounter和GetTickCount虽然使用相同硬件,但是二者的准确程度不同,而RDTSC是CPU内部计数器,其准确程度最高

RDTSC

x86CPU中存在一个名为TSC(Time Stamp Counter,时间戳计数器)的64位寄存器。CPU对每个Clock Cycle(时钟周期)计数,然后保存到TSC。

RDTSC是一条汇编指令,用来将TSC值读入到EDX:EAX寄存器(TSC大小为64位,其高32位保存到EDX寄存器,低32位保存至EAX寄存器)

两次RDTSC指令存在一定的时间间隔,通过计算时间差(Delta)来判断进程是否处于调试状态。Delta不是固定值,一般在0xFFF-0XFFFFFFFF之间取

在0x0040101C-0x0040102A之间的代码区域,只要执行一次F7或者F8,Count的值就会大于0xFFFFFFFF

破解之法

1、不适用跟踪命令,直接在0x0040102C下断点,然后F9运行,虽然速度略慢于正常运行速度,但是比代码跟踪要快很多。

2、操作第二个RDTSC,使之与第一个结果相同,这样就能顺利通过CMP语句

3、操纵条件分支语句(CMP,JCC)

在调试器中强制修改Flags的值,阻止执行跳转至0x40103E地址处。大部分的Jcc指令会受CF或ZF的影响,只要修改这些标志即可控制Jcc指令。

4、利用内核模式驱动程序使RDTSC指令失效

利用内核模式驱动程序可以从根本使基于RDTSC的动态反调试技术失效(其实,Olly Advanced PlugIn就采用了该方法)

陷阱标志

单步执行

陷阱标志指EFLAGS寄存器的第九个(index 8)比特位

TF设置成1时,CPU进入单步执行模式。单步执行模式中,CPU执行1条指令后就会触发1个EXCEPTION_SINGLE_STEP异常,然后陷阱标志位就会清零,这个异常可以与SEH相结合,在反调试技术中用于探测调试器。

如果程序正常运行,则运行注册的SEH,如果时调试运行,就继续执行下面的NOP MOV EAX,-1 JMP EAX

JMP EAX=JMP 0xFFFFFFFF肯定会报错,导致程序崩溃

由于EFLAGS不能直接修改,所以我们通过PUSHFD/POPFD与OR运输修改TF位的值

此时EFLAGS也被恢复成202了

破解之法

让被调试者直接处理EXCEPTION_SINGLE_STEP异常

INT 2D

INT 2D 原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。

1、忽略下条指令的第一个字节

在调试模式中执行完 INT 2D指令后,下条指令的第一个字节将被忽略,后一个字节会被识别为新的指令继续执行

2、一直运行到断点处

INT 2D 指令的另一特征,使用StepInto(F7)或StepOver(F8)命令跟踪INT 2D指令时,程序不会停在其下条指令的地方,而是一直运行,直到遇到断点,就像使用RUN(F9)命令运行程序一样。

破解之法

有时候我们会跟踪SEH逐行调试代码,这时候我们需要一种方法可以使程序执行到SEH

首先。使OD中忽视EXCEPTION_SINGLE_STEP异常,然后运行40101E地址处,并且在SEH处下一个断点,否则这里面直接秒执行完

现在我们修改TF位或者修改EFLAGS(+0x100)

此时按照之前的实验,我们只要执行一步,就会触发异常,进入SEH,但是我们F8之后并没有进行跳转,TF也没有清零

这是因为INT 2D是内核指令,在用户模式调试器中不会被识别为正常指令。因此我们再执行一次NOP,跳转到SEH

可能有人会好奇为什么INT 2D后面的第一个字节NOP没有被忽略,这是因为被忽略的情况只发生在TF为0的情况下,当TF为1时,其后面的第一个字节不会被忽略。

0xCC 探测

程序调试过程中,我们一般 会设置许多软件断点,断点对应的x86指令为0xCC,如果能检测到该指令,即可以判断程序是否处于调试状态。但是0xCC可以用作操作码、移位码、立即数、数据、地址等,所以仅仅扫描内存中的代码区域并不可靠。

API断点

若只调试程序中的某个局部功能,一个比较快的方法是先在程序要调试的API处设置断点,再运行程序。运行暂停在相应断点后,再查看存储在栈中的返回地址。

“跟踪返回地址调试相应部分”的方式能够大幅缩小代码调试范围。反调试技术中,探测这些位置在API上的断点就能准确判断当前进程是否处于调试状态。一般而言,断点都是设置在API代码的开始部分,所以,只需要检测API代码的第一个字节是否是0xCC即可判断出当前进程是否处于调试之中

代码逆向人员常用API列表:
进程
CreatProcess				CreateProcessAsUser     	CreateRemoteThread
CreatThread					GetThreadContext			SetThreadContext
EnumProcesses				EnumProcessModules			OpenProcess
CreateToolhelp32Snapshot	Process32First				Process32Next
ShellExecuteA				WinExec						TerminateProcess
内存
ReadProcessMemory			WriteProcessMemory			VirtualAlloc
VirtualAllocEx				VirtualProtect				VirtualProtectEx
VirtualQuery				VirtualQueryEx
文件
CreateFile					ReadFile					WriteFile
CopyFile					CreateDirectory				DeleteFile
MoveFile					MoveFileEx					FindFirstFile
FindNextFile				GetFileSize					GetWindowsDirectory
GetSystemDirectory			GetFileAttributes			SetFileAttributes
SetFilePointer				CreateFileMapping			MapViewOfFile
MapViewOfFileEx				UnmapViewOfFile				_open
_write						_read						_lseek
_tell
寄存器
RegCreateKeyEx				RegDeleteKey				RegDeleteValue
RegEnumKeyEx				RegQueryValueEx				RegSetValue
RegSetValueEx
网络
WSAStartup					socket						inet_addr
closesocket					getservbyname				gethostbybname
htons						connect						inet_htoa
recv						send						HttpOpenRequest
HttpSendRequest				HttpQueryInfo				InternetCloseHandle
InternetConnect				InternetGetConnectedState	InternetOpen
InternetOpenUrl				InternetReadFile			URLDownloadToFile
其他
OpenProcessToken			LookupPrivilegeVaule		AdjustTokenPrivileges
OpenSCManager				CreateService				OpenService
ControlService				DeleteService				RegisterServiceCtrlHandler
SetServiceStatus			QueryServiceStatusEx		CreateMutex
OpenMutex					FindWindow					LoadLibrary
GetProAddress				GetModuleFileNameA			GetCommandLine
OutputDebugString			………………………………………………
破解之法

向系统API设置断点时尽量避开第一个字节,将之设置在代码的中间部分。此外,设置硬件断点也能避开上述所说的反调试技术。

比较校验和

检测代码中设置的软件断点的另一个方法是,比较特定代码区域的校验和值。比如,假定程序中0x401000~0x401070地址区域的校验和值为0x12345678,在代码调试时,必然会设置一些断点(0xCC),这样一来,新的校验和值就与原值不一样了。像这样的话,比较校验和即可判断是否处于调试状态

未设置任何断点时,计算出来的校验值保存在内存单元0x40BDC0中,在程序运行利用循环重新计算一遍校验值,然后再和原来对比。这里计算完后,直接修改je跳转即可跳转

破解之法

从理论上讲,只要不在计算CRC的代码区域中设置断点或修改其中代码,基于校验和的反调试技术就会失效。但是最好的破解办法是直接修改CRC比较语句


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com