Windows下反(反)調試技術匯總


反調試技術,惡意代碼用它識別是否被調試,或者讓調試器失效。惡意代碼編寫者意識到分析人員經常使用調試器來觀察惡意代碼的操作,因此他們使用反調試技術盡可能地延長惡意代碼的分析時間。為了阻止調試器的分析,當惡意代碼意識到自己被調試時,它們可能改變正常的執行路徑或者修改自身程序讓自己崩潰,從而增加調試時間和復雜度。很多種反調試技術可以達到反調試效果。這里介紹當前常用的幾種反調試技術,同時也會介紹一些逃避反調試的技巧。
一.探測Windows調試器
惡意代碼會使用多種技術探測調試器調試它的痕跡,其中包括使用Windows API、手動檢測調試器人工痕跡的內存結構,查詢調試器遺留在系統中的痕跡等。調試器探測是惡意代碼最常用的反調試技術。
1.使用Windows API
使用Windows API函數檢測調試器是否存在是最簡單的反調試技術。Windows操作系統中提供了這樣一些API,應用程序可以通過調用這些API,來檢測自己是否正在被調試。這些API中有些是專門用來檢測調試器的存在的,而另外一些API是出於其他目的而設計的,但也可以被改造用來探測調試器的存在。其中很小部分API函數沒有在微軟官方文檔顯示。通常,防止惡意代碼使用API進行反調試的最簡單的辦法是在惡意代碼運行期間修改惡意代碼,使其不能調用探測調試器的API函數,或者修改這些API函數的返回值,確保惡意代碼執行合適的路徑。與這些方法相比,較復雜的做法是掛鈎這些函數,如使用rootkit技術。
1.1IsDebuggerPresent
IsDebuggerPresent查詢進程環境塊(PEB)中的IsDebugged標志。如果進程沒有運行在調試器環境中,函數返回0;如果調試附加了進程,函數返回一個非零值。

BOOL CheckDebug()
{
return IsDebuggerPresent();
}
1.2CheckRemoteDebuggerPresent
CheckRemoteDebuggerPresent同IsDebuggerPresent幾乎一致。它不僅可以探測系統其他進程是否被調試,通過傳遞自身進程句柄還可以探測自身是否被調試。

BOOL CheckDebug()
{
BOOL ret;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
return ret;
}
1.3NtQueryInformationProcess
這個函數是Ntdll.dll中一個原生態API,它用來提取一個給定進程的信息。它的第一個參數是進程句柄,第二個參數告訴我們它需要提取進程信息的類型。為第二個參數指定特定值並調用該函數,相關信息就會設置到第三個參數。第二個參數是一個枚舉類型,其中與反調試有關的成員有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如將該參數置為ProcessDebugPort,如果進程正在被調試,則返回調試端口,否則返回0。

BOOL CheckDebug()
{
int debugPort = 0;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL);
return debugPort != 0;
}

BOOL CheckDebug()
{
HANDLE hdebugObject = NULL;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL);
return hdebugObject != NULL;
}

BOOL CheckDebug()
{
BOOL bdebugFlag = TRUE;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL);
return bdebugFlag != TRUE;
}
1.4GetLastError
編寫應用程序時,經常需要涉及到錯誤處理問題。許多函數調用只用TRUE和FALSE來表明函數的運行結果。一旦出現錯誤,MSDN中往往會指出請用GetLastError()函數來獲得錯誤原因。惡意代碼可以使用異常來破壞或者探測調試器。調試器捕獲異常后,並不會立即將處理權返回被調試進程處理,大多數利用異常的反調試技術往往據此來檢測調試器。多數調試器默認的設置是捕獲異常后不將異常傳遞給應用程序。如果調試器不能將異常結果正確返回到被調試進程,那么這種異常失效可以被進程內部的異常處理機制探測。
對於OutputDebugString函數,它的作用是在調試器中顯示一個字符串,同時它也可以用來探測調試器的存在。使用SetLastError函數,將當前的錯誤碼設置為一個任意值。如果進程沒有被調試器附加,調用OutputDebugString函數會失敗,錯誤碼會重新設置,因此GetLastError獲取的錯誤碼應該不是我們設置的任意值。但如果進程被調試器附加,調用OutputDebugString函數會成功,這時GetLastError獲取的錯誤碼應該沒改變。

BOOL CheckDebug()
{
DWORD errorValue = 12345;
SetLastError(errorValue);
OutputDebugString("Test for debugger!");
if (GetLastError() == errorValue)
{
return TRUE;
}
else
{
return FALSE;
}
}
對於DeleteFiber函數,如果給它傳遞一個無效的參數的話會拋出ERROR_INVALID_PARAMETER異常。如果進程正在被調試的話,異常會被調試器捕獲。所以,同樣可以通過驗證LastError值來檢測調試器的存在。如代碼所示,0x57就是指ERROR_INVALID_PARAMETER。

