
我们如果想要解决上面的两个问题,就需要知道什么是系统服务表,大家来看一下下图。

首先先给大家介绍一下系统服务表的表的结构,系统服务表有四个成员,第一个成员里存储的是一个指针,这个指针指向了一张表,这张表里存储的全是函数的地址,这是第一个成员;第二个成员指的是当前的系统服务表被调用了多少次;第三个成员指的是当前的系统服务表里面有多少个函数;第四个成员也是一个指针,这个指针指向了另外一个表,这个表里面存储的是函数参数的个数。参数的个数是以字节为单位的,比如说你传了两个参数,每个参数是四字节,那么这个表里面存储的值就是8。还有一个地方需要注意,ServiceTable也就是第一个成员指向的的函数地址表里面的每一个成员有4个字节,因为它存储的是函数的地址,所以它需要有四个字节,但是这个ArgmentTable指向的参数表里面由于存储的是每一个函数参数的个数,它没有必要用哪个四个字节了,所以这个表里面存储的每一个成员只有一个字节。系统服务表一共只有两张,第一张表也就是上面绿色的部分,这个函数地址表的成员是从哪里来的呢?刚才给大家解释过了,这个函数地址表里面存储的全是函数的地址,这些函数是哪里来的呢?这些函数全是内核文件导出的函数,我们知道内核文件就是Ntoskrl.exe里面导出的函数,但是不要以为这里面是这个内核导出函数的所有,它不是所有、它只是其中的一部分,也就是说我们在三环中常用的那些内核函数已经全部在这张表里了。这个函数参数表与函数地址表之间的顺序是一致的,比如在函数地址表中的第一个函数它对应的参数个数就是函数参数表里面的第一个成员。这就是系统服务表的结构,在xp系统中,系统服务表有两张,分别是绿色部分和黄色部分,下方的黄色部分的系统服务表的结构和上面的绿色部分是完全一样的,第一个成员也是一个指针,指向了一个函数的地址表,每一个成员宽四个字节;最后一个成员指向的是函数参数表也就是函数参数的个数,它的单位也是字节。那么这两张表的区别在哪呢?第二个系统服务表里面的所有的函数都是另外一个模块叫做Win32k.sys,也就是说这里面所有的函数都是Win32k.sys这个模块里面的导出函数,那么这个ntoskrl.exe内核文件里面包含的都是常用的一些系统的服务,但是windows把一些与图形显示或者用户界面相关的一些系统服务全部给它放到了另一个模块里面,这个模块是内核模块,大家要注意这个内核模块就是win32k.sys模块,比如说我们看到一个图形界面程序的时候,比如一个窗口,我们需要使用到user32dll,但user32dll的窗口是怎么绘制出来的呢?user32的底层都是使用一个叫GDI32的dll来实现绘制界面的效果,但GDI32这个dll本身也仅仅是一些函数的接口,它并没有实现。它的实现体现在哪里呢?全在win32k.sys这个系统内核中实现的,所以我们说win32k.sys是跟图形界面相关的系统提供的一些服务功能全在这个win32k.sys模块里,我们对这个系统服务表有一个初步的印象以后呢,我们再往下看,大家一定要记住这两个表的结构,它俩的结构的两张系统服务表是完全一样的,结构是完全一样的,一个是ntoskrl导出的函数,另一个是win32k导出的函数,那么我们知道了这个系统服务表存在哪,那么我们程序执行的时候去哪里找这个系统服务表呢?

在上一节课给大家介绍了一个结构体,就是每一个线程都有一个在零环的结构体来描述当前线程的信息,我们要找的这个系统服务表在这个线程结构体也就是_KTHREAD结构体里面的偏移处0xE0的偏移处存储的就是系统服务表里面的值, 这样的话在程序执行的时候,只要CPU能找到当前的线程,就能找到系统服务表在哪里了,这是我们的程序执行的时候到哪里去找这个系统服务表。

