close

異步過程調用(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可以用來有效地實現一些回調通知機制。

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 殘月影 的頭像
    殘月影

    Rootkit --逆向工程技術--

    殘月影 發表在 痞客邦 留言(0) 人氣()