现在我们知道如何进入0环了,有两种方式,通过中断门或者快速调用。上一篇中最后留下了几个问题,其中一个就是关于如何保存那些3环寄存器原先的值(俗称保存现场),从而能够在执行完0环实现的功能后,顺利的返回到3环,今天我们就来探究一下这个问题,首先我们来认识几个结构:Trap_Frame,ETHREAD/KTHREAD,KPCR
_Trap_Frame
在Windbg中通过dt _KTrap_Frame进行查看
- (0x7c~0x88)在保护模式下没有被使用,只在虚拟8086模式中用得到
- (0x68~0x78)中断门进0环时,用于存储3环的CS,SS,ESP,EIP,EFLAGS
- (0x48~0x64)保存现场
- (0x00~0x44)调式及其它作用
简要介绍完了Trap_Frame结构,了解了这是保存现场用到的结构,后面在分析KiSystemService时,会介绍保存现场的主要过程。
ETHREAD&KTHREAD
ETHREAD(执行体线程块)是执行体层上的线程对象的数据结构。在Windows内核中,每个进程的每一个线程都对应着一个ETHREAD数据结构。
在Windbg中通过dt _ETHREAD进行查看
ETHREAD结构内嵌了一个KTHREAD对象作为第一个数据成员,因此一个指向ETHREAD对象的指针同时也是一个指向KTHREAD对象的指针。
在Windbg中通过dt _KTHREAD进行查看
大致先解下这些结构即可,后续在介绍到线程与进程处时,会慢慢分析各个字段。
KPCR
描述:
- 全称为CPU控制区(Processor Control Region)
- 每一个CPU都有一个CPU控制区,跟TLB一样,一核一个KPCR
指令:
- dt _KPCR:查看KPCR结构
- dd KeNumberProcessors:查看KPCR数量
- dd KiProcessorBlock:查看KPCR位置
由于当前虚拟机只分配了一个核,所以数量是1
同理,因为单核,这里只显示了一个值,这个地址显示的是ffdff120,也就是KPCR偏移0x120的位置。KPCR偏移0x120的位置是 _KPRCB,可以理解为扩展的KPCR
KiSystemService
了解完上面介绍的结构,下面我们就可以分析一下0环函数KiSystemService,到底是如何保存现场的。
函数主体并不长,按照填充的结构不同我们来逐步分析。
0x1
1 2 3 4 5 6
| 804df631 6a00 push 0 //ErrorCode 804df633 55 push ebp 804df634 53 push ebx 804df635 56 push esi 804df636 57 push edi 804df637 0fa0 push fs
|
首先来看这一段,为什么要push 0起手呢? 这里先回顾一下Trap_Frame结构。
这是一个结构,换句话说,就是进入0环后的堆栈将会像这种形式组织起来,在刚进0环是,esp是位于 (0x78) 的位置,我们知道,通过中断门进0环时,会将3环的寄存器压栈,包括CS,SS,EIP,ESP和EFLAGS。因此在进入0环后,ESP的位置是位于 (0x68) 处。虽然2E号中断只会压入5个值,但是有些情况会压入6个值(例如发生缺页异常时),而第6个值,就是ErrCode,为了对齐,保持堆栈平衡,操作系统这里会自己补一个0,这也就解释了为什么第一步是push 0。
接下来,就是保存ebp,ebx,esi,esi,fs依次压栈,保存到Trap_Frame结构中描述的位置。
0x2
1 2 3 4 5 6 7
| 804df639 bb30000000 mov ebx,30h 804df63e 8ee3 mov fs,bx //写入fs段寄存器 804df640 ff3500f0dfff push dword ptr ds:[0FFDFF000h] //保存旧的异常链表(ExceptionList) 804df646 c70500f0dfffffffffff mov dword ptr ds:[0FFDFF000h],0FFFFFFFFh //将新的异常链表赋值为-1 804df650 8b3524f1dfff mov esi,dword ptr ds:[0FFDFF124h] //获取当前线程KTHREAD 804df656 ffb640010000 push dword ptr [esi+140h] //将先前模式(PreviousMode)压栈 804df65c 83ec48 sub esp,48h //提升堆栈(栈顶执行Trap_Frame头)
|
我们来看这部分做了什么事
- 首先是将段选择子0x30写入fs段寄存器 根据段选择子确定段描述符,然后可以发现fs指向的地方(0xffdff000)刚好是KPCR这个结构。
- 然后压栈了KPCR首地址位置的值 可以发现,KPCR首地址位置存的是异常链表(ExceptionList),这里压栈了旧的异常链表,并将异常链表的值置为-1。至于异常链表的结构,留到后面再讲。
- 接着获取到KPCR + 0x124位置的值,并存入esi,然后将esi+0x140处的值压栈 可以发现KPCR+0x124处(赋给esi)的值,存的是当前线程(CurrentThread)的KTHREAD,我们再找到(esi)KTHREAD+0x140偏移,发现压栈的字段叫做先前模式(PerviousMode)
- 最后提升堆栈0x48个字节
经过这一部分的操作后,堆栈栈顶刚好指向_Trap_Frame的首地址,并完成了异常链表和先前模式的压栈操作。
0x3
1 2 3 4 5 6 7 8
| 804df65f 8b5c246c mov ebx,dword ptr [esp+6Ch] //取进入中断门压栈的CS 804df663 83e301 and ebx,1 //计算出调用中断门前的权限 804df666 889e40010000 mov byte ptr [esi+140h],bl //重新填写KTHREAD中的先前模式 804df66c 8bec mov ebp,esp //让ebp指向_Trap_Frame首地址 804df66e 8b9e34010000 mov ebx,dword ptr [esi+134h] 804df674 895d3c mov dword ptr [ebp+3Ch],ebx //将旧的_Trap_Frame保存到edx中 804df677 89ae34010000 mov dword ptr [esi+134h],ebp //更新_Trap_Frame 804df67d fc cld
|
继续分析这一部分
- 我们来看前3行,它做了什么事呢,先取出_Trap_Frame 0x6C偏移处的值,即进入中断门前,程序CS的值,然后和1进行了与运算,并将bl的值,填入上面提到的先前模式 为什么和1进行与运算就可以算出先前模式呢?难道不直接填3吗?首先我们知道,Windows只用了0环和3环,其次,即使执行中断门,执行前的程序也可以是0环程序,所以保守起见,这里和CPL的最低位进行与运算;若结果为1,说明是3环程序执行了中断门,若为0,说明是0环程序执行的中断门。从而算出先前模式,并填入到当前线程KTHREAD的先前模式字段中。
- 上面3行更新了先前模式,接下来4行的作用就是更新了_Trap_Frame,我们知道栈顶指向Trap_Frame的首地址,现在让栈底也指向Trap_Frame的首地址,便于寻址。 由0x2的分析可知esi指向KTHREAD,KTHREAD+0x134则指向Trap_Frame,这里的Trap_Frame是旧的地址(这里则是Null),因此将它保存至堆栈,再将现在的Trap_Frame的地址写入,也就完成了更新。
- cld指令修改了EFLAGS寄存器的DF位
0x4
1 2 3 4 5 6 7 8 9 10
| 804df67e 8b5d60 mov ebx,dword ptr [ebp+60h] //取3环的ebp给ebx 804df681 8b7d68 mov edi,dword ptr [ebp+68h] //取3环的eip给edi 804df684 89550c mov dword ptr [ebp+0Ch],edx //edx存的是3环第一个参数的地址,赋到_Trap_Frame的DbgArgPointer的位置 804df687 c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h //将操作系统用的标志赋给DbgArgMark 804df68e 895d00 mov dword ptr [ebp],ebx //将3环的ebp赋值到DbgEbp 804df691 897d04 mov dword ptr [ebp+4],edi //将3环的eip赋值到DbgEip 804df694 f6462cff test byte ptr [esi+2Ch],0FFh //判断DebugActive处的值是否为-1 804df698 0f858efeffff jne nt!Dr_kss_a (804df52c) //跳转至调试寄存器保存函数 804df69e fb sti 804df69f e9dd000000 jmp nt!KiFastCallEntry+0x8d (804df781)
|
来看最后一部分
- 前6行,很好理解,主要是对_Trap_Frame调试部分的填充,一张图就可以概括
- 接下来的两行,会比较esi+0x2C的位置是否为-1 这个地方值如果不是-1,说明处于调试状态,紧接着会跳转到Dr_kss_a这个例程里,这个例程作用是将Dr0~Dr7这些调试寄存器的值保存到_Trap_Frame中,用于调试。同样的,了解了这个字段后,我们可以写一个程序,不断的修改这个值,将DebugActive这个值置为-1,这样程序就不会保存调试寄存器,也就无法调试,这是一种反调试的手段。
- 最后,程序会跳转到KiFastCallEntry+0x8D这个位置继续执行,而这个位置,也是KiFastCallEntry执行完后跳转的地方。之所以分了两种方式,是因为中断门进0环时,压栈了5个值(ESP,EIP,CS,SS,EFLAGS)而快速调用没有,导致它们在填写_Trap_Frame结构的方式不同,但是在填完后,保存现场以后,后面执行的函数就一样了。
KiFastCallEntry
KiFastCallEntry保存现场的方式略微复杂,因为没有通过中断门对3环的5个寄存器进行压栈。由于分析的代码较多,这部分就不贴图了,可以参照KiSystemService的方法,在Windbg中找到对应结构进行分析。
KiFastCallEntry要分为两个部分来看,第一个部分,是和KiSystemService所做的一样,对_Trap_Frame结构的填充,进行保存现场。完了之后,第二个部分,从KiFastCallEntry+0x8D开始,这是KiSystemService执行完后跳转的地方,也是KiFastCallEntry顺序执行的地方,是双方都要执行的代码,这也意味着,从这个地方开始,两种进0环的方式就统一了。
本篇只介绍第一部分,看看KiFastCallEntry在填充_Trap_Frame时与KiSystemService有何不同吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| kd> u KiFastCallEntry L55 nt!KiFastCallEntry: 804df6f0 b923000000 mov ecx,23h 804df6f5 6a30 push 30h 804df6f7 0fa1 pop fs //令fs寄存器指向KPCR首地址 804df6f9 8ed9 mov ds,cx //令ds=0x23 804df6fb 8ec1 mov es,cx //令es=0x23 804df6fd 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h] //另ecx指向TSS 804df703 8b6104 mov esp,dword ptr [ecx+4] //取TSS中的esp0赋值给当前esp 804df706 6a23 push 23h //将3环ss压栈(_Trap_Frame+0x78) 804df708 52 push edx //将3环栈顶esp3压栈(+0x74) 804df709 9c pushfd //将eflags寄存器压栈(+0x70) 804df70a 6a02 push 2 804df70c 83c208 add edx,8 //获取外部第一个参数的位置(ReadProcessMemory共Call 804df70c //了两次才到sysenter,因此压栈了2个返回地址,需要+8) 804df70f 9d popfd //将2写入eflags寄存器 804df710 804c240102 or byte ptr [esp+1],2 //没看懂有啥用 804df715 6a1b push 1Bh //将3环cs压栈(+0x6c) 804df717 ff350403dfff push dword ptr ds:[0FFDF0304h] //将3环eip压栈(+0x68) 804df71d 6a00 push 0 //将Errcode压栈(+0x64) 804df71f 55 push ebp //将3环ebp压栈(+0x60) 804df720 53 push ebx //将3环ebx压栈(0x5c) 804df721 56 push esi //将3环esi压栈(+0x58) 804df722 57 push edi //将3环edi压栈(+0x54) 804df723 8b1d1cf0dfff mov ebx,dword ptr ds:[0FFDFF01Ch] //将指向KPCR自己的指针存到ebx里 804df729 6a3b push 3Bh //将3环fs压栈(+0x50) 804df72b 8bb324010000 mov esi,dword ptr [ebx+124h] //将当前线程的KTHREAD存到esi 804df731 ff33 push dword ptr [ebx] //将异常链表(ExceptionList)压栈(+0x4c) 804df733 c703ffffffff mov dword ptr [ebx],0FFFFFFFFh //更新异常链表的值为-1 804df739 8b6e18 mov ebp,dword ptr [esi+18h] //通过KTHREAD的InitialStack更新0环栈底 804df73c 6a01 push 1 //将旧的先前模式(PreviousMode)压栈(+0x48) 804df73e 83ec48 sub esp,48h //令esp指向_Trap_Frame首地址 804df741 81ed9c020000 sub ebp,29Ch //这部分没看懂,舒默的分析是计算初试stack的Trap_Frame基址 804df741 //这个0x29c的值等于:NPX_FRAME_LENGTH + TRAP_FRAME_LENGTH 804df741 //其中NPX_FRAME_LENGTH = 0x210, TRAP_FRAME_LENGTH = 0x8c 804df747 c6864001000001 mov byte ptr [esi+140h],1 //更新先前模式为1 804df74e 3bec cmp ebp,esp //比较两个Trap_Frame基址,若不同则跳转去处理 804df750 0f8572ffffff jne nt!KiFastCallEntry2+0x24 (804df6c8) 804df756 83652c00 and dword ptr [ebp+2Ch],0 //将Dr7的值置0(+0x2c) 804df75a f6462cff test byte ptr [esi+2Ch],0FFh //判断当前线程的DebugActive是否为-1 804df75e 89ae34010000 mov dword ptr [esi+134h],ebp //更新当前线程的_Trap_Frame基址 804df764 0f8546feffff jne nt!Dr_FastCallDrSave (804df5b0)//若DebugActive不为-1则跳转 804df76a 8b5d60 mov ebx,dword ptr [ebp+60h] //将3环的Ebp赋值给当前ebx 804df76d 8b7d68 mov edi,dword ptr [ebp+68h] //将3环的Eip赋值给当前edi 804df770 89550c mov dword ptr [ebp+0Ch],edx //将第一个参数的地址存到DbgArgPointer 804df773 c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h //将0x0BADB0D00存到DbgArgMark 804df77a 895d00 mov dword ptr [ebp],ebx //将3环ebp存到DbgEbp 804df77d 897d04 mov dword ptr [ebp+4],edi //将3环eip存到DbgEip 804df780 fb sti //设置EFLAGS的IF位,允许中断发生
|
这里就先分析到这,至于从KiFastCallEntry+0x8D开始的第二部分,由于是KiSystemService和KiFastCallEntry的公共代码,两种进0环的方式都会执行,就不再本篇中分析了,留到下一篇介绍系统服务表和SSDT时再介绍。
总结
这一篇通过学习_Trap_Frame,KTHREAD,KPCR这些结构,分析KiSystemService&KiFastCallEntry了解了在进入0环后,保存现场的方式。尽管采用了两种不同的手段,但是思路总体来说是一样的,就是通过填充Trap_Frame结构完成3环寄存器的保存。在下一篇中,我们将继续探究,在保存完现场后,程序是如何找到想要执行的函数的。
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=40
参考文章:https://blog.csdn.net/qq_41988448/article/details/102886413
https://blog.csdn.net/qq_38474570/article/details/103652993