然后大家再去回想一下,我们通过三环调用零环函数的时候是传递了一个所谓的系统服务号的,在三环的时候放到了eax寄存器里了,这个系统符号将决定我们要调用的函数到底怎么找、在哪里,参数怎么找、在哪里,系统符号都是直接相关的,这个系统符号虽然是一个四字节的数只有32位,但是它真正使用的只有低13位,前面的高18位是没有使用的、是可以被忽略的。这个系统服务号在使用的时候分成两个部分,一是低12位,二是下标12位。下标为12的这个位置也就是第13位的值如果为0,那么它要找的函数就在第一张表也就是绿色部分,如果为1那么它要找的函数就在第二张表也就是黄色部分,这就是下标为12的位置的作用。接着往下说,假如我们的下标为12的位置的值是0,那么我们就要从这张绿色的表里面去找,那么此时它的低12位就要发挥作用了,具体来说就是低12位就是对应的函数表的索引,我们是根据低12来找我们想要的函数在哪里。比如说我们的函数是第二个,那么怎么找参数呢?函数式第二个,那么对应的参数个数就是函数参数表里面的第二项。所以我们简单总结一下,这个所谓的系统服务号只有低13位在使用,第13位如果是0那么就查第一个表,如果是1就查第二个表;后面的低12位是函数地址和函数参数表的索引。
OK,我们给大家介绍完了如何通过系统符号来找我们要执行的函数,接下来我们来看一下反汇编代码里面是怎么找的。

这个loc_40003F就是海哥这两天带着大家分析的这个函数,大家如果做过之前留的那个练习,那么可能已经发现一个这个问题了,那就是无论是从kifastcallentryservice进来的,还是从kifastcallentry进来的,它俩只有开头的地方不一样,也就是初始化那个结构体是不一样的, 初始化完了以后,他俩要执行的地方全到下面的位置进来了:

如上图所示就是它俩必须都要走的地方,我们就从这里开始分析如何找到它的函数的,大家注意观察这行eax,我们知道这个eax存储的是就是系统的服务号,也就是三环传进来的那个数,那么在这,我们把它的值放到edi里了。紧接着下面的两行,这两行很关键,这个右移8位紧接着判断右移后的值跟30h比较,我们之前已经和大家说过了——这个系统符号它使用的只有低13位,也就是截止到下标为12的位置。其中下标第12的位置是用来判断我们到底要查哪张表,这个右移8位后再与,其结果只能有两个,如果与后edi的值为0,那么就查第一张表,如果与后edi的值为1那么就查第二张表,这里说的0和10都是16进制的,大家看这个算法其实是很巧妙的,假设我传进来的符号经过这两步运算后,edi中的值是0的话,就证明了我的下标第12位那个位是0,如果这个程序运行完以后,edi中存的结果是10的话,那么就意味着下标为12的那个位置是1,那接下来该怎么做?如果你的edi与运算以后得到的结果是0的话,大家看下面这行:
add edi,[esi+0E0h]
我们刚才给大家介绍过esi了,我们描述线程的那个结构体叫KTHREAD,它的E0这个位置取出来的就是右侧写的注释即ServiceTable,取出来的这个值就是所谓的系统服务表, 那么系统服务表大家观察,它直接加上了edi的值,这块它设计的很巧妙,如果你的第12位为0,算出来的时候edi的值就是0,那0加上这个位置仍然是0,那就意味着我要读取的是谁呢?如果你是0,那么我们要找的这个表就是第一张表。

