R3反调试专题

发布于:2025-07-06 ⋅ 阅读:(17) ⋅ 点赞:(0)

调试器的工作流程

调试器工作流程如下:

1.被调试程序未运行,通过调试器创建进程,也就是我们常见的直接将被调试程序拖入调试器中。其原理是调试器通过CreateProcess并设置DEBUG_PROCESS模式,此时该进程以调试的形式启动。

代码如下所示:

STARTUPINFOA startupInfo = {0};
startupInfo.cb = sizeof(STARTUPINFOA);
PROCESS_INFORMATION pInfo = {0};
BOOL ret = CreateProcess(NULL,arg[1],NULL,NULL,FALSE,DEBUG_PROCESS,NULL,NULL,&startupInfo,&pInfo);

2.被调试程序已运行,通过调试器附加进程,也就是我们通过调试器内部手动以附加进程的方式调试被调试程序,其原理是调试器通过调用DebugActiveProcess函数,此时该进程以调试的形式启动。

如下是WatiForDebugEvent原型:

BOOL WaitForDebugEvent( 
    LPDEBUG_EVENT lpDebugEvent, //操作系统传递给调试器的事件
    DWORD dwMilliseconds        //等待时长
);

其中_DEBUG_EVENT结构体如下:

typedef struct _DEBUG_EVENT {
  DWORD dwDebugEventCode;
  DWORD dwProcessId;
  DWORD dwThreadId;
  union {
    EXCEPTION_DEBUG_INFO      Exception;//异常
    CREATE_THREAD_DEBUG_INFO  CreateThread;
    CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
    EXIT_THREAD_DEBUG_INFO    ExitThread;
    EXIT_PROCESS_DEBUG_INFO   ExitProcess;
    LOAD_DLL_DEBUG_INFO       LoadDll;
    UNLOAD_DLL_DEBUG_INFO     UnloadDll;
    OUTPUT_DEBUG_STRING_INFO  DebugString;
    RIP_INFO                  RipInfo;
  } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

注意:无论是通过打开进程还是附加进程来实现调试,都只是开始调用的方式不一样,后续调试器和操作系统之间的交互方式都是相同的。

在创建了调试进程后接下来就是死循环等待调试事件:当程序在被调试时,被调试进程执行的一些操作会以事件的方式通知调试器:比如dll的加载和卸载,thread的创建和销毁,异常信息等等。当有事件需要通知调试器时,Windows内核会挂起进程中触发事件的线程,然后把事件通知给调试器,等待调试器的处理。调试器通过WatiForDebugEvent来等待事件消息,调试事件被封装到了DEBUG_EVENT结构体中。调试器循环接受不同的调试事件然后根据DEBUG_EVENT结构体中传递过来的不同的调试信息进行相应的处理。每当调试器处理完毕一个线程的调试事件后,就会调用ContinueDebugEvent函数恢复挂起的该线程,如此循环直至恢复所有的线程

标志位检测调试

所谓的反调试就是当程序被调试的时候让调试器失效,从而使得程序无法被调试。要想实现反调试的功能,我们首先应该知道怎么判断程序正在被调试

最简单的可以通过以下两个标志位去检测程序是否正在被调试:

1.BeginDebugged标志设置为1,该标志位于PEB + 2的偏移处,即fs:[30] + 0x02

2.NtGlobalFlag标志设置为0x70,该标志位于PEB+0x68偏移处,即fs:[30] + 0x68

BeginDebugged

我们可以通过调用isDebugPresent API可以获取BeginDebugged的值,当该值为1时,表示当前程序正在被调试。

接下来我们对该API进行一个应用:

#include<Windows.h>
#include<iostream>

int main()
{
    BOOL ret = IsDebuggerPresent();
    if(ret == 1)
    {
        printf("该程序处于被调试状态!\n");
    }
    else
    {
        printf("该程序未被调试!\n");   
    }
    return 0;
}

当该程序被调试时,可以检测到被调试

接下来我们观察IsDebuggerPresent内部是怎么工作的:

其中fs:[30]存放着PEB,由此可知标志BeginDebugged存放在PEB + 2的偏移处

NtGlobalFlag

接下来我们代码观察该标志的值

#include<Windows.h>
#include<iostream>

int main()
{
    DWORD isDebug = 1;
    __asm
    {
        mov eax, fs:[30]
        mov eax, [eax + 0x68]
        mov isDebug, eax
    }
    if(isDebug == 0x70)
    {
        printf("该程序处于被调试状态!\n");
    }
    else
    {
        printf("该程序未被调试!\n");   
    }
    return 0;
}

当该程序被调试时,可以检测到该程序被调试

注意:当以附加进程的方式对程序进行调试时,NtGlobalFlag标志的值不会发生变化。

NtQuerySystemInformation检测调试

CheckRemoteDebuggerPresen

在正式了解NtQuerySystemInformation前,我们先来了解CheckRemoteDebuggerPresen。

当进程处于调试状态时,操纵系统就会为它分配一个调试端口:Debug Port。通过CheckRemoteDebuggerPresent API可以判断调试端口是否存在,进而观察程序是否处于调试状态。当端口存在时,该函数返回TRUE,否则FALSE

接下来我们通过代码来进行应用

#include<Windows.h>
#include<iostream>

int main()
{
    DWORD isDebug = 0;
    CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebug) 
    if(isDebug == TRUE)//调试窗口存在
    {
        printf("该程序处于被调试状态!\n");
    }
    else
    {
        printf("该程序未被调试!\n");   
    }
    return 0;
}

