Windows內核分析索引目錄:https://www.cnblogs.com/onetrainee/p/11675224.html
APC
1. APC的本質
一個線程,其一直占用着CPU,對CPU擁有所有權,不可能從外部改變行為。
APC的本質就是:通過一個函數,讓線程執行,從而可以從外部改變該線程的行為。
1)APC的掛入位置 _KAPC_STATE
_ETHREAD+0x034 ApcState,存在一個_KAPC_STATE結構體:
其存在兩個雙向鏈表,稱為APC隊列,一個掛內核APC隊列,一個掛用戶APC隊列。
//0x18 bytes (sizeof) struct _KAPC_STATE {
struct _LIST_ENTRY ApcListHead[2]; //0x0 用戶/內核 APC隊列
struct _KPROCESS* Process; //0x10 所掛靠的進程
UCHAR
KernelApcInProgress; //0x14 當前內核APC函數是否執行 0/1
UCHAR KernelApcPending; //0x15 是否存在內核APC 0/1
UCHAR UserApcPending; //0x16 是否存在用戶APC 0/1
};
2)APC存儲的單元 _KAPC
前面我們介紹過 _KAPC_STATE,其中一個Entry_List,里面一個個結構就是 _KAPC結構,如下:
//0x30 bytes (sizeof)
struct _KAPC {
SHORT Type; //0x0 類型
SHORT Size; //0x2 大小
ULONG Spare0; //0x4 未發現使用
struct _KTHREAD* Thread; //0x8 目標線程
struct _LIST_ENTRY ApcListEntry; //0xc APC隊列掛的位置(雙向鏈表)
VOID (*KernelRoutine)(struct _KAPC* arg1, VOID (**arg2)(VOID* arg1, VOID* arg2, VOID* arg3), VOID** arg3, VOID** arg4, VOID** arg5); //0x14 APC完成釋放內存
VOID (*RundownRoutine)(struct _KAPC* arg1); //0x18
VOID (*NormalRoutine)(VOID* arg1, VOID* arg2, VOID* arg3); //0x1c APC函數所在的位置:
如果是內核APC,其是函數地址;如果是用戶APC,則是三環總入口
VOID* NormalContext; //0x20 VOID* SystemArgument1; //0x24 內核APC:略;用戶APC:當前函數的總入口。
VOID* SystemArgument1; //0x24 APC函數的參數
VOID* SystemArgument2; //0x28 APC函數的參數
CHAR ApcStateIndex; //0x2c 掛哪個隊列,有四個值 0,1,2,3
CHAR ApcMode; //0x2d UCHAR 用戶APC 內核APC
Inserted; //0x2e 表示當前APC是否已經掛入
};
3)APC函數何時執行
關注一下KiServiceExit,從零環返回三環就通過這個函數,該函數是系統調用、異常和中斷的必經之路。
APC處理函數通過 _KiDeliverApc 函數來執行,而 KiServiceExit 上來就先檢查是否存在用戶APC,如果有就調用該函數來執行。
該函數先判斷是否存在內核APC,如果內核APC存在就先執行內核APC,然后再執行用戶APC。
2. 備用APC隊列
_Kthread+0x14c SavedApcState存在一個備用APC隊列,其與 +0x034 ApcState位置結構體完全一樣。
其和進程掛靠相關,如果不了解,可以去看《進程與線程》一節,該節后面介紹了進程掛靠相關細節。
1)線程APC中的函數都是與進程相關聯的
線程APC中的函數要執行,執行的是當前CR3的內存地址,但是線程可以掛靠,當線程A掛靠到其他進程的CR3時,
如果此時線程A的APC函數要進行內存讀寫,其就會讀寫掛靠進程的內存地址,顯然會發生錯誤。
2)SavedApc作用:
SavedApc函數就是為了避免當出現線程掛靠時內存讀取錯誤,當線程掛靠時,其將該線程的APC存儲到SavedApc中。
等到解除掛靠,再還原回來,這樣就避免了內存執行錯誤。
3)SavedApc真實運行策略:
在掛靠環境下,也是可以向當前線程插入APC的,比如X進程中A線程掛靠T進程,此時也可以插入APC函數,只不過針對B進程的。
ApcState:B進程相關的APC函數。
SavedApcState:A進程相關的APC函數。
4)_KTHREAD+0x138 ApcStatePointer[2]:
Windows為了方便操作這兩個_APC_STATE,設置了一組指針,在_KTHREAD+0x138處 ApcStatePointer[2],其操作情況如下。
因此,如果找原線程的APC,直接ApcStatePointer[0]就好,找Saved就找ApcStatePointer[1],很好理解。
5)_KTHREAD+0x165 ApcStateIndex 實現組合尋址
0 正常狀態 / 1 掛靠狀態
其經常會結合ApcStatePointer來進行尋址。
A進程的線程掛靠B進程,如果在非掛靠的情況下,此時插入的是A進程的APC,因此為ApcStatePointer[ApcState];
如果此時在掛靠情況下,插入的進程就是關於B進程的APC,此時A進程的APC被備份到SavedApcState,B進程的也為ApcStatePointer[ApcState]。
6)_KTHREAD+0x166 ApcQueueAble
表示當前線程是否可以插入APC,比如線程退出時,不允許插入APC。
此時會將ApcQueueAble置為0,則進制APC掛入。
3. APC的插入
1)KeInitalizeApc函數分析
該函數聲明如下,簡單來說就是對應KAPC中的各個成員(可在文章開頭查看)
2)KAPC.ApcStateIndex 作用
注意,其與KTHREAD.ApcStateIndex同名,但其值只有0/1,我們在之前的進程掛靠講過,配合ApcStatePointer來指向有關地址。
0 原始環境 ;1 掛靠環境 ;2 當前環境 ;3 插入APC時的當前環境。
結合掛靠那一節,我們來分析下面的各種情況,以A進程的線程掛靠B進程為例(可能有點亂,一定結合上面掛靠來看)
0 原始環境:ApcStatePointer[0] 正常:ApcState;掛靠:SavedApcState,其都是寫入A進程的ApcState。
1 掛靠環境:ApcStatePointer[1] 正常:SavedApcState;掛靠:ApcState,都是寫入B進程的ApcState。
2 當前環境:其在初始化時修改為當前線程的Kthread.ApcStateIndex,Pointer[ApcStateIndex],掛靠哪個插入哪個。
3 插入Apc時當前環境: 真正指向插入時(KiInserQueueApc),再做判斷,插入當前進程的Apc中。(初始化到插入時,可能APC又被修改)
3)KiInsertQueueApc函數分析
該函數雖然長,但結構體比較單一,很好分析其對應的操作步驟。
4)Kthread+0x164 Alterable屬性
Kthread+0x164 Alterable,其表示是否可以被用戶APC喚醒。
我們在掛起線程調用SleepEx或WaitForSingleObjectEx,其最后一個參數就是修改這個值(注意,必須是Ex結尾的函數)。
當在KiInsertQueueApc插入用戶KAPC之后,其會判斷是否需要喚醒當前線程,如果此時值為1,則喚醒線程執行用戶APC。
4. 內核APC執行過程
1)APC函數的執行與插入不是一個線程
A線程向B線程插入一個APC,插入的動作在A線程中完成的,但什么時候執行則由B線程決定!所以叫“異步過程調用”。
內核APC函數與用戶APC函數的執行時間和執行方式也有區別。
2)內核APC的時機
①SwapContext
我們在線程切換時,會判斷是否要有用戶APC執行,注意,此時作為SwapContext的返回值返回,其一直返回到KiSwapThread中。
此時如果返回值為1,其會調用KiDeliverApc函數來處理當前線程的Apc。
②KiServiceExit
KiServiceExit中也會判斷是否存在用戶APC,調用KiDeliverApc函數來執行。
3)KiDeliverApc函數分析
內核APC如下(注意,其_LIST_ENTRY偏移在中間,故看起來很不美觀),其直接從_KAPC中取出kernel
5. 用戶APC執行過程
1) 用戶APC函數的執行時機
當程序在零環執行完成返回三環時,其調用_KiServiceExit,此時其調用_KiDeliverApc來檢查是否有派發的APC函數,然后執行。
2) 用戶APC執行流程
當發現有用戶APC要執行時,其處於零環,要執行必須返回三環。
執行流程為:零環->三環(執行用戶APC)->零環->三環(正常退出)。
之前我們在系統調用這中提到過如何從三環進到零環,其三環現場保存在_KTRAP_FRAME(_ETHREAD+0x124)這個結構體中。
此時回去肯定不能從_KTRPA_FRAME中返回三環。
3)構建_CONTEXT結構體返回三環
返回三環時根據_KTRAP_FRAME.Eip來返回三環,因此我們想要處理用戶APC,其必須修改KTrapFrame.EIP。
1> KiDiverApc函數中調用KiInitalUserApc來初始化用戶APC環境
2> KiInitalUserApc函數中調用KiContextFromKframes將TrapFrame轉換為CONTEXT結構體
這一步的目的是為了備份原來的TrapFrame,因為返回三環必然修改TrapFrame,因此將舊的轉換為CONTEXT預先放到三環的堆棧。
3> KiInitalUserApc函數中將_APC_RECORD和_CONTEXT保存到三環的堆棧中
雖然此時處於零環,但是可以從TrapFrame.esp來獲取三環的堆棧地址,然后將兩者保存進去。
因為APC執行完之后還必須從三環進入零環,此時直接在堆棧進行操作進行復原即可。
1* 獲取esp並計算提升堆棧大小
2* 將 _Context 寫入三環地址
3* 將ApcRecord寫入三環地址
4* 保存之后三環的堆棧空間
4> KiInitalUserApc函數中修改TrapFrame為返回三環做准備
其修改很多TrapFrame的值,但對於我們最重要的就是回到三環后的落腳點。
其回到KeUserApcDispatch來執行用戶的APC函數,至於函數地址,ApcRecord.NormalContext存放的是真正的APC函數。
4)總結
理解上面過程,此時,我們就可以通過KiServiceExit函數利用KTrapFrame來返回用戶層,其返回的就是KiInitalizeUserApc函數,然后執行用戶APC。
當用戶APC執行完成之后,返回零環,此時就是Context。我們直接在三環把Context再轉換為KtrapFrame,這之后就很好理解了。