avatar

Catalog
API函数的调用过程(系统服务表)

前面的学习过程中,我们了解到程序进入0环后,有一个保存现场的过程,会将3环的各种寄存器都保存到一个叫做_Trap_Frame的结构体中。在3环部分,程序将一个编号存到了eax中,这个编号叫做系统服务号,此外,在保存现场的过程中,程序还让edx指向了3环第一个参数的地址。回忆起这两步,接下来,就可以继续探究执行内核函数的过程了。

系统服务表

在分析代码前,我们先来学习一个结构,系统服务表(System Service Table)

在Windows XP系统下,系统服务表有两张,这两张表存着内核文件的导出函数(不包括内核文件的所有函数,主要是3环函数常用的内核函数)。第一张表导出的内核函数主要来源于ntoskrl.exe,实现大部分3环函数基本功能;第二张表导出的内核函数主要来源于win32k.sys,主要实现图形界面相关功能(例如GDI32.dll的底层实现)。

系统服务表结构

根据示例图,我们先简单认识一下系统服务表,先从结构看起:

  • ServiceTable:指向一个函数地址表,通过系统服务号可以在函数地址表中找到指定的内核函数。
  • Count:指当前系统服务表被调用的次数
  • ServiceLimit:函数地址表的大小,即系统服务函数的个数
  • ArgmentTable:系统服务函数参数的大小,以字节为单位,每个成员大小为1个字节。

系统服务表位置

这个系统服务表位于KTHREAD结构的0xE0偏移处。这样,在进入0环后我们可以通过fs:[0]找到KPCR结构,然后在KPCR->0x124找到当前线程的KTHREAD结构,再根据KTHREAD->0xE0就可以找到当前线程所拥有的系统服务表。

Code
1
fs:[0] -> KPCR -> KPCR+0x124 -> KTHREAD -> KTHREAD+0xE0 -> 系统服务表

系统服务号

系统服务号用来定位所要寻找的系统服务表的函数。

系统服务号只有低13位是有用的

  • 下标12:判断去查服务表,0去查第一张表;1去查第二张表
  • 下标0~11:函数地址表的索引

SharedCode分析

在前面保存现场的代码分析中,由于进入0环的方式不同,中断进0环(int 2E)和快速调用(sysenter)保存现场的方式也不一样,但是当这两种方式,将寄存器保存到_Trap_Frame结构中以后(保存现场),便会从同一个地放(KiFastCallEntry+0x8D)开始执行,我们把这一部分共同的代码称作SharedCode(引用Joney的文中的称呼),接下来,我们简要分析一下SharedCode到底做了什么事。

这里还是将代码放在一起分析,更有连贯性,具体细节可以到Windbg中动手实现

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
50
51
52
53
54
55
804df781 8bf8            mov     edi,eax			//eax保存的是系统服务号(参考3环部分),edi获
804df781 //取服务号
804df783 c1ef08 shr edi,8 //将edi右移8位,系统服务号只用到13位,这样还
804df783 //剩下5位
804df786 83e730 and edi,30h //将余下的5位与0x30(0011 0000)进行与
804df786 //算,得到的结果只有可能是0x10或者0,这里非常巧
804df786 //妙,因为0x10刚好是16个字节
804df789 8bcf mov ecx,edi //将与后的结果,赋值给ecx
804df78b 03bee0000000 add edi,dword ptr [esi+0E0h] //esi指向KTHREAD,[esi+0xE]就算获取到第一个系
804df78b //统服务表的首地址,然后加上edi。这里如果edi的
804df78b //值是0x10,运算后,edi就会指向第二个系统服务
804df78b //表的首地址,所以上一步的与运算很巧妙
804df791 8bd8 mov ebx,eax //ebx获取服务号
804df793 25ff0f0000 and eax,0FFFh //对服务号进行与运算,因为上面的步骤已经确定了
804df793 //从哪一个系统服务表里找,这里将下标12的位置零
804df793 //,获取在函数地址表里的索引
804df798 3b4708 cmp eax,dword ptr [edi+8] //[edi+8]指向ServiceLimit,这里与系统服务号作
804df798 //比较,防止系统符服务号越界
804df79b 0f8341fdffff jae nt!KiBBTUnexpectedRange (804df4e2) //若越界则跳转异常处理
804df7a1 83f910 cmp ecx,10h //这里判断系统服务号属于哪个表(虽然前面的步骤判
804df7a1 //断过一次了,前面是为了判断服务号是否越界,这
804df7a1 //里主要是找到合适的处理函数)
804df7a4 751a jne nt!KiFastCallEntry+0xcc (804df7c0) //若不等,就跳转到处理查找第一个系统服
804df7a4 //务表的例程
804df7a6 8b0d18f0dfff mov ecx,dword ptr ds:[0FFDFF018h] //若要去查找第二个系统服务表,从这里走,不作详细
804df7a6 //分析
804df7ac 33db xor ebx,ebx
804df7ae 0b99700f0000 or ebx,dword ptr [ecx+0F70h]
804df7b4 740a je nt!KiFastCallEntry+0xcc (804df7c0)
804df7b6 52 push edx
804df7b7 50 push eax
804df7b8 ff1564b25580 call dword ptr [nt!KeGdiFlushUserBatch (8055b264)]
804df7be 58 pop eax
804df7bf 5a pop edx
804df7c0 ff0538f6dfff inc dword ptr ds:[0FFDFF638h] //如果查第一个表,会跳到这里来 [0xffdff638]
804df7c0 //KPRCB结构的0x518偏移处,存的是KeSystemCalls
804df7c0 //,这里自增1(具体用处到APC那块会讲到)
804df7c6 8bf2 mov esi,edx //edx存着3环第一个参数的地址,赋给esi
804df7c8 8b5f0c mov ebx,dword ptr [edi+0Ch] //ebx获取函数参数表地址
804df7cb 33c9 xor ecx,ecx //清空ecx,之前用来判断寻找哪个系统服务表
804df7cd 8a0c18 mov cl,byte ptr [eax+ebx] //cl获取函数参数个数
804df7d0 8b3f mov edi,dword ptr [edi] //另edi直接指向函数地址表首地址
804df7d2 8b1c87 mov ebx,dword ptr [edi+eax*4] //ebx获取到0环实现的内核函数
804df7d5 2be1 sub esp,ecx //提升堆栈,ecx里存的是参数个数的总字节
804df7d7 c1e902 shr ecx,2 //相当于运算ecx/4,方便rep movsd,因为rep
804df7d7 //movsd是4字节运算
804df7da 8bfc mov edi,esp //rep movsd指令用,Copy的目的地
804df7dc 3b35d40b5680 cmp esi,dword ptr [nt!MmUserProbeAddress (80560bd4)]//检测三环参数地址范围是否越界
804df7e2 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (804df990) //若越界,进行异常处理
804df7e8 f3a5 rep movs dword ptr es:[edi],dword ptr [esi] //将参数复制到堆栈
804df7ea ffd3 call ebx //调用0环函数!
804df7ec 8be5 mov esp,ebp
804df7ee 8b0d24f1dfff mov ecx,dword ptr ds:[0FFDFF124h]
804df7f4 8b553c mov edx,dword ptr [ebp+3Ch]
804df7f7 899134010000 mov dword ptr [ecx+134h],edx