当使用x32dbg调试该程序时, 打印该程序处于被调试状态!,由此可知操作系统确实为该程序分配了一个调试端口

接下来我们深入CheckRemoteDebuggerPresent 内部进行一个观察:

该API内部调用了NtQuerySystemInformation,该函数存在于Nt.dll中,不可直接调用 

NtQuerySystemInformation

现在我们开始学习NtQuerySystemInformation,其函数原型:

NTSTATUS  NTAPI NtQueryInformationProcess(
  HANDLE           ProcessHandle,//进程句柄
  PROCESSINFOCLASS ProcessInformationClass,//进程信息类型 枚举值 用于指定要查询的类型信息
  PVOID            ProcessInformation,//输出参数 缓冲区
  ULONG            ProcessInformationLength,//缓冲区大小
  PULONG           ReturnLength//实际大小
);

其中PROCESSINFOCLASS为枚举类型,常见的有以下几种枚举常量:

typedef enum _PROCESSINFOCLASS {
    ProcessDebugPort = 7,//调试端口
    ProcessDebugObjectHandle = 30,//获取调试对象句柄,句柄为空表示未调试,否则处于调试状态。
    ProcessDebugFlags = 31        //检测调试标志位 当为0表示处于调试状态,1非调试状态
} PROCESSINFOCLASS;

我们可以通过该枚举类型的三个类型实现调试的检测

代码实现如下:

#include<Windows.h>
#include<iostream>

typedef NTSTATUS  (NTAPI *_NtQueryInformationProcess)(
    HANDLE           ProcessHandle,
    DWORD            ProcessInformationClass,
    PVOID            ProcessInformation,
    ULONG            ProcessInformationLength,
    PULONG           ReturnLength
    );
_NtQueryInformationProcess NtQueryInformationProcess;

int main()
{
    HMODULE hmod = LoadLibraryA("ntdll.dll");
    NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress(hmod, "NtQueryInformationProcess");
    DWORD debugPort = 0; 
    NtQueryInformationProcess(GetCurrentProcess(), 7, &debugPort, sizeof(DWORD), NULL);//调试端口
    HANDLE debugHandle = 0;
    NtQueryInformationProcess(GetCurrentProcess(), 30, &debugHandle, sizeof(HANDLE), NULL);//调试对象句柄
    BOOL ProcessDebugFlags = FALSE; 
    NtQueryInformationProcess(GetCurrentProcess(), 31, &ProcessDebugFlags, sizeof(BOOL), NULL);//检测调试标志位
    if(debugPort != 0)
    {
        printf("debufPort检测到调试器!\n");
    }
    else
    {
        printf("debufPort未检测到调试器!\n");
    }
        if(debugHandle != 0)
    {
        printf("debugHandle检测到调试器!\n");
    }
    else
    {
        printf("debugHandle未检测到调试器!\n");
    }
    if(ProcessDebugFlags == 0)
    {
        printf("ProcessDebugFlags检测到调试器!\n");
    }
    else
    {
        printf("ProcessDebugFlags未检测到调试器!\n");
    }
    return 0;
}

当我们将该程序进行调试后,可以发现这三种方式都可以检测到程序是否被调试

注意:当我们使用OD进行调试时,通过ProcessDebugFlags并不能检测到程序是否被调试,这是因为OD具有反反调试功能

CloseHanlde检测调试

我们可以通过CloseHanlde()来实现反调试:当使用CloseHandle关闭一个不存在的句柄时,如果程序处于调试状态,就会触发异常,否则没任何反应。CloseHandle是在内核层检测调试器是否存在的

由于该方法比较简单,因此直接进行代码演示:

#include<Windows.h>
#include<iostream>

int main()
{
    __try
    {
        CloseHandle((HANDLE)0X1234);    
    }
    __expect(1)
    {
        printf("CloseHandle检测到调试器!\n");
    }
    return 0;
}

