Windows安全 之 向量化異常處理機制(Vectored Exception Handling, VEH) 之Hook

本文需讀者具備基礎的匯編知識,調用約定知識, 以及基本逆向概念

提到Windows異常處理機制, 可分兩大類, 一類是向量化異常處理(VEH), 一類是結構化異常處理(Structured Exception Handling, SEH), 由於在Windows中, VEH 是除雙機調試及調試器外, 最優先處理的函數, 因此本文先集中解釋VEH

很多人編程時都不會使用異常機制, 因為當正常運行時, 異常根本沒有作出任何用途。

但當異常出現在我們預計之內, 異常處理函數就變得十分強大。

本文分兩大部份
1. 硬件斷點
2. VEH的使用與分析


1. 硬件斷點

這個在網上很多文章講得十分詳盡, 我這裡輕輕帶過。
每個線程中, 都有自己一套的CPU寄存器, 稱為上下文(CONTEXT) 結構如下:
typedef struct _CONTEXT {

//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//

DWORD ContextFlags;

//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//

DWORD   Dr0;
DWORD   Dr1;
DWORD   Dr2;
DWORD   Dr3;
DWORD   Dr6;
DWORD   Dr7;

//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//

FLOATING_SAVE_AREA FloatSave;

//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//

DWORD   SegGs;
DWORD   SegFs;
DWORD   SegEs;
DWORD   SegDs;

//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//

DWORD   Edi;
DWORD   Esi;
DWORD   Ebx;
DWORD   Edx;
DWORD   Ecx;
DWORD   Eax;

//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//

DWORD   Ebp;
DWORD   Eip;
DWORD   SegCs;              // MUST BE SANITIZED
DWORD   EFlags;             // MUST BE SANITIZED
DWORD   Esp;
DWORD   SegSs;

//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//

BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;
typedef CONTEXT *PCONTEXT;


每當線程被調度時, 線程會刷新一次CPU寄存器的值,以供使用, 未被調度時,就會保存起來。供下次調度使用, 線程輪詢就是這樣的一個操作。

我們得到了線程的句柄(HANDLE) , 可以透過它調用GetThreadContext(HANDLE hThread, CONTEXT &context) 獲取對應於該線程的上下文。
而在GET之前我們應該通過SuspendThread(HANDLE hThread) 掛起線程, 再設定好context的ContextFlags 為我們想要的寄存器 防止寄存器出錯
我們在獲取後, 可以透過SeThreadContext(HANDLE hThread, CONTEXT &context) 設定上下文, 再調用ResumeThread(HANDLE hThread) 恢復線程。


我們這次要說的是, Debug Register調試寄存器, 一共有八個, DR0 到DR7 , 他們都是負責一個任務, 就是下斷點(breakpoint) , 什麼是breakpoint 不明白可以百度一下:)
DR到DR3 負責紀錄進程中需要下斷點的地址,
DR4,DR5 暫未使用, 如使用自動調配到Dr6,7

Dr7激活斷點, 設置斷點屬性(讀,寫,執行)
Dr7設置如下, 下圖為<64位微處理器系統編程>



例如我需要對Dr0 寄存器下執行硬件斷點, 可以把Dr7設為0x1

2.VEH 向量化異常處理

這個異常用途極之簡單,就兩個函數
PVOID WINAPI AddVectoredExceptionHandler(
  _In_  ULONG FirstHandler,
  _In_  PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
ULONG WINAPI RemoveVectoredExceptionHandler(
  _In_  PVOID Handler
);

把Handler設定好, 直接加入鏈表 就好了

不過這次要談的是, 硬件斷點 與VEH 做成的Hook

當我們下了一個硬件斷點的時侯, Windows其實會觸發異常, 然後調用按次序 找異常處理函數 首先是雙機調試->調試器->向量化異常處理函數(進程相關)->結構化異常處理函數(線程相關)-> TopLevalEH

當異常調用我們的函數時 如以下函數:
LONG NTAPI TestVeh(PEXCEPTION_POINTERS ExceptionInfo){
/**/
  DWORD dwRet = EXCEPTION_CONTINUE_EXECUTION;
   if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0x80000004){  
     if (ExceptionInfo->ExceptionRecord->ExceptionAddress == (PVOID)0x54DF60){
           ExceptionInfo->ContextRecord->Eip = (DWORD)testing;  
       return -1;
     }
   }
  return dwRet; 
}


如上, 我們可以透過ExceptionInfo 獲取線程相關和異常相關的資訊 , 並透過修改EIP 使代碼到testing處, 這裡開始要十分的小心 我們來逆向分析一下
我們的HOOK函數不能被定義為_stdcall 或其他調用約定, 我們必需要把函數定義為_declspec(naked) 告訴compiler 我們不需要添加任何 包括函數頭的匯編代碼到我們函數。 這個十分十分重要, 我們要做的不是寫一個新函數, 而是作為一個中間代碼,插入目標函數中, 必需保持維護堆棧, 一旦堆棧被破壞, 整個程序就極大可能會導致崩潰。

以下解釋為什麼堆棧會被破壞

首先硬斷觸發, 進入異常處理函數, 我們可以首先發現, EIP 其實並沒有立刻被修改, 而是把他保存在context中的+0xB8位置, 是EIP的位置, 可是線程不會立刻把這個CONTEXT更新, 而是經過以下一連串驗證, 如異常返回值是否-1 如果是才算被處理完成, 否則再需要繼續分發異常到其他異常函數。
圖1:




圖2:





圖3:






圖4:







圖5:







圖6:






圖7:






以上都是一直跳轉和異常函數返回後系統的一些驗證,如果是處理好的異常(返回值為-1)才會來到這裡

這一步開始,就是我們解釋,為什麼我們不能定義一般的函數約定(_stdcall)作為hook函數, 由圖8函數進入後(圖中的函數頭)直至圖9程序把EBP作調用hook函數前的最後一次修改,

然後把兩個參數TRUE/FALSE 和 Context 地址壓棧, 再調用ZwContinue 繼而進入內核態調用NtContinue 等函數, 表明異常已經處理, 可以繼續執行由CONTEXT中的EIP
(注意: 如果EIP沒有被修改, 將會為斷點發生的那一句地址, 而不是下一句地址),


當如果我們使用_stdcall等指令很自然編譯器會幫我們函數加上就會如圖14中所示的函數調用


圖14中表明,
一個原因, 因為這種調用約定有幾個指定動作 見圖14
push ebp
mov ebp, esp
;...........
mov esp, ebp
pop ebp
retn


以上幾句基本上是一樣的, 當直接修改EIP的時侯 而不是CALL EIP的話, 這意味調用者並沒有把EIP壓棧而直接調用函數,這是一個致命點,如果使用retn指令返回, 這個可能性必然會崩潰, 拿了不知名的地址返回

當我們使用這類調用約定, 有些人會這樣說retn是 pop eip, jmp eip 就是這道理,

當函數進入時沒有壓入返回地址, 他retn時 約定出棧的"返回地址"亦不是真正的返回地址了,


圖8






圖9:






圖10:





圖11:






圖12:






圖13:





圖14(畫得有點差):



總結而言, 當我們需要用到硬斷及VEH 進行hook函數時, 必定要把HOOK函數設為_declspec(naked) 而不是_stdcall 或其他會基於EBP 尋返回地址的值, 如stdcall 會到[ebp+0x4]中找返回地址 這是函數約定

Comments

Popular posts from this blog

Android Kernel Development - Kernel compilation and Hello World

How does Nested-Virtualization works?

Understanding ACPI and Device Tree