You are on page 1of 26

Một số kỹ thuật anti debug và anti anti-debug

1. IsDebuggerPresent()
Hàm kernel32 IsDebuggerPresent() kiểm tra xem chương trình đang chạy có đang được
debug bởi người dùng bằng OllyDbg, x64Dbg hay không. Hàm bool này trả về giá trị
“TRUE” nếu chương trình đang bị debug và “FALSE” nếu chương trình không bị debug.
Nhìn chung hàm này chỉ kiểm tra cờ BeingDebugged của Khối Môi trường Quy trình
(PEB).
Phương pháp này được thực hiện như sau:
a. Assembly:

section .text
global _start

_start:
xor eax, eax
mov ebx, 0x30
int 0x2D
test al, al
jz not_debugging
...
not_debugging:
...

b. C++:

#include <windows.h>
#include <iostream>

BOOL IsDebuggerPresent();

int main()
{
if (IsDebuggerPresent())
{
//being debugged
ExitProcess(-1);
}
else
{
//not being debugged
...
}
return 0;
}

2. PEB (Process Environment Block)


Phương pháp này là một cách kiểm tra cờ BeingDebugged của khối PEB mà không cần
gọi đến hàm IsDebuggerPresent().

#include <Windows.h>
#include <iostream>
#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)

#pragma comment(lib, "user32.lib")

typedef struct _PEB {


BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[21];
} PEB, * PPEB;

BOOL BeingDebuggedFlag() {
#ifdef _WIN64
PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif // _WIN64
if (pPeb->BeingDebugged)
MessageBox(NULL, L"Being debugged", L"Warning", MB_OK);
else
MessageBox(NULL, L"Normal running", L"Notice", MB_OK);

return pPeb->BeingDebugged;
}

int main(int argc, char* argv[]) {


BOOL check = BeingDebuggedFlag();
return 0;
}

Hình 1.

Ở đây có giá trị gs:60h kiểm tra lệnh nhảy tới loc_7FF7285F1032, dump giá trị này để
chỉnh sửa lệnh nhảy và bypass hoặc có thể sử dụng các plugin bên thứ 3 như ScyllaHide:
https://github.com/x64dbg/ScyllaHide
Đây là chương trình khi phát hiện trình debug.

Hình 2.

Sau khi kích hoạt plugin ScyllaHide, đây là kết quả khi debug:
Hình 3.

3. TLS Callback:
Thread Local Storage(TLS) là phương pháp mà mỗi một luồng trong một quy trình đa
luồng nhất định có thể phân bổ các vị trí để lưu trữ dữ liệu giành riêng cho luồng. Thời gian
chạy bị ràng buộc được hỗ trợ bằng API TLS (TlsAlloc). Win32 và trình biên dịch
Microsoft C++ hiện hỗ trợ dữ liệu trên luồng được liên kết tĩnh ngoài việc triển khai API
hiện có.
#include <Windows.h>
#include <stdio.h>
/* Pointer to a TLS callback function */
typedef void(__stdcall* TLS_CALLBACK_PTR)(void* instance, int
reason, void* reserved);
void __stdcall tls_callback1(void* instance, int reason, void*
reserved)
{
if (reason == DLL_PROCESS_ATTACH)
{
MessageBox(NULL, L"Hidden action in callback 1", L"Callback 1",
MB_OK);
}
}
void __stdcall tls_callback2(void* instance, int reason, void*
reserved)
{
if (reason == DLL_PROCESS_ATTACH) {
MessageBox(NULL, L"Hidden action in callback 2", L"Callback 2",
MB_OK);
ExitProcess(0);
}
}
#pragma comment(linker, "/INCLUDE:__tls_used")
#pragma data_seg(".CRT$XLAAA")
EXTERN_C TLS_CALLBACK_PTR p_tls_callback1 = tls_callback1;
#pragma data_seg(".CRT$XLAAB")
EXTERN_C TLS_CALLBACK_PTR p_tls_callback2 = tls_callback2;
int main(int argc, char* argv[])
{
/* Will never be called, as the last TLS callback does
ExitProcess() */
MessageBox(NULL, L"Decoy: Hello, world!", L"From main!", MB_OK);
return 0;
}

