KiFastCallEntry() 机制分析
- 概述
- Win32 子系统 API 调用
- ntdll!ZwWriteFile() 函数
- ntdll!KiFastSystemCall() 函数
- _KUSER_SHARED_DATA 结构
- 切入 KiFastCallEntry() 函数
- 系统服务例程号与 ServiceTable
- KiFastCallEntry() 的返回处理
1. 概述
从 windows xp 和 windows 2003 开始使用了快速切入内核的方式提供系统服务例程的调用。
KiFastCallEntry() 的实现是直接使用汇编语言,C 语言不能直接表达某些操作。我从 windows 2003 里反汇编出来,写成 C 伪码形式,点击这里察看:KiFastCallEntry()
在下面的篇章里,我将分析从 Win32 子系统 API WriteFile() 的调用为例,介绍如何切入到 nt 模块的 NtWriteFile() 系统服务例程。
2. Win32 子系统 API 调用
当我向一个文件或 device 使用 WriteFile() API 来写入一些数据时,像下面的调用:
//
// 往设备里写数据
//
if (WriteFile(hFile, _T("Hello, world!"), 20, &nCount, NULL) == FALSE)
{
_tprintf(_T("fail: WriteFile for device, ErrorCode: %d\n"), GetLastError());
CloseHandle(hFile);
return -2;
}
象这样的 API 会切入到 kernel 执行系统服务例程(Service Routine),在切入 kernel 前会经过一些 stub 函数转发。
如下图所示:
在用户代码里,对于 WriteFile() 函数的调用,编译器在用户代码里会生成对子系统 DLL 的 ntdll 模块的 ZwWriteFile() 函数的调用,如下代码所示:
Status = ZwWriteFile(hFile, // file handle
0, // event handle
NULL, // APC routine
NULL, // APC context
&IoStatus, // IO_STATUS_BLOCK 块
lpBuffer, // buffer
nNumberOfBytesToWrite, // write bytes
NULL, // Byte Offset, PLARGE_INTEGER 指针
NULL); // key: ULONG 指针
这个真实的 ntdll!ZwWriteFile() 是 9 个参数,ntdll!NtWriteFile() 会定向到 ntdll!ZwWriteFile() 里,因此:它们是完全一样的,只是名字不同而已!
3. ntdll!ZwWriteFile() 函数
ZwWriteFile() 是一个 stub 函数,作用也只是转发一下,因此它很简单:
ntdll!ZwWriteFile:
7c957b9d b81c010000 mov eax,11Ch ; 系统服务例程号
7c957ba2 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300) ; 取得 KiFastCallEntry() stub 函数
7c957ba7 ff12 call dword ptr [edx] ; 调用这个 stub 函数
7c957ba9 c22400 ret 24h
ZwWriteFile() 在 eax 寄存器里传递一个系统服务例程号,NtWriteFile() 服务例程这号码是 11Ch。从 UserSharedData 结构里得到另一个 stub 函数。
这个 stub 函数是 KiFastCallEntry() 的 stub 函数。
4. ntdll!KiFastSystemCall() 函数
根据上面的代码,ZwWriteFile() 函数从一个叫 UserSharedData 结构区域里得到一个函数地址值,它就是 KiFastSystemCall() 函数。
这个函数是最后一个运行在用户层的 stub 函数,它将会转入 kernel 层:
ntdll!KiFastSystemCall:
7c958458 8bd4 mov edx,esp ; 传送 caller 的 stack frame pointer
7c95845a 0f34 sysenter ; 快速切入到 kernel
7c95845c c3 ret ; 注意:实际上这是一个独立的 ntdll!KiFastSystemCallRet() 例程
值得注意的是:这三行代码实际上包含了两个 ntdll 例程,最后一条 ret 指令,它是一个独立的 ntdll!KiFastSystemCallRet() 例程,我们可以在后面介绍 _KUSER_SHERED_DATA 结构时看到。
KiFastSystemCall() 使用 sysenter 指令快速切入到内核的 nt!KiFastCallEntry() 代码里。
注意:给 edx 寄存器传送当前的 esp 值,这一点很重要,看看下图的 stack 布局:
在内核层的 KiFastCallEntry() 代码里,将 edx + 8 来获得传递给 ZwWriteFile() 的参数地址,从而读取完整的参数。
5. _KUSER_SHARED_DATA 结构
在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于 User 层和 Kernel 层共享某些数据,在 sysenter 快速切入机制里就使用了这个区域。
它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在 User 和 Kernel 层地址分别为:
- User 层地址为:0x7ffe0000
- Kernnel 层地址为:0xffdf0000
值得注意的是: User 层和 Kernel 层的 _KUSER_SHARED_DATA 区域都映射到同一个物理地址,下面是在 windbg 里得到 windows 2003 里的信息:
kd> !pte 7ffe0000
VA 7ffe0000
PDE at C0601FF8 PTE at C03FFF00
contains 00000000108AD067 contains 0000000000041025
pfn 108ad ---DA--UWEV pfn 41 ----A--UREV
kd> !pte ffdf0000
VA ffdf0000
PDE at C0603FF0 PTE at C07FEF80
contains 0000000000513063 contains 0000000000041163
pfn 513 ---DA--KWEV pfn 41 -G-DA--KWEV
可以看到:它们都映射到物理页面 0x41000 上。因此:User 层和 Kernel 层的 _KUSER_SHARED_DATA 区域内容是完全一样的。基于这种设计可以方便地在 User 层和 Kernel 层共享某些数据。
另一点:在 User 层里 _KUSER_SHARED_DATA 区域是只读的,只有在 Kernel 层才是可写的。因此:Kernel 在初始化阶段某个时刻对 _KUSER_SHHARED_DATA 区进行设置。User 层只能读取其中的值,如前面的 stub 函数所示:
ntdll!ZwWriteFile:
7c957b9d b81c010000 mov eax,11Ch ; 系统服务例程号
7c957ba2 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300) ; 取得 KiFastCallEntry() stub 函数
7c957ba7 ff12 call dword ptr [edx] ; 调用这个 stub 函数
7c957ba9 c22400 ret 24h
下面,来看看 _KUSER_SHARED_DATA 区域是些什么内容(User 层和 Kernel 层是一样的),在 windbg 用 dt 命令来查看:
kd> dt _KUSER_SHARED_DATA 0x7ffe0000
ntdll!_KUSER_SHARED_DATA
+0x000 TickCountLowDeprecated : 0
+0x004 TickCountMultiplier : 0xfa00000
+0x008 InterruptTime : _KSYSTEM_TIME
+0x014 SystemTime : _KSYSTEM_TIME
+0x020 TimeZoneBias : _KSYSTEM_TIME
+0x02c ImageNumberLow : 0x14c
+0x02e ImageNumberHigh : 0x14c
+0x030 NtSystemRoot : [260] 0x43
+0x238 MaxStackTraceDepth : 0
+0x23c CryptoExponent : 0
+0x240 TimeZoneId : 0
+0x244 LargePageMinimum : 0x200000
+0x248 Reserved2 : [7] 0
+0x264 NtProductType : 3 ( NtProductServer )
+0x268 ProductTypeIsValid : 0x1 ''
+0x26c NtMajorVersion : 5
+0x270 NtMinorVersion : 2
+0x274 ProcessorFeatures : [64] ""
+0x2b4 Reserved1 : 0x7ffeffff
+0x2b8 Reserved3 : 0x80000000
+0x2bc TimeSlip : 0
+0x2c0 AlternativeArchitecture : 0 ( StandardDesign )
+0x2c8 SystemExpirationDate : _LARGE_INTEGER 0x0
+0x2d0 SuiteMask : 0x112
+0x2d4 KdDebuggerEnabled : 0x3 ''
+0x2d5 NXSupportPolicy : 0x2 ''
+0x2d8 ActiveConsoleId : 0
+0x2dc DismountCount : 0
+0x2e0 ComPlusPackage : 0xffffffff
+0x2e4 LastSystemRITEventTickCount : 0x239f29d
+0x2e8 NumberOfPhysicalPages : 0x17f1b
+0x2ec SafeBootMode : 0 ''
+0x2f0 TraceLogging : 0
+0x2f8 TestRetInstruction : 0xc3
+0x300 SystemCall : 0x7c958458 <--------- System Call stub 函数
+0x304 SystemCallReturn : 0x7c95845c <--------- System Call return 函数
+0x308 SystemCallPad : [3] 0
+0x320 TickCount : _KSYSTEM_TIME
+0x320 TickCountQuad : 0x2481d8
+0x330 Cookie : 0xa4a0f27b
+0x334 Wow64SharedInformation : [16] 0
其中 +0x300 位置上就是 KiFastSystemCall() stub 函数地址,而 +0x304 位置上就是返回函数地址:
ntdll!KiFastSystemCall:
7c958458 8bd4 mov edx,esp ; 传送 caller 的 stack frame pointer
7c95845a 0f34 sysenter ; 快速切入到 kernel
7c95845c c3 ret ; 注意:实际上这是一个独立的 ntdll!KiFastSystemCallRet() 例程
地址 0x7c958458 是 ntdll!KiFastSystemCall() 函数地址,地址 0x7c95845c 是 ntdll!KiFastSystemCallRet() 函数地址。
6. 切入 KiFastCallEntry()
在用户层的 stub 函数会使用 sysenter 指令切入到内核层的 KiFastCallEntry() 函数,再由 KiFastCallEntry() 函数分发到相应的系统服务例程执行。
如下图所示:
KiFastCallEntry() 函数是使用汇编语言来实现的,我将写成 C 伪码形式,点击这里察看:KiFastCallEntry(),这里不再重复贴出。
6.1 读取 TSS 信息
在 x86 体系的 sysenter/sysexit 指令快速切入机制里 IA32_SYSENTER_ESP 寄存器(MSR 地址为 175h)提供了 ESP 值。但是,在 windows 里并没有使用这个值,而是使用 KPCR 结构内 TSS 块里的 ESP 值。
//
// 得到当前 TSS 块,并读取 0 级的 esp 值
// 注意:这个 Esp0 指向一个 KTRAP_FRAME 结构的 V86Es 成员!
// Esp0 值减去 0x7c 就等于 KTRAP_FRAME 结构地址,trap 用于 context 信息
// esp 被赋予 KTRAP_FRAME 结构地址:esp = KtrapFrame,它以 push 的方式保存 context 信息
//
PKTSS Tss = GetCurrentTss();
PKTRAP_FRAME KtrapFrame = (PKTRAP_FRAME)(Tss->Esp0 - 0x7c);
在 KPCR(Processor Cotnrol Region)区域的 +0x40 位置是 TSS 指针(指向一个 KTSS 结构),KPCR 结构的地址在 0xffdff000:
kd> dt _kpcr 0xffdff000
ntdll!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : 0xf6ac85b8 _EXCEPTION_REGISTRATION_RECORD
+0x004 Used_StackBase : (null)
+0x008 PerfGlobalGroupMask : (null)
+0x00c TssCopy : 0x80042000 Void
+0x010 ContextSwitches : 0x10d344b
+0x014 SetMemberCopy : 1
+0x018 Used_Self : 0x7ffdf000 Void
+0x01c SelfPcr : 0xffdff000 _KPCR
+0x020 Prcb : 0xffdff120 _KPRCB
+0x024 Irql : 0 ''
+0x028 IRR : 0
+0x02c IrrActive : 0
+0x030 IDR : 0xffffffff
+0x034 KdVersionBlock : 0x8088e3b8 Void
+0x038 IDT : 0x8003f400 _KIDTENTRY
+0x03c GDT : 0x8003f000 _KGDTENTRY
+0x040 TSS : 0x80042000 _KTSS <------ TSS 结构地址
+0x044 MajorVersion : 1
+0x046 MinorVersion : 1
+0x048 SetMember : 1
+0x04c StallScaleFactor : 0x95a
+0x050 SpareUnused : 0 ''
+0x051 Number : 0 ''
+0x052 Spare0 : 0 ''
+0x053 SecondLevelCacheAssociativity : 0 ''
+0x054 VdmAlert : 0
+0x058 KernelReserved : [14] 0
+0x090 SecondLevelCacheSize : 0
+0x094 HalReserved : [16] 0
+0x0d4 InterruptMode : 0
+0x0d8 Spare1 : 0 ''
+0x0dc KernelReserved2 : [17] 0
+0x120 PrcbData : _KPRCB
我们看到这个 KTSS 结构地址在 0x80042000 里,这个 KTSS 结构如下:
kd> dt _ktss 0x80042000
ntdll!_KTSS
+0x000 Backlink : 0xc45
+0x002 Reserved0 : 0x4d8a
+0x004 Esp0 : 0xf649bde0 <------- 0 级的 Esp 值,这指向一个 KTRAP_FRAME 结构 V86Es 成员
+0x008 Ss0 : 0x10
+0x00a Reserved1 : 0xb70f
+0x00c NotUsed1 : [4] 0x5031ff00
+0x01c CR3 : 0x8b55ff8b
+0x020 Eip : 0xc75ffec
+0x024 EFlags : 0xe80875ff
+0x028 Eax : 0xfffffbdd
+0x02c Ecx : 0x1b75c084
+0x030 Edx : 0x8b184d8b
+0x034 Ebx : 0x7d8b57d1
+0x038 Esp : 0x2e9c110
+0x03c Ebp : 0xf3ffc883
+0x040 Esi : 0x83ca8bab
+0x044 Edi : 0xaaf303e1
+0x048 Es : 0xeb5f
+0x04a Reserved2 : 0x6819
+0x04c Cs : 0x24fc
+0x04e Reserved3 : 0x44
+0x050 Ss : 0x75ff
+0x052 Reserved4 : 0xff18
+0x054 Ds : 0x1475
+0x056 Reserved5 : 0x75ff
+0x058 Fs : 0xff10
+0x05a Reserved6 : 0xc75
+0x05c Gs : 0x75ff
+0x05e Reserved7 : 0xe808
+0x060 LDT : 0
+0x062 Reserved8 : 0xffff
+0x064 Flags : 0
+0x066 IoMapBase : 0x20ac
+0x068 IoMaps : [1] _KiIoAccessMap
+0x208c IntDirectionMap : [32] "???"
KTSS 结构内的 Esp0 指向 KTRAP_FRAME 结构的 V86Es 成员,如下图所示:
这个 Esp0 值被赋值给 esp 寄存器,那么 KiFastCallEntry() 将会使用这个值来压入 context 信息,如下代码所示:
8088387d 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h] ; 读取 KTSS 结构
80883883 8b6104 mov esp,dword ptr [ecx+4] ; 读取 Esp0,Esp0 指向 KTRAP_FRAME 的 V86Es 成员
80883886 6a23 push 23h ; 压入 HardwareSegSs 值,也就是 SS 值
esp 指向 KTRAP_FRAME 结构 V86Es,当 push 23h 时则等于将 HardwareSegSs 赋值为 23h 值。我们将在后面了解到 KTRAP_FRAME 结构
6.2 KTRAP_FRAME 结构
在 KiFastCallEntry() 中将 context 信息保存在一个被称为 KTRAP_FRAME 的结构里,在前面我们看到 KTRAP_FRAME 结构的地址被赋予 esp 寄存器,因此:KTRAP_FRAME 结构就是 KiFastCallEntry() 函数的 stack 区域。 KTRAP_FRAME 结构如下面所示:
kd> dt _ktrap_frame 0xf649bde0-0x7c <--- KTRAP_FRAME 结构基址等于 Esp0 值减 0x7c
ntdll!_KTRAP_FRAME
+0x000 DbgEbp : 0x12fa74
+0x004 DbgEip : 0x7c95845c
+0x008 DbgArgMark : 0xbadb0d00
+0x00c DbgArgPointer : 0x12fa30
+0x010 TempSegCs : 0
+0x014 TempEsp : 0
+0x018 Dr0 : 0
+0x01c Dr1 : 0
+0x020 Dr2 : 0
+0x024 Dr3 : 0
+0x028 Dr6 : 0
+0x02c Dr7 : 0
+0x030 SegGs : 0
+0x034 SegEs : 0x23
+0x038 SegDs : 0x23
+0x03c Edx : 0xc
+0x040 Ecx : 2
+0x044 Eax : 0x12f6a0
+0x048 PreviousPreviousMode : 1
+0x04c ExceptionList : 0xffffffff _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : 0x3b
+0x054 Edi : 1
+0x058 Esi : 0
+0x05c Ebx : 0
+0x060 Ebp : 0x12fa74
+0x064 ErrCode : 0
+0x068 Eip : 0x7c95845c
+0x06c SegCs : 0x1b
+0x070 EFlags : 0x213
+0x074 HardwareEsp : 0x12fa28
+0x078 HardwareSegSs : 0x23
+0x07c V86Es : 0
+0x080 V86Ds : 0
+0x084 V86Fs : 0
+0x088 V86Gs : 0
注意:KTRAP_FRAME 结构的基址是 Esp0 值减 0x7c 而来,因为:Esp0 指向 V86Es 成员。在执行 push 23h 指令后,HardwareSegSs 就等于 23h。 上面显示的 KTRAP_FRAME 结构的内容是 KiFastCallEntry() 在已经保存好 context 信息后的内容,将要转入执行真正的系统服务例程(nt!NtWriteFile)。
KiFastCallEntry() 在 KTRAP_FRAME 里保存下面的内容:
//
// 注意:下面保存 context 的操作是以 push 方式压入 TrapFrame 为 esp 的栈中
//
KtrapFrame->HardwareSegSs = 0x23; // 保存原 R3 的 SS 值
KtrapFrame->HardwareEsp = edx; // edx 是原 R3 的 ESP 值
KtrapFrame->EFlags = eflags; // 保存原 eflags 值
KtrapFrame->EFlags.IF = 1; // context 中的 eflags.IF 置位
eflags = 0; // 当前的 eflags 清为 0
//
// 当前 edx 保存着 sysenter 进入前的 esp 值
// 加上 8 后:edx 指向 native API 调用中的第 1 个参数
//
PVOID ArgumentPointer = edx + 8;
KtrapFrame->SegCs = 0x1b; // 保存原 R3 的 CS 值
KtrapFrame->Eip = UserSharedData->SystemCallReturn; // 保存返回函数
KtrapFrame->ErrCode = 0; // 错误码为 0
KtrapFrame->Ebp = ebp;
KtrapFrame->Ebx = ebx;
KtrapFrame->Esi = esi;
KtrapFrame->Edi = edi;
KtrapFrame->SegFs = 0x3b; // 原 R3 的 FS 值
PKPCR Kpcr = (PKPCR)0xffdff000; // 也就是 fs.base
KtrapFrame->ExceptionList = Kpcr->NtTib.ExceptionList; // 保存原 SEH 链底
Kpcr->->NtTib.ExceptionList = -1; // 设置为空 SEH 链
PKTHREAD Thread = Kpcr->PrcbData.CurrentThread; // 得到当前线程结构
PVOID InitialStack = Thread->InitialStack; // 得到初始的 stack 地址
KtrapFrame->PreviousPreviousMode = 1; // 1 值是原 MODE_MASK 值
KtrapFrame = (PKTRAP_FRAME)((ULONG)KtrapFrame - 0x48); // 计算出 Ktrap_frame 基地址
//
// 计算初始 stack 的 ktrap_frame 基址:
// 这个 0x29c 值等于:NPX_FRAME_LENGTH + KTRAP_FRAME_LENGTH
// NPX_FRAME_LEGNTH = 0x210
// KTRAP_FRAME_LENGTH = 0x8c
//
InitialStack = (PVOID)((ULONG)InitialStack - 0x29c);
Thread->PreviousMode = 1;
//
// 假如这两个 stack 基址值不同
//
if (KtrapFrame != InitialStack)
{
goto 3869;
}
//
// 此时 InitialStack 指向 KtrapFrame 基址,也就是:InitialStack == KtrapFrame
//
InitialStack->Dr7 = 0; // 清 Dr7 值
Thread->TrapFrame = InitialStack;
//
// 检测是否需要保存 debug context 信息(debug 寄存器)
//
if (Thread->Header.DebugActive != 0xff)
{
goto 372c;
}
ebx = KtrapFrame->Ebp; // 读取原 ebp 值
edi = KtrapFrame->Eip; // 读取 UserSharedData->SystemCallReturn
KtrapFrame->DbgArgPointer = ArgumentPointer; // native API 调用的第 1 个参数
KtrapFrame->DbgArgMark = 0xbadb0d00;
//
// 实际上:当前 KtrapFrame 值等价于当前 esp
// 因此,下面两行代码是构建一个标准的 call 返回流程
// 1. push UserSharedData->SystemCallReturn(返回地址)
// 2. push ebp(原 stack frame base )
//
// 当前:
// 1. ebp == esp
// 2. ebp 指向 KtrapFrame->DbgEbp 值(stack top)
//
KtrapFrame->DbgEbp = ebx;
KtrapFrame->DbgEip = edi;
7. 系统服务例程号与 ServiceTable
KiFastCallEntry() 保存好相关的 context 信息后,接下来要重的一步是分析系统服务例程号以便读取系统服务例程地址。如下图所示:
32 位的系统服务例程号,实际只使用低 12 位,bit12 位是 index 值,用来在 Service table 里查找自己的系统服务例程表。
补充:这个 ServiceTable 的内容貌似就是 SDT(Service Descirptor Table)!
7.1 ServiceTable
windows 的 Service Table 来自于 KTHREAD 结构内的 ServiceTable 成员,ServiceTable 的寻址是:
PKPCR Pcr = (PKPCR)0xffdff000; // 内核中的 Processor Cotnrol Region 地址为 0FFDFF000h
PKTHREAD Thread = Pcr->PrcbData.CurrentThread; // 得到 kernel 中的 KTHREAD 结构
PVOID ServiceTable = Thread->ServiceTable; // 得到 KTHREAD 结构中的 ServiceTable 地址
实际上 KTHREAD 的地址在 0x880c7330 上,在 windbg 上观察如下:
kd> dt _kthread 0x880c7330
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY [ 0x880c7340 - 0x880c7340 ]
+0x018 InitialStack : 0xf649c000 Void
+0x01c StackLimit : 0xf6495000 Void
+0x020 KernelStack : 0xf649b914 Void
+0x024 ThreadLock : 0
+0x028 ApcState : _KAPC_STATE
+0x028 ApcStateFill : [23] "Xs???"
+0x03f ApcQueueable : 0x1 ''
+0x040 NextProcessor : 0 ''
+0x041 DeferredProcessor : 0 ''
+0x042 AdjustReason : 0 ''
+0x043 AdjustIncrement : 2 ''
+0x044 ApcQueueLock : 0
+0x048 ContextSwitches : 0x14cda
+0x04c State : 0x2 ''
+0x04d NpxState : 0xa ''
+0x04e WaitIrql : 0 ''
+0x04f WaitMode : 1 ''
+0x050 WaitStatus : 0n0
+0x054 WaitBlockList : 0x880c73d8 _KWAIT_BLOCK
+0x054 GateObject : 0x880c73d8 _KGATE
+0x058 Alertable : 0x1 ''
+0x059 WaitNext : 0 ''
+0x05a WaitReason : 0x6 ''
+0x05b Priority : 12 ''
+0x05c EnableStackSwap : 0x1 ''
+0x05d SwapBusy : 0 ''
+0x05e Alerted : [2] ""
+0x060 WaitListEntry : _LIST_ENTRY [ 0xffdffb70 - 0xffdffb70 ]
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
+0x068 Queue : (null)
+0x06c WaitTime : 0x5378
+0x070 KernelApcDisable : 0n0
+0x072 SpecialApcDisable : 0n0
+0x070 CombinedApcDisable : 0
+0x074 Teb : 0x7ffdf000 Void
+0x078 Timer : _KTIMER
+0x078 TimerFill : [40] "???"
+0x0a0 AutoAlignment : 0y0
+0x0a0 DisableBoost : 0y0
+0x0a0 GuiThread : 0y0
+0x0a0 VdmSafe : 0y0
+0x0a0 ReservedFlags : 0y0000000000000000000000000000 (0)
+0x0a0 ThreadFlags : 0n0
+0x0a8 WaitBlock : [4] _KWAIT_BLOCK
+0x0a8 WaitBlockFill0 : [23] "???"
+0x0bf SystemAffinityActive : 0 ''
+0x0a8 WaitBlockFill1 : [47] "???"
+0x0d7 PreviousMode : 1 ''
+0x0a8 WaitBlockFill2 : [71] "???"
+0x0ef ResourceIndex : 0x1 ''
+0x0a8 WaitBlockFill3 : [95] "???"
+0x107 LargeStack : 0x1 ''
+0x108 QueueListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x110 TrapFrame : 0xf649bd64 _KTRAP_FRAME
+0x114 CallbackStack : (null)
+0x118 ServiceTable : 0x8089f440 Void <--- 这是 ServiceTable 地址
+0x11c ApcStateIndex : 0 ''
+0x11d IdealProcessor : 0 ''
+0x11e Preempted : 0 ''
+0x11f ProcessReadyQueue : 0 ''
+0x120 KernelStackResident : 0x1 ''
+0x121 BasePriority : 8 ''
+0x122 PriorityDecrement : 2 ''
+0x123 Saturation : 0 ''
+0x124 UserAffinity : 1
+0x128 Process : 0x87d2e958 _KPROCESS
+0x12c Affinity : 1
+0x130 ApcStatePointer : [2] 0x880c7358 _KAPC_STATE
+0x138 SavedApcState : _KAPC_STATE
+0x138 SavedApcStateFill : [23] "ht???"
+0x14f FreezeCount : 0 ''
+0x150 SuspendCount : 0 ''
+0x151 UserIdealProcessor : 0 ''
+0x152 CalloutActive : 0 ''
+0x153 Iopl : 0 ''
+0x154 Win32Thread : 0xe10b2350 Void
+0x158 StackBase : 0xf649c000 Void
+0x15c SuspendApc : _KAPC
+0x15c SuspendApcFill0 : [1] "??? 0$"
+0x15d Quantum : 32 ' '
+0x15c SuspendApcFill1 : [3] "???"
+0x15f QuantumReset : 0x24 '$'
+0x15c SuspendApcFill2 : [4] "???"
+0x160 KernelTime : 0x21e
+0x15c SuspendApcFill3 : [36] "???"
+0x180 TlsArray : (null)
+0x15c SuspendApcFill4 : [40] "???"
+0x184 LegoData : (null)
+0x15c SuspendApcFill5 : [47] "???"
+0x18b PowerState : 0 ''
+0x18c UserTime : 0x133
+0x190 SuspendSemaphore : _KSEMAPHORE
+0x190 SuspendSemaphorefill : [20] "???"
+0x1a4 SListFaultCount : 0
+0x1a8 ThreadListEntry : _LIST_ENTRY [ 0x88204888 - 0x87d2e9a8 ]
+0x1b0 SListFaultAddress : (null)
7.2 ServiceTable entry
由 index 值在 ServiceTable 里定位 Service Table entry 结构,它是 16 字节大,它看起来包括:
- ServiceRoutineTable:提供真正的系统服务例程的地址
- unknow 未知的元素
- MaxServiceNumber:最大的系统服务例程号
- ArgumentSizeTable:提供每个例程所需要的参数大小,这个值将要用来从 caller 里复制多少个参数。
当 index = 1 时,是指向 GUI 类的系统服务例程表,它们在 GUI 类系统内核驱动模块 win32k.sys 模块。例如:win32k!NtGdiBitBlt() 例程, index = 0 时,使用普通的系统例程表。
在 windows service 2003 系统上,两个系统服务例程表的 MaxServiceNumber 值分别是:
- index = 0:MaxServiceNumber 为 0x128
- index = 1:MaxServiceNumber 为 0x299
我们可以在 windbg 里查看这些 entry 值是多少,如下所示:
kd> dd 0x8089f440
8089f440 80830f84 00000000 00000128 80831428 ; index = 0
8089f450 bf9a7000 00000000 00000299 bf9a7d08 ; index = 1
下面的情况下是属于超出例程号:
if (ServiceNumber >= ServceTableEntry->MaxServiceNumber)
{
goto nt!KiBBTUnexpectedRange (80883662)
}
当提供的服务例程号大小等于 MaxServiceNumber 时就属于超限!因此:普通的系统服务例程号最大为 0x127 号,而 GUI 类例程号最大为 0x298 号
7.3 读取目标例程地址和参数 size
根据 ServiceNumber 号在 ServiceRoutineTable 里找到最终的系统服务例程地址值,例如 WriteFile() 的服务例程号是 0x11c,那么将在地址 [ServiceRoutineTable + 0x11c * 4] 的位置上就 nt 模块的 WriteFile() 地址。
而 ArgumentSize 值被读取后,用它来复制参数在 KiFastCallEntry() 的栈上:
80883949 8a0c18 mov cl,byte ptr [eax+ebx] ; 读取 ArgumentSize 值
8088394c 8b3f mov edi,dword ptr [edi] ; 读取 ServiceRoutineTable
8088394e 8b1c87 mov ebx,dword ptr [edi+eax*4] ; 读取服务例程地址
80883951 2be1 sub esp,ecx ; 在当前栈上开辟空间容纳参数
80883953 c1e902 shr ecx,2
80883956 8bfc mov edi,esp
80883958 3b35e8588980 cmp esi,dword ptr [nt!MmUserProbeAddress (808958e8)] ; 是否属于用户空间
8088395e 0f83f0010000 jae nt!KiSystemCallExit3+0x90 (80883b54)
nt!KiFastCallEntry+0xf4:
80883964 f3a5 rep movs dword ptr es:[edi],dword ptr [esi] ; 复制参数到当前栈上
80883966 ffd3 call ebx ; 调用最终的服务例程
在复制前,KiFastCallEntry() 还会判断 caller 的地址是否属于用户空间,当大于等于 MmUserProbeAddress 值时,属于内核空间,那么会进行另外的处理。
这个 MmUserProbeAddress 值为 0x7FFF0000,它是用户空间最大可用的地址值。在复制完参数后,紧接着就调用最终的系统服务例程!
8. KiFastCallEntry() 的返回处理
KiFastCallEntry() 的返回处理很复杂,需要检测多种情况,主要是检查出调用者属于 0 级,还是 3 级情况下。
之所以需要过多的检测,是因为进入 KiFastCallEntry() 的途径可能有多种,下面我们看看返回前的一些处理。
8.1 检查 APC 和提交 APC
//
// 假如调用者是 ring 3 并且需要检查 APC
//
if ((KiEnableApcCheck & 0x01) && (KtrapFrame->SegCs.RPL == 3))
{
KIRQL Irql = KeGetCurrentIrql();
//
// 如果当前 Irql 不是 PASSIVE_LEVEL 级别:抛出 BugCheck 错误!
//
if (Irql != PASSIVE_LEVEL)
{
Kpcr->Irql = PASSIVE_LEVEL;
KeBugCheck2(0x4A,
NtWriteFile,
Irql,
0,
0,
InitialStack);
}
Thread = Kpcr->CurrentThread;
//
// 检查 process 是否 attached ?
// 或者 APCs 是否被 disable ?
// 如果是的话:抛出 BugCheck 错误!
//
if ((Thread->ApcStateIndex & 0xff) || (Thread->CombinedApcDisable != 0))
{
KeBugCheck2(1,
NtWriteFile,
Thread->ApcStateIndex,
Thread->CombinedApcDisable,
0,
InitialStack);
}
}
//
// 恢复 stack frame
//
esp = ebp;
PKTRAP_FRAME OldTrapFrame = KtrapFrame->Edx; // 找到原 esp 值(进入 sysenter 之前)
Thread->TrapFrame = OldTrapFrame; // 保存在 Thread 的 TrapFrame 域里
cli();
if ((KtrapFrame->EFlags.VM == 1) || (KtrapFrame->SegCs.RPL == 3))
{
Thread->Alerted = 0;
while (Thread->ApcStateFill.AsUserApcPending != 0)
{
KtrapFrame->Eax = eax; // 保存返回值
KtrapFrame->SegFs = 0x3b;
KtrapFrame->SegDs = 0x23;
KtrapFrame->SegEs = 0x23;
KtrapFrame->SegGs = 0;
//
// 下面代码将 IRQL 提升到 APC_LEVEL 级别
// 然后提交 APC 排队处理(需要开中断)
// 完成后恢复原 IRQL 级别并关闭中断许可
//
OldIrql = KfRaiseIrql(APC_LEVEL);
sti();
KiDeliverApc(1, 0, KtrapFrame); // 提交 APC 处理
KfLowerIrql(OldIrql);
cli();
Thread->Alerted = 0;
}
}
当调用者是 user 层的话,如果需要检查 APC,则检查:
- 当前的 IRQL 必须在 PASSIVE_LEVEL 级别,否则会引发 BugCheck(死亡蓝屏)。
- 检查 process 是否 attached,或者 kernel APCs 是否被 disable 掉,这两种情况都会引发 BugCheck!
并且当有 APC 在 pending 状态时,需要提交完所有的 APC 进行处理。
8.2 检查是否开启调试功能
if (KtrapFrame->Dr7 & 0xffff23ff)
{
//
// 如果开启了 DR7 调试功能
//
if ((KtrapFrame->EFlags.VM == 1) || (KtrapFrame->SegCs.RPL == 3))
{
dr7 = 0; // 先关闭调试功能
dr0 = KtrapFrame->Dr0;
dr1 = KtrapFrame->Dr1;
dr2 = KtrapFrame->Dr2;
dr3 = KtrapFrame->Dr3;
dr6 = KtrapFrame->Dr6;
dr7 = KtrapFrame->Dr7;
}
}
如果是的话:恢复原来的 debug 寄存器值。
8.3 恢复部分 context 信息
if (KtrapFrame->EFlags.VM == 1)
{
//
// 如果开启了 V8086 模式
//
edx = KtrapFrame->Edx;
ecx = KtrapFrame->Ecx;
eax = KtrapFrame->Eax;
}
else if (KtrapFrame->SegCs & 0xFFF9 == 0)
{
//
// 如果 CS selector 为 0 值(0级下的 NULL selector)
//
KtrapFrame->SegCs = KtrapFrame->TempSegCs;
//
// ErrCode 指向构造的 TempStack 结构
//
PVOID TempStack = KtrapFrame->TempEsp - 0x0c;
KtrapFrame->ErrCode = TempStack;
//
// 下面构造一个 stack 结构(TempStack)以便使用 iretd 指令返回:
//
// Eflags
// SegCs
// esp ------> Eip
//
TempStack->Eflags = KtrapFrame->EFlags;
TempStack->SegCs = KtrapFrame->SegCs;
TempStack->Eip = KtrapFrame->Eip;
edi = KtrapFrame->Edi;
esi = KtrapFrame->Esi;
ebx = KtrapFrame->Ebx;
ebp = KtrapFrame->Ebp;
//
// 基于构造的 TempStack 来中断返回
//
esp = &TempStack;
iretd;
}
else if (KtrapFrame->SegCs.RPL == 3)
{
//
// 恢复 ring 3 的寄存器值
//
eax = KtrapFrame->Eax;
edx = KtrapFrame->Edx;
ecx = KtrapFrame->Ecx;
gs = KtrapFrame->SegGs;
es = KtrapFrame->SegEs;
ds = KtrapFrame->SegDs;
fs = KtrapFrame->SegFs;
}
else if (KtrapFrame->SegCs != 8)
{
fs = KtrapFrame->SegFs;
}
上面的代码中,其中一项:KtrapFrame->SegCs & 0xFFF9 == 0,意图是检查调用者的 CS 是否为 0,因为 windows 只使用 0 和 3 级的权限运行级别。
这段代码比较奇怪,注意,我不能完全理解它的用意!它在 ErrCode 的地方,构造一个 32 位宽的 far pointer,用来加载到 16 位的 SP 寄存器和 SS 寄存器。我想是为了返回到 16 位代码
8.4 完成一个典型的中断调用 stack 结构
在返回前,经过一系列的 pop 操作之后,形成一个典型的中断调用栈结构:
//
// 现在:esp 指向 KtrapFrame->Edi 域
// 下一步工作是:pop 出相关的值
//
esp = &KtrapFrame->Edi;
//
// 下面恢复原部分寄存器 context 信息
// 也就是执行:
// pop edi ----> 此时 esp 指向 edi 保存的地址
// pop esi
// pop ebx
// pop ebp
//
edi = KtrapFrame->Edi;
esi = KtrapFrame->Esi;
ebx = KtrapFrame->Ebx;
ebp = KtrapFrame->Ebp;
//
// 此时 stack frame 内的值为:
//
// esp ----> KtrapFrame->ErrCode
// KtrapFrame->Eip
// KtrapFrame->SegCs
// KtrapFrame->EFlags
// ktrapFrame->HardwareEsp
// KtrapFrame->HardwareSegSs
//
// 这是一个标准的调用中断 handler 入栈的情形,esp 指向 ErrorCode
//
它形成的 stack 结构如下图所示:
同样这个 stack 结构是基于 KtrapFrame 来构造的,因为此时 esp 指向 KtrapFrame 结构的 ErrCode 成员。形成这样一个典型的中断调用 stack 目的是:出于另一个调用途径可以使用 iret 指令来执行中断返回!
8.5 根据调用者的权限级别来决定如何返回
最后,KiFastCallEntry() 根据调用者是 Ring0 还是 Ring3 来使用何种方式返回。
-
- 当属于 ring0 时:
//
// 下面进行判断两种情形:
// 1. 当调用者属于 Ring 0 时,直接使用 jmp 指令返回到目标返回地址
// 2. 当调用者属于 Ring 3 时,使用 sysexit 指令返回
//
esp = esp + 4; // 跳过 ErrCode
if (KtrapFrame->SegCs.RPL == 0)
{
//
// 属于 ring0 的调用,下面操作是:手动销栈,读出 Eip 值到 edx 寄存器
// 等价于下面的操作:
// pop edx
// pop ecx
// popfd
// jmp edx
//
edx = KtrapFrame->Eip; // 得到返回地址
ecx = KtrapFrame->SegCs; // SegCs 值
eflags = KtrapFrame->Eflags; // pop 出 eflags 寄存器
//
// 跳转到 edx 地址上,也就是跳到:ntdll!KiFastSystemCallRet() 例程
// 这个 ntdll!KiFastSystemCallRet() 例程只有一条 ret 指令
// 通过这种方式返回到 API 的调用者(而非执行 sysexit 指令)
//
jmp edx
}
返回到 ring0 时,分别主动 pop 出 Eip,SegCs 以及 Eflags 值到 edx,ecx 以及 eflags 寄存器,然后使用 jmp edx 指令直接跳转到返回地址上。当然在 pop 操作之前需要先跳过 ErrCode 码。
显然这种情形发生在:在 ring0 里通过 KiFastCallEntry() 来分发系统服务例程时使用。
-
- 当属于 User 层时:
else
{
//
// 当调用者是用户代码时,返回的地址属于 3 级的用户层代码
//
if (KtrapFrame->EFlags.TF == 1)
{
//
// 如果开启单步调试,则使用 iretd 指令返回
//
iretd;
}
else
{
//
// 下面的处理,目的是:
// 1. 销掉部分 stack
// 2. 找到返回地址到 EDX 寄存器
// 3. 找到返回的 esp 值到 ECX 寄存器
// 4. 使用 sysexit 指令返回
//
edx = KtrapFrame->Eip; // pop 出 EIP 值
KtrapFrame->EFlags.IF = 0; // 清掉 stack 的中 IF 标志位
eflags = KtrapFrame->EFlags; // pop 出 eflags 值
ecx = ktrapFrame->HardwareEsp; // pop 出 Esp 值到
sti(); // 返回前打开中断
sysexit; // 执行 sysexit 指令返回
}
}
在返回 User 层里,当调用者的 Eflags.TF = 1 时(开启了单步调试)时,直接使用 iretd 指令返回!
最后返回正常途径下的 User 层时这是一个典型地通过 sysexit 指令返回的情形:将 pop 出 Eip 值到 edx 寄存器,pop 出 EFlags 值到 eflags 寄存器,还有 pop 出 Esp 值到 ecx 寄存器,构造一个 sysexit 指令的执行环境,通过 sysexit 指令返回!
至此:我对 KiFastCallEntry() 的大致流程分析完毕。
后记:事实上可能会多种途径通过 KiFastCallEntry() 来分发系统服务例程,以及返回调用者,这里的分析只是基于一种常用的使用途径。这里的分析并不代表全部!敬请留意。
版权所有 ©2009 - 2012 mik
jpg改rar