至此,这块KiFastCallEntry和KiSystemService最终都会执行的一段被我们称作SharedCode的代码段,就分析完了,

SSDT

前文提到了,我们可以通过fs找到KPCR,在通过KPCR找到KTHREAD,然后在KTHREAD+0xE0处找到系统服务表,这里再介绍另一种找到系统服务表的办法,通过SSDT。

SSDT&SSDT Shadow

SSDT(System Services Descriptor Table)系统服务描述符表,在这个结构中包含4个成员,每个成员都是一个系统服务表的结构体,可以在Windbg中通过dd KeServiceDescriptorTable指令进行查看(在程序中可以直接声明全局变量KeServiceDescriptorTable,从而找到找到系统服务表。):

我们可以看到第一个成员的ServiceTable,Count,ServiceLimit,ArgmentTable字段,Windows Xp只使用了2张表,所以第三个和第四个成员的位置是空的,此外,由于SSDT第二个成员是未导出的,所以第二个成员的位置也是空的。这里介绍一个新的指令,dd KeServiceDescriptorTableShadow,通过全局变量KeServiceDescriptorTableShadow可以查看两张完整的系统服务表。

但是,全局变量KeServiceDescriptorTableShadow也是未导出的,在实际写程序时,不能通过直接访问win32k.sys导出的第二张系统服务表的函数地址,因为里面的函数地址都是无效的。原因是,win32k.sys导出的第二张系统服务表只有在当前进程访问GDI相关的API时,里面的函数地址表才会挂载到物理页上。如果进程没有用到GDI相关的API,那么第二张系统服务表里面的函数地址表就不会挂载到物理内存,那么里面的函数也无效。

内核函数查找

有了SSDT表,我们查找3环API对应的内核函数就很简单了,拿之前分析过的3环API函数ReadProcessMemory举例,在进入0环之前,给eax赋值了一个系统服务号0xba,那我们就用这个ba来查看这个这个函数在内核的实现。

通过这张图,可以很清晰的看出来,ReadProcessMemory所实现的功能,在底层是由一个叫做NtReadVirtualMemory完成的。

总结

API函数的调用过程,从3环进入0环,再到找到对应的内核函数,这部分到这就差不多了,当然,真正的调用过程并没有到此结束,因为调用完0环的函数,总得返回3环呀!只是这部分需要用到APC的知识点,因此这里还不能完整实现。此外,在API函数调用这块,还有个小实验,在SSDT表中追加一个函数地址(NtReadVirtualMemory),自己编写API的3环部分调用这个新增的函数(注意:使用2-9-9-12分页,10-10-12会蓝屏),就留到后面补上了

参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=41

参考文章:

  1. https://www.cnblogs.com/joneyyana/p/12585469.html
  2. https://blog.csdn.net/qq_38474570/article/details/103674271
  3. https://blog.csdn.net/qq_41988448/article/details/102994374
Author: cataLoc
Link: http://cata1oc.github.io/2020/03/27/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%E7%B3%BB%E7%BB%9F%E6%9C%8D%E5%8A%A1%E8%A1%A8%EF%BC%89/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