Windows核心新手上路3——掛鈎KeUserModeCallBack
1. 簡介
在Windows系統中,提供了幾種方式從R0調用位於R3的函數,其中一種方式是KeUserModeCallBack,此函數流程如下:
nt!KeUserModeCallback->nt!KiCallUserMode->nt!KiServiceExit->ntdll!KiUserCallbackDispatcher->回呼函數-> int2B->nt!KiCallbackReturn-> nt!KeUserModeCallback(調用後)這是一個 ring0->ring3->ring0的過程,在堆棧準備完畢後,借用KiServiceExit的力量回到了ring3,它的著陸點是 KiUserCallbackDispatcher,然後KiUserCallbackDispatcher從PEB中取出
KernelCallbackTable的基址,再以ApiIndex作為索引在這個表中尋找對應的回呼函數並調用,調用完之後再int2B觸發 nt!KiCallbackReturn再次進入核心,修正堆棧後跳回KeUserModeCallback,完成調用。
系統所有的訊息鉤子回調都是利用KeUserModeCallBack完成的。所以可以通過掛鈎KeUserModeCallback用來過濾對鉤子的調用。
1.1 inline hook
KeUserModeCallback沒有對應的R3調用介面,所以沒有在SSDT SHADOW中出現,需要用另外的方式來HOOK,可以採用的Inline Hook,即在函數頭部加入一條JMP指令(機器碼E9)跳入代理函數,然後過濾之後決定是否調用真實的KeUserModeCallback函數。掛鈎過程如下:
ULONG StartHookKeyUserModeCallBack() { ULONG tmp; memset (Ori_Func, 0x90, 100); // nop Ori_Func[50] = 0xE9; tmp = (ULONG)ProxyFunc - (ULONG)KeUserModeCallback - 5; memcpy(jmp_bytes+1, &tmp, 4); HeadLen = GetPatchSize (KeUserModeCallback, 5); memcpy(Ori_Func, (PVOID)KeUserModeCallback, HeadLen); //原始位元組 //中間跳 tmp = (ULONG)KeUserModeCallback + HeadLen - (ULONG)(&Ori_Func[50]) - 5; memcpy(&Ori_Func[51], &tmp, 4); // 去掉記憶體保護 __asm { cli mov eax, cr0 and eax, not 10000h mov cr0, eax } memcpy(KeUserModeCallback, jmp_bytes, 5); // 恢複記憶體保護 __asm { mov eax, cr0 or eax, 10000h mov cr0, eax sti } return TRUE; } |
代碼1 掛鈎KeUserModeCallback
代理函數ProxyFunc代碼如代碼2:
__declspec(naked) VOID ProxyFunc(VOID) { __asm { MOV EAX, ESP PUSHAD PUSH [EAX+4*5] PUSH [EAX+4*4] PUSH [EAX+4*3] PUSH [EAX+4*2] PUSH [EAX+4] LEA EAX, MyKeUserModeCallback CALL EAX TEST EAX, EAX JZ continue_exe //return STATUS_SUCCESS POPAD RETN 0x14 continue_exe: POPAD LEA EAX, Ori_Func //跳回原始函數 JMP EAX } } |
代碼2 代理函數ProxyFunc
首先調用自訂的過濾函數MyKeUserModeCallback,然後判斷傳回值是否是STATUS_SUCCESS,如果是,則調用真實的KeUserModeCallback,否則直接返回,拒絕調用R3的回調用函數。
1.2
MyKeUserModeCallback
下面分部分講解MyKeUserModeCallback對鉤子的過濾操作,其中涉及到很多未公開技術,其代碼通過逆向工程技術得到。
1.2.1 過濾WH_KEYBOARD_LL類型鉤子
在Windows作業系統中,按鍵訊息到達具體的視窗訊息佇列之前會調用WH_KEYBOARD_LL鉤子函數,可以利用此類鉤子截取鍵盤輸入。虛擬碼如代碼3所示。
判斷ApiNumber是否是KBD_LL_HOOK_API_NUM 將InputBuffer轉換為PFNHKINLPKBDLLHOOKSTRUCTMSG 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼3 WH_KEYBOARD_LL過濾
在對WH_KEYBOARD_LL的過濾過程中,還有一點需要做特殊處理。現在有很多的軟體採用WH_KEYBOARD_LL鉤子來做安全輸入控制項,而不是通過傳統的EDIT控制項,所以在返回的時候需要將虛擬鍵盤產生的虛假的按鍵修正為真實的按鍵。
1.2.2 過濾WH_KEYBOARD鉤子
在Windows作業系統中,按鍵訊息到達具體的視窗訊息佇列之前會調用WH_KEYBOARD鉤子函數,可以利用此類鉤子截取鍵盤輸入。過濾的虛擬碼如代碼4所示。
判斷ApiNumber是否是KBD_HOOK_API_NUM 將InputBuffer轉為PFNHKINLPKBDLLHOOKSTRUCTMSG結構 判斷PFNHKINLPKBDLLHOOKSTRUCTMSG結構中GeneralHookHeader成員的nCode成員的值是否為0x20000 如果是則是WH_KEYBOARD鉤子 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼4 WH_KEYBOARD鉤子過濾
WH_KEYBOARD鉤子會同其他一些回調共用一個ApiNumber,這裡要做進一步的判斷,而且全域的WH_KEYBOARD鉤子需要在其他進程中LoadLibrary。
同WH_KEYBOARD_LL鉤子,有的程式會利用WH_KEYBOARD鉤子做安全輸入控制項,在返回時需要將虛擬鍵盤產生的虛假按鍵修正為真實的按鍵。
1.2.3 WH_DEBUG鉤子過濾
在Windows作業系統中,調用其他類型的鉤子函數之前,會首先調用WH_DEBUG鉤子函數,用於鉤子調試。惡意程式利用WH_DEBUG類型鉤子可以擷取鍵盤輸入。過濾虛擬碼如代碼5所示。
判斷ApiNumber是否是DEBUG_HOOK_API_NUM 判斷此DEBUG資訊中的鉤子類型是否包含鍵盤輸入資訊 如果包含,則填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼5 WH_DEBUG鉤子過濾
同WH_KEYBOARD_LL WH_KEYBOARD類型的鉤子,有些程式會使用WH_DEBUG鉤子實現安全輸入控制項,在返回時,需要將虛擬鍵盤產生的虛假按鍵修正為真實的按鍵。
1.2.4 Ke_LoadLibrary過濾
在Windows作業系統中,除了WH_KEYBOARD_LL鉤子外,其他類型的全域訊息鉤子都需要有DLL被載入進入目標進程(WH_KEYBOARD_LL鉤子只是一次線程切換,詳細資料可以查看MSDN)。所以過濾Ke_LoadLibrary可以阻止大部分的訊息鉤子(事實上,目前所有的密碼保護程式都通過此方法過濾訊息鉤子,用來保護密碼,但是此方法對WH_KEYBOARD_LL類型的鉤子無效)。過濾虛擬碼如代碼6所示。
判斷ApiNumber是否LOAD_LIBRARY_API_NUM 如果是,擷取參數資訊(包括DLL路徑) 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼6 Ke_LoadLibrary過濾
1.2.5 WH_JOURNALRECORD過濾
在Windows作業系統中,WH_JOURNALRECORD設計的初衷是做訊息的記錄和回放,但是惡意程式利用此類型的鉤子可以擷取鍵盤輸入訊息,從而達到記錄密碼的目的。過濾虛擬碼如代碼7所示。
判斷ApiNumber是否EVENT_MSG_HOOK_API_NUM 進一步判斷訊息類型是否是按鍵訊息 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼7 WH_JOURNALRECORD過濾
Windows核心新手上路3——掛鈎KeUserModeCallBack
1. 簡介
在Windows系統中,提供了幾種方式從R0調用位於R3的函數,其中一種方式是KeUserModeCallBack,此函數流程如下:
nt!KeUserModeCallback->nt!KiCallUserMode->nt!KiServiceExit->ntdll!KiUserCallbackDispatcher->回呼函數-> int2B->nt!KiCallbackReturn-> nt!KeUserModeCallback(調用後)這是一個 ring0->ring3->ring0的過程,在堆棧準備完畢後,借用KiServiceExit的力量回到了ring3,它的著陸點是 KiUserCallbackDispatcher,然後KiUserCallbackDispatcher從PEB中取出
KernelCallbackTable的基址,再以ApiIndex作為索引在這個表中尋找對應的回呼函數並調用,調用完之後再int2B觸發 nt!KiCallbackReturn再次進入核心,修正堆棧後跳回KeUserModeCallback,完成調用。
系統所有的訊息鉤子回調都是利用KeUserModeCallBack完成的。所以可以通過掛鈎KeUserModeCallback用來過濾對鉤子的調用。
1.1 inline hook
KeUserModeCallback沒有對應的R3調用介面,所以沒有在SSDT SHADOW中出現,需要用另外的方式來HOOK,可以採用的Inline Hook,即在函數頭部加入一條JMP指令(機器碼E9)跳入代理函數,然後過濾之後決定是否調用真實的KeUserModeCallback函數。掛鈎過程如下:
ULONG StartHookKeyUserModeCallBack() { ULONG tmp; memset (Ori_Func, 0x90, 100); // nop Ori_Func[50] = 0xE9; tmp = (ULONG)ProxyFunc - (ULONG)KeUserModeCallback - 5; memcpy(jmp_bytes+1, &tmp, 4); HeadLen = GetPatchSize (KeUserModeCallback, 5); memcpy(Ori_Func, (PVOID)KeUserModeCallback, HeadLen); //原始位元組 //中間跳 tmp = (ULONG)KeUserModeCallback + HeadLen - (ULONG)(&Ori_Func[50]) - 5; memcpy(&Ori_Func[51], &tmp, 4); // 去掉記憶體保護 __asm { cli mov eax, cr0 and eax, not 10000h mov cr0, eax } memcpy(KeUserModeCallback, jmp_bytes, 5); // 恢複記憶體保護 __asm { mov eax, cr0 or eax, 10000h mov cr0, eax sti } return TRUE; } |
代碼1 掛鈎KeUserModeCallback
代理函數ProxyFunc代碼如代碼2:
__declspec(naked) VOID ProxyFunc(VOID) { __asm { MOV EAX, ESP PUSHAD PUSH [EAX+4*5] PUSH [EAX+4*4] PUSH [EAX+4*3] PUSH [EAX+4*2] PUSH [EAX+4] LEA EAX, MyKeUserModeCallback CALL EAX TEST EAX, EAX JZ continue_exe //return STATUS_SUCCESS POPAD RETN 0x14 continue_exe: POPAD LEA EAX, Ori_Func //跳回原始函數 JMP EAX } } |
代碼2 代理函數ProxyFunc
首先調用自訂的過濾函數MyKeUserModeCallback,然後判斷傳回值是否是STATUS_SUCCESS,如果是,則調用真實的KeUserModeCallback,否則直接返回,拒絕調用R3的回調用函數。
1.2
MyKeUserModeCallback
下面分部分講解MyKeUserModeCallback對鉤子的過濾操作,其中涉及到很多未公開技術,其代碼通過逆向工程技術得到。
1.2.1 過濾WH_KEYBOARD_LL類型鉤子
在Windows作業系統中,按鍵訊息到達具體的視窗訊息佇列之前會調用WH_KEYBOARD_LL鉤子函數,可以利用此類鉤子截取鍵盤輸入。虛擬碼如代碼3所示。
判斷ApiNumber是否是KBD_LL_HOOK_API_NUM 將InputBuffer轉換為PFNHKINLPKBDLLHOOKSTRUCTMSG 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼3 WH_KEYBOARD_LL過濾
在對WH_KEYBOARD_LL的過濾過程中,還有一點需要做特殊處理。現在有很多的軟體採用WH_KEYBOARD_LL鉤子來做安全輸入控制項,而不是通過傳統的EDIT控制項,所以在返回的時候需要將虛擬鍵盤產生的虛假的按鍵修正為真實的按鍵。
1.2.2 過濾WH_KEYBOARD鉤子
在Windows作業系統中,按鍵訊息到達具體的視窗訊息佇列之前會調用WH_KEYBOARD鉤子函數,可以利用此類鉤子截取鍵盤輸入。過濾的虛擬碼如代碼4所示。
判斷ApiNumber是否是KBD_HOOK_API_NUM 將InputBuffer轉為PFNHKINLPKBDLLHOOKSTRUCTMSG結構 判斷PFNHKINLPKBDLLHOOKSTRUCTMSG結構中GeneralHookHeader成員的nCode成員的值是否為0x20000 如果是則是WH_KEYBOARD鉤子 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼4 WH_KEYBOARD鉤子過濾
WH_KEYBOARD鉤子會同其他一些回調共用一個ApiNumber,這裡要做進一步的判斷,而且全域的WH_KEYBOARD鉤子需要在其他進程中LoadLibrary。
同WH_KEYBOARD_LL鉤子,有的程式會利用WH_KEYBOARD鉤子做安全輸入控制項,在返回時需要將虛擬鍵盤產生的虛假按鍵修正為真實的按鍵。
1.2.3 WH_DEBUG鉤子過濾
在Windows作業系統中,調用其他類型的鉤子函數之前,會首先調用WH_DEBUG鉤子函數,用於鉤子調試。惡意程式利用WH_DEBUG類型鉤子可以擷取鍵盤輸入。過濾虛擬碼如代碼5所示。
判斷ApiNumber是否是DEBUG_HOOK_API_NUM 判斷此DEBUG資訊中的鉤子類型是否包含鍵盤輸入資訊 如果包含,則填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼5 WH_DEBUG鉤子過濾
同WH_KEYBOARD_LL WH_KEYBOARD類型的鉤子,有些程式會使用WH_DEBUG鉤子實現安全輸入控制項,在返回時,需要將虛擬鍵盤產生的虛假按鍵修正為真實的按鍵。
1.2.4 Ke_LoadLibrary過濾
在Windows作業系統中,除了WH_KEYBOARD_LL鉤子外,其他類型的全域訊息鉤子都需要有DLL被載入進入目標進程(WH_KEYBOARD_LL鉤子只是一次線程切換,詳細資料可以查看MSDN)。所以過濾Ke_LoadLibrary可以阻止大部分的訊息鉤子(事實上,目前所有的密碼保護程式都通過此方法過濾訊息鉤子,用來保護密碼,但是此方法對WH_KEYBOARD_LL類型的鉤子無效)。過濾虛擬碼如代碼6所示。
判斷ApiNumber是否LOAD_LIBRARY_API_NUM 如果是,擷取參數資訊(包括DLL路徑) 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼6 Ke_LoadLibrary過濾
1.2.5 WH_JOURNALRECORD過濾
在Windows作業系統中,WH_JOURNALRECORD設計的初衷是做訊息的記錄和回放,但是惡意程式利用此類型的鉤子可以擷取鍵盤輸入訊息,從而達到記錄密碼的目的。過濾虛擬碼如代碼7所示。
判斷ApiNumber是否EVENT_MSG_HOOK_API_NUM 進一步判斷訊息類型是否是按鍵訊息 填充SYS_EVENT_STRUCT結構,通知R3控製程序作出判斷 如果使用者允許該調用則返回STATUS_SUCCESS 否則返回STATUS_UNSUCCESSFUL |
虛擬碼7 WH_JOURNALRECORD過濾