
我们这节课主要是钻研作业的第一个问题,以kifastcallentry为例,要认识这些东西之前,大家要先认识几个结构体,这几个结构体如果不了解的话代码是看不懂的,第一个结构体叫Trap_Fram_,大家如果想看这个结构体的话可以使用如下的指令去查看,注意一定要加一个k:

这个结构体是非常重要的,无论是通过中断门进入零环,还是通过sysentry快速调用进入零环,这个三环的所有的寄存器都会存储到这个结构体里面,这是这个结构体的作用,这个结构体是存储在零环的结构体,是由Windows操作系统来维护的。
第二个结构体叫线程相关的结构体,线程的一些信息在零环也是有对应的结构体的,这个结构体呢叫_ETHREAD结构体,依旧是想查看这个结构体的话就在windebug里面通过dt _ETHREAD指令去查看:

这个ETHREAD结构体里面有一个非常重要的结构体叫KTHREAD,这个子结构体里面存储的都是相对比较重要的信息,这个是ETHREAD结构体的相关信息。
线程有一个具体的结构体来描述它的状态,CPU本身也有一个结构体来描述它的状态,那这个结构体呢叫_KPCR,KPCR的中文翻译叫做CPU控制区。

每一个CPU都会有一个KPCR这样的结构体来描述当前CPU所处的状态,如果你有两核CPU就有两个KPCR,如果有三核就是三个KPCR。那么如果在windebug里面如果我想查看我现在有几个核,可以通过这个指令来查看:

比如我海哥的xp系统只有一个核,那个海哥这里执行这个指令就是1,然后呢,还可以通过另外一个指令就是上图中后面的kiprocessorblock L2来看当前的kpcr结构体都存在哪里了,比如这里有两个L2,我们列出了两个KPCR的结构体,但是呢,由于我的CPU里面只有一个核,所以可以看到打印出来的线性地址第一个是有值的,第二个是00000000纯空的,如果我有两个核的话,那么你会看到第二个位置也有值。这就是这三个结构体,我们最后再总结一下,_Trap_Frame存储三环的寄存器,由Windows操作系统来维护,其位置处于零环;_ETHREAD是存储当前线程,每一个线程都会有这样的一个结构体来描述其信息;_KPCR是用来描述CPU的各种状态。有了这三个结构体,我们就可以看代码了:

大家可以来看一下代码,第一行写了个push 0,这个push 0对应的结构体是什么位置呢?在这里面需要和大家强调一下,


大家注意观察一下,这个结构体后面有很多成员,其中最后四个成员在我们的保护模式下面是没有使用的,也就是我们不需要考虑最后四个成员,这四个成员是当CPU处于虚拟8086模式下,它会存储这几个值,保护模式下它不存。我们再回忆一下,当初学习保护模式的时候,我们学习过中断门,当权限发生切换时,中断门会向零环的堆栈里面压入五个值,分别是三环的SS、ESP、EFLAG、CS、EIP,也就是说,如果我们通过中断门进入零环的话,当这个kisystemservice这个函数一开始执行的时候,这个结构体里面就已经有这五个值了,那么如果我们通过快速调用也就是sysenter进入的时候,是没有这五个值的,从这个角度讲,我们也能够理解为什么操作系统在设计的时候要有两个不同的入口进入零环,现在大家应该明白为什么分成两个函数,一个是kisystemservice一个是kifastcallentry,原因就是在于进来的时候压的值不一样,如果都走一条线会出问题。那么现在我们知道了,只要我们通过中断门进来也就是我们的这个kisystemservice执行的时候,那五个值就已经存在了,然后它第一行写了一个push 0,这个push 0存在哪里了呢?存在了这个结构体里面偏移为64的位置也就是ErrCode,我们在学习的时候知道权限切换压入五个值,实际上有些情况中断门会压入六个值的,大家如果想了解细节的话就看一下白皮书第三卷的中断与异常其中的errorcode,但是当我们通过int 2e进入零环的时候并没有这个errorcode,操作系统为了对齐,自己补了一个0,这就是我们看到的为什么kisystemservice的第一行先写了一个push 0,这个push 0就写入到errorcode这个位置。


