前言
我们现在已经知道API怎么从三环进入零环,从三环进零环需要带两个寄存器,分别是eax和edx。eax保存的是系统的服务号,edx保存的是三环的esp,通过esp可以找到三环的参数。
这次要解决的问题是如何通过eax找到零环的函数,零环的函数是怎么被调用的,并且零环的函数执行的时候是怎么使用三环的参数的。
SystemServiceTable 系统服务表
在操作系统里,有一张非常重要的表,SystemServiceTable 系统服务表 。系统服务表是操作系统内核的一张表,结构如图:
总共有四个成员:
- ServiceTable 这个成员是个地址,通过这个地址可以找到一个函数地址表。从三环进零环的eax的服务号就是函数地址表的索引
- count 当前的系统服务表被调用了多少次
- ServiceLimit:保存的是函数地址表的大小,也就是服务函数的个数
- ArgmentTable:函数参数表,里面保存的是函数地址表对应的参数个数,以字节为单位。例如函数地址表下标为1的函数需要三个参数,那么函数参数表下标为1的内容就是12
- 系统服务表有两份,其中一份函数来自于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 ; 调用函数
最后将三环的参数赋值到零环,开始调用真正的内核函数