当程序正常运行时,没有任何变化。但当我们调试该程序时,可以发现检测到了程序被调试

ZwSetInformationThread检测调试

我们可以通过ZwSetInformationThread设置线程信息分离调试器来实现反调试的功能

ZwSetInformationThread函数原型如下:

typedef NTSTATUS(NTAPI* ZwSetInformationThread)(
	IN HANDLE ThreadHandle,//指定线程句柄
	IN THREAD_INFO_CLASS ThreadInformaitonClass,//指定要设置的信息类型
	IN PVOID ThreadInformation,//设置信息的变量指针
	IN ULONG ThreadInformationLength//信息大小
	);

其中THREAD_INFO_CLASS也是一个枚举类型,有如下几种枚举常量:

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

通常我们只使用ThreadHideFromDebugger,这就意味着将线程和调试器分离

接下来我们通过代码学习他的应用:

#include<Windows.h>
#include<iostream>

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

typedef NTSTATUS(NTAPI* _ZwSetInformationThread)(
    IN HANDLE ThreadHandle,
	IN THREAD_INFO_CLASS ThreadInformaitonClass,
	IN PVOID ThreadInformation,
	IN ULONG ThreadInformationLength
	);

_ZwSetInformationThread ZwSetInformationThread;

int main()
{
    HMODULE hmod = LoadLibraryA("ntdll.dll");
    ZwSetInformationThread = (_ZwSetInformationThread)GetProcAddress(hmod, "ZwSetInformationThread");
    ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
    printf("123\n");
    return 0;
}

当程序正常运行时,打印123。但程序被调试时,程序运行被终止,无法调试。这就是线程与调试器分离

硬件断点检测调试

如下所示为调试寄存器:

对于硬件断点反调试来说,DR0-DR3以及DR7是很重要的

DR0-DR3是地址寄存器,用于保存硬件断点地址

DR6为调试异常产生后显示的一些信息,DR7断点属性。

DR7解析:LO=1表示DR0这个地址断点有效 并且是一个局部断点。

L0-L3对应DR0-DR3的断点是否有效(0表示无效,1表示有效),L表示是局部断点

G0-G3对应的DR0-DR3的断点是否有效(0表示无效,1表示有效),G表示是全局断点(windows下没用) 

LEN0-LEN3对应DR0-DR3断点长度。00表示1字节,01表示2字节,10表示8字节,11表示4字节

RW0-RW3对应DR0-DR3断点的类型。 00表示执行断点,01写入断点,10表示I/O断点,11读写断点。

当程序在调试器中进行调试时,被下了硬件断点,这时调试寄存器DR0-DR3中的某一个便存放了断点地址。此时我们就可以通过判断DR0-DR3中是否存在断点地址来判断程序是否被调试。接下了我们以代码进行演示:

#include<windows.h>
#include<iostream>

int main()
{
    CONTEXT context{ 0 };//用于保存上下文信息
    context.ContextFlags = CONTEXT_DEBUG_REGISTERS;//用于指定获获取调试寄存器信息,必须指定否则获取不了任何信息
    GetThreadContext(GetCurrentThread(), &context);//获取线程上下文
    if(context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3 != 0)
    {
        printf("检测到硬件断点!\n");
    }
    return 0;
}

此时我们用调试器调试该程序,并下一个硬件断点,运行程序后发现检测成功

接下来我们来学习一种更高级的方法,构造SEH实现硬件检测调试,这种方式不易被反反调试:

#include<windows.h>
#include<iostream>

EXCEPTION_DISPOSITION  WINAPI myExceptHandler(
	struct _EXCEPTION_RECORD* ExceptionRecord,
	PVOID EstablisherFrame,
	PCONTEXT pcontext,
	PVOID DispatcherContext
)
{	
    if(context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3 != 0)
    {
        printf("检测到硬件断点!\n");
        ExitProcess(0);
    }
    pcontext->Eip = pcontext->EIP + 2;//跳过异常指令
    return ExceptionContinueSearch;
}

int main()
{
    __asm//构造新SEH
    {
        push myExceptHandler
        mov eax, fs:[0]
        push eax
        mov fs:[0], esp
    }
    int a = 5;
    a = a / 0;//该指令两字节
    printf("程序正常结束"); 
    return 0;
}

父进程ID检测调试

我们正常打开一个程序,该程序的父进程是explorer.exe,即资源管理器。因此我们可以通过查询一个程序的父进程ID是否是explorer来判断程序是否可能被调试了。

我们可以通过如下函数进行查询父进程ID

NTSTATUS  NTAPI NtQueryInformationProcess(
  HANDLE           ProcessHandle,//进程句柄
  PROCESSINFOCLASS ProcessInformationClass,//进程信息类型 枚举值 用于指定要查询的类型信息
  PVOID            ProcessInformation,//输出参数 缓冲区
  ULONG            ProcessInformationLength,//缓冲区大小
  PULONG           ReturnLength//实际大小
);