Phương pháp Bypass:


Sử dụng CFF ta có thể kiểm tra địa chỉ callback của các TLS từ đó có thể phân tích đoạn
mã nằm ở phần TLS.
Hình 4.

Dùng IDA để nhảy tới địa chỉ AddressOfCallBack 004020EC:

Hình 5.

Hình 6.

Trình debug như x64dbg cũng hỗ trợ phân tích TLS call back:

4. NtGlobalFlag
Trên dòng máy 32-bit, trường NtGlobalFlag được đặt ở vị trí offset của PEB (khối môi
trường biến tiến trình) là 0x68, và trên dòng máy 64-bit nó nằm ở offset ‘0xBC’. Giá trị mặc
định cho trường này là 0. Khi debug, trường này được đặt thành một giá trị cụ thể trong khi
thiết bị đang chạy.
Ứng dụng:
 32 bit:

mov eax, fs:[30h] ;Process Environment Block


mov al, [eax+68h] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged

 64 bit:

push 60h pop rsi gs:lodsq ;Process Environment Block


mov al, [rsi*2+rax-14h] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged

 Code C++:
#include <Windows.h>
#include <stdio.h>
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[21];
} PEB, * PPEB;
#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 checkNtGlobalFlag() {
PPEB ppeb;
#ifdef _WIN64
ppeb = (PPEB)__readgsqword(0x30);
#else
ppeb = (PPEB)__readfsdword(0x18);
#endif // _WIN64
DWORD myNtGlobalFlag = ppeb->BeingDebugged;
MessageBox(NULL, (myNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED) ? L"Being
debugged!" : L"Normal running time", L"Notice", MB_OK);
return 0;
}

int main(int argc, char* argv[]) {


DWORD check = checkNtGlobalFlag();
return 0;
}

Phương pháp để vượt qua detection:


 Thay đổi giá trị của GlobalFLag tại:
HKLM\System\CurrentControlSet\Control\SessionManager
Hình 7.

Khi debug bằng IDA mà chưa thực hiện bypass:


Hình 8.

Sử dụng plugin ScyllaHide ta cũng có thể vượt qua cơ chế kiểm tra này:

Hình 9.

Hình 10.

5. Timing
Khi một tiến trình có hiện tượng bị debug, độ trễ thực thi sẽ lớn hơn so với lúc tiến trình
chạy bình thường. Việc so sánh độ trễ bình thường của một đoạn mã với độ trễ của tiến trình
thực tế để phát hiện nó đang bị debug.
Để vượt qua cơ chế bảo mật này, vì bản thân phương pháp này không đáng tin cậy nên
chỉ cần tăng tốc thời gian giữa các lần gọi hàm là có thể vượt qua phương pháp bảo mật này.
ScyllaHide cũng hộ trợ bypass phương pháp Timing.
Một số phương pháp timing như:
a. GetLocalTime():

bool IsDebugged(DWORD64 qwNativeElapsed)


{
SYSTEMTIME stStart, stEnd;
FILETIME ftStart, ftEnd;
ULARGE_INTEGER uiStart, uiEnd;
GetLocalTime(&stStart);

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;
}

b. GetTickCount()

bool IsDebugged(DWORD dwNativeElapsed)


{
DWORD dwStart = GetTickCount();

return (GetTickCount() - dwStart) > dwNativeElapsed;
}

c. GetSystemTime()

bool IsDebugged(DWORD64 qwNativeElapsed)


{
SYSTEMTIME stStart, stEnd;
FILETIME ftStart, ftEnd;
ULARGE_INTEGER uiStart, uiEnd;
GetSystemTime(&stStart);

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;
}
Ví dụ:

#include <Windows.h>
#include<sysinfoapi.h>

bool IsDebugged(DWORD dwNativeElapsed)


