# Anti-Debugging ## Debug Flags Debug Flags dựa trên trạng thái của những flag đặc biệt mà có thể chỉ ra rằng tiến trình đó đang bị debug ### 1. Using Win32 API #### 1.1. IsDebuggerPresent() [IsDebuggerPresent()](https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-isdebuggerpresent) trả về giá trị khác 0 thì process đang bị debug bởi một `user-mode debugger` Assembly Code x86 ```masm call IsDebuggerPresent test al, al jne being_debugged ... being_debugged: push 1 call ExitProcess ``` #### 1.2. CheckRemoteDebuggerPresent() Hàm [CheckRemoteDebuggerPresent()](https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-checkremotedebuggerpresent) sẽ đặt trường `pbDebuggerPresent` thành **True** hoặc **False** dựa trên trạng thái của process được chỉ tới bằng con trỏ `hProcess`. Nếu là True => Process bị debug Assembly Code x86 ```masm lea eax, [bDebuggerPresent] push eax push -1 ; GetCurrentProcess() call CheckRemoteDebuggerPresent cmp [bDebuggerPresent], 1 jz being_debugged ... being_debugged: push -1 call ExitProcess ``` #### 1.3. NtQueryInformationProcess() Hàm [NtQueryInformationProcess()](https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess) lấy các thông tin cần thiết trong một process. Trường `ProcessInformationClass` sẽ chỉ ra thông tin chúng ta muốn lấy trong process và đưa thông tin đó vào trường `ProcessInformation` ##### 1.3.1. ProcessDebugPort Để biết process có bị debug hay không. Chúng ta sẽ đặt trường `ProcessInformationClass` có giá trị là `ProcessDebugPort (7)`. Sau khi gọi hàm `NtQueryInformationProcess()` thì ta kiểm tra trường `ProcessInformation` để biết xem process có bị debug hay không. Nếu giá trị = -1 => Process bị debug Assembly Code x86 ```masm lea eax, [dwReturned] push eax ; ReturnLength push 4 ; ProcessInformationLength lea ecx, [dwProcessDebugPort] push ecx ; ProcessInformation push 7 ; ProcessInformationClass push -1 ; ProcessHandle call NtQueryInformationProcess inc dword ptr [dwProcessDebugPort] jz being_debugged ... being_debugged: push -1 call ExitProcess ``` ##### 1.3.2. ProcessDebugFlags Trường `ProcessDebugFlags (0x1f)` là một lớp không có trong tài liệu. Khi sử dụng trường này, nó sẽ lấy giá trị từ trường `NoDebugInherit` của structure [EPROCESS](https://www.nirsoft.net/kernel_struct/vista/EPROCESS.html#:~:text=ULONG%20NoDebugInherit%3A%201%3B). Nếu giá trị trả về = 0 => Process bị debug Assembly Code x86 ```masm lea eax, [dwReturned] push eax ; ReturnLength push 4 ; ProcessInformationLength lea ecx, [dwProcessDebugPort] push ecx ; ProcessInformation push 1Fh ; ProcessInformationClass push -1 ; ProcessHandle call NtQueryInformationProcess cmp dword ptr [dwProcessDebugPort], 0 jz being_debugged ... being_debugged: push -1 call ExitProcess ``` ##### 1.3.3. ProcessDebugObjectHandle Khi bắt đầu debug, kernel object `debug object` được khởi tạo. Điều đó giúp ta kiểm tra được process có bị debug hay không qua lớp `ProcessDebugObjectHandle (0x1e)`. Nếu giá trị trả về != 0 thì process bị debug Assembly Code x86 ```masm lea eax, [dwReturned] push eax ; ReturnLength push 4 ; ProcessInformationLength lea ecx, [hProcessDebugObject] push ecx ; ProcessInformation push 1Eh ; ProcessInformationClass push -1 ; ProcessHandle call NtQueryInformationProcess cmp dword ptr [hProcessDebugObject], 0 jnz being_debugged ... being_debugged: push -1 call ExitProcess ``` #### 1.4. RtlQueryProcessHeapInformation() Hàm `ntdll!RtlQueryProcessDebugInformation()` không phải hàm api chính thức. Khi gọi hàm, trong struct [RTL_DEBUG_INFORMATION](https://docs.rs/ntapi/latest/ntapi/ntrtl/struct.RTL_DEBUG_INFORMATION.html#structfield.ProcessHeap) chứa [RTL_PROCESS_HEAPS](https://docs.rs/ntapi/latest/ntapi/ntrtl/struct.RTL_PROCESS_HEAPS.html), rồi trong struct đó có [RTL_HEAP_INFORMATION](https://docs.rs/ntapi/latest/ntapi/ntrtl/struct.RTL_HEAP_INFORMATION.html#structfield.Flags) và ta tìm được trường `Flags`. Nếu Flags != 2 thì process bị debug C/C++ Code ```cpp bool Check() { ntdll::PDEBUG_BUFFER pDebugBuffer = ntdll::RtlCreateQueryDebugBuffer(0, FALSE); if (!SUCCEEDED(ntdll::RtlQueryProcessHeapInformation((ntdll::PRTL_DEBUG_INFORMATION)pDebugBuffer))) return false; ULONG dwFlags = ((ntdll::PRTL_PROCESS_HEAPS)pDebugBuffer->HeapInformation)->Heaps[0].Flags; return dwFlags & ~HEAP_GROWABLE; # HEAP_GROWABLE = 2. So if return != 0 => process being debugged } ``` #### 1.5. RtlQueryProcessDebugInformation() Hàm [RtlQueryProcessDebugInformation()](https://docs.rs/ntapi/latest/ntapi/ntrtl/fn.RtlQueryProcessDebugInformation.html) đọc các trường nhất định thậm chí có cả trường `flags` trong struct [RTL_DEBUG_INFORMATION](https://docs.rs/ntapi/latest/ntapi/ntrtl/struct.RTL_DEBUG_INFORMATION.html) #### 1.6. NtQuerySystemInformation() Chúng ta gọi hàm [NtQuerySystemInformation()](https://docs.rs/ntapi/latest/ntapi/ntexapi/fn.NtQuerySystemInformation.html) có tham số đầu vào là SYSTEM_INFORMATION_CLASS = lớp SystemKernelDebuggerInformation (0x23). Đây là lớp không có trong tài liệu. Khi đó trường `Systemlnformation` là con trỏ tới struct [SYSTEM_KERNEL_DEBUGGER_INFORMATION](https://docs.rs/ntapi/latest/ntapi/ntexapi/struct.SYSTEM_KERNEL_DEBUGGER_INFORMATION.html). Nếu trường `KernelDebuggerEnabled` = **True** và `KernelDebuggerNotPresent` = **False** , thì process bị debug ### 2. Manual checks #### 2.1. PEB!BeingDebugged Flag Kiểm tra cờ `BeingDebugged` của `PEB` (offset 0x02), nếu giá trị != 0 thì process bị debug Assembly Code x86 ```masm! mov eax, fs:[30h] cmp byte ptr [eax+2], 0 jne being_debugged ``` #### 2.2. NtGlobalFlag Kiểm tra trường `NtGlobalFlag` của `PEB` (offset: x86: 0x68; x64: 0xBC). Nếu process được tạo bởi trình gỡ lỗi thì những cờ sau được đặt: - FLG_HEAP_ENABLE_TAIL_CHECK (0x10) - FLG_HEAP_ENABLE_FREE_CHECK (0x20) - FLG_HEAP_VALIDATE_PARAMETERS (0x40) Nếu giá trị trả về = 0x70 thì process bị debug Assembly Code x86 ```masm mov eax, fs:[30h] mov al, [eax+68h] and al, 70h cmp al, 70h jz being_debugged ``` #### 2.3. Heap Flags Trong heap có 2 trường là `Flags` và `ForceFlag`. Giá trị của `Flags` thường = `HEAP_GROWABLE (2)`; `ForceFlags` = 0. Vậy nên nếu 2 trường này có giá trị khác thì process bị debug #### 2.4. Heap Protection Nếu cờ `HEAP_TAIL_CHECKING_ENABLED` được đặt trong `NtGlobalFlag` thì 8 byte 0xAB (với Win 32bit) hoặc 16 byte 0xAB (với Win 64bit) sẽ được thêm vào cuối heap block. Vậy nên nếu thấy ở cuối heap block có thì chương trình bị debug Ngoài ra, nếu chương trình bị debug, thì nếu cần điềm thêm byte vào cuối heap block, chương trình sẽ thêm các byte 0xFEEEFEEE đến `memory block` tiếp theo C/C++ Code ```cpp bool Check() { PROCESS_HEAP_ENTRY HeapEntry = { 0 }; do { if (!HeapWalk(GetProcessHeap(), &HeapEntry)) return false; } while (HeapEntry.wFlags != PROCESS_HEAP_ENTRY_BUSY); PVOID pOverlapped = (PBYTE)HeapEntry.lpData + HeapEntry.cbData; return ((DWORD)(*(PDWORD)pOverlapped) == 0xABABABAB); } ``` #### 2.5. Check KUSER_SHARED_DATA structure Struct này nằm cố định ở 0x7ffe02d4 (Thực chất là 0x7ffe0000 + 0x2d4 ). Nếu giá trị ở đó là 0x01 hoặc 0x02 thì process bị debug C/C++ Code ```cpp bool check_kuser_shared_data_structure() { unsigned char b = *(unsigned char*)0x7ffe02d4; return ((b & 0x01) || (b & 0x02)); } ``` ## Object Handles Đây là cách sử dụng `kernel objects handles` để phát hiện trình gỡ lỗi. Các tham số của `kernel objects` sẽ bị thay đổi khi debug hoặc có những `kernel objects` được tạo ra khi bắt đầu debug ### 1. OpenProcess() Một số trình gỡ lỗi có thể bị phát hiện bằng cách sử dụng hàm `kernel32!OpenProcess()` trên process csrss.exe. Nếu gọi hàm thành công thì chương trình bị debug C/C++ Code ```cpp typedef DWORD (WINAPI *TCsrGetProcessId)(VOID); bool Check() { HMODULE hNtdll = LoadLibraryA("ntdll.dll"); if (!hNtdll) return false; TCsrGetProcessId pfnCsrGetProcessId = (TCsrGetProcessId)GetProcAddress(hNtdll, "CsrGetProcessId"); if (!pfnCsrGetProcessId) return false; HANDLE hCsr = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pfnCsrGetProcessId()); if (hCsr != NULL) { CloseHandle(hCsr); return true; } else return false; } ``` ### 2. CreateFile() Khi debug, handle của tệp được debug được lưu trong cấu trúc `CREATE_PROCESS_DEBUG_INFO`. Do trình gỡ lỗi đang đọc thông tin từ tệp này nên nếu trình gỡ lỗi không đóng handle thì ta không thể mở file được. Vậy nếu không mở được tệp của process hiện tại không được thì process bị debug ### 3. CloseHandle() Khi process đang chạy trong trình gỡ lỗi thì khi ta gọi hàm `CloseHandle()`Thì exeption `EXCEPTION_INVALID_HANDLE (0xC0000008)` sẽ được return. C/C++ Code ```cpp bool Check() { __try { CloseHandle((HANDLE)0xDEADBEEF); return false; } __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return true; } } ``` ### 4. LoadLibrary() KHi ta tải một tệp lên bằng hàm `kernel32!LoadLibraryW()`, Handle của file sẽ được lưu trữ trong cấu trúc `LOAD_DLL_DEBUG_INFO `. Nếu trình gỡ lỗi không đóng trình xử lý thì sẽ không mở được tệp. Vậy nên nếu lệnh gọi hàm kernel32!CreateFileA() không thành công tức là chương trình đang bị debug C/C++ Code ```cpp bool Check() { CHAR szBuffer[] = { "C:\\Windows\\System32\\calc.exe" }; LoadLibraryA(szBuffer); return INVALID_HANDLE_VALUE == CreateFileA(szBuffer, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); } ``` ### 5. NtQueryObject() Khi debug, một `kernel object` là "debug object" được tạo. Để check chương trình có bị debug hay không thì ta gọi hàm NtQueryObject() để liệt kê các object, nếu có `DebugObject` thì chương trình đang bị debug ## Exception ### 1. UnhandledExceptionFilter() UnhandledExceptionFilter() được sử dụng để đặt hàm xử lý ngoại lệ. Nếu có ngoại lệ xảy ra, chương trình sẽ gọi hàm `UnhandledExceptionFilter() ` để xử lý. Nếu không thì chương trình đang bị debug Assembly Code x86 ```asm include 'win32ax.inc' .code start: jmp begin not_debugged: invoke MessageBox,HWND_DESKTOP,"Not Debugged","",MB_OK invoke ExitProcess,0 begin: invoke SetUnhandledExceptionFilter, not_debugged int 3 jmp being_debugged being_debugged: invoke MessageBox,HWND_DESKTOP,"Debugged","",MB_OK invoke ExitProcess,0 .end start ``` ### 2. RaiseException() Để phát hiện debug thì ta gọi hàm kernel32!RaiseException() để kiểm tra xem handle của các expeptions `DBC_CONTROL_C hoặc DBG_RIPEVENT` có được chuyển đến trình xử lý không. Nếu trình xử lý ngoại lệ không được gọi thì process bị debug C/C++ Code ```cpp bool Check() { __try { RaiseException(DBG_CONTROL_C, 0, 0, NULL); return true; } __except(DBG_CONTROL_C == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return false; } } ``` ## Timing Khi debug, sẽ có độ trễ rất lớn. Ta có thể đo và so sanh độ trễ gốc và độ trễ thực tế bằng những phương pháp sau: ### 1. RDPMC/RDTSC Những instructions này yêu cầu cờ PCE đặt trong thanh ghi CR4 **LƯU Ý**: RDPMC chỉ sử dụng được ở trong Kernel Mode C/C++ Code ```cpp bool IsDebugged(DWORD64 qwNativeElapsed) { ULARGE_INTEGER Start, End; __asm { xor ecx, ecx rdpmc mov Start.LowPart, eax mov Start.HighPart, edx } // ... some work __asm { xor ecx, ecx rdpmc mov End.LowPart, eax mov End.HighPart, edx } return (End.QuadPart - Start.QuadPart) > qwNativeElapsed; } ``` ### 2. GetLocalTime() C/C++ Code ```cpp bool IsDebugged(DWORD64 qwNativeElapsed) { SYSTEMTIME stStart, stEnd; FILETIME ftStart, ftEnd; ULARGE_INTEGER uiStart, uiEnd; GetLocalTime(&stStart); // ... some work GetLocalTime(&stEnd); if (!SystemTimeToFileTime(&stStart, &ftStart)) return false; if (!SystemTimeToFileTime(&stEnd, &ftEnd)) return false; uiStart.LowPart = ftStart.dwLowDateTime; uiStart.HighPart = ftStart.dwHighDateTime; uiEnd.LowPart = ftEnd.dwLowDateTime; uiEnd.HighPart = ftEnd.dwHighDateTime; return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed; } ``` ### 3. GetSystemTime() C/C++ Code ```cpp bool IsDebugged(DWORD64 qwNativeElapsed) { SYSTEMTIME stStart, stEnd; FILETIME ftStart, ftEnd; ULARGE_INTEGER uiStart, uiEnd; GetSystemTime(&stStart); // ... some work GetSystemTime(&stEnd); if (!SystemTimeToFileTime(&stStart, &ftStart)) return false; if (!SystemTimeToFileTime(&stEnd, &ftEnd)) return false; uiStart.LowPart = ftStart.dwLowDateTime; uiStart.HighPart = ftStart.dwHighDateTime; uiEnd.LowPart = ftEnd.dwLowDateTime; uiEnd.HighPart = ftEnd.dwHighDateTime; return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed; } ``` ### 4. GetTickCount() C/C++ Code ```cpp bool IsDebugged(DWORD dwNativeElapsed) { DWORD dwStart = GetTickCount(); // ... some work return (GetTickCount() - dwStart) > dwNativeElapsed; } ``` ### 5. ZwGetTickCount() / KiGetTickCount() C/C++ Code ```cpp bool IsDebugged(DWORD64 qwNativeElapsed) { ULARGE_INTEGER Start, End; __asm { int 2ah mov Start.LowPart, eax mov Start.HighPart, edx } // ... some work __asm { int 2ah mov End.LowPart, eax mov End.HighPart, edx } return (End.QuadPart - Start.QuadPart) > qwNativeElapsed; } ``` ### 6. QueryPerformanceCounter() C/C++ Code ```cpp bool IsDebugged(DWORD64 qwNativeElapsed) { LARGE_INTEGER liStart, liEnd; QueryPerformanceCounter(&liStart); // ... some work QueryPerformanceCounter(&liEnd); return (liEnd.QuadPart - liStart.QuadPart) > qwNativeElapsed; } ``` ### 7. timeGetTime() C/C++ Code ```cpp bool IsDebugged(DWORD dwNativeElapsed) { DWORD dwStart = timeGetTime(); // ... some work return (timeGetTime() - dwStart) > dwNativeElapsed; } ``` ## Process Memory ### 1. Breakpoints #### 1.1. Software Breakpoints (INT3) Khi đặt breakpoint để debug, debugger sẽ chòn INT 3 (opcode 0xCC). Nên ta kiểm tra những byte `0xCC` . Nếu bất thường thì bị debug C/C++ Code ```cpp bool CheckForSpecificByte(BYTE cByte, PVOID pMemory, SIZE_T nMemorySize = 0) { PBYTE pBytes = (PBYTE)pMemory; for (SIZE_T i = 0; ; i++) { // Break on RET (0xC3) if we don't know the function's size if (((nMemorySize > 0) && (i >= nMemorySize)) || ((nMemorySize == 0) && (pBytes[i] == 0xC3))) break; if (pBytes[i] == cByte) return true; } return false; } bool IsDebugged() { PVOID functionsToCheck[] = { &Function1, &Function2, &Function3, }; for (auto funcAddr : functionsToCheck) { if (CheckForSpecificByte(0xCC, funcAddr)) return true; } return false; } ``` #### 1.2. Anti-Step-Over Khi ta trình gỡ lỗi Step Over qua lệnh gọi hàm. Thì ta kiểm tra xem software breakpoint có nằm ở địa chỉ trả về hay không. Nếu có thì chương trình bị debug ##### 1.2.1. Direct Memory Modification Kiểm tra địa chỉ trả về có 0xCC không. Nếu có thì thay bằng 0x90(Nop) C/C++ Code ```cpp #include <intrin.h> #pragma intrinsic(_ReturnAddress) void foo() { // ... PVOID pRetAddress = _ReturnAddress(); if (*(PBYTE)pRetAddress == 0xCC) // int 3 { DWORD dwOldProtect; if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { *(PBYTE)pRetAddress = 0x90; // nop VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect); } } // ... } ``` ##### 1.2.2. ReadFile() Dùng hàm ReadFile() để patch code ở địa chỉ trả về C/C++ Code ```cpp #include <intrin.h> #pragma intrinsic(_ReturnAddress) void foo() { // ... PVOID pRetAddress = _ReturnAddress(); if (*(PBYTE)pRetAddress == 0xCC) // int 3 { DWORD dwOldProtect, dwRead; CHAR szFilePath[MAX_PATH]; HANDLE hFile; if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { if (GetModuleFileNameA(NULL, szFilePath, MAX_PATH)) { hFile = CreateFileA(szFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (INVALID_HANDLE_VALUE != hFile) ReadFile(hFile, pRetAddress, 1, &dwRead, NULL); } VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect); } } // ... } ``` ##### 1.2.3. WriteProcessMemory() Dùng hàm WriteProcessMemory() để patch code ở địa chỉ trả về C/C++ Code ```cpp #include <intrin.h> #pragma intrinsic(_ReturnAddress) void foo() { // ... BYTE Patch = 0x90; PVOID pRetAddress = _ReturnAddress(); if (*(PBYTE)pRetAddress == 0xCC) { DWORD dwOldProtect; if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { WriteProcessMemory(GetCurrentProcess(), pRetAddress, &Patch, 1, NULL); VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect); } } // ... } ``` ##### 1.2.4. Toolhelp32ReadProcessMemory() Hàm này cho phép ta chống Step-Over C/C++ Code ```cpp #include <TlHelp32.h> bool foo() { // .. PVOID pRetAddress = _ReturnAddress(); BYTE uByte; if (FALSE != Toolhelp32ReadProcessMemory(GetCurrentProcessId(), _ReturnAddress(), &uByte, sizeof(BYTE), NULL)) { if (uByte == 0xCC) ExitProcess(0); } // .. } ``` #### 1.3. Memory Breakpoints Cấp phát một bộ đệm chứa lệnh RET, sau đó đánh dấu là guard page. Nếu bị debug, chương trình sẽ chạy lệnh RET và nhảy đến địa chỉ đã push vào stack C/C++ Code ```cpp bool IsDebugged() { DWORD dwOldProtect = 0; SYSTEM_INFO SysInfo = { 0 }; GetSystemInfo(&SysInfo); PVOID pPage = VirtualAlloc(NULL, SysInfo.dwPageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == pPage) return false; PBYTE pMem = (PBYTE)pPage; *pMem = 0xC3; // Make the page a guard page if (!VirtualProtect(pPage, SysInfo.dwPageSize, PAGE_EXECUTE_READWRITE | PAGE_GUARD, &dwOldProtect)) return false; __try { __asm { mov eax, pPage push mem_bp_being_debugged jmp eax } } __except(EXCEPTION_EXECUTE_HANDLER) { VirtualFree(pPage, NULL, MEM_RELEASE); return false; } mem_bp_being_debugged: VirtualFree(pPage, NULL, MEM_RELEASE); return true; } ``` #### 1.4. Hardware Breakpoints Nếu các thanh ghi gỡ lỗi DR0, DR1, DR2 và DR3 có giá trị != 0 thì chương trình bị debug C/C++ Code ```cpp bool IsDebugged() { CONTEXT ctx; ZeroMemory(&ctx, sizeof(CONTEXT)); ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; if(!GetThreadContext(GetCurrentThread(), &ctx)) return false; return ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3; } ``` ### 2. Other memory checks #### 2.1. NtQueryVirtualMemory() Kiểm tra các trường Shared và ShareCount của Khối Working Set để tìm trang có mã. Nếu debug thì software breakpoint sẽ được đặt các trường này và trả về giá trị = 0 #### 2.2. Detecting a function patch Chúng ta kiểm tra xem hàm kernel32!IsDebuggerPresent() có bị sửa đổi hay không. Nếu có thì chương trình bị debug #### 2.3. Patch ntdll!DbgBreakPoint() Nếu chúng ta xóa breakpoint bên trong ntdll!DbgBreakPoint() thì trình gỡ lỗ sẽ không xâm nhập vào luồng và sẽ thoát #### 2.4. Patch ntdll!DbgUiRemoteBreakin() Patch ntdll!DbgUiRemoteBreakin() hoặc kernel32!TerminateProcess() để chương trình sẽ tự động thoát nếu bị gắn debugger #### 2.5 Performing Code Checksums Kiểm tra code có bị sửa hay không. Nếu có thì chương trình bị debug ## Assembly instructions ### 1. INT 3 Khi gặp INT3 thì exception EXCEPTION_BREAKPOINT (0x80000003) sẽ được tạo ra và được gọi. Nếu chương trình bị debug thì expeption không được gọi ### 2. INT 2D Tương tự như INT3. Chỉ khác là nếu có debbugger thì chương trình sẽ nhảy đến EIP + 1 ### 3. DebugBreak Nếu chương trình không debug thì sẽ được chuyển tới trình xử lý ngoại lê. Nếu không, việc thực thi bị debugger chặn lại ### 4. ICE Opcode 0xF1 gây ra exception EXCEPTION_SINGLE_STEP (0x80000004). Nếu tiếp tục thực thi câu lệnh sau ICE thì chương trình bị debug ### 5. Stack Segment Register Việc so sánh giá trị của Trap Flag sẽ cho ta biết chương trình có bị debug hay không. Nếu giá trị khác 1 thì chương trình bị debug C/C++ Code ```cpp bool IsDebugged() { bool bTraced = false; __asm { push ss pop ss pushf test byte ptr [esp+1], 1 jz movss_not_being_debugged } bTraced = true; movss_not_being_debugged: // restore stack __asm popf; return bTraced; } ``` ### 6. Instruction Counting Đặt các hardware breakpoint tạo ra những EXCEPTION_SINGLE_STEP trong 1 chuỗi. Dùng thanh ghi để đếm số lần gọi Exception handler. Nếu khác với giá trị của chúng ta thì chương trình đang bị debug. ### 7. POPF and Trap Flag Khi Trap Flag được đặt, exception SINGLE_STEP sẽ được tạo.Nếu Trap Flag bị debugger xóa thì chúng ta không thấy exception ### 8. Instruction Prefixes Assembly Code x86 ```masm IsDebugged() { __try { // 0xF3 0x64 disassembles as PREFIX REP: __asm __emit 0xF3 __asm __emit 0x64 // One byte INT 1 __asm __emit 0xF1 return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } } ``` Trong trường hợp này. Nếu chúng ta thực thi trong OllyDbg thì sau byte F3, ta sẽ đi đến cuối khối lệnh này. Còn nếu chương trình không bị debug thì sẽ xuất hiện 1 exception và sẽ nhảy vào hàm xử lý ## Direct debugger interaction Những kĩ thuật bên dưới cho phép tương tác với process gốc để phát hiện những điểm không nhất quán ### 1. Self-Debugging Chúng ta sử dụng 3 hàm bên dưới để đính kèm dưới dạng debugger vào process đang chạy: `kernel32!DebugActiveProcess()` `ntdll!DbgUiDebugActiveProcess()` `ntdll!NtDebugActiveProcess()` Nếu không đính kèm được tức process bị debug ### 2. GenerateConsoleCtrlEvent() Hàm này cho phép ta tạo ra 1 event liên quan đến việc nhấn phím Ctrl+C hoặc Ctrl+Break trong cửa sổ console, và gọi hàm `kernel32!ExitProcess()` để kết thúc process. Tuy nhiên ta có thể sử dụng hàm `SetConsoleCtrlHandler()` để thay vì kết thúc thì ta có thể điều khiển việc tiếp tục chương trình sau khi nhấn Ctrl+C hoặc Ctrl+Break. Khi ta ấn Ctrl+C thì hệ thống sẽ tạo ra exception `DBG_CONTROL_C`. Nếu exception có giá trị TRUE thì process bị debug ### 3. BlockInput() Hàm này chặn tất cả các event từ chuột và bàn phím. Vậy nên nếu trả về giá trị TRUE thì tức là process đang bị debug ### 4. NtSetInformationThread() Ta có thể sử dụng hàm này để ẩn 1 thread khỏi debugger. Sau khi ẩn, nó sẽ tiếp tục chạy nhưng debugger không nhận được các event từ thread này khiến cho process gặp sự cố và debugger bị kẹt. C/C++ Code ```cpp #define NtCurrentThread ((HANDLE)-2) bool AntiDebug() { NTSTATUS status = ntdll::NtSetInformationThread( NtCurrentThread, ntdll::THREAD_INFORMATION_CLASS::ThreadHideFromDebugger, NULL, 0); return status >= 0; } ``` ### 5. EnumWindows() and SuspendThread() Gọi hàm `user32!EnumWindows()` hoặc `user32!EnumThreadWindows()` để liệt kê tất cả các cửa sổ đang chạy, tìm kiếm cửa sổ có ID là ID của process gốc và kiểm tra title. Nếu giống title của debugger thì ta sử dụng SuspendThread() hoặc ntdll!NtSuspendThread() để tạm dừng thread ### 6. SwitchDesktop() Lợi dụng việc hỗ trợ nhiều destop trên windows, ta chuyển input sang desktop khác khiến cho không thể tương tác được với debugger C/C++ Code ```cpp BOOL Switch() { HDESK hNewDesktop = CreateDesktopA( m_pcszNewDesktopName, NULL, NULL, 0, DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP, NULL); if (!hNewDesktop) return FALSE; return SwitchDesktop(hNewDesktop); } ``` ### 7. OutputDebugString() Kĩ thuật này chỉ hoạt động với các phiên bản Windows Vista trở xuống. Khi ta gọi hàm này mà không xảy ra lỗi thì chương trình bị debug ## Misc ### 1. FindWindow() Kĩ thuật này liệt kê tất cả các lớp cửa sổ trong hệ thống và so sanh với các lớp debugger đã biết C/C++ Code ```cpp const std::vector<std::string> vWindowClasses = { "antidbg", "ID", // Immunity Debugger "ntdll.dll", // peculiar name for a window class "ObsidianGUI", "OLLYDBG", "Rock Debugger", "SunAwtFrame", "Qt5QWindowIcon" "WinDbgFrameClass", // WinDbg "Zeta Debugger", }; bool IsDebugged() { for (auto &sWndClass : vWindowClasses) { if (NULL != FindWindowA(sWndClass.c_str(), NULL)) return true; } return false; } ``` ### 2. Parent Process Check Do khi ta mở bằng cách nhấn đúp vào tệp thì có shell process là “explorer.exe”. Vậy nên để phát hiện debugger thì ta so sánh PID của process gốc với PID của “explorer.exe”. #### 2.1. NtQueryInformationProcess() Ta lấy handle của cửa sổ shell process bằng cách sử dụng user32!GetShellWindow() và lấy ID process của nó bằng cách gọi user32!GetWindowThreadProcessId().Sau đó, ID process gốc có thể được lấy từ cấu trúc PROCESS_BASIC_INFORMATION bằng cách gọi ntdll!NtQueryInformationProcess() với lớp ProcessBasicInformation. C/C++ Code ```cpp bool IsDebugged() { HWND hExplorerWnd = GetShellWindow(); if (!hExplorerWnd) return false; DWORD dwExplorerProcessId; GetWindowThreadProcessId(hExplorerWnd, &dwExplorerProcessId); ntdll::PROCESS_BASIC_INFORMATION ProcessInfo; NTSTATUS status = ntdll::NtQueryInformationProcess( GetCurrentProcess(), ntdll::PROCESS_INFORMATION_CLASS::ProcessBasicInformation, &ProcessInfo, sizeof(ProcessInfo), NULL); if (!NT_SUCCESS(status)) return false; return (DWORD)ProcessInfo.InheritedFromUniqueProcessId != dwExplorerProcessId; } ``` #### 2.2. CreateToolhelp32Snapshot() Ta lấy ID và tên process gốc bằng hàm này và hàm Process32Next() ### 3. Selectors Bằng cách kiểm tra các selectors và sử dụng các exception để xác định liệu giá trị selectors có bị thay đổi hay không thì ta có thể xem chương trình có bị debug không. Nếu selectors này bị thay đổi, điều này có thể cho thấy rằng một debugger đang can thiệp vào bộ nhớ. ### 4. DbgPrint() Khi ta gọi hàm này gây ra exception. Nếu hàm này không gây ra lỗi tức chương trình bị debug C/C++ Code ```cpp bool IsDebugged() { __try { RaiseException(DBG_PRINTEXCEPTION_C, 0, 0, 0); } __except(GetExceptionCode() == DBG_PRINTEXCEPTION_C) { return false; } return true; } ``` ### 5. DbgSetDebugFilterState() Hàm này đặt 1 cờ sẽ được kiểm tra bởi kernel-mode debugger. Nếu gọi hàm thành công thì process bị debug ### 6. NtYieldExecution() / SwitchToThread() Khi chương trình bị debug và single-step được thực hiện thì CPU không thể chuyển sang luồng khác và khiến hàm NtYieldExecution() trả về giá trị = 0. C/C++ Code ```cpp bool IsDebugged() { BYTE ucCounter = 1; for (int i = 0; i < 8; i++) { Sleep(0x0F); ucCounter <<= (1 - SwitchToThread()); } return ucCounter == 0; } ``` ### 7. VirtualAlloc() / GetWriteWatch() Ta sử dụng hàm GetWriteWatch() để truy xuất các địa chỉ của các trang bộ nhớ được ghi dữ liệu vào trong bộ nhớ đã được cấp phát bởi VirtualAlloc() với flag `MEM_WRITE_WATCH`. Nếu các trang bộ nhớ này được ghi vào mà không có lý do cụ thể thì nghĩa là chương trình bị debug C/C++ Code ```cpp bool Generic::CheckWrittenPages1() const { const int SIZE_TO_CHECK = 4096; PVOID* addresses = static_cast<PVOID*>(VirtualAlloc(NULL, SIZE_TO_CHECK * sizeof(PVOID), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); if (addresses == NULL) { return true; } int* buffer = static_cast<int*>(VirtualAlloc(NULL, SIZE_TO_CHECK * SIZE_TO_CHECK, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE)); if (buffer == NULL) { VirtualFree(addresses, 0, MEM_RELEASE); return true; } // Read the buffer once buffer[0] = 1234; ULONG_PTR hits = SIZE_TO_CHECK; DWORD granularity; if (GetWriteWatch(0, buffer, SIZE_TO_CHECK, addresses, &hits, &granularity) != 0) { return true; } else { VirtualFree(addresses, 0, MEM_RELEASE); VirtualFree(buffer, 0, MEM_RELEASE); return (hits == 1) ? false : true; } } } ```