文章目錄
Windows APC異步過程調用之二
上一篇我們討論了APC的初始化過程,但是我們還留了疑問,ACP是在什麼時候被執行的呢?本文就來討論一下APC的執行過程。
對於內核APC執行的流程很多,比如KeLowerIrql
降低IRQL到APC級別的時候,就會處理內核APC。那麼用戶層的APC呢,下面來具體研究用戶層的APC的執行過程。
1. 起點
在用戶調用函數從內核退出到應用層的時候,就會開始分發用戶層的APC,這個過程在KiServiceExit
,我們看下這個函數分發APC的地方:
.text:83E3C8DD loc_83E3C8DD: ; CODE XREF: _KiServiceExit+5↑j
.text:83E3C8DD ; _KiServiceExit+6F↓j
.text:83E3C8DD mov ebx, large fs:124h
.text:83E3C8E4 test byte ptr [ebx+2], 2
.text:83E3C8E8 jz short loc_83E3C8F2
.text:83E3C8EA push eax
.text:83E3C8EB push ebx
.text:83E3C8EC call _KiCopyCounters@4 ; KiCopyCounters(x)
.text:83E3C8F1 pop eax
.text:83E3C8F2
.text:83E3C8F2 loc_83E3C8F2: ; CODE XREF: _KiServiceExit+18↑j
.text:83E3C8F2 mov [ebx+_KTHREAD.Alerted], 0
.text:83E3C8F6 cmp [ebx+_KTHREAD.___u12.ApcState.UserApcPending], 0
.text:83E3C8FA jz short loc_83E3C944
.text:83E3C8FC mov ebx, ebp
.text:83E3C8FE mov [ebx+44h], eax
.text:83E3C901 mov dword ptr [ebx+50h], 3Bh
.text:83E3C908 mov dword ptr [ebx+38h], 23h
.text:83E3C90F mov dword ptr [ebx+34h], 23h
.text:83E3C916 mov dword ptr [ebx+30h], 0
.text:83E3C91D mov ecx, 1 ; NewIrql
.text:83E3C922 call ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x)
.text:83E3C928 push eax
.text:83E3C929 sti
.text:83E3C92A push ebx
.text:83E3C92B push 0
.text:83E3C92D push 1
.text:83E3C92F call _KiDeliverApc@12 ; KiDeliverApc(x,x,x)
.text:83E3C934 pop ecx ; NewIrql
.text:83E3C935 call ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
.text:83E3C93B mov eax, [ebx+44h]
.text:83E3C93E cli
.text:83E3C93F jmp short loc_83E3C8DD
這個函數作爲用戶層APC的起點,主要流程如下:
cmp [ebx+_KTHREAD.___u12.ApcState.UserApcPending], 0
判斷UserApcPending
狀態標記,從上一篇分析文檔我們知道,這個標記在線程處於Alerted狀態的時候被設置的;這裏也是MSDN描述的爲什麼用戶層APC必須在Alert狀態才能響應的原因。call ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x)
提升IRQL到APC級別。call _KiDeliverApc@12 ; KiDeliverApc(x,x,x)
開始分發APC。
從上面我們可以看到,APC的分發從KiDeliverApc
開始。
2. KiDeliverApc
這個函數會先分發內核APC,然後再分發用戶APC,過程如下:
VOID
KiDeliverApc (
IN KPROCESSOR_MODE PreviousMode,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame
)
{
//分發內核APC
//...
if ((IsListEmpty(&Thread->ApcState.ApcListHead[UserMode]) == FALSE) && //存在用戶APC
(PreviousMode == UserMode) && (Thread->ApcState.UserApcPending == TRUE)) { //用戶APC正在等待執行
Thread->ApcState.UserApcPending = FALSE; //設置不執行,也就是從這個之後的APC將不執行,需要等待下次Alert
NextEntry = Thread->ApcState.ApcListHead[UserMode].Flink;
Apc = CONTAINING_RECORD(NextEntry, KAPC, ApcListEntry);
KernelRoutine = Apc->KernelRoutine;
NormalRoutine = Apc->NormalRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
RemoveEntryList(NextEntry);
Apc->Inserted = FALSE;
KiUnlockApcQueue(Thread, OldIrql);
(KernelRoutine)(Apc, &NormalRoutine, &NormalContext,
&SystemArgument1, &SystemArgument2); //調用內核輔助函數
if (NormalRoutine == (PKNORMAL_ROUTINE)NULL) {
KeTestAlertThread(UserMode); //沒有用戶層的回調函數,那麼自己alert,執行後面的APC
} else {
KiInitializeUserApc(ExceptionFrame, TrapFrame, NormalRoutine,
NormalContext, SystemArgument1, SystemArgument2);
}
} else {
KiUnlockApcQueue(Thread, OldIrql);
}
}
上面的函數已經註釋比較清楚了,主要流程是通過KiInitializeUserApc
來實現的,下面主要看下這個函數流程。
3. KiInitializeUserApc
整函數的代碼如下:
VOID
KiInitializeUserApc (
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN PKNORMAL_ROUTINE NormalRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
)
{
EXCEPTION_RECORD ExceptionRecord;
CONTEXT ContextFrame;
LONG Length;
ULONG UserStack;
if (TrapFrame->EFlags & EFLAGS_V86_MASK) {
return ;
}
//保存TrapFrame
ContextFrame.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
KeContextFromKframes(TrapFrame, ExceptionFrame, &ContextFrame);
//設置新的TrapFrame
try {
ASSERT((TrapFrame->SegCs & MODE_MASK) != KernelMode); // Assert usermode frame
Length = ((sizeof(CONTEXT) + CONTEXT_ROUND) &
~CONTEXT_ROUND) + sizeof(KAPC_RECORD);
UserStack = (ContextFrame.Esp & ~CONTEXT_ROUND) - Length;
ProbeForWrite((PCHAR)UserStack, Length, CONTEXT_ALIGN);
RtlMoveMemory((PULONG)(UserStack + (sizeof(KAPC_RECORD))),
&ContextFrame, sizeof(CONTEXT)); //ContextFrame保存的陷阱幀
TrapFrame->SegCs = SANITIZE_SEG(KGDT_R3_CODE, UserMode);
TrapFrame->HardwareSegSs = SANITIZE_SEG(KGDT_R3_DATA, UserMode);
TrapFrame->SegDs = SANITIZE_SEG(KGDT_R3_DATA, UserMode);
TrapFrame->SegEs = SANITIZE_SEG(KGDT_R3_DATA, UserMode);
TrapFrame->SegFs = SANITIZE_SEG(KGDT_R3_TEB, UserMode);
TrapFrame->SegGs = 0;
TrapFrame->EFlags = SANITIZE_FLAGS( ContextFrame.EFlags, UserMode );
if (KeGetCurrentThread()->Iopl) {
TrapFrame->EFlags |= (EFLAGS_IOPL_MASK & -1); // IOPL = 3
}
TrapFrame->HardwareEsp = UserStack;
TrapFrame->Eip = (ULONG)KeUserApcDispatcher; //用戶層函數
TrapFrame->ErrCode = 0;
*((PULONG)UserStack)++ = (ULONG)NormalRoutine; //棧上第一個
*((PULONG)UserStack)++ = (ULONG)NormalContext; //第二個
*((PULONG)UserStack)++ = (ULONG)SystemArgument1; //第三個
*((PULONG)UserStack)++ = (ULONG)SystemArgument2; //第四個
} except (KiCopyInformation(&ExceptionRecord,
(GetExceptionInformation())->ExceptionRecord)) {
ExceptionRecord.ExceptionAddress = (PVOID)(TrapFrame->Eip);
KiDispatchException(&ExceptionRecord,
ExceptionFrame,
TrapFrame,
UserMode,
TRUE);
}
return;
}
看到這個函數也許有點莫名其妙,因爲這個函數看起來並沒有調用任何APC的函數,那麼是怎麼分發APC的呢?其實這裏用到一個技巧,就是當系統從內核層退出到用戶層的時候,就會從TRAP_FRAME陷阱幀中獲取用戶層的寄存器信息,用戶層根據這些寄存器信息來執行代碼(例如ebp,esp,eip等等)。
那麼在這個函數做了什麼事情呢?因爲用戶APC是要在用戶層執行的,所以我們執行APC的時候,需要退出到用戶層,這裏的一個技巧就是重新設置TRAP_FRAME,將TRAP_FRAME指向執行APC的地方(也就是KeUserApcDispatcher
並設置好參數信息。
因此其實在KeUserApcDispatcher
中要做兩件事情:
- 執行當前APC例程。
- 將執行地址還原到上次
KiServiceExit
返回陷阱幀的地址。
其實KeUserApcDispatcher
也是這麼做的,我們具體分析一下這些流程。
4. KeUserApcDispatcher
KeUserApcDispatcher
這個函數是指向用戶層的地址,爲KiUserApcDispatcher
,我們看下這個函數的代碼:
.text:77F06F58 ; __stdcall KiUserApcDispatcher(x, x, x, x)
.text:77F06F58 public _KiUserApcDispatcher@16
.text:77F06F58 _KiUserApcDispatcher@16 proc near ; DATA XREF: .text:off_77EF61B8↑o
.text:77F06F58
.text:77F06F58 Context = CONTEXT ptr 10h
.text:77F06F58 arg_2D8 = byte ptr 2DCh
.text:77F06F58
.text:77F06F58 lea eax, [esp+arg_2D8]
.text:77F06F5F mov ecx, large fs:0
.text:77F06F66 mov edx, offset _KiUserApcExceptionHandler@16 ; KiUserApcExceptionHandler(x,x,x,x)
.text:77F06F6B mov [eax], ecx
.text:77F06F6D mov [eax+4], edx
.text:77F06F70 mov large fs:0, eax
.text:77F06F76 pop eax //棧上面第一個參數,爲NormalRoutine(APC例程)
.text:77F06F77 lea edi, [esp-4+Context] //內核中保存的陷阱幀在棧中的位置
.text:77F06F7B call eax //調用APC例程
.text:77F06F7D mov ecx, [edi+2CCh]
.text:77F06F83 mov large fs:0, ecx
.text:77F06F8A push 1 ; TestAlert
.text:77F06F8C push edi ; Context
.text:77F06F8D call _ZwContinue@8 ; ZwContinue(x,x) //繼續回到內核
.text:77F06F92 mov esi, eax
這個主要是兩件事情:
pop eax
取出APC函數,call eax
執行APC函數。call _ZwContinue@8
恢復到執行APC之前的調用。
5. NtContinue
我們主要看一下NtContinue
的執行流程,如下:
.text:83E4039C ; __stdcall NtContinue(x, x)
.text:83E4039C _NtContinue@8 proc near ; DATA XREF: .text:83E6382C↓o
.text:83E4039C
.text:83E4039C var_s0 = dword ptr 0
.text:83E4039C arg_0 = dword ptr 8
.text:83E4039C arg_4 = byte ptr 0Ch
.text:83E4039C arg_34 = dword ptr 3Ch
.text:83E4039C
.text:83E4039C push ebp
.text:83E4039D mov ebx, large fs:124h
.text:83E403A4 mov edx, [ebp+arg_34]
.text:83E403A7 mov [ebx+128h], edx
.text:83E403AD mov ebp, esp
.text:83E403AF mov eax, [ebp+var_s0]
.text:83E403B2 mov ecx, [ebp+arg_0]
.text:83E403B5 push eax
.text:83E403B6 push 0
.text:83E403B8 push ecx
.text:83E403B9 call _KiContinue@12 ; KiContinue(x,x,x) //恢復陷阱幀
.text:83E403BE or eax, eax
.text:83E403C0 jnz short loc_83E403DC
.text:83E403C2 cmp [ebp+arg_4], 0
.text:83E403C6 jz short loc_83E403D4
.text:83E403C8 mov al, [ebx+13Ah]
.text:83E403CE push eax
.text:83E403CF call _KeTestAlertThread@4 ; KeTestAlertThread(x) //繼續APC
.text:83E403D4
.text:83E403D4 loc_83E403D4: ; CODE XREF: NtContinue(x,x)+2A↑j
.text:83E403D4 pop ebp
.text:83E403D5 mov esp, ebp
.text:83E403D7 jmp _KiServiceExit2 //開始退出到應用層
上面代碼中,其中KiContinue
就是恢復陷阱幀的代碼,如下:
NTSTATUS
KiContinue (
IN PCONTEXT ContextRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame
)
{
//...
KeContextToKframes(TrapFrame,
ExceptionFrame,
ContextRecord,
ContextRecord->ContextFlags,
PreviousMode);
//...
}
執行到.text:83E403D7 jmp _KiServiceExit2
這一句語句的時候,這個時候的環境跟上一次執行APC之前開始退出內核層的狀態一致了,所以這裏形成一個循環處理所有的APC例程。