接下来我们通过代码来实现查询父进程ID检测调试的功能

#include<windows.h>
#include<iostream>
#include<TlHelp32.h>

typedef enum _PROCESSINFOCLASS {
    ProcessBasicInformation,
    ProcessQuotaLimits,
    ProcessIoCounters,
    ProcessVmCounters,
    ProcessTimes,
    ProcessBasePriority,
    ProcessRaisePriority,
    ProcessDebugPort,
    ProcessExceptionPort,
    ProcessAccessToken,
    ProcessLdtInformation,
    ProcessLdtSize,
    ProcessDefaultHardErrorMode,
    ProcessIoPortHandlers,          // Note: this is kernel mode only
    ProcessPooledUsageAndLimits,
    ProcessWorkingSetWatch,
    ProcessUserModeIOPL,
    ProcessEnableAlignmentFaultFixup,
    ProcessPriorityClass,
    ProcessWx86Information,
    ProcessHandleCount,
    ProcessAffinityMask,
    ProcessPriorityBoost,
    ProcessDeviceMap,
    ProcessSessionInformation,
    ProcessForegroundInformation,
    ProcessWow64Information,
    ProcessImageFileName,
    ProcessLUIDDeviceMapsEnabled,
    ProcessBreakOnTermination,
    ProcessDebugObjectHandle,
    ProcessDebugFlags,
    ProcessHandleTracing,
    ProcessIoPriority,
    ProcessExecuteFlags,
    ProcessResourceManagement,
    ProcessCookie,
    ProcessImageInformation,
    MaxProcessInfoClass             // MaxProcessInfoClass should always be the last enum
} PROCESSINFOCLASS;

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS ExitStatus;
    DWORD PebBaseAddress;//为方便,直接改为DWORD类型
    ULONG_PTR AffinityMask;
    LONG BasePriority;
    ULONG_PTR UniqueProcessId;
    ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;

typedef NTSTATUS
(NTAPI*
_NtQueryInformationProcess) (
    __in HANDLE ProcessHandle,
    __in PROCESSINFOCLASS ProcessInformationClass,
    __out_bcount(ProcessInformationLength) PVOID ProcessInformation,
    __in ULONG ProcessInformationLength,
    __out_opt PULONG ReturnLength
    );
_NtQueryInformationProcess NtQueryInformationProcess;

int main()
{
    NtQueryInformationProcess = (_NtQueryInformationProcess )GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");由于当我们程序在加载内存时,操作系统会自动加载ntdll.dll,因此我们也没有必要专门通过LoadLibrary去加载ntdll.dll了
    PROCESSENTRY32 processInfo = { 0 };
    processInfo.dwSize = sizeof(PROCESSENTRY32);
    PROCESS_BASIC_INFORMATION basicInfo = { 0 };
    DWORD realSize = 0;
    NtQueryInformationProcess(GetCurrentProcess(), ProcessBasicInformation, &basicInfo, sizeof(PROCESS_BASIC_INFORMATION), &realSize);//获取父进程ID
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);//遍历进程
    if(hSnap == INVALID_HANDLE_VALUE)
    {
        printf("创建进程快照失败!\n");
    }
    Proccess32First(hSnap, &processInfo);
    do
    {
        if(wcscmp(L"explorer.exe"), processInfo.szExeFile == 0)
        {
            if(basicInfo.InheritedFromUniqueProcessId != processInfo.th32ProcessID)//程序父进程ID与资源管理器ID进行比较
            {
                printf("程序可能被调试了!\n");
            }
            else
            {
                printf("程序没有被调试");
            }
        }
    }while(Process32Next(hSnap, &processInfo))
    return 0;
}

整个代码的流程就是先获取当前程序的父进程ID,然后通过进程快照找到资源管理器,获取其进程ID,然后两者进行比较。

注意:该方法并不是万能的,毕竟是可能被调试,仅 作为一个参考

调试对象检测调试

当调试器调试某进程时会创建一个调试对象类型的内核对象,通过检测是否存在调试对象可以实现检测程序是否被调试

我们可以使用NtQueryObject来检测是否存在调试对象,其函数原型如下:

NTSTATUS
NTAPI
NtQueryObject (
    __in HANDLE Handle,
    __in OBJECT_INFORMATION_CLASS ObjectInformationClass,//查询对象类型枚举值
    __out_bcount_opt(ObjectInformationLength) PVOID ObjectInformation,//输出结果缓冲区
    __in ULONG ObjectInformationLength,//缓冲区大小
    __out_opt PULONG ReturnLength//实际需要大小
    );

