上文中讲到投递 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 , <ecx, 0, eax>
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 , <eax> ; test alert for current thread
; 如果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 ,至此才会返回真正发起原始系统调用的地方!