我们再假设,如果计算完的结果,edi是10,通过这两步运算,计算完的结果是10的话就意味着下标为12的那个位置里面存储的是1,如果那个位置是1的话,edi存的值就是10,如果是10,那么加上我们取出来的系统服务表的这个基址,指向的刚好是第二张表,为什么呢?因为每一个表的大小刚好是0x10转成十进制也就是16个字节,由于每一个成员占四个字节,所以4乘以4个成员就是16转换为十六进制就刚好是0x10h,所以它判断的时候是通过这种方式去判断的,如果你第十二位为1的话,那edi的值不就是10吗,10再加上基址取的就是第二张表了,这块呢可以说算的是很巧妙的。好了,我们继续观察,不管你是0还是10,都可以正确地找到我要查的那个表,那么计算后,这个edi就指向第一张表或者指向第二张表,我们已经正确找到了,所以下标为12的那个位置实际上就相当于一个表的索引——查第一张表还是第二张表。紧接着下一行代码:
mov ebx,eax
把当前的eax也就是三环传进来的那个系统的符号存到ebx里备份一份,之后将eax和0FFFh做了一个与的运算,我们说过了它只有后12位是用来查函数表的索引,因为它的下标为12的哪个位置是用来判断查哪张表,剩下的12位是用来查函数在表里的位置,所以它先做了一个与的运算,与完以后,只保留它的后12位也就是低12位,之后:
cmp eax,[edi+8]
edi里面存储的东西我们刚才讲过了,是我们存储的系统服务表,这个系统服务表的结构我们刚才给大家画过图了,有四个成员,其中第一个成员指向了一个函数的地址表,第四个成员指向了函数的参数表;第二个成员是表里的服务被访问了多少次,第三个成员是你这个表里面提供了多少个函数。这个edi现在指向的是这个表开始的位置,那么加0是它加4,这个加8呢就是numberofservice也就是这个函数里面到底有多少个表,这里先做一个比较,如果你传进来的这个第12位的值的数字如果比这个数量要多的话,就会代码越界了,所以这行是判断你要找的函数有没有超过它所有函数的范围,那么假设我们没有越界,它的代码就会继续往下走,这个时候呢,它会拿ecx跟10h进行比较,为什么它跟10h做比较呢?ecx是什么呢?ecx是存储edi的值,是哪个edi呢?是edi刚和30h进行与操作后的edi的值。
shr edi,8
and edi,30h
这两步的运算就是为了判断下标为12的哪个位置是1还是0,因为只有两种情况,如果你下标为12的那个位置是0的话,与后的结果就是0反之则是10,所以在这里就是判断是不是10h,如果是的话就意味着要查第二张系统服务表,这么一来就会调用一个函数,这个函数大家感兴趣的话可以自己查一下,这里就不细讲了。

总之,如果ecx为0也就是你想查第一个系统服务表的话,那它就直接跳转了,也就是说,这个KeCdiFlushUserBatch函数只有当你调用第二个系统服务表的时候,它才会执行,这块呢大家可以查资料拓展一下,因为第二张系统服务表里面存储的是图形相关的函数,这些函数不是一次性加载的、是动态加载的,也就是说只有用的时候才会加载,那这些KeCdiFlushUserBatch正好是和函数有关联的,这也就是为什么我们查第一张表的时候它不用调这个函数,但是我们查第二张表的时候就需要调用这个函数。

好了,代码现在走到这里了,我们观察一下,edx是什么?回想一下edx,当我们在三环进入零环的时候一共往两个寄存器里面存了值,一个寄存器是eax,里面存的是系统的符号;第二个寄存器就是edx,我们存储的是三环的参数的指针。如果你忘了就看之前的视频。那么这里它把三环的参数取出来放到哪了呢?放到esi里,接着我们观察,这个edi指向的是我们要查找的系统服务表的起始位置,至于是第一张还是第二张,取决于你的符号的下标为12的那个位置到底是0还是1,0第一张,1第二张。这个edi+0Ch找的是谁呢?我们刚才给大家看过那个结构:

