大概是在一年半之前我在本身CSDN博客上寫了詳解反虛擬機技術和詳解反調試技術,沒想到看的人還不少,有人甚至給我發私信發郵件,在百度和谷歌搜索「反調試」和「反虛擬機」,第一條結果就是個人文章。我決定在看雪也分享一下。固然我只是作了個整理收集的工做,沒有一條技術和一行代碼是我原創的,參考連接會附在最後。html
反調試技術,惡意代碼用它識別是否被調試,或者讓調試器失效。惡意代碼編寫者意識到分析人員常用調試器來觀察惡意代碼的操做,所以他們使用反調試技術儘量地延長惡意代碼的分析時間。爲了阻止調試器的分析,當惡意代碼意識到本身被調試時,它們可能改變正常的執行路徑或者修改自身程序讓本身崩潰,從而增長調試時間和複雜度。不少種反調試技術能夠達到反調試效果。這裏介紹當前經常使用的幾種反調試技術,同時也會介紹一些逃避反調試的技巧。ios
惡意代碼會使用多種技術探測調試器調試它的痕跡,其中包括使用Windows API、手動檢測調試器人工痕跡的內存結構和查詢調試器遺留在系統中的痕跡等。調試器探測是惡意代碼最經常使用的反調試技術。git
使用Windows API函數檢測調試器是否存在是最簡單的反調試技術。Windows操做系統中提供了這樣一些API,應用程序能夠經過調用這些API,來檢測本身是否正在被調試。這些API中有些是專門用來檢測調試器的存在的,而另一些API是出於其餘目的而設計的,但也能夠被改造用來探測調試器的存在。其中很小部分API函數沒有在微軟官方文檔顯示。一般,防止惡意代碼使用API進行反調試的最簡單的辦法是在惡意代碼運行期間修改惡意代碼,使其不能調用探測調試器的API函數,或者修改這些API函數的返回值,確保惡意代碼執行合適的路徑。與這些方法相比,較複雜的作法是掛鉤這些函數,如使用rootkit技術。程序員
IsDebuggerPresent查詢進程環境塊(PEB)中的IsDebugged標誌。若是進程沒有運行在調試器環境中,函數返回0;若是調試附加了進程,函數返回一個非零值。github
1
2
3
4
|
BOOL
CheckDebug()
{
return
IsDebuggerPresent();
}
|
CheckRemoteDebuggerPresent同IsDebuggerPresent幾乎一致。它不只能夠探測系統其餘進程是否被調試,經過傳遞自身進程句柄還能夠探測自身是否被調試。windows
1
2
3
4
5
6
|
BOOL
CheckDebug()
{
BOOL
ret;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
return
ret;
}
|
這個函數是Ntdll.dll中一個API,它用來提取一個給定進程的信息。它的第一個參數是進程句柄,第二個參數告訴咱們它須要提取進程信息的類型。爲第二個參數指定特定值並調用該函數,相關信息就會設置到第三個參數。第二個參數是一個枚舉類型,其中與反調試有關的成員有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如將該參數置爲ProcessDebugPort,若是進程正在被調試,則返回調試端口,不然返回0。數組
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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;
}
|
編寫應用程序時,常常須要涉及到錯誤處理問題。許多函數調用只用TRUE和FALSE來代表函數的運行結果。一旦出現錯誤,MSDN中每每會指出請用GetLastError()函數來得到錯誤緣由。惡意代碼可使用異常來破壞或者探測調試器。調試器捕獲異常後,並不會當即將處理權返回被調試進程處理,大多數利用異常的反調試技術每每據此來檢測調試器。多數調試器默認的設置是捕獲異常後不將異常傳遞給應用程序。若是調試器不能將異常結果正確返回到被調試進程,那麼這種異常失效能夠被進程內部的異常處理機制探測。
對於OutputDebugString函數,它的做用是在調試器中顯示一個字符串,同時它也能夠用來探測調試器的存在。使用SetLastError函數,將當前的錯誤碼設置爲一個任意值。若是進程沒有被調試器附加,調用OutputDebugString函數會失敗,錯誤碼會從新設置,所以GetLastError獲取的錯誤碼應該不是咱們設置的任意值。但若是進程被調試器附加,調用OutputDebugString函數會成功,這時GetLastError獲取的錯誤碼應該沒改變。安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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。數據結構
1
2
3
4
5
6
|
BOOL
CheckDebug()
{
char
fib[1024] = {0};
DeleteFiber(fib);
return
(GetLastError() != 0x57);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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;
}
}
|
ZwSetInformationThread擁有兩個參數,第一個參數用來接收當前線程的句柄,第二個參數表示線程信息類型,若其值設置爲ThreadHideFromDebugger(0x11),使用語句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);調用該函數後,調試進程就會被分離出來。該函數不會對正常運行的程序產生任何影響,但若運行的是調試器程序,由於該函數隱藏了當前線程,調試器沒法再收到該線程的調試事件,最終中止調試。還有一個函數DebugActiveProcessStop用來分離調試器和被調試進程,從而中止調試。兩個API容易混淆,須要牢記它們的區別。框架
雖然使用Windows API是探測調試器存在的最簡單辦法,但手動檢查數據結構是惡意代碼編寫者最常使用的辦法。這是由於不少時候經過Windows API實現的反調試技術無效,例如這些API函數被rootkit掛鉤,並返回錯誤信息。所以,惡意代碼編寫者常常手動執行與這些API功能相同的操做。在手動檢測中,PEB結構中的一些標誌暴露了調試器存在的信息。這裏,咱們關注檢測調試器存在經常使用的一些標誌。
Windows操做系統維護着每一個正在運行的進程的PEB結構,它包含與這個進程相關的全部用戶態參數。這些參數包括進程環境數據,環境數據包括環境變量、加載的模塊列表、內存地址,以及調試器狀態。
進程運行時,位置fs:[30h]指向PEB的基地址。爲了實現反調試技術,惡意代碼經過這個位置檢查BeingDebugged標誌,這個標誌標識進程是否正在被調試。
1
2
3
4
5
6
7
8
9
10
11
|
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便可。
Reserved數組中一個未公開的位置叫做ProcessHeap,它被設置爲加載器爲進程分配的第一個堆的位置。ProcessHeap位於PEB結構的0x18處。第一個堆頭部有一個屬性字段,它告訴內核這個堆是否在調試器中建立。這些屬性叫做ForceFlags和Flags。在Windows XP系統中,ForceFlags屬性位於堆頭部偏移量0x10處;在Windows 7系統中,對於32位的應用程序來講ForceFlags屬性位於堆頭部偏移量0x44處。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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進行比較。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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插件,它會禁用調試堆建立功能而不須要手動設置。
因爲調試器中啓動進程與正常模式下啓動進程有些不一樣,因此它們建立內存堆的方式也不一樣。系統使用PEB結構偏移量0x68處的一個未公開位置,來決定如何建立堆結構。若是這個位置的值爲0x70,咱們就知道進程正運行在調試器中。
1
2
3
4
5
6
7
8
9
10
11
12
|
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的反調試技術而不須要手動設置。
一般,咱們使用調試工具來分析惡意代碼,但這些工具會在系統中駐留一些痕跡。惡意代碼經過搜索這種系統痕跡,來肯定你是否試圖分析它。
下面是調試器在註冊表中的一個經常使用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系統)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系統)
該註冊表項指定當應用程序發生錯誤時,觸發哪個調試器。默認狀況下,它被設置爲Dr.Watson。若是該這冊表的鍵值被修改成OllyDbg,則惡意代碼就可能肯定它正在被調試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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;
}
}
|
FindWindow函數檢索處理頂級窗口的類名和窗口名稱匹配指定的字符串。
1
2
3
4
5
6
7
8
9
10
11
|
BOOL
CheckDebug()
{
if
(FindWindowA(
"OLLYDBG"
, NULL)!=NULL || FindWindowA(
"WinDbgFrameClass"
, NULL)!=NULL || FindWindowA(
"QWidget"
, NULL)!=NULL)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
EnumWindows函數枚舉全部屏幕上的頂層窗口,並將窗口句柄傳送給應用程序定義的回調函數。
1
2
3
4
5
6
7
8
9
10
|
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;
}
|
1
2
3
4
5
6
|
BOOL
CheckDebug()
{
BOOL
ret = FALSE;
EnumWindows(EnumWndProc, (
LPARAM
)&ret);
return
ret;
}
|
GetForegroundWindow獲取一個前臺窗口的句柄。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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權限等。
調試器設置斷點的基本機制是用軟件中斷指令INT 3臨時替換運行程序中的一條指令,而後當程序運行到這條指令時,調用調試異常處理例程。INT 3指令的機器碼是0xCC,所以不管什麼時候,使用調試器設置一個斷點,它都會插入一個0xCC來修改代碼。惡意代碼經常使用的一種反調試技術是在它的代碼中查找機器碼0xCC,來掃描調試器對它代碼的INT 3修改。repne scasb指令用於在一段數據緩衝區中搜索一個字節。EDI需指向緩衝區地址,AL則包含要找的字節,ECX設爲緩衝區的長度。當ECX=0或找到該字節時,比較中止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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;
}
|
在OllyDbg的寄存器窗口按下右鍵,點擊View debug registers能夠看到DR0、DR一、DR二、DR三、DR6和DR7這幾個寄存器。DR0、Dr一、Dr二、Dr3用於設置硬件斷點,因爲只有4個硬件斷點寄存器,因此同時最多隻能設置4個硬件斷點。DR四、DR5由系統保留。 DR六、DR7用於記錄Dr0-Dr3中斷點的相關屬性。若是沒有硬件斷點,那麼DR0、DR一、DR二、DR3這4個寄存器的值都爲0。
1
2
3
4
5
6
7
8
9
10
11
12
|
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;
}
|
惡意代碼能夠計算代碼段的校驗並實現與掃描中斷相同的目的。與掃描0xCC不一樣,這種檢查僅執行惡意代碼中機器碼CRC或者MD5校驗和檢查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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;
}
}
|
被調試時,進程的運行速度大大下降,例如,單步調試大幅下降惡意代碼的運行速度,因此時鐘檢測是惡意代碼探測調試器存在的最經常使用方式之一。有以下兩種用時鐘檢測來探測調試器存在的方法。
記錄一段操做先後的時間戳,而後比較這兩個時間戳,若是存在滯後,則能夠認爲存在調試器。
記錄觸發一個異常先後的時間戳。若是不調試進程,能夠很快處理完異常,由於調試器處理異常的速度很是慢。默認狀況下,調試器處理異常時須要人爲干預,這致使大量延遲。雖然不少調試器容許咱們忽略異常,將異常直接返回程序,但這樣操做仍然存在不小的延遲。
較經常使用的時鐘檢測方法是利用rdtsc指令(操做碼0x0F31),它返回至系統從新啓動以來的時鐘數,而且將其做爲一個64位的值存入EDX:EAX中。惡意代碼運行兩次rdtsc指令,而後比較兩次讀取之間的差值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
BOOL
CheckDebug()
{
DWORD
time1, time2;
__asm
{
rdtsc
mov time1, eax
rdtsc
mov time2, eax
}
if
(time2 - time1 < 0xff)
{
return
FALSE;
}
else
{
return
TRUE;
}
}
|
同rdtsc指令同樣,這兩個Windows API函數也被用來執行一個反調試的時鐘檢測。使用這種方法的前提是處理器有高分辨率能力的計數器-寄存器,它能存儲處理器活躍的時鐘數。爲了獲取比較的時間差,調用兩次QueryPerformanceCounter函數查詢這個計數器。若兩次調用之間花費的時間過於長,則能夠認爲正在使用調試器。GetTickCount函數返回最近系統重啓時間與當前時間的相差毫秒數(因爲時鐘計數器的大小緣由,計數器每49.7天就被重置一次)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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;
}
}
|
通常雙擊運行的進程的父進程都是explorer.exe,可是若是進程被調試父進程則是調試器進程。也就是說若是父進程不是explorer.exe則能夠認爲程序正在被調試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
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);
}
|
explorer.exe建立進程的時候會把STARTUPINFO結構中的值設爲0,而非explorer.exe建立進程的時候會忽略這個結構中的值,也就是結構中的值不爲0。因此能夠利用STARTUPINFO來判斷程序是否在被調試。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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;
}
}
|
默認狀況下進程是沒有SeDebugPrivilege權限的,可是當進程經過調試器啓動時,因爲調試器自己啓動了SeDebugPrivilege權限,當調試進程被加載時SeDebugPrivilege也就被繼承了。因此咱們能夠檢測進程的SeDebugPrivilege權限來間接判斷是否存在調試器,而對SeDebugPrivilege權限的判斷能夠用可否打開csrss.exe進程來判斷。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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)回調、插入中斷、異常等。這些技術當且僅當程序處於調試器控制之下時才試圖擾亂程序的運行。
Thread Local Storage(TLS),即線程本地存儲,是Windows爲解決一個進程中多個線程同時訪問全局變量而提供的機制。TLS能夠簡單地由操做系統代爲完成整個互斥過程,也能夠由用戶本身編寫控制信號量的函數。當進程中的線程訪問預先制定的內存空間時,操做系統會調用系統默認的或用戶自定義的信號量函數,保證數據的完整性與正確性。下面是一個簡單的TLS回調的例子,TLS_CALLBACK1函數在main函數執行前調用IsDebuggerPresent函數檢查它是否正在被調試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#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 <iostream>
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段特別突出。
由於調試器使用INT 3來設置軟件斷點,因此一種反調試技術就是在合法代碼段中插入0xCC(INT 3)欺騙調試器,使其認爲這些0xCC機器碼是本身設置的斷點。
1
2
3
4
5
6
7
8
9
10
11
12
|
BOOL
CheckDebug()
{
__try
{
__asm
int
3
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
|
除了使用_try和_except之外還能夠直接使用匯編代碼安裝SEH。在下面的代碼中若是進程沒有處於調試中,則正常終止;若是進程處於調試中,則跳轉到非法地址0xFFFFFFFF處,沒法繼續調試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
#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的攻擊)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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指令時不要使用單步。
1
2
3
4
5
6
7
8
9
10
11
12
|
BOOL
CheckDebug()
{
__try
{
__asm __emit 0xF1
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
|
EFLAGS寄存器的第八個比特位是陷阱標誌位。若是設置了,就會產生一個單步異常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
BOOL
CheckDebug()
{
__try
{
__asm
{
pushfd
or word ptr[esp], 0x100
popfd
nop
}
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
|
前面已經討論了各類使用異常機制的反調試手段。
RaiseException函數產生的若干不一樣類型的異常能夠被調試器捕獲。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
BOOL
TestExceptionCode(
DWORD
dwCode)
{
__try
{
RaiseException(dwCode, 0, 0, 0);
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
BOOL
CheckDebug()
{
return
TestExceptionCode(DBG_RIPEXCEPTION);
}
|
進程中發生異常時若SEH未處理或註冊的SEH不存在,會調用UnhandledExceptionFilter,它會運行系統最後的異常處理器。UnhandledExceptionFilter內部調用了前面提到過的NtQueryInformationProcess以判斷是否正在調試進程。若進程正常運行,則運行最後的異常處理器;若進程處於調試,則將異常派送給調試器。SetUnhandledExceptionFilter函數能夠修改系統最後的異常處理器。下面的代碼先觸發異常,而後在新註冊的最後的異常處理器內部判斷進程正常運行仍是調試運行。進程正常運行時pExcept->ContextRecord->Eip+=4;將發生異常的代碼地址加4使得其可以繼續運行;進程調試運行時產生無效的內存訪問異常,從而沒法繼續調試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#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格式文件時的常見漏洞。
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不存在這種漏洞。
惡意代碼常嘗試利用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/
參考資料:
1.《惡意代碼分析實戰》第16章反調試技術(本文的主體框架)
2.《逆向工程核心原理》第51章靜態反調試技術&第52章動態反調試技術
4.天樞戰隊官方博客(本文大部分代碼的來源)