作者 | 榴蓮
編輯 | 楌橪
Windows操作系統中存在多種異常處理,我們現在需要的是其中的VEH(VectoredExceptionHandler)異常處理,也就是向量化異常處理。我們之所以可以使用VEH異常來進行HOOK的主要原因,在於兩點。其一,VEH異常處理的優先級是高於SEH異常處理的,也就是說可以先手拿到異常,確保不會被其他異常處理流程將異常截獲而導致HOOK失敗。其二就是在VEH異常處理的回調函數中,可以獲取及修改異常發生處的上下文環境,這就意味著我們可以操作的東西會非常多,例如通過上下文環境中的ESP(棧頂指針寄存器)就可以拿到HOOK位置觸發異常時的堆棧數據。而我們設置的HOOK位置通常位於函數內部的起始位置,這就意味著我們可以直接通過堆棧裡的數據獲取到被HOOK函數的參數,並且可以對其進行修改。
在我們之前的文章中,我們已經使用過利用軟件斷點(int 3 0xCC)觸發異常,實現HOOK。
但是這種方法也是有一定缺陷的。例如,如果目標進程具有CRC32一類的完整性檢查,int3 軟件斷點又會修改指令。這樣就無法通過完整性檢查了。所以,我們這一次,提出一種新的方式。依然是基於VEH異常的,但是可以實現“無痕”的效果。不修改任何一個字節就完成HOOK。那麼,這種方式就是基於硬件調試寄存器實現的。也就是硬件斷點。因為硬件斷點的地址是存儲在寄存器裡的,所以不會修改內存。
那麼在學習具體的HOOK方法之前,我們首先需要了解一下硬件斷點的基本知識:
上圖就是Intel手冊中對於調試斷點的說明圖,下面我們對其字段進行一定解釋:
DR0 - DR3就是用來保存硬件斷點的地址的,這個地址是線性地址而不是物理地址,因為CPU是在線性地址被翻譯成物理地址之前出處理斷點的,也因此,我們在保護模式內不能用調試寄存器對物理內存地址設置斷點。
DR4和DR5是保留的,如果調試擴展開啟了(CR4的DE位設置成1就是開啟了),任何對DR4和DR5的調用都會導致一個非法指令異常#UD,如果調試擴展禁用了,那麼DR4和DR5其實就是DR6和DR7的別名寄存器。
DR7寄存器是調試控制寄存器:
R/W0 - R/W3 讀寫域 四個讀寫域分別與DR0-DR3寄存器所對應,用來指定被監控地點的訪問類型。
佔兩位,所以有以下四種狀態:
00:僅執行對應斷點的時候中斷(執行斷點)
01:僅寫數據中斷(寫入斷點)
10:(需要開啟CR4的DE【調試擴展】)I/O時中斷
11:讀寫數據都中斷,但是讀指令除外(訪問斷點)
LEN0 - LEN3 長度域 四個長度域分別與DR0-DR3寄存器所對應,用來指定監控區域的長度
佔兩位,所以有以下四種狀態:
00:1字節長
01:2字節長
10:8字節長
11:4字節長
如果R/W位是00,那麼這裡應該設置成0
L0-L3 局部斷點啟用 分別與DR0-DR3寄存器所對應,對應項為1就是開啟斷點,為0就是關閉斷點,執行後自動清除該位
G0-G3 全部斷點啟用 分別與DR0-DR3寄存器所對應,對應項為1就是開啟斷點,為0就是關閉斷點,CPU不會主動清除
LE和GE 忽略即可,高版本CPU不用了,486之前才會用
GD啟用訪問檢測,如果GD是1,那麼CPU遇到修改DR寄存器的指令,會產生一條異常。
DR6寄存器是調試狀態寄存器
B0-B3 分別與DR0-DR3寄存器所對應,如果B0被置1了,那說明R/W0 len0 DR0的條件都被滿足了
BD 與DR7的GD位相關聯,當CPU發現了需要修改DR寄存器的指令,那麼就會停止執行,把BD設置成1,然後交給#DB的處理程序
BS 單步 與EFLAGS裡的TF位相關聯,如果這一位是1,則表示是單步觸發的
BT 任務切換 與任務段相關,TSS的T標志(調試陷阱)相關聯,當任務切換,發現下一個TSS的T是1,那麼就會中斷到調試中斷程序裡
了解了以上內容之後,我們開始編寫具體的無痕HOOK,以MessageBoxA的HOOK為例:
我這裡採用的操作系統是Windows 10 20H2(19042.1288),集成開發環境採用的是Visual Studio 2017。那麼我們先來創建一個DLL項目。步驟如下:
1.選擇新建項目
2:選擇Windows桌面->動態鏈接庫(DLL),點擊確定
3:注釋#include “pch.h”,添加#include <Windows.h>。刪除framework.h、pch.h以及pch.cpp文件。
4:配置
4.1 選擇屬性
4.2 修改運行庫以及Spectre緩解,選擇應用
4.3 修改預編譯頭,選擇應用
5. 在每一個分支中,添加break,防止DLL注入失敗。
6.使用AddVectoredExceptionHandler函數添加一個VEH異常的回調。VEH的回調是一個鏈表,掛著很多的處理程序。AddVectoredExceptionHandler函數的第一個參數就是用來指定新增VEH處理函數位於鏈表的哪個位置。如果第一個參數的值是0,那麼新增的VEH處理函數將處於整個鏈表的最後。如果第一個參數的值是一個非0值,那麼新增的VEH處理函數就會位於鏈表的頭部。AddVectoredExceptionHandler函數的第二個參數就是指定新增的回調函數
6.1函數的原型可以通過在AddVectoredExceptionHandler的函數上按F12,看到函數的原型。
6.2在紅框的位置上,繼續F12,就可以看到這個參數的原型,實際上就是一個回調函數的函數指針原型。
6.3然後復制出來,修改成如下樣式。去掉typedef,把指針修改成函數名,增加花括號的函數體,異常處理的部分就需要在函數體內實現。
6.4最後,將其添加到DLL_PROCESS_ATTACH中。
6. 獲取MessageBoxA的函數地址,並且保存起來
7. 這裡我們需要編寫一個HOOK函數
8. 硬件斷點和軟件斷點是有區別的,軟件斷點是修改內存,所以只需要修改一處,但是因為硬件調試寄存器是每個線程一套,所以如果需要HOOK函數而不漏接,那麼就是需要對所有線程的函數都下一個硬件斷點。具體操作方式如下:
9. 在DLL_PROCESS_ATTACH中調用HOOK函數
10. 同樣是因為硬件斷點屬於線程環境,所以當創建新線程的時候,需要調用SetThreadHook函數對新線程進行HOOK。
11. 接下來,我們需要在異常處理函數內,處理HOOK,首先判斷是不是我們自己的HOOK地址
12.處理參數的HOOK
在這部分代碼中,之所以需要將EIP + 2。是因為在x86的函數頭是如下樣式的。
也就是說,我們的斷點0xCC實際上就是改在了紅框的位置上,替換了8B,而原本的函數中,8BFF的硬編碼組成了mov edi,edi,這裡實際上別沒有什麼用,所以我們如果直接跳過這兩個硬編碼也並不會影響程序的正常執行。
12. 如果不是我們自己的HOOK地址,重新下一次HOOK,防止HOOK失效
13.生成文件
14.取出文件到桌面或其他位置
15.測試HOOK效果
15.1首先寫一個目標程序,代碼如下
15.2使用注入器(自行編寫或網上下載,這裡我用的是自己寫的)將我們生成的模塊注入到目標進程中。
正常情況下:
HOOK後:
到了這裡,我們就完成了整個無痕HOOK的代碼編寫。
關於作者
作者:rkvir(榴蓮老師)
簡介:曾任某安全企業技術總監;看雪講師;曾任職國內多家大型安全公司;參與*2國家級安全專案
擅長:C/C++/Python/x86/x64彙編/系統原理&
研究方向:二進位制漏洞/FUZZ/Windows核心安全/內網攻防