avatar

Catalog
API函数的调用过程(保存现场)

现在我们知道如何进入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

描述:

  1. 全称为CPU控制区(Processor Control Region)
  2. 每一个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

Code
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

Code
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头)

我们来看这部分做了什么事

  1. 首先是将段选择子0x30写入fs段寄存器 根据段选择子确定段描述符,然后可以发现fs指向的地方(0xffdff000)刚好是KPCR这个结构。
  2. 然后压栈了KPCR首地址位置的值 可以发现,KPCR首地址位置存的是异常链表(ExceptionList),这里压栈了旧的异常链表,并将异常链表的值置为-1。至于异常链表的结构,留到后面再讲。
  3. 接着获取到KPCR + 0x124位置的值,并存入esi,然后将esi+0x140处的值压栈 可以发现KPCR+0x124处(赋给esi)的值,存的是当前线程(CurrentThread)的KTHREAD,我们再找到(esi)KTHREAD+0x140偏移,发现压栈的字段叫做先前模式(PerviousMode)
  4. 最后提升堆栈0x48个字节

经过这一部分的操作后,堆栈栈顶刚好指向_Trap_Frame的首地址,并完成了异常链表和先前模式的压栈操作

0x3

Code
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

继续分析这一部分

  1. 我们来看前3行,它做了什么事呢,先取出_Trap_Frame 0x6C偏移处的值,即进入中断门前,程序CS的值,然后和1进行了与运算,并将bl的值,填入上面提到的先前模式 为什么和1进行与运算就可以算出先前模式呢?难道不直接填3吗?首先我们知道,Windows只用了0环和3环,其次,即使执行中断门,执行前的程序也可以是0环程序,所以保守起见,这里和CPL的最低位进行与运算;若结果为1,说明是3环程序执行了中断门,若为0,说明是0环程序执行的中断门。从而算出先前模式,并填入到当前线程KTHREAD的先前模式字段中。
  2. 上面3行更新了先前模式,接下来4行的作用就是更新了_Trap_Frame,我们知道栈顶指向Trap_Frame的首地址,现在让栈底也指向Trap_Frame的首地址,便于寻址。 由0x2的分析可知esi指向KTHREAD,KTHREAD+0x134则指向Trap_Frame,这里的Trap_Frame是旧的地址(这里则是Null),因此将它保存至堆栈,再将现在的Trap_Frame的地址写入,也就完成了更新。
  3. cld指令修改了EFLAGS寄存器的DF位

0x4

Code
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)

来看最后一部分

  1. 前6行,很好理解,主要是对_Trap_Frame调试部分的填充,一张图就可以概括
  2. 接下来的两行,会比较esi+0x2C的位置是否为-1 这个地方值如果不是-1,说明处于调试状态,紧接着会跳转到Dr_kss_a这个例程里,这个例程作用是将Dr0~Dr7这些调试寄存器的值保存到_Trap_Frame中,用于调试。同样的,了解了这个字段后,我们可以写一个程序,不断的修改这个值,将DebugActive这个值置为-1,这样程序就不会保存调试寄存器,也就无法调试,这是一种反调试的手段。
  3. 最后,程序会跳转到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有何不同吧。

Code
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

Author: cataLoc
Link: http://cata1oc.github.io/2020/03/26/API%E5%87%BD%E6%95%B0%E7%9A%84%E8%B0%83%E7%94%A8%E8%BF%87%E7%A8%8B%EF%BC%88%E4%BF%9D%E5%AD%98%E7%8E%B0%E5%9C%BA%EF%BC%89/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