BOOL CheckDebug()
{
char fib[1024] = {0};
DeleteFiber(fib);
return (GetLastError() != 0x57);
}
同樣還可以使用CloseHandle、CloseWindow產生異常,使得錯誤碼改變。

BOOL CheckDebug()
{
DWORD ret = CloseHandle((HANDLE)0x1234);
if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE)
{
return TRUE;
}
else
{
return FALSE;
}
}

BOOL CheckDebug()
{
DWORD ret = CloseWindow((HWND)0x1234);
if (ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE)
{
return TRUE;
}
else
{
return FALSE;
}
}
1.5ZwSetInformationThread
ZwSetInformationThread擁有兩個參數,第一個參數用來接收當前線程的句柄,第二個參數表示線程信息類型,若其值設置為ThreadHideFromDebugger(0x11),使用語句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);調用該函數后,調試進程就會被分離出來。該函數不會對正常運行的程序產生任何影響,但若運行的是調試器程序,因為該函數隱藏了當前線程,調試器無法再收到該線程的調試事件,最終停止調試。還有一個函數DebugActiveProcessStop用來分離調試器和被調試進程,從而停止調試。兩個API容易混淆,需要牢記它們的區別。
2.手動檢測數據結構
雖然使用Windows API是探測調試器存在的最簡單辦法,但手動檢查數據結構是惡意代碼編寫者最常使用的辦法。這是因為很多時候通過Windows API實現的反調試技術無效,例如這些API函數被rootkit掛鈎,並返回錯誤信息。因此,惡意代碼編寫者經常手動執行與這些API功能相同的操作。在手動檢測中,PEB結構中的一些標志暴露了調試器存在的信息。這里,我們關注檢測調試器存在常用的一些標志。
2.1檢測BeingDebugged屬性
Windows操作系統維護着每個正在運行的進程的PEB結構,它包含與這個進程相關的所有用戶態參數。這些參數包括進程環境數據,環境數據包括環境變量、加載的模塊列表、內存地址,以及調試器狀態。

進程運行時,位置fs:[30h]指向PEB的基地址。為了實現反調試技術,惡意代碼通過這個位置檢查BeingDebugged標志,這個標志標識進程是否正在被調試。

BOOL CheckDebug()
{
int result = 0;
__asm
{
mov eax, fs:[30h]
mov al, BYTE PTR [eax + 2]
mov result, al
}
return result != 0;
}
這種檢查有多種形式,最終,條件跳轉決定代碼的路徑。避免這種問題最簡單的方法是在執行跳轉指令前,手動修改零標志,強制執行跳轉(或者不跳轉)。
可以或者手動修改BeingDebugged屬性值為0。在OllyDbg中安裝命令行插件,為了啟動該插件,用OllyDbg加載惡意代碼,選擇Plugins->Command Line->Command Line選項,在命令行窗口輸入下面的命令。

如圖所示,這條命令會將BeingDebugged屬性轉儲到轉儲面板窗口。右鍵單擊BeingDebugged屬性,選擇Binary->Fill With 00's,這時屬性被設置為0。
OllyDbg的一些插件可以幫助我們修改BeingDebugged標志。其中最流行的有HideDebugger、Hidedebug和PhantOm。以PhantOm為例,同樣將dll文件拷貝到OllyDbg的安裝目錄下就會自動安裝。選擇Plugins->PhantOm->Options選項,勾選hide from PEB即可。

2.2檢測ProcessHeap屬性
Reserved數組中一個未公開的位置叫作ProcessHeap,它被設置為加載器為進程分配的第一個堆的位置。ProcessHeap位於PEB結構的0x18處。第一個堆頭部有一個屬性字段,它告訴內核這個堆是否在調試器中創建。這些屬性叫作ForceFlags和Flags。在Windows XP系統中,ForceFlags屬性位於堆頭部偏移量0x10處;在Windows 7系統中,對於32位的應用程序來說ForceFlags屬性位於堆頭部偏移量0x44處。
BOOL CheckDebug()
{
int result = 0;
DWORD dwVersion = GetVersion();
DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
//for xp
if (dwWindowsMajorVersion == 5)
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 10h]
mov result, eax
}
}
else
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 44h]
mov result, eax
}
}
return result != 0;
}
同樣,惡意代碼也可以檢查Windows XP系統中偏移量0x0C處,或者Windows 7系統中偏移量0x40處的Flags屬性。這個屬性總與ForceFlags屬性大致相同,但通常情況下Flags與值2進行比較。

