# Source:
https://anti-debug.checkpoint.com/
> Me nói cho ME hỉu.
# Các kiểu dữ liệu mới trong Lib <window.h>
Trong code C khi cài đặt thư viện `window`, ta có thể sử dụng các kiểu dữ liệu đặc biệt sau đây (các kiểu dữ liệu này được quy định chỉ được phép định nghĩa và sử dụng để chạy trong môi trường Window mà thôi):
- `Handle`(Trong C là `*void`): 1 con trỏ không định kiểu, được sử dụng để tham chiếu đến 1 đối tượng trong kernel (file, thread, ...).
- `HWND`: `Handle to Handle` Một Handle đặc biệt được sử dụng để tham chiếu đến màn hình.
- `DWORD` = `unsigned long`.
- `ULONG` = giống hệt DWORD.
- `LONG` = `long`.
- `WORD` = `unsigned short` - số nguyên ko dấu 16 bit.
- `BYTE` = `unsigned char` - số nguyên không dấu 8 bit.
- `BOOL` = `int` - True or False.
- `PVOID, LPVOID`: Đây là con trỏ không chỉ định kiểu, có thể dùng để trỏ đến bất kì vùng dữ liệu nào (Thường dùng cho bộ nhớ đệm). Đặc biệt, con trỏ `void*` không thể deference trực tiệp mà muốn deference thì phải ép kiểu `*(int*)p nếu như void* p`.
- vv...
# ?
`Anti-Debug` là 1 kĩ thuật mà người lập trình cố tình để gây khó khăn, ngăn chạn hay phát hiện việc chương trình của họ có bị phân tích bằng 1 trình debug hay không. Mục đích là trách chương trình bị lỗ mã nguồn, bẻ khóa hay cracking, phân tích mã độc.
# Debug-flags
Các `flag` đặc biệt trong hệ thống vốn được "nhúng" trong bộ nhớ tiến trình và được hệ điều hành thiết lập, có thể được dùng để chỉ ra tiến trình đó đang được debugging. Trạng thái của các `flag` có thể được xác định bằng các hàm API hoặc tra trực tiếp trên bảng hệ thống trong bộ nhớ.
Các mã độc thường sử dụng phương pháp này.
Đây có thể được coi là 1 trong các cách chống debug dễ + hiệu quả nhất. Khi 1 chương trình được khởi chạy, hệ điều hành sẽ cung cấp cho nó 1 vùng nhớ riêng và vùng nhớ này sẽ duy trì bảng hệ thống để quản lý tiến trình.
Khi dùng 1 trình debug để attach vào chương trình, hệ điều hành biết điều này và sẽ ping cờ.
Khi phát hiện debugger, chương trình hay malware sẽ ngay lập tức thoát hoặc thực thi các hành động
## Win32 API:
### IsDebuggerPresent()
Hàm này đọc cờ BeingDebug() trong PEB và thoát nếu cờ được đặt giá trị. Được sử dụng để check tiến trình hiện tại có đang bị debug bởi 1 trình debug user-mode như OllyDbg hoặc x64dbg hay không?
```asm!
call IsDebuggerPresent
test al, al
jne being_debugged
...
being_debugged:
push 1
call ExitProcess
```
### NtQueryInformationProcess()
Hàm được dùng để truy vấn thông tin trong 1 tín trình. Sử dụng đối số `ProcessInformationClass` để tiến hành truy vấn loại dữ liệu ta cần và ép kiểu đầu ra bằng đối số `ProcessInformation`.
#### ProcessDebugPort
Thay vì tìm số cờ trong PEB (user mode) thì PDD gọi thẳng lên kernal để tìm số port của Debuger.
`Cách hoạt động`:
- Khi tiến hành đính 1 debugger vào chương trình để tiến hành debug, Kernel sẽ tạo 1 class là `Debugger port` hay cổng gỡ lỗi như 1 kênh giao tiếp giữa debugger với chương trình.
- Nếu khồn có debugger thì cái class này sẽ ko được tạo.
- Hàm`NtQueryInformationProcess()` là 1 hàm cấp thấp (native API) nằm trong `ntdll.dll` như đã đề cập có thể hỏi kernel về nhiều thông tin bao gồm chính nó và các tiến trình khác. Từ đó dùng `ProcessDebugPort` để gọi kernel trả về port nếu có thì là -1 hoặc ko thì là 0 (Tùy trường hợp).
`Pseudo code C`:
```cpp!
typedef NTSTATUS (NTAPI *TNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength
);
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
if (hNtdll)
{
auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress(
hNtdll, "NtQueryInformationProcess");
if (pfnNtQueryInformationProcess)
{
DWORD dwProcessDebugPort, dwReturned;
NTSTATUS status = pfnNtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugPort,
&dwProcessDebugPort,
sizeof(DWORD),
&dwReturned);
if (NT_SUCCESS(status) && (-1 == dwProcessDebugPort))
ExitProcess(-1);
}
}
```
- Đầu tiên, tự định nghĩa 1 con trỏ hàm NTSTATUS, tuân theo quy ước NTAPI
- Tiếp theo, tiến hành nạp thư viện: "ntdll.dll" vào bộ nhớ. Rồi sau đó dùng GetProcAddress để tải địa chỉ `NtQueryInformationProcess` trong thư viện vừa tải vào `pfnNtQueryInformationProcess`. Giờ biến này sẽ mang địa chỉ của hàm và lúc này chỉ cần truyền tham số vào nữa thôi là hàm sẽ chạy.
- Cuối cùng, tạo 2 biến có kích thước DWORD để chứa kết quả hàm, sau đó sử dụng NT_SUCCESS để check tiến trình và so sánh kết quả port được lưu xem có phải -1 ko (Nếu là -1 tức là chương trình đã bị debug và sẽ thoát ngay lập tức).
`pseudo-code x84`:
```asm!
lea eax, [dwReturned]
push eax ; ReturnLength
push 4 ; ProcessInformationLength
lea ecx, [dwProcessDebugPort]
push ecx ; ProcessInformation
push 7 ; ProcessInformationClass
push -1 ; ProcessHandle
call NtQueryInformationProcess
inc dword ptr [dwProcessDebugPort]
jz being_debugged
...
being_debugged:
push -1
call ExitProcess
```
`pseudo-code x64`:
```asm!
lea rcx, [dwReturned]
push rcx ; ReturnLength
mov r9d, 4 ; ProcessInformationLength
lea r8, [dwProcessDebugPort]
; ProcessInformation
mov edx, 7 ; ProcessInformationClass
mov rcx, -1 ; ProcessHandle
call NtQueryInformationProcess
cmp dword ptr [dwProcessDebugPort], -1
jz being_debugged
...
being_debugged:
mov ecx, -1
call ExitProcess
```
#### ProcessDebugFlags()
`EPROCESS` là 1 cấu trúc dữ liệu của kernel, là hiện thân của 1 đối tượng tiến trình và chứa 1 trường là `NoDebugInherit`. Ta có thể truy cập vào giá trị nghịch đảo của trường này bằng Class `ProcessDebugFlags (0x1f)` và nó ko được công bố chính thức nên cần phải được khai báo. Nếu trả về 0 thì nghĩa là hiện tại đang có 1 trình debug.
`pseudo-code C`:
```cpp!
typedef NTSTATUS(NTAPI *TNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN DWORD ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength
);
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
if (hNtdll)
{
auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress(
hNtdll, "NtQueryInformationProcess");
if (pfnNtQueryInformationProcess)
{
DWORD dwProcessDebugFlags, dwReturned;
const DWORD ProcessDebugFlags = 0x1f;
NTSTATUS status = pfnNtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugFlags,
&dwProcessDebugFlags,
sizeof(DWORD),
&dwReturned);
if (NT_SUCCESS(status) && (0 == dwProcessDebugFlags))
ExitProcess(-1);
}
}
```
`pseudo-code x86`:
```asm!
lea eax, [dwReturned]
push eax ; ReturnLength
push 4 ; ProcessInformationLength
lea ecx, [dwProcessDebugFlags]
push ecx ; ProcessInformation
push 1Fh ; ProcessInformationClass
push -1 ; ProcessHandle
call NtQueryInformationProcess
cmp dword ptr [dwProcessDebugFlags], 0
jz being_debugged
...
being_debugged:
push -1
call ExitProcess
```
`pseudo-code x64`:
```asm!
lea rcx, [dwReturned]
push rcx ; ReturnLength
mov r9d, 4 ; ProcessInformationLength
lea r8, [dwProcessDebugFlags]
; ProcessInformation
mov edx, 1Fh ; ProcessInformationClass
mov rcx, -1 ; ProcessHandle
call NtQueryInformationProcess
cmp dword ptr [dwProcessDebugFlags], 0
jz being_debugged
...
being_debugged:
mov ecx, -1
call ExitProcess
```
#### ProcessDebugObjectHandle()
Khi việc gỡ lỗi bắt đầu, kernel sẽ tạo 1 đối tượng có tên `debug object` và ta có thể truy vấn giá trị của handle này bằng cách sử dụng 1 lớp không được công bố chính thức có tên là `ProcessDebugObjectHandle(0x1e)`. Nếu như chương trình không bị debug thì giá trị trả về sẽ là 0 hoặc null.
`Pseudo-code C`:
```asm!
typedef NTSTATUS(NTAPI * TNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN DWORD ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength
);
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
if (hNtdll)
{
auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress(
hNtdll, "NtQueryInformationProcess");
if (pfnNtQueryInformationProcess)
{
DWORD dwReturned;
HANDLE hProcessDebugObject = 0;
const DWORD ProcessDebugObjectHandle = 0x1e;
NTSTATUS status = pfnNtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugObjectHandle,
&hProcessDebugObject,
sizeof(HANDLE),
&dwReturned);
if (NT_SUCCESS(status) && (0 != hProcessDebugObject))
ExitProcess(-1);
}
}
```
`Pseudo-code x86`:
```asm!
lea eax, [dwReturned]
push eax ; ReturnLength
push 4 ; ProcessInformationLength
lea ecx, [hProcessDebugObject]
push ecx ; ProcessInformation
push 1Eh ; ProcessInformationClass
push -1 ; ProcessHandle
call NtQueryInformationProcess
cmp dword ptr [hProcessDebugObject], 0
jnz being_debugged
...
being_debugged:
push -1
call ExitProcess
```
`Pseudo-code x64`:
```asm!
lea rcx, [dwReturned]
push rcx ; ReturnLength
mov r9d, 4 ; ProcessInformationLength
lea r8, [hProcessDebugObject]
; ProcessInformation
mov edx, 1Eh ; ProcessInformationClass
mov rcx, -1 ; ProcessHandle
call NtQueryInformationProcess
cmp dword ptr [hProcessDebugObject], 0
jnz being_debugged
...
being_debugged:
mov ecx, -1
call ExitProcess
```
## Manual checks
Không dùng API mà tự kiểm tra bộ nhớ tiến trình để nhận biết liệu chương trình có đang bị debug hay ko và cũng có thể tìm được những thông tin mà API thông thường không cung cấp được.
### PEB!BeingDebugged Flag
Đây là cách check xem Flag `BeingDebugged` có ping không 1 cách thủ công:
`pseudo-code C`:
```cpp!
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif // _WIN64
if (pPeb->BeingDebugged)
goto being_debugged;
```
`32bit process`:
```asm!
mov eax, fs:[30h]
cmp byte ptr [eax+2], 0
jne being_debugged
```
`64bit process`:
```asm!
mov rax, gs:[60h]
cmp byte ptr [rax+2], 0
jne being_debugged
```
- Trong window x64, thanh ghi đoạn `gs` trỏ đến vùng nhớ quan trọng là TEB (Thread Environment Block). `60h - 0x60` đây là vị trí trong TEB mà chứa 1 con trỏ trỏ đến vùng nhớ PEB.
- Sau khi nạp địa chỉ vào rax, thì lúc này `[rax + 2]` lại chính là vùng nhớ chứa flag BeingDebugged trong PEB, lúc này so sánh nó với với 0 mà ảnh báo bị debugged nếu khác 0.
### NtGlobalFlag
Trường `NtGlobalFlag (0x68 offset on 32-Bit and 0xBC on 64-bit Windows)` trong PEB có giá trị mặc định là 0. Việc gán 1 debugger vào tiến trình đang chạy sẽ không làm trường này thay đổi giá trị. Tuy nhiên, nếu tiền trình được tạo bởi 1 debugger thì các Flags sau sẽ được bật:
- FLG_HEAP_ENABLE_TAIL_CHECK (0x10) `Check tràn bộ nhớ`
- FLG_HEAP_ENABLE_FREE_CHECK (0x20) `Check lỗi giải phóng vùng nhớ`
- FLG_HEAP_VALIDATE_PARAMETERS (0x40) `Check tham số truyền vào hàm quản lý heap có hợp lệ không?`
=> Sự hiện diện của các cờ sẽ thông báo sự hiện diện của debugger.
`pseudo-code C`:
```cpp!
#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;
```
`32bit`:
```asm!
mov eax, fs:[30h]
mov al, [eax+68h]
and al, 70h
cmp al, 70h
jz being_debugged
```
`64bit`:
```asm!
mov rax, gs:[60h]
mov al, [rax+BCh]
and al, 70h
cmp al, 70h
jz being_debugged
```
- Tản mạn về code asm, ta sẽ vẫn load PEB vào rax rồi truy cập vào trường NtGlobalFlag vào al.
- Sau đó tiến hành lọc 3 giá trị của 3 cờ đã nêu trên trong trường NtGlobalFlag đang giữ bởi al bằng toán tử `and`. Sau đó so sánh kết quả với `0x70 vốn là tổng giá trị 3 cờ` và thông báo bị debug nếu chúng bằng nhau.
### Heap Flags
Bộ nhớ Heap chứa trường có thể bị ảnh hưởng bởi sự hiện diện của Debugger và việc chúng bị ảnh hưởng như thế nào còn tùy thuộc vào phiên bản Window. Hai trường này là : `Flags and ForceFlags`.
Khi 1 debugger xuất hiện, trường `Flags` sẽ được set thành tổ hợp của các Flag sau trên Windows NT, Windows 2000 và 32-bit Win XP:
- EAP_GROWABLE (2)
- HEAP_TAIL_CHECKING_ENABLED (0x20)
- HEAP_FREE_CHECKING_ENABLED (0x40)
- HEAP_SKIP_VALIDATION_CHECKS (0x10000000)
- HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000).
Còn đối với Windows 64-bit Window XP hay Windows Vista hoặc đời cao hơn nữa thì `Flags` sẽ được set thành tổ hợp của các Flags sau:
- HEAP_GROWABLE (2)
- HEAP_TAIL_CHECKING_ENABLED (0x20)
- HEAP_FREE_CHECKING_ENABLED (0x40)
- HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000).
Với trường `ForceFlags`:
- HEAP_TAIL_CHECKING_ENABLED (0x20)
- HEAP_FREE_CHECKING_ENABLED (0x40)
- HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)
`Pseudo-code C`:
```cpp!
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);
}
```
- Đầu tiên, Chương trình sẽ tìm địa chỉ của block PEB dựa trên bản Windows 32 hay 64-bit.
- Đối với mỗi bản Windows, tạo con trỏ trỏ tới vùng nhớ Heap (cái này cũng tiếp tục phải dựa vào phiên bản Windows), giả sử nếu hệ điều hành là Window 32-bit:
```cpp!
PVOID pHeapBase = !m_bIsWow64
? (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18))
: (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x1030));
```
- Đoạn code này hoạt động như sau: Đầu tiên nó sẽ check xem môi trường hiện tại có phải WOW64 hay không (chương trình con 32-bit chạy trên tiến trình của 64-bit Windows). Nếu đúng thì chạy vào `pHeapBases = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18))` còn nếu không thì nhảy đến thằng sau `:`.
- Tiếp theo, lấy thông tin của trường `Flags` và `ForceFlags` dựa trên phiên bản Window hiện tại.
- Cuối cùng, test xem Thứ nhất nếu `Flags` ngoài cờ Heap_Growable ra thì trường này còn chứa gì nữa không, nếu không trả về False. Còn đối với thằng `ForceFlags` giá trị mặc định của nó là 0 và nếu trường này có gì bất thường xảy ra với nó thì sẽ trả về False.
=> Nói chung, hàm `check()` sẽ trả về False nếu không có debugger.
## Mitigation for Win API
### IsDebuggerPresent():
Đặt cờ `BeingDebugged` của PEB về = 0.
### CheckRemoteDebuggerPresent() and NtQueryInformationProcess():
Vì CheckRemoteDebuggerPresent() gọi đến NtQueryInformationProcess(), cách duy nhất là "hook" (can thiệp) vào `NtQueryInformationProcess()` và đặt các giá trị sau vào bộ đệm trả về:
- Nếu truy vấn `ProcessDebugPort`, cài đặt giá trị trả về là cái gì đo skhacs `-1`.
- Nếu truy vấn `ProcessDebugFlag` thì sẽ cài đặt giá trị trả về là gì đó khác `0`.
- Trả về `0` nếu truy `ProcessDebugObjectHandle`.
Phần bộ nhớ đệm trả về hay tham số đầu ra:
```cpp!
DWORD dwProcessDebugPort, dwReturned; // <-- 1. Chương trình tạo sẵn "hộp chứa"
NTSTATUS status = pfnNtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugPort,
&dwProcessDebugPort, // <-- 2. Chương trình đưa "địa chỉ" của hộp chứa
sizeof(DWORD),
&dwReturned);
```
## Mitigation for Manual Checks
### PEB!BeingDebugged Flag:
Ta sẽ set giá trị `BeingDebugged` về 0 và thủ thuật này có thể hoàn toàn bằng `DLL injection`. Nếu ta dùng trình debugeer mạnh như `OllyDbg or x32/64dbg`, ta có thể cài đặt sẵn plugin `anti-anti debug` như ScyllaHide.
```cpp!
//DLL Injection
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif // _WIN64
pPeb->BeingDebugged = 0;
```
### NtGlobalFlag:
Cũng tương tự như `BeingDebugged FLag`, ta sẽ set `NtGlobalFlag` = 0 thông qua `DDL enjection`. Điều này cũng có thể được thực hiện tự đồng nếu dùng pluggin sẵn như trên.
```cpp!
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
*(PDWORD)((PBYTE)pPeb + 0x68) = 0;
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
*(PDWORD)((PBYTE)pPeb + 0xBC); = 0;
#endif // _WIN64
```
### Heaps Flag:
Ta có thể set `Flags -> HEAP_GROWABLE` và `ForceFlags` thành 0. Một lần nữa những cái này có thể bypass tự động thông qua plugin giống hệt phần trên.
```cpp!
#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)((PBYTE)pHeapBase + dwHeapFlagsOffset) = HEAP_GROWABLE;
*(PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset) = 0;
```
# Object Handles
## Handles:
`Handle` là 1 số tham chiếu mà Windows dùng để quản lý các tài nguyên. Ta có thể tưởng tượng nó như 1 cái chìa khóa phòng, chứ không phải 1 căn phòng. Nhưng, nó là thứ đại diện và giúp ta truy cập 1 cách an toàn.
### Định nghĩa:
Khi chương trình muốn sử dụng tài nguyên hệ thống (Như file, tiến trình, font chữ) thì nó sẽ không thể truy cập trực tiếp được mà thay vào đó:
- `Ta yêu cầu`: Chương trình gọi 1 API để yêu cầu truy cập.
- `Kernel cấp phát`: Kernel (OS) kiểm tra yêu cầu. Nếu hợp lệ, nó sẽ tạo mở tài nguyên mà đó trong không gian bộ nhớ được bảo vệ của nó.
- `Kernel trao Handle`: Kernel trả về cho chương trình 1 con số duy nhất, đó là `Handle`.
- `Sử dụng Handle`: Khi muốn thực hiện các thao tác (đọc, ghi, đóng) thì chương trình ta sẽ tiến hành đưa `Handle` này cho các API khác. Kernel sẽ nhận Handle, xem xét nó tương ứng với tài nguyên nào rồi tiến hành thực hiện thao tác giúp ta.
Điều này giúp tránh việc chương trình vô tình làm hỏng CTDL của hệ điều hành.
## CloseHandle()
Nếu 1 tiến trình được chạy dưới 1 debugger và 1 handle không hợp lệ được truyền vào `ntdll!NtClose()` hoặc `kernel32!CloseHandle()` thì ngoại lệ EXCEPTION_INVALID_HANDLE (0xC0000008) sẽ được ném ra. Và ngoại lệ này có thể được bắt lại và việc chương trình trong phần được bắt lại được chạy sẽ thể hiện rằng đang có 1 debugger đang hoạt động.
`pseudo-code C`:
```cpp!
bool Check()
{
__try
{
CloseHandle((HANDLE)0xDEADBEEF);
return false;
}
__except (EXCEPTION_INVALID_HANDLE == GetExceptionCode()
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH)
{
return true;
}
}
```
- Nếu như ta chạy trong môi trường bình thường:
- `CloseHandle((HANDLE)0xDEADBEEF)` sẽ được gọi.
- Kernel kiểm tra và phát hiện ra đây là 1 invalid handle.
- Bản thân việc đưa invalid handle vào cái wrapper `CloseHandle` thì không hề raise cái SEH exception nào cả. Vì vậy, chương trình sẽ chảy đến `return false`.
- Còn nếu như ta sử dụng `debugger` thì:
- Đầu tiên `CloseHandle((HANDLE)0xDEADBEEF)` vẫn sẽ được gọi.
- Kernel thấy handle invalid → ngoài việc return code, nó raise một first-chance exception (EXCEPTION_INVALID_HANDLE = 0xC0000008) để debugger “nhìn thấy bug”. (Tức là, cứ thấy invalid là nó sẽ nhảy đi xem exception trước).
- Exception lọt xuống SEH.
- Tiếp theo, `exception` block sẽ được kích hoạt. Tiếp theo nó sẽ chảy phần `handler` sang phần return true.
### NtQueryObject()
Khi 1 tiến trình gỡ lỗi được bát đầu, 1 đối tượng kernel được tạo ra có tên `debug object`, và 1 handle liên kết với nó. Sử dụng hàm `ntdll!NtQueryObject()` ta có thể truy vấn danh sách các đối tượng đang tồn tại, và check số lượng handle đang được liên kết với các `debug object` đang tồn tại.
Tuy nhiên thủ thuật này không chắc rằng liệu tiến trình hiện tại có đang bị gỡ lỗi hay không. Nó chỉ cho thấy rằng liệu có debugger nào xuất hiện từ lúc bắt đầu hệ thống.
```cpp!
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;
}
```
### Giải thích:
Vì code pseudo phàn này khá là dài nên ta cần định nghĩa lại rõ ràng hơn 1 chút nó đang làm gì:
- Đầu tiên, định nghĩa các cấu trúc dữ liệu và hằng số ta sẽ sử dụng:
```cpp!
// Định nghĩa cấu trúc lưu thông tin cho MỘT loại đối tượng (ví dụ: File, Thread...)
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName; // Tên của loại đối tượng, ví dụ "DebugObject"
ULONG TotalNumberOfHandles; // Tổng số handle đang mở cho loại này
ULONG TotalNumberOfObjects; // Tổng số đối tượng loại này đang tồn tại
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION; // POBJECT... là kiểu con trỏ
// Định nghĩa cấu trúc cho TOÀN BỘ bản báo cáo
typedef struct _OBJECT_ALL_INFORMATION {
ULONG NumberOfObjects; // Có bao nhiêu LOẠI đối tượng trong báo cáo
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1]; // Một mảng chứa thông tin của từng loại
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION; // Mảng [1] là một mẹo để cấp phát động sau này
// Định nghĩa một "khuôn mẫu" cho con trỏ hàm NtQueryObject
typedef NTSTATUS (WINAPI *TNtQueryObject)(
HANDLE Handle,
OBJECT_INFORMATION_CLASS ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength
);
// Đặt tên cho số 3, là mã yêu cầu "Lấy thông tin tất cả các loại đối tượng"
enum { ObjectAllTypesInformation = 3 };
// Đặt tên cho mã lỗi 0xC0000004 để code dễ đọc hơn
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
```
- Tiến hành tạo hàm check để kiểm tra xem có đang hay đã có trình debug nào hay chưa:
- `status - Trạng thái trả về của các hàm NTAPI`: Được chia thành các phần nhưng phần quan trọng nhất chính phần 2 bit đầu tiên:
- `00`: (Bắt đầu bằng 0x0...) Thành công.
- `01`: (Bắt đầu bằng 0x4...) Thành công nhưng có thông tin bổ sung.
- `10`: (Bắt đầu bằng 0xB...) Thành công nhưng có cảnh báo đi kèm (VD: Bộ nhớ đệm quá nhỏ).
- `11`: (Bắt đầu bằng 0xC...) Thất bại.
```cpp!
bool Check() {
// --- Khai báo các biến sẽ dùng ---
bool bDebugged = false; // Cờ kết quả, mặc định là không có debugger
NTSTATUS status; // Lưu trạng thái trả về của các hàm NTAPI
LPVOID pMem = nullptr; // Con trỏ sẽ giữ "bản báo cáo" từ hệ điều hành
ULONG dwMemSize; // Kích thước của bản báo cáo
POBJECT_ALL_INFORMATION pObjectAllInfo; // Con trỏ để đọc bản báo cáo một cách có cấu trúc
PBYTE pObjInfoLocation; // Con trỏ phụ để duyệt qua từng mục trong báo cáo
HMODULE hNtdll; // Handle của thư viện ntdll.dll
TNtQueryObject pfnNtQueryObject; // Con trỏ để gọi hàm NtQueryObject
```
- Tiếp theo, tiến hành nạp thư viện `ntdll.dll` vào chương trình, đồng thời lấy địa chỉ của hàm `NtQueryObject`:
```cpp!
hNtdll = LoadLibraryA("ntdll.dll");
if (!hNtdll)
return false; // Nếu không load được thư viện, thoát
pfnNtQueryObject = (TNtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");
if (!pfnNtQueryObject)
return false; // Nếu không tìm thấy hàm, thoát
```
- Ta sẽ cần thăm dò kích thước cần thiết để dùng `NtQueryObject`. Mẹo để làm điều này đó chính là gọi hàm này lần đầu với bộ đệm `NULL`, ta làm vậy để hàm thất bại, sau đó nó sẽ ghi kích thước cần thiết vào `dwMemSize`.
```cpp!
status = pfnNtQueryObject(
NULL,
(OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
&dwMemSize, sizeof(dwMemSize), &dwMemSize);
if (STATUS_INFO_LENGTH_MISMATCH != status)
goto NtQueryObject_Cleanup; // Nếu không nhận được lỗi "thiếu bộ nhớ", có gì đó sai, dọn dẹp rồi thoát
```
- Đã có được thông tin kích thước cần thiết trong `dwMemSize`, tiến hành tạo 1 vùng nhớ để `pMem` trỏ vào:
```cpp!
pMem = VirtualAlloc(NULL, dwMemSize, MEM_COMMIT, PAGE_READWRITE);
if (!pMem)
goto NtQueryObject_Cleanup; // Nếu không cấp phát được bộ nhớ, dọn dẹp rồi thoát
```
- Lần này, gọi `NtQueryObject` thực sự:
```cpp!
status = pfnNtQueryObject(
(HANDLE)-1, // Đây là 1 Handle đặc biệt yêu cầu cung cấp thông tin của toàn bộ hệ thống.
(OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
pMem, dwMemSize, &dwMemSize);
if (!SUCCEEDED(status))
goto NtQueryObject_Cleanup; // Nếu lần này gọi mà vẫn lỗi, dọn dẹp rồi thoát
```
- Sau khi lấy thông tin, tiến hành đọc báo cáo:
```cpp!
pObjectAllInfo = (POBJECT_ALL_INFORMATION)pMem;
pObjInfoLocation = (PBYTE)pObjectAllInfo->ObjectTypeInformation;
// Lặp qua tất cả các loại đối tượng trong báo cáo
for(UINT i = 0; i < pObjectAllInfo->NumberOfObjects; i++)
{
// Coi vị trí hiện tại là một cấu trúc OBJECT_TYPE_INFORMATION
POBJECT_TYPE_INFORMATION pObjectTypeInfo =
(POBJECT_TYPE_INFORMATION)pObjInfoLocation;
// So sánh tên của loại đối tượng với chuỗi "DebugObject"
if (wcscmp(L"DebugObject", pObjectTypeInfo->TypeName.Buffer) == 0)
{
// Nếu tìm thấy, kiểm tra xem số lượng có lớn hơn 0 không
if (pObjectTypeInfo->TotalNumberOfObjects > 0)
bDebugged = true; // Nếu có, đặt cờ phát hiện
break; // Thoát khỏi vòng lặp vì đã tìm thấy thứ cần tìm
}
// --- Đoạn code phức tạp để nhảy đến mục tiếp theo trong báo cáo ---
// Lấy địa chỉ của chuỗi tên
pObjInfoLocation = (PBYTE)pObjectTypeInfo->TypeName.Buffer;
// Cộng thêm độ dài của chuỗi
pObjInfoLocation += pObjectTypeInfo->TypeName.Length;
// Làm tròn xuống bội số của 4 để căn lề (alignment)
ULONG tmp = ((ULONG)pObjInfoLocation) & -4;
// Cộng thêm 4 byte để đến đầu của cấu trúc tiếp theo
pObjInfoLocation = ((PBYTE)tmp) + sizeof(DWORD);
}
```
- Cuối cùng là dọn dẹp và trả về kết quả:
```cpp!
NtQueryObject_Cleanup:
if (pMem)
VirtualFree(pMem, 0, MEM_RELEASE); // Giải phóng bộ nhớ đã cấp phát
return bDebugged; // Trả về kết quả cuối cùng
```
## Mitigations
Cách đơn giản nhất để bypass các `check()` này đó là trace chương trình hay tiến trình 1 cách thủ công đến khi gặp 1 hàm `check` thì skip nó (VD: Patch bằng các lệnh `NOPs - ghi đè các lệnh đáng ngờ bằng lệnh ko làm gì cả Nop` hoặc set lại Zero Flag về 0 sau khi kiểm tra).
Nếu ta cần viết giải pháp `anti-anti-debug`, ta sẽ cần `hook` các hàm đã được liệt kê ở trên để thay đổi giá trị trả về (hook ở đây đó là đi đến vị trí hàm rồi thay đổi kết cấu của nó như giá trị trả về hoặc thêm dòng lệnh để đến khi tiến trình đi đến hàm này sẽ lập tức nhảy sang hàm khác chẳng hạn):
- Đối với `Close Handle - ntdll!NtClose`: (Chương trình cố gọi `CloseHandle` với Handle không hợp lệ để gây exception) Ta sẽ cần hook vào hàm `NtClose()`, lúc này ta dùng 1 hàm an toàn khác là NtQueryObject() để kiểm tra trước xem handle này có phải hợp lệ hay không. Nếu như Handle không hợp lệ thì skip hàm `NtClose` và cứ thế trả về 1 mã lỗi False.
- Đối với `NtQueryObject`: (Hàm này sẽ tiến hành truy vấn debugger trên cả OS) Ta sẽ cần lọc các `Debug Objects` ra khỏi báo cáo trả về. Ta hook vào `NtQueryObject`, cho nó chạy để trả về báo cáo. Nhưng trước khi báo cáo được trả về cho chương trình, ta sẽ code loại bỏ các dòng có chữ `Debug Object` đi.
# Exceptions
## RaiseException()
Các ngoại lệ như `DBG_CONTROL_C` hay `DBG_RIPEVENT` sẽ được consume bởi trình `debugger`, nếu có debugger hiện diện nó sẽ xử phần này hộ trường trình. Gỉa sử như ta thử ném ngoại lệ trên cho `RaiseException()`, nếu như không có trình debugger thì chương trình sẽ coi như đang có 1 ngoại lệ và nhảy đến phần exception handling. Trong trường hợp còn lại, nếu tiến trình đang được chạy trong môi trường debugger thì debugger sẽ nuốt và xử lý ngoại lệ này, khi đó chương trình được coi là không có ngoại lệ và tiếp tục chạy tiếp:
```cpp!
bool Check()
{
__try
{
RaiseException(DBG_CONTROL_C, 0, 0, NULL);
return true; // Nếu có debugger, Exception sẽ được handle bởi nó, chương trình chạy tiếp coi như ko có lỗi.
}
__except(DBG_CONTROL_C == GetExceptionCode()
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH)
{
return false; // Nếu không có debugger, Exxception sẽ được handle bởi chính chương trình hiện tại hay nhảy đến code exception.
}
}
```
## Hiding Control Flow with Exception Handlers
Cách này không check sự hiện diện của debugger, mà sẽ thực hiện dấu codeflow trong chuỗi các exception handler.
Ta sẽ đăng ký 1 trình xử lý ngoại lệ (có cấu trúc hoặc vectored) mà nó sẽ ném ra 1 ngoại lệ khác mà trong ngoại lệ này cũng ném ra 1 ngoại lệ khác nữa, cứ như vậy. Chuỗi các exception handler này sẽ chạy rồi dẫn đến tiến trình mà ta muốn che giấu:
```cpp!
#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 Handling (VEH):
`VEH` là 1 cơ chế xử lý ngoại lệ toàn cục của Windows:
Điểm mạnh:
- Phạm vi toàn cục: Bắt được ngoại lệ ở bất cứ đâu trong chương trình.
- Ưu tiên cao nhất: Nó được thông báo về ngoại lệ trước cả SEH.
- Có thể tạo chuỗi: Ta có thể đăng ký nhiều Handler, tạo thành 1 chuỗi xử lý phức tạp.
`Áp dụng VEH vào che dấu flow code`:
```cpp!
#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;
}
```
## Mitigations:
- `RaiseException`: Ta cần hook vào đoạn hàm `check()` sau đó thay các lệnh RaiseException thành NOP.
- `Hiding Control Flow with Exception Handlers`: Đối với các phương pahsp sử dụng ném Exception để làm nhiễu flowcode, ta phải truy vấn thủ công chuỗi các Exception Handlers này.
# Timing
Khi 1 tiến trình được trace bởi 1 debugger, có 1 khoảng delay rõ rệt giữa việc thực thi lệnh này với thực thi lệnh tiếp theo. Độ trê `native` sẽ được đo rồi so sánh với độ trễ thực tế trong môi trường debug để kiểm tra sự hiện diện của nó bằng nhiều cách khác nhau.
## RDPMC/RDTSC
Để sử dụng các lệnh này, cờ `PCE` trong các thanh ghi `CR4` cần phải được bật (=1).
`RDPMC` là 1 lệnh assembly cấp thấp được dùng để đọc 1 trong các bộ đếm hiệu năng (Performance Monitoring Counter) của CPU. Nó chỉ có thể được sử dụng trong chế độ Kernel:
```cpp!
bool IsDebugged(DWORD64 qwNativeElapsed)
{
ULARGE_INTEGER Start, End; // Tạo 2 biến lưu số chu kỳ (clock cycles) của CPU tại 2 thời điểm khác nhau: Ban đầu và sau khi làm việc.
__asm
{
xor ecx, ecx // Chỉnh giá trị của ecx về 0, vì rdpmc sẽ chọn xem nên đọc ở bộ đếm nào do CPU có nhiều bộ đếm khác nhau. Gía trị 0 là bộ đếm chu kỳ (clock cycles).
rdpmc
mov Start.LowPart, eax // Sau khi chạy rdpmc, kết quả các bit thấp được lưu ở eax
mov Start.HighPart, edx // Các bit cao được lưu ở rdx.
}
// ... some work
__asm
{
xor ecx, ecx // Sau khi chạy thử 1 đoạn code, tiến hành gọi rdpmc 1 lần nữa
rdpmc
mov End.LowPart, eax // Tương tự lưu kết quả trả về ở biến End
mov End.HighPart, edx
}
return (End.QuadPart - Start.QuadPart) > qwNativeElapsed; // So sánh chênh lệch clock cycles ở lần gọi đầu và lần gọi sau khi chạy vài đoạn code. So sánh chênh lệch này so với chênh lệch gốc chuẩn, nếu lớn hơn thì khả năng chương trình đã bị debugged.
}
```
`RTDSC` là 1 lệnh trong Chế độ Người dùng (User mode). Đây là người anh em họ dễ sử dụng hơn của RDPMC, viết tắt của `Read Time Stamp Counter - Đọc bộ đếm dấu thời gian`.
```cpp!
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;
}
```
## QueryPerformanceCounter()
Đây là API của `Window`, chức năng của nó giống hệt `RTDSC`:
```cpp!
bool IsDebugged(DWORD64 qwNativeElapsed)
{
LARGE_INTEGER liStart, liEnd;
QueryPerformanceCounter(&liStart);
// ... some work
QueryPerformanceCounter(&liEnd);
return (liEnd.QuadPart - liStart.QuadPart) > qwNativeElapsed;
}
```
## Mitigations
- Trong quá trình debug: Thay thế các hàm `Timing check` này bằng `NOPs` và set kết quả cho các hàm kiểm tra này thành giá trị thích hợp.
# Process Memory
Một tiến trình có thể tự truy vấn bộ nhớ của chính nó để phát hiện debugger hoặc cản trở debugger.
Phần này bao gồm các kĩ thuật như:
- Kiểm tra bộ nhớ tiền trình và ngữ cảnh các nguồn(thread contexts).
- Tìm kiếm các breakpoints.
- Vá/hack lại các hàm (function patching).
## BreakPoint: Software Breakpoints (INT3)
Ý tưởng ở đây đó chính là kiểm tra mã máy của chương trình để tìm byte `0xCC` = `INT 3` trong lệnh assembly.
Cách làm này có thể gây ra nhiều trường hợp dương tính giả nên cần được sử dụng cẩn thận.
```cpp!
bool CheckForSpecificByte(BYTE cByte, PVOID pMemory, SIZE_T nMemorySize = 0)
{
PBYTE pBytes = (PBYTE)pMemory;
for (SIZE_T i = 0; ; i++)
{
// Break on RET (0xC3) if we don't know the function's size
if (((nMemorySize > 0) && (i >= nMemorySize)) ||
((nMemorySize == 0) && (pBytes[i] == 0xC3))) // Điều kiện: Đã check đủ byte or Số byte check = 0xC3 thì dừng lại
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;
}
```
## Memory Breakpoints
### Trang canh khác (PAGE_GUARD):
> Bom nổ 1 lần:
`Trang bộ nhớ - Memmory Page` là đơn vị quản lý bộ nhớ cơ bản nhất mà CPU và Hệ điều hành sử dụng. Hãy tưởng tượng toàn bộ bộ nhớ ảo của chương trình bạn là một cuốn sách khổng lồ. Thay vì quản lý từng chữ cái một (từng byte), hệ điều hành sẽ chia cuốn sách đó thành các trang có kích thước cố định.
`Page Guard`:
- Gài PageGuard này lên 1 trang bộ nhớ nào đó.
- Bất cứ ai (đọc, ghi hay thực thi) chạm vào trang đó đầu tiên, trang sẽ ném ra 1 ngoại lệ `STATUS_GUARD_PAGE_VIOLATION`.
- Ngay sau khi ném ngoại lệ, PageGuard sẽ tự động biến mất và trang bộ nhớ trở lại như cũ, lần truy cập tiếp theo sẽ không ném lỗi nữa.
Debugger sử dụng cơ chế này để set Memory Breakpoints: Gài bẫy `PAGE_GUARD` lên 1 trang nhớ, khi chương trình chạm vào trang đó, bẫy nổ và ném ra ngoại lệ, debugger bắt lại và dừng chương trình.
### Phân tích:
Quay lại chủ đề chính: Ta có thể lợi dụng cách mà debugger tạo Memory Breakpoints để kiểm tra xem chương trình có được chạy trong môi trường debugger hay không. Cụ thể, chương trình sẽ tự gài `PageGuard` rồi xem phản ứng của môi trường xung quanh:
`Các bước thực hiện`:
- Đầu tiên, tạo 1 vùng nhớ nhỏ chỉ chứa duy nhất lệnh RET - 0xC3.
- Gài bẫy `PAGE_GUARD` vào vùng nhớ này thông bằng `VirtualProtect`.
- Chuẩn bị lối thoát: Đẩy địa chỉ đoạn code xử lý anti-debug vào stack.
- JUMP hoặc CALL đến vùng nhớ hoặc hàm thực thi RET có gẵn bẫy kia.
`Khi này, nếu có debugger`:
- CPU chạm vào trang nhớ, `PAGE_GUARD` nổ.
- Debugger đang theo dõi chương trình sẽ thấy xuất hiện `STATUS_GUARD_PAGE_VIOLATION` và nghĩ rằng đây là breakpoint mà nó đã đặt, tiến hành xử lý ngoại lệ, gỡ cờ `PAGE_GUARD` và rồi từ đó chương trình tiếp tục chạy tiếp như bình thường.
- Lệnh RET được thực thi, nó lấy địa chỉ của "Lối thoát" mà ta đã giăng sẵn và nhảy đến đó. Ta phát hiện đang có 1 debugger.
`No debugger`:
- Set up bẫy và chạy như trên.
- Lúc này khi ngoại lệ xuất hiện, vì không có debugger handle ngoại lệ này, chương trình sẽ crash hoặc tìm 1 trình xử lý ngoại lệ. Đây là dấu hiệu cho thấy không xuất hiện debugger.
```cpp!
bool IsDebugged()
{
DWORD dwOldProtect = 0;
SYSTEM_INFO SysInfo = { 0 };
GetSystemInfo(&SysInfo);
PVOID pPage = VirtualAlloc(NULL, SysInfo.dwPageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == pPage)
return false;
PBYTE pMem = (PBYTE)pPage;
*pMem = 0xC3; // Set byte đầu tiên của Page - trang nhớ thành lệnh RET.
// 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;
}
```
## Hardware Breakpoints
Các thanh ghi của Debugger như `DR0, DR1, DR2 and DR3` có thể được truy cập từ CONTEXT của tiến trình. Nếu các thanh ghi này chứa các giá trị khác 0 thì nghĩa là tiến trình hiện tại đang bị debugged.
```cpp!
bool IsDebugged()
{
CONTEXT ctx;
ZeroMemory(&ctx, sizeof(CONTEXT));
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if(!GetThreadContext(GetCurrentThread(), &ctx))
return false;
return ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3;
}
```
## Other memory check: Performing code checksums:
Xác nhận code checksum là 1 cách tốt để kiểm tra anti debug.
Trước khi chương trình khởi động, nó sẽ quét qua các đoạn mã lệnh của chính nó và tính toán giá tị `checksome` (Giống như 1 kiểu fingerprint: CRC32, MD5,...). Gía trị này gọi là fingerprint gốc. Tiếp theo, chương trình sẽ liên tục hoặc ngẫu nhiên tính toán lại giá trị này, và nếu chúng không khớp thì có thể hệ thống đã bị thay đổi (Khi sử dụng debugger, nó sẽ ghi đè mã lệnh = 1 byte `0xCC`, sự thay đổi nhỏ này sẽ làm thay đổi mã lệnh gốc làm vân tya bị thay đổi theo).
Thử nhìn ví dụ dưới đây:
```cpp!
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;
}
```
- Đầu tiên, tại hàm main tiến hành tính toán `checksome gốc` khi chương trình vẫn còn sạch.
- Tiếp theo, tạo 1 luồng thực thi song song như ` người bảo vệ ` với nhiệm vụ duy nhất là chạy hàm `ThreadFuncCRC32`.
- Hàm `ThreadFuncCRC32` có nhiệm vụ tính toán giá trị `checksome`, nếu giá trị này khác so với giá trị gốc thì chương trình tự động thoát, nếu không thì tiếp tục kiểm tra lần tiếp theo sau mỗi 10s.
## Mitigations:
- Đối với các trick Anti-Step-Over: StepIn vào hàm có sử dụng StepOver check, rồi sau đó chạy cho đến khi return (Ctrl+F9 in OllyDbg/x32/x64dbg).
- Cách tốt nhất để vượt qua `memory check` đó là phải mò chính xác đoạn kiểm tra và vá nó bằng `NOP`, hoặc đặt giá trị trả về hợp lệ để chương trình tiếp tục chạy được xa hơn.
# Assembly instructions
## POPF and Trap Flag:
Có 1 cái TrapFlag trong thanh ghi Flag, khi TrapFlag đã được cài đặt thì exception `SINGLE STEP` sẽ được ném ra. Nếu như ta trace, debug code thì debugger sẽ xóa TrapFlag và không gặp được ngoại lệ này.
```cpp!
bool IsDebugged()
{
__try
{
__asm // Tiến hành bật TrapFlag
{
pushfd //push flag dword - Đẩy toàn bộ thanh ghi Flag (có chứa TrapFlag) lên stack.
mov dword ptr [esp], 0x100 // Ghi đè giá trị đỉnh stack bằng 0x100 - Đây là giá trị để bật TrapFlag(cờ ở bit thứ 8)
popfd //Lúc này đỉnh stack chỉ còn là 0x100, pop và truyền lại vào thanh Ghi Flag.
nop
}
return true; // Không nhảy vào exception chứng tỏ chương trình đang bị Debug, trả về True.
}
__except(GetExceptionCode() == EXCEPTION_SINGLE_STEP
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_EXECUTION)
{
return false;
}
}
```
## Instruction Prefixes
Trick này chỉ hiệu quả với 1 số debuggers.
Nếu chúng ta chạy code này trong Ollydbg, sau khi step in vào byte đầu tiên F3, ta sẽ ngay lập tức chạy vào cuối block. Debugger sẽ skip nguyên đoạn prefix và nhảy đến INT 1 instruction luôn. (Trong ví dụ này nếu chạy trong môi trường bình thường thì chương trình sẽ ném lỗi và chạy vào catch).
```cpp!
bool IsDebugged()
{
__try
{
// 0xF3 0x64 disassembles as PREFIX REP:
__asm __emit 0xF3
__asm __emit 0x64
// One byte INT 1
__asm __emit 0xF1
return true;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
}
```
- Ta có tình tạo 1 đống byte prefix bẫy, khi debug nhảy đến chúng sẽ bị rỗi và tìm đến instruction gần nhất đó là INT 1 và âm thầm nuốt lỗi. Lúc này hàm check debug sẽ trả về true.
- Nếu chạy trong chương trình bình thường, khi chạy đến INT 1 (F1) sẽ ném lỗi và ngay lập tức chuyển qua except để thực thi lệnh ở đó (trả về false - ko có debug).
## Mitigations:
- Vá NOP: Đây là cách phổ biến và khá đáng tin cậy:
- Hầu hết các bẫy anti-debug đều có một lệnh nhảy điều kiện (je, jne, jz...) để quyết định xem nên thoát chương trình (nhánh "có debug") hay chạy tiếp (nhánh "không debug"). Ta dùng debugger tìm đến chính xác lệnh nhảy đó. Sau đó, bạn "vá" nó bằng cách ghi đè lệnh nhảy bằng các lệnh NOP.
- Né anti-tracing:
- Bẫy `TrapFlag` thường được thiết kế để nổ thi ta Stepin, vậy để né thì ta nên nhảy cóc thay vì chạy từng lệnh.
- Ta tìm đoạn code theo sau đoạn checks rồi đặt 1 breakpoint rồi chạy liên tục chương trình cho đến cái breakpoint này.
# Direct debugger
Phần này nói về các cách mà chương trình chống lại debugger bằng cách tương tác trực tiếp với hệ thống hoặc tiến trình cha chứ không đơn thần là check các flag.
## Self-debug:
`Ý tưởng chính`: 1 tiến trình chỉ có thể được debug bởi 1 debugger duy nhất tại 1 thời điểm.
Có tất cả ít nhất 3 hàm có thể dùng để gắn như 1 debugger trong 1 tiến trình:
- kernel32!DebugActiveProcess()
- ntdll!DbgUiDebugActiveProcess()
- ntdll!NtDebugActiveProcess()
Nhìn vào ví dụ sau:
```cpp!
#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) // Kịch bản 1: Tiến trình cha.
{
if (IsDebugged()) // Tiến trình cha tự kiểm tra xem đang có bị debug không, nếu có thì thoát ngay.
ExitProcess(0);
}
// Kịch bản 2: Tiến trình con:
else
{
DWORD dwParentPid = atoi(argv[1]); //Lấy PID tiến trình cha từ tham số dòng lệnh.
HANDLE hEvent = OpenEventW(EVENT_MODIFY_STATE, FALSE, EVENT_SELFDBG_EVENT_NAME); // Mở flag cha tạo
// Xin phép Window "Debug".
if (hEvent && EnableDebugPrivilege())
{
// Cố gắng debug.
if (FALSE == DebugActiveProcess(dwParentPid)) // Nếu debug thất bại (do tiến trình cha đã bị debug từ trước), báo động.
SetEvent(hEvent);
else
DebugActiveProcessStop(dwParentPid); //Debug thành công thì gỡ debug ra ngay.
}
ExitProcess(0); // Hoàn thành nhiệm vụ , tự thoát.
}
// ...
return 0;
}
```
## NtSetInformationThread()
Hàm ntdll!NtSetInformationThread() có thể được sử dụng để giấu 1 Luồng khỏi debugger. Điều này có thể thực hiện được nhờ có Gía trị không chính thức `THREAD_INFORMATION_CLASS::ThreadHideFromDebugger (0x11)`. Thứ này sinh ra để sử dụng ngoại, nhưng các Luồng đều có thể sử dụng.
Sau khi giấu Luồng, Luồng này có thể thực hiện các phương pháp anti-debug: code-checksum, debug flags, vv...
Tuy nhiên nếu Luồng được dấu có chứa Breakpoint hoặc ta cố tình dấu Luồng chính `main thread` đi thì chương trình sẽ bị crash và debugger sẽ bị kẹt.
```cpp!
#define NtCurrentThread ((HANDLE)-2)
bool AntiDebug()
{
NTSTATUS status = ntdll::NtSetInformationThread(
NtCurrentThread,
ntdll::THREAD_INFORMATION_CLASS::ThreadHideFromDebugger,
NULL,
0);
return status >= 0;
}
```
## EnumWindows() and SuspendThread()
Ý tưởng của cách này đó là chặn Luồng đang sở hữu tiến trình cha. Nói cách khác, đây là ý tưởng đóng băng Luồng chính của Debugger.
Nhưng làm thế nào để biết được Luồng cha là 1 debugger. Ta có thể liệt kê mọi cửa sổ cấp cao nhất trên màn hình (bằng cách sử dụng user32!EnumWindows() or user32!EnumThreadWindows()), tìm kiếm các cửa sổ xem cái nào có PID giống như PID của tiến trình cha (user32!GetWindowThreadProcessId()) và kiểm tra title của cửa sổ này (sử dụng user32!GetWindowTextW()). Nếu như title khả nghi thì ta có thể chặn Luồng bằng kernel32!SuspendThread() or ntdll!NtSuspendThread().
```cpp!
DWORD g_dwDebuggerProcessId = -1;
// hwnd: handle cửa sổ window
// lparam: Dữ liệu ta truyền vào (ở đây là PID của cha).
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;
}
```
## Mitigation:
Trong quá trình debug, nên bỏ qua các lần gọi hàm đáng ngờ (giả sử như ta có thể thay thế chúng = NOP).
# Misc
## FindWindow()
Đây là cách liệt kê các Window class và so sánh chúng với debug class đã biết.
Các hàm sau có thể được sử dụng để thực hiền điều này:
- user32!FindWindowW()
- user32!FindWindowA()
- user32!FindWindowExW()
- user32!FindWindowExA()
```cpp!
const std::vector<std::string> vWindowClasses = {
"antidbg",
"ID", // Immunity Debugger
"ntdll.dll", // peculiar name for a window class
"ObsidianGUI",
"OLLYDBG",
"Rock Debugger",
"SunAwtFrame",
"Qt5QWindowIcon"
"WinDbgFrameClass", // WinDbg
"Zeta Debugger",
};
bool IsDebugged()
{
for (auto &sWndClass : vWindowClasses)
{
if (NULL != FindWindowA(sWndClass.c_str(), NULL))
return true;
}
return false;
}
```
## Parent Process Check
Bình thường, 1 tiến trình được chạy ở user-mode bằng cách nhấn đúp 2 lần vào file icon. Nếu như 1 tiến trình được chạy bằng cách này thì tiến trình cha sẽ là tiến trình shell `shell process - explorer.exe`.
Ý tưởng chính của 2 phương pháp dưới này đó là kiểm tra PID của tiến trình cha với PID của `explorer.exe`.
### NtQueryInformationProcess()
Phương pháp này bao gồm: Lấy `shell process window handle` của shell process bằng `user32!GetShellWindow()` và lấy PID của nó bằng cách gọi `user32!GetWindowThreadProcessId()`.
Sau đó, ID của tiến trình cha sẽ được lấy từ `PROCESS_BASIC_INFORMATION` bằng cách gọi: `ntdll!NtQueryInformationProcess() cùng với ProcessBasicInformation class`:
```cpp!
bool IsDebugged()
{
HWND hExplorerWnd = GetShellWindow();
if (!hExplorerWnd)
return false;
DWORD dwExplorerProcessId;
GetWindowThreadProcessId(hExplorerWnd, &dwExplorerProcessId);
ntdll::PROCESS_BASIC_INFORMATION ProcessInfo;
NTSTATUS status = ntdll::NtQueryInformationProcess(
GetCurrentProcess(),
ntdll::PROCESS_INFORMATION_CLASS::ProcessBasicInformation,
&ProcessInfo,
sizeof(ProcessInfo),
NULL);
if (!NT_SUCCESS(status))
return false;
return (DWORD)ProcessInfo.InheritedFromUniqueProcessId != dwExplorerProcessId;
}
```
## Mitigations:
Tương tự với những phần antidebug bypass trên, ở phần này dường như cách thay thế các khối kiểm tra debug bằng NOPs là khả thi nhất.