{
DWORD dwStart = GetTickCount();
// Simulate some work by running a loop
for (int i = 0; i < 10000000; ++i)
{
// Perform some dummy calculations
int result = i * 2 + 3;
}
return(GetTickCount() - dwStart) > dwNativeElapsed;
}
/*
x86_64-w64-mingw32-g++ timing.cpp -O2 -o timing.exe -I/usr/share/mingw-
w64/include/ -s -ffunction-sections -fdatasections -Wno-write-strings -
fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -
fpermissive
*/
int main()
{
DWORD dwNativeElapsed = 1000; // Example elapsed time inmilliseconds
bool isDebugged = IsDebugged(dwNativeElapsed);
if (isDebugged)
{
// The application is being debugged
// Take appropriate action here
MessageBox(NULL, L"Debugger detected", L"Warning", MB_OK);
}
else
{
MessageBox(NULL, L"Normal runtime", L"Notice", MB_OK);
// The application is not being debugged
}
return 0;
}

Trước khi áp dụng ScyllaHide:


Hình 11.

Sau khi sử dụng ScyllaHide:

Hình 12.
Hình 13.

6. INT 3
Kỹ thuật này còn được gọi là “software breakpoint” hoặc là “trap interrupt”. Là một cách
thường được sử dụng để phát hiện và ngăn chặn quá trình debug. Khi được triển khai trên
phần mềm, INT 3 sẽ tạo ra “exception” để chương trình tự động ngắt hoặc dẫn đến sự gián
đoạn khi chương trình được thực thi trên debugger
Để sử dụng INT 3, trong mã assembly hoặc là mã nguồn của chương trình, một đoạn mã
INT 3 sẽ được chèn vào. Khi chương trình chạy tới đây, nó sẽ tạo ra exception để ngắt
chương trình khi phát hiện debugger.
Lấy file mẫu là một challenge trong CTF Seccon 2016: bin.exe.

Hình 14.
Ta thấy rằng trong dissasemble code có những hàm để kiểm tra OllyDbg, IDA,
VMware…
Chương trình cho nhập mật khẩu nhưng có kiểm tra các yếu tố như debugger và máy ảo:

printf("Input password >");


v3 = (FILE *)sub_40223D();
fgets(Str1, 64, v3);
strcpy(Str2, "I have a pen.");
v11 = strncmp(Str1, Str2, 0xDu);
if ( !v11 )
{
puts("Your password is correct.");
if ( IsDebuggerPresent() )
{
puts("But detected debugger!");
exit(1);
}
if ( sub_401120() == 112 )
{
puts("But detected NtGlobalFlag!");
exit(1);
}
CurrentProcess = GetCurrentProcess();
CheckRemoteDebuggerPresent(CurrentProcess, pbDebuggerPresent);
if ( pbDebuggerPresent[0] )
{
printf("But detected remotedebug.\n");
exit(1);
}
TickCount = GetTickCount();
pbDebuggerPresent[3] = 0;
pbDebuggerPresent[1] = 1000;
if ( GetTickCount() - TickCount > 0x3E8 )
{
printf("But detected debug.\n");
exit(1);
}
Hình 15.

Ta thấy ở đây chương trình thực hiện so sánh các biến với kết quả trả về từ các hàm kiểm
tra sau đó thực hiện ngắt nếu phát hiện máy ảo hay là debugger.
Ta có thế điều chỉnh các giá trị né các lệnh exit trong các câu điều kiện để có thể gọi
được message box:

Hình 16.
Hình 17.

Vậy ta có thể dùng OllyDebug để vừa debug vừa sửa đổi các giá trị trên:

Hình 18. Chỉnh sửa GlobalFlag thành 0

Sau khi thay đổi toàn bộ giá trị kiểm tra thì ta đã có thể gọi ra message box:

Hình 19.
Hình 20.

7. CheckRemoteDebuggerPresent()
Hàm CheckRemoteDebuggerPresent() kiểm tra xem có một debugger nào khác tác động
đến tiến trình hiện tại hay không (Remote debug IDA dùng để debug code trên máy ảo
Linux).

Hình 21.

Hình 22.
Để kiểm tra remote debugger chỉ cần thêm đoạn script sau vào mã nguồn.
a. C++

#include <Windows.h>
#include <stdio.h>

BOOL IsRemoteDebuggerPresent() {
BOOL isDebuggerPresent = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebuggerPresent);
return isDebuggerPresent;
}