紧接着kisystemservice连续把三环几个寄存器压栈了,分别是ebp、ebx、esi、edi和fs,我们观察一下对应的值(右上图),继续读代码,它向ebx存了一个值叫30,它怎么用的这个30呢?大家观察最后那一行代码
mov fs,bx
这行代码的含义就是把30这个段选择子,根据这个段选择子查询GDT表,找到相对应的段描述符,把段描述符加载到fs段寄存器。我们分析一下30加载的到底是谁呢?把30拆分一下,前面八位0我们就省略掉了,因为我们知道段选择子一共是16位的,我们只列出了后面的八位也就是0011 0000,从右往左数最低的两位是RPL,第三位指的是要查哪张表,0查GDT、1查LDT,根据位的值我们可以判断出这里查的是GDT;接着从右往左数0110它的索引加起来是6;我们可以在Windbg中找到GDT这张表,然后看一下这张表里面索引为6的描述符具体是什么。这里已经把索引为6的段描述符的数据列出来了,ffc093df`f0000001,大家再回想一下段描述符的结构,ff、df和f000合在一起拼起来的是一个32位的地址,这个地址就是一个base,我们把这几个base拼完以后,得到的地址是FFDFF000,这个地址不是一个普通的地址,其刚好指向的就是_KPCR这个结构,所以现在大家能理解了,为什么我们的fs在三环的时候和在零环的时候其值会发生切换,因为代表的值不一样,fs在三环的时候,它指向的是一个叫TEB的结构,是一个线程的状态、一个三环能够看到的一个线程的状态。一旦进入零环,fs指向的不再是线程,不再是TEB这样的一个结构体了,而是另一个结构体即KPCR,那么它怎么指向KPCR呢?就是通过段描述符的Base指向这个KPCR,所以到目前为止我们就了解了FS进入零环它到底变成了什么。我们继续观察代码


这行代码的值是0ffdff000h,这个值我们很熟悉,这个值就是fs指向的那个基址,这行代码先执行了一个push,然后再把老的exceptionlist存了起来,存到哪里了呢?存到了0x4c的位置,存到这里了以后紧接着做了一个什么事情呢?下一行代码把exceptionlist变成了-1,见名知意这个叫做异常链表,那么什么叫异常链表呢?等我们将异常专题的时候再详细介绍,现在先不用管,总之这里我们把这个异常链表的值变为了-1,这个很好理解,因为原来的值现在已经没办法用了所以我们要换成一个新的值,原来的值是一个三环的exceptionlist,现在我们进入零环了,所以没有必要保留原来的异常链表了。我们把现在的异常链表清零了变成了-1,接着往下看又执行了这个
mov esi,ds:0FFDFF124h
这行代码的偏移是124,我们刚才已经介绍过了,这个指向的是KPCR,那我们现在就需要找124这个偏移是个什么东西,我们先找到KPCR这个结构体,这个KPCR结构体大家观察一下,这个120对应的是一个子结构体:

那么很明显,124就是这个CurrentThread,所以以后如果分析内核代码的话,经常能看到124这种东西,因为124指的是CurrentThread也就是当前CPU正在跑的线程,它的线程信息是什么样的。我们刚才给大家讲解过了,每一个线程都会有一个自己对应的结构体来描述它的状态,那么当前的CPU跑的是哪个线程呢?就是通过这个CurrentThread来找到当前线程的信息,所以我们现在应该知道现在esi存储的就是一个Kthread结构体,结构体里面存储的是当前CPU正在跑的线程,接着往下看:

接下来的代码走到了ESI+140,这时候我们就需要看ETHREAD结构体成员了,因为ETHREAD里面的第一个成员就是KTHREAD,KTHREAD的140是什么呢?我们看到了其存储的是一个PreviousMode也就是先前模式。


这个先前模式我们一会再解释,大家先不用管,现在只需要知道这一行代码把这个先前模式也保存到我们最开始说的那个结构体里了,也就是那个绿色的结构体把原来的异常链表压入了。然后紧接着呢,又push了一个值也就是说把原来的这个先前模式也存到了这个位置。我们继续观察:

接下来是提升了堆栈,提升了48h,提升了多少呢?我们观察一下结构体:

相当于提升了剩下的这些部分,如果提升到0x000的话正好是需要0x48h字节,所以当前的esp指向的是0x48也就是previouspreviousMode,提升了以后,esp将指向这个trapframe的第一个成员,也就是指向了0x0的位置,这就是esp提升0x48h字节的作用。紧接着我们观察这个位置:
mov ebx,[esp+68h+arg_0]
这个位置,我们看看它读的是谁,esp+68h+arg_0,其中arg_0的值是多少?我们将鼠标移动到这个变量位置,看一下鼠标列出来的:

可以看到这个arg_0对应的值是4,那也就是说esp+68再加上4即加6c,把这个值取出来放到ebx里,我们看看esp+6c的值是谁,刚才的分析中我们知道esp现在指向的是0x00,esp+6c就是这个SegCs。

这个SegCs是谁呢?就是三环原来的那个CS的值,把三环原来的CS的值取出来,取出来以后在这里干什么呢?做了一个与运算,把这个值跟1做了一个与的运算,这样一来会得到什么样的结果呢?如果原来的CS最低位是0的话,与完了以后得结果为0,如果你原来的CS低两位是3的话,那么与完了以后的值是1。大家好好理解理解这两行代码,把与完的结果存储到哪里去了呢,存储到esi+140h这个位置了,esi我们知道是当前线程,esi加上140就是当前线程的先前模式。现在可以给大家解释一下什么是先前模式了,先前模式指的是当你调用我这个代码的时候,你原来是几环的,如果原来是一个零环的程序,那么先前模式就是0,如果是三环的程序,那么先前模式就是1,这个1和0有什么具体的含义呢?有些内核代码可以从零环调用,也可以从三环调用,但是从零环调用和从三环调用,其执行的内容是不一样的,操作系统根据什么来区分到底是零环在调用我还是三环在调用我呢?它就可以查看一下你的先前模式就可以了,这就是先前模式具体的含义。如果我们是从三环进来的,那么ebx这个值就是1,我们把1存到了esi+140h这个位置,然后我们再接着观察:
mov ebp,esp
这样一个栈底提升操作,即把esp的值放到ebp里,这样的话使得ebp和esp都指向同一个位置,就是0x00h这个位置,指向其第一个成员。我们继续读代码,这个代码:
mov ebx,[esi+134h]
这个代码读出来esi+134h这个值,我们查一下esi的134h对应的是谁。

是当前线程的信息,可以看到134h是TrapFrame指针,也就是说原来线程里面是存了一个trapFrame这样的一个地址的,现在呢它把这个地址取出来放到了ebp的这个位置,然后把ebp放到ebp+3c的位置,我们来看一看,ebp+3c是什么呢?ebp跟esp都指向0x00这个位置,加3c后就是edx:

实际上这里只是临时存在0x3c这个位置,等它真正要返回三环的时候,它还要把这个值再取回来,大家要记住它临时存放到这里了。存放完之后,它把ebp的值又压到了当前线程134h这个位置,为什么要这么做了?因为trapframe这个结构体已经发生变化了,所以我要把新的trapframe的地址放到当前线程134h的这个位置。

刚才我们的代码虽然看着很复杂,但是实际上也没做什么事情,无非就是填充这个_Trap_Frame结构体,然后把这个结构体新的地址放到当前线程的134h的那个位置,剩下的代码就比较好理解了,继续把三环的ebp+60h放到ebx中,其中这里的ebp+60我们看一下是谁,注意现在的ebp指向的是0x00:

我们可以看到这里ebp+60就是原来的三环的ebp,它把原来的三环的ebp取出来然后放到ebx里,之后把ebx放到ebp+0的位置也就是DbgEbp

后面的一大堆寄存器呢就是调试用的寄存器了,再存一份是操作系统要用的。之后就是把ebp+68h的值取出来了放到edi中,ebp+68h就是把原来的EIP存储到edi里面去:

之后又把原来三环的EIP存到了ebp+4的这个位置:

这个ebp+4的位置在哪里呢?如下图所示,就是这个DbgEip。

之后接着往下读代码,大家回想一下edx是谁,我们通过中断门进入零环的时候,edx里面存储的就是三环参数的指针,三环进入零环的时候存储两个值,一个是edx,还有一个是eax,eax里面存储的是系统的服务号,edx里面存储的是三环参数的指针,它把edx这个值存储到ebp+0Ch这里。

对于ebp+0Ch,我们继续看这张图:

也就是存储到了DbgArgPointer的位置,存完以后接着读代码。
mov dword ptr [ebp+8],00ADB0D00h
把这个操作系统标志存储到了ebp+8的位置,之后接下来的两行代码我们已经解释过了,接着我们往下读代码。

这里有一个test,比较的是esi+2Ch的位置,我们看一下当前线程esi+2c的位置在哪里

可以看到是DebugActive,这个值是什么意思呢?指的是当前的线程是否处于调试状态,如果处于调试状态,那么当前的值就不是-1(也就是0FFh)。代码在这里做了一个比较,如果这个值不是-1就说明处于调试状态,会进行一个跳转,跳到如下位置:


实际上这几行代码就做了几件事情:把调试相关的寄存器DR0~DR7(这部分我们还没有讲)也存到_Trap_Frame结构体里面了,也就是从0x18~0x27:

这也就是意味着如果这个值是-1的话,它不会保存这几个值,因为它认为你并不是处在调试状态,只有当你处于调试状态的时候,才会把这几个值压到栈里;那么我们反过来推测,如果我们不想让它存这几个值,我们可以写一个程序,把test esp+0x2c这个位置的值变为-1就可以了,即如果你想调试我,我一直让你变成-1,这样的话你就无法得到我的DR0~DR7的寄存器。如果你使用过OD并且下过硬件断点你应该都知道,硬件断点都是使用这几个寄存器的,那这样的话如果我们人为地将其变为-1,我们就无法使用硬件断点了。
如果我们不是处在调试状态的话,代码就继续往后走了。

跳转到loc_408000:

大家接着往上看,实际上是跳转到另外一个函数叫kifastcallentry里面去了。

从上面两个图我们会发现什么问题呢?如果我们从KiSystemService进来的话,最终会跳转到loc_408000这个位置;如果你从KiFastCallEntry进来的话,那么它执行下去,最终也要执行到loc_408000这个位置,也就是说我们从两个口进来,最终执行的代码是一样的,而为什么要分成两个口进来,我们刚才已经给大家分析过了,就是最开始中断的时候进来的时候压了五个值,而快速调用的时候没有压五个值,所以它们填充_Trap_Frame这个表的时候,填充方式是不一样的。那么到现在为止,我们已经知道当我们进入零环的时候,我们知道其把三环的这些寄存器都保存到哪里了,虽然有些寄存器我们还没有用过,我们不清楚它做什么,但这个没有关系我们后面会讲,我们解决了一个问题——存到哪里的问题。

这节课是用KiSystemService分析的它是如何保存这些寄存器的,我们再做一个课后练习,KiFastCallEntry是如何填充这个结构体的。