Windows系統是建立在事件驅動的機制上的,說穿了就是整個系統都是通過消息的傳遞來實現的。而鉤子是Windows系統中非常重要的系統接口,用它
可以截獲並處理送給
其他應用程序的消息,來完成普通應用程序難以實現的功能。鉤子可以監視系統或進程中的各種事件消息,截獲發往目標窗口的消息並進行處理。這樣,我們就可以
在系統中安裝自定義的鉤子,監視系統中特定事件的發生,完成特定的功能,比如截獲鍵盤、鼠標的輸入,屏幕取詞,日誌監視等等。可見,利用鉤子可以實現許多
特殊而有用的功能。因此,對於高級編程人員來說,掌握鉤子的編程方法是很有必要的。
鉤子的類型
一. 按事件分類,有如下的幾種常用類型
(1) 鍵盤鉤子和低級鍵盤鉤子可以監視各種鍵盤消息。
(2) 鼠標鉤子和低級鼠標鉤子可以監視各種鼠標消息。
(3) 外殼鉤子可以監視各種Shell事件消息。比如啟動和關閉應用程序。
(4) 日誌鉤子可以記錄從系統消息隊列中取出的各種事件消息。
(5) 窗口過程鉤子監視所有從系統消息隊列發往目標窗口的消息。
此外,還有一些特定事件的鉤子提供給我們使用,不一一列舉。
下面描述常用的Hook類型:
1、WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks
WH_CALLWNDPROC和WH_CALLWNDPROCRET
Hooks使你可以監視發送到窗口過程的消息。系統在消息發送到接收窗口過程之前調用WH_CALLWNDPROC
Hook子程,並且在窗口過程處理完消息之後調用WH_CALLWNDPROCRET Hook子程。
WH_CALLWNDPROCRET Hook傳遞指針到CWPRETSTRUCT結構,再傳遞到Hook子程。CWPRETSTRUCT結構包含了來自處理消息的窗口過程的返回值,同樣也包括了與這個消息關聯的消息參數。
2、WH_CBT Hook
在以下事件之前,系統都會調用WH_CBT Hook子程,這些事件包括:
1. 激活,建立,銷毀,最小化,最大化,移動,改變尺寸等窗口事件;
2. 完成系統指令;
3. 來自系統消息隊列中的移動鼠標,鍵盤事件;
4. 設置輸入焦點事件;
5. 同步系統消息隊列事件。
Hook子程的返回值確定系統是否允許或者防止這些操作中的一個。
3、WH_DEBUG Hook
在系統調用系統中與其他Hook關聯的Hook子程之前,系統會調用WH_DEBUG Hook子程。你可以使用這個Hook來決定是否允許系統調用與其他Hook關聯的Hook子程。
4、WH_FOREGROUNDIDLE Hook
當應用程序的前台線程處於空閒狀態時,可以使用WH_FOREGROUNDIDLE Hook執行低優先級的任務。當應用程序的前台線程大概要變成空閒狀態時,系統就會調用WH_FOREGROUNDIDLE Hook子程。
5、WH_GETMESSAGE Hook
應用程序使用WH_GETMESSAGE Hook來監視從GetMessage or PeekMessage函數返回的消息。你可以使用WH_GETMESSAGE Hook去監視鼠標和鍵盤輸入,以及其他發送到消息隊列中的消息。
6、WH_JOURNALPLAYBACK Hook
WH_JOURNALPLAYBACK
Hook使應用程序可以插入消息到系統消息隊列。可以使用這個Hook回放通過使用WH_JOURNALRECORD
Hook記錄下來的連續的鼠標和鍵盤事件。只要WH_JOURNALPLAYBACK Hook已經安裝,正常的鼠標和鍵盤事件就是無效的。
WH_JOURNALPLAYBACK Hook是全局Hook,它不能象線程特定Hook一樣使用。
WH_JOURNALPLAYBACK
Hook返回超時值,這個值告訴系統在處理來自回放Hook當前消息之前需要等待多長時間(毫秒)。這就使Hook可以控制實時事件的回放。
WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。(估計按鍵精靈是用這個hook做的)
7、WH_JOURNALRECORD Hook
WH_JOURNALRECORD Hook用來監視和記錄輸入事件。典型的,可以使用這個Hook記錄連續的鼠標和鍵盤事件,然後通過使用WH_JOURNALPLAYBACK Hook來回放。
WH_JOURNALRECORD Hook是全局Hook,它不能象線程特定Hook一樣使用。
WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。
8、WH_KEYBOARD Hook
在應用程序中,WH_KEYBOARD Hook用來監視WM_KEYDOWN and WM_KEYUP消息,這些消息通過GetMessage or PeekMessage function返回。可以使用這個Hook來監視輸入到消息隊列中的鍵盤消息。
9、WH_KEYBOARD_LL Hook
WH_KEYBOARD_LL Hook監視輸入到線程消息隊列中的鍵盤消息。
10、WH_MOUSE Hook
WH_MOUSE Hook監視從GetMessage 或者 PeekMessage 函數返回的鼠標消息。使用這個Hook監視輸入到消息隊列中的鼠標消息。
11、WH_MOUSE_LL Hook
WH_MOUSE_LL Hook監視輸入到線程消息隊列中的鼠標消息。
12、WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks
WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我們可以監視菜單,滾動條,消息框,對話框消息並且發現用戶使用ALT+TAB or ALT+ESC 組合鍵切換窗口。
WH_MSGFILTER Hook只能監視傳遞到菜單,滾動條,消息框的消息,以及傳遞到通過安裝了Hook子程的應用程序建立的對話框的消息。
WH_SYSMSGFILTER Hook監視所有應用程序消息。
WH_MSGFILTER 和 WH_SYSMSGFILTER
Hooks使我們可以在模式循環期間過濾消息,這等價於在主消息循環中過濾消息。通過調用CallMsgFilter
function可以直接的調用WH_MSGFILTER
Hook。通過使用這個函數,應用程序能夠在模式循環期間使用相同的代碼去過濾消息,如同在主消息循環裡一樣。
13、WH_SHELL Hook
外殼應用程序可以使用WH_SHELL Hook去接收重要的通知。當外殼應用程序是激活的並且當頂層窗口建立或者銷毀時,系統調用WH_SHELL Hook子程。
WH_SHELL 共有5鍾情況:
1. 只要有個top-level、unowned 窗口被產生、起作用、或是被摧毀;
2. 當Taskbar需要重畫某個按鈕;
3. 當系統需要顯示關於Taskbar的一個程序的最小化形式;
4. 當目前的鍵盤佈局狀態改變;
5. 當使用者按Ctrl+Esc去執行Task Manager(或相同級別的程序)。
按照慣例,外殼應用程序都不接收WH_SHELL消息。所以,在應用程序能夠接收WH_SHELL消息之前,應用程序必須調用SystemParametersInfo function註冊它自己。
以上是13種常用的hook類型!
二. 按使用範圍分類,主要有線程鉤子和系統鉤子
(1) 線程鉤子監視指定線程的事件消息。
(2) 系統鉤子監視系統中的所有線程的事件消息。因為系統鉤子會影響系統中所有的應用程序,所以鉤子函數必須放在獨立的動態鏈接庫(DLL)
中。這是系統鉤子和線程鉤子很大的不同之處。
幾點需要說明的地方:
(1) 如果對於同一事件(如鼠標消息)既安裝了線程鉤子又安裝了系統鉤子,那麼系統會自動先調用線程鉤子,然後調用系統鉤子。
(2) 對同一事件消息可安裝多個鉤子處理過程,這些鉤子處理過程形成了鉤子鏈。當前鉤子處理結束後應把鉤子信息傳遞給下一個鉤子函數。而且最近安裝的鉤子放在鏈的開始,而最早安裝的鉤子放在最後,也就是後加入的先獲得控制權。
(3) 鉤子特別是系統鉤子會消耗消息處理時間,降低系統性能。只有在必要的時候才安裝鉤子,在使用完畢後要及時卸載。
編寫鉤子程序
編寫鉤子程序的步驟分為三步:定義鉤子函數、安裝鉤子和卸載鉤子。
1.定義鉤子函數
鉤子函數是一種特殊的回調函數。鉤子監視的特定事件發生後,系統會調用鉤子函數進行處理。不同事件的鉤子函數的形式是各不相同的。
下面以鼠標鉤子函數舉例說明鉤子函數的原型:
LRESULT CALLBACK HookProc(int nCode ,WPARAM wParam,LPARAM lParam)
參數wParam和 lParam包含所鉤消息的信息,比如鼠標位置、狀態,鍵盤按鍵等。nCode包含有關消息本身的信息,比如是否從消息隊列中移出。
我們先在鉤子函數中實現自定義的功能,然後調用函數 CallNextHookEx.把鉤子信息傳遞給鉤子鏈的下一個鉤子函數。
CallNextHookEx.的原型如下:
LRESULT CallNextHookEx( HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam )
參數 hhk是鉤子句柄。nCode、wParam和lParam 是鉤子函數。
當然也可以通過直接返回TRUE來丟棄該消息,就阻止了該消息的傳遞。
2.安裝鉤子
在程序初始化的時候,調用函數SetWindowsHookEx安裝鉤子。
其函數原型為:
HHOOK SetWindowsHookEx( int idHook,HOOKPROC lpfn, INSTANCE hMod,DWORD dwThreadId )
參數idHook表示鉤子類型,它是和鉤子函數類型一一對應的。比如,WH_KEYBOARD表示安裝的是鍵盤鉤子,WH_MOUSE表示是鼠標鉤子等等。
Lpfn是鉤子函數的地址。
HMod是鉤子函數所在的實例的句柄。對於線程鉤子,該參數為NULL;對於系統鉤子,該參數為鉤子函數所在的DLL句柄。
dwThreadId 指定鉤子所監視的線程的線程號。對於全局鉤子,該參數為NULL。
SetWindowsHookEx返回所安裝的鉤子句柄。
3.卸載鉤子
當不再使用鉤子時,必須及時卸載。簡單地調用函數 BOOL UnhookWindowsHookEx( HHOOK hhk)即可。
值得注意的是線程鉤子和系統鉤子的鉤子函數的位置有很大的差別。線程鉤子一般在當前線程或者當前線程派生的線程內,而系統鉤子必須放在獨立的動態鏈接庫中,實現起來要麻煩一些。
線程鉤子的編程實例:
按照上面介紹的方法實現一個線程級的鼠標鉤子。鉤子跟蹤當前窗口鼠標移動的位置變化信息。並輸出到窗口。
(1)在VC++6.0中利用MFC
APPWizard(EXE)生成一個不使用文檔/視結構的單文檔應用mousehook。打開childview.cpp文件,加入全局變量:
HHOOK hHook;//鼠標鉤子句柄
CPoint point;//鼠標位置信息
CChildView *pView;
// 鼠標鉤子函數用到的輸出窗口指針
在CChildView::OnPaint()添加如下代碼:
CPaintDC dc(this);
char str[256];
sprintf(str,「x=%d,y=%d",point.x,point.y);
//構造字符串
dc.TextOut(0,0,str); //顯示字符串
(2)childview.cpp文件中定義全局的鼠標鉤子函數。
LRESULT CALLBACK MouseProc
(int nCode, WPARAM wParam, LPARAM lParam)
{//是鼠標移動消息
if(wParam==WM_MOUSEMOVE||wParam
==WM_NCMOUSEMOVE)
{
point=((MOUSEHOOKSTRUCT *)lParam)->pt;
//取鼠標信息
pView->Invalidate(); //窗口重畫
}
return CallNextHookEx(hHook,nCode,wParam,lParam);
//傳遞鉤子信息
}
(3)CChildView類的構造函數中安裝鉤子。
CChildView::CChildView()
{
pView=this;//獲得輸出窗口指針
hHook=SetWindowsHookEx(WH_MOUSE,MouseProc,0,GetCurrentThreadId());
}
(4)CChildView類的析構函數中卸載鉤子。
CChildView::~CChildView()
{
if(hHook)
UnhookWindowsHookEx(hHook);
}
系統鉤子的編程實例:
由於系統鉤子要用到dll,所以先介紹下win32 dll的特點:
Win32 DLL與 Win16 DLL有很大的區別,這主要是由操作系統的設計思想決定的。
一方面,在Win16 DLL中程序入口點函數和出口點函數(LibMain和WEP)是分別實現的;而在Win32 DLL中卻由同一函數DLLMain來實現。
無論何時,當一個進程或線程載入和卸載DLL時,都要調用該函數,它的原型是
BOOL WINAPI DllMain (HINSTANCE hinstDLL,DWORD fdwReason, LPVOID lpvReserved);
其中,第一個參數表示DLL的實例句柄;第三個參數系統保留;
這裡主要介紹一下第二個參數,它有四個可能的值:
DLL_PROCESS_ATTACH(進程載入),
DLL_THREAD_ATTACH(線程載入),
DLL_THREAD_DETACH(線程卸 載),
DLL_PROCESS_DETACH(進程卸載),
在DLLMain函數中可以對傳遞進來的這個參數的值進行判別,並根據不同的參數值對DLL進 行必要的初始化或清理工作。
舉個例子來說,當有一個進程載入一個DLL時,系統分派給DLL的第二個參數DLL_PROCESS_ATTACH,這時,
你可以根據這個參數初始化特定的數據。另一方面,在Win16環境下,所有應用程序都在同一地址空間;而在Win32環境下,所有應用程序都有自己的私有
空間,每個進程的空間都是相互獨立的,這減少了應用程序間的相互影響,但同時也增加了編程的難度。
大家知道,在Win16環境中,DLL的全局數據對每個
載入它的進程來說都是相同的;而在Win32環境中,情況卻發生了變化,當進程在載入DLL時,系統自動把DLL地址映射到該進程的私有空間,而且也複製
該DLL的全局數據的一份拷貝到該進程空間,也就是說每個進程所擁有的相同的DLL的全局數據其值卻並不一定是相同的。因此,在Win32環境下要想在多
個進程中共享數據,就必須進行必要的設置。亦即把這些需要共享的數據分離出來,放置在一個獨立的數據段裡,並把該段的屬性設置為共享。
在VC6 中有三種形式的MFC DLL(在該DLL中可以使用和繼承已有的MFC類)可供選擇,即Regular
statically linked to MFC DLL(標準靜態鏈接MFC DLL)和Regular using the shared MFC
DLL(標準動態鏈接MFC DLL)以及Extension MFC DLL(擴展MFC DLL)。
第一種DLL的特點是,在編譯時把使用的MFC代碼加入到DLL中,因此,在使用該程序時不需要其他MFC動態鏈接類庫的存在,但佔用磁盤空間 比較大;
第二種DLL的特點是,在運行時,動態鏈接到MFC類庫,因此減少了空間的佔用,但是在運行時卻依賴於MFC動態鏈接類庫;這兩種DLL既可以被 MFC程序使用也可以被Win32程序使用。
第三種DLL的特點類似於第二種,做為MFC類庫的擴展,只能被MFC程序使用。
2010年12月24日 星期五
鉤子的類型和實現
2010年12月23日 星期四
[原創]簡單逆向HS-NtReadVirtualMemory
來份聖誕大禮~!!
搞了一下午的成果 第一次逆驅動 還是直接用KD逆的
可能有些許錯誤 放著之後再修改吧~!
By Kost0911 2010.12.24
[ebp-C] = ReturnLength
[ebp-28] = ProcessInformation
[ebp-4] = ProcessHandle
[ebp-10] = 返回值(success Or error c)
[ebp-8] = Pid
[ebp-18] = ???
----------保護暫存器---------------
0xB31F7C00 mov eax, esp
0xB31F7C02 add eax, 8
0xB31F7C05 pushad
0xB31F7C06 pushfd
0xB31F7C07 push ebp
0xB31F7C08 mov ebp, esp
0xB31F7C0A sub esp, 40
0xB31F7C0D mov ecx, dword ptr [eax+8]
0xB31F7C10 mov dword ptr [ebp-4], ecx
0xB31F7C13 mov ecx, B3206968 //B3206968=ProcessInformation
0xB31F7C18 call dword ptr [<&ntkrnlpa.InterlockedIncrement>] //不讓多執行序共享變量(保護)
0xB31F7C1E lea eax, dword ptr [ebp-C] //把局部變量3 ebp-c的地址給eax
0xB31F7C21 push eax //ReturnLength
0xB31F7C22 push 18 //ProcessInformationLength
0xB31F7C24 lea ecx, dword ptr [ebp-28]
0xB31F7C27 push ecx //ProcessInformation
0xB31F7C28 push 0 //ProcessInformationClass
0xB31F7C2A mov edx, dword ptr [ebp-4] //保存在局部變量1 ebp-4=ProcessHandle
0xB31F7C2D push edx //ProcessHandle
0xB31F7C2E call dword ptr [<&ntkrnlpa.ZwQueryInformationProcess>]
0xB31F7C34 mov dword ptr [ebp-10], eax //返回值(success Or error)給局部變量4
0xB31F7C37 xor eax, eax //eax=0
0xB31F7C39 cmp dword ptr [ebp-10], 0
0xB31F7C3D setge al //如果ZF=1則,al等於1,否則等於0
0xB31F7C40 cmp eax, 1 //比較eax是否為1
0xB31F7C43 jne B31F7D13 //eax不為1 ●跳到 "正確"
0xB31F7C49 call B31FFB66 //PsGetCurrentId 得到目前PID值
0xB31F7C4E mov dword ptr [ebp-8], eax //ebp-8 = Pid
0xB31F7C51 push 1
0xB31F7C53 mov ecx, dword ptr [ebp-8] //ecx = Pid
0xB31F7C56 push ecx //Pid
0xB31F7C57 call B31FD460 //這個Call內部有 KeGetCurrentIrql(得到目前IRQL)
//和建立快速互斥 and 釋放快速互斥
0xB31F7C5C movzx edx, al //返回值給edx
0xB31F7C5F cmp edx, 1 //edx和1做比較
0xB31F7C62 jne short B31F7CB1 //繼續判斷
0xB31F7C64 push 4
0xB31F7C66 mov eax, dword ptr [ebp-18]
0xB31F7C69 push eax
0xB31F7C6A push B320F3A0
0xB31F7C6F call B31F07F0 ●這個Call出現多次 把Pid當參數傳入 回傳值 1 Or 0
0xB31F7C74 movzx ecx, al //把回傳值給ecx
0xB31F7C77 cmp ecx, 1 //ecx和1比較
0xB31F7C7A jne short B31F7CAF ●ecx 為0 "跳到正確"
0xB31F7C7C push 4
0xB31F7C7E mov edx, dword ptr [ebp-8] //edx=Pid
0xB31F7C81 push edx //push Pid
0xB31F7C82 push B320F3A0
0xB31F7C87 call B31F07F0 ●這個Call出現多次 把Pid當參數傳入 回傳值 1 Or 0
0xB31F7C8C movzx eax, al
0xB31F7C8F cmp eax, 1
0xB31F7C92 je short B31F7C99 //如果eax=1 正確就繼續判斷
0xB31F7C94 jmp B31F7D29 ●eax = 0 跳到錯誤
0xB31F7C99 push 1
0xB31F7C9B call B31FFB6C //PsGetCurrentThreadId 得到目前的Tid值
0xB31F7CA0 push eax //eax = Tid
0xB31F7CA1 call B31F2680 //這個Call內部有建立快速互斥 and 釋放快速互斥
0xB31F7CA6 mov edx, dword ptr [ebp-8] edx = Pid
0xB31F7CA9 push edx
0xB31F7CAA call B31FD3B0 //這個Call內部有 KeGetCurrentIrql(得到目前IRQL)
//和建立快速互斥 and 釋放快速互斥
//Call中又有個Call 呼叫InterlockedPushEntrySlist函數在棧頂添加一個元素
0xB31F7CAF jmp short B31F7D13 ●"跳到正確"
0xB31F7CB1 push 4
0xB31F7CB3 mov eax, dword ptr [ebp-18]
0xB31F7CB6 push eax
0xB31F7CB7 push B320F3A0
0xB31F7CBC call B31F07F0 ●這個Call出現多次 把Pid當參數傳入 回傳值 1 Or 0
0xB31F7CC1 movzx ecx, al
0xB31F7CC4 cmp ecx, 1
0xB31F7CC7 jne short B31F7D13 ●ecx = 0 跳到正確
0xB31F7CC9 push 4
0xB31F7CCB mov edx, dword ptr [ebp-8] //eax=Pid
0xB31F7CCE push edx
0xB31F7CCF push B320F3A0
0xB31F7CD4 call B31F07F0 ●這個Call出現多次 把Pid當參數傳入 回傳值 1 Or 0
0xB31F7CD9 movzx eax, al
0xB31F7CDC cmp eax, 1 //比較eax是否=1
0xB31F7CDF je short B31F7D06 ●如果eax=1 跳到正確
0xB31F7CE1 push 1
0xB31F7CE3 call dword ptr [<&ntkrnlpa.IoGetCurrentProcess>] //得到一個PEPROCESS結構
0xB31F7CE9 push eax eax=目前調用的PEPROCESS結構
0xB31F7CEA call B31FD596 //Hs的判斷CALL 可能是黑名單 or 白名單
0xB31F7CEF movzx ecx, al
0xB31F7CF2 cmp ecx, 1 //ecx和1判斷
0xB31F7CF5 je short B31F7D04 ●如果ecx=1 跳到正確
0xB31F7CF7 push 1
0xB31F7CF9 mov edx, dword ptr [ebp-8] //edx=Pid
0xB31F7CFC push edx //push Pid
0xB31F7CFD call B31FD230 //這個Call內部有 KeGetCurrentIrql(得到目前TRQL)
//和建立快速互斥 and 釋放快速互斥
//Call中又有個Call 呼叫InterlockedPushEntrySlist函數在棧頂添加一個元素
0xB31F7D02 jmp short B31F7D29 ●跳到錯誤
0xB31F7D04 jmp short B31F7D13 ●跳到正確
0xB31F7D06 push 1
0xB31F7D08 call B31FFB6C //PsGetCurrentThreadId
0xB31F7D0D push eax //eax=Tid
0xB31F7D0E call B31F2680 //傳入Tid值的Call
0xB31F7D13 mov ecx, B3206968 //ecx=ProcessInformation
0xB31F7D18 call dword ptr [<&ntkrnlpa.InterlockedDecrement>] //在多線程中保護某個變量
-------------------還原現場----------------------------
0xB31F7D1E add esp, 40
0xB31F7D21 pop ebp
0xB31F7D22 popfd
0xB31F7D23 popad
0xB31F7D24 jmp B31F72E0 //這句Jmp ●(8053CB90) ●原本被Hook前的Call地址 "正確"
0xB31F7D29 mov ecx, B3206968 ●ecx=ProcessInformation 下面接著返回錯誤代碼 "錯誤"
0xB31F7D2E call dword ptr [<&ntkrnlpa.InterlockedDecrement>] //在多線程中保護某個變量
0xB31F7D34 add esp, 40
0xB31F7D37 pop ebp
0xB31F7D38 popfd
0xB31F7D39 popad
0xB31F7D3A pop eax
0xB31F7D3B pop eax
0xB31F7D3C pop eax
0xB31F7D3D mov eax,C0000022 //返回eax = C0000022 錯誤代碼
0xB31F7D42 retn 14
0xB31F7D45 CC int3
0xB31F7D46 CC int3
0xB31F7D47 CC int3
0xB31F7D48 CC int3
Dll 模塊隱藏技術
本文主要講的是怎樣隱藏一個dll模塊,這裡說的隱藏是指,dll被加載後怎樣使它 用一般的工具無法檢測出來。
為什麼要這麼做呢?
1.遠程線程中的應用
(1)大家都知道,遠程線程注入主要有兩種一種是直接copy母體中預注入的代碼到目標進程地址空間(WriteProcessMemory),
然後啟動注入的代碼(CreateRemoteThread),這種遠程線程一旦成功實現,那麼它只出現在目標進程的內存中,
並沒有對應的磁盤文件,堪稱進程隱藏中的高招,可是缺點就是,你必須要在注入代碼中對所有直接尋址的指令進行修正,
這可是個力氣活,用彙編寫起來很煩。
(2)另一種更為常用的方法是注入一個 dll 文件到目標進程,這種方法的實現可以是 以一個 消息Hook 為由進行注入,
或者仍然使用 CreateRemoteThread,這種方法的優點是 Dll 文件自帶 重定位 表,也就是說你不必再為修正直接尋址
指令而煩惱了!,dll 自己會重定位!~~~嗯,真是不錯的方法 --- 可是我們說它不如上面說的方法牛。為什麼?
因為它的致命傷就是 可以用進程管理工具 看見被加載的 dll 文件名、文件路徑。這真是太不爽了,因為只要用戶看看模塊列表
很容易發現可疑模塊!,再依據名字,找到路徑,定位文件 --- dll文件就這樣暴露了.這樣也就不是很完美的隱藏進程。
[現在不用怕啦~~ 本文將介紹的方法就是為了上述 不足而存在地~~~,讓一般的工具看不到已加載的某個dll]
2.自身文件的需要
這個說起來比較簡單,比如我的一個程序運行了,我不想讓用戶知道我的EXE使用了某個dll,那麼同樣的也需要這種隱身技術.
3. 技術實現
(1).
說完了這麼多,該說說,到底應該怎麼實現了.
熟悉SEH的肯定對 PEB 這個結構並不陌生-- PEB (Process Environment Block)進程環境信息塊,這裡儲存著進程的重要信息
主要原理就是這個結構,和它的成員相關結構
首先我們回顧一下如何找到這個結構,常見的代碼是這個:
mov eax,fs:[30h] ;就這一句足矣,執行後 eax --> PEB (eax指向PEB結構,即eax中是PEB結構在進程空間中的地址)
熟悉TEB和SEH中反調試知識的童鞋一定對上面這個很熟悉了~~不多說了--(不懂得童鞋去學習一下SEH的相關知識你就會認清 fs 了)
下面看一下 PEB 結構的定義 :
;=================================================================
PEB STRUCT ; sizeof = 1E8h
InheritedAddressSpace BYTE ? ; 0000h
ReadImageFileExecOptions BYTE ? ; 0001h
BeingDebugged BYTE ? ; 0002h
SpareBool BYTE ? ; 0003h
Mutant PVOID ? ; 0004h
ImageBaseAddress PVOID ? ; 0008h
Ldr PVOID ? ; 000Ch PTR PEB_LDR_DATA
ProcessParameters PVOID ? ; 0010h PTR RTL_USER_PROCESS_PARAMETERS
SubSystemData PVOID ? ; 0014h
~~~~~~~~~~~~~~~~~ ~~~~ ~~ ;PEB 結構以下部分省略
PEB ENDS
;==================================================================
由於PEB結構太龐大了,因此本文指截取了開頭的一部分,因為我們主要使用的是它的 Ldr 成員,看見了嗎? 對!,就是它在結構偏移 0Ch 處
後面已經指出了Ldr成員是一個指向 PEB_LDR_DATA 結構的指針,下面我們就得看看這個結構了:
;==================================================================
PEB_LDR_DATA STRUCT ; sizeof = 24h
_Length DWORD ? ; original name Length
Initialized BYTE ? ; 04h
db 3 dup(?) ; padding
SsHandle PVOID ? ; 08h
InLoadOrderModuleList LIST_ENTRY <> ; 0Ch
InMemoryOrderModuleList LIST_ENTRY <> ; 14h
InInitializationOrderModuleList LIST_ENTRY <> ; 1Ch
PEB_LDR_DATA ENDS
;==================================================================
啊哈~~~這裡我們看到了想要的東西, Module 這個單詞被我們發現了,ModuleList 就是模塊 列表嘛~~~
InLoadOrderModuleList 就是按照模塊加載順序描述模塊信息的,InMemoryOrderModuleList是按照內存中存儲順序描述,
InInitializationOrderModuleList是按照初始化dll模塊的順序描述的(你可以利用它們之一獲得kernel32.dll的基址
這是許多無導入表程序的必做之事)
為了弄清Module信息究竟是怎麼儲存的,我們又必須知道LIST_ENTRY結構的定義
一個LIST_ENTRY結構描述了一個雙鏈表
;======================================
LIST_ENTRY STRUCT
Flink pLIST_ENTRY
Blink pLIST_ENTRY
LIST_ENTRY ENDS
pLIST_ENTRY typedef PTR LIST_ENTRY ;pLIST_ENTRY 表示指向LIST_ENTRY結構的指針
;=====================================
根據圖片我們可以看出LIST_ENTRY的用法,它嵌入在一個結構類型內,Flink指向下一個這種結構類型內的LIST_ENTRY
這樣由表頭,就可以找到所有的data struct結構了!
啊哈~~ MSDN 又說 InMemoryOrderModuleList 指向一個 LDR_DATA_TABLE_ENTRY 結構,也就是說,我們的圖片的
data struct 1,2,3 就是指 LDR_DATA_TABLE_ENTRY 結構,再看看 它的 定義:(雖然結構有點繞,別暈啊~~快勝利了)
;===========================================================
LDR_DATA_TABLE_ENTRY STRUCT
InLoadOrderLinks LIST_ENTRY ;0h
InMemoryOrderLinks; LIST_ENTRY ;8h
InInitializationOrderLinks; LIST_ENTRY ;10h
DllBase; dword ;18h ;DllBase模塊基址
EntryPoint; dword ;1Ch ;模塊入口點
SizeOfImage; dword ;20h ;模塊的內存映像大小
FullDllName; UNICODE_STRING ;24h
BaseDllName; UNICODE_STRING ;2Ch
Flags; dword ;34h
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~ ;LDR_DATA_TABLE_ENTRY結構以下部分省略
LDR_DATA_TABLE_ENTRY ENDS
;===========================================================
怎麼樣?看成員名字就知道~~~~,模塊信息就在此處!
可以肯定一個LDR_DATA_TABLE_ENTRY描述一個模塊的信息,依靠LIST_ENTRY與下一個或前一個LDR_DATA_TABLE_ENTRY想連.
它的前三個成員都是 LIST_ENTRY 類型!,再看看上面圖片,你應該明白了吧,PEB_LDR_DATA中的三個LIST_ENTRY的後繼依次就是這三個
這裡我們主要使用InLoadOrderModuleList這個成員來遍歷模塊,原因是InLoadOrderModuleList對應的嵌入在LDR_DATA_TABLE_ENTRY結構中
的InLoadOrderLinks位於結構首部,用它尋址會方便、清楚一些(當然你用其它兩個,InMemoryOrderModuleList 和
InInitializationOrderModuleList也可以啊)
看這個彙編代碼,我們要定位於首個LDR_DATA_TABLE_ENTRY:
mov eax,fs:[30h] ;eax-->PEB
mov eax,[eax + 0Ch] ;eax == PEB.Ldr --> PEB_LDR_DATA
mov eax,[eax + 0Ch] ;eax == PEB_LDR_DATA.InLoadOrderModuleList.Flink --> LDR_DATA_TABLE_ENTRY
OK,執行後eax中就是第一個LDR_DATA_TABLE_ENTRY結構的地址啦!!!
3個mov指令,但是用到了好多結構啊~~~
對於我們的遍歷方式,第一個LDR_DATA_TABLE_ENTRY描述的應該是dll所屬的EXE的信息,包括入口點,基址,文件名什麼的 ~~~
到這裡我們還差一小步 那就是 UNICODE_STRING 結構
;=============================================================
UNICODE_STRING STRUCT ;sizeof == 08h
Length word ;0h
MaximumLength word ;02h
Buffer dword ;04h
UNICODE_STRING STRUCT ENDS
;=============================================================
Length 指明由Buffer字段指向的UNICODE串的長度,不包括結尾的 00 00
比如" C:\A "這個UNICODE串那麼Length就是4*2==8
Buffer 指向Unicode字串的指針!
太好了終於大功告成,解決複雜的結構了--
現在假設我們要獲得某個模塊的全路徑:
mov eax,fs:[30h] ;eax-->PEB
mov eax,[eax + 0Ch] ;eax == PEB.Ldr --> PEB_LDR_DATA
mov eax,[eax + 0Ch] ;eax == PEB_LDR_DATA.InLoadOrderModuleList.Flink --> LDR_DATA_TABLE_ENTRY
mov eax,[eax + 24h + 4h] ;eax == LDR_DATA_TABLE_ENTRY.FullDllName.Buffer --> 模塊路徑unicode串
執行過後,eax 中存儲的就是我們遍歷的首個模塊的模塊全路徑字串的地址
也就是模塊字串名稱的指針.
要訪問下一個LDR_DATA_TABLE_ENTRY,在上幾句代碼的基礎上只需這樣:
mov eax,[eax] ;因為eax本身就是指向LIST_ENTRY結構地~~~,這樣mov指令使得
;當前LIST_ENTRY的Flink傳送到eax,eax自然就指向下一個LDR_DATA_TABLE_ENTRY結構了~~~
(2).
好了說說我們的dll隱身技術,我們就是要將這裡的LDR_DATA_TABLE_ENTRY.FullDllName指向的字符串清除!
因為快照函數或其它的函數,一般都會在底層訪問這個結構,在這裡查詢 模塊 的信息,我們把它給清除了,看它還能查到了嗎?
下面我們假設我們已經使用CreateRemoteThread創建一個遠程線程,遠程線程入口就是 LoadLibraryA 函數,傳入參數就是我們的欲注入
的dll文件名,現在我們看看這個dll文件的核心代碼,看它如何清除自己的FullDllName串實現隱藏:
DllEntry proc _hInstance,_dwReason,_dwReserved
pushad
mov eax,_dwReason
.if eax == DLL_PROCESS_ATTACH ;Dll初始化時執行!
push _hInstance
pop hInstDll ;hInstDll保存本dll加載基址
;------------------------------------------
;定位eax指向首個LDR_DATA_TABLE_ENTRY結構
;------------------------------------------
assume fs:nothing
mov eax,fs:[30h] ;eax-->PEB
mov eax,[eax + 0Ch] ;eax==Ldr-->PEB_LDR_DATA
mov eax,[eax + 0Ch] ;LoadOrderList.Flink-->LDR_DATA_TABLE_ENTRY
;-----------------------------------------------------------
;通過循環遍歷LDR_DATA_TABLE_ENTRY結構,比較DllBase與hInstDll
;找到描述本dll模塊的LDR_DATA_TABLE_ENTRY結構
;-----------------------------------------------------------
@@:
mov ebx,[eax + 18h] ;[eax + 18h]為當前處理的模塊的基址
cmp ebx,hInstDll ;找到本模塊的LDR_DATA_TABLE_ENTRY
jne _No
mov ebx,[eax + 24h + 4]
movzx ecx,word ptr [eax + 24h] ;Unicode String length 獲得子串長度
mov edi,ebx ;Unicode String Address
cld
rep stosb ;用Al的值填充
mov dword ptr [ebx],0 ;(確保String首4個byte為0)
jmp @F
_No:
mov eax,[eax] ;鏈表遍歷
jmp @B
@@:
.elseif -- - -- - - --
在Dll初始化時,響應 DLL_PROCESS_ATTACH ,在這其中清除我們的「自己的名字」,來個神不知鬼不覺!,哈哈~~
這同時也說明,初始化時,Windows已經把各結構都填寫好了.
做到這裡我們已經能實現這樣的功能,即 一般的進程查看工具看不到我們注入的dll了!
因為我們把它要查的信息清除了!
說到這可能有大牛說,我暴力搜索內存,匹配 MZ--PE ,看你往哪跑? 的卻我是跑不了 ~~
如果你這樣做的話,首先你得知道我注入的是哪個進程,其次,你得有耐心暴力搜索,然後--- 真的被您搜到了 ---
此時我的 dll 的代碼便可以被人家隨便分析啦 ~~ 一看 哈哈,原來就是清除了 某某結構的內容啊 ---
哼! 我在你的 導出表 裡找到你的 dll 名字!,然後找到你的磁盤文件,那你就任我魚肉吧! 哈哈哈哈~~~~~
好可怕啊~~~,是啊,用OD看看,然後靜態反彙編一下,我們的代碼就露餡了--唉 --
等等!!
要不然 我們來個 Self Modify 將dll中的這段代碼 也給它清除掉,對! 還有 EXPORT 的那個 dll 名,一起除掉,
給它來個 毀屍滅跡 O(∩_∩)O哈哈~
說幹就幹,看下面代碼:
DllEntry proc _hInstance,_dwReason,_dwReserved
pushad
mov eax,_dwReason
.if eax == DLL_PROCESS_ATTACH
push _hInstance
pop hInstDll
_BEGIN:
call @F
@@:
pop eax ;eax返回@@標號處的線性地址
sub eax,5 ;執行後eax等於_BEGIN處的線性地址
push eax
mov ebx,offset _END - offset _BEGIN
invoke VirtualProtect,eax,ebx,PAGE_EXECUTE_READWRITE,addr flOldProtect ;flOldProtect萬萬不可少
;------------------------------------------
;清除PEB結構中,(UNICODE STRING)FullDllName
;------------------------------------------
assume fs:nothing
mov eax,fs:[30h] ;eax-->PEB
mov eax,[eax + 0Ch] ;eax==Ldr-->PEB_LDR_DATA
mov eax,[eax + 0Ch] ;LoadOrderList.Flink-->LDR_DATA_TABLE_ENTRY
@@:
mov ebx,[eax + 18h]
cmp ebx,hInstDll ;找到本模塊的LDR_DATA_TABLE_ENTRY
jne _No
mov ebx,[eax + 24h + 4]
movzx ecx,word ptr [eax + 24h] ;Unicode String length
mov edi,ebx ;Unicode String Address
cld
rep stosb
mov dword ptr [ebx],0 ;(確保String首4個byte為0)
jmp @F
_No:
mov eax,[eax]
jmp @B
@@:
;---------------------------------
;清除Dll映像中導出表中的Dll文件名
;---------------------------------
mov esi,hInstDll
add esi,[esi + 03Ch]
assume esi:ptr IMAGE_NT_HEADERS
mov edi,[esi].OptionalHeader.DataDirectory[0].VirtualAddress
add edi,hInstDll
assume edi:ptr IMAGE_EXPORT_DIRECTORY
mov edi,[edi].nName
add edi,hInstDll ;edi-->DllName(Export)
mov [esp - 4*4],edi
xor eax,eax
mov ecx,-1
cld
repnz scasb ;[edi] != 0 -->> continue
sub edi,[esp - 4*4] ;edi == length + 1
mov [esp - 4*3],edi
mov dword ptr [esp - 4*2],PAGE_EXECUTE_READWRITE
lea eax,flOldProtect
mov [esp - 4*1],eax
sub esp,4*4
call VirtualProtect ;調用VirtualProtect更改頁屬性
xor eax,eax
xchg ecx,edi ;edi == length + 1
xchg edi,[esp - 4*4] ;[esp - 4*4] --> DllName
cld
rep stosb
;-------------------------------------
;將_BEGIN 與 _END 之間內容填充為int 3
;-------------------------------------
pop edi
mov eax,0CCh
mov ecx,offset _END - offset _BEGIN
cld
rep stosb
_END:
;--------------------------------------------
;創建自定義線程,you can do anything you want
;--------------------------------------------
invoke CreateThread,0,0,addr _ThreadProc,0,0,0
.elseif eax == DLL_PROCESS_DETACH
NOP
.endif
popad
mov eax,TRUE
ret
DllEntry Endp
這最後一部分就是 Dll 的完全的核心代碼啦 ~~~ 調用VirtualProtectl兩次,第一次用於更改
_BEGIN與_END標號處的屬性,以便毀屍滅跡填充 int 3.第二次用於更改導出表dll文件名的地方
的屬性,以便將其清零.
最後程序可以創建個自定義線程,在這個線程裡 you can do anything you like
;/////////////////////////////////////////////////////////////////////////////////////////////
至此,我們的工程完成啦 ~~~ ,即使搜索內存字符串也是搜不到的--因為所有的相關串都被我們毀了
##我測試環境是 Win 7,ProcessExplorer 和 360自帶的 進程管理器 都查不出來 已加載的Dll模塊.##
##冰刃的話說是不支持win 7 因此未測試,不知結果不敢妄言##
好了說說我們的附件:
你可以看到本文使用的是 MASM32 的語法
附件中有完整的 dll 文件的源代碼,還有編譯好的 dll 文件。
為了測試程序的方便我還特意用 ASM 寫了個控制台程序,這個控制台程序是個"dll 注入器"
採用CreateRemoteThread的方式注入,你可以輸入進程名來完成注入,以便測試我們的這個dll
這個控台程序的源碼同樣在附件中.
注意:注入前 dll 文件要放在 PATH 變量指定的目錄中,否則LoadLibraryA找不到dll會失敗
dll成功注入後首先會彈出個MessageBox,告訴你,它被 Loaded 了,然後 自定義線程
會 每隔 6 秒彈出一個 MessgeBox,告訴你 It is alive !
注:由於涉及到遠程注入,因此部分殺軟會報毒的,如果想繼續測試,請暫時關閉 進程防火牆 或 其它可能的主動防禦
2010年12月21日 星期二
IRP Hook全家福
我們今天一起來彙總看看IRP
HOOK的方法。又是長篇大論,別著急,慢慢看。談到irp攔截,基本上有三種方式,一種是在起點攔截,一種是在半路攔截,一種是在終點攔截。
下面我們會詳細分析這幾種方式哪些是有效的,哪種是無效的。 要理解這幾種攔截,我們需要看看irp地傳送過程。我們看下圖的標準模型。請看大屏幕。
8.JPG
注意這個標準模型中,並不是每種IRP都經過這些步驟,由於設備類型和IRP種類的不同某些步驟會改變或根本不存在。
一、IRP創建。
由於IRP開始於某個實體調用I/O管理器函數創建它,可以使用下面任何一種函數創建IRP:
IoBuildAsynchronousFsdRequest 創建異步IRP(不需要等待其完成)。該函數和下一個函數僅適用於創建某些類型的IRP。
IoBuildSynchronousFsdRequest 創建同步IRP(需要等待其完成)。
IoBuildDeviceIoControlRequest 創建一個同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL請求。
IoAllocateIrp 創建上面三個函數不支持的其它種類的IRP。
由此我們知道,第一種起點攔截的辦法就清楚了,那就是HOOK這幾個IRP的創建函數。
由於函數有多個,並且此時irp雖然已經創建,但是還沒有進程初始化,也就是說irp堆棧
單元的內容還沒有填充。因此起點攔截的辦法是得不到有用信息的。這種辦法無效。
二、發往派遣例程
那麼irp是什麼時間初始化的呢?
創建完IRP後,你可以調用IoGetNextIrpStackLocation函數獲得該IRP第一個堆棧單元的指針。然後初始化這個堆棧單元。在初始
化過程的最後,你需要填充MajorFunction代碼。堆棧單元初始化完成後,就可以調用IoCallDriver函數把IRP發送到設備驅動程序
了。IoCallDriver是一個宏,它內部實現中調用了IofCallDriver. 因此,到這裡便有了第二種攔截方法,即中途攔截。
三、派遣例程的作用
1)在派遣例程中完成irp。通常我們做的過濾驅動或者一些簡單的驅動,都是這麼完成的,直接在派遣例程中返回。不需要經過後面的步驟,派遣函數立即完成該IRP。
例如:NTSTATUS OnStubDispatch( IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (Irp, IO_NO_INCREMENT );
return Irp->IoStatus.Status;
}
派遣例程把該IRP傳遞到處於同一堆棧的下層驅動程序 。
在這種情況下,通過調用IcCallDriver可以將irp傳遞到其他的驅動,或者傳遞到下一層驅動,這時irp變成其他驅動要處理的事情,如果其他
驅動的派遣例程處理了irp,就類似1)的情況了,如果沒處理,繼續向下傳,如果中間FDO沒有處理,最後傳到最低層的硬件驅動上去,也就是我們所謂的
PDO.
這個時候,I/O管理器就調用一次StartIo例程,硬件抽象層會通過硬件中斷ISR,一個ISR最可能做的事就是調度DPC例程(推遲過程調用)。最
後完成這個IRP.,回到I/O管理器。
排隊該IRP以便由這個驅動程序中的其它例程來處理 。
例如:NTSTATUS DispatchXxx(...)
{
...
IoMarkIrpPending(Irp);
IoStartPacket(device, Irp, NULL, NULL);
return STATUS_PENDING;
}
如果設備正忙,IoStartPacket就把請求放到隊列中。如果設備空閒,IoStartPacket將把社
備置成忙並調用StartIo例程。 接下來類似於2)中描述的那樣,完成這樣一個過程。
我們寫驅動的時候,對感興趣的irp,我們都會寫派遣例程來進行處理。如果我們把派遣例程給替換了,便有了第三種的irp攔截。
對於第三種的攔截,有兩種辦法:
一種是寫一個過濾驅動放在要攔截的驅動的上層,這是一種安全的辦法。例如:
如果我們想攔截系統的文件操作,就必須攔截I/O管理器發向文件系統驅動程序的IRP。而攔
截IRP最簡單的方法莫過於創建一個上層過濾器設備對象並將之加入文件系統設備所在的設備堆棧中。具體方法如下:首先通過IoCreateDevice創
建自己的設備對象,然後調用IoGetDeviceObjectPointer來得到文件系統設備(Ntfs,Fastfat,Rdr或Mrxsmb,
Cdfs)對象的指針,最後通過IoAttachDeviceToDeviceStack或者IoAttachDevice等函數,將自己的設備放到設備
堆棧上成為一個過濾器。這是攔截IRP最常用也是最保險的方法。
還有一種就是直接替換要攔截驅動對象的派遣例程函數表。它的方法更簡單且更為直接。
例如:如果我們想攔截系統的文件操作,它先通過ObReferenceObjectByName得到文件系統驅動對象的指針。然後將驅動對象中
MajorFunction數組中的打開,關閉,清除,設置文件信息,和寫入調度例程入口地址改為我們驅動中相應鉤子函數的入口地址來達到攔截IRP的目
的。
總結:
1) 可用辦法之一:hook IofCallDriver實現irp 攔截。
2) 可用辦法之二:寫一個過濾驅動,掛在你要hook其irp的那個驅動之上。
3) 可用辦法之三:直接修改你要hook其irp的那個驅動的MajorFunction函數表。
針對於三種可用方法,我們分別給出例子說明:
方法一例子:沒必要再細寫,只需要注意一點:
lkd> u IofCallDriver
nt!IofCallDriver:
804ef0f6 ff2500c85480 jmp dword ptr [nt!KeTickCount+0x1460 (8054c800)]
804ef0fc cc int 3
804ef0fd cc int 3
這裡我們看到IofCallDriver的地址在開頭偏移2個字節地方。看明白這個,後面代碼的寫法就能搞清楚。
#include "ntddk.h"
typedef NTSTATUS (FASTCALL
*pIofCallDriver)(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp);
pIofCallDriver old_piofcalldriver;
UNICODE_STRING SymbolicLinkName;
PDRIVER_OBJECT g_drvobj;
UNICODE_STRING DeviceName;
PDEVICE_OBJECT deviceObject;
ULONG oData;
#define IOCTL_DISABLE CTL_CODE(FILE_DEVICE_UNKNOWN ,0x8101,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define IOCTL_ENABLE CTL_CODE(FILE_DEVICE_UNKNOWN ,0x8100,METHOD_BUFFERED,FILE_ANY_ACCESS)
NTSTATUS FASTCALL
NewpIofCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
NTSTATUS stat;
DbgPrint("Hacked Great!");
__asm
{
mov ecx,DeviceObject
mov edx,Irp
Call old_piofcalldriver
mov stat,eax
}
return stat;
}
NTSTATUS DriverIoControl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PIO_STACK_LOCATION pisl;
NTSTATUS ns = STATUS_UNSUCCESSFUL;
ULONG BuffSize, DataSize;
PVOID pBuff, pData,pInout;
KIRQL OldIrql;
ULONG i;
pisl = IoGetCurrentIrpStackLocation (Irp);
BuffSize = pisl->Parameters.DeviceIoControl.OutputBufferLength;
pBuff = Irp->AssociatedIrp.SystemBuffer;
Irp->IoStatus.Information = 0;
switch(pisl->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_DISABLE:
{
DbgPrint("IOCTL_DISABLE");
ns = STATUS_SUCCESS;
}
break;
case IOCTL_ENABLE:
{
DbgPrint("IOCTL_ENABLE");
ns = STATUS_SUCCESS;
}
break;
}
Irp->IoStatus.Status = ns;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return ns;
}
NTSTATUS DrivercreateClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
void UnHookpIofCallDriver()
{
KIRQL oldIrql;
ULONG addr = (ULONG)IofCallDriver;
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
mov eax,cr0
mov oData,eax
and eax,0xffffffff
mov cr0,eax
mov eax,addr
mov esi,[eax+2]
mov eax,old_piofcalldriver
mov dword ptr [esi],eax
mov eax,oData
mov cr0,eax
}
KeLowerIrql(oldIrql);
return ;
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UnHookpIofCallDriver();
IoDeleteSymbolicLink(&SymbolicLinkName);
IoDeleteDevice(deviceObject);
}
NTSTATUS DriverClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
return DrivercreateClose(DeviceObject,Irp);
}
NTSTATUS IoComplete(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
void HookpIofCallDriver()
{
KIRQL oldIrql;
ULONG addr = (ULONG)IofCallDriver;
__asm
{
mov eax,addr
mov esi,[eax+2]
mov eax,[esi]
mov old_piofcalldriver,eax
}
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
mov eax,cr0
mov oData,eax
and eax,0xffffffff
mov cr0,eax
mov eax,addr
mov esi,[eax+2]
mov dword ptr [esi],offset NewpIofCallDriver
mov eax,oData
mov cr0,eax
}
KeLowerIrql(oldIrql);
return ;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
NTSTATUS status;
PDRIVER_DISPATCH *ppdd;
ULONG i;
PCWSTR dDeviceName = L"\\Device\\irphook";
PCWSTR dSymbolicLinkName = L"\\DosDevices\\irphook";
RtlInitUnicodeString(&DeviceName, dDeviceName);
RtlInitUnicodeString(&SymbolicLinkName, dSymbolicLinkName);
status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, &deviceObject);
if (!NT_SUCCESS(status)) return status;
status = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);
DriverObject->DriverUnload = DriverUnload;
ppdd = DriverObject->MajorFunction;
for(i =0;i<=IRP_MJ_MAXIMUM_FUNCTION;i++)
ppdd = IoComplete;
ppdd [IRP_MJ_CREATE] = DrivercreateClose;
ppdd [IRP_MJ_DEVICE_CONTROL ] = DriverIoControl;
g_drvobj = DriverObject;
HookpIofCallDriver();
return status;
}
方法二例子
這個例子比較長,我們只看關鍵代碼並說明.
1。將自己掛接到"\\Device\\KeyboardClass0"設備上
NTSTATUS HookKeyboard(IN PDRIVER_OBJECT pDriverObject)
{
DbgPrint("Entering Hook Routine...\n");
PDEVICE_OBJECT pKeyboardDeviceObject;
NTSTATUS status = IoCreateDevice(pDriverObject,sizeof(DEVICE_EXTENSION), NULL, //no name
FILE_DEVICE_KEYBOARD, 0, true, &pKeyboardDeviceObject);
if(!NT_SUCCESS(status))
return status;
DbgPrint("Created keyboard device successfully...\n");
pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE);
pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags & ~DO_DEVICE_INITIALIZING;
DbgPrint("Flags set succesfully...\n");
RtlZeroMemory(pKeyboardDeviceObject->DeviceExtension, sizeof(DEVICE_EXTENSION));
DbgPrint("Device Extension Initialized...\n");
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pKeyboardDeviceObject->DeviceExtension;
CCHAR ntNameBuffer[64] = "\\Device\\KeyboardClass0";
STRING ntNameString;
UNICODE_STRING uKeyboardDeviceName;
RtlInitAnsiString( &ntNameString, ntNameBuffer );
RtlAnsiStringToUnicodeString( &uKeyboardDeviceName, &ntNameString, TRUE );
IoAttachDevice(pKeyboardDeviceObject,&uKeyboardDeviceName,&pKeyboardDeviceExtension->pKeyboardDevice);
RtlFreeUnicodeString(&uKeyboardDeviceName);
DbgPrint("Filter Device Attached Successfully...\n");
return STATUS_SUCCESS;
}
//我們感興趣的irp處理。由於我們要處理的按鍵信息,需要等底層驅動處理完成返回後才能取回
//按鍵值,因此,我們設置完成例程,用於底層驅動完成irp後回調我們的例程。我們設置好完成例//程後,就把irp傳到底層驅動進行處理。
NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
DbgPrint("Entering DispatchRead Routine...\n");
PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(pIrp);
PIO_STACK_LOCATION nextIrpStack = IoGetNextIrpStackLocation(pIrp);
*nextIrpStack = *currentIrpStack;
IoSetCompletionRoutine(pIrp, OnReadCompletion, pDeviceObject, TRUE, TRUE, TRUE);
numPendingIrps++;
DbgPrint("Tagged keyboard 'read' IRP... Passing IRP down the stack... \n");
return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pKeyboardDevice ,pIrp);
}
//這是完成例程,我們在這裡處理得到的按鍵信息。
NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp, IN PVOID Context)
{
DbgPrint("Entering OnReadCompletion Routine...\n");
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
if(pIrp->IoStatus.Status == STATUS_SUCCESS)
{
PKEYBOARD_INPUT_DATA keys = (PKEYBOARD_INPUT_DATA)pIrp->AssociatedIrp.SystemBuffer;
int numKeys = pIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
for(int i = 0; i < numKeys; i++)
{
DbgPrint("ScanCode: %x\n", keys.MakeCode);
if(keys.Flags == KEY_BREAK)
DbgPrint("%s\n","Key Up");
if(keys.Flags == KEY_MAKE)
DbgPrint("%s\n","Key Down");
KEY_DATA* kData = (KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA));
kData->KeyData = (char)keys.MakeCode;
kData->KeyFlags = (char)keys.Flags;
DbgPrint("Adding IRP to work queue...");
ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,
&kData->ListEntry,
&pKeyboardDeviceExtension->lockQueue);
KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,0,1,FALSE);
}
}
if(pIrp->PendingReturned)
IoMarkIrpPending(pIrp);
numPendingIrps--;
return pIrp->IoStatus.Status;
}
在這個demo中要注意的是,由於irp的處理函數的IRQL = DISPATCH_LEVEL,因此,我們申請內存的話,只能申請非分頁內存。在這個IRQL級別,我們不能創建或者保存文件來記錄按鍵信息。
我們只能創建一個系統線程,在系統線程中完成按鍵信息的保存。
後面附上DEMO.
2010年12月8日 星期三
Hook過濾架構搭建
仿照了下360 的過濾架構,搭建了個Hook
框架,360的Hook架構的確很優秀,我覺得很值得我們學習與研究。這裡我按照大牛們已經逆向出來的思路實現了下代碼(都逆向出來了坐下代碼工作不會怎
麼樣吧?….只是學習架構)。不要鄙視我等代碼工………,好吧大牛們想BS就BS吧,我表示毫無壓力~~~~,我是菜鳥我怕誰!
廢話不多說發代碼,如果有錯誤和白痴的地方請指出,我水平有限…….
搭建這個架構大致需要以下幾個模塊,一是安裝KiFastCallEntry的Hook模塊,二是FakeKiFastCallEntry代理模塊,三是
SysCallFilter系統調用是否過濾的判斷模塊。其餘的模塊主要是過濾函數了,還有個獲取KiFastCallEntry的patch地址的模
塊,最後是釋放模塊和初始化模塊,這樣大致的架構就搭建起來了。
接下來看看每個模塊是怎麼工作的。
首先是安裝KiFastCallEntry的Hook模塊,這個原理我就不多說了,大家都懂的
/************************************************************************
* 函數名稱:HookKiFastCallEntry
* 功能描述:安裝KiFastCallEntry鉤子
* 參數列表:
* 返回值:狀態
*************************************************************************/
NTSTATUS HookKiFastCallEntry()
{
NTSTATUS status=STATUS_SUCCESS;
if (!GetKiFastCallEntryPatchAddr())
{
KdPrint(("(HookKiFastCallEntry) GetKiFastCallEntryPatchAddr failed"));
return STATUS_UNSUCCESSFUL;
}
RtlCopyMemory(OriginalHead2,(PVOID)PatchAddr,5);
*(ULONG *)(ReplaceHead2+1)=(ULONG)FakeKiFastCallEntry-(PatchAddr+5);
KIRQL Irql;
Irql=WOFF();
//寫入新的函數頭
RtlCopyMemory((BYTE *)PatchAddr,ReplaceHead2,5);
WON(Irql);
return status;
}
這個模塊有個地方就是GetKiFastCallEntryPatchAddr()獲取KiFastCallEntry的Patch點這個有點小技巧,大
家可以學習下,360是用SetEvent鉤子棧回朔實現的,這個大家聽了應該都能明白,就是調用函數時候會PUSH
返回到的EIP,這個EIP就是KiFastCallEntry中的了。
要patch的地方是按特徵碼搜索的,這個是xp sp3的
BOOL GetKiFastCallEntryPatchAddr()
{
ULONG ulCallNum;
PULONG pHookAddr;
PBYTE pCode;
ULONG i;
BOOL bRet=true;
KIRQL Irql;
hFakeEvent=(HANDLE)FakeHandle;
ulCallNum=*(PULONG)((PBYTE)ZwSetEvent+1);
pHookAddr=(PULONG)(pSysCallFilterInfo->ulSSDTAddr+ulCallNum*4);
RealNtSetEvent=*pHookAddr;//保存真實地址
Irql=WOFF();
*pHookAddr=(ULONG)FakeNtSetEvent; // 寫入代理地址
WON(Irql);
ZwSetEvent(hFakeEvent,NULL);
Irql=WOFF();
*pHookAddr=RealNtSetEvent; // 寫回真實地址
WON(Irql);
if (MmIsAddressValid((PVOID)BackTrackingAddr))
{
pCode=(PBYTE)BackTrackingAddr;
for (i=0;i<SearchByte;i++)
{
if (*(pCode-i)==0xe1&&*(pCode-i-1)==0x2b)
{
PatchAddr=(ULONG)(pCode-i-1);
break;
}
if (*(pCode-i)==0xfc&&*(pCode-i-1)==0x8b)
{
RetAddress=(ULONG)(pCode-i-1);
}
}
}
if (!PatchAddr||!RetAddress)
{
bRet=false;
}
return bRet;
}
這個代理函數裡面獲取EIP
NTSTATUS FakeNtSetEvent (
__in HANDLE EventHandle,
__out_opt PLONG PreviousState
)
{
NTSTATUS status=STATUS_SUCCESS;
if (EventHandle!=hFakeEvent||ExGetPreviousMode()==UserMode)// 不是自己調用,或者調用來自UserMode,直接調用原函數
{
status=((NTSETEVENT)RealNtSetEvent)(&EventHandle, PreviousState);
}
else
{
_asm
{
mov eax,dword ptr [ebp+4h]
mov BackTrackingAddr,eax
}
}
return status;
}
安裝好Hook後就是Hook的代理函數了
這段代碼 ……..好吧被BS咱也莫有辦法,代理函數傳入三個參數,這三個參數的含義可以參考內核情景分析一書中有詳細介紹,給SysCallFileter來判斷是否過濾。
_declspec (naked) NTSTATUS FakeKiFastCallEntry()
{
_asm
{
mov edi,edi
pushfd
pushad
push edi
push ebx
push eax
call SysCallfilter
mov dword ptr [esp+10h],eax
popad
popfd
sub esp, ecx
shr ecx, 2
push RetAddress
retn
}
}
接下來就是判斷過濾的函數了,這裡我略去了SHADOW SSDT,這段代碼也……
/************************************************************************
* 函數名稱:SysCallfilter
* 功能描述:過濾系統調用
* 參數列表:
ULONG SysCallNum:系統調用號
ULONG FunAddr:系統調用函數入口地址
ULONG ServiceBase:系統調用表指針
* 返回值:過濾則返回代理函數地址,否則返回真實地址
*************************************************************************/
ULONG SysCallfilter(ULONG SysCallNum,ULONG FunAddr,ULONG ServiceBase)
{
if( ServiceBase==pSysCallFilterInfo->ulSSDTAddr&&SysCallNum<=pSysCallFilterInfo->ulSSDTNum)
{
if(pSysCallFilterInfo->SSDTSwitchTable[SysCallNum]&&HookOrNot(SysCallNum,FALSE))
{
return pSysCallFilterInfo->ProxySSDTTable[SysCallNum];//
}
}
return FunAddr;
}
這個模塊可以考慮添加適當的過濾規則,但最好效率點,這裡我沒加什麼過濾,主要是搭建框架。
/************************************************************************
* 函數名稱:HookOrNot
* 功能描述:判斷是否過濾系統調用
* 參數列表:
ULONG SysCallNum:系統調用號
BOOL Flags:SSDT還是SDOWSSDT標誌
* 返回值:返回表示不過濾,表示過濾
*************************************************************************/
ULONG HookOrNot(ULONG SysCallNum,BOOL Flags)
{
if (ExGetPreviousMode()==KernelMode)
{
return 0;
}
if (Flags)
{
return 1;
}
else
return 1;
}
好了基本功能模塊搭建好了,現在就要初始化這些模塊內所要使用的數據結構,來運作起來。
初始化裡面我直接把savessdttable原始函數表填充為文件獲取的原始地址表了,這裡大家可以不必這麼做。
/************************************************************************
* 函數名稱:InitSysCallFilter
* 功能描述:初始化系統調用過濾
* 參數列表:
* 返回值:狀態
*************************************************************************/
NTSTATUS InitSysCallFilter()
{
NTSTATUS status=STATUS_SUCCESS;
PVOID FileBuffer,FunBuffer;
ULONG ulSSDTLimit;
PKSERVICE_TABLE_DESCRIPTOR pServiceDescriptor;
//init
//Init SysCallFilterInfo buffer
pSysCallFilterInfo=(PSYSCALL_FILTER_INFO_TABLE)ExAllocatePoolWithTag(
NonPagedPool,
sizeof(SYSCALL_FILTER_INFO_TABLE),
MM_TAG_FILT);
RtlZeroMemory(pSysCallFilterInfo,sizeof(SYSCALL_FILTER_INFO_TABLE));
//Init SSDT address
pServiceDescriptor=(PKSERVICE_TABLE_DESCRIPTOR)GetKeServiceDescriptorTable();
pSysCallFilterInfo->ulSSDTAddr=(ULONG)pServiceDescriptor->Base;
//Init SSDT Table
FileBuffer=ExAllocatePoolWithTag(NonPagedPool,(SSDT_MAX_NUM)*sizeof(ULONG),MM_TAG_FILT);
FunBuffer=ExAllocatePoolWithTag(NonPagedPool,(SSDT_MAX_NUM)*sizeof(ULONG),MM_TAG_FILT);
if (!FileBuffer||!FunBuffer)
{
KdPrint(("(InitSysCallFilter) MmBuffer FunBuffer failed"));
return STATUS_UNSUCCESSFUL;
}
status=EnumOriginalSSDT(FileBuffer,FunBuffer,&ulSSDTLimit);
if (!NT_SUCCESS(status))
{
KdPrint(("(InitSysCallFilter) EnumOriginalSSDT failed"));
ExFreePool(FileBuffer);
ExFreePool(FunBuffer);
return STATUS_UNSUCCESSFUL;
}
memcpy(pSysCallFilterInfo->SavedSSDTTable,FileBuffer,ulSSDTLimit*4);
ExFreePool(FileBuffer);
ExFreePool(FunBuffer);
pSysCallFilterInfo->ulSSDTNum=ulSSDTLimit;
//Init Proxy SSDT table
pSysCallFilterInfo->ProxySSDTTable[97]=(ULONG)FakeNtLoadDriver;
//這裡就可以隨意添加Hook,相當方便
//Init SSDT Swicth table
pSysCallFilterInfo->SSDTSwitchTable[97]=1;
//記得要開開關
return status;
}
最後是釋放清理模塊了。
void UnHookKiFastCallEntry()
{
KIRQL Irql;
if (*(PULONG)OriginalHead2)
{
Irql=WOFF();
//寫回原來的函數頭
RtlCopyMemory((BYTE *)PatchAddr,OriginalHead2,5);
WON(Irql);
}
};
NTSTATUS FreeSysCallFilter()
{
NTSTATUS status=STATUS_SUCCESS;
UnHookKiFastCallEntry();
if (pSysCallFilterInfo)
{
ExFreePool(pSysCallFilterInfo);
}
return status;
}
這裡順帶髮個過濾函數以及R3通信架構的搭建好了
這個過濾是NtLoadDriver的
NTSTATUS FakeNtLoadDriver( __in PUNICODE_STRING DriverServiceName)
{
PEPROCESS pCurProcess;
DRIVER_TRANS_INFO DriverTransInfo;
if (DriverServiceName==NULL)
{
return ((NTLOADDRIVER)pSysCallFilterInfo->SavedSSDTTable[97])(DriverServiceName);
}
DriverTransInfo.Size=sizeof(DRIVER_TRANS_INFO);
pCurProcess=PsGetCurrentProcess();
if (pCurProcess)
{
GetProcessFullPathW((ULONG)pCurProcess,DriverTransInfo.ProcessFullPath);
}
RtlStringCchCopyW(DriverTransInfo.WarmReason,MAX_REASON*sizeof(WCHAR),L"嘗試加載驅動,一旦加載驅動進程將會獲得最高權限,允許此操作將可能導致危險發生,驅動文件為:");
RtlStringCchCatW(DriverTransInfo.WarmReason,MAX_PATH*sizeof(WCHAR),DriverServiceName->Buffer);
if (!GoOrNot((PVOID)&DriverTransInfo,TYPE_DRIVER_MONITOR))
{
return STATUS_ACCESS_DENIED;
}
else
{
return ((NTLOADDRIVER)pSysCallFilterInfo->SavedSSDTTable[97])(DriverServiceName);
}
}
然後是GoOrNot與R3通信等待R3命令
BOOL GoOrNot(__in PVOID pMonitorInfo,__in ULONG Type)
{
BOOL bRet=false;
switch (Type)
{
case TYPE_DRIVER_MONITOR:
bRet=GetUserCommand(g_DeviceExtension->DriverMonitorInfo.pNotifyEvent,
g_DeviceExtension->DriverMonitorInfo.SharedMemInfo.pShareMemory,
pMonitorInfo,
sizeof(DRIVER_TRANS_INFO));
break;
default:
;
}
return bRet;
}
//獲取用戶層命令
BOOL GetUserCommand(__in PKEVENT pNotifyEvent,
__in PVOID pShareMemory,
__in PVOID pTransInfo,
__in ULONG pTransLen)
{
BOOL bRet;
PDRIVER_TRANS_INFO pDriverTransInfo;
memcpy(pShareMemory,pTransInfo,pTransLen);
KeSetEvent(pNotifyEvent,0,false);
KeWaitForSingleObject(
pNotifyEvent,
Executive,
KernelMode,
false,
NULL);
pDriverTransInfo=(PDRIVER_TRANS_INFO)pShareMemory;
if (pDriverTransInfo->Command==COMMAND_GO)
{
bRet=true;
}
else if (pDriverTransInfo->Command==COMMAND_STOP)
{
bRet=false;
}
return bRet;
}
這裡發段R3和R0共享內存的,用的是內核創建pool在建MDL映射到用戶空間的方法。
BOOL CreateSharedMemory(__out PSHARE_MEMORY_INFO pShareMemInfo,
__in ULONG MemorySize)
{
BOOL bRet=true;
PMDL pMdl;
PVOID UserVAToReturn;
PIO_STACK_LOCATION pIoStackLocation;
ULONG ulBufferLengthOut;
PVOID pSharedBuffer;
pSharedBuffer=ExAllocatePoolWithTag(NonPagedPool,MemorySize,MM_TAG_ANTI);
if (!pSharedBuffer)
{
KdPrint(("(IrpCreateSharedMemory) pSharedBuffer allocate failed"));
return false;
}
pMdl=IoAllocateMdl(pSharedBuffer,MemorySize,false,false,NULL);
if (!pMdl)
{
KdPrint(("(IrpCreateSharedMemory) IoAllocateMdl( failed"));
ExFreePool(pSharedBuffer);
return false;
}
MmBuildMdlForNonPagedPool(pMdl);
UserVAToReturn=MmMapLockedPagesSpecifyCache(pMdl,
UserMode,
MmCached,
NULL,
false,
NormalPagePriority);
if (!UserVAToReturn)
{
IoFreeMdl(pMdl);
ExFreePool(pSharedBuffer);
return false;
}
RtlZeroMemory(pSharedBuffer,MemorySize);
KdPrint(("UserVAToReturn:0x%08x",UserVAToReturn));
//輸出
pShareMemInfo->pShareMemory=pSharedBuffer;
pShareMemInfo->pSharedMdl=pMdl;
pShareMemInfo->UserVA=(ULONG)UserVAToReturn;
return bRet;
}
R3的創建事件和開線程我就不發了,很簡單大家可以自己嘗試下。
最後附下整個架構的部分數據結構
//GoOrNot Type宏定義
#define TYPE_DRIVER_MONITOR 0x01
//GoOrNot Command宏定義
#define COMMAND_GO 0x01
#define COMMAND_STOP 0x02
//危險攔截提示語句
#define WARM_DRI_LOAD L"嘗試加載驅動,一旦加載驅動進程將會獲得系統最高權限,允許此操作將可能導致危險發生,驅動文件路徑:"
//************數據定義***************************************************
typedef struct _SYSCALL_FILTER_INFO_TABLE
{
ULONG ulSSDTAddr;
ULONG ulSHADOWSSDTAddr;
ULONG ulSSDTNum;
ULONG ulSHADOWSSDTNum;
ULONG SavedSSDTTable[SSDT_FILTER_NUM]; //SSDT原始函數地址表
ULONG ProxySSDTTable[SHADOWSSDT_FILTER_NUM]; //SSDT代理函數地址表
ULONG SavedShadowSSDTTable[SSDT_FILTER_NUM]; //ShadowSSDT原始函數地址表
ULONG ProxyShadowSSDTTable[SHADOWSSDT_FILTER_NUM]; //ShadowSSDT代理函數地址表
ULONG SSDTSwitchTable[SSDT_FILTER_NUM]; //SSDT Hook開關表
ULONG ShadowSSDTSwitchTable[SHADOWSSDT_FILTER_NUM];//ShadowSSDT Hook開關表
}SYSCALL_FILTER_INFO_TABLE,*PSYSCALL_FILTER_INFO_TABLE;
好的宏定義也可以簡化工程,這裡大家可自行考慮。
這樣差不多整個架構就搭建起來了,一個小型的監控系統就可以完成了。優秀的架構的確可以事半功倍,不過過濾函數的規則其實才是重中之重呀……………..。大家可以多討論下,這個過濾規則是很需要仔細研究的。當然你要藏著咱也沒辦法呵…………
最後說下:
由於這個源代碼是我一個大文件裡面的一部分,所以整個也不好給出,其實也沒必要給出來,畢竟沒多少技術含量,大家都可以寫得出的,這裡只是就框架總結下而已。全部發了也沒意思一大堆你也不想看,還不如發點核心的,然後你也可以嘗試搭建下自己的更有樂趣呢!
第一次發這種貼,如果有不當之處敬請諒解
對APC的一點理解
異步過程調用(APCs) 是NT異步處理體系結構中的一個基礎部分,理解了它,對於瞭解NT怎樣操作和執行幾個核心的系統操作很有幫助。
1) APCs允許用戶程序和系統元件在一個進程的地址空間內某個線程的上下文中執行代碼。
2)
I/O管理器使用APCs來完成一個線程發起的異步的I/O操作。例如:當一個設備驅動調用IoCompleteRequest來通知I/O管理器,它已
經結束處理一個異步I/O請求時,I/O管理器排隊一個apc到發起請求的線程。然後線程在一個較低IRQL級別,來執行APC.
APC的作用是從系統空間拷貝I/O操作結果和狀態信息到線程虛擬內存空間的一個緩衝中。
3) 使用APC可以得到或者設置一個線程的上下文和掛起線程的執行。
儘管APCs在nt體系結構下被廣泛使用,但是關於怎樣使用它的文檔卻非常的缺乏。本篇我們詳細介紹下nt系統是怎樣處理APCs的,並且記錄導出的nt
函數,方便設備驅動開發者在他們的程序中使用APCs。我也會展示一個非常可靠的NT的APC調度子程序KiDeliverApc的實現,來幫助你更好的
掌握APC調度的內幕。
APC對象
在NT中,有兩種類型的APCs:用戶模式和內核模式。用戶APCs運行在用戶模式下目標線程當前上下文中,並且需要從目標線程得到許可來
運行。特別是,用戶模式的APCs需要目標線程處在alertable等待狀態才能被成功的調度執行。通過調用下面任意一個函數,都可以讓線程進入這種狀
態。這些函數是:KeWaitForSingleObject, KeWaitForMultipleObjects,
KeWaitForMutexObject, KeDelayExecutionThread。
對於用戶模式下,可以調用函數SleepEx, SignalObjectAndWait, WaitForSingleObjectEx,
WaitForMultipleObjectsEx,MsgWaitForMultipleObjectsEx
都可以使目標線程處於alertable等待狀態,從而讓用戶模式APCs執行,原因是這些函數最終都是調用了內核中的
KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject,
KeDelayExecutionThread等函數。另外通過調用一個未公開的alert-test服務KeTestAlertThread,用戶線程
可以使用戶模式APCs執行。
當一個用戶模式APC被投遞到一個線程,調用上面的等待函數,如果返回等待狀態STATUS_USER_APC,在返回用戶模式時,內核轉去控制APC例程,當APC例程完成後,再繼續線程的執行.
和用戶模式APCs比較,內核模式APCs執行在內核模式下。可以被劃分為常規的和特殊的兩類。當APCs被投遞到一個特殊的線程,特殊的內核模式
APCs不需要從線程得到許可來運行。然而,常規的內核模式APCs在他們成功執行前,需要有特定的環境。此外,特殊的內核APC被儘可能快地執行,既只
要APC_LEVEL級上有可調度的活動。在很多情況下,特殊的內核APC甚至能喚醒阻塞的線程。普通的內核APC僅在所有特殊APC都被執行完,並且目
標線程仍在運行,同時該線程中也沒有其它內核模式APC正執行時才執行。用戶模式APC在所有內核模式APC執行完後才執行,並且僅在目標線程有
alertable屬性時才執行。
每一個等待執行的APC都存在於一個線程執行體,由內核管理的隊列中。系統中的每一個線程都包含兩個APC隊列,一個是為用戶模式APCs,另一個是為內核模式APCs的。
NT通過一個成為KAPC的內核控制對象來描述一個APC.儘管DDK中沒有明確的文檔化APCs,但是在NTDDK.H中卻非常清楚的定義了APC對
象。從下面的KAPC對象的定義看,有些是不需要說明的。像Type和Size。Type表示了這是一個APC內核對象。在nt中,每一個內核對象或者執
行體對象都有Type和Size這兩個域。由此處理函數可以確定當前處理的對象。Size表示一個字對齊的結構體的大小。也就是指明了對象佔的內存空間大
小。Spare0看起來有些晦澀難懂,但是它是沒用什麼任何深遠的意義,僅僅是為了內存補齊。其他的域將在下面的篇幅中介紹。
//-------------------------------------------------------------------------------------------------------
幾個函數聲明和結構定義:
typedef struct _KAPC {
CSHORT Type;
CSHORT Size;
ULONG Spare0;
struct _KTHREAD *Thread;
LIST_ENTRY ApcListEntry;
PKKERNEL_ROUTINE KernelRoutine;
PKRUNDOWN_ROUTINE RundownRoutine;
PKNORMAL_ROUTINE NormalRoutine;
PVOID NormalContext;
//
// N.B. The following two members MUST be together.
//
PVOID SystemArgument1;
PVOID SystemArgument2;
CCHAR ApcStateIndex;
KPROCESSOR_MODE ApcMode;
BOOLEAN Inserted;
} KAPC, *PKAPC, *RESTRICTED_POINTER PRKAPC;
//------
APC環境
一個線程在它執行的任意時刻,假設當前的IRQL是在Passive級,它可能需要臨時在其他的進程上下文
中執行代碼,為了完成這個操作,線程調用系統功能函數KeAttachProcess,在從這個調用返回時,線程執行在另一個進程的地址空間。先前所有在
線程自己的進程上下文中等待執行的APCs,由於這時其所屬進程的地址空間不是當前可用的,因此他們不能被投遞執行。然而,新的插入到這個線程的APCs
可以執行在這個新的進程空間。甚至當線程最後從新的進程中分離時,新的插入到這個線程的APCs還可以在這個線程所屬的進程上下文中執行。
為了達到控制APC傳送的這個程度,NT中每個線程維護了兩個APC環境或者說是狀態。每一個APC環境包含了用戶模式的APC隊列和內核模式的APC隊
列,一個指向當前進程對象的指針和三個控制變量,用於指出:是否有未決的內核模式APCs(KernelApcPending),是否有常規內核模式
APC在進行中(KernelApcInProgress),是否有未決的用戶模式的APC(UserApcPending).
這些APC的環境保存在線程對象的ApcStatePointer域中。這個域是由2個元素組成的數組。即:+0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE
typedef struct _KAPC_STATE {
LIST_ENTRY ApcListHead[MaximumMode];
struct _KPROCESS *Process;
BOOLEAN KernelApcInProgress;
BOOLEAN KernelApcPending;
BOOLEAN UserApcPending;
} KAPC_STATE, *PKAPC_STATE, *PRKAPC_STATE;
lkd> dt _kthread
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY
+0x018 InitialStack : Ptr32 Void
+0x01c StackLimit : Ptr32 Void
+0x020 Teb : Ptr32 Void
+0x024 TlsArray : Ptr32 Void
+0x028 KernelStack : Ptr32 Void
+0x02c DebugActive : UChar
+0x02d State : UChar
+0x02e Alerted : [2] UChar
+0x030 Iopl : UChar
+0x031 NpxState : UChar
+0x032 Saturation : Char
+0x033 Priority : Char
+0x034 ApcState : _KAPC_STATE
+0x04c ContextSwitches : Uint4B
+0x050 IdleSwapBlock : UChar
+0x051 Spare0 : [3] UChar
+0x054 WaitStatus : Int4B
+0x058 WaitIrql : UChar
+0x059 WaitMode : Char
+0x05a WaitNext : UChar
+0x05b WaitReason : UChar
+0x05c WaitBlockList : Ptr32 _KWAIT_BLOCK
+0x060 WaitListEntry : _LIST_ENTRY
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
+0x068 WaitTime : Uint4B
+0x06c BasePriority : Char
+0x06d DecrementCount : UChar
+0x06e PriorityDecrement : Char
+0x06f Quantum : Char
+0x070 WaitBlock : [4] _KWAIT_BLOCK
+0x0d0 LegoData : Ptr32 Void
+0x0d4 KernelApcDisable : Uint4B
+0x0d8 UserAffinity : Uint4B
+0x0dc SystemAffinityActive : UChar
+0x0dd PowerState : UChar
+0x0de NpxIrql : UChar
+0x0df InitialNode : UChar
+0x0e0 ServiceTable : Ptr32 Void
+0x0e4 Queue : Ptr32 _KQUEUE
+0x0e8 ApcQueueLock : Uint4B
+0x0f0 Timer : _KTIMER
+0x118 QueueListEntry : _LIST_ENTRY
+0x120 SoftAffinity : Uint4B
+0x124 Affinity : Uint4B
+0x128 Preempted : UChar
+0x129 ProcessReadyQueue : UChar
+0x12a KernelStackResident : UChar
+0x12b NextProcessor : UChar
+0x12c CallbackStack : Ptr32 Void
+0x130 Win32Thread : Ptr32 Void
+0x134 TrapFrame : Ptr32 _KTRAP_FRAME
+0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE
+0x140 PreviousMode : Char
+0x141 EnableStackSwap : UChar
+0x142 LargeStack : UChar
+0x143 ResourceIndex : UChar
+0x144 KernelTime : Uint4B
+0x148 UserTime : Uint4B
+0x14c SavedApcState : _KAPC_STATE
+0x164 Alertable : UChar
+0x165 ApcStateIndex : UChar
+0x166 ApcQueueable : UChar
+0x167 AutoAlignment : UChar
+0x168 StackBase : Ptr32 Void
+0x16c SuspendApc : _KAPC
+0x19c SuspendSemaphore : _KSEMAPHORE
+0x1b0 ThreadListEntry : _LIST_ENTRY
+0x1b8 FreezeCount : Char
+0x1b9 SuspendCount : Char
+0x1ba IdealProcessor : UChar
+0x1bb DisableBoost : UChar
主APC環境是位於線程對象的ApcState 域,即:
+0x034 ApcState : _KAPC_STATE
線程中等待在當前進程上下文中執行的APC保存在ApcState的隊列中。無論何時,NT的APC派發器
(dispatcher)和其他系統元件查詢一個線程未決的APCs時,
他們都會檢查主APC環境,如果這裡有任何未決的APCs,就會馬上被投遞,或者修改它的控制變量稍後投遞。
第二個APC環境是位於線程對象的SavedApcState域,當線程臨時掛接到其他進程時,它是用來備份主APC環境的。
當一個線程調用KeAttachProcess,在另外的進程上下文中執行後續的代碼時,ApcState域的內容就被拷貝到SavedApcState
域。然後ApcState域被清空,它的APC隊列重新初始化,控制變量設置為0,當前進程域設置為新的進程。這些步驟成功的確保先前在線程所屬的進程上
下文地址空間中等待的APCs,當線程運行在其它不同的進程上下文時,這些APCs不被傳送執行。隨後,ApcStatePointer域數組內容被更新
來反映新的狀態,數組中第一個元素指向SavedApcState域,第二個元素指向ApcState域,表明線程所屬進程上下文的APC環境位於
SavedApcState域。線程的新的進程上下文的APC環境位於ApcState域。最後,當前進程上下文切換到新的進程上下文。
對於一個APC對象,決定當前APC環境的是ApcStateIndex域。ApcStateIndex域的值作為ApcStatePointer域數組的索引來得到目標APC環境指針。隨後,目標APC環境指針用來在相應的隊列中存放apc對象.
當線程從新的進程中脫離時(KeDetachProcess),
任何在新的進程地址空間中等待執行的未決的內核APCs被派發執行。隨後SavedApcState
域的內容被拷貝回ApcState域。SavedApcState
域的內容被清空,線程的ApcStateIndex域被設為OriginalApcEnvironment,ApcStatePointer域更新,當前
進程上下文切換到線程所屬進程。
使用APCs
設備驅動程序使用兩個主要函數來利用APCs,
第一個是KeInitializeApc,用來初始化APC對象。這個函數接受一個驅動分配的APC對象,一個目標線程對象指針,APC環境索引(指出
APC對象存放於哪個APC環境),APC的kernel,rundown和normal例程指針,APC類型(用戶模式或者內核模式)和一個上下文參
數。 函數聲明如下:
NTKERNELAPI
VOID
KeInitializeApc (
IN PRKAPC Apc,
IN PKTHREAD Thread,
IN KAPC_ENVIRONMENT Environment,
IN PKKERNEL_ROUTINE KernelRoutine,
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL,
IN KPROCESSOR_MODE ApcMode,
IN PVOID NormalContext
);
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment
} KAPC_ENVIRONMENT;
KeInitializeApc
首先設置APC對象的Type和Size域一個適當的值,然後檢查參數Environment的值,如果是CurrentApcEnvironment,
那麼ApcStateIndex域設置為目標線程的ApcStateIndex域。否則,ApcStateIndex域設置為參數Environment
的值。隨後,函數直接用參數設置APC對象Thread,RundownRoutine,KernelRoutine域的值。為了正確地確定APC的類
型,KeInitializeApc
檢查參數NormalRoutine的值,如果是NULL,ApcMode域的值設置為KernelMode,NormalContext域設置為
NULL。如果NormalRoutine的值不是NULL,這時候它一定指向一個有效的例程,就用相應的參數來設置ApcMode域和
NormalContext域。最後,KeInitializeApc
設置Inserted域為FALSE.然而初始化APC對象,並沒有把它存放到相應的APC隊列中。
從這個解釋看,你可以瞭解到APCs對象如果缺少有效的NormalRoutine,就會被當作內核模式APCs.尤其是它們會被認為是特殊的內核模式APCs.
實際上,I/O管理器就是用這類的APC來完成異步I/O操作。相反地,APC對象定義了有效的NormalRoutine,並且ApcMode域是
KernelMode,就會被當作常規的內核模式APCs,否則就會被當作是用戶模式APCs. NTDDK.H中KernelRoutine,
RundownRoutine, and NormalRoutine 的定義如下:
typedef
VOID
(*PKKERNEL_ROUTINE) (
IN struct _KAPC *Apc,
IN OUT PKNORMAL_ROUTINE *NormalRoutine,
IN OUT PVOID *NormalContext,
IN OUT PVOID *SystemArgument1,
IN OUT PVOID *SystemArgument2
);
typedef
VOID
(*PKRUNDOWN_ROUTINE) (
IN struct _KAPC *Apc
);
typedef
VOID
(*PKNORMAL_ROUTINE) (
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
//------------------
通常,無論是什麼類型,每個APC對象必須要包含一個有效的KernelRoutine 函數指針。當這個APC被NT的APC
dispatcher傳送執行時,這個例程首先被執行。用戶模式的APCs必須包含一個有效的NormalRoutine
函數指針,這個函數必須在用戶內存區域。同樣的,常規內核模式APCs也必須包含一個有效的NormalRoutine,但是它就像
KernelRoutine一樣運行在內核模式。作為可選擇的,任意類型的APC都可以定義一個有效的RundownRoutine,這個例程必須在內核
內存區域,並且僅僅當系統需要釋放APC隊列的內容時,才被調用。例如線程退出時,在這種情況下,KernelRoutine和
NormalRoutine都不執行,只有RundownRoutine執行。沒有這個例程的APC對象會被刪除。
記住,投遞APCs到一個線程的動作,僅僅是操作系統調用KiDeliverApc完成的。執行APC實際上就是調用APC內的例程。
一旦APC對象完成初始化後,設備驅動調用KeInsertQueueApc來將APC對象存放到目標線程的相應的APC隊列中。這個函數接受一個由
KeInitializeApc完成初始化的APC對象指針,兩個系統參數和一個優先級增量。跟傳遞給KeInitializeApc函數的參數
context 一樣,這兩個系統參數隻是在APC的例程執行時,簡單的傳遞給APC的例程。
NTKERNELAPI
BOOLEAN
KeInsertQueueApc (
IN PRKAPC Apc,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2,
IN KPRIORITY Increment
);
//-----------------
在KeInsertQueueApc 將APC對象存放到目標線程相應的APC隊列之前,它首先檢查目標線程是否是APC
queueable。如果不是,函數立即返回FALSE.如果是,函數直接用參數設置SystemArgument1域和SystemArgument2
域,隨後,函數調用KiInsertQueueApc來將APC對象存放到相應的APC隊列。
KiInsertQueueApc
僅僅接受一個APC對象和一個優先級增量。這個函數首先得到線程APC隊列的spinlock並且持有它,防止其他線程修改當前線程的APC結構。隨後,
檢查APC對象的Inserted
域。如果是TRUE,表明這個APC對象已經存放到APC隊列中了,函數立即返回FALSE.如果APC對象的Inserted
域是FALSE.函數通過ApcStateIndex域來確定目標APC環境,然後把APC對象存放到相應的APC隊列中,即將APC對象中的
ApcListEntry
域鏈入到APC環境的ApcListHead域中。鏈入的位置由APC的類型決定。常規的內核模式APC,用戶模式APC都是存放到相應的APC隊列的末
端。相反的,如果隊列中已經存放了一些APC對象,特殊的內核模式APC存放到隊列中第一個常規內核模式APC對象的前面。如果是內核定義的一個當線程退
出時使用的用戶APC,它也會被放在相應的隊列的前面。然後,線程的主APC環境中的UserApcPending域杯設置為TRUE。這時
KiInsertQueueApc 設置APC對象的Inserted
域為TRUE,表明這個APC對象已經存放到APC隊列中了。接下來,檢查這個APC對象是否被排隊到線程的當前進程上下文APC環境中,如果不是,函數
立即返回TRUE。如果這是一個內核模式APC,線程主APC環境中的KernelApcPending域設置為TRUE。
在WIN32 SDK文檔中是這樣描述APCs的:
當一個APC被成功的存放到它的隊列後,發出一個軟中斷,APC將會在線程被調度運行的下一個時間片執行。然而這不是完全正確的。這樣一個軟中斷,僅僅是
當一個內核模式的APC(無論是常規的內核模式APC還是特殊的內核模式APC)針對於調用線程時,才會發出。隨後函數返回TRUE。
1)如果APC不是針對於調用線程,目標線程在Passive權限等級處在等待狀態;
2)這是一個常規內核模式APC
3)這個線程不再臨界區
4)沒有其他的常規內核模式APC仍然在進行中
那麼這個線程被喚醒,返回狀態是STATUS_KERNEL_APC。但是等待狀態沒有aborted。
如果這是一個用戶模式APC,KiInsertQueueApc檢查判斷目標線程是否是alertable等待狀態,並且WaitMode域等於
UserMode。如果是,主APC環境的UserApcPending
域設置為TRUE。等待狀態返回STATUS_USER_APC,最後,函數釋放spinlock,返回TRUE,表示APC對象已經被成功放入隊列。
早期作為APC管理函數的補充,設備驅動開發者可以使用未公開的系統服務NtQueueApcThread來直接將一個用戶模式的APC投遞到某個線程。
這個函數內部實際上是調用了KeInitializeApc 和KeInsertQueueApc 來完成這個任務。
NTSYSAPI
NTSTATUS
NTAPI
NtQueueApcThread (
IN HANDLE Thread,
IN PKNORMAL_ROUTINE NormalRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
NT的APC派發器
NT檢查是否線程有未決的APCs. 然後APC派發器子程序KiDeliverApc在這個線程上下文執行來開始將未決的APC執行。注意,這個行為中斷了線程的正常執行流程,首先將控制權給APC派發器,隨後當KiDeliverApc完成後,繼續線程的執行。
例如:當一個線程被調度運行時,最後一步,上下文切換函數 SwapContext 用來檢查是否新的線程有未決的內核APCs.如果是,SwapContext要麼(1)請求一個APC級別的軟中斷來開始APC執行,由於新線程運行在低的IRQL(Passive級別。
或者(2)返回TRUE,表示新的線程有未決的內核APCs。
究竟是執行(1)還是(2)取決於新線程所處的IRQL級別. 如果它的權限級別高於Passive級,SwapContext 執行(1),如果它是在Passive級,則選擇執行(2).
SwapContext的返回值僅僅是特定系統函數可用的,這些系統函數調用SwapContext來強制切換線程上下文到另一個線程.
然後,當這些系統函數經過一段時間再繼續時,他們通常檢查SwapContext
的返回值,如果是TRUE,他們就會調用APC派發器來投遞內核APCs到當前的線程. 例如:
系統函數KiSwapThread被等待服務用來放棄處理器,直到等待結束。這個函數內部調用SwapContext。當等待結束,繼續從調用
SwapContext處執行時,就會檢查SwapContext的返回值。如果是TRUE,KiSwapThread會降低IRQL級別到APC級,然
後調用KiDeliverApc來在當前線程執行內核APCs.
對於用戶APCs,
內核調用APC派發器僅僅是當線程回到用戶模式,並且線程的主APC環境的UserApcPending域為TRUE時。例如:當系統服務派發器
KiSystemService完成一個系統服務請求正打算回到用戶模式時,它會檢查是否有未決的用戶APCs。在執行上,KiDeliverApc調用
用戶APC的KernelRoutine.
隨後,KiInitializeUserApc函數被調用,用來設置線程的陷阱幀。所以從內核模式退出時,線程開始在用戶模式下執行
。KiInitializeUserApc的函數的作用是拷貝當前線程先前的執行狀態(當進入內核模式時,這個狀態保存在線程內核棧創建的陷阱幀裡),從
內核棧到線程的用戶模式棧,初始化用戶模式APC。APC派發器子程序KiUserApcDispatcher在Ntdll.dll內。最後,加載陷阱幀
的EIP寄存器和Ntdll.dll中KiUserApcDispatcher的地址。當陷阱幀最後釋放時,內核將控制轉交給
KiUserApcDispatcher,這個函數調用APC的NormalRoutine例程,NormalRoutine函數地址以及參數都在棧中,
當例程完成時,它調用NtContinue來讓線程利用在棧中先前的上下文繼續執行,彷彿什麼事情也沒有發生過。
當內核調用KiDeliverApc來執行一個用戶模式APC時,線程中的PreviousMode域被設為UserMode.
TrapFrame域指向線程的陷阱幀。當內核調用KiDeliverApc來執行內核APCs時,線程中的PreviousMode域被設為
KernelMode. TrapFrame域指向NULL。
注意,無論何時只要KernelRoutine被調用,傳遞給它的指針是一個局部的APC屬性的副本,由於APC對象已經脫離了隊列,所以可以安全的在
KernelRoutine中釋放APC內存。此外,這個例程在它的參數被傳遞給其他例程之前,有一個最後的機會來修改這些參數。
結論:
APC提供了一個非常有用的機制,允許在特定的線程上下文中異步的執行代碼。作為一個設備驅動開發者,你可以依賴APCs在某個特定的線程上下文中執行一個例程,而不需要線程的許可和干涉。對於用戶應用程序,用戶模式APCs可以用來有效地實現一些回調通知機制。