動态反調試
本文介紹了幾種常見的動态反調試技術,在閱讀之前您可能要了解一些靜态反調試手段.
前文 : 常見靜态反調試技術總結
本文執行個體所用資源 : https://download.csdn.net/download/weixin_45551083/12555778
文章目錄
-
- 動态反調試
-
- 1 SEH
-
- 1.0 SEH基本概念
-
- 基本概念
- 示例程式 seh.exe
- 1.1 異常
-
- (1) 幾個常見異常
- (2) 異常編碼對照表
- (3) OS 的異常處理方式
- 1.2 SEH詳細說明
-
- (1) SEH鍊
- (2)異常處理函數
- (3) 通路SEH鍊 - TEB.NtTib.ExceptionList
- (4) SEH安裝
- 1.3 調試seh.exe
- 1.4 OllyDbg中針對SEH的設定
- 2 Time Checking
-
- 2.1 時間測量方法
- 2.2 RDTSC
-
- 示例DynAD_ RDTSC.exe
- 3 陷阱标志TF
-
- 原理
- 示例DynAD_SingleStep.exe
- 4 INT 2D
-
- 原理
- 示例:DynAD_INT2D.exe
- 5 0xCC探測
-
- 原理
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指令等方法 如: