Windows UserMode 的反调试技术(总结)

1. 检测调试技术

1.1 NtGlobalFlag 标志位

// 通过检查 PEB.NtGlobalFlag[0x68] 判断是否处于被调试状态
// 如果被调试的话, NtGlobalFlag = 0x70
bool CheckNtGlobalFlag()
{
    DWORD NtGlobalFlag = 0;
    __asm
    {
        ; 获取 PEB 字段
        mov eax, fs:[0x30]
        ; 获取偏移为 0x68 的字段
        mov eax, [eax + 0x68]
        ; 获取 NtGlobalFlag 的值并保存
        mov NtGlobalFlag, eax
    }
    // 通过将 NtGlobalFlag 设置为非 0x70 也可以反反调试
    return NtGlobalFlag == 0x70;
}

int main()
{
    if (CheckNtGlobalFlag())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}*

1.2 BeingDebugged 标志位 \ IsDebuggerPresent 函数

int main()
{
    // IsDebuggerPresent 实际检查的也是 PEB.BeindDebugged[0x002]
    // 可以通过修改字段或者 HOOKAPI 进行反反调试
    if (IsDebuggerPresent())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

1.3 ProcessDebugPort 

// 通过查询调试端口是否为-1判断有没有被调试,如果非调试状态就是0
bool CheckProcessDebugPort()
{
    int nDebugPort = 0;
    NtQueryInformationProcess(
        GetCurrentProcess(), // 目标进程句柄
        ProcessDebugPort, // 查询信息类型
        &nDebugPort, // 输出查询信息
        sizeof(nDebugPort), // 查询类型大小
        NULL); // 实际返回数据大小
    return nDebugPort == 0xFFFFFFFF ? true : false;
}

int main()
{
    while (true)
    {
        if (CheckProcessDebugPort())
            printf("当前处于[被]调试状态\n");
        else
            printf("当前处于[非]调试状态\n");
    }

    system("pause");
    return 0;
}

1.4 DebugObjectHandle

// 当程序处于被调试状态,调试句柄应该是非空的
bool CheckProcessDebugObjectHandle()
{
    HANDLE hProcessDebugObjectHandle = 0;
    NtQueryInformationProcess(
        GetCurrentProcess(), // 目标进程句柄
        (PROCESSINFOCLASS)0x1E, // 查询信息类型
        &hProcessDebugObjectHandle, // 输出查询信息
        sizeof(hProcessDebugObjectHandle), // 查询类型大小
        NULL); // 实际返回大小
    return hProcessDebugObjectHandle ? true : false;
}

int main()
{
    if (CheckProcessDebugObjectHandle())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

1.5 DebugFlag 标记

// 如果进程被调试,那么 bProcessDebugFlag = 1
bool CheckProcessDebugFlag()
{
    BOOL bProcessDebugFlag = 0;
    NtQueryInformationProcess(
        GetCurrentProcess(), // 目标进程句柄
        (PROCESSINFOCLASS)0x1F, // 查询信息类型
        &bProcessDebugFlag, // 输出查询信息
        sizeof(bProcessDebugFlag), // 查询类型大小
        NULL); // 实际返回大小

    return bProcessDebugFlag ? false : true;
}

int main()
{
    if (CheckProcessDebugFlag())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

1.6 检测父进程ID

bool CheckParentProcess()
{
    // 自己定的结构体
    struct PROCESS_BASIC_INFORMATION {
        ULONG ExitStatus; // 进程返回码
        PPEB PebBaseAddress; // PEB地址
        ULONG AffinityMask; // CPU亲和性掩码
        LONG BasePriority; // 基本优先级
        ULONG UniqueProcessId; // 本进程PID
        ULONG InheritedFromUniqueProcessId; // 父进程PID
    }stcProcInfo;

    NtQueryInformationProcess(
        GetCurrentProcess(),
        ProcessBasicInformation, // 需要查询进程的基本信息
        &stcProcInfo,
        sizeof(stcProcInfo),
        NULL);

    DWORD ExplorerPID = 0;
    DWORD CurrentPID = stcProcInfo.InheritedFromUniqueProcessId;
    GetWindowThreadProcessId(FindWindow(L"Progman", NULL), &ExplorerPID);
    return ExplorerPID == CurrentPID ? false : true;
}

int main()
{
    if (CheckParentProcess())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

1.7 KernelDebuggerEnabled 标志

bool CheckSystemKernelDebuggerInformation()
{
    // 通过检测 KUSER_SHARED_DATA.KdDebuggerEnabled[0x2d4] 标记位
    // (KdDebuggerEnabled & 0x2 为 0 代表没有内核调试器, 否则有内核调试器)
    // 结构体保存的是查询到的信息
    struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION {
        BOOLEAN KernelDebuggerEnabled;
        BOOLEAN KernelDebuggerNotPresent;
    }DebuggerInfo = { 0 };

    NtQuerySystemInformation(
        (SYSTEM_INFORMATION_CLASS)0x23, // 查询信息类型
        &DebuggerInfo, // 输出查询信息
        sizeof(DebuggerInfo), // 查询类型大小
        NULL); // 实际返回大小

    // 通过是否开启内核调试器知道当前系统有没有被调试
    return DebuggerInfo.KernelDebuggerEnabled;
}

int main()
{
    if (CheckSystemKernelDebuggerInformation())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

1.8 ThreadHideFromDebugger

typedef enum THREAD_INFO_CLASS {
ThreadHideFromDebugger = 17
};

// 函数的原型
typedef NTSTATUS(NTAPI *ZW_SET_INFORMATION_THREAD)(
    IN HANDLE ThreadHandle,
    IN THREAD_INFO_CLASS ThreadInformaitonClass,
    IN PVOID ThreadInformation,
    IN ULONG ThreadInformationLength);

void ZSIT_DetachDebug()
{
    ZW_SET_INFORMATION_THREAD ZwSetInformationThread;

    // 这个函数是未公开函数
    ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(
        LoadLibrary(L"ntdll.dll"), "ZwSetInformationThread");
    
    // 设置 ThreadHideFromDebugger 此参数将使这条线程对调试器“隐藏”。即调试器收不到调试信息
    ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
}

int main()
{
    ZSIT_DetachDebug();
    printf("runnning...\n");
    system("pause");
    return 0;
}

1.9 遍历进程名称(一般都会通过 GetProcAddress 动态获取函数地址, OD函数调用列表查看不到)

 

(1) GetProcAddress获取函数地址
(2) EnumProcesses 遍历进程获取PID函数
(3) OpenProcess 根据PID获取进程句柄函数
(4) EnumProcessesModules 获取进程基址函数
(5) GetModuleBaseNameA 根据进程基址获取进程名称函数
(6) 然后通过比较名称再利用TerminateProcess结束目标进程
-------- 遍历当前进程,判断进程名称 --------
(1) CreateToolhelp32Snapshot 获取当前进程快照
(2) Process32First、Process32Next
(3) 然后通过比较名称再利用TerminateProcess结束目标进程 

2.0 通过窗口类名或标题查找目标

(1)FindWindow 获取指定类名或标题的窗口句柄
(2)GetWindowsThreadProcessId 获取窗口句柄对应的PID
(3)OpenProcess 根据PID获取进程句柄函数
(4)TerminateProcess结束目标进程

int main()
{
    // 通用性不够,窗口名是会变动的,推荐查询进程名称
    if (FindWindow(L"OllyDbg", NULL))
        printf("存在调试器\n");
    else
        printf("没检测到调试器\n");

    return 0;
}

2.1.0 通过 UEH 函数来进行反调试

// 其实逆向 X86UserMode 可以发现其最终是调用了 GlobalTopLevelExceptionFilter(ExceptionInfo); // 调用UEH函数处理异常
(1)通过 SetUnhandledExceptionFilter(/* 自定义的顶层异常处理函数地址 */); 注册一个UnhandledExceptionFilter(UEH)函数
(2)再这个UEH函数中调用ZwQueryInformationProcess/ZwSetInformationThread等函数来检测
(3)发现调试直接GG

2.1.1 通过 SEH 来进行反调试

// __try{}__exception(1){} 除了这种方式
// 我们也可以手动插入 FS:[0] 处, 也就是 Teb.ExceptionList 处
// 安装 SEH 反调试
VOID CheckDebugSeh()
{
	__asm {
		// install SEH
		push Handler;			 // EXCEPTION_REGISTRATION_RECORD.Handler
		push dword ptr fs : [0]; // EXCEPTION_REGISTRATION_RECORD.Next
		mov dword ptr fs : [0], esp;

		// 触发异常
		int 0x3; // 0xCD 0x03 也可以

		// 有 Debug, 直接触发访问异常
		mov eax, 0x0;
		jmp eax;
	Handler: // 通过 KiUserExceptionDispatcher 调度到这
		mov eax, dword ptr[esp + 0xc];  // 获取 CONTEXT 地址
		mov ebx, Normal_Code;
		mov dword ptr[eax + 0xb8], ebx; // CONTEXT.Eip = Normal_Code 标签地址
		xor eax, eax;

		ret;
	Normal_Code:
		// remove SEH
		pop dword ptr fs : [0];
		add esp, 0x4;
	}

	// 无 Debug
}

2.1.2 使用 INT 2D 进行反调试

// INT 2D原为内核模式中用来触发断点异常的指令 
// 也可以在用户模式下触发异常, 但程序调试运行时(OD不会触发异常), 指令下一条地址的第一个字节将被忽略
VOID CheckDebugSeh()
{
	BOOL bDebugging = FALSE;

	__asm {
		// install SEH
		push Handler;			 // EXCEPTION_REGISTRATION_RECORD.Handler
		push dword ptr fs : [0]; // EXCEPTION_REGISTRATION_RECORD.Next
		mov dword ptr fs : [0], esp;

		// 触发异常(OD忽略该异常,并走下去)
		int 02dh;
    
        // 如果有调试, 会一直走下去(否则触发异常)
		nop;    // 这个字节会被忽略
		mov bDebugging, 1h;
		jmp Normal_Code;

	Handler: // 通过 KiUserExceptionDispatcher 调度到这
		mov eax, dword ptr[esp + 0xc];  // 获取 CONTEXT 地址
		mov ebx, Normal_Code;
		mov dword ptr[eax + 0xb8], ebx; // CONTEXT.Eip = Normal_Code 标签地址
		xor eax, eax;

		mov bDebugging, 0h;

		ret;
	Normal_Code:
		// remove SEH
		pop dword ptr fs : [0];
		add esp, 0x4;
	}

	if (bDebugging)
	{
		MessageBoxA(0, "有反调试起来嗨!", "提示", MB_OK);
	}
	else
		MessageBoxA(0, "毛都没有!", "提示", MB_OK);
}

2.1.3 使用 0xF1 反单步调试

// 运行 0xF1 将会产生一个单步异常,如果通过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不执行先前设置的异常处理例程
BOOL CheckDebugF1()
{
	__try
	{
		__asm __emit 0xF1
	}
	__except (1)
	{
		return FALSE;
	}
	return TRUE;
}

2.1.4 使用 EFLAGS.TF 标志位 反单步调试

// 产生一个单步异常,如果通过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不执行先前设置的异常处理例程
BOOL CheckDebugTF()
{
	__try
	{
		__asm
		{
			pushfd
			or word ptr[esp], 0x100
			popfd
			nop
		}
	}
	__except (1)
	{
		return FALSE;
	}
	return TRUE;
}

2.2 ProcessHeap 标志位

// 通过检查堆结构的两个 Flags 标志判断是否被调试
// 如果没有被调试, 正常情况下应为2与0
bool CheckProcessHeap()
{
    // 因为不同的系统中两个标志偏移可能不同,所以
    // 这个方法时不通用的。
    DWORD Flags = 0, ForceFlags = 0;

    __asm
    {
        ; 获取 PEB
        mov eax, fs:[0x30]

        ; 获取 PEB.ProcessHeap
        mov eax, [eax + 0x18]

        ; 获取标志位 1
        mov ecx, [eax + 0x40]
        mov Flags, ecx

        ; 获取标志位 2
        mov ecx, [eax + 0x44]
        mov ForceFlags, ecx
    }
    printf("%X - %X\n", Flags, ForceFlags);
    return (ForceFlags == 0 && Flags == 2) ? FALSE : TRUE;
}

int main()
{
    if (CheckProcessHeap())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

2.3 CheckRemoteDebuggerPresent 函数

BOOL CheckDebug()  
{  
    BOOL ret;  
    CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);  
    return ret;  
}  

2.4 断点检测

2.4.1 软件断点检测(CC断点)

 // CC 断点检测
BOOL CheckDebugCC()
{
	PIMAGE_DOS_HEADER pDosHeader;
	PIMAGE_NT_HEADERS pNtHeaders;
	PIMAGE_SECTION_HEADER pSectionHeader;
	DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);

	pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
	pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
	pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders 
		+ sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) 
		+ (WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
	DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
	DWORD dwCodeSize = pSectionHeader->SizeOfRawData;

	BOOL Flag = FALSE;
	__asm
	{
		cld;
		mov edi, dwAddr; // 查找当前 EXE ,SizeOfRawData个字节
		mov ecx, dwCodeSize;
		mov al, 0CCH;
		repne scasb; // 从 edi 单字节查找 al, ecx 次
		jnz NotFound; // 没有找到 ZF = 0 跳转
		mov Flag, 01H;
	NotFound:
	}

	return Flag;
}

2.4.2 硬件断点检查

// 直接将 Dr1 ~ Dr3 清空, DR6、DR7用于记录Dr0-Dr3中断点的相关属性
BOOL CheckDebug()  
{  
    CONTEXT context;    
    HANDLE hThread = GetCurrentThread();    
    context.ContextFlags = CONTEXT_DEBUG_REGISTERS;  // 获取调试寄存器  
    GetThreadContext(hThread, &context);    
    if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0)     
    {    
        return TRUE;    
    }    
    return FALSE;    
}  

2.4.3 代码和校验检查

// 计算校验和,来判断文件是否被修改. 
// 校验和检测
BOOL CheckDebugSum()
{
	PIMAGE_DOS_HEADER pDosHeader;
	PIMAGE_NT_HEADERS pNtHeaders;
	PIMAGE_SECTION_HEADER pSectionHeader;
	DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);

	pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
	pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
	pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders
		+ sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER)
		+ (WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
	DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
	DWORD dwCodeSize = pSectionHeader->SizeOfRawData;

	DWORD checksum = 0;
	__asm {
		cld;
		mov esi, dwAddr;
		mov ecx, dwCodeSize;
		xor eax, eax;
	checksum_loop:
		movzx ebx, byte ptr[esi];
		add eax, ebx;
		rol eax, 1; // 和计算
		inc esi; // 下一个
		loop checksum_loop;
		mov checksum, eax; // 获取校验和
	}

	if (checksum != pNtHeaders->OptionalHeader.CheckSum) // 之前计算的校验和
	{
		return FALSE;
	}
	else {
		return TRUE;
	}
}

2.5 时间检测

检测程序前后执行的时间对比,如果太长就认定为有调试器

2.5.1 Rdtsc 指令 

// Rdtsc 指令
BOOL CheckTimeRdtsc()
{
	BOOL dwTime1, dwTime2;
	__asm {
		rdtsc;
		mov dwTime1, eax;

		rdtsc;
		mov dwTime2, eax;
	}

	if ((dwTime2 - dwTime1) < 0xFF) {
		return FALSE; 
	}
	else {
		return TRUE; // 有调试器
	}
}

2.5.2  QueryPerformanceCounter 函数 和 GetTickCount 函数

BOOL CheckTimeGetTickCount()
{
	DWORD dwTime1 = GetTickCount();

	/******执行代码********/
	// ..........

	DWORD dwTime2 = GetTickCount();
	if ((dwTime2 - dwTime1) > 0x1A)
	{
		return TRUE; // 有调试器
	}
	else
		return FALSE;
}

2.6 使用 TLS 回调

    在到达程序 OEP 前,会先执行 TLS 回调函数, 使用了 TLS, 节中会出现 .tls, 所以现在使用的人不多, 可使用PEView查看TLS回调函数地址(使用OD在系统断点断下可饶过)2

// 声明使用TLS回调函数
#pragma comment(linker,"/INCLUDE:__tls_used")

// 定义回调函数
void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD dwReason, PVOID Reserved);

//注册TLS回调函数
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTls_CallBack[] = { TLS_CALLBACK1,0 };
#pragma data_seg()

void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	if (IsDebuggerPresent())
	{
		MessageBoxA(0, "调试器!", "提示", MB_OK);
		ExitProcess(1);
	}
	else {
		MessageBoxA(0, "毛都没有!", "提示", MB_OK);
	}
} 

2.7 RaiseException 函数

// RaiseException函数产生的若干不同类型的异常可以被调试器捕获
BOOL TestExceptionCode(DWORD dwCode)  
{  
      __try  
      {  
            RaiseException(dwCode, 0, 0, 0);  
      }  
      __except(1)  
      {  
            return FALSE;  
      }  
      return TRUE;  
}  
    
BOOL CheckDebug()  
{  
      return TestExceptionCode(DBG_RIPEXCEPTION);      
} 

2.8 调试器漏洞 

DataDirectory 漏洞(一般为0x10)
OutputDebugString 漏洞

 

 

 

 

原创文章,转载请注明: 转载自Windows内核安全驱动编程

本文链接地址: Windows UserMode 的反调试技术(总结)

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注