HOOK相關原理與例子
- 2020 年 4 月 3 日
- 筆記
消息HOOK
原理:
1. 用戶輸入消息,消息被放到系統消息隊列。
2. 程序發生了某些需要獲取輸入的事件,就從系統消息隊列拿出消息放到程序消息隊列中。
3. 應用程序檢測到有新的消息進入到程序消息隊列中後,調用相應的事件去處理該消息。
所以在系統消息隊列與程序消息隊列的中間安裝hook,即可獲取消息隊列中的信息。
安裝:
SetWindowsHookEx(鍵盤消息(WH_xxx),Hook函數(處理鍵盤輸入的函數),句柄(hook函數所在的DLL的句柄),線程ID(要hook的線程ID,0為所有線程))
API在簡單高效的同時也有一個弊端,就是它只能監視較少的消息,如:擊鍵消息、鼠標移動消息、窗口消息。
SEH(調試)HOOK
原理:與調試器工作方式類似,讓進程發生異常,然後自己捕獲到異常,對於除於被調試狀態下的級進行操作。
1. 正常情況下,進程未被其他進程調試時,當進程發生異常事件,系統將捕獲該事件,並進行事件處理。
2. 當進程被其他進程調試時,處理該進程的異常事件的工作則交給了調試進程。(調試進程未處理或不關心的調試事件由系統處理)
3. 調試HOOK的核心思路就是將API的第一個位元組修改為0xCC(INT 3,留給調試工具的中斷,調試工具運行完後,會將下一條指令手動替換回原先的代碼),當API被調用時,由於觸發了異常,控制權就被轉交給調試器(調試進程)。
利用調試技術來HOOK API函數的相關步驟如下
· 1對想要鉤取的進程進行附加操作,使之成為被調試者。
· 2將要鉤取的API的起始地址的第一個位元組修改為0xcc(或者使用硬件斷點)。
· 3當調用目標API的時候,控制權就轉移到調試器進程。
· 4執行需要的操作。
· 5脫鉤,將API 函數的第一個位元組恢復。
· 6運行相應的API。
注入HOOK
原理:Hook的核心思想就是修改API的代碼,使用DLL注入技術,我們將Hook的代碼寫入一個DLL(或直接一個shellcode),將此DLL注入到目標進程中,此時因為DLL在目標進程的內存中,所以就有權限直接修改目標進程內存中的代碼了。
shellcode:填充數據,利用軟件漏洞而執行的代碼,可在暫存器eip溢出後,塞入一段可讓CPU執行的shellcode機器碼,讓電腦可以執行攻擊者的任意指令。
API HOOK:又分為IAT HOOK和inline HOOK,都是改變函數以達到跳轉到自己的HOOK API的目的。但又有差別。
一、
1.IAT HOOK:通過修改IAT(導入表)的函數地址,實現對API進行HOOK。在把原函數替換成目標函數,並在目標函數執行完後,必須要調用回原函數(HOOK前應保存原函數地址),這樣才能保證功能的完整性。
· 1.計算出導入表的位置
· 2.在導入表中找到原函數的位置(即將被執行的函數),保存該函數的地址
· 3.將原函數地址改為目標函數,運行完目標函數後,調用回原函數。
註:該操作會將程序中所有調用被hook的函數變為調用hook後的函數,可加判斷,如果為自己調用操作,則進行hook函數的處理,如果是系統調用,直接將數據調用給原函數去處理
2.inline HOOK:直接修改內存中任意函數的代碼,將其劫持至Hook API。同時,它比IAT Hook的適用範圍更廣,因為只要是內存中有的函數它都能Hook,而後者只能Hook IAT表裡存在的函數(有些程序會動態加載函數)。
inline HOOK的目標是系統函數,直接修改函數的前5位元組,改為jmp目標函數地址,執行完後進行unhook(脫鉤)操作,以便將原函數恢復。Hook的目的是當調用某個函數時,我們能劫持進程的執行流。現在我們已經劫持了進程的執行流,便可以恢復原函數代碼,以便我們的惡意代碼可以正常調用。
· 1.獲取原API地址,保存起來(方便後續還原)
· 2.修改內存屬性為RWX(即read讀(編號4)write寫(2)execute執行(1))
· 3.備份原代碼(同1)
· 4.實時計算JMP的相對偏移
· 5.最後修改API前5位元組的代碼(跳轉到目標函數地址)
· 6.恢復內存屬性。
註:
自編inline hook:從hook的位置跳到自己的函數中執行想要的動作,但需要將原指令再執行一遍,再跳轉回到被hook的位置+指令長度的位置。以達成棧平衡的目的。
如果hook的位置為跳轉指令,則需要將該指令所跳轉的目標地址給計算並保存起來(普通跳轉指令(非FF15 FF25 push時),跳轉的為偏移量,一旦hook了偏移量就不再適用了),並在自己的函數中跳轉到目標地址。
mhook庫:編寫一個參數 返回類型與被HOOK函數一樣的新函數,hook後,代替原函數被調用,注意該hook只能hook一整個函數,並且會將所以調用原函數的行為用於調用新函數,因此應該加個判斷條件,判斷是系統進行調用還是我們要hook的程序進行調用。
二、
HotFix HOOK
原理:Code Hook存在一個效率的問題,因為每次Code Hook都要進行“掛鈎+脫鉤”的操作(對API的前5位元組修改兩次),當要進行全局Hook的時候,系統運行效率會受影響。而且,當一個線程嘗試運行某段代碼時,若另一個線程正在對該段代碼進行“寫”操作,會程序衝突,最終引發一些錯誤。
API的起始代碼上都有這樣的特點,5個NOP(空)指令,1個“MOV EDI,EDI”(佔2位元組),這7位元組的指令實際沒有任何意義,因此可以通過修改這7位元組來實現HOOK操作,這種方法可以使得進程處於運行狀態時臨時更改進程內存中的庫文件,因此被稱為打“熱補丁”。
在上述5位元組代碼修改技術中,unhook(脫鉤)是為了調用原函數,但使用HotFix HOOK API時,在API代碼被修改的狀態下仍然能夠正常的調用原API(從[原API起始地址+2]開始,仍能正常調用原API,且 執行動作一致)。
· 1.將內存屬性修改為RWX
· 2.計算HOOK函數與被HOOK函數之間的地址偏移
· 3.將JMP [得到的結果]寫入原函數-5的位置(即5個NOP)
· 4.再將JMP-7寫到原函數的位置(MOV EDI,EDI)
· 5.恢復內存屬性。
由於HotFix Hook需要修改7個位元組的代碼,所以並不是所有API都適用這種方法,若不適用,請使用5位元組代碼修改技術。
SSDT HOOK
原理:SSDT Hook屬於內核層Hook,也是最底層的Hook。由於用戶層的API最後實質也是調用內核API(Kernel32->Ntdll->Ntoskrnl),所以該Hook方法最為強大。
SSDT(System Service Descriptor Table):系統服務描述符表
定義類型變量:
DD(Define Dword):雙字類型,一個雙字數據佔4位元組。DW:字類型,佔2位元組。DB:位元組類型,佔1位元組
內核通過SSDT調用各種內核函數,SSDT就是一個函數表,只要得到一個索引值,就能根據這個索引值在該表中得到想要的函數地址。
SSDT所在地址後面的第一個32位數據即為SSDT的基地址,跳到基地址後,第一個位32位數據即為SSDT表中第一個函數的地址,對該地址反彙編後,就能得到該函數相關的信息(包括該函數的索引號)。
例:要找AABBCC函數的地址,先對該函數進行反彙編u nt!ZwAABBCC,得到它的索引號為0x12;那麼它的地址為:基地址+0x12 對其反彙編後,即可得到該函數的詳細信息。
· 1.修改內存屬性為RWX(即read讀(編號4)write寫(2)execute執行(1))
· 2.實時計算JMP的相對偏移
· 3.備份原代碼頭5位元組(同1)
· 4.將頭5位元組替換成2.的彙編碼
· 5.運行完後還原頭5位元組
· 6.恢復內存屬性。
例子及博主自身理解:
API HOOK——IAT HOOK實例
技巧:
》通過模塊句柄,得到PE頭:(PBYTE)PE頭=(PBYTE)進程句柄
》通過PE頭,獲得dos頭:PIMAGE_DOS_HEADER dos頭=(PIMAGE_DOS_HEADER)PE頭
》通過PE頭和dos頭,獲得NT頭 PIMAGE_NT_HEADERS NT頭=(PIMAGE_NT_HEADERS)(PE頭+ dos頭->e_lfanew)
》通過NT頭的結構成員的數組成員,獲得導入表信息:
IMAGE_DATA_DIRECTORY 數據表=NT頭->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
導入表的尺寸=數據表.size 起始地址(基址)=數據表.VirtualAddress
》 獲取函數的地址(DWORD),GetProcAddress(GetModuleHandleA(DllName),ProcName)
》通過PE頭+導入表基址定位到導入表:PIMAGE_IMPORT_DESCRIPTOR 導入表 = (PIMAGE_IMPORT_DESCRIPTOR)(PE頭 + 導入表基址);
》通過對比DLL名 循環查找每個導入表,找到DLL所在的結構體
strcmp((char*)DLL名,(char*)(PE頭+導入表-name))找到目標DLL所在結構體; PIMAGE_THUNK_DATA 結構體= (PIMAGE_THUNK_DATA)(PE頭 + 導入表->FirstThunk);
》通過對比函數地址,循環查找每個結構體的u1.Function成員,找到目標函數結構體->u1.Function == (DWORD)要被替換的函數地址
!!切記,應在32位下的Release版本生成dll,否則替換新的函數地址處會出錯
》找到目標函數位置後,替換成新的函數的地址,pthunk->u1.Function = NewFuncAddress;
然後將位置(這位置保存的是地址)保存起來,方便事後還原。(保存至全局變量)
DWORD g_dwIatAddr = (DWORD)&pthunk->u1.Function;(<-保存一個指針指向的變量所保存的地址)
事後還原:DWORD * pdwAddr = (DWORD*)g_dwIatAddr;//把舊地址所在的位置傳遞給一個指針
*pdwAddr = oldFuncAddress; //將舊地址放入該位置 達到還原
注意,每次更改導入表的函數地址時,都應該調用VirtualProtect來修改保護屬性
VirtualProtect((LPVOID)&pthunk->u1.Function, 4, PAGE_EXECUTE_READWRITE, &oldprotect);
通過REG注入DLL (即通過註冊表HOOK)
平時用程序來進行DLL注入時,需要被注入程序在運行起來之後才能進行注入,在某些初始化階段就想通過HOOK來控制程序時,達不到效果。此時,我們可以通過註冊表表進行注入,被注入的程序在運行前就已經被HOOK。註:被注入的進程與DLL必須同為32/64位。
REG注入原理:利用在Windows 系統中,當REG以下鍵值中存在有DLL文件路徑時,會跟隨EXE文件的啟動加載這個DLL文件路徑中的DLL文件。當如果遇到有多個DLL文件時,需要用逗號或者空格隔開多個DLL文件的路徑。
1.通過WIN+R運行regedit打開註冊表
2.分別打開:HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionWindows
HKEY_LOCAL_MACHINESOFTWAREWow6432NodeMicrosoftWindows NTCurrentVersionWindows
把路徑下的AppInit_DLLs改為將要注入的DLL路徑;
3.並將LoadAppInit_DLLs註冊表項的值修改為1:(64位改2個 32位改1個)
註:注入表注入的DLL由mHOOK生成,因為注入表HOOK會給所有進程HOOK
所以要加入限制條件:獲取當前進程名,當進程名=目標進程名時,往下執行HOOK的操作
自寫inline hook
1.自定義一個全局變量 讓hook只運行一次
2.通過進程IDGetProcessId(HANDLE(-1))得到當前進程ID 和程序名(自定義),獲得目標進程的模塊入口地址(即HOOK進程的基地址)(要HOOK的地方屬於哪個模塊,就獲取哪個模塊的句柄)
2/1.直接GetModuleHandle(“進程名”);獲得模塊入口地址或者通過下面4個步驟
2-1.根據進程ID獲得進程快照,該快照包含所有包含該進程ID的模塊 CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
2-2.通過該快照,獲得與該進程相關的第一個模塊的信息 Module32First(2-1返回的句柄, &存放模塊信息的結構)
2-3.通過對比,找到目標模塊srcmp(存放模塊信息的結構.szModule, 進程名(即1的程序名)) == 0) ,如果不匹配,Module32Nex(2-1返回的句柄, &存放模塊信息的結構)查看下一個。(進程名可通過任務管理器-詳細信息查看)
2-4.找到目標模塊,返回模塊句柄存放模塊信息的結構.hModule
3.得到要替換的原指令的絕對偏移位置RVA=VA-IB(要被替換的第一條指令—模塊入口地址) (可通過MDebug查看,如要HOOK的為自定義函數,則該模塊名稱為進程名.exe,如果要HOOK的為系統函數,則模塊名為該系統函數所在的DLL)(找到合適的HOOK位置是逆向的關鍵)
4.得到當前要hook的指令所在位置。原指令的絕對偏移地址(3得到的)+基地址(不定,每次加載時都容易改變,所以要通過2來求得)
5.保存等下要返回的地址 返回地址= 當前要hook的指令所在位置+hook指令的長度(5位元組或以上,替換掉了多長就加多長)
6.保存原始指令內容UCHAR szOldInstruct[指令長度]={指令內容},指令內容為從左到右從上到下保存。 如不還原,則可不保存
7.計算跳轉位置。DWORD 跳轉位置 = (DWORD)自己編寫函數(彙編編寫)- 要hook的指令的起始位置- 指令所佔長度;這樣跳轉後即能略過原本指令,執行自定義指令(遠跳轉指令e9佔5位元組,近跳轉EB佔2位元組,跳轉的位置為偏移量)
偏移量=目標地址 – 指令地址 – 指令長度
8.編寫跳轉指令UCHAR 跳轉指令[5] = { 0xe9, 0, 0, 0, 0 };(e9為跳轉指令jmp),如果指令長於5位元組,則在地址後填充nop空指令(0x90)
也可定死為5位元組,因為跳轉指令由e9+4位元組地址組成,而返回地址自動跳過了該指令的位置,所以只改前面5位元組數據亦可
memcpy(跳轉指令 + 1(e9佔1位), &(3得到的), sizeof(3得到的));
9.修改要hook的指令所在位置的保護屬性 VirtualProtect((LPVOID)(4得到的),, 5, PAGE_EXECUTE_READWRITE, &保存舊屬性的變量);
10.將要hook的指令替換成要跳轉的指令 memcpy((LPVOID)(4得到的), (8得到的), sizeof((8得到的)));
11.還原指令所在位置的保護屬性 此時inline hook完成
註:跳轉指令所指的函數必須為彙編指令編寫
通過上面例子生成DLL後,通過注入來HOOK實例
在生成完DLL後(具體參照前面例子),如有.lib文件 需拉到同一目錄並加載
_T在項目定義了UNICODE時,等價於L,即(_T”123″)同理(L”123″); 否則就是多位元組。
在數組中 TCHAR如果定義了寬位元組UNICODE TCHAR就為wchar_t 否則為char
DLL所在地址可用UnICODE,方便處理漢字這種雙位元組字符。
1.搜索指定類名或窗口名(窗口標題)findwindow(類名,窗口名);,得到窗口句柄。
2.GetWindowThreadProcessId(窗口句柄,&保存ID的變量);得到該進程的ID。
3.OpenProcess(PROCESS_ALL_ACCESS,FALSE,進程ID);得到該進程的句柄
4.VirtualAllocEx(進程句柄,內存地址,內存大小,分配方式(MEM_COMMIT),權限(PAGE_READWRITE));在該進程中申請一個內存空間(用於打開DLL)
5.將DLL路徑寫入對方進程中
WriteProcessMemory (進程句柄(3),內存位置(4),DLL所在地址(絕對路徑),路徑所佔空間,&寫入位元組數)
注意,此時的路徑含有漢字,用的為寬字符聲明:L”c\xxx”,所以可用wcslen( )求得長度,長度*2+2(結束符) 即得到寬字符路徑所佔空間
6.創建遠程線程,讓目標進程調用LoadLibrary來打開注入的DLL
CreateRemoteThread(進程句柄,安全屬性(NULL),線程大小(0,即默認),目標線程調用的函數名(函數名即函數起始位置),
傳遞給函數的參數(此處為4申請的空間,該空間存放着DLL的路徑),線程創建標誌(NULL),線程ID的指針(不需要保存則為NULL) )
這裡傳遞的函數名為LoadLibrary 用來加載DLL
7.檢測句柄的信號狀態WaitForSingleObject (線程句柄(6),等待時間(-1無限));當參1現場為有信號狀態,或者到了參2時間,該函數返回。否則掛起
8.釋放目標進程的空間VirtualFreeEx(進程句柄(3),空間首地址(4),空間大小(與申請時一致),釋放類型(MEM_DECOMMIT));
注意:可直接跳到步驟3開始執行 進程ID從任務管理器-詳細信息-目標進程的PID中獲得
小技巧:如果想實現嵌套hook 應該保存被hook的函數指令,然後在hook的函數裏面 用函數再次實現該指令,如果為jmp指令,則需獲得跳轉指令後接的4位元組偏移地址 :因為跳轉指令後接的為偏移量,而hook位置的地址為該指令所在的地址,並非偏移量
具體操作如下:1.memcpy(g_szOldInstruct, (char*)(HOOK的位置), 5); //將hook位置的指令保存下來(指令多長就複製多長 如果是jmp的話一般都是5位元組E9 或2位元組EB)
2.lea eax, dword ptr[g_szOldInstruct]; //取出這行指令的值(為E9 XXXX XXXX)
3 add eax, 1; mov eax, dword ptr[eax]; //+1為跳過e9,然後取4位元組則為取出跳轉的偏移地址
得到了偏移地址 就可以求得被hook的指令所要實現的跳轉位置 跳轉的目標地址=偏移地址+跳轉指令長度+跳轉指令所在的位置(即被hook前 這行指令所在的位置)
調試DLL是否執行成功,可在生成DLL的函數的代碼段添加__asm int 3;來進行調試
在下斷點後,如果運行起來一直會回到斷點處,說明系統在該程序運行時會一直調用斷點處的函數,此時應在hook函數前加判斷條件,如果是自己操作的HOOK下來,如果是系統調用的,就調用原函數