其中OBJECT_INFORMATION_CLASS有以下几种枚举常量:

typedef enum _OBJECT_INFORMATION_CLASS {
    ObjectBasicInformation,
    ObjectNameInformation,
    ObjectTypeInformation,//对象类型信息
    ObjectTypesInformation,//所有对象类型信息
    ObjectHandleFlagInformation,
    ObjectSessionInformation,
    MaxObjectInfoClass  // MaxObjectInfoClass should always be the last enum
} OBJECT_INFORMATION_CLASS;

接下来我们通过代码实现调试对象检测调试:

#include<windows.h>
#include<iostream>
#include<TlHelp32.h>

typedef enum _OBJECT_INFORMATION_CLASS {
    ObjectBasicInformation,
    ObjectNameInformation,
    ObjectTypeInformation,
    ObjectTypesInformation,
    ObjectHandleFlagInformation,
    ObjectSessionInformation,
    MaxObjectInfoClass  // MaxObjectInfoClass should always be the last enum
} OBJECT_INFORMATION_CLASS;

typedef struct _UNICODE_STRING {
    USHORT Length;//字符串的字节长度,不含终结符
    USHORT MaximumLength;//缓冲区总字节长度,含终结符,使用时注意内存对齐
    PWSTR  Buffer;//该指针指向的缓冲区紧跟在该结构体后面
} UNICODE_STRING, *PUNICODE_STRING;

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_TYPES_INFORMATION//自定义一个结构体方便后续遍历使用
{
    ULONG numberofTypesInfo;//表示获取到信息的对象的个数
    OBJECT_TYPE_INFORMATION typeInfo[1];//设置为1是为了节约内存,后续再进行内存拓展  
}OBJECT_TYPES_INFORMATION,*POBJECT_TYPES_INFORMATION 

NTSTATUS
(NTAPI*
_NtQueryObject) (
    __in HANDLE Handle,
    __in OBJECT_INFORMATION_CLASS ObjectInformationClass,//查询对象类型枚举值
    __out_bcount_opt(ObjectInformationLength) PVOID ObjectInformation,//输出结果缓冲区
    __in ULONG ObjectInformationLength,//缓冲区大小
    __out_opt PULONG ReturnLength//实际需要大小
    );
_NtQueryObject NtQueryObject;

int main()
{
    NtQueryObject = (_NtQueryObject)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryObject");
    POBJECT_TYPES_INFORMATION typesInfo = NULL; 
    char* buff = (char*)malloc(0x4000);//由于后续需要获取所有内核对象类型信息,但并不确定有多少信息,因此我们需要提供一个足够大的缓冲区
    DWORD realSize = 0;
    NTSTATUS ret = NtQueryObject(NULL, ObjectTypesInformation,buff, 0x4000, &realSize);//检测程序中存在的所有类型对象信息
    if(ret != 0)
    {
        printf("NtQueryObject error");
    }
    typesInfo = (POBJECT_TYPES_INFORMATION)buff;
    POBJECT_TYPE_INFORMATION typeInfo = typesInfo->typeInfo;
    for(ULONG i = 0; i < typesInfo->numberOfTypesInfo; i++) 
    {
        if(wcscmp(L"DebugObject", typeInfo->TypeName.Buffer) == 0)//检测调试对象类型
        {
            if(typeInfo->TotalNumberOfObjects > 0)//判断调试对象数量
            {
                printf("检测到了调试对象");
            }
        }     
        DWORD buffLen = typeInfo->TypeName.MaximumLength;
        buffLen += bufflen % 4;
        typeInfo = (POBJECT_TYPE_INFORMATION)((DWORD)typeInfo + bufflen);   
        typeInfo++;
    }
    return 0;
}

 CRC检测调试

CRC的全称是循环冗余校验,作用是为了检测程序数据的准确性和完整性。程序发生修改以后,其CRC的值也会发生变化。

CRC32检测原理:程序被编译后,数据段内容会随着程序的运行可能发生变化,但代码段是固定不变的。我们在调试程序的时候,下断点或者是修改汇编指令都会影响代码段的CRC32值,这个时候只需要检测最初的CRC32跟当前时刻的CRC32值是否一致就能判断出代码是否被修改了。

我们可以通过观察PE文件的各个段的属性是否是可执行的来判断该段是否是代码段,一般为.text段。一个快速但不保证正确的方法:观察属性值首位是否为6,如果是说明是代码段

接下来我们通过代码实现CRC的应用:

首先在网上找个简单的crc计算代码,然后添加到头文件以方便使用

#include<iostream>

uint32_t crc32_table[256];

