# DEADLINE 4: TÌM HIỂU VỀ ANTI DEBUG ## `Anti debug` là gì? * Anti debug là một kỹ thuật được sử dụng để ngăn chặn việc phân tích và giám sát chương trình máy tính. Nó nhằm gây khó khăn cho các kỹ sư đảo ngược và nhà phân tích malware khi cố gắng phân tích mã nguồn hoặc quá trình chạy của một chương trình. Kỹ thuật này thường được các Malware dùng để tránh bị reverse ## Một số lợi ích và tác hại của `Anti debug` `Anti debug` có thể mang lại lợi ích và tác hại khác nhau trong các tình huống và mục đích sử dụng khác nhau. ### Lợi ích: * Bảo vệ bản quyền và sở hữu trí tuệ: Việc sử dụng các tricks `Anti debug` có thể giúp bảo vệ phần mềm và ứng dụng khỏi việc sao chép và crack, giúp tăng thu nhập và đảm bảo lợi ích cho các nhà phát triển và chủ sở hữu. * Bảo mật và phòng ngừa tấn công: `Anti debug` có thể ngăn chặn kẻ tấn công khai thác lỗ hổng bằng cách giảm khả năng reverse và hiểu hơn về cấu trúc và logic của phần mềm. Điều này có thể giúp tăng cường bảo mật và làm cho các ứng dụng khó bị tấn công. ### Tác hại: * Gây khó khăn cho các `Malware analysiser` và `reverser`, điều này thường được các `hacker` sử dụng. 4. Gây thách thức cho việc duy trì và sửa lỗi: `Anti debug` có thể làm tăng khó khăn trong việc duy trì và sửa lỗi phần mềm. Khi các công cụ gỡ lỗi không thể được sử dụng, việc tìm và sửa lỗi có thể trở nên phức tạp hơn và tốn nhiều thời gian hơn. ### Ví Dụ về Anti Debug: * Đây là một chall từ `Seccon CTF 2016`: [challenge Anti_debugging](https://www.mediafire.com/file/ci53iqsvdmrnx8a/bin.rar) em tìm được lấy từ trên mạng, em sẽ dùng file này để ví dụ về kỹ thuật Anti_debugging: * Đầu tiên load file vào IDA check thử ta sẽ thấy: ![Screenshot 2024-02-23 173410](https://hackmd.io/_uploads/HkEtHxU3T.png) * Dễ dàng thấy chương trình nhận Input từ người dùng và lần lượt chạy qua các hàm để check debug bao gồm `IsDebuggerPresent()`, GetCurrentProcess(),.... * Khi debug thử thì sẽ bị detect như sau: ![Screenshot 2024-02-23 185538](https://hackmd.io/_uploads/S1sG_Z836.png) ---------> Tóm lại chương trình hoạt động như sau: 1. Chương trình yêu cầu người dùng nhập mật khẩu và so sánh với một mật khẩu cụ thể ("I have a pen."). 2. Sau đó, chương trình kiểm tra xem có tricks nào hay k bằng cách sử dụng các phương pháp như kiểm tra Debugger Present, kiểm tra NtGlobalFlag, CheckRemoteDebuggerPresent. 3. Nếu phát hiện có tricks chương trình sẽ in ra thông báo và kết thúc. 4. Nếu không có tricks nào được phát hiện, chương trình tiếp tục kiểm tra các điều kiện khác nhau như có debugger cụ thể như Ollydbg, ImmunityDebugger, và IDA hay k. 4. Nếu không phát hiện được bất kỳ debugger nào, chương trình sẽ in ra output và kết thúc. ## Một số Kỹ thuật `Anti debug` ### I. Debug Flags * Các cờ đặc biệt trong bảng hệ thống nằm trong `process memory` và do hệ điều hành thiết lập, có thể được sử dụng để chỉ ra là chương trình đang được debug hay không. Trạng thái của các cờ này có thể được xác minh bằng cách sử dụng các hàm API cụ thể hoặc kiểm tra các bảng hệ thống trong bộ nhớ. * Suy ra ta có 2 cách để xem chương trình có đang được debug hay không thông qua các flags bao gồm `sử dụng hàm API win32` và `kiểm tra thủ công qua system table` #### 1. Sử dụng hàm API Win32 * Có tổng cộng 6 hàm API hỗ trợ ##### 1.1 IsDebuggerPresent() ``` call IsDebuggerPresent test al, al jne being_debugged ... being_debugged: push 1 call ExitProcess ``` * Kiểm tra cờ BeingDebugged của Khối môi trường quy trình (PEB). Nếu giá trị trả về = 1 thì thoát chương trình ngay lập tức. ##### 1.2 CheckRemoteDebuggerPresent() * Hàm này được sử dụng để kiểm tra xem một quá trình từ xa đang chạy trong môi trường debug hay không. Nó kiểm tra xem có một trình gỡ lỗi từ xa đang được sử dụng trên quá trình đó hay không ``` lea eax, [bDebuggerPresent] push eax push -1 ; GetCurrentProcess() call CheckRemoteDebuggerPresent cmp [bDebuggerPresent], 1 jz being_debugged ... being_debugged: push -1 call ExitProcess ``` * Kiểm tra xem trình gỡ lỗi (trong một quy trình khác trên cùng một máy) có được gắn vào quy trình hiện tại hay không. Nếu giá trị trả ra [bDebuggerPresent] = 1 thì kết thúc quá trình. ##### 1.3 NtQueryInformationProcess() * Hàm này được gọi bên trong của CheckRemoteDebuggerPresent. Nó có thể truy xuất một loại thông tin khác từ một quy trình. Nó chấp nhận tham số ProcessInformationClass chỉ định thông tin bạn muốn nhận và xác định loại đầu ra của tham số ProcessInformation. ##### 1.4 RtlQueryProcessHeapInformation() * Hàm này được sử dụng để truy vấn thông tin về heap của một process, bao gồm các phân vùng heap và các khối bộ nhớ được cấp phát bởi heap đó. ``` 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; } ``` ##### 1.5 RtlQueryProcessDebugInformation() * Hàm ntdll!RtlQueryProcessDebugInformation() có thể được sử dụng để đọc các trường nhất định từ bộ nhớ tiến trình của tiến trình được yêu cầu, bao gồm cả cờ heap. ``` bool Check() { ntdll::PDEBUG_BUFFER pDebugBuffer = ntdll::RtlCreateQueryDebugBuffer(0, FALSE); if (!SUCCEEDED(ntdll::RtlQueryProcessDebugInformation(GetCurrentProcessId(), ntdll::PDI_HEAPS | ntdll::PDI_HEAP_BLOCKS, pDebugBuffer))) return false; ULONG dwFlags = ((ntdll::PRTL_PROCESS_HEAPS)pDebugBuffer->HeapInformation)->Heaps[0].Flags; return dwFlags & ~HEAP_GROWABLE; } ``` ##### 1.6 NtQuerySystemInformation() * Hàm này được sử dụng để truy vấn thông tin hệ thống. Nó cho phép ứng dụng truy vấn các thông tin như danh sách các quá trình đang chạy, danh sách các mô-đun đã được tải, thông tin về bộ nhớ hệ thống, và nhiều thông tin khác liên quan đến hệ thống. #### 2 Kiểm tra thủ công (Manual Check) ##### 2.1 PEB * Tại vị trí thanh ghi FS trỏ vào 30h thì trả về 4byte offset của PEB.Khi vào PEB,tại vị trí PEB+2h là cờ debug.Vì thế nếu check được cờ này bật hay không là xác định được chương trình có bị debug không. ``` mov eax, fs:[30h] cmp byte ptr [eax+2], 0 jne being_debugged ``` ##### 2.2 NtGlobal * NtGlobalFlag nằm ở offset 0x68 của PEB, tương tự như BeingDebugged nó cũng được Set là 0 nêu như không bị Debug và sẽ là 0x70 trong trường hợp bị debug : ``` mov eax, fs:[30h] mov al, [eax+68h] and al, 70h cmp al, 70h jz being_debugged ``` ##### 2.3 HeapFlag * Heap Flag là ProcessHeap của PEB Structure. Nêu một không bị debug thì nó mang giá trị 0x2 hoặc 0x00 ### II. Object Handles * kiểm tra sử dụng các xử lý đối tượng kernel để phát hiện sự hiện diện của trình gỡ lỗi #### OpenProcess() * Vì chương trình ta là 1 process con của process của debugger.Vì thế nó cũng sẽ thừa kế quyền từ debugger.Chính vì lý do này.Ta có thể mở bất cứ process nào đang chạy từ chương trình chúng ta. * Một số trình gỡ lỗi có thể được phát hiện bằng cách sử dụng hàm kernel32!OpenProcess() trên process csrss.exe. Lời gọi hàm sẽ chỉ thành công nếu người dùng của process là thành viên của nhóm quản trị viên và có đặc quyền gỡ lỗi. ``` #include <iostream> #include <windows.h> // Định nghĩa kiểu con trỏ hàm TCsrGetProcessId có kiểu trả về là DWORD và không có tham số typedef DWORD(WINAPI* TCsrGetProcessId)(VOID); // Hàm kiểm tra xem chế độ debug có được kích hoạt không bool Check() { // Load thư viện ntdll.dll vào bộ nhớ HMODULE hNtdll = LoadLibraryA("ntdll.dll"); if (!hNtdll) // Kiểm tra xem việc load thư viện có thành công không return false; // Lấy con trỏ của hàm CsrGetProcessId từ thư viện ntdll.dll TCsrGetProcessId pfnCsrGetProcessId = (TCsrGetProcessId)GetProcAddress(hNtdll, "CsrGetProcessId"); if (!pfnCsrGetProcessId) // Kiểm tra xem việc lấy con trỏ có thành công không return false; // Gọi hàm CsrGetProcessId để lấy Process Id của quy trình csrss.exe DWORD dwProcessId = pfnCsrGetProcessId(); if (dwProcessId == 0) // Nếu Process Id bằng 0, tức là không thành công return false; // Mở quy trình csrss.exe HANDLE hCsr = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (hCsr != NULL) // Nếu mở quy trình thành công { CloseHandle(hCsr); // Đóng handle của quy trình return true; // Trả về true, ngụ ý là chế độ debug có thể đã được kích hoạt } else return false; // Nếu không mở được quy trình, trả về false } int main() { // Gọi hàm Check() để kiểm tra chế độ debug if (Check()) { std::cout << "Detect Debug" << std::endl; // In ra thông báo nếu detect được debug } else { std::cout << "No Detect Debug" << std::endl; // In ra thông báo nếu không detect được debug } return 0; } ``` #### CreateFile() * Thông tin file thực thi được lưu ở CREATE_PROCESS_DEBUG_INFO. Do đó các Debugger có thể đọc dữ liệu từ đây, một số Debugger có thể quên đóng Handle này * Sử dụng hàm kernel32!CreateFileW() hoặc kernel32!CreateFileA() để tệp tin với quyền truy cập độc quyền. Nếu thất bại kết quả trả về là:INVALID_HANDLE_VALUE thì có nghĩa là chương trình đang được debug. #### CloseHandle() * Nếu process đang được debug những nếu handle k hợp lệ pass qua hàm `ntdll!NtClose()` hoặc là `kernel32!CloseHandle()` thì `EXCEPTION_INVALID_HANDLE (0xC0000008)` sẽ được bật, nó có thể được lưu vào bộ nhớ đêm, nếu phát hiện thì ta có thể biết được chương trình đang được debug ``` bool Check() { __try { CloseHandle((HANDLE)0xDEADBEEF); return false; } __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return true; } } ``` #### LoadLibrary() * Nếu một file được load sử dụng kernel32!LoadLibrary (ntdll!LdrLoadDll). Handle của nó sẽ được mở và lưu trong LOAD_DLL_DEBUG_INFO struct. Nếu handle này không được đóng bởi debbuger, file này sẽ không thể mở và cũng như một vài debugger sẽ quên đóng. * Để kiêm tra ta sẽ load bất cứ file nào dùng `LoadLivrary()` và mở riêng nó bằng `CreateFile()`. Nếu call bị lỗi thì có debugger. ### III.Exceptions * Các phương pháp sau đây cố tình gây ra ngoại lệ để xác minh xem hành vi tiếp theo có phải là hành vi điển hình đối với một quy trình đang chạy mà không có trình gỡ lỗi hay không. #### UnhandledExceptionFilter() * Khi xảy ra exception và không có Exception Handlers được đăng kí. Hàm UnhandledExceptionFilter sẽ được gọi. * Có thể đăng ký một custom unhandled exception filter dùng kernel32!SetUnhandledExceptionFilter(). Nhưng nếu chương trình đang được debug, custom filter sẽ không được gọi ta có thể biết được đang có debugger hay k. #### RaiseException() * Các exceptions như `DBC_Control_C` hoặc `DBG_RIPEVENT` không được chuyển tới exception handlers của process hiện tại và được debugger sử dụng. Điều này cho phép chúng ta đăng ký một trình xử lý ngoại lệ, đưa ra các ngoại lệ này bằng cách sử dụng hàm `kernel32!RaiseException()` và kiểm tra xem điều khiển có được chuyển đến process của chúng ta hay không. Nếu exception handlers không được gọi thì quá trình này có thể đang được gỡ lỗi. ``` 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; } } ``` #### Hiding Control Flow with Exception Handlers * Trick này sẽ k giúp phát hiện có debugger hay không mà thay vào đó nó sẽ ẩn luồng hoạt động của chương trình đi bằng cách tạo một exception handler (structured hay vectored) để đưa ra một exception khác, trình xử lý tiếp theo sẽ đưa ra ngoại lệ tiếp theo, v.v. Cuối cùng, trình tự xử lý sẽ dẫn đến thủ tục mà chúng ta muốn ẩn. ### IV. Timing #### RDPMC/RDTSC * RDPMC (Read Performance-Monitoring Counters) và RDTSC (Read Time-Stamp Counter) là các instruction x86 ASM được sử dụng để đo thời gian và hiệu suất của CPU, thường được sử dụng trong việc tối ưu hóa mã máy tính và phân tích hiệu suất hệ thống. * 2 instructions này sử dụng cờ PCE trong thanh ghi CR4. * RDPMC chỉ được sử dụng ở Kernel #### GetLocalTime() ``` 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; } ``` * Lấy thời gian bắt đầu của công việc bằng cách gọi hàm GetLocalTime() và lưu vào biến stStart. * Tiến hành một số công việc không được hiển thị. * Lấy thời gian kết thúc của công việc bằng cách gọi lại hàm GetLocalTime() và lưu vào biến stEnd. * Chuyển đổi thời gian từ định dạng SYSTEMTIME sang FILETIME bằng cách sử dụng hàm SystemTimeToFileTime(), sau đó lưu vào biến ftStart và ftEnd. * Chuyển đổi thời gian từ định dạng FILETIME sang ULARGE_INTEGER để có thể thực hiện so sánh, sau đó lưu vào biến uiStart và uiEnd. * So sánh sự chênh lệch thời gian giữa uiEnd và uiStart với qwNativeElapsed. Nếu thời gian thực thi vượt quá ngưỡng thời gian cụ thể, hàm trả về true, ngược lại trả về false. #### GetSystemTime(), GetTickCount(), QueryPerformanceCounter(), timeGetTime() * stStart: Biến này lưu thời điểm bắt đầu của phần công việc cần đo thời gian thực thi. Nó được gán bằng thời gian hiện tại của hệ thống khi bắt đầu thực hiện công việc. * stEnd: Biến này lưu thời điểm kết thúc của phần công việc cần đo thời gian thực thi. * Cách phát hiện debug ở các tricks này cũng sẽ chỉ phát hiện độ trễ nếu debugger có đặt `breakpoint` hay `steptrace`. ### V. Process Memory * Tricks này là `Process Memory` sẽ có kỹ thuật chính là `Detect Breakpoints` và một số Memory check khác #### 1. Detect Breakpoints * Có 3 kiểu detect breakpoint trong quá trình reverse bao gồm : `Anti_Step_Over` ,`Software`, `Hardware`, và `Memory`. Breakpoint là bản chất của các quá trình reverses,vì thế,tìm hiểu bản chất của việc thực hiện các breakpoint này rất quan trọng trong việc anti-debugger …Breakpoint tạo ra 1 điểm dừng trong quá trình thực thi chương trình tại 1 điểm nào đó trong quá trình thực hiện chương trình. ##### 1.1 Software Breakpoint(INT 3h) * Opcode của INT 3 là CC(0xCC).Chỉ cần kiểm tra trong vùng nhớ chương trình đang chạy có bất kỳ opcode CC nào không là đủ.Cách kiểm tra đơn giản chỉ check trong vùng nhớ chương trình có opcode nào là CC không.Cách detect này không phải lúc nào cũng đúng vì nhiều như debugger đặt breakpoint để ngắt debug ở những vùng không nằm trong vùng nhớ test ``` 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 * Debugger cho phép bạn bỏ qua lệnh gọi hàm. Trong trường hợp như vậy, debugger sẽ ngầm đặt `Software Breakpoint` trên lệnh theo sau lệnh gọi (tức là địa chỉ trả về của hàm được gọi). * Để phát hiện xem debugger có cố gắng vượt qua hàm hay không, chúng ta có thể kiểm tra byte bộ nhớ đầu tiên tại địa chỉ trả về. Nếu điểm dừng phần mềm ( 0xCC ) nằm ở địa chỉ trả về, chúng tôi có thể vá nó bằng một số hướng dẫn khác (ví dụ NOP ). Rất có thể nó sẽ phá mã và làm hỏng quá trình. Mặt khác, chúng ta có thể vá địa chỉ trả về bằng một số mã có ý nghĩa thay vì NOP và thay đổi luồng điều khiển của chương trình. #### 1.3. Memory Breakpoint * Memory breakpoint hoạt động dựa trên thuộc tính bảo vệ của vùng nhớ và các exception sinh ra khi truy cập trái phép với thuộc tính bảo vệ của vùng nhớ đó. * Khi ta truy cập vào 1 vùng nhớ có thuộc tính PAGE_GUARD thì nếu ta chạy bình thường thì chương trình sẽ xuất ra 1 exception là STATUS_GUARD_PAGE_VIOLATION.Nhưng khi chạy với debug thì nó sẽ xem đó là 1 memory breakpoint (ollydbg). * Do đó ta có thể detect debugger như sau: 1. Đầu tiên ta tiến hành cấp phát 1 vùng nhớ bằng VirtualAlloc,sau đó cấp phát cho page thuộc tính PAGE_GUARD. 2. Ta sẽ chèn opcode RET(0xC3) để nhảy về vị trí 1 nhãn nào đó để thoát khỏi breakpoint này.Để quá trình RET xảy ra thì ta phải PUSH vào stack địa chỉ của hàm trước khi detect memory breakpoint. #### 1.4. Hardware Breakpoint * Các thanh ghi gỡ lỗi DR0, DR1, DR2 và DR3 có thể được truy xuất từ ngữ cảnh luồng. Nếu chúng chứa các giá trị khác 0, điều đó có thể có nghĩa là quy trình được thực thi trong trình gỡ lỗi và điểm dừng phần cứng đã được đặt. * Ta sẽ tìm hiểu về các thanh ghi debug ![image](https://hackmd.io/_uploads/ryb2ibIha.png) * Có bốn thanh ghi debug đó là : DR0, DR1, DR2, DR3, bốn thanh ghi này sẽ được sử dụng để lưu trữ những địa chỉ mà ta thiết lập `Hardware Breakpoint`. * DR0, DR1, DR2, DR3: Các thanh ghi này được sử dụng để đặt các điểm dừng (breakpoints) trong mã máy. Mỗi thanh ghi này có thể lưu trữ một địa chỉ bộ nhớ, và CPU sẽ tạo ra một ngoại lệ khi nó thực hiện một lệnh tại địa chỉ này. Điều này cho phép người gỡ lỗi ngừng chương trình tại các điểm nhất định để kiểm tra trạng thái của chương trình. * DR6: Thanh ghi này chứa các cờ trạng thái của ngoại lệ debug. Các bit trong thanh ghi này sẽ được thiết lập khi một ngoại lệ debug xảy ra, cho phép người gỡ lỗi biết nguyên nhân của ngoại lệ. * DR7: Thanh ghi này được sử dụng để cấu hình các thanh ghi DR0 đến DR3. Nó xác định loại điểm dừng (breakpoint), bao gồm breakpoint kích hoạt, breakpoint kích hoạt ghi, breakpoint kích hoạt đọc, và breakpoint kích hoạt ghi/đọc. * Điều kiện của mỗi breakpoints để cho dừng sự thực thi của chương trình lại được lưu trong một thanh ghi đặc biệt khác là thanh ghi DR7. * Khi bất kì một điều kiện nào thỏa mãn (TRUE) thì processor sẽ quăng một exception là INT1 (khà khà vậy là nó dùng INT1 nhé) và quyền điều khiển lúc này sẽ được trả về cho trình Debug của chúng ta. ### VI. Assembly instructions #### 1. INT 3 * Tạo EXCEPTION_BREAKPOINT (0x80000003) và exception handler sẽ được gọi. Nếu không được gọi thì chương trình đang được debug. ``` bool IsDebugged() { __try { __asm int 3; return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } } ``` #### 2. INT 2D * Giống với INT3 thì INT 2D cũng tạo EXCEPTION_BREAKPOINT VÀ EXCEPTION HANDLER sẽ được gọi. * Nhưng với INT2D , Windows sử dụng thanh ghi EIP làm địa chỉ ngoại lệ và sau đó tăng giá trị thanh ghi EIP . Windows cũng kiểm tra giá trị của thanh ghi EAX trong khi INT2D được thực thi. Nếu là 1, 3 hoặc 4 trên tất cả các phiên bản Windows hoặc 5 trên Vista+, địa chỉ ngoại lệ sẽ tăng thêm một * Instruction này có thể gây ra sự cố cho một số debugger vì sau khi kích hoạt EIP , byte theo sau lệnh INT2D sẽ bị bỏ qua và việc thực thi có thể tiếp tục từ lệnh bị hỏng. ``` bool IsDebugged() { __try { __asm xor eax, eax; __asm int 0x2d; __asm nop; return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } } ``` #### 3. DeBugBreak * DebugBreak tạo ra một Exception Breakpoint xảy ra trong quy trình hiện tại. Điều này cho phép luồng gọi báo hiệu debugger xử lý Exception. * Nếu chương trình được thực thi mà không có debugger, điều khiển sẽ được chuyển tới trình xử lý ngoại lệ. Nếu không, việc thực thi sẽ bị debugger chặn lại. ``` bool IsDebugged() { __try { DebugBreak(); } __except(EXCEPTION_BREAKPOINT) { return false; } return true; } ``` #### 4. ICE * Nếu lệnh ICE được thực thi, EXCEPTION_SINGLE_STEP ( 0x80000004 ) sẽ được đưa ra. * Tuy nhiên, nếu chương trình đã được traced, debugger sẽ coi Exception này là thông thường được tạo bằng cách thực hiện lệnh với bit SingleStep được đặt trong thanh ghi Flags. Do đó, trong trình gỡ lỗi, trình xử lý ngoại lệ sẽ không được gọi và việc thực thi sẽ tiếp tục sau lệnh ICE . ``` bool IsDebugged() { __try { __asm __emit 0xF1; return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } } ``` #### 5. Stack Segment Register * Đây là một thủ thuật có thể được sử dụng để phát hiện xem chương trình có đang bị traced hay không. Thủ thuật này bao gồm việc truy tìm chuỗi instruction sau: ``` push ss pop ss pushf ``` Sau khi thực hiện một bước trong trình gỡ lỗi thông qua mã này, Trap flag sẽ được đặt. Thông thường, nó không hiển thị khi trình gỡ lỗi xóa Cờ bẫy sau mỗi sự kiện trình gỡ lỗi được gửi. Tuy nhiên, nếu trước đây chúng ta lưu EFLAGS vào ngăn xếp, chúng ta sẽ có thể kiểm tra xem Trap flag có được đặt hay không. #### 6. Counting * Sử dụng trình gỡ lỗi cho EXCEPTION_SINGLE_STEP. * Thủ thuật này đặt các `Hardware Breakpoint` cho mỗi lệnh theo một số trình tự được xác định trước (ví dụ: trình tự các NOP ). Việc thực thi lệnh có `Hardware Breakpoint` trên đó sẽ làm tăng Exception EXCEPTION_SINGLE_STEP mà trình `Vectored Exception handler` có thể bắt được. Trong Exception handler, tăng một thanh ghi đóng vai trò là bộ đếm lệnh ( EAX trong trường hợp của chúng ta) và con trỏ lệnh EIP để chuyển điều khiển sang lệnh tiếp theo trong chuỗi. * Do đó, mỗi lần điều khiển được chuyển sang lệnh tiếp theo trong chuỗi của chúng ta, ngoại lệ sẽ tăng lên và bộ đếm sẽ tăng lên. Sau khi chuỗi kết thúc, chúng tôi kiểm tra bộ đếm và nếu nó không bằng độ dài chuỗi của chúng tôi, chúng tôi coi đó như thể chương trình đang được debug. #### 7. POPF và Trap Flag * Dùng Trap Flag. Khi Trap Flag được đặt, SINGLE_STEP sẽ được nâng lên. Tuy nhiên, nếu trace code thì Trap Flag sẽ bị debugger xóa nên sẽ k thấy được exception. #### 8. Instruction Prefixes * Lơi dụng các debugger khi gặp các prefixes, Nếu thực thi cùng debugger, sau khi đến byte F3; Debugger skip prefix và nhảy tới câu lệnh INT1. Nếu chạy mà không có debugger, exeption sẽ raise và luồng thực thi sẽ vào exception handler. ### VII. Direct debugger interaction #### 1. Self_Debugging * Có ít nhất 3 hàm có thể đính kèm vào debugger đó là kernel32!DebugActiveProcess(),ntdll!DbgUiDebugActiveProcess(), ntdll!NtDebugActiveProcess() * Nếu một tiến trình đang được gỡ lỗi thì không thể đính kèm một trình gỡ lỗi khác vào nó. Để kiểm tra xem ứng dụng có được gỡ lỗi hay không bằng cách tận dụng thực tế này, cần bắt đầu một quy trình khác để cố gắng đính kèm vào ứng dụng. #### 2. GenerateConsoleCtrlEvent() * Khi người dùng nhấn Ctrl+C hoặc Ctrl+Break và cửa sổ bảng điều khiển nằm trong tiêu điểm, Windows sẽ kiểm tra xem có trình xử lý nào cho sự kiện này hay không. Tất cả các tiến trình của bảng điều khiển đều có hàm xử lý Ctrl+Break. * Tuy nhiên, nếu quy trình bàn điều khiển đang được debug và CTRL+C chưa bị tắt thì hệ thống sẽ tạo ra exception DBG_Control_C . Thông thường, exception này bị chặn bởi debugger, nhưng nếu ta đăng ký một exception handler, ta sẽ có thể kiểm tra xem DBG_Control_C có được bật lên hay không. Nếu ta chặn exception DBG_Control_C trong trình exception handler của riêng mình thì điều đó có thể cho biết rằng quy trình đang được debug. #### 3. BlockInput() * Hàm user32!BlockInput() có thể chặn tất cả chuột và bàn phím, đây là một cách khá hiệu quả để tắt debugger. * Ta cũng có thể phát hiện xem có công cụ nào kết nối user32!BlockInput() và các lệnh gọi anti debugging khác hay k. Chức năng này chỉ cho phép chặn đầu vào một lần. Cuộc gọi thứ hai sẽ trả về FALSE . Nếu hàm trả về TRUE bất kể đầu vào là gì, nó có thể chỉ ra rằng có một số giải pháp hooking hiện diện. #### 4. NtSetInformationThread() * Hàm ntdll!NtSetInformationThread() có thể được sử dụng để ẩn một chuỗi khỏi debugger. Sau khi luồng bị ẩn khỏi debugger, nó sẽ tiếp tục chạy nhưng debugger sẽ không nhận được các sự kiện liên quan đến luồng này. Chuỗi này có thể thực hiện kiểm tra anti_debug như tổng kiểm tra mã, xác minh debug flags, v.v. * Tuy nhiên, nếu có một breakpoint trong luồng ẩn hoặc nếu chúng ta ẩn luồng chính khỏi trình gỡ lỗi thì quá trình sẽ gặp sự cố và trình gỡ lỗi sẽ bị kẹt. #### 5. EnumWindows() và SuspendThread() * Kỹ thuật này là tạm dừng luồng sở hữu của tiến trình gốc. * Đầu tiên, ta cần xác minh xem tiến trình gốc có phải là debugger hay k. Có thể thực hiện bằng cách liệt kê tất cả các cửa sổ cấp cao nhất trên màn hình (sử dụng user32!EnumWindows() hoặc user32!EnumThreadWindows() ), tìm kiếm cửa sổ có ID tiến trình là ID của tiến trình gốc (sử dụng user32!GetWindowThreadProcessId() ) và kiểm tra tiêu đề của cửa sổ này (bởi user32!GetWindowTextW() ). * Nếu tiêu đề cửa sổ của quy trình gốc trông giống tiêu đề debugger, chúng ta có thể tạm dừng chuỗi sở hữu bằng cách sử dụng kernel32!SuspendThread() hoặc ntdll!NtSuspendThread(). #### 6. OutputDebugString() * Check xem có `kernel32!OutputDebugString` được goi k, nếu k thì sẽ có lỗi * Những tricks này hiện k được dùng nữa ### VIII. MISC #### 1. FindWindow() * Kỹ thuật này bao gồm việc liệt kê đơn giản các lớp cửa sổ trong hệ thống và so sánh chúng với các lớp debugger Windows đã biết. Có thể sử dụng các chức năng sau: 1. user32!FindWindowW() 2. user32!FindWindowA() 3. user32!FindWindowExW() 4. user32!FindWindowExA() ``` 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. Selectors * Các giá trị của `Selectors` có thể có vẻ ổn định nhưng thực tế chúng không ổn định trong một số trường hợp nhất định và cũng tùy thuộc vào phiên bản Windows. ``` xor eax, eax push fs pop ds l1: xchg [eax], cl xchg [eax], cl ``` #### 3. DbgPrint() * Các hàm gỡ lỗi như ntdll!DbgPrint() và kernel32!OutputDebugStringW() gây ra DBG_PRINTEXCEPTION_C (0x40010006). Nếu một chương trình được thực thi với debugger đính kèm thì debugger sẽ xử lý exception này. Nhưng nếu không có debugger và exceptions handler được bật, thì exceptions này sẽ bị exceptions handler bắt. ``` bool IsDebugged() { return NT_SUCCESS(ntdll::NtSetDebugFilterState(0, 0, TRUE)); } ``` #### 4. DbgSetDebugFilterState() * Các hàm ntdll!DbgSetDebugFilterState() và ntdll!NtSetDebugFilterState() chỉ đặt một cờ sẽ được kiểm tra là trình gỡ lỗi chế độ kernel nếu có. Vì vậy, nếu một trình gỡ lỗi kernel được gắn vào hệ thống, các chức năng này sẽ thành công. Tuy nhiên, các chức năng này cũng có thể thành công do tác dụng phụ do một số trình gỡ lỗi ở chế độ người dùng gây ra. ``` bool IsDebugged() { return NT_SUCCESS(ntdll::NtSetDebugFilterState(0, 0, TRUE)); } ```