系统调用003 系统服务表

前言

我们现在已经知道API怎么从三环进入零环,从三环进零环需要带两个寄存器,分别是eax和edx。eax保存的是系统的服务号,edx保存的是三环的esp,通过esp可以找到三环的参数。

这次要解决的问题是如何通过eax找到零环的函数,零环的函数是怎么被调用的,并且零环的函数执行的时候是怎么使用三环的参数的。

SystemServiceTable 系统服务表

在操作系统里,有一张非常重要的表,SystemServiceTable 系统服务表 。系统服务表是操作系统内核的一张表,结构如图:

在这里插入图片描述

总共有四个成员:

  1. ServiceTable 这个成员是个地址,通过这个地址可以找到一个函数地址表。从三环进零环的eax的服务号就是函数地址表的索引
  2. count 当前的系统服务表被调用了多少次
  3. ServiceLimit:保存的是函数地址表的大小,也就是服务函数的个数
  4. ArgmentTable:函数参数表,里面保存的是函数地址表对应的参数个数,以字节为单位。例如函数地址表下标为1的函数需要三个参数,那么函数参数表下标为1的内容就是12
  5. 系统服务表有两份,其中一份函数来自于Ntoskrl.exe内核模块的导出函数,里面保存常用的系统服务;另外一份来自Win32k.sys的导出函数,里面是与图形以及用户界面相关的系统服务。向三环提供的内核函数全在这两张表里。

系统服务表在哪

在这里插入图片描述

系统服务表位于_KTHREAD结构体0xE0的位置。

判断调用的函数在哪个表

问题在于系统服务表有两张,那么我们怎么判断需要调用的函数在哪个表呢

在这里插入图片描述

  • 要找两张表取决于eax系统服务号。这个系统服务号总共有32位,但是真正只使用了13位。
  • 系统服务号在使用的时候分为两部分,低12位表示的是函数地址表的索引,下标为12的位置的值,决定了使用哪张表
  • 如果第12位为0,则找第一张表,如果第12位为1,则找第二张表。

API函数的调用过程

接下来就来分析零环代码是怎么通过服务号找到零环函数,并且零环的函数是如何使用在三环的参数的。

在这里插入图片描述

用IDA打开ntkrnlpa.exe,找到KiFastCallEntry函数。KiFastCallEntry和KiSystemService前面的代码都是用于保存现场,大致相同。

我们从保存现场之后开始分析。

.text:0046579D                 mov     edi, eax        ; 取出三环传进来的系统调用号

首先将三环传递过来的系统调用号保存到edi里

.text:0046579F                 shr     edi, 8          ; 系统调用号右移8位
.text:004657A2                 and     edi, 30h        ; 判断调用号的第12是0还是1
.text:004657A5                 mov     ecx, edi        ; ecx存储的值是00或者0x10

然后将系统调用号右移8位,然后再和0x30做与运算。此时edi的结果只能有两种,要么是0x0,要么是0x10。如果是0的话,就说明调用号下标为12的位置是0,如果edi的结果是0x10,那久说明调用号下标为12的位置是1。

.text:004657A7                 add     edi, [esi+0E0h] ; edi指向KTHREAD--->ServiceTable

esi指向的是ETHREAD线程结构体,这个结构体+0xE的位置是ServiceTable系统服务表。用系统服务表直接加上edi的值。如果edi的值是0,加上ServiceTable的基址还是等于原来的值,这个时候就会找第一张系统服务表。

如果edi的值是0x10,再加上ServiceTable的基址指向的刚好是第二张表。因为系统服务表的大小正好是0x10,加上0x10就能找到第二张表。这个地方的算法非常巧妙。

.text:004657AD                 mov     ebx, eax        ; 把三环的系统服务号存到ebx备份
.text:004657AF                 and     eax, 0FFFh      ; 系统调用号 只保留后12位

接着把系统调用号备份到ebx,然后和0FFFh做与运算,保留后12位

.text:004657B4                 cmp     eax, [edi+8]
.text:004657B7                 jnb     _KiBBTUnexpectedRange