int make_crc32_table()
{
    uint32_t c;
    int i = 0;
    int bit = 0;
    for (i = 0; i < 256; i++)
    {
        c = (uint32_t)i;
        for (bit = 0; bit < 8; bit++)
        {
            if (c & 1)
            {
                c = (c >> 1) ^ (0xEDB88320);
            }
            else
            {
                c = c >> 1;
            }
        }
        crc32_table[i] = c;
    }
    return 1;
}

uint32_t make_crc(unsigned char* string, uint32_t size)
{
    uint32_t crc= 0xFFFFFFFF;
    make_crc32_table();
    while (size--)
        crc = (crc >> 8) ^ (crc32_table[(crc ^ *string++) & 0xff]);
    return crc;
}
​

随后我们在正式的cpp文件中实现crc检测功能 

#include<Windows.h>
#include<iostream>
#include"CRC32.h"

int main()
{
    char *buffer=(char*)GetModuleHandleA(0);//参数为0就获取当前进程的句柄
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;
    PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + buffer);
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
    for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections - 1; i++)
    {
        if (pSectionHeader->Characteristics / 0x10000000 == 6)//判断代码段
        {
            printf("%s\n", pSectionHeader->Name)
            unsigned int crc = make_crc((unsigned char*)(pSectionHeader->VirtualAddress + buffer), pSectionHeader->Misc.VirtualSize);
            printf("%x\n", crc)
        }
        pSectionHeader++;
    }
	system("pause");
	return 0;
}

此时正常运行程序,获取原始的crc值。随后将程序进行调试,在代码段下一个断点,观察打印出的crc值,发现改变了。由于可以推断出该程序被调试了

检测虚拟机

在虚拟机中,资源管理器中有一个进程叫作vmtoolsd.exe,根据这个文件我们便可以有如下几个方法判断一个程序是否是在虚拟机中运行:

方法一:通过进程快照查找是否存在进程vmtoolsd.exe

方法二:检测C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe文件是否存在

方法三:遍历系统所有服务,判断是否存在VMware开头的服务

方法四:特权指令检测,如下汇编代码在真实环境中会触发异常,而虚拟机中不会

push   edx
push   ecx
push   ebx
mov    eax, 'VMXh'
mov    ebx, 0
mov    ecx, 10 
mov    edx, 'VX' 
in     eax, dx 
cmp    ebx, 'VMXh'
pop    ebx
pop    ecx
pop edx

接下来我们通过方法三来实现检测虚拟机:

#include<Windows.h>

int main()
{    
    WC_HANDLE hSchandle = OpenSCManagerA(NULL, NULL, SC_MANAGER_ALL_ACCESS);//连接服务管理器
    LPENUM_SERVICE_STATUSA lpService = NULL;
    lpService = (LPENUM_SERVICE_STATUSA )malloc(0x4000);
    DWORD realNeedSize = 0;
    DWORD serviceNumber = 0;
    DWORD resumeHandle = 0;
    BOOL ret = EnumServicesStatusA(hSchandle, SERVICE_WIN32, lpService, 0x4000, &realNeedSize, &serviceNumber, &resumeHandle);//枚举系统服务
    if(ret == FALSE)
    {
        return 0;
    }
    for(size_t i = 0; i < serviceNumber; i++)
    {
        if(strstr(lpService->lpDisplayName, "Vmware") == 0)//判断当前服务是否出现Vmware
        {
            MessageBoxA(0, "检测到虚拟机", "提示", MB_OK);
            break;
        }
        lpService++;
    }
    if(hSchandle)
    {
        CloseServiceHandle(hSchandle);
    }
	return 0;
}

附加进程反调试

方法一

编译器通过DebugActiveProcess附加进程调试程序时,在第一个线程被恢复的时候被调试程序会去调用 Ntdll.dll中的DbgBreakPoint函数,该函数实际是一个int3,如下图所示:

当执行int 3时,被调试进程触发异常暂停下来。有的游戏会在这设置一个hook,直接调用终止进程函数,使得调试器附加进程的时候直接结束,这就实现了反调试的目的了

我们现在观察编译器调用DebugActiveProcess做了那些事:

DebugActiveProcess->ntdll.DbgUiDebugActiveProcess(底层)-> call ntdll.DbgUiIssueRemoteBreakin(创建远程断点)-> push ntdll.DbgUiRemoteBreakin(函数地址作为参数 )-> call ntdll.ZwCreateThreadEx(在目标进程创建调试线程)-> 调试线程从DbgUiRemoteBreakin函数地址处开始执行-> call ntdll.DbgBreakPoint(触发异常)-> 调试器接收DEBUG_Event-> 调试器附加进程成功

因此当我们使程序被编译器附加进程时,不让它执行DbgBreakPoint,这就使得它无法被调试,也就达到了反调试的目的 

