SYSENTER
如果我们已经稍微熟悉了ntdll里面的基本结构,我们就可以知道从R3转向R0的入口大概都是一个汇编,这里以OpenProccess函数为例
这里强调一下,图中红框标出的位置就是同一段汇编的两个不同的声明,所以Nt开头和Zw开头是一样的
可以看到,汇编就只有短短的几行,这里注释一下
mov eax, 0BEh //系统服务例程号
mov edx, 7FFE0300h // 取得 KiFastCallEntry() stub 函数
call dword ptr [edx] //调用这个 stub 函数
这是什么意思呢?这就说明,在ntdll封装的实现中,本质上也是一个转发,在找到了不同的系统服务例程号(不同函数不同)后,去找到KiFastCallEntry这个函数,然后根据具体的调用号来进入内核调用
那第二个这个将7ffe0300移入edx有什么讲究吗?
有的兄弟,有的
_KUSER_SHARED_DATA
在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于 User 层和 Kernel 层共享某些数据,在 sysenter 快速切入机制里就使用了这个区域。
它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在 User 和 Kernel 层地址分别为:
- User 层地址为:0x7ffe0000
- Kernnel 层地址为:0xffdf0000
介绍到这里,这个7ffe0300就是KUSER_SHARED_DATA这个结构里面偏移0x300的位置,就是我们需要的系统调用的入口
0: kd> dt _KUSER_SHARED_DATA 0x7ffe0000
ntdll!_KUSER_SHARED_DATA
+0x000 TickCountLowDeprecated : 0
+0x004 TickCountMultiplier : 0xf99a027
+0x008 InterruptTime : _KSYSTEM_TIME
+0x014 SystemTime : _KSYSTEM_TIME
+0x020 TimeZoneBias : _KSYSTEM_TIME
+0x02c ImageNumberLow : 0x14c
+0x02e ImageNumberHigh : 0x14c
+0x030 NtSystemRoot : [260] "C:\Windows"
+0x238 MaxStackTraceDepth : 0
+0x23c CryptoExponent : 0
+0x240 TimeZoneId : 0
+0x244 LargePageMinimum : 0x200000
+0x248 Reserved2 : [7] 0
+0x264 NtProductType : 1 ( NtProductWinNt )
+0x268 ProductTypeIsValid : 0x1 ''
+0x26c NtMajorVersion : 6
+0x270 NtMinorVersion : 1
。。。。。。。。。。。。。。。。。。。。
+0x2f4 DataFlagsPad : [1] 0
+0x2f8 TestRetInstruction : 0xc3
+0x300 SystemCall : 0x772670b0//系统调用stub的位置
而它又会转入内核层里面的KiFastSystemcall,这里就看见了我们要介绍的主角 sysenter
; _DWORD __stdcall KiFastSystemCall()
public _KiFastSystemCall@0
_KiFastSystemCall@0 proc near
mov edx, esp
sysenter
_KiFastSystemCall@0 endp
指令执行过程
在R3的代码调用了sysenter指令之后,CPU会做出如下操作
1.将SYSENTER_CS_MSR的值装载到cs寄存器 174
2.将SYSENTER_EIP_MSR的值装载到eip寄存器 176
3.将SYSENTER_CS_MSR的值加8,装载到ss寄存器
4.将SYSENTER_ESP_MSR的值装载到esp寄存器 175
5.将特权基本切换到Ring0
6.如果EFLAGs寄存器的VM标志被置位,则清除该标志
7.开始执行指定的R0代码
咋一看,有点复杂,走了七步
但是我们仔细一看,前4步都是在设置环境,也是我们之前在保护模式里面所强调的R3进R0的本质
先来看看这些SYSENTER_MSR里面到底是什么
0: kd> rdmsr 176
msr[176] = 00000000`83e5c0c0
0: kd> rdmsr 175
msr[175] = 00000000`80790000
0: kd> rdmsr 174
msr[174] = 00000000`00000008//这里cs的值是0x8
我们还可以顺便看看这些eip指向的是什么,这里我没有加载符号表,但是这个函数就是KiFastCallEntry
0: kd> u 00000000`83e5c0c0
ReadVirtual: 83e5c0c0 not properly sign extended
83e5c0c0 b923000000 mov ecx,23h
83e5c0c5 6a30 push 30h
83e5c0c7 0fa1 pop fs
83e5c0c9 8ed9 mov ds,cx
83e5c0cb 8ec1 mov es,cx
83e5c0cd 648b0d40000000 mov ecx,dword ptr fs:[40h]
83e5c0d4 8b6104 mov esp,dword ptr [ecx+4]
83e5c0d7 6a23 push 23h
最后看看esp,这里esp就是一个过渡的值,这个esp值在win32里面并没有被直接使用
0: kd> u 00000000`80790000
80790000 ?? ???
^ Memory access error in 'u 00000000`80790000'
而是使用 KPCR 结构内 TSS 块里的 ESP 值
KPCR
当线程进入0环时,FS:[0]指向KPCR
每个CPU都有一个KPCR结构体
KPCR中存储了CPU本身要用的一些重要数据:GDT、IDT以及线程相关的一些信息。
0: kd> dt _KPCR
ntdll!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Used_StackBase : Ptr32 Void
。。。。。。。。。。。。。。。。。。。。。。。。。。。
+0x040 TSS : Ptr32 _KTSS//TSS块
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
。。。。。。。。。。。。。。。。。。。。。。。。。
0: kd> dt _ktss
ntdll!_KTSS
。。。。。。。。。。。。。。。。。。。。。。。。
+0x002 Reserved0 : Uint2B
+0x004 Esp0 : Uint4B//这里才是真正的esp的值
+0x008 Ss0 : Uint2B
。。。。。。。。。。。。。。。。
最后看一个设置VM位的解释
VM(bit 17) [Virtual-8086 mode flag] 置1以允许虚拟8086模式,清除则返回保护模式。
KTRAP_FRAME
还有一点点逻辑没有补全,我们之前一直在介绍R3跳入R0时候的几个值从哪里来,那么R3的值此时是被覆盖了的,所以我们也需要保存这些原来的值,不然在返回R3时肯定就会发生崩溃
保存这些值的结构就是KTRAP_FRAME
0: kd> dt _KTRAP_FRAME
ntdll!_KTRAP_FRAME
+0x000 DbgEbp : Uint4B
+0x004 DbgEip : Uint4B
+0x008 DbgArgMark : Uint4B
+0x00c DbgArgPointer : Uint4B
+0x010 TempSegCs : Uint2B
+0x012 Logging : UChar
+0x013 Reserved : UChar
+0x014 TempEsp : Uint4B
+0x018 Dr0 : Uint4B
+0x01c Dr1 : Uint4B
+0x020 Dr2 : Uint4B
+0x024 Dr3 : Uint4B
+0x028 Dr6 : Uint4B
+0x02c Dr7 : Uint4B
+0x030 SegGs : Uint4B
+0x034 SegEs : Uint4B
+0x038 SegDs : Uint4B
+0x03c Edx : Uint4B
+0x040 Ecx : Uint4B
+0x044 Eax : Uint4B
+0x048 PreviousPreviousMode : Uint4B
+0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : Uint4B
+0x054 Edi : Uint4B
+0x058 Esi : Uint4B
+0x05c Ebx : Uint4B
+0x060 Ebp : Uint4B
+0x064 ErrCode : Uint4B
+0x068 Eip : Uint4B
+0x06c SegCs : Uint4B
+0x070 EFlags : Uint4B
+0x074 HardwareEsp : Uint4B
+0x078 HardwareSegSs : Uint4B
+0x07c V86Es : Uint4B
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B
可以看见我们一般涉及的寄存器都出现了,这也是为我们所保存的现场
那么这个突然跳出来的KTRAP_FRAME又和谁有关联呢?不卖关子了,就是esp0
KTRAP_FRAME 结构基址等于 Esp0 值减 0x7c,同理也就是说Esp0的值指向了上面KTRAP_FRAME 结构里面的V86Es
是不是一切逻辑都联系起来了(😀
KiFastCallEntry(请配合ntkrlpa.exe食用)
介绍完了基础知识,那么我们就可以来研究一下这个快速调用的入口了(也就是我们之前在R3转R0时候的那个eip)
为了方便起见,接下来我就不截图,改为介绍汇编了
首先就是将ds,es置为23,fs置为30,这也是之前我们所做的测试中,如果在保护模式下修改寄存器强行进入R0而其他线程运行没有报错的原因——每次进R0前都会强制把几个寄存器置为在R3的状态
mov ecx, 23h ; '#'
push 30h ; '0'
pop fs
mov ds, ecx
mov es, ecx
这里我们看见首先从fs偏0x40处取了一个值,之前通过学习我们知道,fs指向的是KPCR这么一个结构,回去复习一下,0x40也就是我们的tss,这也是我们之前说的sysenter指令所给的SYSENTER_ESP_MSR这个值实际上只是一个过渡,之后系统会自己去线程里面找这个esp的值
mov ecx, large fs:40h
mov esp, [ecx+4]
push 23h ; '#'
push edx
pushf
0: kd> dt _ktss
ntdll!_KTSS
+0x000 Backlink : Uint2B
+0x002 Reserved0 : Uint2B
+0x004 Esp0 : Uint4B//取到了这里
+0x008 Ss0 : Uint2B
+0x00a Reserved1 : Uint2B
+0x00c NotUsed1 : [4] Uint4B
接着上面的汇编还没完,它连续压了三个值,这时候我们又打开KTRAP_FRAME 这个结构来看
+0x070 EFlags : Uint4B//保存了eflags
+0x074 HardwareEsp : Uint4B//edx,如果还记得KiFastSystemCall的内容,就会知道这里的edx就是R3下的esp,
+0x078 HardwareSegSs : Uint4B//23
+0x07c V86Es : Uint4B//最开始我们堆栈的值指向这里
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B
继续向下看
loc_43E0DB:
push 2
add edx, 8
popf
这里首先看见把一个2的值弹入了eflags中,这个edx加8又是什么意思呢?
esp -》 retaddress
previous_ebp
参数1
参数2
之前我们说过edx此时就是R3时的esp,所以这个+8也就意味着我们的edx在这之后都指向了R3的参数
继续向下,这次截长一点
or byte ptr [esp+1], 2
push 1Bh
push dword ptr ds:0FFDF0304h
push 0
push ebp
push ebx
push esi
push edi
mov ebx, large fs:1Ch
push 3Bh ; ';'
mov esi, [ebx+124h]
push dword ptr [ebx]
mov dword ptr [ebx], 0FFFFFFFFh
mov ebp, [esi+28h]
push 1
sub esp, 48h
sub ebp, 29Ch
首先一个or了一个esp+1的位置,此时的esp根据我们前面的分析,是eflags,加1字节也就是加8位
指向的TF,或上2,也就是把中断使能标志(IF)打开了,简而言之,开中断
接下来连续push了好几个值我就直接上结构了
+0x054 Edi : Uint4B//edi
+0x058 Esi : Uint4B//esi
+0x05c Ebx : Uint4B//ebx
+0x060 Ebp : Uint4B//ebp
+0x064 ErrCode : Uint4B//0
+0x068 Eip : Uint4B//0FFDF0304
+0x06c SegCs : Uint4B //1b
这里面,这个eip的值可以说道说道,在本文的上面说过有一个结构KUSER_SHARED_DATA,这个结构在R0中的地址是0xffdf0000,那这里的0FFDF0304打开结构一看,是我们的系统返回地址,这里后面我们会继续介绍,这里暂时按下不表
+0x304 SystemCallReturn : 0x772670b4
相信真的一点一点看到这里的人,没被我弄晕的话,应该有自己简单分析的能力了
mov ebx, large fs:1Ch //KPCR结构+1c 也就是+0x01c SelfPcr这里
push 3Bh ; ';'压3b进KTRAP_FRAME中的segfs
mov esi, [ebx+124h]//ebx此时是kpcr,kpcr本身只有0x120,第0x120是PrcbData,这是一个KPCRB结构,可以算是扩展,KPCRB结构再偏4就是CurrentThread这么一个地方,它是_KTHREAD结构
push dword ptr [ebx]//有点复杂,kpcr.NtTib.ExceptionList,等价压入了这样一个异常链
mov dword ptr [ebx], 0FFFFFFFFh//异常链的初始化
mov ebp, [esi+28h]//此时的ebp被换为了_KTHREAD偏0x28的值,也就是InitialStack
push 1//KTRAP_FRAME中的PreviousPreviousMode,如果从R3跳R0那就是1,反之就是0
忙完了这么一段,最后是两句汇编,一下子往上提了一段栈esp和ebp都指向了同一个位置,恍然大悟,原来到这里本质上都是我们函数调用的保存现场
sub esp, 48h
sub ebp, 29Ch
也就是,在提栈后,我们才真正进入线程的堆栈。
之后的分析我就不具体说是哪里哪里的偏移了,直接会讲这是什么
再接下来,还是对于现场的保存,只是换成了调试寄存器
mov byte ptr [esi+13Ah], 1 ; PreviousMode
cmp ebp, esp ; 如果堆栈异常则进入KiTrap06
jnz short loc_43E0BB
and dword ptr [ebp+2Ch], 0 ; dr7寄存器初始化
test byte ptr [esi+3], 0DFh ; 这个地方就是硬件断点的检测
mov [esi+128h], ebp ; 之前说过了,提栈之后,esp和ebp都指向了KTRAP_FRAME
jnz Dr_FastCallDrSave ; 保存调试寄存器,如果检测出硬件断点那么就会把KTRAP_FRAME里面的调试寄存器填满
继续向下走,这里就是把几个参数保存起来了,方便我们在后续如果出现了异常,可以知道几个基本的现场的参数
mov ebx, [ebp+60h] ; KTRAP_FRAME ebp 保存到ebx里面
mov edi, [ebp+68h] ; KTRAP_FRAME eip 保存到edi里面
mov [ebp+0Ch], edx ; DbgArgPointer 临时变量里面装了edx的值,也就是我们参数的位置
mov dword ptr [ebp+8], 0BADB0D00h ; 不知道什么用
mov [ebp+0], ebx ; DbgEbp
mov [ebp+4], edi ; DbgEip
sti ; 开中断
我们继续向下
mov edi, eax ; eax里面存的就是ssdt号
shr edi, 8 ; 服务号右移8位
and edi, 10h ; 与一个10,本质上是在区分ssdt和sssdt
这里插一个小知识我们这时候的ssdt号并不是索引,它还只是一个服务号还没经过处理
如果是经由ntdll的系统调用,不和UI产生关系,这样的服务号都在0x1000以下
如果是经由user32.dll GDI32.dll 这类过UI的,这样的服务号都在0x1000以上
比如我是一个0x1008的服务号,右移了8位后就是0x00000010,这样和0x10相与就不会为0
如果是0x990 同理就是0x00000009,和0x10相与后为0
所以这里就是为了区分这两种服务号
这个ssdt表在32位里面是导出的
kd> dd KeServiceDescriptorTable
83f839c0 83e97d9c 00000000 00000191 83e983e4 //83e97d9c函数表地址,00000191函数表中函数个数
83f839d0 00000000 00000000 00000000 00000000
83f839e0 83ef66af 00000000 00000000 00000bb8
83f839f0 00000011 00000100 5385d2ba d717548f
83f83a00 83e97d9c 00000000 00000191 83e983e4
83f83a10 00000000 00000000 00000000 00000000
83f83a20 00000000 00000000 83f83a24 00000340
83f83a30 00000340 00000000 00000007 00000000
同理,sssdt也是
kd> dd KeServiceDescriptorTableShadow
83f83a00 83e97d9c 00000000 00000191 83e983e4
83f83a10 00000000 00000000 00000000 00000000
83f83a20 00000000 00000000 83f83a24 00000340
83f83a30 00000340 00000000 00000007 00000000
83f83a40 00000000 00000000 00000000 00000000
83f83a50 00000000 00000000 00000000 00000000
83f83a60 00000000 00000000 00000000 00000000
83f83a70 00000000 00000000 00000000 ffffffff
继续向下看
mov ecx, edi
add edi, [esi+0BCh] ; 计算取哪个表的偏移
mov ebx, eax
and eax, 0FFFh ; 取出索引
cmp eax, [edi+8] ; 判断数组是否越界,实际上就是看eax中的索引号有没有大于ssdt表中的函数个数
jnb _KiBBTUnexpectedRange
继续
cmp ecx, 10h ; 如果是UI线程
jnz short loc_43E18E
mov ecx, [esi+88h] ; 线程的teb
xor esi, esi
loc_43E17C: ; DATA XREF: _KiTrap0E+156↓o
or esi, [ecx+0F70h]
jz short loc_43E18E
push edx
push eax
call ds:_KeGdiFlushUserBatch ; 批量刷新UI
pop eax
pop edx
再往后就是对参数的处理
inc large dword ptr fs:6B0h ; 调用一次系统调用此书就+1
mov esi, edx ; 获取参数,之前说过了edx就是参数地址
xor ecx, ecx
mov edx, [edi+0Ch] ; 获取参数表
mov edi, [edi] ; 函数表
mov cl, [eax+edx] ; 函数索引
mov edx, [edi+eax*4] ; 函数地址
sub esp, ecx ; 提升堆栈,准备copy参数
shr ecx, 2 ; 计算参数个数
mov edi, esp
cmp esi, ds:_MmUserProbeAddress ; 看看是不是R3的地址
jnb loc_43E3E5
最后一小段。最后是call了函数表里面的函数地址
loc_43E1B7: ; CODE XREF: _KiFastCallEntry+329↓j
rep movsd ; 复制参数
test byte ptr [ebp+6Ch], 1 ; 判断TRAP_FRAME.SegCs,看看调用来自R3还是R0
jz short loc_43E1D5 ; 如果是R0那么直接往下跳
mov ecx, large fs:124h ; KPCR.PrcbData.CurrentThread
mov edi, [esp+7Ch+var_7C]
mov [ecx+13Ch], ebx ; 储存ETHREAD.tcb.SystemCallNumber
mov [ecx+12Ch], edi ; 存储ETHREAD.Tcb.FirstArgument
mov ebx, edx
test byte ptr ds:dword_537908, 40h ; 系统日志相关
setnz byte ptr [ebp+12h]
jnz loc_43E574
call ebx ; 这里的ebx来自于之前的edx,也就是函数地址