+0Ch这个位置存储的是一个指针,这个指针指向了一个表,这个表里面是一个一个数字组成的,每个数字代表一个函数有多少个参数,是以字节为单位的,那么回到刚才的代码。
mov ebx,[edi+0Ch]
这行代码的目的就是把函数参数表的这个地址放到ebx中,那么也就是说ebx现在指向的就是那张参数表的起始位置,参数表起始的位置再加上eax,其中eax存储的是一个索引,这个索引指的是我要调用的函数的编号是多少,也就是说,这个参数表的基址加上这个索引得到的这个值就是你要调用的这个函数它有多少个参数,我把这个个数存到cl里面去了。那么这一行:
mov cl,[eax+ebx]
执行完了这行代码以后,我们就已经知道了当前要调用的这个函数它有多少个参数,然后我们再看后面的代码,我们取出edi第一个成员的值直接放到edi里,edi指向的是下图这张表,其第一个成员就是ServiceTableBase,即我们刚才说的指向函数表的地址,是下图2的地址。


我们把这个值取出来以后,当前edi里面存储的就是函数地址表,那么大家注意观察下一行代码,eax存储的是函数的索引就是低12位的值,前面mov cl,eax+ebx的时候没有乘以4是因为参数里面单位是以字节为单位的,这里乘了4是因为函数地址表里面不一样,函数地址表里面每一个成员有4个字节,比如说我传进来的这个参数如果是2的话那么当然要乘以4了,这就是为什么它要乘以4,一个地址的宽度是4个字节,那么这行代码:
mov ebx,[edi+eax*4]
执行完了以后,ebx中存储的就是我们要调用的内核函数的地址, 这就是ebx。那我们现在知道了cl中存储的是函数有几个,ebx中存储的是你的要调用的零环的函数在哪里,我们继续读代码。下面有一个:
sub esp,ecx
这么一个提升堆栈的操作,把esp的值减少了多少个呢?你有多少个字节的参数,就减少多少个。因为现在堆栈是处于零环的,而参数现在是在三环的,它提升一个堆栈其目的是为了把零环的参数复制进来,为的是把三环堆栈中的参数复制到零环的堆栈里面来,所以这个提升的个数不是随便提升的。
shr ecx,2
右移一位相当于除以2,右移两位相当于除以4,这里为什么要右移呢?原因很简单,大家看一下后面的代码就知道了:

rep这个指令是重复执行的意思,它执行多少次取决于ecx里面的值是多少,如果ecx是3就重复3次如果是2就重复2次,但是它复制的时候是一次复制4个字节,而刚才我们反复强调那个参数表里面的参数数量是以字节为单位的,但是现在一次复制4个字节,所以这里要除以4,那这样的话,假设你的参数数量写的是4,这个执行一次就够了,如果是8的话,那么除完了以后,8除以4就是2,所以执行两次就够了,所以这里结合这两行代码一起看就可以看懂了。在它真正要调用call ebx这个函数之前,我们知道ebx里面存储的是你当前要执行的那个函数在哪里,在它真正要执行ebx函数调用的时候,它还做了一个判断就是这行代码:
cmp esi,ds:MmUserProbeAddress
判断的是esi,esi在哪里?esi里面存储的是三环函数的指针,这一点在mov esi,edx中已有描述。我们edx最开始没有做变化的时候就把这个值存到esi里面了。就是你三环的函数存在哪里,这里做了一个判断,就是esi和这个全局变量进行的判断即你用户程序能访问的地址的最大范围,它在这里面就是判断你三环的参数其地址范围有没有越界、有没有超过用户能够访问的最大范围。如果你超过了那么就跳转到错误处理的模块了,如果没有超过的话就开始执行rep那一块的代码了。 所以rep那行代码的作用就是把三环的参数复制到零环的堆栈里,而call那行代码就是真正调用内核函数。如果你看懂了刚才讲的那些图,你再配合着这些代码,你就会觉得非常清晰了,虽然我们讲的时候是一步一步领着大家分析的,但是如果大家想了解里面的细节,一定要自己做一做实验也就是重新再分析一下这些函数,因此课后练习就是重新分析一下课堂上讲解的那些功能。