對於安全研究人員來講,調試過程當中常常會碰到反調試技術,緣由很簡單:調試能夠窺視程序的運行「祕密」,而程序做者想要經過反調試手段隱藏他們的「祕密」,普通程序須要防止核心代碼被調試逆向,惡意代碼須要隱藏本身的惡意行爲防止被跟蹤。就像病毒和殺軟的關係同樣,爲了順利的逆向分析,有反調試手段就有對應的破0解方法-反反調試。對此,天融信阿爾法實驗室研究人員總結了各種常見反調試手段以及對應的繞過方案,做爲總結但願與君共勉。git
關於各類反調試手段,網絡上和各類安全書籍上都有對應的介紹、各類調試工具插件已經集成了反調試功能,但有的只是介紹了反調試方法,並無對應的破0解方式,有的反調試手段已經失效。本文並非研究新的反調試方法,而是對windows平臺的反調試技術進行分類總結,並介紹其原理和應對方法。github
合理的分類有助於學習和理解各類反調試技術的原理,因爲理解不一樣,分類也不盡相同。當你掌握了這些技術後,能夠按照本身的理解從新給它們分類。本文按照靜態反調試方法與動態反調試方法將反調試技術分爲兩大類,其中靜態反調試技術的分類原理爲:程序啓動時,系統會根據正常運行和調試運行分配不一樣的進程環境,經過檢測進程環境來檢測進程是否處於調試狀態;根據逆向人員的工做環境和程序的正常運行環境不一樣,能夠經過檢測調試器或逆向分析工具實現反調試。動態反調試技術的分類原理是:進程運行時的執行流程是否正常、執行狀態是否正常。算法
本文測試環境:windows
測試代碼: https://github.com/alphaSeclab/anti-debug瀏覽器
調試器爲原版OllyDbg1.1(http://www.ollydbg.de/odbg110.zip) 由於各類魔改帶插件的OD已經自帶許多反反調試功能,會影響測試效果。安全
測試系統爲win7 SP1 32位旗艦版網絡
PE編輯器:LordPE編輯器
許多靜態反調試技術對系統有較強的依賴,本文測試的靜態反調試技術時使用的是win 7 SP1 32位旗艦版。像PEB中ProcessHeap的Flags與ForceFlags等相似反調試手段針對的是NT5.X如下版本,win7 下檢測就會失效,因此本文未作介紹。ide
咱們不同:正常啓動的進程和調試啓動的進程的某些初始信息是不一樣的,好比進程環境塊PEB、STARTUPINFO等,這些信息使用方便,常常被普遍應用於反調試技術。函數
2.1.1 BeingDebugged
PEB結構體的內容以下(部分省略):
上表中列出了Windows 7 32位SP1中PEB的結構體成員,其中咱們將會介紹到的與反調試相關的成員有:偏移爲0×002的BeingDebugged和偏移爲0×068的NtGlobalFlag。BeingDebugged是一個標誌位,就像它的名字同樣,用來表示進程是否處於被調試狀態,NtGlobalFlag用來表示進程的堆內存特性,調試和非調試時其值不一樣。因此咱們只須要獲取這兩個值與正常狀態的值比較,就能知道進程是否處於調試狀態。
利用fs段寄存器指向的TEB結構能夠獲得PEB結構體的地址:
mov EAX,DWORD PTR FS:[0x30]
調試時該值爲TRUE,非調試時該值爲FALSE。因此咱們能夠這樣判斷進程是否被調試:
正常運行時,該函數返回FALSE,用OD打開調試運行時,該函數返回TRUE。
其實有一個API能夠直接獲取BeingDebugged的值:IsDebuggerPresent();咱們看下這個API的實現代碼:在OD反彙編窗口按下快捷鍵CTRL+G,輸入函數名跳轉到函數地址
能夠看到該函數其實也是讀取的PEB中的BeingDebugged標誌位的值。和咱們的代碼相比,它多了一行MOV EAX,DWORD PTRFS:[18],這行代碼的做用是獲取TEB的地址,咱們上面說了,PEB的地址在TEB中,因此能夠先獲取TEB的地址,再獲取PEB的地址。
破0解方法:調試時手動將其置0。
OD打開被調試程序,在內存窗口CTRL+G,輸入FS:[30],內存窗口將跳轉到PEB的地址:
選中偏移爲2的位置CTRL+E,將1改成0便可。
2.1.2 NtGlobalFlags
當進程處於調試狀態時,PEB結構體偏移爲0×68的NtGlobalFlag的值會被設置爲0×70。因此和BeingDebugged成員檢查同樣,咱們只須要檢查其值是否爲0×70就能判斷程序是否被調試。
破0解方法:和修改BeingDebugged的值同樣,內存窗口CTRL+G輸入fs:[30]跳轉到PEB的地址,選中偏移0×68的地方,CTRL+E將其改成0便可。
2.1.3 NtQueryInformationProcess()
除了利用PEB查詢進程相關的調試信息外,還能夠利用NtQueryInformationProcess()API來獲取其餘各類與進程調試相關的信息。
其中第1個參數是要查詢的進程的句柄值,第2個參數是要查詢進程的哪些信息,該參數是一個枚舉類型的值,能夠查詢的信息以下:
第2個參數不一樣的值對應於第3個參數指向不一樣類型的結構體,好比第2個參數填0(ProcessBasicInformation),就表示你想獲取PEB的信息,而後就能夠判斷BeingDebugged,NtGlobalFlag的值了,和咱們前面講到的同樣,可是這種實現方式稍顯複雜。
這節咱們主要使用該函數查詢2個值:ProcessDebugPort、ProcessDebugObjectHandle,對應的枚舉值爲7和0x1E。
進程處於調試狀態時,系統會爲它分配一個調試端口,第2個參數傳7時,NtQueryInformationProcess()就能獲取調試端口的值,調試狀態該值爲0xFFFFFFFF,正常運行時該值爲0。
一樣,有一個API也能夠檢測調試端口的值:CheckRemoteDebuggerPresent()。CTRL+G定位到該函數實現部分,發現該函數其實就是內部調用了一下第2個參數爲7的NtQueryInformationProcess:(實際調用的Zw版的函數,功能相同)
進程被調試時會生成調試對象,當函數的第2個參數值爲0x1E(ProcessDebugObjectHandle)時就能獲取調試對象句柄,正常運行時該值爲0,調試運行時該值爲非0值。
破0解方法:若是被調試程序只是調用幾回API,而不是在多個地方反覆調用,咱們能夠在調用的時候手動修改特定參數下的調用結果:修改返回值或函數參數指向的內存。像上面的例子同樣,咱們能夠在該函數調用結束的後手動修改參數指向的內存,修改調用結果。當函數調用傳參爲7/0x1E的時候,修改其第3個參數獲取的值:
在call處下斷,命中斷點後,棧區右鍵,定位到第3個參數的緩衝區:
F8步過等函數調用完畢,修改第3個參數緩衝區內的值
CTRL+E,修改成0。
若是該函數在多個地方被反覆調用,那麼咱們能夠採起API HOOK的技術,修改函數內部執行代碼的流程。
OD從新運行程序,反彙編區CTRL+G,輸入ZwQueryInformationProcess(實際調用的是該函數),跳轉到函數反彙編指令處。
第1條指令正好5個字節,咱們今後處HOOK。
ALT+M查看進程的內存映射表,找到其代碼區範圍:
代碼起始地址:0×1281000,大小爲0×5000。
回到反彙編窗口ALT+C:CTRL+G跳轉到0×1281000+0×4000的位置
這段空間是原程序代碼區對齊用的,沒有實際的代碼,咱們在這裏實現咱們的HOOK代碼,將ZwQueryInformationProcess的第1條指令修改成jmp 1285000,執行HOOK代碼,先判斷第2個參數是否是7或0x1E,是則把第3個參數指向的緩衝區填0,直接返回,不是則執行ZwQueryInformationProcess的第1條指令mov eax,0xEA,而後跳轉到ZwQueryInformationProcess第2條指令處繼續執行原函數功能。
ZwQueryInformationProcess函數的兩個MOV和後面的CALL RETN指令都可做爲HOOK點,這裏選擇的第1條指令HOOK,對於這種反反調試的手段,程序能夠檢查API的第1個指令是否是原指令來判斷是否是有HOOK代碼來繼續實現反調試,和動態反調試的API斷點類似,後面的動態反調試會介紹怎麼應對HOOK檢測。
2.1.4 STARTUPINFO
程序正常啓動(雙擊啓動)時,實際是資源瀏覽器經過CreateProcess()函數建立進程啓動的:
explorer啓動程序時,會把倒數第2個參數STARTUPINFO結構體中的值設置爲0,但調試器啓動程序的時候不會,因此咱們能夠經過判斷該結構體中的某些值是否爲0來判斷是否被調試:
這裏咱們選擇判斷結構體中窗口座標和大小這幾個值(也能夠多選幾個)是否爲0來實現反調試檢測:
破0解方法:同NtQueryInformationProcess同樣,當函數調用次數較少時,能夠直接修改函數調用結束後的參數值:
函數調用處下斷,命中斷點後,數據區定位到第1個參數指向的結構體地址,操做方法同2.1.3中修改NtQueryInfomationProcess調用結果相同。F8步過,CTRL+E修改結構體中對應的值:
當函數調用次數較多時-像NtQueryInformationProcess,咱們採用的是API HOOK,那麼這個函數可否使用API HOOK技術呢?咱們只是修改該結構體中的部分值,而不是所有值,因此修改結構體內容要發生在函數執行完畢,因此HOOK點要選擇函數將要返回時,通常這種狀況是不容易HOOK的,反彙編區CTRL+G 輸入GetStartupInfoW,看下GetStartupInfoW的函數實現尾部:
尾部POP EBP和RETN 4共4個字節,不符合咱們的要求,並且返回前還有JNZ跳轉另外一個返回分支,不是咱們指望的HOOK點。該函數實際須要填充參數指向的結構體的值,咱們看下函數體中它填充的值來自哪裏:
由彙編代碼實現,咱們能夠發現,該函數實際是先定位到PEB中偏移爲0×10的_RTL_USER_PROCESS_PARAMETERS結構體(參考2.1.1中的PEB信息)地址處,而後獲取參數STARTUPINFO的地址,分別將兩個地址值賦值給ECX和EAX,以這兩個寄存器爲偏移基址,分別獲取_RTL_USER_PROCESS_PARAMETERS結構體內的值,並賦值給STARTUPINFO結構體。因此,咱們只須要將結構體_RTL_USER_PROCESS_PARAMETERS中的相應值置0便可實現相似API HOOK的效果。_RTL_USER_PROCESS_PARAMETERS結構體內容以下:(win7 32位SP1)
STARTUPINFO的dwX、dwY、dwXSize、dwYSize分別對應該結構體中偏移爲0x4C、0×50、0×5四、0×58處的值,下面咱們找到結構體_RTL_USER_PROCESS_PARAMETERS在內存中的位置:OD數據區CTRL+G輸入FS:[0x30]定位到PEB地址,找到偏移0×10處的_RTL_USER_PROCESS_PARAMETERS地址:
0x4E1298就是咱們要找的地址,數據窗口CTRL+G跳轉到該地址處:
爲方便咱們找到那幾個偏移值,數據窗口右鍵把數據以地址方式顯示:
找到對應的偏移處的值,CTRL+E將其置爲0便可。咱們這裏只比較了4個值,你也能夠把結構體中其餘的值置0。
2.1.5 SedebugPrivilege
通常程序正常啓動時是不具有調試權限(SedebugPrivilege)的,除非本身有提權的須要主動開啓,可是調試器啓動程序的時候,因爲調試器自己會開啓調試權限,因此被調試進程會繼承調試權限,所以咱們能夠經過檢查進程是否具備調試權限來進行反調試。
檢查進程是否具備調試權限的方式很簡單,系統啓動的時候會啓動一個核心進程csrss.exe,咱們能夠經過判斷可否使用OpenProcess打開該進程來檢查當前進程是否具備調試權限,由於只有擁有管理員權限+調試權限的進程才能打開csrss.exe的句柄。嚴格來講這種檢查方法是不太嚴格的,由於當進程有調試權限無管理員權限的時候也不能打開csrss.exe的句柄,幸運的是,大多數調試器都會要求提供管理員權限,因此被調試程序也會同時擁有管理員權限+調試權限。
經過CsrGetProcessId函數能夠獲取csrss.exe的PID,而後經過OpenProcess嘗試打開csrss.exe的句柄:
破0解方法:在OpenProcess函數調用上或函數起始地址下斷,判斷PID的值是否是csrss.exe的PID,注意:有兩個csrss.exe的進程,經過任務管理器能夠查看其PID的值,不要只關心一個PID。若是是這兩個中的一個,直接執行到函數返回,並修改EAX的值爲0便可(EAX就是函數的返回值):以下圖所示(0×228 == 552)
除了修改函數返回結果其實也能夠直接修改查找的字符串。有的反調試程序會使用遍歷進程的方式查找csrss.exe的PID值,但最終仍是會調用OpenProcess,破0解方式與上面相同,這裏介紹另一種破0解方式:直接搜索「csrss.exe」字符串,將其緩衝區直接置0便可,這樣它就找不到csrss.exe進程了:
遍歷進程查找csrss.exe:
破0解方式:ALT+M轉到內存窗口,右鍵search或快捷鍵ctrl+B:
在unicode欄輸入csrss(固然也有多是asc字符串,示例代碼用的是unicode)
成功找到csrss.exe的字符串地址,ctrl+L繼續找還有沒其餘地址,發現只有這一處,CTRL+E將其置0便可。
在2.1中介紹了各類進程自己狀態檢測的反調試技術,其原理是調試狀態下的進程和正常運行時進程自己不一樣,這些檢查是固定不變的,當全部的檢查項都被咱們假裝經過了以後,這些反調試手段就失效了。本節將介紹針對調試環境檢測的反調試手段及反反調試手段。安全研究員要調試程序,他的工做環境和正常的工做環境是不一樣的,像各類分析工具、虛擬機環境等等,這些工做環境的檢測就成爲除進程檢測外的另外一種反調試手段。這時候無論進程有沒有被調試,只要發現程序所處環境不正常,就認爲本身被調試分析了.
2.2.1 註冊表檢測
當程序運行發生錯誤的時候,會有一個錯誤彈框,讓用戶選擇關閉程序仍是調試程序:
正經常使用戶只能選擇關閉程序,由於他沒有調試器,對於安全研究人員則不一樣,他們的工做環境上搭配有對應的調試器,當程序崩潰時,他們能夠選擇設置好的JIT調試器調試程序。以OD舉例,將OD設置爲JIT調試器:Options->Just-in-time debugging:
一旦設置了JIT調試器,系統就會更改註冊表項中對應JIT的值:
這個註冊表的位置爲:
咱們能夠經過查找該註冊表項對應的值是否包含經常使用調試器的字符串來檢測調試器:
破0解方法:搜索字符串AeDebug,步驟和2.1.5相同,只是這裏會搜到兩個結果(一個32位路徑,一個64位路徑),將其所有置0便可。
2.2.2 窗口檢測
窗口檢測即經過查找當前系統中運行的程序窗口名稱是否包含敏感程序來進行反調試。經常使用的函數有FindWindow、EnumWindows。FindWindow能夠查找符合指定類名或窗口名的窗口句柄,EnumWindows能夠枚舉全部頂層窗口的句柄值,並傳給回調函數。
破0解方法:搜索字符串OllyDbg或OLLYDBG(你使用的調試器的類名或窗口名),將其置0便可。
2.2.3 父進程檢測
程序正常啓動(雙擊)時,其父進程爲exeplorer.exe,調試啓動時其父進程爲調試器,因此檢查其父進程是否爲explorer.exe也能夠做爲一個反調試手段。可是這種檢測方式容易誤判,由於程序也多是由其餘正常進程經過CreateProcess正常啓動。NtQueryInformationProcess獲取當前進程的父進程PID,經過遍歷進程來獲取該PID對應的進程名是否explorer.exe來實現反調試。這裏還有另外一種選擇:獲取exeplorer.exe的PID和當前進程的父進程PID比較,可是這種檢測遇到一種狀況會失效-系統啓動兩個explorer.exe,這時候會有兩個explorer的PID。因此示例代碼只演示比較進程名而不是比較PID。
破0解方法:除了字符串搜索法外,能夠選擇把本身的調試器exe文件改爲explorer.exe再運行,這樣檢測父進程名稱時調試器的名稱也變成explorer.exe了,另外一種方法就是修改_wcsicmp/_stricmp的比較結果,讓其返回0:
2.2.4 進程掃描
進程掃描是比父進程檢測更暴力的檢測方式:只要系統進程列表內有敏感進程,就執行反調試手段。
破0解方法:最直接的仍是更改調試器名稱,隨便換個名字便可,另外一種方法就是查找字符串」OllyDbg.exe」,將其置0便可。
2.2.5 內核對象掃描
在2.1.3中咱們介紹了查找調試對象句柄的方式實現反調試,其實也能夠經過查找這個調試對象來實現反調試。當調試會話創建時會建立一個「DebugObject」類型的內核對象,經過遍歷內核對象鏈表查找是否包含該類型的內核對象便可實現反調試:
破0解方法:查找字符串「DebugObject」,將其置0便可。
2.2.6 調試模式檢測
安全研究工做通常是在虛擬機環境中進行的,當進行內核調試時,虛擬機系統會處於被調試狀態,VirtualKD (http://virtualkd.sysprogs.org)能夠輕鬆實現內核調試。
當系統處於被調試狀態時,咱們能夠經過檢測該狀態來實現反調試:
破0解方法:系統正常啓動,不開啓調試模式,或開啓調試模式可是不創建內核調試會話。若是有內核調試的須要,能夠函數調用處下斷,修改函數執行結果,或HOOK API。
2.2.7 調試器攻擊
這裏說的調試器攻擊並非指對調試器形成破壞,而是指讓調試器沒法調試程序。好比ZwSetInformationThread()函數傳參ThreadHideFromDebugger時,若是程序正常運行,那麼該函數就至關於什麼都沒作,但若是程序處於被調試狀態,那麼該函數就可使當前線程(通常是主線程)脫離調試器,使調試器沒法繼續接收該線程的調試事件,效果就像是調試器崩潰了同樣:
破0解方式:API HOOK,當ZwSetInformationThread第2個參數爲17的時候,函數直接返回。HOOK步驟參考2.1.3,HOOK代碼以下:
另外一種調試器攻擊方式就是利用OD加載進程時的一個BUG,在PE文件格式中,數據目錄表項的個數通常是0×10個,若是大於這個值,雙擊運行時,系統加載器會忽略這個值,可是OD加載進程時會認爲PE文件格式錯誤,拒絕加載。
其次,雙擊運行系統加載器加載進程區段時,採用的是區段信息表中區段在虛擬內存中的大小和區段在文件中大小的較小值,而OD則固定採用文件大小,若是把區段在文件中的大小值改的很大,則系統加載器能夠正常運行程序,而OD則會認爲PE文件格式錯誤,拒絕加載。
破0解方法:把數據目錄表改回0×10,RSize改爲VSize爲0×200的整數倍便可。
在第二章中介紹了基於進程狀態和運行環境檢測的靜態反調試技術,靜態反調試技術實際上是對整個程序的保護,反調試手段相對「死板」,破0解方式也比較簡單。其實程序開發者只是想對程序的關鍵算法和核心數據進行保護、避免逆向分析。因此他們大多利用的是動態反調試手段:調試行爲檢測。
動態反調試的原理基於調試行爲自己,好比,調試運行的程序比正常運行的程序執行速度慢,因此能夠用時鐘檢測來進行反調試;遇到異常時,調試器會先接收異常事件而不是直接傳遞給進程自己,因此能夠利用異常處理機制來實現反調試;逆向分析核心代碼時須要下斷點、單步跟蹤等,能夠檢測斷點、單步等實現反調試。
動態反調試的主要目的是干擾調試器,使其沒法正常跟蹤程序的執行流程或加大其逆向分析難度。相比靜態反調試,其隱蔽性較強,技術難度更高,破0解難度也更大。
程序被調試時什麼事都得向調試器通知報備一下,就像是調試器的提線木偶,調試器還要和用戶交互,等待用戶命令再下發給程序,哪有徹底自主來的爽。因此在調試器中跟蹤程序代碼比程序正常運行耗費的時間要多不少。時鐘檢測技術就是經過計算關鍵內容運行時間差別來判斷進程是否處於被調試狀態。同時程序在虛擬機中的運行速度也比正常速度慢,因此時鐘檢測技術通常也用於反虛擬機/反模擬器技術。計算運行時差的方式通常有兩種:讀取CPU時鐘計數器、時間計數相關API。
3.1.1 CPU記數器
x86 CPU中存在一個名爲TSC(Time Stamp Counter)的64位寄存器。CPU對每一個時鐘週期計數,而後保存到TSC。RDTSC是一條彙編指令,用來將TSC的值讀入EDX:EAX寄存器(有的程序也會使用RDTSCP彙編指令,用法相同)。
破0解方法:F9運行步過這段檢測代碼,若是有單步調試的須要則修改RDTSC的執行結果,把EAX,EDX置0或其餘值,只要使兩次RDTSC結果相同便可。
3.1.2 時間API
時間計數相關的API有不少,在時鐘檢測上RDTSC的效率和準確度相比這些API是最高的。但這些API也是一種反調試手段,這些API有:QueryPerformanceCounter、GetTickCount、GetSystemTime、GetLocalTime等,具體參考MSDN(https://docs.microsoft.com/zh-cn/windows/desktop/SysInfo/time-functions)。
這些API的使用方式大同小異,這裏只介紹QueryPerformanceCounter的使用:
破0解方法:改變函數調用結果,使兩次獲取時間相同,注意:通常這些時間API也會用於其餘用途,因此除非明確知道全部的該API調用都是用來反調試,不然不要隨便HOOK。
異經常用於動態反調試技術。正常運行的進程發生異常時,在SEH(Structured Exception Handling)機制的做用下,OS會接收異常,而後調用進程中註冊的SEH處理。可是,若進程正被調試器調試,那麼調試器就會先於SEH接收處理。利用該特徵可判斷進程是正常運行仍是調試運行,而後根據不一樣的結果執行不一樣的操做,這就是利用異常處理機制不一樣的反調試原理。
3.2.1 SEH
Windows中的一些典型異常以下:
https://msdn.microsoft.com/zh-tw/library/aa915076.aspx
以INT3異常EXCEPTION_BREAKPOINT爲例,當該異常觸發時,若程序處於正常運行狀態,則自動調用已經註冊過的SEH;若程序處於調試運行狀態,則系統會中止運行程序將控制權轉給調試器。反調試程序通常會在SEH中更改程序的執行流程,若控制權交給調試器,而調試器又沒有執行SEH代碼,程序流程就會走向未知。
破0解方法:OD調試選項中,設置忽略指定異常並傳遞給程序便可,意思就是這個異常不是OD設置的,是程序本身觸發的,OD不處理,程序本身處理吧。分析明白異常處理邏輯後,找到異常處理更改後(若是異常處理程序更改了)的EIP,繼續分析。
這裏只是用INT3斷點舉例,要想主動觸發其餘類型的異常可使用RaiseException()函數。這種反調試手段看似有點雞肋,其實它的主要目的是在異常處理中改變程序的執行流程,增長逆向分析難度,除非把異常處理代碼分析明白,不然就搞不懂異常處理完畢程序的新EIP在哪,從何處繼續分析。並且這種把忽略異常把異常交給程序本身處理的方法並非萬能的,後面的INT 2D會「教OD作人」。
3.2.2 SetUnhandledExceptionFilter()
主動觸發的異常能夠被調試器交給程序本身處理,那麼當SEH不存在,或SEH處理不了該異常的時候怎麼辦呢?這時候系統會調用UnhandledExceptionFilter()函數,該函數會檢查進程是否處於調試狀態,如果,就把異常傳遞給調試器,不然就彈個錯誤對話框,而後結束程序:
SetUnhandledExceptionFilter()能夠添加一個異常處理函數來替換彈框行爲,沒有調試器的時候就會把異常傳遞給該異常處理函數去處理:
破0解方法:首先像3.2.1同樣要讓OD把這些程序主動觸發的異常忽略並交給程序處理,而後異常處理流程就走到UnhandledExceptionFilter,由於UnhandledExceptionFilter檢測到沒有調試器就會把異常交給SetUnhandledExceptionFilter註冊的函數,而它檢查有無調試器的方式就是2.1.3中提到的NtQueryInformationProcess函數第2個參數傳7(ProcessDebugPort),查找調試端口。後面的處理方式就和2.1.3中同樣,把NtQueryInformationProcess()函數 HOOK掉便可。
3.2.3 INT 2D
INT 2D是一個特殊的指令,原爲內核模式中用來觸發斷點異常的,也能夠在用戶模式下正常運行時觸發異常。但程序在OD中調試運行時有如下特色:
① 不會觸發異常,只是忽略
② INT 2D的下一條指令的第一個字節會被忽略。
③ F7/F8單步命令跟蹤INT 2D時,程序不會停在下條指令開始的地方,而是一直運行,直到遇到斷點。
對於OD來講,忽略異常設置也沒用,由於OD並無把INT 2D看成異常,而是直接忽略。
破0解方式:由於OD直接忽略INT 2D異常,因此設置忽略異常傳遞給程序自身處理無效,那麼咱們直接把INT 2D改變成OD不會忽略的異常便可,好比把它改爲INT3異常。這樣OD就能把異常正常傳遞給異常處理程序了。
在程序調試過程當中,咱們通常會設置許多軟件斷點。軟件斷點的原理其實就是調試器在對應設置斷點的位置上修改該地址的字節爲0xCC,0xCC就是x86指令中的INT 3。如果關鍵位置檢測到該指令,便可判斷進程處於調試狀態。檢測時要注意不是全部的位置均可以,由於0xCC既能夠是INT 3指令,也能夠是其餘指令的操做數。基於這種原理的反調試技術稱爲「0xCC探測技術」。
3.3.1 API斷點
在2.1.3中咱們介紹了API HOOK,其中提到HOOK代碼能夠經過檢查HOOK點是否原API指令來檢測HOOK實現反調試。API斷點的檢測原理和檢測HOOK代碼相同,API斷點通常下在API的首地址處或函數返回地址處。因此檢測這些易被下斷的地址首字節是否爲0xCC便可判斷程序是否正在被調試。
破0解方式:把斷點下在函數中間,或使用硬件斷點下斷。
3.3.2 校驗和
爲防止調試時把斷點下在函數中間,反調試程序除探測0xCC外,一般採用比較特殊代碼區域(易被下斷點的區域)的校驗和的值。這樣,在該代碼區域內調試時,只要調試器在該區域內設置一些0xCC斷點,如此一來,新的校驗值就和原來的不同了,這樣就能判斷出進程是否被調試了。
破0解方法:遇到校驗和檢測的時候,首選F9運行過被檢測代碼區,須要單步分析的時候則使用F7單步,須要步過使用硬件斷點步過。也能夠經過下內存訪問斷點,找到計算校驗和代碼的地方,改變比較跳轉。
在3.3中咱們知道0xCC斷點容易被檢測到,這時須要硬件斷點來臨時過渡。硬件斷點的實現實際依賴於幾個調試寄存器:Dr0~Dr7。Dr0~Dr3保存硬件斷點的地址,Dr四、Dr5保留,Dr六、Dr7用於說明哪一個硬件斷點觸發的相關屬性。因此同時最多能設置4個硬件斷點。只要檢查Dr0~Dr4這幾個寄存器的值是否爲0就知道有沒有沒下硬件斷點、有幾個硬件斷點。查詢調試寄存器的方式通常有兩種:API直接查詢寄存器的值、主動觸發異常查詢寄存器的值。
3.4.1 API直接查詢
直接API查詢寄存器的值:
破0解方式:修改參數中context.ContextFlags的值,或HOOK GetThreadContext的參數值,把context.ContextFlags的值改成CONTEXT_INTEGER(0×10002)值,不讓該函數查詢調試寄存器的值便可。
或HOOK GetThreadContext,HOOK API首地址:
注意,GetThreadContext會被系統函數調用,因此通常不要HOOK該函數,或判斷調用是否來自用戶代碼再決定是否執行HOOK代碼。
3.4.2 異常間接查詢
主動觸發異常檢查寄存器的值:
破0解方法:這種反調試技術沒有直接調用API而是調用SEH根據異常CONTEXT查詢寄存器的值不容易被發現,特別是這個異常處理函數須要OD主動忽略異常才能傳遞給程序處理,這時候若是逆向分析人員沒有追蹤分析異常處理函數,就很容易「被反調試」。這種反調試就只能跟蹤異常處理函數自己,而後在查詢調試寄存器的地方改變其查詢比較結果便可。
逆向分析人員在分析關鍵代碼區的時候除了在API下斷,還須要不斷的單步分析,OD的F7單步步入和F8單步步過的原理就是設置單步異常或0xCC斷點。反調試程序能夠針對這一點設置陷阱,檢測TF或0xCC實現反調試。
3.5.1 rep/call步過
當遇到call指令或rep指令的時候,逆向人員常常會採用F8步過的方式快速步過指令執行序列。這時候,call指令或rep指令的下一條指令起始字節就會被設置一個軟件斷點(0xCC)。反調試程序能夠在執行call指令或rep指令的時候檢測下條指令是否是被下0xCC斷點或直接把下條指令改爲NOP指令使0xCC斷點失效來實現反調試:
使0xCC斷點失效原理:
當12F207B遇到rep或call習慣性F8步過的時候,12F207D的0x8B就會變成0xCC(INT3),這時rep指令或call指令會把12F207D從新恢復成原指令字節8B。這樣rep或call步事後,本應斷下的0xCC斷點並不會斷下,調試器沒有從新獲得控制權,程序繼續執行,又得從新分析。固然這種反調試手段麻煩的一點在於從新把原字節(0x8B)寫回去須要把代碼段屬性修改成可寫。不過這種反調試手段通常在惡意代碼中比較常見,而惡意代碼常常申請堆內存執行惡意行爲,而申請的堆內存時的屬性如果可讀可寫可執行的,就不存在這個麻煩。
檢測是否存在因調試步過而設置的0xCC斷點:
這種檢測rep/call步過的0xCC斷點的反調試技術能夠應用在任何地方。這裏的例子只是爲了說明其原理,實際應用實能夠有多種變化。
破0解方法:碰到rep或call的時候要多注意esi/edi這些敏感寄存器實際指向的地址,並且這種步過檢測會有讀取/寫入代碼區字節的行爲,逆向分析遇到代碼區有讀取/寫入行爲的時候要多使用內存斷點和硬件斷點,儘可能不用軟件斷點。
3.5.2 TF檢測
當EFLAGS的TF標誌位被置1時,CPU將進入單步執行模式。單步執行模式中,CPU執行1條指令後即觸發一個EXCEPTION_SINGLE_STEP異常,而後TF會自動清零。TF檢測通常有兩種和方式,第1種:主動觸發TF異常、與SEH結合使用探測調試器。
破0解方法:這種先pushfd修改後再popfd的方式是最多見的TF標誌反調試手段,破0解方法和3.2中異常處理同樣,OD調試設置項就能夠解決,不過須要仔細分析異常處理函數過程。
第二種檢測TF的方式相反,不主動設置TF標記位,檢測是否關鍵區有代碼被單步調試:檢測調試器是否在設置TF標記位。檢測原理是:
但前面已經說過,當F7單步執行pushfd時,TF標誌位會被自動清零,因此後面的test結果永遠爲0。這時候須要用到一條特殊的指令:pop ss。pop ss會將異常和中斷掛起,直到下一條指令執行完畢:
單步執行pop ss時,TF異常會被掛起,執行完下一條指令纔會處理TF異常,因此程序不會停在pushfd這條指令處,而是停在test指令處。
破0解方法:直接運行跳過這段代碼,或修改test比較結果。
同一個進程不容許同時被兩個調試器調試,利用這一點能夠本身先調試運行本身,防止被另外一個調試器繼續調試。通常這種反調試程序會建立一個用於同步的內核對象,用第1次打開的進程主動第2次打開進程,並調試第2個進程。
3.6.1 CreateProcess
進程第1次運行時會嘗試訪問同步內核對象,若是不存在,則說明當前進程第1次運行,建立一個內核對象,並以調試方式建立進程打開「本身」。這時若調試器首次調試運行進程則至關於在調試一個調試器,因爲第2次運行的進程是被第1次運行的調試打開的,因此調試器也沒法繼續調試第2次運行的進程。
破0解方式:
一、 調試打開程序時,修改OpenXXX等打開同步對象的API返回值,使程序走正常流程。
二、 主動建立一個同名內核對象CreateXXX,使OpenXXX等打開同步對象的操做成功,走向正常流程。
這裏只是簡單演示這種自調試的原理,在實際逆向中,被逆向程序多是個加密、壓縮、代碼被偷取等等不完整的進程,須要在第2次被自身調試打開時在調試循環中完成自修補。這樣的反調試纔是難對付的,大大增長了逆向分析難度。須要逆向分析人員程序的調試循環進行跟蹤分析,把修補後的完整程序從內存中DUMP出來再逆向分析。
3.6.2 DebugActiveProcess
自調試除上節講的CreateProcess()以調試方式打開進程外,還能夠選擇正常建立自身,而後立刻附加建立進程的操做來實現。DebugActiveProcess()就能夠作到這一點。
破0解方式:因爲這時是先建立進程再立刻附加的方式實現自調試,因此除3.5.1的破0解方式外,咱們還可讓程序在建立自身進程後直接退出,不讓其附加建立的進程,由調試器去附加。而面臨的問題和3.5.1相同,自調試只是個手段,自修補纔是核心,不讓它自調試程序就不能補全自身,給逆向分析帶來麻煩。
經過本文總結的這些反調試技術能夠看出,靜態反調試技術偏於「大格局」,像進程自己的一些標誌位查詢、調試環境檢查等,動態反調試偏於「小細節」,像異常處理、斷點檢測等。這些技術若是隻單純應用其中的一種,實際上是不難發現與破0解的,但若是他們綜合利用,互相隱蔽,那就大大增長了發現和破0解的難度。這些反調試技術綜合來講仍是「死的」,弄明白原理就不難破0解,因此如今的優秀反調試技術已經不侷限於用這些「死板的」技術去調戲逆向分析人員了,他們開始使用像代碼混淆、花指令、VMP等手段在精神上給逆向分析人員形成魔法傷害,而這種反調試手段的破0解方式就是:老實逆向分析,一樣的加花、混淆、VMP保護手段分析出原理了,之後就能增長魔抗、快速破0解反調試手段了。因此,終級的反反調試手段仍是增長本身的基本功-「他強任他強 清風拂山崗」。
1.《逆向工程核心原理》
2. The 「Ultimate」 Anti-debugging Reference
3. 反調試技術總結
4. An Anti-Reverse EngineeringGuide
5. Anti Debugging ProtectionTechniques With Examples
6. Anti-debugging Techniques CheatSheet
7. OpenRCE