我们以代码的形式简单的通过修改DbgBreakPoint的int 3为ret来使其不能够触发异常,无法附加进程成功:

#include<Windows.h>
#include<iostream>

int main()
{
    DWORD oldProtect = 0;
    BYTE* funaddr = GetProcAddress(GetModuleHandleA("ntdll.dll"), "DebugBreakPoint");
    VirtualProtect((LPVOID)funaddr ,1, PAGE_EXECUTE_READWRITE, &oldProtect);
    funaddr[0] = 0xC3;
    VirtualProtect((LPVOID)funaddr ,1, PAGE_EXECUTE_READWRITE, &oldProtect);
    system("pause");
    return 0;
}

方法二

利用调试器的原理,我们可以通过创建一个调试模式下的进程 ,此时该进程就不能被其它进程调试了,因为它处于调试模式下,已经在被调试了

实现代码如下:

#include<iostream>
#include<Windows.h>

int main()
{
    STARTUPINFOA sinfo = { 0 };
    sinfo.cb = sizeof(STARTUPINFOA);
    PROCESS_INFORMATION pinfo = { 0 }; 
    BOOL ret = CreateProcessA("D://FateMouse.exe", NULL, NULL, FALSE, DEBUF_PROCESS, NULL, NULL, &sinfo, &pinfo);
    if(!ret)
    {
        MessageBoxA(0, "打开进程失败!", "提示", MB_OK);
        return 0;
    }
    while(TRUE)
    {
        DEBUG_EVENT debugEvent = { 0 };
        BOOL rdebug = WaitForDebugEvent(&debugEvent, -1);
        if(rdebug)
        {
            printf("EventCode = %d\n", debugEvent.dwDebugEventCode);
        }
        ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
    }
    return 0;
}

此时运行程序,进程FateMouse便以调试的方式启动了,同时我们也让他输出了一些调试信息。此时FateMouse因处于调试状态不能再被调试器调试了  

反Strong OD过检测

Strong OD是ollydbg中的一个插件,可以用于反反附加,其通过LdrInitializeThunk实现。

在调试器附加进程的过程中,调试器调用NtCreateThreadEx创建调试线程后,会进入内核态,在内核态中调用LdrInitializeThunk,之后再调用ntdll.ZwContinue重新回到用户态,这时NtCreateThreadEx才算真正执行完毕

而Strong OD通过将LdrInitializeThunk的前五个字节修改从而HOOK到自己的代码,然后触发异常使调试器接收事件,然后附加进程成功,结束调试线程。这就使得原附加进程的流程得到了改变,不会执行NtCreateThreadEx后面的步骤,也就杜绝了附加进程反调试

调试器在进行进程附加调试时,进程先运行再附加,因此在明白Strong OD反反调试的原理以后,我们可以创建一个线程,用于循环检测LdrInitializeThunk的前五个字节是否被修改(被HOOK),当Strong OD进行HOOK时,就把LdrInitializeThunk的前五个字节改为原来的字节,这就避免了Strong OD的HOOK

代码实现:

#include<Windows.h>
#include<iostream>

//保存LdrInitializeThunk的前五个字节
DWORD g_InitializeFunAddr;
BYTE  InitializeFun[5]{};
BOOL SaveLdrInitializeCode()
{
    DWORD funaddr = GetProcAddress(GetModuleHandleA("ntdll.dll"), "LdrInitializeThunk");
    LdrInitializeThunk = funaddr;
    memcpy(InitializeFun, (LPVOID)funaddr, 5);
    return TRUE;
}

DWORD WINAPI threadProc(LPVOID lparam)
{
    while(true)
    {
        DWORD newCode = *(DWORD*)g_InitializeFunAddr;
        if(newCode != *(DWORD)InitializeFun)
        {
            printf("Strong OD要HOOK");
            DWORD oldProtect = 0;  
            VirtualProtect((LPVOID)g_InitializeFunAddr,5, PAGE_EXECUTE_READWRITE, &oldProtect);//加载内存后代码段不可写因此要修改内存属性
            memcpy((LPVOID)g_InitializeFunAddr, InitializeFun, 5);
            VirtualProtect((LPVOID)g_InitializeFunAddr,5, oldProtect, &oldProtect);
        }
    }
}

BOOL hookDebugBreakPoint()
{
    DWORD oldProtect = 0;
    BYTE* funaddr = GetProcAddress(GetModuleHandleA("ntdll.dll"), "DbgBreakPoint");
    VirtualProtect((LPVOID)funaddr ,1, PAGE_EXECUTE_READWRITE, &oldProtect);
    funaddr[0] = 0xC3;
    VirtualProtect((LPVOID)funaddr ,1, PAGE_EXECUTE_READWRITE, &oldProtect);
    return TRUE;
}