int main() {
if (IsRemoteDebuggerPresent()) {
MessageBox(NULL, L"Remote Debugger Detected!", L"Warning",
MB_OK);
}
else {
MessageBox(NULL, L"No Remote Debugger Detected.", L"Notice",
MB_OK);
}
return 0;
}

b. Assembly:

lea rdx, [bDebuggerPresent]


mov rcx, -1 ; GetCurrentProcess()
call CheckRemoteDebuggerPresent
cmp [bDebuggerPresent], 1
jz being_debugged
...
being_debugged:
mov ecx, -1
call ExitProcess

Hình 23.
8. NtQueryInformationProcess()
Hàm NtQueryInformationProcess() (ntdll) có thể truy xuất các thông tin khác nhau từ
một tiến trình (ProcessInformationClass được chỉ định thông tin muốn lấy từ tiến trình).
Các hàm có thể được sử dùng cùng với NtQueryInformationProcess():
a. DebugActiveProcess()
Trong Window API, hàm này cho phép ứng dụng có nó được phép debug một tiến trình
khác. Trong anti debug, nó có thể cho phép một ứng dụng debug chính nó sau đó kiểm tra
xem bản thân có đang bị debug bởi một ứng dụng khác hay không. Phương pháp này cũng
có thể được ứng dụng để chống remot debug.

typedef LONG (WINAPI *NtQueryInformationProcessPtr)(


HANDLE ProcessHandle,
DWORD ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
BOOL IsBeingDebugged() {
NtQueryInformationProcessPtr NtQIP =
(NtQueryInformationProcessPtr)GetProcAddress(
GetModuleHandle(L"ntdll.dll"), "NtQueryInformationProcess"
);

if (NtQIP) {
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;

NtQIP(GetCurrentProcess(), 0, &pbi, sizeof(pbi), &returnLength);

return pbi.PebBaseAddress->BeingDebugged != 0;
}

return FALSE;
}

BOOL IsDebuggedWithDebugActiveProcess() {
BOOL isDebugged = FALSE;
DebugActiveProcess(GetCurrentProcess());
isDebugged = IsBeingDebugged();
DebugActiveProcessStop(GetCurrentProcess());
return isDebugged;
}

b. OutputDebugStringW(), OutputDebugStringA()
Đây cũng là những hàm trong Window API dùng để ghi lại các thông điệp vào debugger
windows. Được sử dụng phổ biến trong các trình biên dịch. Tuy nhiên trong ngữ cảnh anti
debug, chúng ta có thể tận dụng việc ghi lại thông tin debug này để kiểm tra xem có thông
tin ứng dụng đang bị debug trả về hay không.

typedef LONG (WINAPI *NtQueryInformationProcessPtr)(


HANDLE ProcessHandle,
DWORD ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);

void DebugPrint(const wchar_t* message) {


OutputDebugStringW(message);
}

BOOL IsBeingDebugged() {
NtQueryInformationProcessPtr NtQIP =
(NtQueryInformationProcessPtr)GetProcAddress(
GetModuleHandle(L"ntdll.dll"), "NtQueryInformationProcess"
);

if (NtQIP) {
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;

NtQIP(GetCurrentProcess(), 0, &pbi, sizeof(pbi), &returnLength);

return pbi.PebBaseAddress->BeingDebugged != 0;
}

return FALSE;
}

c. ContinueDebugEvent()
Hàm Window API cho phép tiếp tục quá trình debug sau khi đã xử lí xong một sự kiện
nào đó trong tiến trình (jump, suspend, continue…). Nó cho phép debugger thông báo tới hệ
điều hành rằng nó đã xử lí xong sự kiện và muốn tiếp tục quá trình debug.
Lợi dụng điểm này, ContinueDebugEvent() có thể được sử dụng để đánh lừa debugger:
int main() {
DEBUG_EVENT debugEvent;
HANDLE hProcess = GetCurrentProcess();
HANDLE hThread = GetCurrentThread();

// Simulate a debug event by filling out the DEBUG_EVENT structure


debugEvent.dwDebugEventCode = EXCEPTION_DEBUG_EVENT;
debugEvent.u.Exception.ExceptionRecord.ExceptionCode =
EXCEPTION_BREAKPOINT;

// Continue the debug event, effectively ignoring it


ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId,
DBG_CONTINUE);

MessageBox(NULL, L"Debugger Detected!", L"Warning", MB_OK);

return 0;
}

Trong ví dụ trên, ta sử dụng ContinueDebugEvent() để tiếp tục quá trình gỡ lỗi


“EXCEPT_BREAKPOINT” mà ta tự tạo. Sự kiện này sẽ dừng việc gỡ lỗi lại sau khi được
xử lí xong.
d. DebugBreak()
Hàm C/C++ DebugBreak() cho phép đặt break point tại vị trí mà nó được đặt trong mã
nguồn. Khi một chương trình được thực thi và gặp phải hàm DebugBreak(), quá trình thực
thi sẽ bị tạm dừng và điều hướng tới debugger đang hoạt động.
Trong ngữ cảnh anti debug, bằng cách đặt hàm này tại các vị trí quan trọng, lập trình viên
có thể ngăn việc debug bằng cách cho break point nằm tại những vị trí mà chưa được xử lí.

#include <Windows.h>

int main() {
// Some code...

DebugBreak(); // Ngăn chặn quá trình gỡ lỗi

// Tiếp tục thực thi sau khi gỡ lỗi bị ngăn chặn


MessageBox(NULL, L"Debugger Detected!", L"Warning", MB_OK);

return 0;
}

e. ProcessDebugObjectHandle()
Khi bắt đầu debug, kernel sẽ gọi lên một cửa sổ debug (debug object). Việc tận dụng sự
tồn tại của cửa sổ này cũng có thể đưa vào NtQueryInformationProcess() để xác định
debugger.
typedef LONG (WINAPI *NtQueryInformationProcessPtr)(
HANDLE ProcessHandle,
DWORD ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);

BOOL IsDebuggerPresentUsingProcessDebugObject() {
// Kiểm tra sự hiện diện của ProcessDebugObjectHandle
HANDLE hDebugObject =
OpenProcessDebugObject(PROCESS_QUERY_INFORMATION, FALSE);
if (hDebugObject != NULL) {
CloseHandle(hDebugObject);
return TRUE;
}
return FALSE;
}

BOOL IsBeingDebuggedUsingNtQueryInformationProcess() {
NtQueryInformationProcessPtr NtQIP =
(NtQueryInformationProcessPtr)GetProcAddress(
GetModuleHandle(L"ntdll.dll"), "NtQueryInformationProcess"
);

if (NtQIP) {
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;

NtQIP(GetCurrentProcess(), 0, &pbi, sizeof(pbi), &returnLength);

return pbi.PebBaseAddress->BeingDebugged != 0;
}

return FALSE;
}

f. Các hàm khác:


 CheckRemoteDebuggerPresent(): Kiểm tra xem tiến trình có đang bị debug từ xa
không.
 ProcessDebugPort(): Truy xuất port của debugger.
 DebugActiveProcessStop(): Dừng debugger.
Phương pháp sử dụng NtQueryInformationProcess() kết hợp với các thành phần trên có
thể bị vượt qua dễ dàng bởi các kỹ thuật nâng cao. Ví dụ ở đây là dùng ScyllaHide ta có thể
bypass gần như tất cả phương pháp NtQueryInformationProcess():
Hình 24.

Hình 25.

Hình 26.

You might also like