寫在前面
在逆向工程中為了防止破解者調試軟件,通常都會在軟件中采用一些反調試技術來防破解。下面就是一些在逆向工程中常見的反調試技巧與示例。
BeingDebuged
利用調試器加載程序時調試器會通過CreateProcess()創建調試進程,同時會創建調試內核對象並保存在當前線程環境塊的DbgSsReserved[1]字段中,此時此線程就不同於普通線程了(一般稱為調試器線程)。接着CreateProcess()會判斷是否有調試器,無則直接返回,有則調用PspCreateProcess(),此函數會根據DbgSsReserved[1]字段設置創建進程的EPROCESS的DebugPort字段,這樣此被創建進程就可以稱為被調試進程。接着PspCreateProcess()會調用MmCreatePeb(),此函數會根據EPROCESS的Debugport字段設置PEB的BeingDebuged字段。(無調試器時其值為0)
我們可以直接通過內聯匯編來訪問BeingDebuged字段,也可以通過IsDebuggerPresent()這個API來得到此字段的值。下面是示例程序。
int main(int argc, char *argv[])
{
DWORD dwBeingDebuged;
TCHAR szTest[] = TEXT("已檢測到調試器!");
TCHAR szSuccess[] = TEXT("運行正常!");
dwBeingDebuged = IsDebuggerPresent();
if(0 != dwBeingDebuged)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}
NtGlobalFlag
當BeingDebuged被設為“TRUE”后程序會將PEB的NtGlobalFlag設置0x70h標志。我們通過內聯匯編檢測進程環境快的NtGlobalFlag字段。
int main(int argc, char *argv[])
{
DWORD dwNtGlobalFlag;
TCHAR szTest[] = TEXT("已檢測到調試器!");
TCHAR szSuccess[] = TEXT("運行正常!");
dwNtGlobalFlag = _AntiDebug();
if(0 != dwNtGlobalFlag)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}
DWORD _AntiDebug()
{
_asm{
mov eax,fs:[0x30]
mov eax,[eax + 0x68]
and eax,0x70
}
}
ProcessHeap的Flags和ForceFlags
當NtGlobalFlag字段被設置0x70標志后,其會改變PEB的字段ProcessHeap所指向的Flags和ForceFlags的值。
對於x86平台而言Flags和ForceFlags偏移地址分別是相對於ProcessHeap指向地址的0x40和0x44處。當程序未被調試器調試時Flags的值應包含0x00000002標志,ForceFlags的值應為0,下面是通過內聯匯編檢測二者的值。
int main(int argc, char *argv[])
{
DWORD dwReturn;
TCHAR szTest[] = TEXT("已檢測到調試器!");
TCHAR szSuccess[] = TEXT("運行正常!");
dwReturn = _AntiDebug();
if(0 != dwReturn)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}
DWORD _AntiDebug()
{
_asm{
push ebx
mov eax,fs:[0x30]
mov eax,[eax + 0x18]
mov ebx,dword ptr [eax + 0x40]
and ebx,0x00000002
cmp ebx,0
je end0
mov ebx,dword ptr [eax + 0x44]
cmp ebx,0
jne end0
xor eax,eax
jmp end
end0:
mov eax,1
end:
pop ebx
}
}
Heap Magic
當進程被調試器調試時其進程堆會被一些特殊的標記填充,這些特殊標記分別是0xABABABAB , 0xBAADF00D , 0xFEEEFEEE。
通過從進程堆中創建內存塊來獲得進程堆的起始地址,然后通過對整個進程堆雙字遍歷來計算這些標志的數量。(如果大於10則證明進程被調試器調試了)
int main(int argc, char *argv[])
{
DWORD dwNum = 0;
PDWORD pHeap;
TCHAR szTest[] = TEXT("已檢測到調試器!");
TCHAR szSuccess[] = TEXT("運行正常!");
pHeap = (DWORD*)HeapAlloc(GetProcessHeap(), NULL, 0x100); //通過在自己的進程默認堆中申請內存得到堆中內存的地址,(還可以通過PEB的LDR_MODULE字段中得到這些標志)
_try{
for(;;){ //檢測Heap Magic標志。
switch(*pHeap++){
case 0xABABABAB:
case 0xBAADF00D:
case 0xFEEEFEEE:
dwNum++;
break;
}
}
}
_except(EXCEPTION_EXECUTE_HANDLER){
if(dwNum > 10)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}
ProcessDebugPort
如果進程被調試則進程會通過一個調試端口與調試子系統通信,進而與調試器通信。我們可以通過CheckRemoteDebuggerPresent()來檢測調試端口是否存在ProcessDebugPort實際就是EPROCESS的DebugPort字段。同時CheckRemoteDebuggerPresent()不僅可以檢測自身是否存在調試端口,還可以檢測其他進程是否存在調試端口。
int main(int argc, char *argv[])
{
BOOL bDebuggerPresent;
TCHAR szTest[] = TEXT("已檢測到調試器!");
TCHAR szSuccess[] = TEXT("運行正常!");
CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebuggerPresent);
if(TRUE == bDebuggerPresent)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}
實際CheckRemoteDebuggerPresent()函數內部是通過調用NtQueryInformationProcess()來檢測進程是否存在調試端口的,所以我們也可以直接通過動態調用NtQueryInformationProcess()來檢測是否存在調試端口。
typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)( //typedef后可以掩飾復合類型
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
//表示檢測進程的調試端口
UINT ProcessDebugPort = 7;
int main(int argc, char *argv[])
{
TCHAR szTest[] = TEXT("已檢測到調試器!");
TCHAR szSuccess[] = TEXT("運行正常!");
DWORD dwDebuggerPresent = 0;
NTSTATUS stNtstatus;
pfnNtQueryInformationProcess NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugPort,
&dwDebuggerPresent,
sizeof(DWORD),
NULL);
if(0 != dwDebuggerPresent && stNtstatus == 0x00000000)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}
ProcessDebugObjectHandle
如果進程被調試會創建一個調試內核對象,通過NtQueryInformationProcess()可以檢測是否存在調試內核對象句柄。
typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
UINT ProcessDebugObjectHandle = 0x1E;
int main(int argc, char* argv[])
{
HANDLE hDebugHandle;
NTSTATUS stNtstatus;
pfnNtQueryInformationProcess NtQueryInformationProcess;
NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugObjectHandle,
&hDebugHandle,
sizeof(HANDLE),
NULL);
if(0x00000000 == stNtstatus && NULL != isDebuggerPresent)
{
MessageBox(NULL, TEXT("已檢測到調試器!"),NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, TEXT("程序正常運行!"), NULL, MB_OK);
return 0;
}
ProcessDebugFlags
當進程被調試時其進程內核對象EPROCESS的NoDebugInherit字段會被置空,調用NtQueryInformationProcess可以檢測此字段的值。
typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
UINT ProcessDebugFlags = 0x1F;
int main(int argc, char* argv[])
{
ULONG DebugFlags;
NTSTATUS stNtstatus;
pfnNtQueryInformationProcess NtQueryInformationProcess;
NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugFlags,
&DebugFlags,
sizeof(ULONG),
NULL);
if(0x00000000 == stNtstatus && NULL == DebugFlags)
{
MessageBox(NULL, TEXT("已檢測到調試器!"),NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, TEXT("程序正常運行!"), NULL, MB_OK);
return 0;
}
ProcessBasicInformation
調用NtQuertyInformationProcess檢測ProcessBasicInformation得到一個數據結構。通過這個數據結構可以得到進程ID,進一步通過PID創建進程快照得到父進程的PID從而進一步得到父進程的用戶名。通過判斷父進程的用戶名是不是調試器而進行反調試。
#include <Windows.h>
#include <winternl.h>
#include <TlHelp32.h>
typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
const UINT uProcessBasicInformation = 0;
DWORD GetParentPID(DWORD dwPid);
BOOL GetExeNameByPID(char * szExeName, DWORD dwPid);
int main(int argc, char* argv[])
{
char szExeName[256] = {0};
DWORD dwParent;
NTSTATUS stNtstatus;
pfnNtQueryInformationProcess NtQueryInformationProcess;
PROCESS_BASIC_INFORMATION stProcessBasicInformation;
NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
uProcessBasicInformation,
&stProcessBasicInformation,
sizeof(PROCESS_BASIC_INFORMATION),
NULL);
dwParent = GetParentPID(stProcessBasicInformation.UniqueProcessId);
GetExeNameByPID(szExeName, dwParent);
if(0x00000000 == stNtstatus && (lstrcmp(szExeName, "explorer.exe") && lstrcmp(szExeName, "cmd.exe") && lstrcmp(szExeName, "services.exe")))
{
MessageBox(NULL, TEXT("已檢測到調試器!"),NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, TEXT("程序正常運行!"), NULL, MB_OK);
return 0;
}
//獲取父進程的PID
DWORD GetParentPID(DWORD dwPid)
{
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcessSnap);
return 0;
}
if(!Process32First(hProcessSnap, &pe32))
{
CloseHandle(hProcessSnap);
return 0;
}
do{
if(pe32.th32ProcessID == dwPid)
break;
}while(Process32Next(hProcessSnap, &pe32));
return pe32.th32ParentProcessID;
}
//獲取對應PID進程的程序名稱
BOOL GetExeNameByPID(char szExeName[], DWORD dwPid)
{
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcessSnap);
return 0;
}
if(!Process32First(hProcessSnap, &pe32))
{
CloseHandle(hProcessSnap);
return 0;
}
do{
if(pe32.th32ProcessID == dwPid)
{
lstrcpy(szExeName, pe32.szExeFile);
break;
}
}while(Process32Next(hProcessSnap, &pe32));
return 1;
}
HideFromDebugger
被調試進程通過調試端口DebugPort與調試子系統通信進一步與調試器通信。如果我們能將DebugPort清除就可以阻止進程與調試器通信達到反調試的目的,但是DebugPort一旦被創建就無法被在設置,所以我們這種思路走不通。
我們發現進程如果產生異常,其會檢測ETHREAD的HideFromDebugger成員是否為“FALSE”,如果是就會向當前進程的調試端口發送調試事件消息(為"TRUE"表示該異常對用戶調試器不可見)。我們可以通過ZwSetInformationThread()設置HideFromDebugger字段的值為TRUE從而令用戶調試器接收不到調試事件,達到反調試的目的。
#include <winternl.h>
typedef NTSTATUS (NTAPI *pfnZwSetInformationThread)(
_In_ HANDLE ThreadHandle,
_In_ UINT ThreadInformationClass,
_In_ PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
);
UINT uThreadHideFromDebugger = 0x11;
int main(int argc, char * argv[])
{
NTSTATUS stNtstatus;
pfnZwSetInformationThread ZwSetInformationThread;
ZwSetInformationThread = (pfnZwSetInformationThread)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("ZwSetInformationThread"));
ZwSetInformationThread(
GetCurrentThread(),
uThreadHideFromDebugger,
0,
0);
return 0;
}
NtCreateThreadEx
通過CreateThread( )的時候利用THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER標志,則在創建該線程時,它將對調試器隱藏。
//通過CreateThread( )的時候利用THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER標志,則在創建該線程時,它將對調試器隱藏。
//#include <Windows.h>
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx) (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ PVOID ObjectAttributes, //POBJECT_ATTRIBUTES
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);
void ThreadProc();
int main(int argc, char* argv[])
{
HANDLE hThread;
pfnNtCreateThreadEx NtCreateThreadEx;
NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtCreateThreadEx"));
NtCreateThreadEx(
&hThread,
0x1FFFFF,
NULL,
GetCurrentProcess(),
(LPTHREAD_START_ROUTINE)ThreadProc,
NULL,
0x00000004, //直接執行並且對調試器隱藏THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
NULL,
NULL,
NULL,
NULL
);
WaitForSingleObject(hThread,INFINITE);
int n = GetLastError();
return 0;
}
//線程回調函數
void ThreadProc()
{
MessageBox(NULL, TEXT("我是一個新線程!"), NULL, MB_OK);
}
參考資料: 看雪學院《加密解密》
張銀奎《軟件調試》
https://www.apriorit.com/dev-blog/367-anti-reverse-engineering-protection-techniques-to-use-before-releasing-software