Windows APC異步過程調用之二

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的起點,主要流程如下:

  1. cmp [ebx+_KTHREAD.___u12.ApcState.UserApcPending], 0 判斷UserApcPending狀態標記,從上一篇分析文檔我們知道,這個標記在線程處於Alerted狀態的時候被設置的;這裏也是MSDN描述的爲什麼用戶層APC必須在Alert狀態才能響應的原因。
  2. call ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x) 提升IRQL到APC級別。
  3. 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中要做兩件事情:

  1. 執行當前APC例程。
  2. 將執行地址還原到上次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

這個主要是兩件事情:

  1. pop eax取出APC函數,call eax執行APC函數。
  2. 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例程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章