BOOL CheckDebug()
{
int result = 0;
DWORD dwVersion = GetVersion();
DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
//for xp
if (dwWindowsMajorVersion == 5)
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 0ch]
mov result, eax
}
}
else
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 40h]
mov result, eax
}
}
return result != 2;
}
避免這種問題方法和前面的差不多。如果用OllyDbg的命令行插件修改,輸入的命令為dump ds:[fs:[30]+0x18]+0x10。如果用PhantOm插件,它會禁用調試堆創建功能而不需要手動設置。
2.3檢測NTGlobalFlag
由於調試器中啟動進程與正常模式下啟動進程有些不同,所以它們創建內存堆的方式也不同。系統使用PEB結構偏移量0x68處的一個未公開位置,來決定如何創建堆結構。如果這個位置的值為0x70,我們就知道進程正運行在調試器中。
BOOL CheckDebug()
{
int result = 0;
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 68h]
and eax, 0x70
mov result, eax
}
return result != 0;
}
操作系統創建堆時,值0x70是下列標志的一個組合。如果進程從調試器啟動,那么進程的這些標志將被設置。
(FLG_HEAP_ENABLE_TAIL_CHECK|FLG_HEAP_ENABLE_FREE_CHECK|FLG_HEAP_VALIDATE_PARAMETERS)
避免這種問題方法和前面的差不多。如果用OllyDbg的命令行插件修改,輸入的命令為dump fs:[30]+0x68。如果用PhantOm插件,它會逃避使用NTGlobalFlag的反調試技術而不需要手動設置。
3.系統痕跡檢測
通常,我們使用調試工具來分析惡意代碼,但這些工具會在系統中駐留一些痕跡。惡意代碼通過搜索這種系統痕跡,來確定你是否試圖分析它。
3.1查找調試器引用的注冊表項
下面是調試器在注冊表中的一個常用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系統)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系統)
該注冊表項指定當應用程序發生錯誤時,觸發哪一個調試器。默認情況下,它被設置為Dr.Watson。如果該這冊表的鍵值被修改為OllyDbg,則惡意代碼就可能確定它正在被調試。
BOOL CheckDebug()
{
BOOL is_64;
IsWow64Process(GetCurrentProcess(), &is_64);
HKEY hkey = NULL;
char key[] = "Debugger";
char reg_dir_32bit[] = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug";
char reg_dir_64bit[] = "SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug";
DWORD ret = 0;
if (is_64)
{
ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_64bit, &hkey);
}
else
{
ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_32bit, &hkey);
}
if (ret != ERROR_SUCCESS)
{
return FALSE;
}
char tmp[256];
DWORD len = 256;
DWORD type;
ret = RegQueryValueExA(hkey, key, NULL, &type, (LPBYTE)tmp, &len);
if (strstr(tmp, "OllyIce")!=NULL || strstr(tmp, "OllyDBG")!=NULL || strstr(tmp, "WinDbg")!=NULL || strstr(tmp, "x64dbg")!=NULL || strstr(tmp, "Immunity")!=NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
3.2查找窗體信息
FindWindow函數檢索處理頂級窗口的類名和窗口名稱匹配指定的字符串。

BOOL CheckDebug()
{
if (FindWindowA("OLLYDBG", NULL)!=NULL || FindWindowA("WinDbgFrameClass", NULL)!=NULL || FindWindowA("QWidget", NULL)!=NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
EnumWindows函數枚舉所有屏幕上的頂層窗口,並將窗口句柄傳送給應用程序定義的回調函數。

BOOL CALLBACK EnumWndProc(HWND hwnd, LPARAM lParam)
{
char cur_window[1024];
GetWindowTextA(hwnd, cur_window, 1023);
if (strstr(cur_window, "WinDbg")!=NULL || strstr(cur_window, "x64_dbg")!=NULL || strstr(cur_window, "OllyICE")!=NULL || strstr(cur_window, "OllyDBG")!=NULL || strstr(cur_window, "Immunity")!=NULL)
{
((BOOL)lParam) = TRUE;
}
return TRUE;
}

BOOL CheckDebug()
{
BOOL ret = FALSE;
EnumWindows(EnumWndProc, (LPARAM)&ret);
return ret;
}
GetForegroundWindow獲取一個前台窗口的句柄。

BOOL CheckDebug()
{
char fore_window[1024];
GetWindowTextA(GetForegroundWindow(), fore_window, 1023);
if (strstr(fore_window, "WinDbg")!=NULL || strstr(fore_window, "x64_dbg")!=NULL || strstr(fore_window, "OllyICE")!=NULL || strstr(fore_window, "OllyDBG")!=NULL || strstr(fore_window, "Immunity")!=NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
為了防范這種技術,在OllyDbg的PhantOm插件中勾選hide OllyDbg windows。
3.3查找進程信息

BOOL CheckDebug()
{
DWORD ID;
DWORD ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while(bMore)
{
if (stricmp(pe32.szExeFile, "OllyDBG.EXE")0 || stricmp(pe32.szExeFile, "OllyICE.exe")0 || stricmp(pe32.szExeFile, "x64_dbg.exe")0 || stricmp(pe32.szExeFile, "windbg.exe")0 || stricmp(pe32.szExeFile, "ImmunityDebugger.exe")==0)
{
return TRUE;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
return FALSE;
}
二.識別調試器行為
在逆向工程中,為了幫助惡意代碼分析人員進行分析,可以使用調試器設置一個斷點,或是單步執行一個進程。然而,在調試器中執行這些操作時,它們會修改進程中的代碼。因此,惡意代碼常使用幾種反調試技術探測軟件/硬件斷點、完整性校驗、時鍾檢測等幾種類型的調試器行為。直接運行惡意代碼與在調試器中運行惡意代碼也會在一些細節上不同,如父進程信息、STARTUPINFO信息、SeDebugPrivilege權限等。
1.軟件斷點檢查
調試器設置斷點的基本機制是用軟件中斷指令INT 3臨時替換運行程序中的一條指令,然后當程序運行到這條指令時,調用調試異常處理例程。INT 3指令的機器碼是0xCC,因此無論何時,使用調試器設置一個斷點,它都會插入一個0xCC來修改代碼。惡意代碼常用的一種反調試技術是在它的代碼中查找機器碼0xCC,來掃描調試器對它代碼的INT 3修改。repne scasb指令用於在一段數據緩沖區中搜索一個字節。EDI需指向緩沖區地址,AL則包含要找的字節,ECX設為緩沖區的長度。當ECX=0或找到該字節時,比較停止。
BOOL CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
BOOL Found = FALSE;
__asm
{
cld
mov edi,dwAddr
mov ecx,dwCodeSize
mov al,0CCH
repne scasb
jnz NotFound
mov Found,1
NotFound:
}
return Found;
}
2.硬件斷點檢查

在OllyDbg的寄存器窗口按下右鍵,點擊View debug registers可以看到DR0、DR1、DR2、DR3、DR6和DR7這幾個寄存器。DR0、Dr1、Dr2、Dr3用於設置硬件斷點,由於只有4個硬件斷點寄存器,所以同時最多只能設置4個硬件斷點。DR4、DR5由系統保留。  DR6、DR7用於記錄Dr0-Dr3中斷點的相關屬性。如果沒有硬件斷點,那么DR0、DR1、DR2、DR3這4個寄存器的值都為0。

BOOL CheckDebug()
{
CONTEXT context;
HANDLE hThread = GetCurrentThread();
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &context);
if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0)
{
return TRUE;
}
return FALSE;
}
3.執行代碼校驗和檢查
惡意代碼可以計算代碼段的校驗並實現與掃描中斷相同的目的。與掃描0xCC不同,這種檢查僅執行惡意代碼中機器碼CRC或者MD5校驗和檢查。
BOOL CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
DWORD checksum = 0;
__asm
{
cld
mov esi, dwAddr
mov ecx, dwCodeSize
xor eax, eax
checksum_loop :
movzx ebx, byte ptr[esi]
add eax, ebx
rol eax, 1
inc esi
loop checksum_loop
mov checksum, eax
}
if (checksum != 0x46ea24)
{
return FALSE;
}
else
{
return TRUE;
}
}
4.時鍾檢測
被調試時,進程的運行速度大大降低,例如,單步調試大幅降低惡意代碼的運行速度,所以時鍾檢測是惡意代碼探測調試器存在的最常用方式之一。有如下兩種用時鍾檢測來探測調試器存在的方法。
記錄一段操作前后的時間戳,然后比較這兩個時間戳,如果存在滯后,則可以認為存在調試器。
記錄觸發一個異常前后的時間戳。如果不調試進程,可以很快處理完異常,因為調試器處理異常的速度非常慢。默認情況下,調試器處理異常時需要人為干預,這導致大量延遲。雖然很多調試器允許我們忽略異常,將異常直接返回程序,但這樣操作仍然存在不小的延遲。
4.1使用rdtsc指令
較常用的時鍾檢測方法是利用rdtsc指令(操作碼0x0F31),它返回至系統重新啟動以來的時鍾數,並且將其作為一個64位的值存入EDX:EAX中。惡意代碼運行兩次rdtsc指令,然后比較兩次讀取之間的差值。
BOOL CheckDebug()
{
DWORD time1, time2;
__asm
{
rdtsc
mov time1, eax
rdtsc
mov time2, eax
}
if (time2 - time1 < 0xff)
{
return FALSE;
}
else
{
return TRUE;
}
}
4.2使用QueryPerformanceCounter和GetTickCount
同rdtsc指令一樣,這兩個Windows API函數也被用來執行一個反調試的時鍾檢測。使用這種方法的前提是處理器有高分辨率能力的計數器-寄存器,它能存儲處理器活躍的時鍾數。為了獲取比較的時間差,調用兩次QueryPerformanceCounter函數查詢這個計數器。若兩次調用之間花費的時間過於長,則可以認為正在使用調試器。GetTickCount函數返回最近系統重啟時間與當前時間的相差毫秒數(由於時鍾計數器的大小原因,計數器每49.7天就被重置一次)。
BOOL CheckDebug()
{
DWORD time1 = GetTickCount();
__asm
{
mov ecx,10
mov edx,6
mov ecx,10
}
DWORD time2 = GetTickCount();
if (time2-time1 > 0x1A)
{
return TRUE;
}
else
{
return FALSE;
}
}
5.判斷父進程是否是explorer.exe 
一般雙擊運行的進程的父進程都是explorer.exe,但是如果進程被調試父進程則是調試器進程。也就是說如果父進程不是explorer.exe則可以認為程序正在被調試。

BOOL CheckDebug()
{
LONG status;
DWORD dwParentPID = 0;
HANDLE hProcess;
PROCESS_BASIC_INFORMATION pbi;
int pid = getpid();
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if(!hProcess)
return -1;
PNTQUERYINFORMATIONPROCESS NtQueryInformationProcess = (PNTQUERYINFORMATIONPROCESS)GetProcAddress(GetModuleHandleA("ntdll"),"NtQueryInformationProcess");
status = NtQueryInformationProcess(hProcess,SystemBasicInformation,(PVOID)&pbi,sizeof(PROCESS_BASIC_INFORMATION),NULL);
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while(bMore)
{
if (pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID)
{
if (stricmp(pe32.szExeFile, "explorer.exe")==0)
{
CloseHandle(hProcessSnap);
return FALSE;
}
else
{
CloseHandle(hProcessSnap);
return TRUE;
}
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
}
6.判斷STARTUPINFO信息
explorer.exe創建進程的時候會把STARTUPINFO結構中的值設為0,而非explorer.exe創建進程的時候會忽略這個結構中的值,也就是結構中的值不為0。所以可以利用STARTUPINFO來判斷程序是否在被調試。
BOOL CheckDebug()
{
STARTUPINFO si;
GetStartupInfo(&si);
if (si.dwX!=0 || si.dwY!=0 || si.dwFillAttribute!=0 || si.dwXSize!=0 || si.dwYSize!=0 || si.dwXCountChars!=0 || si.dwYCountChars!=0)
{
return TRUE;
}
else
{
return FALSE;
}
}
7.判斷是否具有SeDebugPrivilege權限
默認情況下進程是沒有SeDebugPrivilege權限的,但是當進程通過調試器啟動時,由於調試器本身啟動了SeDebugPrivilege權限,當調試進程被加載時SeDebugPrivilege也就被繼承了。所以我們可以檢測進程的SeDebugPrivilege權限來間接判斷是否存在調試器,而對SeDebugPrivilege權限的判斷可以用能否打開csrss.exe進程來判斷。

BOOL CheckDebug()
{
DWORD ID;
DWORD ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while(bMore)
{
if (strcmp(pe32.szExeFile, "csrss.exe")==0)
{
ID = pe32.th32ProcessID;
break;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
if (OpenProcess(PROCESS_QUERY_INFORMATION, NULL, ID) != NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
三.干擾調試器的功能
惡意代碼可以用一些技術來干擾調試器的正常運行。例如線程本地存儲(TLS)回調、插入中斷、異常等。這些技術當且僅當程序處於調試器控制之下時才試圖擾亂程序的運行。
1.使用TLS回調
Thread Local Storage(TLS),即線程本地存儲,是Windows為解決一個進程中多個線程同時訪問全局變量而提供的機制。TLS可以簡單地由操作系統代為完成整個互斥過程,也可以由用戶自己編寫控制信號量的函數。當進程中的線程訪問預先制定的內存空間時,操作系統會調用系統默認的或用戶自定義的信號量函數,保證數據的完整性與正確性。下面是一個簡單的TLS回調的例子,TLS_CALLBACK1函數在main函數執行前調用IsDebuggerPresent函數檢查它是否正在被調試。

include "stdafx.h"

include <stdio.h>

include <windows.h>

void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD dwReason, PVOID Reserved);

ifdef _M_IX86

pragma comment (linker, "/INCLUDE:__tls_used")

pragma comment (linker, "/INCLUDE:__tls_callback")

else

pragma comment (linker, "/INCLUDE:_tls_used")

pragma comment (linker, "/INCLUDE:_tls_callback")

endif

EXTERN_C

ifdef _M_X64

pragma const_seg (".CRT$XLB")

const

else

pragma data_seg (".CRT$XLB")

endif

PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK1,0};

pragma data_seg ()

pragma const_seg ()

include

void NTAPI __stdcall TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (IsDebuggerPresent())
{
printf("TLS_CALLBACK: Debugger Detected!\n");
}
else
{
printf("TLS_CALLBACK: No Debugger Present!\n");
}
}

int main(int argc, char* argv[])
{
printf("233\n");
return 0;
}

要在程序中使用TLS,必須為TLS數據單獨建一個數據段,用相關數據填充此段,並通知鏈接器為TLS數據在PE文件頭中添加數據。_tls_callback[]數組中保存了所有的TLS回調函數指針。數組必須以NULL指針結束,且數組中的每一個回調函數在程序初始化時都會被調用,程序員可按需要添加。但程序員不應當假設操作系統已何種順序調用回調函數。如此則要求在TLS回調函數中進行反調試操作需要一定的獨立性。
正常運行這個程序會打印下面的內容。
TLS_CALLBACK: No Debugger Present!
233
如果把在OllyDbg中運行,在OllyDbg暫停之前會打印下面的內容。
TLS_CALLBACK: Debugger Detected!
使用PEview查看.tls段,可以發現TLS回調函數。通常情況下,正常程序不使用.tls段,如果在可執行程序中看到.tls段,應該立即懷疑它使用了反調試技術。

在OllyDbg中選擇Options->Debugging Options->Events,然后設置System break-point作為第一個暫停的位置,這樣就可以讓OllyDbg在TLS回調執行前暫停。

在IDA Pro中按Ctrl+E快捷鍵看到二進制的入口點,該組合鍵的作用是顯示應用程序所有的入口點,其中包括TLS回調。雙擊函數名可以瀏覽回調函數。

由於TLS回調已廣為人知,因此同過去相比,惡意代碼使用它的次數已經明顯減少。為數不多的合法程序使用TLS回調,所以可執行程序中的.tls段特別突出。
2.利用中斷
因為調試器使用INT 3來設置軟件斷點,所以一種反調試技術就是在合法代碼段中插入0xCC(INT 3)欺騙調試器,使其認為這些0xCC機器碼是自己設置的斷點。

BOOL CheckDebug()
{
__try
{
__asm int 3
}
__except(1)
{
return FALSE;
}
return TRUE;
}
除了使用_try和_except以外還可以直接使用匯編代碼安裝SEH。在下面的代碼中如果進程沒有處於調試中,則正常終止;如果進程處於調試中,則跳轉到非法地址0xFFFFFFFF處,無法繼續調試。

include "stdio.h"

include "windows.h"

include "tchar.h"

void AD_BreakPoint()
{
printf("SEH : BreakPoint\n");

__asm {
    // install SEH
    push handler
    push DWORD ptr fs:[0]
    mov DWORD ptr fs:[0], esp
    
    // generating exception
    int 3

    // 1) debugging
    //    go to terminating code
    mov eax, 0xFFFFFFFF
    jmp eax                 // process terminating!!!

    // 2) not debugging
    //    go to normal code

handler:
mov eax, dword ptr ss:[esp+0xc]
mov ebx, normal_code
mov dword ptr ds:[eax+0xb8], ebx
xor eax, eax
retn

normal_code:
// remove SEH
pop dword ptr fs:[0]
add esp, 4
}

printf("  => Not debugging...\n\n");

}

int _tmain(int argc, TCHAR* argv[])
{
AD_BreakPoint();

return 0;

}
雙字節操作碼0xCD03也可以產生INT 3中斷,這是惡意代碼干擾WinDbg調試器的有效方法。在調試器外,0xCD03指令產生一個STATUS_BREAKPOINT異常。然而在WinDbg調試器內,由於斷點通常是單字節機器碼0xCC,因此WinDbg會捕獲這個斷點然后將EIP加1字節。這可能導致程序在被正常運行的WinDbg調試時,執行不同的指令集(OllyDbg可以避免雙字節INT 3的攻擊)。

BOOL CheckDebug()
{
__try
{
__asm
{
__emit 0xCD
__emit 0x03
}
}
__except(1)
{
return FALSE;
}
return TRUE;
}
INT 2D原為內核模式中用來觸發斷點異常的指令,也可以在用戶模式下觸發異常。但程序調試運行時不會觸發異常,只是忽略。INT 2D指令在ollydbg中有兩個有趣的特性。在調試模式中執行INT 2D指令,下一條指令的第一個字節將被忽略。使用StepInto(F7)或者StepOver(F8)命令跟蹤INT 2D指令,程序不會停在下一條指令開始的地方,而是一直運行,就像RUN(F9)一樣。在下面的代碼中,程序調試運行時,執行INT 2D之后不會運行SEH,而是跳過NOP,把bDebugging標志設置為1,跳轉到normal_code;程序正常運行時,執行INT 2D之后觸發SEH,在異常處理器中設置EIP並把bDebugging標志設置為0。

BOOL CheckDebug()
{
BOOL bDebugging = FALSE;

__asm {
    // install SEH
    push handler
    push DWORD ptr fs:[0]
    mov DWORD ptr fs:[0], esp
    
    int 0x2d

    nop
    mov bDebugging, 1
    jmp normal_code

handler:
mov eax, dword ptr ss:[esp+0xc]
mov dword ptr ds:[eax+0xb8], offset normal_code
mov bDebugging, 0
xor eax, eax
retn

normal_code:
// remove SEH
pop dword ptr fs:[0]
add esp, 4
}

printf("Trap Flag (INT 2D)\n");
if( bDebugging )  return 1;
else              return 0;

}
片內仿真器(ICE)斷點指令ICEBP(操作碼0xF1)是Intel未公開的指令之一。由於使用ICE難以在任意位置設置斷點,因此ICEBP指令被設計用來降低使用ICE設置斷點的難度。運行ICEBP指令將會產生一個單步異常,如果通過單步調試跟蹤程序,調試器會認為這是單步調試產生的異常,從而不執行先前設置的異常處理例程。利用這一點,惡意代碼使用異常處理例程作為它的正常執行流程。為了防止這種反調試技術,執行ICEBP指令時不要使用單步。

BOOL CheckDebug()
{
__try
{
__asm __emit 0xF1
}
__except(1)
{
return FALSE;
}
return TRUE;
}
3.設置陷阱標志位
EFLAGS寄存器的第八個比特位是陷阱標志位。如果設置了,就會產生一個單步異常。

BOOL CheckDebug()
{
__try
{
__asm
{
pushfd
or word ptr[esp], 0x100
popfd
nop
}
}
__except(1)
{
return FALSE;
}
return TRUE;
}
4.使用異常
前面已經討論了各種使用異常機制的反調試手段。
4.1RaiseException
RaiseException函數產生的若干不同類型的異常可以被調試器捕獲。

BOOL TestExceptionCode(DWORD dwCode)
{
__try
{
RaiseException(dwCode, 0, 0, 0);
}
__except(1)
{
return FALSE;
}
return TRUE;
}

BOOL CheckDebug()
{
return TestExceptionCode(DBG_RIPEXCEPTION);
}
4.2SetUnhandledExceptionFilter
進程中發生異常時若SEH未處理或注冊的SEH不存在,會調用UnhandledExceptionFilter,它會運行系統最后的異常處理器。UnhandledExceptionFilter內部調用了前面提到過的NtQueryInformationProcess以判斷是否正在調試進程。若進程正常運行,則運行最后的異常處理器;若進程處於調試,則將異常派送給調試器。SetUnhandledExceptionFilter函數可以修改系統最后的異常處理器。下面的代碼先觸發異常,然后在新注冊的最后的異常處理器內部判斷進程正常運行還是調試運行。進程正常運行時pExcept->ContextRecord->Eip+=4;將發生異常的代碼地址加4使得其能夠繼續運行;進程調試運行時產生無效的內存訪問異常,從而無法繼續調試。

include "stdio.h"

include "windows.h"

include "tchar.h"

LPVOID g_pOrgFilter = 0;

LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS pExcept)
{
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)g_pOrgFilter);

// 8900    MOV DWORD PTR DS:[EAX], EAX
// FFE0    JMP EAX
pExcept->ContextRecord->Eip += 4;

return EXCEPTION_CONTINUE_EXECUTION;

}

void AD_SetUnhandledExceptionFilter()
{
printf("SEH : SetUnhandledExceptionFilter()\n");

g_pOrgFilter = (LPVOID)SetUnhandledExceptionFilter(
                            (LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter);

__asm {
    xor eax, eax;
    mov dword ptr [eax], eax
    jmp eax                     
}

printf("  => Not debugging...\n\n");

}

int _tmain(int argc, TCHAR* argv[])
{
AD_SetUnhandledExceptionFilter();

return 0;

}
在OllyDbg中,選擇Options->Debugging Options->Exceptions來設置把異常傳遞給應用程序。

四.調試器漏洞
與所有軟件一樣,調試器也存在漏洞,有時惡意代碼編寫者為了防止被調試,會攻擊這些漏洞。這里我們展示幾種OllyDbg調試器處理PE格式文件時的常見漏洞。
1.PE頭漏洞
OllyDbg非常嚴格地遵循了微軟對PE文件頭部的規定。在PE文件的頭部,通常存在一個叫作IMAGE_OPTIONAL_HEADER的結構。

需要特別注意這個結構中的最后幾個元素。NumberOfRvaAndSizes屬性標識后面DataDirectory數組中的元素個數。DataDirectory數組表示在這個可執行文件中的什么地方可找到其他導入可執行模塊的位置,它位於可選頭部結構的末尾,是一個比IMAGE_DATA_DIRECTORY略大一些的數組。數組中每個結構目錄都指明了目錄的相對虛擬地址和大小。DataDirectory數組的大小被設置為IMAGE_NUMBEROF_DIRECTORY_ENTRIES,它等於0x10。因為DataDirectory數組不足以容納超過0x10個目錄項,所以當NumberOfRvaAndSizes大於0x10時,Windows加載器將會忽略NumberOfRvaAndSizes。OllyDbg遵循了這個標准,並且無論NumberOfRvaAndSizes是什么值,OllyDbg都使用它。因此,設置NumberOfRvaAndSizes為一個超過0x10的值,會導致在程序退出前,OllyDbg對用戶彈出一個窗口。如圖所示,使用LordPE打開可執行文件,修改RVA數及大小並保存,再用OllyDbg打開,會提示錯誤Bad or unknown format of 32-bit executable file。

另一種PE頭的欺騙與節頭部有關。文件內容中包含的節包括代碼節、數據節、資源節,以及一些其他信息節。每個節都擁有一個IMAGE_SECTION_HEADER結構的頭部。

VirtualSize和SizeOfRawData是其中兩個比較重要的屬性。根據微軟對PE的規定,VirtualSize應該包含載入到內存的節大小,SizeOfRawData應該包含節在硬盤中的大小。Windows加載器使用VirtualSize和SizeOfRawData中的最小值將節數據映射到內存。如果SizeOfRawData大於VirtualSize,則僅將VirtualSize大小的數據復制入內存,忽略其余數據。因為OllyDbg僅使用SizeOfRawData,所以設置SizeOfRawData為一個類似0x77777777的大數值時,會導致OllyDbg崩潰。如圖所示,使用LordPE打開可執行文件,點擊區段,在區段表上右擊,點擊編輯區段,修改物理大小並保存,再用OllyDbg打開,會提示同樣的錯誤。

對抗這種反調試技術的最簡單方法是用類似的編輯器手動修改PE頭部。OllyDbg2.0和WinDbg不存在這種漏洞。
2.OutputDebugString漏洞
惡意代碼常嘗試利用OllyDbg1.1的格式化字符串漏洞,為OutputDebugString函數提供一個%s字符串的參數,讓OllyDbg崩潰。因此,需要注意程序中可疑的OutputDebugString調用,例如OutputDebugString("%s%s%s%s%s%s%s%s%s")。如果執行了這個調用,OllyDbg將會崩潰。

總結

最后讓我們總結一下提到的內容。騰訊2016游戲安全技術競賽有一道題,大概意思就是給一個exe,要求編寫一個Tencent2016D.dll,並導出多個接口函數CheckDebugX。X為1-100之間的數字,比如CheckDebug1,CheckDebug8,...,CheckDebug98。函數功能是檢測自己是否處於被調試狀態,是返回TRUE,否則返回FALSE。函數的原型都是typedef BOOL (WINAPI* Type_CheckDebug)();。編譯好dll之后,放在Tencent2016D.exe的同目錄,運行Tencent2016D.exe,點擊檢測按鈕,正常運行時,函數接口輸出為0,調試運行或者被附加運行時,接口輸出1。我們把提到的知識綜合一下完成這道題目。
解題的參考代碼和題目相關信息:https://github.com/houjingyi233/test-debug/
參考資料:《惡意代碼分析實戰》第16章反調試技術、《逆向工程核心原理》第51章靜態反調試技術&第52章動態反調試技術、freebuf技術分享:利用異常實現反調試、天樞戰隊官方博客、吾愛破解論壇、《windows anti-debugger reference》
————————————————
版權聲明:本文為CSDN博主「houjingyi233」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_32400847/java/article/details/52798050


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM