内容回顾
前文提到了API进入0环以后会调用的函数:KiSystemService()、KiFastCallEntry()。
前文的练习
- 进入0环后,原来的寄存器存在哪里?
- 如何根据系统调用号(EAX寄存器中储存)找到要执行的内核函数?
- 调用时的参数是储存到3环的堆栈,如何传递给内核函数?
- 2种调用方式入口(KiSystemService()、KiFastCallEntry())是不一样的,它们是如何返回到3环的?
进入0环后,原来的寄存器存在哪里?本文将以KiSystemService()来分析举例。
如果想分析0环KiSystemService()的代码首先要来了解几个结构体,如果不了解的话,代码是看不懂的。
Trap Frame 结构体
存储3环的寄存器,由Windows操作系统来维护,处于0环。
可以在windbg中使用指令:kd> dt _Ktrap_frame 来查看。
该结构体是非常重要的,无论是通过中断门进入0还还是通过sysenter进入0环。
3环的所有寄存器都会储存到这个结构体中,该结构体本身是处于在0环的结构体,是由操作系统来进行维护的。
ETHREAD 线程相关的结构体
存储当前线程,每个线程都会有这个结构体来储存信息。
可以在windbg中使用指令:kd> dt _ETHREAD 来查看。
线程进程的概念暂时没有提到,但是有应用开发的经验应该理解线程的概念。
线程一些状态信息在0环也是有对应的结构体的,这个结构体就是 _ETHREAD 结构体。
_ETHREAD结构体中有一个非常重要的子结构体:_KTHREAD,该子结构体中存储的都是相对比较重要的信息。
KPCR
描述当前CPU的各种状态。
- 线程有一个具体的结构体来描述它的状态。
- CPU本身也有一个结构体来描述它的状态,这个结构体叫做:KPCR——CPU控制区(Process Control Region)。
- 每一个CPU都会有一个KPCR结构体,来描述当前CPU所处的状态。如果是2核CPU就有2个KPCR,如果是3核CPU就有3个KPCR。
如果在windbg中想查看当前CPU有几个核心,可以使用:kd> dd KeNumberProcessors指令。
同时,可以通过:kd> dd KiProcessorBlock L2指令,查看当前的KPCR结构体都存在哪里:
L2表示列出2个KPCR结构体,但是当前CPU只有一个核,所以可以看到线性地址中第一个是有值的,但是第二个是空的。
代码
KiSystemService()的代码:
- 该结构体中有很多成员,最后的4个成员(紫色部分)在保护模式下是没有使用的,不用考虑这4个成员。
- 在前文提到过中断门,当权限发生切换的时候,中断门会向0环的堆栈压入5个值,分别是3环的:SS、ESP、EFlags、CS、EIP。
- 在中断门提权发生权限切换的时候,会压入以上五个值。
- 也就是说,通过中断门进入0环,当KiSystemService()一开始执行的时候,这个结构体中就已经有这5个值了。
- 但是,当通过快速调用(Sysenter)进入0环的时候,这5个值是没有的。
- 通过以上能了解为什么操作系统在设计的时候要有两个不同的入口进入0环,如果没有差异的话完全可以使用一个函数。
- 为什么要分为2个函数原因就在这里,进入的时候压入的值不同,如果都使用一个函数,就会出问题。
- 上图代码中的push 0对应的结构体是什么位置?
- 存储在该结构体中偏移为64的位置,也就是:0x64(ErrCode)。
- 在前文学习中断门的时候,权限切换压入5个值,其实有些情况下,中断门会压入6个值。
- 什么时候压入6个值?可以参考Intel白皮书第三卷里的中断与异常,有一小节叫ErrCode。
- 但是通过INT 0x2E并没有ErrCode,操作系统为了对齐,它自己补充了一个0,这就是上图KiSystemService代码中看到的第一行是push 0。
这个push 0就写入到ErrCode这个位置。
然后KiSystemService()连续把3环的几个寄存器压栈了,分别是ebp、ebx、esi、edi、fs。
观察一下上图,对应的值分别是:
然后继续看代码,当执行到mov ebx, 30h 的时候,向ebx储存了一个值:30,如何使用这个值的?可以看到下面有mov fs, bx。
这行代码并不陌生了,这是一行写段寄存器的代码。0x30这个段选择子,根据段选择子查询GDT表,找到对应的段描述符,把对应的段描述符加载到fs段寄存器。
将其进行拆分0x30:0000 00000 0011 0000:
最低的两位是RPL,0环的。
然后第三位是要查询哪个表,0查GDT,1查LDT。
索引:0110,加起来是6。
可以在windbg中找到这张表,然后看一下GDT表中索引为6的段描述符是什么(ffc093df`f0000001)。
ffc093df`f0000001,标红部分拼起来是一个32位的地址,该地址就是一个BASE,拼完以后得到的地址:FFDFF000,它指向KPCR这个结构体。
为什么FS在3环和在0环的时候会发生切换,因为代表的含义不一样。
FS在3环指向的是一个叫TEB(线程状态)的结构,是3环能看的。
一旦进入0环,FS指向的不再是TEB这样的结构体,而是另外的结构体:KPCR,它是通过段描述符的BASE,指向KPCR的。
然后继续往下看代码:
首先执行了 push dword ptr ds:0FFDFF000h,这行代码的值是0FFDFF000,这个值就是FS指向的基址。
把老的ExceptionList(异常链表)保存起来,存到哪里呢?存到了下图的位置:
紧接着把现在的ExceptionList(异常链表)变为 -1,原来的值已经没法用了,所以要换成新的值。
原来的值是3环的ExceptionList(异常链表),现在进入了0环,所以没有必要再保存原来的ExceptionList(异常链表)了。
然后执行了mov esi, ds:0FFDFF124h,这行代码指向的是KPCR结构体偏移为0x124的位置:
如果以后分析内核代码,能经常看到0x124(CurrentThread)这种东西,它指的是当前CPU在跑的线程,线程的信息是什么。
现在知道了mov esi, ds:0FFDFF124h,esi中存储的就是一个 _KTHREAD 结构体,结构体中存储的是当前CPU在跑的线程是什么样的。
接着往下看,push dword ptr [esi+140h]:
esi+140,需要看的是_ETHREAD结构体,因为其中的第一个成员就是_KTHREAD,_KTHREAD结构体的0x140(PreviousMode):先前模式,后面再解释。
它将这个先前模式也保存到了结构体中:
然后,进行了一个提升堆栈的操作:sub esp, 48h:
提升ESP以后将指向_Trap_Frame结构体的第一个成员。
下一行代码:mov ebx, [esp+68h+arg_0]:
arg_0对应的值是4h,也就变成了:mov ebx, [esp+6Ch],将其取出放入了ebx。
当前ESP指向的是_Trap_Frame结构体的第一个成员,那么寻找一下0x6C,是CS:
它是3环原来的CS的值,将其取出后做了一个跟1的与运算(and ebx, 1),得到结果:如果原CS最低位是0,运算后结果是0,如果原来的CS低2位是3,运算后的结果就是1。
然后:mov [esi+140h], bl,esi是当前线程,偏移0x140(先前模式):
什么是先前模式?指的是调用这个代码的时候,原来如果是0环的,那么先前模式就存0,如果原来的是3环的,先前模式就存1。
有些内核代码可以从0环调用也可以从3环调用,但是执行的内容是不一样的。操作系统根据先前模式来区分到底是0环还是3环在调用。
如果是从3环进来的,上图存完后就是1,将1存到了esi+140h的位置。
然后将esp的值放到ebp中(mov ebp, esp),esp和esp都将指向同一个位置:_Trap_Frame结构体最开始的位置,第一个成员。
mov ebp, [esi+134h]:esi现在是_KTHREAD结构体,偏移0x134是:
这个成员是一个指针,就是TrapFrame的指针,也就是说原来的线程里面是存放了一个TrapFrame的地址的,现在把它取出来。
然后将它放到ebp+3Ch的位置(mov [ebp+3Ch], ebx)。ebp+3C是edx,但是它只是临时存放在这里。
真正要返回3环的时候,还要把它取出来。存放完成后接下来:mov [esi+134h], ebp,将ebp的值又压入当前线程0x134的位置。
因为当前TrapFrame的地址发生了变化,所以要把新的地址放到当前线程0x134的位置。
mov ebx, [ebp+60h],将原来的3环的ebp放到ebx中,然后又放到了ebp+0的位置:mov [ebp+0], ebx
ebp+0对应的就是_Trap_Frame结构体中的第1个成员。
同样的 mov edi, [ebp+68h],将原来3环的EIP取出来放到了edi中,然后下面mov [ebp+4], edi,将原来3环的EIP放到了ebp+4的位置。
ebp+4对应的就是_Trap_Frame结构体中的第2个成员。
mov [ebp+0Ch], edx,edx里面存储的就是3环参数的指针,也就是3环的参数在哪里,堆栈在哪里。
3环进0环有2个值,一个是edx,另一个是eax。eax存储的是系统调用号(内核函数编号),edx存储的是3环参数的指针。
它将其存到了ebp+0C的位置,对应的就是_Trap_Frame结构体中的DbgArgPointer成员。
mov dword ptr [ebp+8],0BADB0D00h:储存了一个操作系统要用的标志,存到ebp+8的位置,对应_Trap_Frame结构体中的DbgArgMark成员。
test byte ptr [esi+2Ch], 0FFh:比较esi+2C的位置,esi+2C是当前线程+2C的位置,对应的就是:DebugActive
DebugActive指的是当前线程是否处于调试状态。
jnz Dr_kss_a:是否处于调试状态,如果处于调试状态,要进行跳转:
跳到这里其实就几件事情,把调试寄存器dr0-dr7也存到_Trap_Frame结构体中(0x18-0x27)。
只有处于调试状态,才会跳转到这里。
如果将其人为的修改为-1,那么就没法使用硬件断点了。
如果不是处于调试状态,那么就会跳转到别的地方:
图1:
跳到这里来,往上翻其实可以发现是跳转到了:KiFastCallEntry()这里来。
图2:
可以发现一个问题,如果从KiSystemService()函数进来,会跳到图1的位置。
如果从KiFastCallEntry进来的话,最终也是会执行到图1的位置。
通过以上内容可以了解到了区别,中断门进来会压入5个值,而快速调用(sysenter)不会,他们填充_Trap_Farme结构体的方式也是不一样的。
练习
- 分析KiFastCallEntry()是如何填充_Trap_Frame结构体的。