这里用eax和edi+0x8的位置做比较,此时的edx指向的是系统服务表,[edi+8]的位置是第三个成员ServiceLimit,是服务函数的个数。如果传入的调用号的低12位大于服务函数的个数,就跳转到_KiBBTUnexpectedRange。

这里是判断要找的函数有没有超过函数地址表的范围。如果没有越界,代码会继续往下走。

.text:004657BD                 cmp     ecx, 10h        ; 判断是否是查第二张系统服务表
.text:004657C0                 jnz     short loc_4657DC ; 如果是查第一张系统服务表则跳转

接着拿ecx和0x10进行比较,ecx的值来自于edi,存储的值是00或者0x10,如果ecx是10的话就说明要查第二张系统服务表。

如果是查第一张系统服务表就会跳转,查第二张则继续往下执行

.text:004657C2                 mov     ecx, ds:0FFDFF018h
.text:004657C8                 xor     ebx, ebx
.text:004657CA
.text:004657CA loc_4657CA:                             ; DATA XREF: _KiTrap0E+113↓o
.text:004657CA                 or      ebx, [ecx+0F70h]
.text:004657D0                 jz      short loc_4657DC
.text:004657D2                 push    edx
.text:004657D3                 push    eax
.text:004657D4                 call    ds:_KeGdiFlushUserBatch
.text:004657DA                 pop     eax
.text:004657DB                 pop     edx

最后会调用_KeGdiFlushUserBatch函数。继续往下分析,假设最后查的是第一张系统服务表

.text:004657DC loc_4657DC:                             ; CODE XREF: _KiFastCallEntry+B0↑j
.text:004657DC                 inc     dword ptr ds:0FFDFF638h ; _KPCRB->0x518 KeSystemCall增加1
.text:004657E2                 mov     esi, edx        ; edx存储的三环的参数指针

这里将edx保存到esi,edx存储的是三环的参数指针

.text:004657E4                 mov     ebx, [edi+0Ch]	;ebx指向参数表起始位置

此时的edi指向系统服务表的起始位置,[edi+0Ch]存储的是ParamTableBase 参数表的基址,此时ebx指向参数表起始位置。

.text:004657E9                 mov     cl, [eax+ebx]   ; eax->函数地址表索引 ebx->参数表起始位置 cl->参数的个数

参数表的基址加上函数地址表的索引,再取内容得到的值就是要调用的函数的参数个数。

.text:004657EC                 mov     edi, [edi]      ; edi指向系统服务表 第一个成员是函数地址表

接下来取出函数地址表放到edi

.text:004657EE                 mov     ebx, [edi+eax*4] ; ebx->零环的函数地址

接下来用函数地址表edi加上索引eax乘以4,这行代码执行完成之后ebx存储的是零环的函数地址

.text:004657F1                 sub     esp, ecx        ; 提升堆栈 提升高度为CL

接着提升堆栈,提升的大小为CL,也就是参数的大小,为了容纳三环的参数

.text:004657F3                 shr     ecx, 2          ; 参数总长度/4=参数的个数
.text:00465804                 rep movsd               ; 开始拷贝参数

然后将ecx右移两位,右移两位相当于除以4,得到的结果是参数的个数。为什么要右移呢?原因很简单,因为后面的rep串操作指令的循环次数取决于ecx的值,而movsd一次复制4个字节。

.text:004657F8                 cmp     esi, ds:_MmUserProbeAddress ; 判断三环的参数的地址范围是否越界
.text:004657FE                 jnb     loc_4659AC      ; 越界跳转到错误处理模块

此时esi指向的是三环的函数指针,这里跟一个全局变量进行比较,这个全局变量是用户程序能访问地址的最大范围。

这里是为了判断三环的参数的地址范围是否越界,如果越界则跳转到错误处理的模块

.text:00465804                 rep movsd               ; 开始拷贝参数
.text:00465806                 call    ebx             ; 调用函数

最后将三环的参数赋值到零环,开始调用真正的内核函数

发布了99 篇原创文章 · 获赞 89 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_38474570/article/details/103654842