owned this note
owned this note
Published
Linked with GitHub
---
tags: [Beginner]
---
# Anti Debug
* **Debugging** là quá trình tìm ra nguyên nhân gốc rễ, cách giải quyết tạm thời và các phương án sửa lỗi có thể áp dụng.
* **Anti debug** là kỹ thuật cản trở quá trình reverse hoặc **debugging**, làm cho việc phân tích trở lên khó khăn hơn. Có thể được sử dụng để ngăn chặn phần mềm khỏi bị crack bởi hacker khi phân tích để bypass cơ chế bảo mật, hay những chương trình muốn che giấu đi hành vi của mã độc, ...
* Dưới đây là các kiểu **anti debug** mình đã tìm hiểu.
# Debug Flags
## 1. Using Win32 API
* Các kỹ thuật sau đây sử dụng các hàm API hiện có (**WinAPI** hoặc **NativeAPI**) để kiểm tra cấu trúc hệ thống trong bộ nhớ quy trình để tìm các cờ cụ thể cho biết quy trình hiện đang được gỡ lỗi.
### 1.1. IsDebuggerPresent()
* Hàm **IsDebuggerPresent()** trong **kernel32.dll** xác định xem chương trình có đang được debug bằng các công cụ như **x64dbg** hoặc **OllyDbg**, .... Nói chung, hàm này chỉ kiểm tra cờ **BeingDebugged** trong **Process Environment Block** [(PEB)](https://www.nirsoft.net/kernel_struct/vista/PEB.html).
(Tiến trình bị debug sẽ được spawn từ tiến trình debugger).
> API cũng hoạt động khi process đang bị attached
>
| isDebuggerPresent()| 32bit | 64bit |
| -------| -------- | -------- |
| Get **PEB** struct| mov eax, dword ptr fs:[30h] | mov rax, qword ptr gs:[60h] |
| Get Being Debuged flag | movzx eax, byte ptr [eax+2h] | movzx eax, byte ptr [rax+2h] |
|Return | ret | ret |
* Ví dụ:
```python
#include <windows.h>
#include <stdio.h>
int main() {
if (IsDebuggerPresent()) {
printf("Debugger detected! Exiting...\n");
return 1;
}
printf("No debugger detected.\n");
return 0;
}
```
* Khi chạy bình thường:

* Khi debug:

* Mã ASM:

* Như ta thấy, lệnh `test eax, eax` để set cờ **Zero Flag (ZF)**. Cụ thể, nếu `eax = 0` thì cờ **ZF** được bật, ngược lại cờ **ZF** tắt. Sau đó sẽ là lệnh `jz`, kết quả như nào thì ở trên.
### 1.2. CheckRemoteDebuggerPresent()
* Hàm kernel32! **CheckRemoteDebuggerPresent()** kiểm tra xem trình debug (trong một tiến trình khác trên cùng một máy) có được đính kèm vào tiến trình hiện tại hay không.
> cũng hoạt động khi được spawn từ trình debugger.
* Ví dụ:
```py
#include <windows.h>
#include <stdio.h>
int main() {
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());
BOOL bDebuggerPresent = FALSE;
if (CheckRemoteDebuggerPresent(hProcess, &bDebuggerPresent)) {
if (bDebuggerPresent) {
printf("Debugger detected! Exiting... \n");
return 1;
} else {
printf("No debugger detected.\n");
}
CloseHandle(hProcess);
return 0;
}
}
```
* Khi chạy bình thường:

* Khi debug:

* Mã ASM:

* Lúc đầu thì `BOOL bDebuggerPresent = FALSE;` (mặc định là không có debugger),tiếp theo `call rax (gọi CheckRemoteDebuggerPresent)`. Nếu phát hiện debug thì đặt `bDebuggerPresent = TRUE`. Sau đó `jz` và kết quả thì như ở trên.
### 1.3. NtQueryInformationProcess()
* Hàm ntdll! **NtQueryInformationProcess()** có thể lấy một loại thông tin khác từ một tiến trình. Nó lấy tham số `ProcessInformationClass` chỉ định thông tin muốn lấy và xác định loại đầu ra của tham số `ProcessInformation` .
* Thông tin của hàm mình đọc ở [đây](https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess)
```py
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
```
* Ở đây tại tham số thứ 2, có 3 thông tin quan trọng là :
* **ProcessDebugPort(0x7)**: Dùng để lấy số hiệu cổng (port) mà debugger đang sử dụng để kết nối tới tiến trình. Nếu giá trị trả về là `0xFFFFFFFF`
(hay `-1`), tức là tiến trình đang bị debug.
> hoạt động cả khi spawn từ debbuger hoặc bị attached
* **ProcessDebugFlags(0x1F)**: Dựa vào một trường trong cấu trúc kernel [**EPROCESS**](https://www.nirsoft.net/kernel_struct/vista/EPROCESS.html) có tên là **NoDebugInherit**. Nếu giá trị trả về là `0`, tức là có trình debug đang gắn vào tiến trình.
* **ProcessDebugObjectHandle(0x1E)**: Dùng để lấy handle của **"debug object"** – một đối tượng kernel chỉ tồn tại khi tiến trình bị debug.
Nếu handle trả về là hợp lệ `(≠ NULL)`, tức là tiến trình đang bị debug.
* Ví dụ:
```py
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif
int main()
{
typedef NTSTATUS(NTAPI* TNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
if (hNtdll)
{
TNtQueryInformationProcess pfnNtQueryInformationProcess =
(TNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");
if (pfnNtQueryInformationProcess)
{
ULONG_PTR ProcessDebugPort = 0;
ULONG dwReturned1;
NTSTATUS status1 = pfnNtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)0x7,
&ProcessDebugPort,
sizeof(ProcessDebugPort),
&dwReturned1);
if (NT_SUCCESS(status1) && (-1 == ProcessDebugPort))
{
printf("Debugger detected from ProcessDebugPort (value: %lu)\n", ProcessDebugPort);
}
else
{
printf("No debugger detected from ProcessDebugPort.\n");
}
DWORD dwProcessDebugFlags;
ULONG dwReturned2;
NTSTATUS status2 = pfnNtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)0x1f,
&dwProcessDebugFlags,
sizeof(DWORD),
&dwReturned2);
if (NT_SUCCESS(status2) && (dwProcessDebugFlags == 0))
{
printf("Debugger detected from ProcessDebugFlags (value: %lu)\n", dwProcessDebugFlags);
}
else
{
printf("No debugger detected from ProcessDebugFlags.\n");
}
HANDLE hProcessDebugObject = 0;
ULONG dwReturned3;
NTSTATUS status3 = pfnNtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)0x1e,
&hProcessDebugObject,
sizeof(HANDLE),
&dwReturned3);
if (NT_SUCCESS(status3) && (hProcessDebugObject != 0))
{
printf("Debugger detected from ProcessDebugObject (handle: %p)\n", hProcessDebugObject);
}
else
{
printf("No debugger detected from ProcessDebugObject.\n");
}
}
FreeLibrary(hNtdll);
}
return 0;
}
```
* Khi chạy bình thường:

* Khi debug:

### 1.4 RtlQueryProcessHeapInformation()
* Hàm **ntdll!RtlQueryProcessHeapInformation()** có thể được sử dụng để đọc cờ heap từ bộ nhớ tiến trình của tiến trình hiện tại.
```py
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;
}
```
* Tại `((PRTL_PROCESS_HEAPS)pDebugBuffer->HeapInformation)->Heaps[0].Flags` thì chương trình lấy **heap** đầu tiên trong danh sách và đọc trường `Flags`.
* 1 chương trình kể cả khi debug hay không bị debug thì **Heaps[0].Flags** luôn chứa `HEAP_GROWABLE (0x2)`. Nhưng khi bị debug thì sẽ thêm mấy trường sau:
| **Flag** | **Giá trị Hex** | **Tác dụng** |
|------------------------------------|------------------|--------------------------------------------------------------|
| `HEAP_TAIL_CHECKING_ENABLED` | `0x20` | Kiểm tra dữ liệu cuối block để phát hiện ghi tràn (overflow) |
| `HEAP_FREE_CHECKING_ENABLED` | `0x40` | Kiểm tra các block đã giải phóng |
| `HEAP_VALIDATE_PARAMETERS_ENABLED` | `0x40000000` | Xác minh các tham số gọi heap |
* Lúc này giá trị **HeapFlags** = `0x40000062` ( tổng giá trị các trường ).
:::success
* Khi bình thường, **HeapFlags** = `HEAP_GROWABLE` = `0x2`.
* Khi debug, **HeapFlags** = `0x40000062`.
:::
### 1.5 RtlQueryProcessDebugInformation()
* **RtlQueryProcessDebugInformation()** có thể được sử dụng để đọc các trường nhất định từ bộ nhớ quá trình của quy trình được yêu cầu, bao gồm các cờ **heap**.
```py
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;
}
```
* Cơ chế này nó cũng gần giống với **RtlQueryProcessHeapInformation()**, tức là kiểm tra các `heap flags` để detect `antidebug`. Nhưng nó ngon hơn ở chỗ là có thể chọn tiến trình khác (bằng cách truyền `PID`), còn **RtlQueryProcessHeapInformation()** chỉ hoạt động cho tiến trình hiện tại.
:::success
* Khi bình thường, **HeapFlags** = `HEAP_GROWABLE` = `0x2`.
* Khi debug, **HeapFlags** = `0x40000062`.
:::
### 1.6. NtQuerySystemInformation()
* Hàm **ntdll!NtQuerySystemInformation()** là một hàm `native API` của **Windows**, cho phép truy vấn nhiều loại thông tin hệ thống khác nhau. Hàm này nhận một tham số đầu vào gọi là **SystemInformationClass** – tức là loại thông tin muốn truy vấn. Xem tại [đây](https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation)
```py
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
```
* Trong đó có 1 class đặc biệt cần quan tâm là **SystemKernelDebuggerInformation** (giá trị `0x23`) sẽ chứa 2 thông tin lưu tại 2 thanh ghi:
| Thanh ghi | Giá trị | Ý nghĩa |
| --------- | ---------------------- | -------------------------------------- |
| `al` | `KdDebuggerEnabled` | = 1 nếu **kernel debugger được bật** |
| `ah` | `KdDebuggerNotPresent` | = 0 nếu **có debugger** |
* Tức là hiểu 1 cách đơn giản:
:::success
* Nếu ah = $0$, có **debugger**.
* Nếu ah = $1$, không có **debugger**.
:::
:::info
Phương pháp này chỉ hoạt động trên phiên bản **WinXP**.
:::
## 2. Manual checks
### 2.1 PEB!BeingDebugged Flag
* Đây là một phương pháp kiểm tra tiến trình có bị debug hay không bằng cách đọc trực tiếp cờ **BeingDebugged** trong **PEB** (`Process Environment Block`) mà không cần gọi hàm **IsDebuggerPresent()**.
```py
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif // _WIN64
if (pPeb->BeingDebugged)
goto being_debugged;
```
:::success
* Nếu BeingDebugged = 1 thì tiến trình đang bị debug.
* Nếu BeingDebugged = 0 thì tiến trình không bị debug.
:::
### 2.2 NtGlobalFlag
* **NtGlobalFlag** là một trường trong PEB (giá trị mặc định = $0$)
* offset = `0x68` on $32$ bit
* offset = `0xBC` on $64$ bit
* Khi phát hiện debug, các **flag** sau được thêm vào:
* **FLG_HEAP_ENABLE_TAIL_CHECK** (`0x10`)
* **FLG_HEAP_ENABLE_FREE_CHECK** (`0x20`)
* **FLG_HEAP_VALIDATE_PARAMETERS** (`0x40`)
```python
#define FLG_HEAP_ENABLE_TAIL_CHECK 0x10
#define FLG_HEAP_ENABLE_FREE_CHECK 0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
#define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS)
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#endif // _WIN64
if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)
goto being_debugged;
```
### 2.3 [Heap Flags](https://ctf-wiki.mahaloz.re/reverse/windows/anti-debug/heap-flags/)
* **Heap** trong **Windows** có hai trường (field) liên quan đến việc phát hiện debugger, đó là **Flags** và **ForceFlags**. Giá trị của hai trường này sẽ thay đổi khi có debugger đang chạy, nhưng cách thay đổi phụ thuộc vào phiên bản **Windows**.
* Khi không debug:
* **Flags**(`HEAP_GROWABLE(0x2)`)
* **ForceFlags**(`0`)
* Khi có debugger hiện diện:
* Trên các phiên bản **Windows NT**, **Windows 2000**, và **Windows XP 32-bit**, trường **Flags** sẽ được thiết lập thành tổ hợp các giá trị sau:
* **HEAP_GROWABLE** (`2`)
* **HEAP_TAIL_CHECKING_ENABLED** (`0x20`)
* **HEAP_FREE_CHECKING_ENABLED** (`0x40`)
* **HEAP_SKIP_VALIDATION_CHECKS** (`0x10000000`)
* **HEAP_VALIDATE_PARAMETERS_ENABLED** (`0x40000000`)
* Trên **Windows XP 64-bit**, **Windows Vista** trở lên, trường **Flags** sẽ được thiết lập thành tổ hợp các giá trị sau (khác với phiên bản 32-bit ở chỗ không có **HEAP_SKIP_VALIDATION_CHECKS**):
* **HEAP_GROWABLE** (`2`)
* **HEAP_TAIL_CHECKING_ENABLED** (`0x20`)
* **HEAP_FREE_CHECKING_ENABLED** (`0x40`)
* **HEAP_VALIDATE_PARAMETERS_ENABLED** (`0x40000000`)
* Trong khi đó, trường **ForceFlags** khi có debugger sẽ luôn được đặt thành tổ hợp của:
* **HEAP_TAIL_CHECKING_ENABLED** (`0x20`)
* **HEAP_FREE_CHECKING_ENABLED** (`0x40`)
* **HEAP_VALIDATE_PARAMETERS_ENABLED** (`0x40000000`)
* Nếu thấy hơi rối thì mọi người có thể đọc thêm ở [đây](https://ctf-wiki.mahaloz.re/reverse/windows/anti-debug/heap-flags/) nha.
```py
bool Check()
{
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
PVOID pHeapBase = !m_bIsWow64
? (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18))
: (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x1030));
DWORD dwHeapFlagsOffset = IsWindowsVistaOrGreater()
? 0x40
: 0x0C;
DWORD dwHeapForceFlagsOffset = IsWindowsVistaOrGreater()
? 0x44
: 0x10;
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
PVOID pHeapBase = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x30));
DWORD dwHeapFlagsOffset = IsWindowsVistaOrGreater()
? 0x70
: 0x14;
DWORD dwHeapForceFlagsOffset = IsWindowsVistaOrGreater()
? 0x74
: 0x18;
#endif // _WIN64
PDWORD pdwHeapFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapFlagsOffset);
PDWORD pdwHeapForceFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset);
return (*pdwHeapFlags & ~HEAP_GROWABLE) || (*pdwHeapForceFlags != 0);
}
```
### 2.4 Heap Protection
* Trong **Windows**, khi một chương trình đang được debug, hệ thống sẽ kích hoạt một số cơ chế để phát hiện lỗi bộ nhớ, đặc biệt là với `heap`. Hai trong số các cờ (`flag`) có liên quan là:
* Nếu cờ **HEAP_TAIL_CHECKING_ENABLED(`0x20`)** được set trong `NtGlobalFlag`:
* Sau mỗi khối bộ nhớ được cấp phát từ `heap`, hệ thống sẽ tự động chèn thêm chuỗi giá trị `0xABABABAB` vào cuối khối đó ($2$ lần trong $32$ bit và $4$ lần trong $64$ bit)
* Mục đích là để kiểm tra tràn bộ nhớ (buffer overflow) – nếu đoạn đệm này bị thay đổi, hệ thống sẽ biết có lỗi xảy ra.
* Nếu cờ **HEAP_FREE_CHECKING_ENABLED(`0x40`)** được set trong `NtGlobalFlag`:
* Khi một khối bộ nhớ bị giải phóng (free), và nếu còn dư byte trống chưa khớp với khối tiếp theo, hệ thống sẽ điền đoạn trống đó bằng chuỗi `0xFEEEFEEE`.
* Mục tiêu là để giúp phát hiện việc truy cập bộ nhớ sau khi đã bị free.
```py
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](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-kuser_shared_data) structure
* **KUSER_SHARED_DATA** là vùng nhớ đặc biệt của **Windows** nằm tại địa chỉ cố định `0x7FFE0000`, chứa các thông tin do `kernel` chia sẻ xuống `user mode` (thời gian, trạng thái hệ thống, v.v).
* Ví dụ: `0x7FFE02D4` là **offset** bên trong đó, được dùng để kiểm tra dấu hiệu có debugger.
* Vùng này:
* Chỉ đọc, không thể ghi từ `user mode`
* Địa chỉ và cấu trúc luôn cố định qua nhiều phiên bản **Windows**
($32$-bit & $64$-bit)
----> Kỹ thuật này dùng để anti-debug mà không cần $API$
(khó bị phát hiện hoặc hook).
```py
bool check_kuser_shared_data_structure()
{
unsigned char b = *(unsigned char*)0x7ffe02d4;
return ((b & 0x01) || (b & 0x02));
}
```
## Bypass
* Sửa lại logic chương trình
# Object Handles
* Trong **Windows**, mỗi tiến trình hoặc luồng có thể tương tác với các đối tượng `kernel` (như: `file`, `thread`, `mutex`, `event`, `process`...) thông qua một thứ gọi là `handle`. Khi một tiến trình bị debug, hệ điều hành sẽ tạo ra một số đối tượng đặc biệt như **DebugObject** và cấp `handle` đến chúng.
* Một số hàm **WinAPI** xử lý `handle` có thể hành xử khác khi bị debug hoặc tạo ra tác dụng phụ, từ đó có thể dùng để phát hiện debugger mà không cần dùng **API** debug công khai.
## 1. OpenProcess()
* **csrss.exe** (Client/Server Runtime Subsystem) là một tiến trình hệ thống quan trọng của **Windows**, chạy với quyền hệ thống (System).
* Khi một chương trình (hoặc debugger) cố mở `handle` đến **csrss.exe** bằng **OpenProcess()**, hệ thống sẽ kiểm tra quyền truy cập:
* Phải là `admin`
* Và có **SeDebugPrivilege** (đặc quyền gỡ lỗi hệ thống)
* Tức là, nếu debugger không có đủ quyền, `call` tới **OpenProcess** sẽ thất bại, từ đó chương trình có thể đoán là không chạy dưới debugger đặc quyền.
* Ngược lại, nếu `call` được ----> có thể dự đoán chương trình đang bị debug.
```py
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;
}
```
:::info
* Chỉ thành công khi chạy dưới quyền của nhóm **administrators** và **debug privileges**. Tuy nhiên, quyền **SeDebugPrivilege** sẽ được bật bởi trình debugger như **OllyDbg** hay **WinDbg**.
* ----> Tức là: nếu mở được **csrss.exe** ----> đồng nghĩa với việc có quyền **SeDebugPrivilege** trong access token ----> có thể đang debug. Với tham số truyền vào là **PID** của tiến trình **csrss.exe**.
:::
## 2. CreateFile()
* Khi một chương trình đang bị debug, **Windows** sẽ gửi một sự kiện đặc biệt gọi là **CREATE_PROCESS_DEBUG_EVENT** cho trình debug.
* Thông tin chi tiết của sự kiện này được lưu trong cấu trúc **[CREATE_PROCESS_DEBUG_INFO](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-create_process_debug_info)**, trong đó có một trường tên là `hFile` – đây là một `handle` trỏ đến file thực thi (exe) của tiến trình đang bị debug.
* Nếu debugger quên đóng `handle` này, thì file thực thi vẫn đang bị chiếm giữ, và không thể được mở lại với quyền **exclusive access** (độc quyền).
* Dựa vào đó, ta có thể phát hiện chương trình đang bị debug bằng cách gọi **API CreateFileW()** hoặc **CreateFileA()** từ `kernel32.dll` để cố gắng mở lại chính file thực thi của chương trình với quyền **exclusive**. Nếu lệnh mở file thất bại → rất có thể đang bị debugger giữ file → chương trình đang bị debug.
* Cấu trúc **CREATE_PROCESS_DEBUG_INFO**:
```py
typedef struct _CREATE_PROCESS_DEBUG_INFO {
HANDLE hFile;
HANDLE hProcess;
HANDLE hThread;
LPVOID lpBaseOfImage;
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpThreadLocalBase;
LPTHREAD_START_ROUTINE lpStartAddress;
LPVOID lpImageName;
WORD fUnicode;
} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;
```
* Ở đây, `hFile` chính là `handle` mà chúng ta nhắc đến.
```py
bool Check()
{
CHAR szFileName[MAX_PATH];
if (0 == GetModuleFileNameA(NULL, szFileName, sizeof(szFileName)))
return false;
return INVALID_HANDLE_VALUE == CreateFileA(szFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
}
```
## 3. CloseHandle()
* Trong **Windows**, hàm `CloseHandle()` được dùng để đóng một `handle` (ví dụ handle file, process, thread...).
* Thông thường, nếu chương trình chạy mà không có `try/except` hoặc `try/catch` để xử lý error, thì chương trình có thể lỗi ----> crash.
* Khi chạy dưới debugger, debugger sẽ can thiệp và bắt **exception** này, sau đó chuyển quyền điều khiển lại cho chương trình (thay vì để crash).
* Tuy nhiên, nếu cố tình truyền vào một `handle` không hợp lệ (invalid handle), thì:
* **Windows** sẽ gọi đến hàm `NtClose()` trong `ntdll.dll`.
* Kết quả là hệ thống sẽ phát sinh một **exception** có mã lỗi **EXCEPTION_INVALID_HANDLE** (`0xC0000008`).
:::success
Tức là chương trình sẽ làm như sau:
* Gọi **CloseHandle()** với một giá trị `handle` không hợp lệ (ví dụ `0xDEADBEEF`).
* Bọc nó trong một khối **__try/__except**.
* Nếu **exception** xảy ra mà chương trình vẫn tiếp tục chạy trong **__except**, thì có khả năng debugger đang can thiệp → chương trình bị debug.
:::
```py
bool Check()
{
__try
{
CloseHandle((HANDLE)0xDEADBEEF);
return false;
}
__except (EXCEPTION_INVALID_HANDLE == GetExceptionCode()
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH)
{
return true;
}
}
```
## 4. LoadLibrary()
* Phần này cũng tương tự như **2. CreateFile()**
* Khi một tệp(`.dll`, ...) được nạp vào bộ nhớ tiến trình bằng hàm **LoadLibraryW()** hoặc **LoadLibraryA()** (trong `kernel32.dll`), hệ điều hành sẽ lưu thông tin vào 1 cấu trúc có tên là **[LOAD_DLL_DEBUG_EVENT](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-load_dll_debug_info)**.
* Dựa vào đó, ta có thể phát hiện chương trình đang bị debug bằng cách nạp 1 tệp bằng **API** **LoadLibraryA()** sau đó gọi **CreateFileA()** từ `kernel32.dll` để cố gắng mở lại chính file thực thi của chương trình với quyền exclusive. Nếu lệnh mở file thất bại → rất có thể đang bị debugger giữ file → chương trình đang bị debug.
```py
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 một phiên gỡ lỗi (`debugging session`) bắt đầu, một đối tượng `kernel` có tên là `“debug object”` sẽ được tạo ra, và một `handle` sẽ được liên kết với `object` này.
* Bằng cách sử dụng hàm **NtQueryObject()** trong **ntdll.dll**, có thể truy vấn danh sách các đối tượng hiện có trong hệ thống và kiểm tra số lượng `handle` đang được liên kết với bất kỳ ``"debug object"`` nào đang tồn tại. Nếu $>0$ có thể đang bị debug.
:::success
* Tuy nhiên, kỹ thuật này không thể chắc chắn 100% rằng tiến trình hiện tại đang bị debug. Nó chỉ cho biết rằng có một trình debug đang chạy trên hệ thống kể từ lúc khởi động máy.
:::
```py
typedef struct _OBJECT_TYPE_INFORMATION
{
UNICODE_STRING TypeName;
ULONG TotalNumberOfHandles;
ULONG TotalNumberOfObjects;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_ALL_INFORMATION
{
ULONG NumberOfObjects;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
typedef NTSTATUS (WINAPI *TNtQueryObject)(
HANDLE Handle,
OBJECT_INFORMATION_CLASS ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength
);
enum { ObjectAllTypesInformation = 3 };
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
bool Check()
{
bool bDebugged = false;
NTSTATUS status;
LPVOID pMem = nullptr;
ULONG dwMemSize;
POBJECT_ALL_INFORMATION pObjectAllInfo;
PBYTE pObjInfoLocation;
HMODULE hNtdll;
TNtQueryObject pfnNtQueryObject;
hNtdll = LoadLibraryA("ntdll.dll");
if (!hNtdll)
return false;
pfnNtQueryObject = (TNtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");
if (!pfnNtQueryObject)
return false;
status = pfnNtQueryObject(
NULL,
(OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
&dwMemSize, sizeof(dwMemSize), &dwMemSize);
if (STATUS_INFO_LENGTH_MISMATCH != status)
goto NtQueryObject_Cleanup;
pMem = VirtualAlloc(NULL, dwMemSize, MEM_COMMIT, PAGE_READWRITE);
if (!pMem)
goto NtQueryObject_Cleanup;
status = pfnNtQueryObject(
(HANDLE)-1,
(OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
pMem, dwMemSize, &dwMemSize);
if (!SUCCEEDED(status))
goto NtQueryObject_Cleanup;
pObjectAllInfo = (POBJECT_ALL_INFORMATION)pMem;
pObjInfoLocation = (PBYTE)pObjectAllInfo->ObjectTypeInformation;
for(UINT i = 0; i < pObjectAllInfo->NumberOfObjects; i++)
{
POBJECT_TYPE_INFORMATION pObjectTypeInfo =
(POBJECT_TYPE_INFORMATION)pObjInfoLocation;
if (wcscmp(L"DebugObject", pObjectTypeInfo->TypeName.Buffer) == 0)
{
if (pObjectTypeInfo->TotalNumberOfObjects > 0)
bDebugged = true;
break;
}
// Get the address of the current entries
// string so we can find the end
pObjInfoLocation = (PBYTE)pObjectTypeInfo->TypeName.Buffer;
// Add the size
pObjInfoLocation += pObjectTypeInfo->TypeName.Length;
// Skip the trailing null and alignment bytes
ULONG tmp = ((ULONG)pObjInfoLocation) & -4;
// Not pretty but it works
pObjInfoLocation = ((PBYTE)tmp) + sizeof(DWORD);
}
NtQueryObject_Cleanup:
if (pMem)
VirtualFree(pMem, 0, MEM_RELEASE);
return bDebugged;
}
```
## Bypass
* Trace đến hàm check patch thành NOPS, hoặc hook các hàm và thay đổi giá trị trả về.
# Exceptions
## 1. UnhandledExceptionFilter()
* Khi chạy bình thường (không có debugger), chương trình gây ra lỗi (ví dụ: truy cập vào con trỏ null) mà không có bất kỳ khối `try/catch` hay `__try/__except` nào, thì hệ thống sẽ gọi hàm **UnhandledExceptionFilter()** trong `kernel32.dll`.
* Khi bị debug, trình debug như **OllyDbg**, **x64dbg** hay **WinDbg** sẽ chặn ngoại lệ trước khi hệ điều hành kịp gọi **UnhandledExceptionFilter()**.
```py
LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
PCONTEXT ctx = pExceptionInfo->ContextRecord;
ctx->Eip += 3; // Skip \xCC\xEB\x??
return EXCEPTION_CONTINUE_EXECUTION;
}
bool Check()
{
bool bDebugged = true;
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)UnhandledExceptionFilter);
__asm
{
int 3 // CC
jmp near being_debugged // EB ??
}
bDebugged = false;
being_debugged:
return bDebugged;
}
```
## 2. RaiseException()
* Các `exception` như **DBG_CONTROL_C** (`0x40010005`) hoặc **DBG_RIPEVENT** (`0x40010007`) là những **exception** đặc biệt được thiết kế để debugger xử lý trước, thay vì được chuyển đến `exception` **handler** của tiến trình.
* Bằng cách kích hoạt các `exception` này thông qua hàm **RaiseException** trong `kernel32.dll`, ta có thể kiểm tra xem chương trình có đang bị debug hay không:
* Nếu chương trình không bị debug, `exception` sẽ được chuyển đến **handler** đã đăng ký và **handler** sẽ được gọi.
* Nếu chương trình đang bị debug, `exception` sẽ bị debugger bắt trước, khiến **handler** không được gọi.
```py
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;
}
}
```
## 3. Hiding Control Flow with Exception Handlers
* Chương trình đăng ký nhiều `exception` **handler** (có thể là **Structured Exception Handler** - **SEH** hoặc **Vectored Exception Handler** - **VEH**).
* Trong **handler** đầu tiên, chương trình cố tình gây lỗi hoặc **raise** một **exception** mới.
* **Exception** mới này được chuyển tiếp sang **handler** kế tiếp, lại **raise** một **exception** khác.
* Quá trình cứ tiếp diễn như vậy (giống như một chuỗi).
* Cuối cùng, sau một chuỗi các **handler**, chương trình điều hướng đến hàm thực sự cần chạy (ví dụ: main logic, decrypt, shellcode...).
* **Mục đích**: ẩn luồng chương trình thực sự, thay vì `call/ jmp` bình thường. Phần chính thực sự nằm sâu trong các **exception handler**.
* **Structured Exception Handlers(`SEH`)**
```py
#include <Windows.h>
void MaliciousEntry()
{
// ...
}
void Trampoline2()
{
__try
{
__asm int 3;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
MaliciousEntry();
}
}
void Trampoline1()
{
__try
{
__asm int 3;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Trampoline2();
}
}
int main(void)
{
__try
{
__asm int 3;
}
__except (EXCEPTION_EXECUTE_HANDLER) {}
{
Trampoline1();
}
return 0;
}
```
* **Vectored Exception Handlers(`VEH`)**
```py
#include <Windows.h>
PVOID g_pLastVeh = nullptr;
void MaliciousEntry()
{
// ...
}
LONG WINAPI ExeptionHandler2(PEXCEPTION_POINTERS pExceptionInfo)
{
MaliciousEntry();
ExitProcess(0);
}
LONG WINAPI ExeptionHandler1(PEXCEPTION_POINTERS pExceptionInfo)
{
if (g_pLastVeh)
{
RemoveVectoredExceptionHandler(g_pLastVeh);
g_pLastVeh = AddVectoredExceptionHandler(TRUE, ExeptionHandler2);
if (g_pLastVeh)
__asm int 3;
}
ExitProcess(0);
}
int main(void)
{
g_pLastVeh = AddVectoredExceptionHandler(TRUE, ExeptionHandler1);
if (g_pLastVeh)
__asm int 3;
return 0;
}
```
## Bypass
* NOPS, ...
# Timing
* Khi process bị debug, thì thời gian bị delay giữa các **instructions** là khá lớn so với lúc bình thường.
## 1. RDPMC/RDTSC
* Cả hai lệnh này có thể được dùng để đo thời gian thực thi một đoạn code, đều sử dụng cờ **PCE** trong thanh ghi **CR4**. Khi đo khoảng thời gian (theo số chu kỳ CPU) giữa 2 điểm trong chương trình, nếu khoảng thời gian đó lớn bất thường, rất có thể chương trình đang bị debug.
* **RDPMC (`Read Performance Monitoring Counters`)**: chỉ chạy ở **Kernel Mode**, không thể dùng trong **User Mode**. Thường dùng để đo thời gian thực thi chính xác cho một đoạn mã, bởi vì nó cho ta số chu kỳ CPU đã chạy, rất nhỏ và chính xác hơn nhiều so với đo bằng giây hoặc mili-giây.
```py
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;
}
```
* **RDTSC (`Read Time-Stamp Counter`)**: chỉ chạy trong **User Mode** (**IDA, ...**), đọc giá trị từ **Performance Monitoring Counters** (`PMC`) - những bộ đếm phần cứng trong CPU dùng để theo dõi các sự kiện như: Số lệnh đã thực thi, ...
```py
bool IsDebugged(DWORD64 qwNativeElapsed)
{
ULARGE_INTEGER Start, End;
__asm
{
xor ecx, ecx
rdtsc
mov Start.LowPart, eax
mov Start.HighPart, edx
}
// ... some work
__asm
{
xor ecx, ecx
rdtsc
mov End.LowPart, eax
mov End.HighPart, edx
}
return (End.QuadPart - Start.QuadPart) > qwNativeElapsed;
}
```
## 2. GetLocalTime, GetSystemTime, GetTickCount(), ...
* Cơ chế thì cũng giống `RDPMC/RDTSC`, chỉ khác là sử dụng các **Windows API** như **GetLocalTime, GetSystemTime, GetTickCount, timeGetTime, QueryPerformanceCounter,...**. Đều ở chế độ **User mode**.
* **GetLocalTime()**:
```py
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;
}
```
* **GetSystemTime()**:
```py
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;
}
```
* **GetTickCount()**:
```py
bool IsDebugged(DWORD dwNativeElapsed)
{
DWORD dwStart = GetTickCount();
// ... some work
return (GetTickCount() - dwStart) > dwNativeElapsed;
}
```
* **timeGetTime()**:
```py
bool IsDebugged(DWORD dwNativeElapsed)
{
DWORD dwStart = timeGetTime();
// ... some work
return (timeGetTime() - dwStart) > dwNativeElapsed;
}
```
* **QueryPerformanceCounter()**:
```py
bool IsDebugged(DWORD64 qwNativeElapsed)
{
LARGE_INTEGER liStart, liEnd;
QueryPerformanceCounter(&liStart);
// ... some work
QueryPerformanceCounter(&liEnd);
return (liEnd.QuadPart - liStart.QuadPart) > qwNativeElapsed;
}
```
## 3. ZwGetTickCount() / KiGetTickCount()
* 2 hàm đều thuộc **Windows Native API** (`NTDLL`).
* Nó truy cập vào vùng **KUSER_SHARED_DATA** để lấy số tick (ms) từ lúc hệ thống khởi động.
* Bản chất tương tự **GetTickCount()**, nhưng được sử dụng từ **kernel mode**.
```py
bool IsDebugged(DWORD64 qwNativeElapsed)
{
ULARGE_INTEGER Start, End;
__asm
{
int 2ah // Trả về tickcount vào EAX:EDX
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;
}
```
## Bypass
* patch các hàm check time = NOPs hoặc set lại giá trị trả về.
# Process Memory
## 1. Breakpoints
### 1.1 Software Breakpoints (INT3)
* Khi bạn đặt breakpoint vào hàm A:
* Debugger sẽ ghi đè byte đầu tiên của hàm A bằng `0xCC` (lệnh `INT 3`).
* Khi chương trình chạy đến đó, CPU sinh ngắt, debugger sẽ bắt lại.
```py
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
* Để detect debugger đang sử dụng chức năng **"Step Over"** (`F8`) để đi qua hàm mà không nhảy vào trong hàm đó.
* Nó tạm thời đặt 1 breakpoint (`0xCC`) tại địa chỉ sau lệnh `CALL` → tức là **return address**.
* Khi `CALL` chạy xong, nhảy về **return address**, thì gặp `0xCC` → debugger sẽ tiếp tục điều khiển.
* ----> Nếu chương trình kiểm tra byte tại **return address** mà thấy `0xCC`, thì nó biết đang bị debug bằng **Step Over**.
#### 1.2.1 Direct Memory Modification
* Trong function, sử dụng `ReturnAddress` để lấy địa chỉ trả về ngay sau của hàm đang được thực thi. Nếu bằng `0xCC` thì có thể ghi đè bằng **NOP**, ...
```py
#include <intrin.h>
#pragma intrinsic(_ReturnAddress)
void foo()
{
PVOID pRetAddress = _ReturnAddress(); // Lấy địa chỉ trả về sau khi gọi foo()
if (*(PBYTE)pRetAddress == 0xCC) // Nếu là breakpoint
{
DWORD dwOldProtect;
if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect))
{
*(PBYTE)pRetAddress = 0x90; // Ghi đè bằng NOP
VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect); // Khôi phục quyền
}
}
// ...
}
```
#### 1.2.2 [ReadFile()](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile)
```py
BOOL ReadFile(
[in] HANDLE hFile,
[out] LPVOID lpBuffer,
[in] DWORD nNumberOfBytesToRead,
[out, optional] LPDWORD lpNumberOfBytesRead,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
```
* `hFile`: **Handle** đến một file đang mở (có thể là chính file thực thi .exe của chương trình hiện tại).
* `lpBuffer`: Con trỏ đến bộ nhớ đệm dùng để chứa dữ liệu được đọc.
* `nNumberOfBytesToRead`: Số byte cần đọc.
----> Nếu `lpBuffer` là một địa chỉ nhạy cảm trong chương trình (ví dụ: địa chỉ trả về của hàm hiện tại), **ReadFile()** sẽ ghi đè dữ liệu lên đó - chính là ký tự ``'M' (0x4D)`` – ký tự đầu của header `PE`: ``'MZ'``.
```py
#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()](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory)
* Tương tự nhưng sử dụng **API** **WriteProcessMemory()** của `kernel32.dll`.
```py
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
```
```py
#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()](https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-toolhelp32readprocessmemory)
* Cho phép đọc bộ nhớ của một process khác – tương tự `ReadProcessMemory()`. Khác ở chỗ nó là **API ẩn** còn `ReadProcessMemory()` phổ biến hơn.
* Nó được dùng để đọc lại mã tại địa chỉ return của chính mình (hoặc lệnh tiếp theo sau call), so sánh với opcode gốc. Nếu phát hiện thấy `0xCC` tại địa chỉ kế tiếp ----> có debugger.
```py
BOOL Toolhelp32ReadProcessMemory(
[in] DWORD th32ProcessID,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T cbRead,
[out] SIZE_T *lpNumberOfBytesRead
);
```
### 1.3. Memory Breakpoints
* Memory breakpoint dùng guard page để phát hiện truy cập bộ nhớ, sinh exception **STATUS_GUARD_PAGE_VIOLATION**. Guard page được tạo bằng cách dùng **PAGE_GUARD** trong các hàm `VirtualAlloc/VirtualProtect`.
* Kỹ thuật anti-debug lạm dụng cơ chế này: tạo bộ đệm chứa lệnh `RET (0xC3)`, đánh dấu là guard page, đẩy địa chỉ xử lý debugger lên stack, rồi nhảy tới bộ đệm. Nếu có debugger, exception được xử lý, lệnh `RET` thực thi và nhảy đến địa chỉ đã đẩy ⇒ phát hiện debugger. Nếu không, exception không được xử lý và chương trình vào trình xử lý ngoại lệ.
```py
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
* Các thanh ghi debug `DR0, DR1, DR2 và DR3` lưu địa chỉ các **hardware breakpoints**. Ta có thể lấy giá trị các thanh ghi này từ context của thread.
* Nếu các thanh ghi này chứa giá trị khác $0$, điều đó có thể cho thấy chương trình đang debug, vì debugger thường thiết lập **hardware breakpoint** bằng cách ghi địa chỉ vào các thanh ghi này.
```py
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()
* Memory page của tiến trình, đặc biệt là section code, là vùng nhớ chia sẻ giữa các process cho đến khi page này bị ghi đè. Khi đó, OS tạo một bản copy của page và map vào vùng nhớ ảo(process virtual memory), khiến page đó không còn được chia sẻ nữa.
* Quá trình này gọi là [Copy-on-write](https://www.studytonight.com/operating-system/copyonwrite-in-operating-system) và xảy ra khi debugger đặt software breakpoint vào phần code.
* Do đó, ta có thể truy vấn [Working Set](https://learn.microsoft.com/en-us/windows/win32/memory/working-set?redirectedfrom=MSDN) của tiến trình hiện tại và kiểm tra hai trường **Shared** và **ShareCoun**. Nếu có software breakpoint, hai trường này sẽ không được set.
```py
namespace ntdll
{
//...
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
// ...
typedef enum _MEMORY_INFORMATION_CLASS {
MemoryBasicInformation,
MemoryWorkingSetList,
} MEMORY_INFORMATION_CLASS;
// ...
typedef union _PSAPI_WORKING_SET_BLOCK {
ULONG Flags;
struct {
ULONG Protection :5;
ULONG ShareCount :3;
ULONG Shared :1;
ULONG Reserved :3;
ULONG VirtualPage:20;
};
} PSAPI_WORKING_SET_BLOCK, *PPSAPI_WORKING_SET_BLOCK;
typedef struct _MEMORY_WORKING_SET_LIST
{
ULONG NumberOfPages;
PSAPI_WORKING_SET_BLOCK WorkingSetList[1];
} MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST;
// ...
}
```
```py
bool IsDebugged()
{
#ifndef _WIN64
NTSTATUS status;
PBYTE pMem = nullptr;
DWORD dwMemSize = 0;
do
{
dwMemSize += 0x1000;
pMem = (PBYTE)_malloca(dwMemSize);
if (!pMem)
return false;
memset(pMem, 0, dwMemSize);
status = ntdll::NtQueryVirtualMemory(
GetCurrentProcess(),
NULL,
ntdll::MemoryWorkingSetList,
pMem,
dwMemSize,
NULL);
} while (status == STATUS_INFO_LENGTH_MISMATCH);
ntdll::PMEMORY_WORKING_SET_LIST pWorkingSet = (ntdll::PMEMORY_WORKING_SET_LIST)pMem;
for (ULONG i = 0; i < pWorkingSet->NumberOfPages; i++)
{
DWORD dwAddr = pWorkingSet->WorkingSetList[i].VirtualPage << 0x0C;
DWORD dwEIP = 0;
__asm
{
push eax
call $+5
pop eax
mov dwEIP, eax
pop eax
}
if (dwAddr == (dwEIP & 0xFFFFF000))
return (pWorkingSet->WorkingSetList[i].Shared == 0) || (pWorkingSet->WorkingSetList[i].ShareCount == 0);
}
#endif // _WIN64
return false;
}
```
### 2.2. Detecting a function patch
* Một cách phổ biến để phát hiện debugger là gọi `kernel32!IsDebuggerPresent()`. Kẻ tấn công/debugger có thể thay đổi giá trị trả về trong **EAX** hoặc patch trực tiếp mã máy của hàm này để tránh bị phát hiện.
* Thay vì tìm breakpoint trong bộ nhớ, ta có thể kiểm tra xem hàm `IsDebuggerPresent()` có bị sửa đổi không bằng cách đọc các byte đầu tiên của hàm này và so sánh với cùng hàm đó từ các tiến trình khác.
* Dù có bật **ASLR**, thư viện của Windows vẫn được nạp ở cùng địa chỉ cơ sở (`base address`) cho tất cả tiến trình trong cùng một phiên làm việc (trừ khi `reboot`).
```py
bool IsDebuggerPresent()
{
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32)
return false;
FARPROC pIsDebuggerPresent = GetProcAddress(hKernel32, "IsDebuggerPresent");
if (!pIsDebuggerPresent)
return false;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (INVALID_HANDLE_VALUE == hSnapshot)
return false;
PROCESSENTRY32W ProcessEntry;
ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);
if (!Process32FirstW(hSnapshot, &ProcessEntry))
return false;
bool bDebuggerPresent = false;
HANDLE hProcess = NULL;
DWORD dwFuncBytes = 0;
const DWORD dwCurrentPID = GetCurrentProcessId();
do
{
__try
{
if (dwCurrentPID == ProcessEntry.th32ProcessID)
continue;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessEntry.th32ProcessID);
if (NULL == hProcess)
continue;
if (!ReadProcessMemory(hProcess, pIsDebuggerPresent, &dwFuncBytes, sizeof(DWORD), NULL))
continue;
if (dwFuncBytes != *(PDWORD)pIsDebuggerPresent)
{
bDebuggerPresent = true;
break;
}
}
__finally
{
if (hProcess)
CloseHandle(hProcess);
}
} while (Process32NextW(hSnapshot, &ProcessEntry));
if (hSnapshot)
CloseHandle(hSnapshot);
return bDebuggerPresent;
}
```
### 2.3 Patch ntdll!DbgBreakPoint()
* Hàm `ntdll!DbgBreakPoint()` được gọi khi debugger attach vào tiến trình đang chạy, nhằm tạo một exception để debugger có thể chặn và kiểm soát tiến trình.
* Nếu ta ghi đè byte đầu tiên của hàm này thành `0xC3 (RET)`, thì exception không còn được tạo ra — debugger sẽ không thể break-in, và thread sẽ thoát ra mà không bị debug.
* Chống debug bằng cách patch hàm `DbgBreakPoint()` để vô hiệu hóa debugger attach.
```py
void Patch_DbgBreakPoint()
{
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll)
return;
FARPROC pDbgBreakPoint = GetProcAddress(hNtdll, "DbgBreakPoint");
if (!pDbgBreakPoint)
return;
DWORD dwOldProtect;
if (!VirtualProtect(pDbgBreakPoint, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect))
return;
*(PBYTE)pDbgBreakPoint = (BYTE)0xC3; // ret
}
```
### 2.4. Patch ntdll!DbgUiRemoteBreakin()
* Khi debugger gọi `kernel32!DebugActiveProcess()`, nó sẽ dẫn đến gọi `ntdll!DbgUiRemoteBreakin()` trong tiến trình bị debug.
* Ta có thể patch hàm `ntdll!DbgUiRemoteBreakin()` để thay vì chờ debugger attach, nó sẽ gọi `kernel32!TerminateProcess()` nhằm kết thúc tiến trình ngay lập tức.
```py
6A 00 push 0
68 FF FF FF FF push -1 ; GetCurrentProcess() result
B8 XX XX XX XX mov eax, kernel32!TreminateProcess
FF D0 call eax
```
```py
#pragma pack(push, 1)
struct DbgUiRemoteBreakinPatch
{
WORD push_0;
BYTE push;
DWORD CurrentPorcessHandle;
BYTE mov_eax;
DWORD TerminateProcess;
WORD call_eax;
};
#pragma pack(pop)
void Patch_DbgUiRemoteBreakin()
{
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll)
return;
FARPROC pDbgUiRemoteBreakin = GetProcAddress(hNtdll, "DbgUiRemoteBreakin");
if (!pDbgUiRemoteBreakin)
return;
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32)
return;
FARPROC pTerminateProcess = GetProcAddress(hKernel32, "TerminateProcess");
if (!pTerminateProcess)
return;
DbgUiRemoteBreakinPatch patch = { 0 };
patch.push_0 = '\x6A\x00';
patch.push = '\x68';
patch.CurrentPorcessHandle = 0xFFFFFFFF;
patch.mov_eax = '\xB8';
patch.TerminateProcess = (DWORD)pTerminateProcess;
patch.call_eax = '\xFF\xD0';
DWORD dwOldProtect;
if (!VirtualProtect(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch), PAGE_READWRITE, &dwOldProtect))
return;
::memcpy_s(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch),
&patch, sizeof(DbgUiRemoteBreakinPatch));
VirtualProtect(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch), dwOldProtect, &dwOldProtect);
}
```
### 2.5 Performing Code Checksums
* Kiểm tra tính toàn vẹn của mã bằng các thuật toán hash như **CRC32**,..., liên tục check và nếu checksum thay đổi ----> debugger detect.
* Phát hiện debugger chèn breakpoints, step-over, hoặc hook hàm, ...
```py
PVOID g_pFuncAddr;
DWORD g_dwFuncSize;
DWORD g_dwOriginalChecksum;
static void VeryImportantFunction()
{
// ...
}
static DWORD WINAPI ThreadFuncCRC32(LPVOID lpThreadParameter)
{
while (true)
{
if (CRC32((PBYTE)g_pFuncAddr, g_dwFuncSize) != g_dwOriginalChecksum)
ExitProcess(0);
Sleep(10000);
}
return 0;
}
size_t DetectFunctionSize(PVOID pFunc)
{
PBYTE pMem = (PBYTE)pFunc;
size_t nFuncSize = 0;
do
{
++nFuncSize;
} while (*(pMem++) != 0xC3);
return nFuncSize;
}
int main()
{
g_pFuncAddr = (PVOID)&VeryImportantFunction;
g_dwFuncSize = DetectFunctionSize(g_pFuncAddr);
g_dwOriginalChecksum = CRC32((PBYTE)g_pFuncAddr, g_dwFuncSize);
HANDLE hChecksumThread = CreateThread(NULL, NULL, ThreadFuncCRC32, NULL, NULL, NULL);
// ...
return 0;
}
```
## Bypass
* NOPs, ...
# Assembly instructions
## 1. INT 3
* Lệnh `INT 3` là một lệnh ngắt phần mềm (`software interrupt`) để đánh dấu `breakpoint` tại một vị trí trong mã chương trình.
* Khi thực thi lệnh `INT 3`, **CPU** sinh ra một exception có mã là `EXCEPTION_BREAKPOINT (0x80000003)` và 1 `exception handler` sẽ được gọi.
* Nếu có debugger ----> luồng điều khiển sẽ không tới được ``exception handler`` đó mà thuộc về debugger tùy cách nó xử lý. ----> Debugger detect.
```py
bool IsDebugged()
{
__try
{
__asm int 3;
return true;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
}
```
## 2. INT 2D
* Cũng `raise EXCEPTION_BREAKPOINT exception` như `INT 3`.
* Nhưng với `INT 2D`, Windows sử dụng thanh ghi `EIP (instruction pointer)` như một exception address, nếu `EAX == 1, 3, 4` trên tất cả phiên bản Windows, hoặc `EAX == 5` từ Windows Vista trở đi → `EIP` được tăng thêm `1`.
* Điều này có thể gây ra vấn đề cho debugger khi tăng `EIP` như vậy thì byte ngay sau `INT 2D` có thể bị bỏ qua --> crash chương trình.
```py
bool IsDebugged()
{
__try
{
__asm xor eax, eax; // EAX = 0, nên EIP sẽ không bị tăng
__asm int 0x2D; // Sinh ra EXCEPTION_BREAKPOINT
__asm nop; // Byte an toàn nếu bị bỏ qua
return true;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
}
```
## 3. DebugBreak
* **DebugBreak()** là hàm **API** của Windows, gây ra một `EXCEPTION_BREAKPOINT` tại thời điểm gọi, tương tự như `INT 3`.
* Nếu chương trình được chạy dưới debugger → debugger sẽ chặn exception và xử lý.
* Nếu không có debugger, thì exception sẽ được chuyển đến `exception handler` trong chương trình.
```py
__try{
DebugBreak();
}
__except(EXCEPTION_BREAKPOINT){
return false;
}
return true;
```
## 4. ICE
* **ICE** là viết tắt của **In-Circuit Emulator** – một instruction không được Intel công bố chính thức có opcode `0xF1`.
* Khi thực thi **ICE**, CPU sẽ raise một exception `EXCEPTION_SINGLE_STEP (0x80000004)`.
* Nếu không có debugger: `exception handler` trong chương trình sẽ được gọi.
* Nếu đang bị trace/debug:
* Debugger sẽ intercept exception này vì `EXCEPTION_SINGLE_STEP` là exception thường gặp khi `Trap Flag` được set.
* Debugger bỏ qua `handler`, tiếp tục thực thi bình thường sau lệnh `0xF1` → debugger detect.
```py
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 anti-debug lợi dụng cơ chế single-stepping của debugger.
* Khi trace qua đoạn lệnh sau:
```py
asm
Copy
Edit
push ss
pop ss
pushf
```
→ CPU set `Trap Flag (TF)` trong `EFLAGS` vì thao tác `MOV SS,` … (hoặc tương đương `POP SS`) sẽ kích hoạt delay đến lệnh sau.
* Bình thường debugger sẽ clear `TF` sau mỗi step, nhưng nếu chúng ta đọc giá trị `EFLAGS` ngay sau lệnh `pop ss` bằng `pushf` thì có thể kiểm tra `TF` có đang set hay không.
* Nếu `Trap Flag == 1` → chương trình đang bị trace.
```py
bool IsDebugged()
{
bool bTraced = false;
__asm
{
push ss // Đẩy segment stack hiện tại vào stack
pop ss // Gán lại → gây ra trap khi debug
pushf // Đẩy EFLAGS vào stack
test byte ptr [esp+1], 1 // Kiểm tra Trap Flag (bit 8 của EFLAGS)
jz not_being_debugged // Nếu không có TF → không bị debug
}
bTraced = true; // Trap Flag được set → đang bị debug
not_being_debugged:
__asm popf; // Khôi phục lại EFLAGS
return bTraced;
}
```
## 6. Instruction Counting
* Phát hiện debugger bằng cách đếm số lượng lệnh thực thi qua `EXCEPTION_SINGLE_STEP`.
* Đặt `hardware breakpoint (HWBP)` lên mỗi `instruction` (ví dụ `NOP`) trong một chuỗi lệnh.
* Mỗi lần `HWBP` bị hit → CPU `raise EXCEPTION_SINGLE_STEP` → được bắt bởi `Vectored Exception Handler`.
* Trong `handler`, ta:
* Tăng `EAX` làm bộ đếm `instruction`.
* Tăng `EIP` để nhảy đến `instruction` tiếp theo.
* Sau chuỗi `N` lệnh, nếu `EAX != N` → debugger can thiệp hoặc bỏ sót → debugger detect.
```py
#include "hwbrk.h"
static LONG WINAPI InstructionCountingExeptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
pExceptionInfo->ContextRecord->Eax += 1;
pExceptionInfo->ContextRecord->Eip += 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
__declspec(naked) DWORD WINAPI InstructionCountingFunc(LPVOID lpThreadParameter)
{
__asm
{
xor eax, eax
nop
nop
nop
nop
cmp al, 4
jne being_debugged
}
ExitThread(FALSE);
being_debugged:
ExitThread(TRUE);
}
bool IsDebugged()
{
PVOID hVeh = nullptr;
HANDLE hThread = nullptr;
bool bDebugged = false;
__try
{
hVeh = AddVectoredExceptionHandler(TRUE, InstructionCountingExeptionHandler);
if (!hVeh)
__leave;
hThread = CreateThread(0, 0, InstructionCountingFunc, NULL, CREATE_SUSPENDED, 0);
if (!hThread)
__leave;
PVOID pThreadAddr = &InstructionCountingFunc;
// Fix thread entry address if it is a JMP stub (E9 XX XX XX XX)
if (*(PBYTE)pThreadAddr == 0xE9)
pThreadAddr = (PVOID)((DWORD)pThreadAddr + 5 + *(PDWORD)((PBYTE)pThreadAddr + 1));
for (auto i = 0; i < m_nInstructionCount; i++)
m_hHwBps[i] = SetHardwareBreakpoint(
hThread, HWBRK_TYPE_CODE, HWBRK_SIZE_1, (PVOID)((DWORD)pThreadAddr + 2 + i));
ResumeThread(hThread);
WaitForSingleObject(hThread, INFINITE);
DWORD dwThreadExitCode;
if (TRUE == GetExitCodeThread(hThread, &dwThreadExitCode))
bDebugged = (TRUE == dwThreadExitCode);
}
__finally
{
if (hThread)
CloseHandle(hThread);
for (int i = 0; i < 4; i++)
{
if (m_hHwBps[i])
RemoveHardwareBreakpoint(m_hHwBps[i]);
}
if (hVeh)
RemoveVectoredExceptionHandler(hVeh);
}
return bDebugged;
}
```
## 7. POPF and Trap Flag
* Dựa vào việc thiết lập `Trap Flag (TF)` trong thanh ghi `EFLAGS` qua lệnh `popfd`.
* Khi `TF = 1`, CPU sẽ `raise exception EXCEPTION_SINGLE_STEP` sau khi thực thi lệnh tiếp theo.
* Nếu chương trình không bị debug: `exception` này sẽ được raise và bắt được trong `__except`.
* Nếu chương trình bị debug: debugger thường sẽ tự động clear `TF` để tránh `exception`, nên `exception` sẽ không xảy ra → sẽ không vào `__except` --> debugger detect.
```py
bool IsDebugged()
{
__try
{
__asm
{
pushfd // Đẩy EFLAGS hiện tại vào stack
mov dword ptr [esp], 0x100 // Thay thế TF (bit 8) = 1
popfd // Lấy lại EFLAGS với TF = 1
nop // Lệnh kế tiếp để trigger exception nếu TF được set
}
return true; // Nếu không xảy ra exception, tức không bị debug
}
__except(GetExceptionCode() == EXCEPTION_SINGLE_STEP
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_EXECUTION)
{
return false; // Bắt được exception Single Step → không bị debug
}
}
```
## 8. Instruction Prefixes
* Lợi dụng cách debugger (ví dụ OllyDbg) xử lý các `instruction prefixes` (tiền tố lệnh).
* Trong debugger như OllyDbg, khi bước qua byte `0xF3`, debugger sẽ bỏ qua `prefix` và chuyển thẳng tới lệnh `INT1 (opcode 0xF1)` --> không xảy ra `exception` → `__try` kết thúc bình thường và `return true`.
* Khi chạy bình thường (không debug), CPU gặp chuỗi byte này sẽ gây ra `exception` vì `0xF3 0x64 0xF1` không phải là một lệnh hợp lệ → chương trình vào khối `exception handler`.
```py
__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;
}
```
## Bypass
* NOPs, ...
# Interactive Checks
* "Direct debugger interaction" — những kỹ thuật này cho phép tiến trình đang chạy (running process) tự điều khiển một giao diện người dùng (user interface) hoặc tương tác trực tiếp với tiến trình cha (parent process) của nó, nhằm phát hiện ra các điểm bất thường, không nhất quán vốn chỉ xảy ra khi tiến trình đó bị gỡ lỗi (debugged).
* Nói cách khác, thay vì chỉ "bí mật" quan sát hoặc dò lỗi từ bên trong, các kỹ thuật này chủ động tương tác với debugger hoặc môi trường gỡ lỗi để phát hiện xem liệu chương trình có đang bị theo dõi, bị thao tác hay không.
## 1. Self-Debugging
* Một tiến trình (process) tự tạo ra một bản sao (instance) của chính nó.
* Bản sao này sẽ đóng vai trò debugger cho tiến trình “bản chính” (parent).
* Vì Windows chỉ cho phép 1 debugger duy nhất đính kèm vào một process, nếu tiến trình “bản chính” đang bị debug bởi một debugger khác (ví dụ OllyDbg, x64dbg…), thì việc đính kèm debugger của bản sao sẽ thất bại.
* Nếu thất bại, bản sao sẽ báo lại cho tiến trình bản chính biết là có debugger khác đang hiện diện → debugger detect.
```py
#define EVENT_SELFDBG_EVENT_NAME L"SelfDebugging"
bool IsDebugged()
{
WCHAR wszFilePath[MAX_PATH], wszCmdLine[MAX_PATH];
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
HANDLE hDbgEvent;
hDbgEvent = CreateEventW(NULL, FALSE, FALSE, EVENT_SELFDBG_EVENT_NAME);
if (!hDbgEvent)
return false;
if (!GetModuleFileNameW(NULL, wszFilePath, _countof(wszFilePath)))
return false;
swprintf_s(wszCmdLine, L"%s %d", wszFilePath, GetCurrentProcessId());
if (CreateProcessW(NULL, wszCmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return WAIT_OBJECT_0 == WaitForSingleObject(hDbgEvent, 0);
}
return false;
}
bool EnableDebugPrivilege()
{
bool bResult = false;
HANDLE hToken = NULL;
DWORD ec = 0;
do
{
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
break;
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid))
break;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if( !AdjustTokenPrivileges( hToken, FALSE, &tp, sizeof(tp), NULL, NULL))
break;
bResult = true;
}
while (0);
if (hToken)
CloseHandle(hToken);
return bResult;
}
int main(int argc, char **argv)
{
if (argc < 2)
{
if (IsDebugged())
ExitProcess(0);
}
else
{
DWORD dwParentPid = atoi(argv[1]);
HANDLE hEvent = OpenEventW(EVENT_MODIFY_STATE, FALSE, EVENT_SELFDBG_EVENT_NAME);
if (hEvent && EnableDebugPrivilege())
{
if (FALSE == DebugActiveProcess(dwParentPid))
SetEvent(hEvent);
else
DebugActiveProcessStop(dwParentPid);
}
ExitProcess(0);
}
// ...
return 0;
}
```
## 2. GenerateConsoleCtrlEvent()
* Khi người dùng nhấn `Ctrl+C` hoặc `Ctrl+Break` trong `console`, Windows sẽ gửi một tín hiệu (signal) tương ứng đến các tiến trình `console`.
* Mặc định, các tiến trình console có bộ xử lý (`handler`) mặc định để nhận tín hiệu này, và thường sẽ gọi `ExitProcess()` để thoát chương trình.
* Tuy nhiên, ta có thể đăng ký một `handler` riêng để bỏ qua hoặc xử lý tín hiệu `Ctrl+C/Break` này theo cách khác.
* Đặc biệt, nếu tiến trình đang bị debug và tín hiệu `Ctrl+C` chưa bị vô hiệu hóa, Windows sẽ sinh ra ngoại lệ `DBG_CONTROL_C` thay vì chỉ đơn giản gửi tín hiệu.
* Thông thường, debugger sẽ chặn ngoại lệ này, nhưng nếu ta tự đăng ký một `vectored exception handler` để bắt ngoại lệ, ta có thể nhận biết được ngoại lệ này.
* Nếu bắt được exception `DBG_CONTROL_C` trong `handler` ----> debugger detect.
```py
bool g_bDebugged{ false };
std::atomic<bool> g_bCtlCCatched{ false };
static LONG WINAPI CtrlEventExeptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
if (pExceptionInfo->ExceptionRecord->ExceptionCode == DBG_CONTROL_C)
{
g_bDebugged = true;
g_bCtlCCatched.store(true);
}
return EXCEPTION_CONTINUE_EXECUTION;
}
static BOOL WINAPI CtrlHandler(DWORD fdwCtrlType)
{
switch (fdwCtrlType)
{
case CTRL_C_EVENT:
g_bCtlCCatched.store(true);
return TRUE;
default:
return FALSE;
}
}
bool IsDebugged()
{
PVOID hVeh = nullptr;
BOOL bCtrlHandlerSet = FALSE;
__try
{
hVeh = AddVectoredExceptionHandler(TRUE, CtrlEventExeptionHandler);
if (!hVeh)
__leave;
bCtrlHandlerSet = SetConsoleCtrlHandler(CtrlHandler, TRUE);
if (!bCtrlHadnlerSet)
__leave;
GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
while (!g_bCtlCCatched.load())
;
}
__finally
{
if (bCtrlHadnlerSet)
SetConsoleCtrlHandler(CtrlHandler, FALSE);
if (hVeh)
RemoveVectoredExceptionHandler(hVeh);
}
return g_bDebugged;
}
```
## 3. BlockInput()
* Hàm `user32!BlockInput()` được dùng để khóa toàn bộ các sự kiện chuột và bàn phím, rất hiệu quả để ngăn chặn debugger can thiệp.
* Trên Windows Vista trở lên, để gọi hàm này thành công cần quyền administrator.
* Một số công cụ chống debug có thể hook (chèn code giả mạo) hàm `BlockInput()` để vô hiệu hóa hoặc thay đổi hành vi của nó.
* Cách phát hiện hook:
* Theo thiết kế, `BlockInput(TRUE)` chỉ cho phép lock đầu vào một lần; lần gọi thứ hai sẽ trả về `FALSE`.
* Nếu cả hai lần gọi đều trả về `TRUE`, rất có thể hàm này đã bị hook ----> debugger detect.
```py
bool IsHooked ()
{
BOOL bFirstResult = FALSE, bSecondResult = FALSE;
__try
{
bFirstResult = BlockInput(TRUE);
bSecondResult = BlockInput(TRUE);
}
__finally
{
BlockInput(FALSE);
}
return bFirstResult && bSecondResult;
}
```
## 4. NtSetIn informationThread()
* Hàm `ntdll!NtSetInformationThread()` có thể được dùng để ẩn một luồng (`thread`) khỏi debugger bằng cách sử dụng `THREAD_INFORMATION_CLASS::ThreadHideFromDebugger (0x11)`.
* Thông thường, hàm này được dùng bởi một tiến trình ngoài để thao tác với các luồng, nhưng bất kỳ luồng nào cũng có thể gọi hàm này để ẩn chính nó khỏi debugger.
* Khi một luồng bị ẩn, debugger sẽ không nhận được các event liên quan đến luồng đó nữa, nhưng luồng vẫn tiếp tục chạy bình thường.
* Luồng ẩn này có thể thực hiện các kiểm tra chống debug như code checksum, kiểm tra cờ debug, v.v.
* Tuy nhiên, nếu ta đặt breakpoint trong luồng bị ẩn hoặc ẩn luồng chính của tiến trình, chương trình có thể bị treo hoặc crash khi debugger cố gắng tương tác với luồng đó.
```py
#define NtCurrentThread ((HANDLE)-2)
bool AntiDebug()
{
NTSTATUS status = ntdll::NtSetInformationThread(
NtCurrentThread,
ntdll::THREAD_INFORMATION_CLASS::ThreadHideFromDebugger,
NULL,
0);
return status >= 0;
}
```
## 5. EnumWindows() và SuspendThread()
* Dùng để phát hiện và tạm dừng một tiến trình mẹ nếu nó đang chạy một trình gỡ lỗi.
* Xác định tiến trình cha
* (parent process) của tiến trình hiện tại.
* Sử dụng `user32!EnumWindows()` hoặc `user32!EnumThreadWindows()` tìm kiếm `top-level windows`, rồi dùng `user32!GetWindowTextW()` xem `title` có chứa từ khóa liên quan đến debugger ``("dbg", "debugger")`` không.
* Nếu đúng ----> tạm dừng bằng `kernel32!SuspendThread()` hoặc `ntdll!NtSuspendThread()`.
```py
DWORD g_dwDebuggerProcessId = -1;
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
DWORD dwProcessId = *(PDWORD)lParam;
DWORD dwWindowProcessId;
GetWindowThreadProcessId(hwnd, &dwWindowProcessId);
if (dwProcessId == dwWindowProcessId)
{
std::wstring wsWindowTitle{ string_heper::ToLower(std::wstring(GetWindowTextLengthW(hwnd) + 1, L'\0')) };
GetWindowTextW(hwnd, &wsWindowTitle[0], wsWindowTitle.size());
if (string_heper::FindSubstringW(wsWindowTitle, L"dbg") ||
string_heper::FindSubstringW(wsWindowTitle, L"debugger"))
{
g_dwDebuggerProcessId = dwProcessId;
return FALSE;
}
return FALSE;
}
return TRUE;
}
bool IsDebuggerProcess(DWORD dwProcessId) const
{
EnumWindows(EnumWindowsProc, reinterpret_cast<LPARAM>(&dwProcessId));
return g_dwDebuggerProcessId == dwProcessId;
}
bool SuspendDebuggerThread()
{
THREADENTRY32 ThreadEntry = { 0 };
ThreadEntry.dwSize = sizeof(THREADENTRY32);
DWORD dwParentProcessId = process_helper::GetParentProcessId(GetCurrentProcessId());
if (-1 == dwParentProcessId)
return false;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwParentProcessId);
if(Thread32First(hSnapshot, &ThreadEntry))
{
do
{
if ((ThreadEntry.th32OwnerProcessID == dwParentProcessId) && IsDebuggerProcess(dwParentProcessId))
{
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, ThreadEntry.th32ThreadID);
if (hThread)
SuspendThread(hThread);
break;
}
} while(Thread32Next(hSnapshot, &ThreadEntry));
}
if (hSnapshot)
CloseHandle(hSnapshot);
return false;
}
```
## 6. SwitchDesktop()
* Ngắt kết nối giữa debugger và process bị debug bằng cách chuyển tiến trình sang một desktop khác → khiến debugger không thể tương tác, quan sát, hoặc điều khiển nữa.
* Windows cho phép nhiều desktop trong cùng một `user session`
* Khi một tiến trình tạo một desktop mới bằng `CreateDesktopA()` rồi gọi `SwitchDesktop()` để chuyển sang desktop đó:
* Tất cả các cửa sổ hiện tại ẩn đi
* Bàn phím và chuột sẽ không gửi event đến các cửa sổ của desktop trước đó.
* Debugger đang chạy trên desktop gốc sẽ không thể truy cập hay điều khiển tiến trình đã switch desktop → vô hiệu hóa debugger.
```py
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 không còn được dùng nữa vì chỉ hoạt động cho các phiên bản Windows Vista trở xuống. Nhưng nó vẫn rất nổi tiếng.
* Nếu chương trình không bị debug, khi gọi tới `kernel32!OutputDebugString` thì lỗi sẽ xảy ra. Vậy không có lỗi đồng nghĩa với việc có debugger.
```py
bool IsDebugged()
{
if (IsWindowsVistaOrGreater())
return false;
DWORD dwLastError = GetLastError();
OutputDebugString(L"AntiDebug_OutputDebugString_v1");
return GetLastError() != dwLastError;
}
```
## Bypass
* NOPs, ...
# Misc
## 1. FindWindow()
* Nhiều trình debug **GUI** sử dụng một class name đặc trưng cho cửa sổ chính (`main window`).
* Dùng các hàm như:
* `FindWindowA(), FindWindowW()` – tìm cửa sổ theo tên class hoặc tiêu đề.
* `FindWindowExA(), FindWindowExW()` – tương tự nhưng hỗ trợ tìm cửa sổ con.
* Nếu `FindWindow*` trả về `handle` khác NULL, nghĩa là một cửa sổ với class name đó đang tồn tại → nhiều khả năng có debugger.
```py
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
* Một tiến trình người dùng thông thường (bấm đúp để chạy) sẽ có tiến trình cha là `explorer.exe`.
* Nếu tiến trình bị debug hoặc được launch từ `IDE/debugger` như `x64dbg, WinDbg...` thì tiến trình cha có thể là:
* `x64dbg.exe, windbg.exe, cmd.exe`, hoặc thậm chí là chính debugger.
* Cách kiểm tra:
* Lấy `PID` của tiến trình hiện tại. Truy ngược Parent PID.
* Dùng `OpenProcess()` và `GetModuleBaseName()` để lấy parent process.
* Nếu `parent process != explorer.exe` → nghi ngờ có debugger hoặc tiến trình bị launch không bình thường.
### 2.1. NtQueryInformationProcess()
* `NtQueryInformationProcess` có thể được dùng để lấy `PROCESS_BASIC_INFORMATION`, trong đó có trường `InheritedFromUniqueProcessId` → chính là `PID` cha của tiến trình hiện tại.
* Dùng `GetWindowThreadProcessId()` để lấy `PID` của `explorer.exe`, từ đó so sánh: nếu `PID cha ≠ PID` của `explorer.exe` → tiến trình không được launch theo cách “thông thường”.
```py
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()
* Cũng như `2.1 NtQueryInformationProcess()`, khác là `ID` và tên của tiến trình cha có thể được lấy bằng cách gọi hàm `kernel32!CreateToolhelp32Snapshot()` và `kernel32!Process32Next()`.
```py
DWORD GetParentProcessId(DWORD dwCurrentProcessId)
{
DWORD dwParentProcessId = -1;
PROCESSENTRY32W ProcessEntry = { 0 };
ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(Process32FirstW(hSnapshot, &ProcessEntry))
{
do
{
if (ProcessEntry.th32ProcessID == dwCurrentProcessId)
{
dwParentProcessId = ProcessEntry.th32ParentProcessID;
break;
}
} while(Process32NextW(hSnapshot, &ProcessEntry));
}
CloseHandle(hSnapshot);
return dwParentProcessId;
}
bool IsDebugged()
{
bool bDebugged = false;
DWORD dwParentProcessId = GetParentProcessId(GetCurrentProcessId());
PROCESSENTRY32 ProcessEntry = { 0 };
ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(Process32First(hSnapshot, &ProcessEntry))
{
do
{
if ((ProcessEntry.th32ProcessID == dwParentProcessId) &&
(strcmp(ProcessEntry.szExeFile, "explorer.exe")))
{
bDebugged = true;
break;
}
} while(Process32Next(hSnapshot, &ProcessEntry));
}
CloseHandle(hSnapshot);
return bDebugged;
}
```
## 4. DbgPrint()
* `ntdll!DbgPrint()` và `kernel32!OutputDebugStringW()` sẽ tạo ra một exception `DBG_PRINTEXCEPTION_C (0x40010006)`.
* Còn phần xử lý sau thì khá giống `INT3/DebugBreak/...`. Tức là detect debugger dựa vào `exception handler` hay debugger xử lí exception này.
```py
bool IsDebugged()
{
__try
{
RaiseException(DBG_PRINTEXCEPTION_C, 0, 0, 0);
}
__except(GetExceptionCode() == DBG_PRINTEXCEPTION_C)
{
return false;
}
return true;
}
```
## 5. DbgSetDebugFilterState()
* Hàm `ntdll!DbgSetDebugFilterState()` và `ntdll!NtSetDebugFilterState()` được dùng để thiết lập một cờ trạng thái debug mà `kernel-mode` debugger có thể kiểm tra.
* Nếu một `kernel debugger` đang gắn vào hệ thống, các hàm này sẽ trả về `success`.
```py
bool IsDebugged()
{
return NT_SUCCESS(ntdll::NtSetDebugFilterState(0, 0, TRUE));
}
```
* Hàm này yêu cầu quyền `Administrator`
## 6. NtYieldExecution() / SwitchToThread()
* Kỹ thuật này không hoàn toàn đáng tin cậy nhưng vẫn có thể dùng làm anti-tracing.
* Khi chạy dưới debugger và thực hiện single-step, việc chuyển đổi ngữ cảnh sang luồng khác sẽ bị hạn chế.
* Vì vậy, hàm `ntdll!NtYieldExecution()` sẽ trả về `STATUS_NO_YIELD_PERFORMED (0x40000024)`, và `kernel32!SwitchToThread()` sẽ trả về $0$.
* Ý tưởng là có một vòng lặp, trong đó một biến đếm sẽ được dịch bit sang trái nếu `SwitchToThread()` trả về $0$ (hoặc `NtYieldExecution()` trả về `STATUS_NO_YIELD_PERFORMED`).
* Nếu sau vòng lặp, biến đếm bằng $0$ (tức đã dịch bit đủ $8$ lần) ----> debbuger detect.
```py
bool IsDebugged()
{
BYTE ucCounter = 1;
for (int i = 0; i < 8; i++)
{
Sleep(0x0F);
ucCounter <<= (1 - SwitchToThread());
}
return ucCounter == 0;
}
```
## Bypass
* NOPs, ...
# Xây dựng thử 1 chương trình antidebug
:::spoiler Code
```py
#include <windows.h>
#include <winternl.h>
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")
#include "for_winner.h"
void SaveAndPlayVideo() {
HANDLE hFile = CreateFile("for_winner.mp4", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
MessageBoxA(NULL, "Cannot create temp video file", "Error", MB_OK);
return;
}
DWORD written = 0;
WriteFile(hFile, for_winner_mp4, for_winner_mp4_len, &written, NULL);
CloseHandle(hFile);
ShellExecuteA(NULL, "open", "for_winner.mp4", NULL, NULL, SW_SHOWNORMAL);
}
#define IDR_WAVFILE 101
void PlayEmbeddedWAV() {
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(IDR_WAVFILE), RT_RCDATA);
if (!hRes) {
MessageBoxA(NULL, "Cannot find WAV resource!", "Error", MB_OK);
return;
}
HGLOBAL hData = LoadResource(NULL, hRes);
if (!hData) {
MessageBoxA(NULL, "Cannot load WAV resource!", "Error", MB_OK);
return;
}
DWORD size = SizeofResource(NULL, hRes);
void* pData = LockResource(hData);
if (!pData) {
MessageBoxA(NULL, "Cannot lock WAV resource!", "Error", MB_OK);
return;
}
PlaySound((LPCSTR)pData, NULL, SND_MEMORY | SND_SYNC);
}
// ######################## Phase 1 ###########################
// ##################### IsDebuggerPresent ####################
// ############################################################
int _IsDebuggerPresent()
{
if (IsDebuggerPresent())
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "IsDebuggerPresent", MB_OK);
ExitProcess(1);
}
else
{
MessageBoxA(NULL, "Bypass!", "IsDebuggerPresent", MB_OK);
}
return 0;
}
// ######################## Phase 2 ###########################
// ############### CheckRemoteDebuggerPresent #################
// ############################################################
int _CheckRemoteDebuggerPresent()
{
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());
BOOL IsDebuggerPresent = FALSE;
if (CheckRemoteDebuggerPresent(hProcess, &IsDebuggerPresent)) {
if (IsDebuggerPresent)
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "CheckRemoteDebuggerPresent", MB_OK);
ExitProcess(1);
}
else
{
MessageBoxA(NULL, "Bypass!", "CheckRemoteDebuggerPresent", MB_OK);
}
CloseHandle(hProcess);
return 0;
}
}
// ######################## Phase 3 ###########################
// ################ NtQueryInformationProcess #################
// ############################################################
int _NtQueryInformationProcess()
{
typedef NTSTATUS(NTAPI* TNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
if (hNtdll)
{
TNtQueryInformationProcess pfnNtQueryInformationProcess =
(TNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");
if (pfnNtQueryInformationProcess)
{
ULONG_PTR ProcessDebugPort = 0;
ULONG dwReturned1;
NTSTATUS status1 = pfnNtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)0x7,
&ProcessDebugPort,
sizeof(ProcessDebugPort),
&dwReturned1);
if (NT_SUCCESS(status1) && (-1 == ProcessDebugPort))
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "NtQueryInformationProcess", MB_OK);
ExitProcess(1);
}
else
{
MessageBoxA(NULL, "Bypass ProcessDebugPort!", "NtQueryInformationProcess", MB_OK);
}
DWORD dwProcessDebugFlags;
ULONG dwReturned2;
NTSTATUS status2 = pfnNtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)0x1f,
&dwProcessDebugFlags,
sizeof(DWORD),
&dwReturned2);
if (NT_SUCCESS(status2) && (dwProcessDebugFlags == 0))
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "NtQueryInformationProcess", MB_OK);
ExitProcess(1);
}
else
{
MessageBoxA(NULL, "Bypass ProcessDebugFlags!", "NtQueryInformationProcess", MB_OK);
}
HANDLE hProcessDebugObject = 0;
ULONG dwReturned3;
NTSTATUS status3 = pfnNtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)0x1e,
&hProcessDebugObject,
sizeof(HANDLE),
&dwReturned3);
if (NT_SUCCESS(status3) && (hProcessDebugObject != 0))
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "NtQueryInformationProcess", MB_OK);
ExitProcess(1);
}
else
{
MessageBoxA(NULL, "Bypass ProcessDebugObjectHandle!", "NtQueryInformationProcess", MB_OK);
}
}
FreeLibrary(hNtdll);
}
return 0;
}
// ######################## Phase 4 ###########################
// ################### BeingDebuggedFlag ######################
// ############################################################
PPEB GetPEB()
{
#ifdef _WIN64
return (PPEB)__readgsqword(0x60);
#else
return (PPEB)__readfsdword(0x30);
#endif
}
void _BeingDebuggedFlag()
{
PPEB pPeb = GetPEB();
if (pPeb->BeingDebugged)
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "BeingDebuggedFlag", MB_OK);
ExitProcess(1);
}
else
{
MessageBoxA(NULL, "Bypass!", "BeingDebuggedFlag", MB_OK);
}
}
// ######################## Phase 5 ###########################
// ###################### NtGlobalFlag ########################
// ############################################################
#define FLG_HEAP_ENABLE_TAIL_CHECK 0x10
#define FLG_HEAP_ENABLE_FREE_CHECK 0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
#define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS)
DWORD Get_dwNtGlobalFlag()
{
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#endif // _WIN64
return dwNtGlobalFlag;
}
void _NtGlobalFlag()
{
DWORD dwNtGlobalFlag = Get_dwNtGlobalFlag();
if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "NtGlobalFlag", MB_OK);
ExitProcess(1);
}
else
{
MessageBoxA(NULL, "Bypass!", "NtGlobalFlag", MB_OK);
}
}
// ######################## Phase 6 ###########################
// ###################### CloseHandle #########################
// ############################################################
void _CloseHandle()
{
__try
{
CloseHandle((HANDLE)0xDEADBEEF);
MessageBoxA(NULL, "Bypass!", "CloseHandle", MB_OK);
}
__except (EXCEPTION_INVALID_HANDLE == GetExceptionCode()
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH)
{
PlayEmbeddedWAV();
MessageBoxA(NULL, "Debugger detected! Exiting...", "CloseHandle", MB_OK);
ExitProcess(1);
}
}
int main()
{
_IsDebuggerPresent();
_CheckRemoteDebuggerPresent();
_NtQueryInformationProcess();
_BeingDebuggedFlag();
_NtGlobalFlag();
_CloseHandle();
SaveAndPlayVideo();
}
```
:::
* **Compile**:
* Vào `start` mở `open x86 Native Tools Command Prompt for VS 2022`
* Chuyển đến thư mục hiện tại.
* Chạy lệnh `cl /W4 /Zi /Feantidebug.exe antidebug.c resource.res user32.lib winmm.lib Shell32.lib
`
* À thực ra mình thích ~~màu mè~~ nên code nó hơi đụt như vậy. Mình nhúng thêm 1 file âm thanh khi nó `ExitProcess()` và 1 video khi bypass được hết. Chứ nếu chỉ viết 1 chương trình antidebug đơn giản thì compile bình thường cũng được. 😁
* Ở đây mình dùng 6 loại antidebug mình ~~hay gặp~~.

* Phân tích hay giải thích thì có lẽ không cần thiết. Mình sẽ đưa ra cách bypass từng loại 1 luôn nha.
## Bypass _IsDebuggerPresent()
* Khi debug ta thấy nó sẽ nhảy vào hàm có `Debugger detected!`.

* Bypass: đơn giản là chỉ cần patch `jz` thành `jmp` thôi.

* Kết quả: 
## Bypass _CheckRemoteDebuggerPresent()
* Khi debug ta thấy nó sẽ nhảy vào hàm có `Debugger detected!`.

* Bypass: patch tiếp `jz` thành `jmp`.

* Kết quả: 
## Bypass _NtQueryInformationProcess()
* Cũng sửa hết `jl` thành `jmp` nha.
## Bypass _BeingDebuggedFlag()
* Patch `jz` thành `jmp`.
## Bypass _NtGlobalFlag()
* Cũng thế nha :))
## Bypass _CloseHandle()
* Ban đầu: 
* Đoạn này thì có rất nhiều cách bypass, nhưng đơn giản thì mình chỉ thay `push 0xDEADBEEF` thành `push 0`.
* Tổng kết lại thì có nhiều cách để bypass, tùy vào từng trường hợp, mình chỉ trình bày cách mà mình ~~nghĩ~~ nhanh và đơn giản nhất.
# Tổng kết
* Vậy ở wu này mình đã trình bày các loại antidebug (~~thực ra cũng chỉ là đi dịch lại~~). Sau đó code thử 1 chương trình để tự bypass. Trong lúc tổng hợp wu có thể có những sai sót, các bạn đọc tham khảo có thể góp ý để wu mình hoàn thiện hơn. Tới đây là kết thúc wu rồi, chúc các bạn 1 ngày tốt lành và mong wu này giúp ích cho các bạn!!!
# Reference:
https://anti-debug.checkpoint.com/
https://hackmd.io/@v13td0x/Anti-debug_DebugFlags_p1#14-ntdllRtlQueryProcessHeapInformation
https://hackmd.io/@TmX4NyoaRF2bCJC8kl2Chw/r11jf8jAo