内核模式到用户模式的回调函数----这篇文章是十年前国外大牛写的
2017-10-23 22:38
309 查看
内核模式到用户模式的回调函数
http://www.nynaeve.net/?p=200NTDLL 拥有一些特定的函数被内核用来代表用户模式执行特定的功能。尽管理解这些函数对于特定的功能(比如用户模式APC)的底层实现的理解是有用的,这些函数提供的功能非常的简单。
下面是一些NTDLL导出的,内核用于与用户模式通讯的函数:
1.KiUserExceptionDispatcher
2. KiUserApcDispatcher
3. KiRaiseUserExceptionDispatcher
4. KiUserCallbackDispatcher
5. LdrInitializeThunk
6. RtlUserThreadStart
7. EtwpNotificationThread
8. LdrHotPatchRoutine
(还有其它的一些导出函数,但是不直接用于和用户模式交流)
尽管每个函数的特定的被调用原因非常的不同,这些函数通常用来通知用户模式某一事件的发生。
KiUserExceptionDispatcher,KiUserApcDispatcher, 和KiRaiseUserExceptionDispatcher只在从用户模式进入内核模式时使用,或者隐式地,因为一个处理器中断 (比如说,一个页面错误将最终导致一个非法访问), 或者显式地,用于系统调用(比如 NtWaitForSingleObject)。内核调用这些入口点的方法就是,当要从内核模式返回到用户模式的时候,修改将要被设置的线程上下文。返回用户模式的上下文信息(一个 KTRAP_FRAME结构) 被修改,因此当从内核模式返回的时候,返回地址被转移到上面的三个分发函数中的一个,而不是执行用户模式转移到内核模式的指令所在的地址。如果需要的话,将给这些分发函数提供参数。
KiUserCallbackDispatcher被内核用来显式调用到用户模式。在支持如待定IRP“反向调用模型”等模型的时候,这个反向操作模型并不被鼓励使用。尽管如此,由于历史原因, Win32子系统(win32k.sys) 在许多任务上使用这个函数。(比如因为一个内核模式窗口消息操作而调用一个用户模式窗口处理函数)。用户模式调用机制不能被扩展为“支持任意的用户模式回调函数目的地” 。
LdrInitializeThunk是任意的用户模式线程在执行真正的线程入口点之前执行的第一个指令。因此,这是系统范围内用户模式线程开始执行的地址。
Windows Vista 和Windows Server 2008(和之后的版本)上,如果调用NtCreateThreadEx 生成线程,RtlUserThreadStart将被用来形成初始入口点上下文 (这与NtCreateThread所采用的方法明显的不同,其中用户模式调用者提供了初始的线程上下文)。
EtwpNotificationThread 和LdrHotPatchRoutine 都相当于一个标准的线程入口点函数。内核在特定的进程的上下文中生成一些线程以代表内核完成某些任务时,这些线程可能会引用这些入口点。之后的两个函数极少遇见,这一系列文章不对它们进行详细的介绍。
尽管或多或少的未兑现“在带热更新的Windows Server 2003系统上更少的重新引导的承诺”,我在支持热补丁的操作系统的整个生命周期中看到了一个或者两个热更新。
KiUserExceptionDispatcher
在上面所列举的一系列用户模式回调入口点中,尽管其中的一些回调在操作模式上有一些相似之处,但就调用约定和它们执行的功能而言,它们之间还是有特别大的区别的。KiUserExceptionDispatcher 是SEH分发器的用户模式的负责函数。当一个异常发生的时候,该异常将生成一个异常事件,内核检查该异常是否是由于执行用户模式代码导致的。如果是这样的话,内核修改栈上的trap frame,因此当内核从中断或者异常返回的时候,线程将从KiUserExceptionDispatcher 函数执行而不是导致异常的指令。内核将另外安排几个参数(一个 PCONTEXT 和一个 PEXCEPTION_RECORD),它们描述了异常发生时机器的状态,而且在线程返回到用户模式之前被传递给KiUserExceptionDispatcher 函数。一旦内核模式栈展开,而且指令转移到用户模式的KiUserExceptionDispatcher 函数,该函数通过调用一个本地的函数RtlDispatchException来处理异常,RtlDispatchException是用户模式异常处理逻辑中的核心函数。如果异常被成功分发的话(也就是SHE 链表中有一个函数宣称可以处理该异常), RtlDispatchException调用RtlRestoreContext 函数实现最终的用户模式上下文的设置,该函数只是加载给定的上下文中的寄存器到到处理器的体系结构执行状态中。
否则,通过调用 NtRaiseException 函数,异常重新被提交到内核模式,这是最后一次机会了。在内核停止该进程之前,这给了用户模式调试器(如果有的话)一个处理该异常的最后机会。 (内核内部在安排KiUserExceptionDispatcher执行之前给了用户模式调试器和内核模式调试器第一次处理该异常的机会)
下面是汇编代码注释以及一个假的C语言表示:
.text:7C958550 ; __stdcall KiUserExceptionDispatcher(x, x) .text:7C958550 public _KiUserExceptionDispatcher@8 .text:7C958550 _KiUserExceptionDispatcher@8 proc near ; DATA XREF: .text:off_7C94C618o .text:7C958550 .text:7C958550 var_C = dword ptr -0Ch .text:7C958550 var_8 = dword ptr -8 .text:7C958550 var_4 = dword ptr -4 .text:7C958550 arg_0 = dword ptr 4 .text:7C958550 .text:7C958550 mov ecx, [esp+arg_0] ; CONTEXT .text:7C958554 mov ebx, [esp+0] ; EXCEPTION_RECORD .text:7C958557 push ecx .text:7C958558 push ebx .text:7C958559 call _RtlDispatchException@8 ; RtlDispatchException(x,x) .text:7C95855E or al, al .text:7C958560 jz short loc_7C95856E ;如果返回FALSE .text:7C958562 pop ebx ; ebx = EXCEPTION_RECORD .text:7C958563 pop ecx ; ecx = CONTEXT .text:7C958564 push 0 .text:7C958566 push ecx ; ecx = CONTEXT .text:7C958567 call _ZwContinue@8 ;已经处理好了,按照CONTEXT 中设置的值继续执行就好了,此函数不返回 .text:7C95856C jmp short loc_7C958579 .text:7C95856E ; --------------------------------------------------------------------------- .text:7C95856E .text:7C95856E loc_7C95856E: ; 没有找到处理函数,提交一个异常->FirstChance = FALSE .text:7C95856E pop ebx ; ebx = EXCEPTION_RECORD .text:7C95856F pop ecx ; ecx = CONTEXT .text:7C958570 push 0 .text:7C958572 push ecx .text:7C958573 push ebx .text:7C958574 call _ZwRaiseException@12 ; ZwRaiseException(x,x,x) .text:7C958574 _KiUserExceptionDispatcher@8 endp ; sp-analysis failed .text:7C958574 .text:7C958579 ; --------------------------------------------------------------------------- .text:7C958579 retn 8 .text:0000000078EA124A public KiUserExceptionDispatcher .text:0000000078EA124A KiUserExceptionDispatcher proc near ; DATA XREF: .rdata:0000000078F54BB0o .text:0000000078EA124A ; .rdata:off_78F56298o .text:0000000078EA124A cld .text:0000000078EA124B mov rax, cs:Wow64PrepareForException .text:0000000078EA1252 test rax, rax .text:0000000078EA1255 jz short loc_78EA1266 .text:0000000078EA1257 mov rcx, rsp .text:0000000078EA125A add rcx, 4F0h ; rcx 为第一个参数ExceptionRecord 0x4F0 为其CONTEXT 的大小 .text:0000000078EA1261 mov rdx, rsp ; rdx 为第二个参数,指向CONTEXT 结构 .text:0000000078EA1264 call rax ; Wow64PrepareForException .text:0000000078EA1266 .text:0000000078EA1266 loc_78EA1266: .text:0000000078EA1266 mov rcx, rsp .text:0000000078EA1269 add rcx, 4F0h ;ExceptionRecord .text:0000000078EA1270 mov rdx, rsp ;ContextRecord .text:0000000078EA1273 call RtlDispatchException ;分发该异常RtlDispatchException(ExceptionRecord,ContextRecord); .text:0000000078EA1278 test al, al .text:0000000078EA127A jz short loc_78EA1288 .text:0000000078EA127C mov rcx, rsp ; ContextRecord .text:0000000078EA127F xor edx, edx ; ExceptionRecord-->0 .text:0000000078EA1281 call RtlRestoreContext .text:0000000078EA1286 jmp short loc_78EA129D .text:0000000078EA1288 ; --------------------------------------------------------------------------- .text:0000000078EA1288 .text:0000000078EA1288 loc_78EA1288: .text:0000000078EA1288 mov rcx, rsp .text:0000000078EA128B add rcx, 4F0h .text:0000000078EA1292 mov rdx, rsp .text:0000000078EA1295 xor r8b, r8b .text:0000000078EA1298 call ZwRaiseException ;ZwRaiseException(ExceptionRecord,ContextRecord,FALSE); .text:0000000078EA129D .text:0000000078EA129D loc_78EA129D: .text:0000000078EA129D mov ecx, eax .text:0000000078EA129F call RtlRaiseStatus ; RtlRaiseStatus(上面的函数的返回值); .text:0000000078EA12A4 nop .text:0000000078EA12A5 jmp short $+2 .text:0000000078EA12A7 ; --------------------------------------------------------------------------- .text:0000000078EA12A7 .text:0000000078EA12A7 loc_78EA12A7: .text:0000000078EA12A7 nop .text:0000000078EA12A7 KiUserExceptionDispatcher endp ; sp-analysis failed VOID KiUserExceptionDispatcher( __in PCONTEXT ContextRecord, __in PEXCEPTION_RECORD ExceptionRecord, ) { NTSTATUS Status; // // (A custom calling convention is used that does not pass the parameter // values in a C-compatible fashion.) // #if defined(_WIN64) // // 如果Wow64.dll 注册它的帮助函数来处理异常事件,调用这个函数 if (Wow64PrepareForException) Wow64PrepareForException( ExceptionRecord, ContextRecord ); #endif if (RtlDispatchException( ExceptionRecord, ContextRecord)) { #if defined(_WIN64) RtlRestoreContext( ContextRecord ); #else NtContinue( ContextRecord, FALSE ); #endif Status = (NTSTATUS)ContextRecord->Rax; RtlRaiseStatus( Status ); // // No return from RtlRaiseStatus. // } Status = NtRaiseException( ContextRecord, ExceptionRecord, FALSE ); RtlRaiseStatus( Status ); // // No return from RtlRaiseStatus. // }
并不是所有的用户模式异常都像这样从内核模式开始。在很多情况下(比如 RaiseException ),异常处理过程是完全从用户模式发起的,并且并不涉及到KiUserExceptionDispatcher 。
http://msdn2.microsoft.com/en-us/library/ms680552.aspx
KiUserApcDispatcher
和异常类似,用户模式APC 是通过NTDLL 中导出的一个单独的分发器函数KiUserApcDispatcher 实现的。该函数构建异常帧并通过Kernel32的一个导出函数来调用真正的用户提供的APC 函数,之后通过NtContinue 函数返回到被用户模式APC 打断的“调用可警醒系统调用的指令”的下一个指令的位置。细节可以通过查看wrk 和 内核情景分析的APC 相关的章节了解。下面是windows server 2003 x86 中 KiUserApcDispatcher 的反汇编,以及原文作者提供的一个C 语言版本的KiUserApcDispatcher 实现。.text:7C9584A0 ; __stdcall KiUserApcDispatcher(x, x, x, x) .text:7C9584A0 public _KiUserApcDispatcher@16 .text:7C9584A0 _KiUserApcDispatcher@16 proc near ; DATA XREF: .text:off_7C94C618o .text:7C9584A0 .text:7C9584A0 arg_C = byte ptr 10h .text:7C9584A0 arg_2D8 = byte ptr 2DCh .text:7C9584A0 .text:7C9584A0 lea eax, [esp+arg_2D8] .text:7C9584A7 mov ec 185ce x, large fs:0 .text:7C9584AE mov edx, offset _KiUserApcExceptionHandler@16 ; 执行APC 时候的异常处理函数 .text:7C9584B3 mov [eax], ecx .text:7C9584B5 mov [eax+4], edx .text:7C9584B8 mov large fs:0, eax ; 安装异常 .text:7C9584BE pop eax ; [CONTEXT] .text:7C9584BE ; SystemArgument2 .text:7C9584BE ; SystemArgument1 .text:7C9584BE ; NormalContext .text:7C9584BE ; NormalRoutine .text:7C9584BF lea edi, [esp-4+arg_C] .text:7C9584C3 call eax ; 调用该APC 函数 .text:7C9584C5 mov ecx, [edi+2CCh] .text:7C9584CB mov large fs:0, ecx .text:7C9584D2 push 1 .text:7C9584D4 push edi .text:7C9584D5 call _ZwContinue@8 ; ZwContinue(x,x) .text:7C9584DA mov esi, eax .text:7C9584DC .text:7C9584DC loc_7C9584DC: ; CODE XREF: .text:7C9584E2j .text:7C9584DC push esi .text:7C9584DD call _RtlRaiseStatus@4 ; RtlRaiseStatus(x) .text:7C9584DD _KiUserApcDispatcher@16 endp ; sp-analysis failed .text:7C9584DD .text:7C9584E2 ; --------------------------------------------------------------------------- .text:7C9584E2 jmp short loc_7C9584DC .text:7C9584E4 ; ------------------------- VOID KiUserApcDispatcher( __in PCONTEXT Context, __in PVOID ApcContext, __in PVOID Argument1, __in PVOID Argument2, __in PKNORMAL_ROUTINE ApcRoutine ) { NTSTATUS Status; try_again: // // A custom calling convention is used, the actual implementation is not C. // // // Call the user APC routine. Note that in Windows x64, the user APC routine // has a hidden additional (fourth) argument that does not strictly conform to // the standard KNORMAL_ROUTINE procedure type. This hidden argument is made // possible by virtue of the fact that the x64 calling convention passes // arguments in registers. // #if defined(_WIN64) ApcRoutine( ApcContext, Argument1, Argument2, Context ); #else ApcRoutine( ApcContext, Argument1, Argument2 ); #endif // // Continue execution. Note that the TestAlert parameter is set to TRUE. // This will invoke the next queued user mode APC, if any exists. Otherwise, // control is resumed at the specified machine register context. In either // case, NtContinue should never normally "return" to its caller; control is // hard transferred to either the desired context, or to KiUserApcDispatcher. // Status = NtContinue( Context, TRUE ); if (Status == STATUS_SUCCESS) goto try_again; RtlRaiseStatus( Status ); // // No return from RtlRaiseStatus. // }
上面的C 语言版本的实现仅仅是便于程序功能的理解,实际的实现还是要看汇编代码。另外有关X64 APC 以及WOW64 APC 的实现细节请暂时没有深入研究。
KiRaiseUserExceptionDispatcher
当一个系统调用想在用户模式引起异常,而不仅仅是返回一个NTSTATUS的时候,使用KiRaiseUserExceptionDispatcher函数是标准的惯例。它使用一个传入的状态码建立一个标准的异常记录(必须提前写入到当前线程的TEB中的一个众所周知的地方。),并将此异常记录传递给RtlRaiseException (Win32 RaiseException 函数的内部实现函数就是它)。// 系统初始化的时候进行的赋值 //PsInitSystem->PspInitPhase1->PspInitializeSystemDll->PspLookupKernelUserEntryPoints()->PspLookupKernelUserEntryPoints-> EntryName = "KiRaiseUserExceptionDispatcher"; Status = PspLookupSystemDllEntryPoint(EntryName, (PVOID *)&KeRaiseUserExceptionDispatcher);
// NtCloseHandle 里面使用该函数 NTSTATUS KeRaiseUserException( IN NTSTATUS ExceptionCode ) /*++ 函数导致在调用线程的用户模式环境中触发一个异常, 通过修改进入内核时候建立的跳转帧,使其指向那个“触发所请求的异常的跳转代码”来实现 --*/ { PKTHREAD Thread; PKTRAP_FRAME TrapFrame; PTEB Teb; ULONG PreviousEsp; Thread = KeGetCurrentThread(); TrapFrame = Thread->TrapFrame; if (TrapFrame == NULL || ((TrapFrame->SegCs & MODE_MASK) != UserMode)) { return ExceptionCode; } Teb = (PTEB)Thread->Teb; try { Teb->ExceptionCode = ExceptionCode; PreviousEsp = KiEspFromTrapFrame (TrapFrame) - sizeof (ULONG); ProbeForWriteSmallStructure ((PLONG)PreviousEsp, sizeof (LONG), sizeof (UCHAR)); *(PLONG)PreviousEsp = TrapFrame->Eip; // 栈上手动申请了一个内存,然后放入之前的代码的EIP // 执行完我们的代码之后将返回到原来的位置继续执行。 } except(EXCEPTION_EXECUTE_HANDLER) { return(ExceptionCode); } KiEspToTrapFrame (TrapFrame, PreviousEsp);// 设置新的ESP TrapFrame->Eip = (ULONG)KeRaiseUserExceptionDispatcher;// 将EIP 设置为我们的跳板函数的地址 return ExceptionCode; }
// 下面分别是汇编版本和C 语言版本的实现 .text:7C95859C _KiRaiseUserExceptionDispatcher@0 proc near ; DATA XREF: .text:off_7C94C618o .text:7C95859C .text:7C95859C var_50 = EXCEPTION_RECORD ptr -50h .text:7C95859C .text:7C95859C push ebp .text:7C95859D mov ebp, esp .text:7C95859F sub esp, 50h .text:7C9585A2 mov [esp+50h+var_50.ExceptionAddress], eax .text:7C9585A6 mov eax, large fs:18h .text:7C9585AC mov eax, [eax+1A4h] .text:7C9585B2 mov [esp+50h+var_50.ExceptionCode], eax ; STATUS_INVALID_HANDLE .text:7C9585B5 mov [esp+50h+var_50.ExceptionFlags], 0 .text:7C9585BD mov [esp+50h+var_50.ExceptionRecord], 0 .text:7C9585C5 mov [esp+50h+var_50.NumberParameters], 0 .text:7C9585CD push esp ; a1 .text:7C9585CE call _RtlRaiseException@4 ; RtlRaiseException(x) .text:7C9585CE _KiRaiseUserExceptionDispatcher@0 endp
VOID KiRaiseUserExceptionDispatcher( VOID ) { EXCEPTION_RECORD ExceptionRecord; // // 建立一个标准的异常记录,状态码通过TEB 中的一个专用的成员指定 // 异常地址描述了函数将要返回的地址,这个异常是可继续执行的 // ExceptionRecord.ExceptionCode = NtCurrentTeb()->ExceptionCode; ExceptionRecord.ExceptionFlags = 0; // Noncontinuable flag not set. ExceptionRecord.Exceptionrecord = 0; // No chained exception record. ExceptionRecord.ExceptionAddress = _ReturnAddress();//汇编代码是通过eax 传递 ExceptionRecord.NumberOfParameters = 0; RtlRaiseException( &ExceptionRecord ); }
// 再下面是关键的函数的实现以及解析 .text:7C9585FF ; void __stdcall __noreturn RtlRaiseException(PEXCEPTION_RECORD a1) .text:7C9585FF public _RtlRaiseException@4 .text:7C9585FF _RtlRaiseException@4 proc near ; CODE XREF: KiUserExceptionDispatcher(x,x)+44p .text:7C9585FF ; KiRaiseUserExceptionDispatcher()+32p ... .text:7C9585FF .text:7C9585FF var_2D0 = CONTEXT ptr -2D0h .text:7C9585FF arg_0 = dword ptr 8 .text:7C9585FF .text:7C9585FF push ebp .text:7C958600 mov ebp, esp .text:7C958602 lea esp, [esp-2D0h] .text:7C958609 push esp ; ContextRecord .text:7C95860A call _RtlCaptureContext@4 ; RtlCaptureContext(x) .text:7C95860F mov edx, [ebp+4] .text:7C958612 mov eax, [ebp+arg_0] .text:7C958615 add [esp+2D0h+var_2D0._Esp], 4 ; CONTEXT.ESP + 4 .text:7C95861D mov [eax+0Ch], edx ; 异常地址放的是此函数的返回地址 .text:7C958620 mov [esp+2D0h+var_2D0.ContextFlags], 10007h ; 1、CONTEXT_DEBUG_REGISTERS,调式 10010H .text:7C958620 ; 2、CONTEXT_FLOATING_POINT,浮点 10008H .text:7C958620 ; 3、CONTEXT_SEGMENTS,段 10004H .text:7C958620 ; 4、CONTEXT_INTEGER,通用 10002H .text:7C958620 ; 5、CONTEXT_CONTROL,控制 10001H .text:7C958620 ; 6、CONTEXT_EXTENDED_REGISTERS,扩展 .text:7C958620 ; 7、CONTEXT_FULL 全部 10007H .text:7C958627 mov eax, large fs:30h .text:7C95862D test byte ptr [eax+2], 0FFh .text:7C958631 jnz short loc_7C95864C .text:7C958633 push esp ; CONTEXT .text:7C958634 push [ebp+arg_0] ; PEXCEPTION_RECORD a1 .text:7C958637 call _RtlDispatchException@8 ; RtlDispatchException(x,x) .text:7C95863C test al, al .text:7C95863E jz short loc_7C95865B .text:7C958640 mov ecx, esp .text:7C958642 push 0 .text:7C958644 push ecx ; CONTEXT .text:7C958645 call _ZwContinue@8 ; ZwContinue(x,x) .text:7C95864A jmp short loc_7C958668
.text:00426D8C ; NTSTATUS __stdcall NtContinue(PCONTEXT Context, BOOLEAN TestAlert) .text:00426D8C _NtContinue@8 proc near ; DATA XREF: .text:00427B44o .text:00426D8C .text:00426D8C var_s0 = dword ptr 0 .text:00426D8C Context = dword ptr 8 .text:00426D8C TestAlert = byte ptr 0Ch .text:00426D8C arg_34 = dword ptr 3Ch .text:00426D8C .text:00426D8C push ebp .text:00426D8D mov ebx, ds:0FFDFF124h .text:00426D93 mov edx, [ebp+arg_34] .text:00426D96 mov [ebx+110h], edx .text:00426D9C mov ebp, esp .text:00426D9E mov eax, [ebp+var_s0] .text:00426DA1 mov ecx, [ebp+Context] .text:00426DA4 push eax ; ebp .text:00426DA5 push 0 ; 0 .text:00426DA7 push ecx ; PCONTEXT .text:00426DA8 call _KiContinue@12 ; KiContinue(x,x,x) .text:00426DAD or eax, eax .text:00426DAF jnz short loc_426DCB .text:00426DB1 cmp [ebp+TestAlert], 0 .text:00426DB5 jz short loc_426DC3 .text:00426DB7 mov al, [ebx+0D7h] .text:00426DBD push eax .text:00426DBE call _KeTestAlertThread@4 ; KeTestAlertThread(x) .text:00426DC3 .text:00426DC3 loc_426DC3: ; CODE XREF: NtContinue(x,x)+29j .text:00426DC3 pop ebp .text:00426DC4 mov esp, ebp .text:00426DC6 jmp _KiServiceExit2 .text:00426DCB ; --------------------------------------------------------------------------- .text:00426DCB .text:00426DCB loc_426DCB: ; CODE XREF: NtContinue(x,x)+23j .text:00426DCB pop ebp .text:00426DCC mov esp, ebp .text:00426DCE jmp _KiServiceExit .text:00426DCE _NtContinue@8 endp .text:00426DCE
NTSTATUS KiContinue ( IN PCONTEXT ContextRecord, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame ) /*++ Routine Description: This function is called to copy the specified context frame to the specified exception and trap frames for the continue system service. Arguments: ContextRecord - 环境记录Supplies a pointer to a context record. ExceptionFrame - 异常帧指针。Supplies a pointer to an exception frame. TrapFrame - 陷阱帧Supplies a pointer to a trap frame. Return Value: STATUS_ACCESS_VIOLATION is returned if the context record is not readable from user mode. STATUS_DATATYPE_MISALIGNMENT is returned if the context record is not properly aligned. STATUS_SUCCESS is returned if the context frame is copied successfully to the specified exception and trap frames. --*/ { KIRQL OldIrql; NTSTATUS Status; // // 同步其它的环境操作 // OldIrql = KeGetCurrentIrql(); if (OldIrql < APC_LEVEL) { KfRaiseIrql(APC_LEVEL); } // // 如果之前不是内核模式,使用封装函数将context 拷贝到内核帧 // 否则,直接拷贝context 到内核帧 // Status = STATUS_SUCCESS; if (KeGetPreviousMode() != KernelMode) { try { KiContinuePreviousModeUser(ContextRecord, ExceptionFrame, TrapFrame); } except(EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); } } else { KeContextToKframes(TrapFrame, ExceptionFrame, ContextRecord, ContextRecord->ContextFlags, KernelMode); } if (OldIrql < APC_LEVEL) { KeLowerIrql(OldIrql); } return Status; } DECLSPEC_NOINLINE VOID KiContinuePreviousModeUser ( IN PCONTEXT ContextRecord, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame ) /*++ Routine Description: 内部操作是先将用户数据拷贝到内核栈中,然后调用内核函数进行内核内存间的拷贝。 N.B. 假设此调用是在一个try 中 Arguments: ContextRecord - Supplies a pointer to a context record. ExceptionFrame - Supplies a pointer to an exception frame. TrapFrame - Supplies a pointer to a trap frame. Return Value: None. --*/ { CONTEXT ContextRecord2; // // 将内容从用户拷贝到内核栈 // ProbeForReadSmallStructure(ContextRecord, sizeof(CONTEXT), CONTEXT_ALIGN); RtlCopyMemory(&ContextRecord2, ContextRecord, sizeof(CONTEXT)); // // 然后再进行拷贝 // KeContextToKframes(TrapFrame, ExceptionFrame, &ContextRecord2, ContextRecord2.ContextFlags, UserMode); return; } VOID KeContextToKframes ( __inout PKTRAP_FRAME TrapFrame, __inout PKEXCEPTION_FRAME ExceptionFrame, __in PCONTEXT ContextFrame, __in ULONG ContextFlags, __in KPROCESSOR_MODE PreviousMode ) /*++ Routine Description: This routine moves the selected contents of the specified context frame into the specified trap and exception frames according to the specified context flags.根据ContextFlags 标志,将CONTEXT 帧中的内容拷贝到特定的陷阱和异常帧 ++*/
其中一个使用KiRaiseUserExceptionDispatcher 函数的是 NtClose,该函数负责关闭一个句柄 (Win32 函数CloseHandle 的内部实现). 当一个调试器负载到一个进程,它的一个受保护句柄 (通过使用HANDLE_FLAG_PROTECT_FROM_CLOSE标志调用SetHandleInformation ) 被传给NtClose,将通过KiRaiseUserExceptionDispatcher 函数触发STATUS_HANDLE_NOT_CLOSABLE 异常。例如,当一个被调试器负载的进程尝试关闭被保护句柄的时候可能会发现下面的调用堆栈:
(2600.1994): Unknown exception - code c0000235 (first chance) (2600.1994): Unknown exception - code c0000235 (second chance) ntdll!KiRaiseUserExceptionDispatcher+0x3a: 00000000`776920e7 8b8424c0000000 mov eax,dword ptr [rsp+0C0h] 0:000> !error c0000235 Error code: (NTSTATUS) 0xc0000235 (3221226037) - NtClose was called on a handle that was protected from close via NtSetInformationObject. 0:000> k RetAddr Call Site 00000000`7746dadd ntdll!KiRaiseUserExceptionDispatcher+0x3a 00000000`01001955 kernel32!CloseHandle+0x29 00000000`01001e60 TestApp!wmain+0x35 00000000`7746cdcd TestApp!__tmainCRTStartup+0x120 00000000`7768c6e1 kernel32!BaseThreadInitThunk+0xd 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
在调试器中,这个异常可以被手动设置为继续执行,以允许程序在异常发生之后继续执行,尽管这样的糟糕的句柄引用通常都象征着一些列的错误。
如果一个程序在应用程序检验器下运行的时候关闭了一个非法的句柄,相似的异常也会出现。在一个非法的句柄的关闭过程中引发异常主要是为了调试的目的,因为大部分程序不会检查CloseHandle 函数的返回值。
在内核中像这样关闭一个非法的句柄将导致蓝屏,而不像用户模式,仅仅触发一个异常就可以了。除非是probe一个用户模式句柄,驱动在引用一个句柄之后没有权限去继续执行。
KiUserCallbackDispatcher
之前介绍的NTDLL内核模式到用户模式回调函数都是“被动”的—-内核没有主动去显式的调用这些函数。目前为止,我们所介绍的所有的函数都是特定情况下,在一个系统调用,中断或者异常处理之后的返回过程中被调用的。与之前讨论的函数相比,KiUserCallbackDispatcher 完全打破了这种被动调用模型。用户模式回调分发器,顾名思义,是一种从内核模式向用户模式发出全面调用的蹦床。(它由NtCallbackReturn 补充,该函数在一个用户模式回调函数执行完毕之后恢复在内核模式的执行。这意味着一个用户模式回调函数可以进行一个辅助调用,该调用直接到达内核模式,而不用返回到原来的用户模式调用者–调用KiUserCallbackDispatcher 函数的位置)。
在Windows 中,用户模式到内核模式的直接调用是非常非传统的,避免这样的操作是有原因的。这样的调用通常是十分危险的,实现的时候需要非常小心以避免导致任何的系统可靠性或者完整性问题。除了简单的检查从用户模式返回到内核的所有数据, KiUserCallbackDispatcher所实现的直接从内核模式调用到用户模式的模型有更多的考虑。例如,运行在用户模式的线程可以被随意的暂停,由于其他的高优先级的线程而延迟比较长的时间,甚至终止。这些行为意味着调用到用户模式的代码不能持有任何的锁,还有像内存等需要释放的资源。
从内核的角度看,使用KiUserCallbackDispatcher 函数实现的用户模式回调函数的原理就是:内核在当前的内核栈上存储当前处理器状态,修改当前内核栈的视图为指向刚刚被保存到栈上的寄存器的状态,之后,设置当前线程的一个成员 (CallbackStack)指向包含了被保存的寄存器的状态的栈帧, (之前的CallbackStack值被保存以支持递归的回调),然后使用标准返回机制返回到用户模式。
用户模式返回地址被设置为—-KiUserCallbackDispatcher。用户模式回调函数的操作非常的简单,首先,它通过索引PEB中存储的一个数组来作为回调函数分发器的参数,这个参数指示了将要被执行的代码。然后,位于数组中的回调函数被调用,并传入了一个内核传递的指针大小的参数。(参数通常是一个结构体指针,将多个参数放到一块连续的内存块中). KiUserCallbackDispatcher 的实际实现非常的简单,下面是一个简单的C语言版本表示:
VOID KiUserCallbackDispatcher( __in PVOID CallbackArgument __in ULONG CallbackIndex ) { NTSTATUS Status; ULONG ReturnStatus; PPEB Peb; // // 使用了标准的调用约定,参数从栈传递,从[rsp+20] 开始(x64上),在X64 平台上没有使用任何的寄存器来传递参数 // // // Make the call to the specified kernel mode to user mode callback. The set // of callback routines is stored in an array pointed to by the // "KernelCallbackTable" member of the PEB. // // Each callback takes a single argument, which is typically a structure // pointer. Most callbacks are in fact actually sub-dispatchers for several // different callbacks that share the same calling convention after the // callback arguments are unpacked from the structure pointer. // // In the case of a Wow64 process, the Wow64 layer will have installed a set // of shadow trampoline function pointers in the PEB to facilitate the // necessary conversion of the callback argument. // Peb = NtCurrentPeb(); ReturnStatus = Peb->KernelCallbackTable[ CallbackIndex ]( CallbackArgument ); // // If the callback did not explicitly return to kernel mode, then do so now. // Note that this is not the typical case, as all User32 callbacks should // call NtCallbackReturn directly. // Status = NtCallbackReturn( 0, 0, ReturnStatus ); // // NtCallbackReturn should never return to us. If it did so, then something // has gone very wrong. // for (;;) RtlRaiseStatus( Status ); }
Win32中,只有User32在窗口相关的方面使用用户模式回调函数。比如,当有用户模式调用了NtUserCreateWindowEx函数来创建一个新的窗口的时候,内核通过这种方法来调用一个窗口过程函数以发送一个WM_NCCREATE 消息。例如,如果我们在一个创建创建过程中在KiUserCallbackDispatcher 函数上下一个断点,我们可能看到下面的调用堆栈:
Breakpoint 1 hit ntdll!KiUserCallbackDispatch: 00000000`77691ff7 488b4c2420 mov rcx,qword ptr [rsp+20h] 0:000> k RetAddr Call Site 00000000`775851ca ntdll!KiUserCallbackDispatch 00000000`7758514a USER32!ZwUserCreateWindowEx+0xa 00000000`775853f4 USER32!VerNtUserCreateWindowEx+0x27c 00000000`77585550 USER32!CreateWindowEx+0x3fe 000007fe`fddfa5b5 USER32!CreateWindowExW+0x70 000007fe`fde221d3 ole32!InitMainThreadWnd+0x65 000007fe`fde2150c ole32!wCoInitializeEx+0xfa 00000000`ff7e6db0 ole32!CoInitializeEx+0x18c 00000000`ff7ecf8b notepad!WinMain+0x5c 00000000`7746cdcd notepad!IsTextUTF8+0x24f 00000000`7768c6e1 kernel32!BaseThreadInitThunk+0xd 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
如果我们更进一步跟踪它的执行过程,我们发现它最终发现它调用了一个称为 user32!_fnINLPCREATESTRUCT的函数,该函数最终以WM_NCCREATE窗口消息调用user32!DispatchClientMessage ,以允许窗口过程函数参与新窗口的创建过程,尽管win32k.sys 在内核已经处理了新窗口的创建。
正如前面提到的,Callbacks 是允许嵌套的(甚至是递归)。例如,在观察到KiUserCallbackDispatcher 的调用之后的一小段时间,我们可能看到下面的调用堆栈:
Breakpoint 1 hit ntdll!KiUserCallbackDispatch: 00000000`77691ff7 488b4c2420 mov rcx,qword ptr [rsp+20h] 0:000> k RetAddr Call Site 00000000`7758b45a ntdll!KiUserCallbackDispatch 00000000`7758b4a4 USER32!NtUserMessageCall+0xa 00000000`7758e55a USER32!RealDefWindowProcWorker+0xb1 000007fe`fca62118 USER32!RealDefWindowProcW+0x5a 000007fe`fca61fa1 uxtheme!_ThemeDefWindowProc+0x298 00000000`7758b992 uxtheme!ThemeDefWindowProcW+0x11 00000000`ff7e69ef USER32!DefWindowProcW+0xe6 00000000`7758e25a notepad!NPWndProc+0x217 00000000`7758cbaf USER32!UserCallWinProcCheckWow+0x1ad 00000000`77584e1c USER32!DispatchClientMessage+0xc3 00000000`77692016 USER32!_fnINOUTNCCALCSIZE+0x3c 00000000`775851ca ntdll!KiUserCallbackDispatcherContinue 00000000`7758514a USER32!ZwUserCreateWindowEx+0xa 00000000`775853f4 USER32!VerNtUserCreateWindowEx+0x27c 00000000`77585550 USER32!CreateWindowEx+0x3fe 00000000`ff7e9525 USER32!CreateWindowExW+0x70 00000000`ff7e6e12 notepad!NPInit+0x1f9 00000000`ff7ecf8b notepad!WinMain+0xbe 00000000`7746cdcd notepad!IsTextUTF8+0x24f 00000000`7768c6e1 kernel32!BaseThreadInitThunk+0xd
这种对于递归调用的支持是win32k.sys 涉及到的一些线程被称为“大内核栈”的原因。当一个调用发生的时候,处理用户模式调用的内核模式分发器将试着把线程转换为大内核栈,因为典型的内核栈的大小不够,不足以支持 复杂的窗口消息调用中产生的递归的内核模式到用户模式调用的数量。
如果是Wow64 进程,PEB 中的回调函数组 指向了Wow64层中的一个转换函数数组,里面的函数将回调函数的参数正确地映射到和32-bit user32.dll 兼容的版本。
LdrInitializeThunk
LdrInitializeThunk 是系统范围内所有的用户模式线程开始它们的执行的位置。尽管Win32 函数CreateThread (甚至其底层实现NtCreateThread)都造成了一种假象,新线程的起始地址就是我们传入的特定的函数地址,但事实并非如此。CreateThread 内部在特定的线程函数和实际的线程的初始化指令之间放置了一层kernel32存根函数(BaseThreadStart or BaseProcessStart)。kernel32 存根函数封装了对于用户指定的线程起始函数的调用,这层封装设置了SHE 帧中的最顶层的异常处理函数,以支持UnhandledExceptionFilter。我们通常在编写DLL 的时候只是声明了DllMain 函数,然而,谁调用了我们的DllMain 函数,并在不同的时机传入不同的参数(线程挂载,进程挂载或者其它)。
答案就是LdrInitializeThunk。当一个用户模式线程初始化完毕并准备好初始执行的时候,初始被设置的上下文并不是传递给NtCreateThread 参数的函数地址(这个函数将最终在用户提供的入口地址结束)。而是NTDLL 中的函数LdrInitializeThunk ,初始执行时内核提供了一个CONTEXT 记录,其中描述了创建线程时请求的线程状态 (NtCreateThread提供的, 在 Windows Vista 上是由NtCreateThreadEx 创建的). 这个context 记录作为参数传递给LdrInitializeThunk,以最终执行用户提供的线程入口点。
当被初始的进程调用,LdrInitializeThunk 调用LdrpInitialize 执行剩下的初始化任务,之后调用NtContinue 还原所提供的context记录。下面是一个C 格式的伪实现:
VOID LdrInitializeThunk( __in PCONTEXT Context, __in PVOID NtDllBaseAddress ) { NTSTATUS Status; // // Call LdrpInitialize to perform process or thread initialization tasks. // LdrpInitialize( Context, NtDllBaseAddress ); // // Resume execution at the user-supplied thread context. // Status = NtContinue( Context, TRUE ); // // NtContinue should never return. // RtlRaiseStatus( Status ); __debugbreak(); }
LdrpInitialize 判断进程是否已经被初始化了,这个步骤特别重要,因为LdrInitializeThunk 不仅仅是已经初始化了的进程中的线程的入口点,它也是新创建的进程的初始线程的入口点。如果进程尚未初始化,LdrpInitialize 通过调用LdrpInitializeProcess 来执行进程初始化任务 (一部分进程初始化任务在LdrpInitialize 函数内部实现)。如果是Wow64 进程, Wow64 NTDLL 被加载,其中的LdrInitializeThunk函数被调用以初始化32-bit NTDLL的(per-process)单进程状态。
否则,如果进程已经初始化过了, LdrpInitialize 调用LdrpInitializeThread 执行单线程初始化,该函数内部主要是在持有加载器锁的前提下为已经加载的模块调用DllMain 和 TLS 回调函数。 (这就是DllMain函数内不能等待一个新创建的线程完成其初始化的原因。这个新线程将立即在加载器锁上死锁,调用者已经持有了加载器锁,又等待新线程完毕,而新线程的初始化需要持有加载器锁) 如果是Wow64 进程,再次支持对于32-bit NTDLL 的调用,以满足执行32-bit 单线程初始化代码的需要。
当需要的进程或线程初始化任务完成之后, LdrpInitialize 返回LdrInitializeThunk,然后设置用户提供的线程开始上下文,并调用 NtContinue 。
这种进程和线程初始化的机制使得单步跟踪进程的创建过程变得稍微有些困难。初始进程的线程是执行LdrInitializeThunk 函数的线程,而不是进程中第一个线程。这意味着,不可能创建一个暂停进程并负载一个加载器然后进行单步调试其初始化过程,因为调试器侵入线程将执行进程的初始化过程,而这个过程正是我们想要进行追踪的。
Windows 调试包中包含此种情景的支持。设置调试器在 ‘create process’ 事件上中断,这样就可能在新进程 (通过调试器创建的进程) 或者子进程的LdrInitializeThunk执行前 (如果子进程调试是开启的) 手动设置断点。
当前被加载模块列表还没有被初始化,符号不可使用,因此应该手动计算LdrInitializeThunk函数的偏移以下断点。
这是一个遗留的问题,如何调试LdrInitializeThunk 函数
相关文章推荐
- 理解Windows内核模式与用户模式
- 用户模式与内核模式
- 内核模式和用户模式
- 用户模式与内核模式
- WINCE下内核模式和用户模式的区别
- 理解Windows内核模式与用户模式
- 理解Windows内核模式与用户模式
- Linux 内核--任务0的运行(切换到用户模式)move_to_user_mode
- WINCE下内核模式和用户模式的区别
- 《Windows核心编程》读书笔记八 用户模式下的内核同步
- Linux 内核--任务0的运行(切换到用户模式)move_to_user_mode
- 理解Windows内核模式与用户模式
- Windows 7驱开发系列(二)--用户模式与内核模式
- 理解Windows内核模式与用户模式
- 转一篇讲诉浏览器内核与兼容性以及渲染模式的文章
- Linux用户模式和内核模式
- linux的用户模式和内核模式
- 使用用户模式linux(UML, User mode linux)来进行内核Debug
- 理解Windows内核模式与用户模式(新)
- 用户模式 VS 内核模式(1)