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)则退出程序。
异常时系统的处理顺序:
- 系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统
挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息。 - 如果程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否设置了相关的异常处理例程,如果
设置了相关的异常处理例程,系统就把异常发送给程序seh处理例程,交由其处理。 - 每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,
可交由链起来的其他例程处理。 - 如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知调试器。
- 如果程序未处于被调试状态或者调试器没有能够处理,并且你调用SetUnhandledExceptionFilter设置了最后异
常处理例程UnhandleExceptionFilter的话,系统转向对它的调用。 - 如果你没有设置最后异常处理例程UnhandleExceptionFilter或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框,
用户可以选择关闭或者最后将其附加到调试器上的调试按钮。如果没有调试器能被附加于其上或者调试器也处理不了,系统
就调用ExitProcess终结程序。 - 不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会。
二、基于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 内核模式下触发中断的指令,也可以在用户模式下触发。但是,程序处于被调试状态时不会触发,只是忽略。
特性:
- 执行后,下一条指令的第一个字节被忽略,不当作指令的部分进行解析,起到混淆的作用。
- 执行后后面的执行部分无法通过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; }