APC機制初探
本質
線程是不能被“殺掉”、“掛起”、“恢復”的,線程在執行的時候自己占據著CPU,別人怎么可能控制它呢?
舉個極端的例子:如果不調用API,屏蔽中斷,并保證代碼不出現異常,線程將永久占用CPU,何談控制呢?所以說線程如果想“死”,一定是自己執行代碼把自己殺死,不存在“他殺”這種情況!
那如果想改變一個線程的行為該怎么辦呢?
可以給他提供一個函數,讓它自己去調用,這個函數就是APC(Asyncroneus Procedure Call),即異步過程調用。
APC隊列
kd> dt _KTHREAD nt!_KTHREAD ... +0x034 ApcState : _KAPC_STATE ... kd> dt _KAPC_STATE nt!_KAPC_STATE +0x000 ApcListHead //2個APC隊列 用戶APC和內核APC +0x010 Process //線程所屬或者所掛靠的進程 +0x014 KernelApcInProgress //內核APC是否正在執行 +0x015 KernelApcPending //是否有正在等待執行的內核APC +0x016 UserApcPending //是否有正在等待執行的用戶APC 用戶APC:APC函數地址位于用戶空間,在用戶空間執行 內核APC:APC函數地址位于內核空間,在內核空間執行
NormalRoutine會找到你提供的APC函數,并不完全等于APC函數的地址。

APC函數何時被執行?
KiServiceExit函數:
這個函數是系統調用、異常或中斷返回用戶空間的必經之路。
KiDeliverApc函數:
負責執行APC函數
逆向TerminateThread/ResumeThread



自己實現APC隊列的插入,在3環調用QueueUserAPC
// APC1.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//
#include
#include
DWORD WINAPI MyThread(LPVOID)
{
int i = 0;
while (true)
{
SleepEx(300, TRUE);
printf("%d", i++);
}
}
void __stdcall MyApcFunction(LPVOID)
{
printf("Run APCFuntion");
printf("APCFunction done");
}
int main(int argc, char* argv[])
{
HANDLE hThread = CreateThread(0, 0, MyThread, 0, 0, 0);
Sleep(1000);
if (!QueueUserAPC((PAPCFUNC)MyApcFunction, hThread, NULL))
{
printf("QueueUserAPC error : %d", GetLastError());
}
getchar();
return 0;
}


QueueUserApc
通過3環的QueueUserApc函數可以完成將APC插入到隊列的操作,首先調用了ntdll.dll的NtQueueApcThread


然后通過0xB4的調用號進入ring0

在windbg里面對應的內核函數為NtQueueApcThread

然后在ntosknl.exe里面定位到NtQueueApcThread

最后是調用KeInitializeApc和KeInsertQueueApc這兩個函數來實現APC的效果

備用APC
備用APC里面有幾個重要的成員
kd> dt _KTHREAD nt!_KTHREAD ... +0x034 ApcState : _KAPC_STATE ... +0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE ... +0x14c SavedApcState : _KAPC_STATE ... +0x165 ApcStateIndex : UChar +0x166 ApcQueueable : UChar
SavedApcState
線程APC隊列中的APC函數都是與進程相關聯的,具體點說:A進程的T線程中的所有APC函數,要訪問的內存地址都是A進程的。
但線程是可以掛靠到其他的進程:比如A進程的線程T,通過修改Cr3(改為B進程的頁目錄基址),就可以訪問B進程地址空間,即所謂“進程掛靠”。
當T線程掛靠B進程后,APC隊列中存儲的卻仍然是原來的APC,具體點說,比如某個APC函數要讀取一個地址為0x12345678的數據,如果此時進行讀取,讀到的將是B進程的地址空間,這樣邏輯就錯誤了
為了避免混亂,在T線程掛靠B進程時,會將ApcState中的值暫時存儲到SavedApcState中,等回到原進程A時,再將APC隊列恢復。
所以,SavedApcState又稱為備用APC隊列。
掛靠環境下ApcState的意義
在掛靠的環境下,也是可以向線程APC隊列插入APC的,那這種情況下,使用的是哪個APC隊列呢?
A進程的T線程掛靠B進程,A是T的所屬進程,B是T的掛靠進程
ApcState B進程相關的APC函數
SavedApcState A進程相關的APC函數
在正常情況下,當前進程就是所屬進程A,如果是掛靠情況下,當前進程就是掛靠進程B。
ApcStatePointer
為了操作方便,_KTHREAD結構體中定義了一個指針數組ApcStatePointer ,長度為2。
正常情況下:
ApcStatePointer[0] 指向 ApcState
ApcStatePointer[1] 指向 SavedApcState
掛靠情況下:
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[1] 指向 ApcState
ApcStateIndex
用來標識當前線程處于什么狀態
0 正常狀態 1 掛靠狀態
ApcStatePointer 與 ApcStateIndex組合尋址
正常情況下,向ApcState隊列中插入APC時:
ApcStatePointer[0] 指向 ApcState 此時 ApcStateIndex 的值為0
ApcStatePointer[ApcStateIndex] 指向 ApcState
掛靠情況下,向ApcState隊列中插入APC時:
ApcStatePointer[1] 指向 ApcState 此時 ApcStateIndex 的值為1
ApcStatePointer[ApcStateIndex] 指向 ApcState
總結:
無論什么環境下,ApcStatePointer[ApcStateIndex] 指向的都是ApcState,ApcState則總是表示線程當前使用的apc狀態
APC掛入
無論是正常狀態還是掛靠狀態,都有兩個APC隊列,一個內核隊列,一個用戶隊列。
每當要掛入一個APC函數時,不管是內核APC還是用戶APC,內核都要準備一個KAPC的數據結構,并且將這個KAPC結構掛到相應的APC隊列中。
KAPC
kd> dt _KAPC nt!_KAPC +0x000 Type //類型 APC類型為0x12 +0x002 Size //本結構體的大小 0x30 +0x004 Spare0 //未使用 +0x008 Thread //目標線程 +0x00c ApcListEntry //APC隊列掛的位置 +0x014 KernelRoutine //指向一個函數(調用ExFreePoolWithTag 釋放APC) +0x018 RundownRoutine//略 +0x01c NormalRoutine //用戶APC總入口 或者 真正的內核apc函數 +0x020 NormalContext //內核APC:NULL 用戶APC:真正的APC函數 +0x024 SystemArgument1//APC函數的參數 +0x028 SystemArgument2//APC函數的參數 +0x02c ApcStateIndex //掛哪個隊列,有四個值:0 1 2 3 +0x02d ApcMode //內核APC 用戶APC +0x02e Inserted //表示本apc是否已掛入隊列 掛入前:0 掛入后 1
掛入流程

KeInitializeApc(APC初始化)
VOID KeInitializeApc ( IN PKAPC Apc,//KAPC指針 IN PKTHREAD Thread,//目標線程 IN KAPC_ENVIRONMENT TargetEnvironment,//0 1 2 3四種狀態 IN PKKERNEL_ROUTINE KernelRoutine,//銷毀KAPC的函數地址 IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL, IN PKNORMAL_ROUTINE NormalRoutine,//用戶APC總入口或者內核apc函數 IN KPROCESSOR_MODE Mode,//要插入用戶apc隊列還是內核apc隊列 IN PVOID Context//內核APC:NULL 用戶APC:真正的APC函數 )
主要看TargetEnvironment這個參數,對應的是ApcStateIndex,與KTHREAD(+0x165)的屬性同名,但含義不一樣
ApcStateIndex 有四個值: 0 原始環境 1 掛靠環境 2 當前環境 3 插入APC時的當前環境 正常情況下: ApcStatePointer[0] 指向 ApcState ApcStatePointer[1] 指向 SavedApcState 掛靠情況下: ApcStatePointer[0] 指向 SavedApcState ApcStatePointer[1] 指向 ApcState 2 初始化的時候,當前進程的ApcState 3 插入的時候,當前進程的ApcState
當傳入值為2時,會直接使用當前進程的ApcState


當傳入值為3時,使用的是當前進程的APC,那么這里跟2有什么區別呢?
當初始化的時候可能處于原始環境,也可能處于掛靠環境,在即將插入的那個時候可能環境發生了改變,所以傳入值設置為3
偽代碼分析

KiInsertQueueApc(插入APC隊列)
1) 根據KAPC結構中的ApcStateIndex找到對應的APC隊列 2) 再根據KAPC結構中的ApcMode確定是用戶隊列還是內核隊列 3) 將KAPC掛到對應的隊列中(掛到KAPC的ApcListEntry處) 4) 再根據KAPC結構中的Inserted置1,標識當前的KAPC為已插入狀態 5) 修改KAPC_STATE結構中的KernelApcPending/UserApcPending
1、Alertable=0 當前插入的APC函數未必有機會執行:UserApcPending = 0
2、Alertable=1 UserApcPending = 1 將目標線程喚醒(從等待鏈表中摘出來,并掛到調度鏈表)
KeInsertQueueApc源碼
NTKERNELAPI BOOLEAN KeInsertQueueApc ( __inout PRKAPC Apc, __in_opt PVOID SystemArgument1, __in_opt PVOID SystemArgument2, __in KPRIORITY Increment //優先級,3環添加用戶APC時默認為0. ); /*++ Routine Description: This function inserts an APC object into the APC queue specifed by the thread and processor mode fields of the APC object. If the APC object is already in an APC queue or APC queuing is disabled, then no operation is performed. Otherwise the APC object is inserted in the specified queue and appropriate scheduling decisions are made. Arguments: Apc - Supplies a pointer to a control object of type APC. SystemArgument1, SystemArgument2 - Supply a set of two arguments that contain untyped data provided by the executive. Increment - Supplies the priority increment that is to be applied if queuing the APC causes a thread wait to be satisfied. Return Value: If the APC object is already in an APC queue or APC queuing is disabled, then a value of FALSE is returned. Otherwise a value of TRUE is returned. --*/

首先根據之前傳入的Enviroment來判斷要取哪個_KPAC_STATE成員

選擇完_KPAC_STATE后,判斷ApcMode與NormalRoutine決定插入到哪個鏈表中。經分析得知,用戶APC回調存在NormalRoutine中,但KernelRoutine會存一個名為PsExitSpecialApc的特殊APC回調(用于釋放當前APC內存空間)。

如果當前APC插入到了備用APC隊列(SavedApcState)中就返回。如果插入的是ApcState隊列中就繼續判斷這個APC是自身插入還是其他線程插入的

如果是插入到其他線程的APC并且是個用戶APC

如果這個APC是內核APC并且是插入到其他線程的

APC的插入位置與傳入的Enviroment函數相關。如果是插入到了備用APC隊列中則執行返回。若是普通APC隊列中則繼續進行多個判斷。
- 當這個APC是自身線程插入給自身的,并且是個特殊內核APC,則會立馬觸發軟中斷執行。
- 如果這個APC是當前線程插入給其他線程的,且是個用戶APC。當APC所屬線程處于等待時,會嘗試喚醒線程來執行APC。如果不是等待狀態,則
UserOrNormalKernel默認為0,插入后不執行APC。 - 如果這個APC是當前線程插入給其他線程的,且是個內核APC。當APC所屬線程處于運行時,會直接觸發軟中斷執行APC或通知其他核觸發軟中斷執行。當APC所屬線程處于等待時,會嘗試喚醒線程來執行APC。其他狀態則不會立馬執行APC。