Windows 下常见的反调试方法
稍稍总结一下在Crack或Rervese中比较常见的一些反调试方法,实现起来也比较简单,之后有写的Demo源码参考,没有太大的难度。
①最简单也是最基础的,Windows提供的API接口:IsDebuggerPresent(),这API实际上就是访问PEB的BeingDebugged标志来判断是否处于调试状态。
if (IsDebuggerPresent()) //API接口 { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); }
②自己用汇编实现实现IsDebuggerPresent,如何获得PEB呢?在应用层,fs寄存器是指向当前线程的TEB结构的,而在内核层,fs寄存器指向PCR(Processor Control Region)的内存,其数据类型是KPCR。所以可以通过当前线程的TEB获得PEB,再取出BeingDebugged的值。
mov eax, fs:18h // TEB Self指针 mov eax, [eax+30h] // PEB movzx eax, [eax+2] // PEB->BeingDebugged
_asm { push eax; //TEB mov eax, fs:[0x30]; // PEB movzx eax, byte ptr[eax + 2]; //BeingDebugged mov dword ptr[Value], eax; //取值 pop eax; } if (Value) //判断 { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); }
讲个题外话,也是关于FS寄存器的前两天有人是问了我一个问题,说是怎么让dll只被特定的进程加载,了解fs寄存器之后就很简单了,在dll加载的DllMain()函数中对加载的当前进程进行判断,看是否是自己的目标进程,如果不是,就拒绝被加载。下面是一段测试代码,可以获得当前进程的进程名。
#include "windows.h" //这段测试代码可以获得当前进程名 #include "stdio.h" int main(void) { LPSTR name; __asm{ mov eax,fs:[0x18] //得到Teb 当线程运行于用户空间时段寄存器FS指向当前线程的TEB,FS:[0x18] 就是指在 //Self,其内容就是TEB的起点 mov eax,[eax+0x30] //Teb偏移0x30处指向Peb mov eax,[eax+0xc] //Peb偏移 0xc 处指向 进程加载的模块的信息 _PEB_LDR_DATA结构 mov eax,[eax+0xc] mov eax,[eax+0x30] mov name,eax } wprintf(L"%s\n",name); return 0; }
③NtGlobalFlag 也是类似于BeingDebugged的一个标志,在Peb中取出。如果是调试状态下,
NtGlobalFlag的值会是0x70,正常情况下不是。之前是利用fs寄存器获得的Peb基地址,这里使用Native API 的方法。
HANDLE hProcess = NULL; DWORD ProcessId = 0; PROCESS_BASIC_INFORMATION Pbi; PFNZwQueryInformationProcess pfunc_ZwQueryInformationProcess = NULL; ProcessId = GetCurrentProcessId(); hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, ProcessId ); if (hProcess != NULL) { HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_ZwQueryInformationProcess = (PFNZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess"); NTSTATUS Status = pfunc_ZwQueryInformationProcess( hProcess, ProcessBasicInformation, &Pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL ); if (NT_SUCCESS(Status)) { DWORD ByteRead = 0; WORD NtGlobalFlag = 0; bool bIsDebug = false; ULONG PebBase = (ULONG)Pbi.PebBaseAddress; if (ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x68), &NtGlobalFlag, 2, &ByteRead) && ByteRead == 2) { // PEB.NtGlobalFlag // +0x068 NtGlobalFlag : Uint4B if (NtGlobalFlag == 0x70) //判断是否处于调试状态 { bIsDebug = true; } } if (bIsDebug) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } } CloseHandle(hProcess);
④PEB.ProcessHeap.Flags 和ForceFlags
进程堆标志,系统创建进程时会将Flags置为0x02(HEAP_GROWABLE),将ForceFlags置为0。但是进程被调试时,这两个标志通常被设置为0x50000062h和0x40000060h。下面是检测代码
HANDLE hProcess = NULL; DWORD ProcessId = 0; PROCESS_BASIC_INFORMATION Pbi; PFNZwQueryInformationProcess pfunc_pZwQueryInformationProcess = NULL; ULONG Flags = 0; ULONG ForceFlags = 0; switch (g_WinVersion) { case WINDOWS_VERSION_XP: Flags = 0xC; ForceFlags = 0x10; break; case WINDOWS_VERSION_2K3_SP1_SP2: Flags = 0xC; ForceFlags = 0x10; break; case WINDOWS_VERSION_7: Flags = 0x40; ForceFlags = 0x44; break; } ProcessId = GetCurrentProcessId(); hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, ProcessId ); if (hProcess != NULL) { HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_pZwQueryInformationProcess = (PFNZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess"); NTSTATUS Status = pfunc_pZwQueryInformationProcess( hProcess, ProcessBasicInformation, &Pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL ); if (NT_SUCCESS(Status)) { DWORD ByteRead = 0; ULONG ProcessHeap = 0; DWORD HeapFlags = 0; DWORD ForceFlagsValue = 1; bool bIsDebug = false; ULONG PebBase = (ULONG)Pbi.PebBaseAddress; if (ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x18), &ProcessHeap, 4, &ByteRead)) { if (ReadProcessMemory(hProcess, (LPCVOID)(ProcessHeap + Flags), &HeapFlags, 4, &ByteRead)) { // PEB.ProcessHeap.Flags // nt!_PEB // +0x018 ProcessHeap : Ptr32 Void _HEAP // nt!_HEAP // WIN7 ProcessHeap 还有一个 Heap // +0x018 Heap : Ptr32 _HEAP // WIN7 // +0x040 Flags : Uint4B // +0x044 ForceFlags : Uint4B // WIN2K3 // +0x00C Flags : Uint4B // +0x010 ForceFlags : Uint4B // WINXP // +0x00C Flags : Uint4B // +0x010 ForceFlags : Uint4B if (HeapFlags != 2) //系统创建进程时会将其设置为HEAP_GROEABLE(0x2) { bIsDebug = true; } else { ReadProcessMemory(hProcess,(LPCVOID)(PebBase+ForceFlags),&ForceFlagsValue,4,&ByteRead); if (ForceFlagsValue != 0) { bIsDebug = true; } } } } if (bIsDebug) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } } CloseHandle(hProcess); }
⑤检测父进程
一般双击运行的进程的父进程都是explorer.exe,但是如果进程被调试,父进程则是调试器进程,也就是说如果父进程不是explorer.exe,就可以认为有调试器存在。
DWORD ExplorerId = 0; PROCESSENTRY32 pe32 = {0}; CString str; BOOL bIsDebug = FALSE; DWORD ProcessId = GetCurrentProcessId(); // 获取 Explorer 进程ID ::GetWindowThreadProcessId(::FindWindow(L"Progman", NULL), &ExplorerId); HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); if (hProcessSnap != INVALID_HANDLE_VALUE) { pe32.dwSize = sizeof(PROCESSENTRY32); if (Process32First(hProcessSnap, &pe32)) { do { if (ProcessId == pe32.th32ProcessID) { str.Format(L"进程ID:%d 父进程ID:%d Explorer进程ID:%d", ProcessId, pe32.th32ParentProcessID, ExplorerId); if (pe32.th32ParentProcessID != ExplorerId) { bIsDebug = TRUE; break; } } } while (Process32Next(hProcessSnap, &pe32)); } AfxMessageBox(str); } if (bIsDebug) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } CloseHandle (hProcessSnap);
⑥SeDebugPrivilege权限判断
默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过OD或者xdbg等调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,当调试进程被加载时SeDebugPrivilege也就被继承了。所以我们可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断可以用能否打开csrss.exe进程来判断
HANDLE hProcess = NULL; PROCESSENTRY32 Pe32 = {0}; HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hProcessSnap == INVALID_HANDLE_VALUE) { return; } Pe32.dwSize = sizeof(PROCESSENTRY32); if (Process32First(hProcessSnap, &Pe32)) { do { if (_wcsicmp(L"csrss.exe", Pe32.szExeFile) == 0) { HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, Pe32.th32ProcessID ); if (hProcess) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } CloseHandle(hProcess); } } while (Process32Next(hProcessSnap, &Pe32)); } CloseHandle(hProcessSnap);
⑦检测硬件断点,其实也不能算是完完全全对调试器的检测,只能说是检测硬件断点,通过设置SHE处理例程,然后自己手动触发异常来检测硬件断点。
#pragma data_seg(".Hardware") BOOL Hardwarei = FALSE; DWORD addr = 0; #pragma data_seg() #pragma comment(linker, "/section:.Hardware,RWS") LONG WINAPI HardwareExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { if (ExceptionInfo->ContextRecord->Dr0 != 0 || ExceptionInfo->ContextRecord->Dr1 != 0 || ExceptionInfo->ContextRecord->Dr2 != 0 || ExceptionInfo->ContextRecord->Dr3 != 0) { Hardwarei = TRUE; ExceptionInfo->ContextRecord->Dr0 = 0; ExceptionInfo->ContextRecord->Dr1 = 0; ExceptionInfo->ContextRecord->Dr2 = 0; ExceptionInfo->ContextRecord->Dr3 = 0; } // 设置新的eip 让程序调转到safe执行 ExceptionInfo->ContextRecord->Eip = addr; return EXCEPTION_CONTINUE_EXECUTION; } BOOL IsHardware() { LPTOP_LEVEL_EXCEPTION_FILTER lpsetun; lpsetun = ::SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)HardwareExceptionFilter); _asm { mov addr, offset safe; int 3; } safe: ::SetUnhandledExceptionFilter(lpsetun); return Hardwarei; } void CAntiDebugDlg::OnBnClickedButton12() { char *p = "aaaaaaaaaaaaaaddddddddddddddbbbbbbbbbbbb"; if (IsHardware()) { AfxMessageBox(L"检测到硬件执行断点"); } else { AfxMessageBox(L"没有检测到硬件执行断点"); } }
⑧上面是通过SHE来检测硬件断点,当然还可以通过VEH来检测,思路是一样的,差别就是SHE与VEH,一个是针对线程的,一个是针对进程的。
/************************************************************************/ /* 硬件执行断点,VEH */ /************************************************************************/ #pragma data_seg(".VehHardware") BOOL VehHardwarei = FALSE; DWORD Vehaddr; #pragma data_seg() #pragma comment(linker, "/section:.VehHardware,RWS") LONG NTAPI VehHardwareExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { if (ExceptionInfo->ContextRecord->Dr0 != 0 || ExceptionInfo->ContextRecord->Dr1 != 0 || ExceptionInfo->ContextRecord->Dr2 != 0 || ExceptionInfo->ContextRecord->Dr3 != 0) { VehHardwarei = TRUE; ExceptionInfo->ContextRecord->Dr0 = 0; ExceptionInfo->ContextRecord->Dr1 = 0; ExceptionInfo->ContextRecord->Dr2 = 0; ExceptionInfo->ContextRecord->Dr3 = 0; } // 设置新的eip 让程序调转到safe执行 ExceptionInfo->ContextRecord->Eip = Vehaddr; return EXCEPTION_CONTINUE_EXECUTION; } BOOL IsVehHardware() { PVOID VEHandle = ::AddVectoredExceptionHandler(1, VehHardwareExceptionFilter); _asm { mov Vehaddr, offset vehsafe; int 3; } vehsafe: if (VEHandle != NULL) { RemoveVectoredExceptionHandler(VEHandle); } return VehHardwarei; } void CAntiDebugDlg::OnBnClickedButton16() { // TODO: 在此添加控件通知处理程序代码 char *p = "aaaaaaaaaaaaaaddddddddddddddbbbbbbbbbbbb"; if (IsVehHardware()) { AfxMessageBox(L"检测到硬件执行断点"); } else { AfxMessageBox(L"没有检测到硬件执行断点"); } }
⑨利用异常处理 通过我们自己安装异常处理例程,然后手动触发异常,如果存在OD则不会走异常处理
/************************************************************************/ /* 异常处理判断是否存在OD */ /************************************************************************/ #pragma data_seg(".excep") BOOL Exceptioni = FALSE; #pragma data_seg() #pragma comment(linker, "/section:.excep,RWS") LONG WINAPI ExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { Exceptioni = TRUE; //进行异常处理,则不存在OD return EXCEPTION_CONTINUE_EXECUTION; } BOOL IsException() { ULONG OldProtect = 0; LPTOP_LEVEL_EXCEPTION_FILTER lpsetun; lpsetun = ::SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter); //安装自己的异常处理 LPVOID pBuff = ::VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE); *((PWORD)pBuff) = 0xc3; ::VirtualProtect(pBuff, 0x1000, PAGE_EXECUTE_READ | PAGE_GUARD, &OldProtect); _asm { call pBuff; //如果存在OD,会把这里的call 当成中断处理,不会进行异常处理 } ::SetUnhandledExceptionFilter(lpsetun); //将原来的异常处理恢复顶层 return Exceptioni; } void CAntiDebugDlg::OnBnClickedButton13() { // TODO: 在此添加控件通知处理程序代码 if (IsException()) { AfxMessageBox(L"没有检测到调试器"); } else { AfxMessageBox(L"检测到调试器"); } }
⑩单步异常检测
单步异常是调试器用来单步调试时使用,我们也可以用来检测调试器的存在,由我们自己设置标志寄存器的值,触发单步异常。如果存在调试器,这里的单步异常会由调试器进行处理,而不会走__except()异常处理,但这种检测方法的局限性就在于检测代码需要被调试器单步调试的时候才有效。扯个题外话,这里我还打算尝试一下C++11新支持的异常处理try{} /catch(){},没想到一弄直接就崩溃了,还是老老实实用__try/__except吧!
/************************************************************************/ /* 单步异常检测 */ /************************************************************************/ //这种方法需要下面的汇编代码在单步跟中的时候才有效 void CAntiDebugDlg::OnBnClickedButton14() { // TODO: 在此添加控件通知处理程序代码 __try { // 触发单步异常 _asm { pushfd; //标志寄存器入栈 or dword ptr [esp], 100h; // 将 TF =1 单步 popfd; } AfxMessageBox(L"检测到调试器"); //如果不触发异常,说明有调试器 } //catch(...) 用C++ 11的catch()还是没有原生__except强大,用catch(...)这里会崩溃 __except(EXCEPTION_EXECUTE_HANDLER) { AfxMessageBox(L"没有检测到调试器"); } }
11.into使调试器一直循环。使用溢出中断,在没有调试器的情况下会走异常处理,然后正常的执行下去,如果存在OD则会自己处理中断,导致一直在这里循环。其实这里用into的溢出中断和int 3的中断效果是一样的,这里采用的是平时使用较少的into溢出中断。
#pragma data_seg(".VehInto") DWORD VehintoAddr = 0; #pragma data_seg() #pragma comment(linker, "/section:.VehInto,RWS") LONG NTAPI VehIntoExceptionFilter( PEXCEPTION_POINTERS ExceptionInfo ) { // 设置新的eip 让程序调转到safe执行 ExceptionInfo->ContextRecord->Eip = VehintoAddr; return EXCEPTION_CONTINUE_EXECUTION; } void IsVehIntoBreak() { PVOID VEHandle = ::AddVectoredExceptionHandler(1, VehIntoExceptionFilter); _asm { // 存放异常处理之后的偏移地址 mov VehintoAddr, offset Intosafe; mov ecx, 1; } here: _asm { /* 如果存在OD调试器,这里会一直循环下去。 */ rol ecx, 1; //不停的循环左移,直至产生溢出,OF位为1 into; //溢出中断 ,会调用异常处理 jmp here; } Intosafe: if (VEHandle != NULL) { RemoveVectoredExceptionHandler(VEHandle); } } void CAntiDebugDlg::OnBnClickedButton15() { // TODO: 在此添加控件通知处理程序代码 IsVehIntoBreak(); }
12 . int 2d
/************************************************************************/ /* INT 2D */ /************************************************************************/ BOOL IsInt2d() { __try { __asm { int 2dh; inc eax;//any opcode of singlebyte.如果开启VT会崩溃 //;or u can put some junkcode,"0xc8"..."0xc2"..."0xe8"..."0xe9" } return TRUE; } __except(EXCEPTION_EXECUTE_HANDLER) { return FALSE; } } void CAntiDebugDlg::OnBnClickedButton21() { // TODO: 在此添加控件通知处理程序代码 if (IsInt2d()) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } }
13. 检验内存校验和
调试器(OD)在设置断点会改变内存属性,改写为0xCC,我们需要检验自身的某一个段内存的校验和就可以判断是否有调试器的存在
/************************************************************************/ /* CheckSum */ /************************************************************************/ BOOL CheckSum() { BOOL bFoundOD; bFoundOD = FALSE; DWORD CHECK_SUM = 5555; //正确校验值 DWORD dwAddr; dwAddr = (DWORD)CheckSum; __asm { ; 检测代码开始 mov esi, dwAddr; mov ecx, 100; xor eax, eax; checksum_loop: movzx ebx, byte ptr [esi]; add eax, ebx; rol eax, 1; inc esi; loop checksum_loop; cmp eax, CHECK_SUM; jz ODNotFound; mov bFoundOD, 1; ODNotFound: } return bFoundOD; } void CAntiDebugDlg::OnBnClickedButton27() { // TODO: 在此添加控件通知处理程序代码 if (CheckSum()) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } }
14. 保护页异常
当执行一个属性为PAGE_GUARD的页面时,触发EXCEPTION_GUARD_PAGE的异常,如果存在调试器一般都是由调试器处理了。
/************************************************************************/ /* “保护页异常” */ /************************************************************************/ //当应用程序尝试执行保护页内的代码时,将会产生一个EXCEPTION_GUARD_PAGE(0x80000001)异常 BOOL IsGuardPages() { SYSTEM_INFO sSysInfo; DWORD dwPageSize = 0; DWORD OldProtect = 0; GetSystemInfo(&sSysInfo); dwPageSize = sSysInfo.dwPageSize; LPVOID lpvBase = VirtualAlloc(NULL, dwPageSize, MEM_COMMIT, PAGE_READWRITE); if (lpvBase == NULL) { return FALSE; } PBYTE lptmpB = (PBYTE)lpvBase; *lptmpB = 0xc3; //retn VirtualProtect(lpvBase, dwPageSize, PAGE_EXECUTE_READ | PAGE_GUARD, &OldProtect); __try { __asm call dword ptr[lpvBase]; VirtualFree(lpvBase, 0, MEM_RELEASE); return TRUE; } __except(EXCEPTION_EXECUTE_HANDLER) { VirtualFree(lpvBase, 0, MEM_RELEASE); return FALSE; } } void CAntiDebugDlg::OnBnClickedButton25() { // TODO: 在此添加控件通知处理程序代码 if (IsGuardPages()) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } }
14. 还有就是关于调试对象的检测,这里就牵扯到内核层的问题,一些基础内容就不进行赘述了。如果往深了研究可以学习系统的内核调试引擎,折腾TX的游戏TP反双机调试。这里只给出一些在应用层利用的比较简单的检测方法。
检测调试对象DebugObject
/************************************************************************/ /* ZwQueryObject */ /************************************************************************/ #ifndef STATUS_INFO_LENGTH_MISMATCH #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) #endif void CAntiDebugDlg::OnBnClickedButton24() { POBJECT_TYPES_INFORMATION pTypesInfo = NULL; ULONG dwSize = 0; PFNZwQueryObject pfunc_ZwQueryObject; HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_ZwQueryObject = (PFNZwQueryObject)GetProcAddress(hModule, "ZwQueryObject"); if (pfunc_ZwQueryObject) { NTSTATUS Status = pfunc_ZwQueryObject( NULL, ObjectAllTypesInformation, NULL, 0, &dwSize ); if (Status == STATUS_INFO_LENGTH_MISMATCH) { pTypesInfo = (POBJECT_TYPES_INFORMATION)VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (pTypesInfo == NULL) return; Status = pfunc_ZwQueryObject( NULL, ObjectAllTypesInformation, pTypesInfo, dwSize, &dwSize ); if (!NT_SUCCESS(Status)) { AfxMessageBox(L"查询错误"); VirtualFree (pTypesInfo, 0, MEM_RELEASE); return; } for (UINT i = 0; i < pTypesInfo->NumberOfTypes; i++) { if (pTypesInfo->TypeInformation[i].TypeName.Buffer != NULL) { if (_wcsicmp(pTypesInfo->TypeInformation[i].TypeName.Buffer, L"DebugObject") == 0) { AfxMessageBox(L"发现OD"); VirtualFree (pTypesInfo, 0, MEM_RELEASE); return; } } } AfxMessageBox(L"没有OD!"); VirtualFree (pTypesInfo, 0, MEM_RELEASE); } } }
DebugObjectHandle
/************************************************************************/ /* ProcessDebugObjectHandle */ /************************************************************************/ void CAntiDebugDlg::OnBnClickedButton22() { // TODO: 在此添加控件通知处理程序代码 HANDLE hProcess = NULL; DWORD dwResult; PFNZwQueryInformationProcess pfunc_ZwQueryInformationProcess; HMODULE hModule = LoadLibrary(L"ntdll.dll"); pfunc_ZwQueryInformationProcess = (PFNZwQueryInformationProcess)GetProcAddress(hModule, "ZwQueryInformationProcess"); pfunc_ZwQueryInformationProcess( GetCurrentProcess(), ProcessDebugObjectHandle, &dwResult, 4, NULL ); if (dwResult != 0) { AfxMessageBox(L"检测到调试器"); } else { AfxMessageBox(L"没有检测到调试器"); } }
一篇关于Anti不错的文章,不过是英文的,可以参考:anti-unpackers.pdf
http://www.openrce.org/reference_library/anti_reversing_view/34/INT%202D%20Debugger%20Detection/