int main()
{

    SaveLdrInitializeCode();
    hookDebugBreakPoint;
    HANDLE hThread = CreateThread(NULL, 0, threadProc, 0, 0, NULL);
    while(true)
    {
        printf("hello\n");
        sleep(3000);
    }
    system("pause");
    return 0;
}

此时我们利用调试器附加该进程发现,不仅没有被Strong OD HOOK掉,而且还因为正常调用了DbgBreakPoint,执行了我们修改的代码retn,导致该进程无法被附加进而无法被调试

查询调试器窗口检测调试

我们可以通过检测当前操作系统上运行的窗口和进程是否包括调试器,如果有则说明调试器可能在调试程序

这是一个很基础的代码,具体实现如下:

#include<Windows.h>
#include<iostream>
#include<Tlhelp32>

//检测调试器窗口
BOOL checkDbgWindow()
{
    HWND hwnd = FindWindow(NULL, "x32dbg");    
    if(hwnd)
    {
        printf("检测到x32dbg窗口!\n");
        return TRUE;
    }
    return FALSE;
}

BOOL checkDbgProcess()
{
    HANDLE handle = CreatToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(!handle)
    {
        return FALSE;
    }
    PROCESSENTRY32W processEntry{ 0 };
    processEntry.dwSize = sizeof(PROCESSENTRY32W);
    Process32First(handle, &processEntry);
    do
    {
        if(wcscmp(L"x32dbg.exe", processEntry .szExeFile) == 0) 
        {
            CloseHandle(handle);
            printf("检测到调试器进程!\n");
            return TRUE;
        }
    }while(Process32NextW(handle, &processEntry));
    
}

DWORD WINAPI checkDbg(LPVOID lparam)
{
    while(checkDbgWindow() || checkDbgProcess())
    {
        printf("有调试器在运行!\n");
    }
    return 0;
}

int main()
{
    HANDLE hthread = CreateThread(NULL, 0, checkDbg, 0, 0,NULL);
    system("pause");
    return 0;
}

EFLAGS检测调试

在intel的x86寄存器中有一种叫EFLAGS的标志寄存器,如下所示:

EFLAGS寄存器中有一个标志位TF,该标志位为单步执行标志。当该标志位设置为1时,CPU进入单步执行模式。此时CPU每执行一条指令,就会触发一个int 1中断异常,然后将该标志位清零。当程序正常运行时,触发异常会执行异常处理程序,但当有调试器调试该程序时,调试器遇int 1会将其当做正常指令执行,因此不会调用异常处理程序。因此当TF标志位为1,然后执行指令后触发异常时,根据程序是否执行异常处理程序就可以判断该程序是否被调试

注意:该方法只适用调试器单步执行调试程序的情况下

实现代码如下所示:

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

void myExitProcess()
{
	printf("检测到调试器\n");
	ExitProcess(0);
}

void checkTFflag()
{
	DWORD addr = (DWORD)myExitProcess;
	__try
	{
		_asm
		{
			pushfd//将EFLAGS寄存器压栈
			or dword ptr ss:[esp],0x100//将TF标志位设置为0 
			popfd//将修改后的值出栈EFLAGS,此时CPU进入单步执行状态
			nop//无操作,目的是执行完无操作以后触发int 1异常
			jmp addr//退出进程
		}
	}
	__except (1)//触发异常时,程序执行异常处理程序
	{
		printf("没有检测到调试器\n");
	}
}

int main()
{
	checkTFflag();
	system("pause");
	return 0;
}

程序子进程窗口特点检测调试

我们所使用的应用程序展示的窗口通常还有子窗口,比如Ollydbg,虽然我们打开它看起来是只有一个窗口,其实它有很多子窗口。我们通过VS工具中的spy++可以检测到:

我们所使用的应用程序,它们的窗口都是属于桌面窗口的子窗口,因此我们可以通过查找桌面的子窗口来判断调试器是否存在

接下来我们代码实现:

#include<Windows.h>
#include<iostream>

BOOL CALLBACK EnumChildProc(
	_In_ HWND   hwnd,
	_In_ LPARAM lParam
)
{
    char str[0x100]{};
    SendMessageA(hwnd, WM_GETTEXT, 0X100, (LPARAM)str);
    printf("窗口名称为:%s\n",str);
    return TRUE;
}

int main()
{
    //获取桌面窗口
    HAND hDeskTop = GetDesktopWindow();
    //获取桌面子窗口
    HANDLE deskSubWindow = GetWindow(hDeskTop, GW_CHILE);
    while(deskSubWindow)
    {
        char str[0x100]{};
        EnumChildWindows(deskSubWindow, EnumChildProc, 0);
    }
    system("pause");
	return 0;
}