上文中讲到投递User Mode APCs是很特殊的,道理很简单,因为User Mode APC是ring3下的回调函数,显然ring0中的KiDeliverAPC()不能像Kernel Mode APC那样直接call,必须要先回到ring3环境下。当然不能像普通情况那样返回(否则就回到ring3系统调用的地方了),只有一个办法,那就是修改TrapFrame ,欺骗系统返回“APC回调函数”!KiInitializeUserApc就是这么做的!
该函数首先把TrapFrame转化为ContextFrame,然后移动用户态ESP指针,分配大约sizeof(CONTEXT)+sizeof(KAPC_RECORD)个字节:
UserStack-> ……
KAPC_RECORD
……
……
CONTEXT
TopOfStack-> ……
KAPC_RECORD就是KiInitializeUserApc的最后四个参数
nt!_KAPC_RECORD
+0x000 NormalRoutine : Ptr32 void
+0x004 NormalContext : Ptr32 Void
+0x008 SystemArgument1 : Ptr32 Void
+0x00c SystemArgument2 : Ptr32 Void
CONTEXT结构主要来存放被修改前的TrapFrame,之所以用CONTEXT结构是跟APC函数返回有关!
TrapFrame->HardwareEsp = UserStack;
TrapFrame->Eip = (ULONG)KeUserApcDispatcher;
TrapFrame->ErrCode = 0;
从上面的代码看到确实修改了TrapFrame,并且返回到的是ring3下的KeUserApcDispatcher,刚才说的_KAPC_RECORD其实也是它的参数!真正我们的User APC回调函数是由KeUserApcDispatcher调用的!
.func KiUserApcDispatcher@16
.globl _KiUserApcDispatcher@16
_KiUserApcDispatcher@16:
/* Setup SEH stack */
lea eax, [esp+CONTEXT_ALIGNED_SIZE+16]
mov ecx, fs:[TEB_EXCEPTION_LIST]
mov edx, offset _KiUserApcExceptionHandler
mov [eax], ecx
mov [eax+4], edx
/* Enable SEH */
mov fs:[TEB_EXCEPTION_LIST], eax
/* Put the Context in EDI */
pop eax
lea edi, [esp+12]
/* Call the APC Routine */
call eax
/* Restore exception list */
mov ecx, [edi+CONTEXT_ALIGNED_SIZE]
mov fs:[TEB_EXCEPTION_LIST], ecx
/* Switch back to the context */
push 1 ; TestAlert
push edi ;edi->CONTEXT结构
call _ZwContinue@8
;;不会返回到这里的
上面的代码并不难理解,我们的User APC回调函数返回后,立即调用了ZwContinue,这是个ntdll中的导出函数,这个函数又通过系统调用进入kernel中的NtContinue!
NTSTATUS
; NtContinue (
; IN PCONTEXT ContextRecord,
; IN BOOLEAN TestAlert
; )
;
; Routine Description:
;
; This routine is called as a system service to continue execution after
; an exception has occurred. Its function is to transfer information from
; the specified context record into the trap frame that was built when the
; system service was executed, and then exit the system as if an exception
; had occurred.
;
; WARNING - Do not call this routine directly, always call it as
; ZwContinue!!! This is required because it needs the
; trapframe built by KiSystemService.
;
; Arguments:
;
; KTrapFrame (ebp+0: after setup) -> base of KTrapFrame
;
; ContextRecord (ebp+8: after setup) = Supplies a pointer to a context rec.
;
; TestAlert (esp+12: after setup) = Supplies a boolean value that specifies
; whether alert should be tested for the previous processor mode.
;
; Return Value:
;
; Normally there is no return from this routine. However, if the specified
; context record is misaligned or is not accessible, then the appropriate
; status code is returned.
;
;--
NcTrapFrame equ [ebp + 0]
NcContextRecord equ [ebp + 8]
NcTestAlert equ [ebp + 12]
align dword
cPublicProc _NtContinue ,2
push ebp ;ebp->TrapFrame
;
; Restore old trap frame address since this service exits directly rather
; than returning.
;
mov ebx, PCR[PcPrcbData+PbCurrentThread] ; get current thread address
mov edx, [ebp].TsEdx ; restore old trap frame address
mov [ebx].ThTrapFrame, edx ;
;
; Call KiContinue to load ContextRecord into TrapFrame. On x86 TrapFrame
; is an atomic entity, so we don't need to allocate any other space here.
;
; KiContinue(NcContextRecord, 0, NcTrapFrame)
;
mov ebp,esp
mov eax, NcTrapFrame
mov ecx, NcContextRecord
stdCall _KiContinue,
or eax,eax ; return value 0?
jnz short Nc20 ; KiContinue failed, go report error
;
; Check to determine if alert should be tested for the previous processor mode.
;
cmp byte ptr NcTestAlert,0 ; Check test alert flag
je short Nc10 ; if z, don't test alert, go Nc10
mov al,byte ptr [ebx]+ThPreviousMode ; No need to xor eax, eax.
stdCall _KeTestAlertThread,
;如果User APCs不为空,它会设置UserApcPending,
;跟Alertable无关
Nc10: pop ebp ; (ebp) -> TrapFrame
mov esp,ebp ; (esp) = (ebp) -> trapframe
jmp _KiServiceExit2 ; common exit
Nc20: pop ebp ; (ebp) -> TrapFrame
mov esp,ebp ; (esp) = (ebp) -> trapframe
jmp _KiServiceExit ; common exit
stdENDP _NtContinue
NtContinue把CONTEXT结构转化成TrapFrame(回复原来的陷阱帧),然后就从KiServiceExit2处退出系统调用!
;++
;
; _KiServiceExit2 - same as _KiServiceExit BUT the full trap_frame
; context is restored
;
;--
public _KiServiceExit2
_KiServiceExit2:
cli ; disable interrupts
DISPATCH_USER_APC ebp
;
; Exit from SystemService
;
EXIT_ALL ; RestoreAll
KiServiceExit2跟KiServiceExit差不多,只是宏参数的不同!同样如果还有User APC又会进入上文描述的情形,直到没有User APC,至此才会返回真正发起原始系统调用的地方!