動態反調試
本文介紹了幾種常見的動態反調試技術,在閱讀之前您可能要了解一些靜態反調試手段.
前文 : 常見靜態反調試技術總結
本文實例所用資源 : https://download.csdn.net/download/weixin_45551083/12555778
文章目錄
1 SEH
1.0 SEH基本概念
基本概念
百度百科:
SEH(“Structured Exception Handling”),即結構化異常處理·是(windows)操作系統提供給程序設計者的強有力的處理程序錯誤或異常的武器。
SEH是Windows操作系統默認的異常處理機制,逆向分析中,SEH除了基本的異常處理功能外,還大量運用於反調試程序. 就是異常時會啓動SEH,SEH裏面包括了處理異常的代碼.
與C語言中的__try,__except,finally
等處理機制類似,不過SEH要早於C語言中的異常處理
示例程序 seh.exe
地址0x401019處嘗試向DS:[0]處寫入數據,引發非法訪問異常(較爲常見的一種異常)
正常運行:
調試器下 : F9運行程序,調試器會暫停在此處,調試器下方會提示
Shift+F7/F8/F9 運行
調試器將異常交給SEH處理,而SEH中又有反調試技術,所以程序顯示Debugger detected
1.1 異常
(1) 幾個常見異常
- EXCEPTION_ACCESS_VIOLATION ( 0xC0000005 )
試圖訪問不存在或不具有訪問權限的內存區域時,就會發生EXCEPTION_ACCESS_VIOLATION(非法訪問,較爲常見)
舉例:
MOV DWORD PTR DS:[0],1 ;內存地址0處是未分配的區域
ADD DWORD PTR DS:[401000],1;.text節區起始地址0x401000僅具有讀權限,無寫權限
XOR DWORD PTR DS:[80000000],1234;內存地址0x80000000屬於內核區域,用戶無法訪問
- EXCEPTION_BREAKPOINT ( 0x80000003 )
運行代碼中設置斷點後,CPU嘗試執行該地址處的指令是,將發生EXCEPTION_BREAKPOINT異常.
調試器就是利用該異常實現斷點功能. 設置斷點時會將該位置設置爲0xCC(int 3) 但是爲了方便代碼可讀性,並不會顯示出來(仍然是最開始的指令).
- EXCEPTION_ILLEGAL_INSTRUCTION ( 0xC000001D )
CPU遇到無法解析的指令時引發該異常,比如"0FFFF"指令在x86CPU中未定義,CPU遇到該指令將引發EXCEPTION_ILLEGAL_INSTRUCTION異常.
- EXCEPTION_INT_DIVIDE_BY_ZERO ( 0xC0000094 )
整數除法運算中,若分母爲0,則引發EXCEPTION_INT_DIVIDE_BY_ZERO異常
- EXCEPTION_SINGLE_STEP ( 0x80000004 )
單步的含義是執行一條指令,然後暫停.CPU進入單步模式後,每執行一條指令就會引發EXCEPTION_SINGLE_STEP異常,暫停運行.將EFLAGS寄存器的TF位設置爲1後,CPU就會進入單步模式
(2) 異常編碼對照表
異常 | 值 | 描述 |
---|---|---|
EXCEPTION_ACCESS_VIOLATION | 0xC0000005 | 程序企圖讀寫一個不可訪問的地址時引發的異常。例如企圖讀取0地址處的內存。 |
EXCEPTION_ARRAY_BOUNDS_EXCEEDED | 0xC000008C | 數組訪問越界時引發的異常。 |
EXCEPTION_BREAKPOINT | 0x80000003 | 觸發斷點時引發的異常。 |
EXCEPTION_DATATYPE_MISALIGNMENT | 0x80000002 | 程序讀取一個未經對齊的數據時引發的異常。 |
EXCEPTION_FLT_DENORMAL_OPERAND | 0xC000008D | 如果浮點數操作的操作數是非正常的,則引發該異常。所謂非正常,即它的值太小以至於不能用標準格式表示出來。 |
EXCEPTION_FLT_DIVIDE_BY_ZERO | 0xC000008E | 浮點數除法的除數是0時引發該異常。 |
EXCEPTION_FLT_INEXACT_RESULT | 0xC000008F | 浮點數操作的結果不能精確表示成小數時引發該異常。 |
EXCEPTION_FLT_INVALID_OPERATION | 0xC0000090 | 該異常表示不包括在這個表內的其它浮點數異常。 |
EXCEPTION_FLT_OVERFLOW | 0xC0000091 | 浮點數的指數超過所能表示的最大值時引發該異常。 |
EXCEPTION_FLT_STACK_CHECK | 0xC0000092 | 進行浮點數運算時棧發生溢出或下溢時引發該異常。 |
EXCEPTION_FLT_UNDERFLOW | 0xC0000093 | 浮點數的指數小於所能表示的最小值時引發該異常。 |
EXCEPTION_ILLEGAL_INSTRUCTION | 0xC000001D | 程序企圖執行一個無效的指令時引發該異常。 |
EXCEPTION_IN_PAGE_ERROR | 0xC0000006 | 程序要訪問的內存頁不在物理內存中時引發的異常。 |
EXCEPTION_INT_DIVIDE_BY_ZERO | 0xC0000094 | 整數除法的除數是0時引發該異常。 |
EXCEPTION_INT_OVERFLOW | 0xC0000095 | 整數操作的結果溢出時引發該異常。 |
EXCEPTION_INVALID_DISPOSITION | 0xC0000026 | 異常處理器返回一個無效的處理的時引發該異常。 |
EXCEPTION_NONCONTINUABLE_EXCEPTION | 0xC0000025 | 發生一個不可繼續執行的異常時,如果程序繼續執行,則會引發該異常。 |
EXCEPTION_PRIV_INSTRUCTION | 0xC0000096 | 程序企圖執行一條當前CPU模式不允許的指令時引發該異常。 |
EXCEPTION_SINGLE_STEP | 0x80000004 | 標誌寄存器的TF位爲1時,每執行一條指令就會引發該異常。主要用於單步調試。 |
EXCEPTION_STACK_OVERFLOW | 0xC00000FD | 棧溢出時引發該異常。 |
(3) OS 的異常處理方式
同一程序在正常運行與調試運行時表現處的行爲動作是不同的
- 正常運行時的異常處理方法
進程運行過程中若發生異常,OS會委託進程處理.若進程代碼中存在具體的異常處理(如SEH異常處理器)代碼,則能順利處理異常,程序繼續運行.
若沒有相關的SEH,則就無法處理,OS會啓動默認的異常處理機制,終止進程運行.
(SEH處理代碼和默認的異常處理機制中均可以加入反調試代碼)
- 調試運行時的異常處理方法
調試運行中發生異常時,OS會首先把異常拋給調試器(調試器就會暫停運行)
調試時遇到異常的幾種處理方法:
(a) 直接修改異常有關的代碼,寄存器,內存.(如用NOP填充異常代碼)
(b) OllyDbg中的Shift+F7/F8/F9,將異常拋給被調試進程
(c ) 終止調試進程,終止調試
1.2 SEH詳細說明
(1) SEH鏈
SEH以鏈表的形式存在.鏈中存在一系列異常處理器.遇到異常先由第一個異常處理器處理,若第一個異常處理器未正常處理異常,則交由鏈上的第二個異常處理器,直到得到處理.(最後一個異常處理器會終止進程)
代碼層面: SEH是由_EXCEPTION_REGISTER_RECODE
結構體構成的鏈表
_EXCEPTION_REGISTER_RECODE
聲明:
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
PEXCEPTION_REGISTRATION_RECORD Next;//指針,指向下一個_EXCEPTION_REGISTER_RECODE結構體
PEXCEPTION_DISPOSITION Handler;//異常處理函數(異常處理器)地址
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
若Next成員的值爲0xFFFFFFFF,則表示它是鏈表最後一個節點.
(2)異常處理函數
定義:
EXCEPTION_DISPOSITION _except_handler
(
EXCEPTION_RECORD *pRecord,
EXCEPTION_REGISTRATION_RECORD *pFrame ,
CONTEXT *pContext,
PVOID pValue
);
- _except_handler 第一個參數 *pRecord 指向 EXCEPTION_RECORD 結構體的指針
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //異常代碼的值(如EXCEPTION_ACCESS_VIOLATION 就是0xC0000005)
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; //發生異常的代碼地址
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
注意ExceptionCode(第一個參數)和ExceptionAddress(第四個參數即可)
- _except_handler 第三個參數*pContext指向Context結構體
CONTENT 線程結構體
typedef struct _CONTEXT
{
DWORD ContextFlags;
DWORD Dr0; //04h
DWORD Dr1; //08h
DWORD Dr2; //0Ch
DWORD Dr3; //10h
DWORD Dr6; //14h
DWORD Dr7; //18h
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs; //88h
DWORD SegFs; //90h
DWORD SegEs; //94h
DWORD SegDs; //98h
DWORD Edi; //9Ch
DWORD Esi; //A0h
DWORD Ebx; //A4h
DWORD Edx; //A8h
DWORD Ecx; //ACh
DWORD Eax; //B0h
DWORD Ebp; //B4h
DWORD Eip; //B8h
DWORD SegCs; //BCh
DWORD EFlags;//C0h
DWORD Esp; //C4h
DWORD SegSs; //C8h
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; //512bytes
} CONTEXT;
多線程環境下,每一個線程內部都有一個CONTEXT結構體用來備份CPU寄存器的值.
CPU離開當前線程去其他線程時,CPU寄存器的值就會保存到當前線程的CONTEXT結構體
CPU再次運行該線程時,會使用保存在CONTEXT結構體中的值來覆蓋CPU寄存器的,從EIP處開始繼續執行
異常發生時,執行異常代碼的線程就會終端運行,轉而運行SEH.
此時OS會把當前線程的CONTEXT結構體傳遞給異常處理函數.
異常處理函數可以修改CONTEXT.Eip的值(偏移B8),設置爲其他的地址.這樣,之前暫停的線程會執行新設置的EIP地址處的代碼.(反調試中可以採用這條技術)
- _except_handler返回值
typedf enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution=0,//繼續執行異常代碼,從發生異常處的代碼繼續運行
ExceptionContinueSearch=1,//使用下一個異常處理器 將異常派送給下一個SEH鏈的異常處理器
ExceptionNestedException=2,//在OS內部使用
ExceptionCollidedUnwind=3//在OS外部使用
}EXCEPTION_DISPOSITION;
(3) 訪問SEH鏈 - TEB.NtTib.ExceptionList
通過TEB結構體的NtTib訪問SEH鏈:
TEB.NtTib.ExceptionList是TEB結構體第一個成員 即位於FS:[0]處
TEB結構體:
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
... ... ...
... ... ...
typedef struct _NT_TIB{
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //TEB.NtTib.ExceptionList
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union {
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;
(4) SEH安裝
C語言中使用__try,__except,finally
彙編中:
push @myhandler ;壓入異常處理器的地址 handler
push DWORD PTR FS:[0] ;SEH鏈的頭部 next
mov DWORD PTR FS:[0]:ESP ;向鏈表中添加該異常處理器
新添加的異常處理器將取代原來鏈表中的第一個位置成爲第一個異常處理器
這裏DWORD PTR FS:[0]是next (原來的第一個異常處理器地址,安裝了新的SEH後變爲第二個)
1.3 調試seh.exe
- 運行到0x401000(斷點處)
-
0x401000-0x40100C 爲SEH的安裝 handler地址爲0040105A
-
F7運行到00401005查看FS:[0]內存處
- 0019FF60處就是SEH鏈的開始(先壓入Handler的地址再壓入next)
- 下面爲未安裝我們的SEH異常處理器的時候的SEH鏈(編譯器,操作系統產生)
- 安裝的SEH異常處理器(接上了原來的鏈)
- 繼續跟蹤,有非法訪問異常
-
運行到此處時在0040105A(第一個SEH處理器處下斷點),然後Shift+F7 運行到SEH代碼處
-
異常處理器代碼
- 前面提到異常處理函數有四個參數,分別來看一下前三個參數
-
第一個參數 : ESP+4處 : 是一個指向EXCEPTION_RECORD 結構體的指針
-
這個結構體第一個參數爲異常值,第三個爲發生異常代碼的地址
- 異常值:C0000005 異常代碼地址:00401019
- 再看第二個參數ESP+8處:0019FF24
-
雖然之前沒有提到第二個參數,這個參數值爲0019FF60, 正是當前FS:[0]的位置
-
第三個參數:*pContext指向CONTEXT的指針0019FA44
-
CONTEXT.Eip的位置0019FA44+B8=0019FAFC
- 裏面正好是 00401019 (觸發異常處)
- 三個參數瞭解之後再看異常調試器代碼
- 這裏SS:[ARG.3]爲異常處理函數第三個參數,取異常處理函數第三個參數(CONTEXT指針)到ESI
- 這裏FS:[30]爲PEB的位置,取地址到EAX
- PEB偏移0x02處爲BeingDebugged , 該值在調試狀態下爲1.
- 與1對比看是否在調試狀態.
- 處於調試狀態則不跳轉.修改DS:[ESI+0B8]處值爲00401023(CONTEXT.Eip)
- 未調試狀態跳轉,在00401076處修改CONTEXT.Eip值爲00401039
-
這兩處剛好是兩種處理方式 , 這裏只是彈個框,實際過程中爲別的指令(干擾調試)
-
再看XOR EAX,EAX(EAX爲返回值) ,返回0 與前面異常處理函數返回值匹配
-
然後是SEH函數的刪除
1.4 OllyDbg中針對SEH的設置
- 可以在此讓調試器選擇忽略一些SEH,即交給被調試進程處理
- 但是SEH中有反調試手段的話也不可忽略,必須調試異常處理代碼
2 Time Checking
通過比較一段代碼的運行時長是否正常來判斷是否處於反調試狀態
2.1 時間測量方法
常用的有如下兩種方法:
-
利用CPU的計數器
RDTSC
kernel32! QueryPerformanceCounter( ) / ntdll !NtQueryPerformanceCounter( )
kernel32! GetTickCount ( )準確度:RDTSC > NtQueryPerformanceCounter( ) > GetTickCount ( )
-
利用系統的實際時間
timeGetTime()
_ftime()
2.2 RDTSC
- x86CPU中存在一個名爲TSC(Time Stamp Counter)64位寄存器, TSC保存着精確的時鐘週期計數
- RDTSC是一條彙編指令,用來將TSC的值讀入EDX:EAX 寄存器(高32位EDX,低32位EAX)
示例DynAD_ RDTSC.exe
- 在0x401000下斷點運行到用戶代碼
- 在0x40101C處是第一次RDTSC指令,0x40102A 第二次
- 代碼部分先比較了高32位,再比較了低32位
- 即若前後兩次計數器差大於FFFFFFFF則屬於異常(高32位比較)
- 大於FFFFFF也異常(低32位比較)
- 大於FFFFFFFF直接跳轉到0040103E ,大於FFFFFF也會執行0040103E處指令
-
0040103E 處非法訪問異常,程序會中止
-
破解之法:
- 直接run過去這段(F9)
- 修改指令 如:
3 陷阱標誌TF
原理
- TF是EFLAGS上面的第九個比特位
- TF設置爲1時,CPU將進入單步模式,單步模式下,CPU執行一條指令就會觸發一個EXCEPTION_SINGLE_STEP異常
- 實際上用到了前面的SEH反調試技術
- 可以在SEH異常處理器中使用反調試技術,修改Context.EIP的值
示例DynAD_SingleStep.exe
- 0x401000下斷點運行到用戶代碼
- 這裏調試器下TF位始終爲0
- 就需要設置忽略單步異常
- 忽略單步異常 在SEH處理函數下斷點,F9運行
- 調試器在單步和未忽略單步異常的情況下會自動設置TF爲0
- 這裏修改了Context.Eip,retn後直接到了卸載SEH異常處理器的代碼
4 INT 2D
原理
INT 2D原爲內核模式中觸發斷點異常的指令,在用戶模式下也會觸發異常.
但在調試程序時,僅僅忽略下一條指令的第一個字節而不會觸發異常
也就是調試模式下,線程的SEH不會觸發,通過這個進行反調試
示例:DynAD_INT2D.exe
- 0x40100下斷點
- 首先在0x40100B處安裝了SEH異常處理程序
- 在0x40101E處有INT 2D指令
- 正常情況下,執行INT 2D應該觸發異常,進入SEH,但調試器下並不會
- SEH代碼:
-
修改了CONTEXT.Eip值爲00401044,然後返回0
-
正常情況下從SEH出來將會直接從00401044繼續執行,而跳過了00401021處的
MOV DWORD PTR SS:[EBP-4],1
-
而下面有判斷會比較此處
- 進而造成兩種代碼執行路徑,達到反調試目的
破解之法:
修改SS:[EBP-4]處的值
或者int 2D 改爲int 3
或修改跳轉等
選擇改爲int 3,要讓調試器忽略此處的異常
就可以進入SEH
5 0xCC探測
原理
0xCC 是 int 3的機器碼,調試下斷點其實就是將 該位置的指令修改爲0xCC
可以通過探測異常的0xCC 指令,來判斷程序是否處於調試狀態
有以下兩種方法
- 探測API斷點
調試過程中爲了方便經常會用到API斷點
獲取某某API起始第一個字節是否爲0xCC 即可判斷是否處於反調試狀態
破解之法:避開在API的第一條指令下斷點
- 比較校驗和
記錄下某個地址區域指令的校驗和,然後進行比較
如: DynAD_Checksum.exe
- 這裏ECX=401070-401000 , 從ESI= 401000 開始循環,每循環一次ESI加1,按字節計算從401000到401070的校驗和
- 校驗和儲存在EAX,與記錄的校驗和比較
- 這裏因爲在校驗區域下了斷點.前後不一致
-
若不相等,SS:[LOCAL.1]會被設置爲1,在後面進行條件判斷 跳轉
-
破解:修改jmp指令等方法 如: