天天看点

[Windows] 系统调用(R3 进入 R0)

背景

一个线程由用户态进入内核态的途径有3种典型的方式:

  • 通过 int 0x2e(软中断自陷) 或 KiFastSystemCall (快速系统调用),主动进入内核。
  • 引发异常或硬件中断,被迫进入内核。
一般 R3 的 API 最终都会去调用 NT 函数,在 NT 函数中根据该 API 对应的索引号去 SSDT / SSDT Shadow 中查找对应的方法,最后通过 SSDT / SSDT Shadow 进入内核。

通过 int 0x2e 进入内核(xp以下)

在 xp 以下,SSDT / SSDT Shadow 进入内核的指令是 int 0x2e,该指令是一条自陷指令(也叫中断门),之后会将用户线程栈切换成内核线程栈,保存 CONTEXT,到 IDT 中寻找 0x2e 对应的异常处理函数(KiSystemService),至此进入内核代码空间。

[Windows] 系统调用(R3 进入 R0)
// 这个函数用来保存寄存器现场和其他状态信息
SaveTrap()  
{
    Push 0                                    // LastError
    Push ebp
    Push ebx
    Push esi
    Push edi
    Push fs                                    // 此时的 fs 若是从用户空间自陷进来的就指着 TEB,反之指着 kpcr
    Push kpcr.ExceptionList
    Push kthread.PreviousMode
    Sub esp,0x48                                // 腾给调式寄存器保存用
    -----------至此,上面的这些语句连同int 2e中的语句在栈上构造了一个trap帧-----------------
    Mov CurTrapFrame,esp                        // 当前Trap帧的地址
    Mov CurTrapFrame.edx, kthread.TrapFrame    // 将上次的trap帧地址记录到edx成员中
    Mov kthread.TrapFrame, CurTrapFrame,        // 修改本线程当前trap帧的地址
    
    Mov kthread.PreviousMode,GetMode(进入内核前的CS)  // 根据CS自动确定上次模式
    Mov kpcr.ExceptionList,-1                         // 表示刚进入内核时,尚未安装seh
    
    Mov fs,kpcr                                     // 一进入内核就让fs改指向当前cpu的描述符kpcr,不再指向TEB
    
    
    If(当前线程处于调试状态)
       保存DR0-DR7到trap帧中
}

// 这个函数用来查表,拷贝参数,调用系统服务
FindTableCall() 
{
    Mov edi,eax                            // 系统函数号,低12位为索引,第13为表示是哪张系统服务表中的索引
    Mov eax, edi.低12位                    // eax=真正的服务号
    If(edi.第13位=1)                        // if这是shadow SSDT中的系统函数号
    {
       If(当前线程.服务描述符表!=shadow)
          当前线程.服务描述符表=shadow        // 换用另外一张描述符表
    }
    服务表描述符=当前线程.服务描述符表[edi.第13位]
    Mod edi=服务表描述符.base                // 这个系统服务表的地址
    Mov ebx,[edi+eax*4]                    // 查表获得这个函数的地址
    Mov ecx=服务表描述符.Number[eax]        // 查表获得的这个系统函数的参数大小
    
    Mov esi,edx                            // esi = 用户空间中的参数地址
    Mov edi,esp                            // esp已经为内核栈的栈顶地址
    Rep movsb                                // 将所有参数从用户空间复制到内核空间,相当于N个连续push压参
    Call  ebx                                // 调用对应的系统服务函数

}

// int 2e的isr,内核服务函数总入口,注意这个函数可以嵌套、递归!!!
KiSystemService()
{
    SaveTrap();
    Sti                            // 开中断
    ---------------上面保存完寄存器等现场后,开始查SSDT表调用系统服务------------------
    FindTableCall();
    ---------------------------------调用完系统服务函数后------------------------------
    Move  esp,kthread.TrapFrame;    // 将栈顶回到 trap 帧结构体处
    Cli                            // 关中断
    If(上次模式==UserMode)
    {
    Call  KiDeliverApc                // 遍历执行本线程的内核APC和用户APC队列中的所有APC函数
    清理Trap帧,恢复寄存器现场
    Iret                            // 返回用户空间
    }
    Else
    {
       返回到原call处后面的那条指令处
    }

}      

总结一下,KiSystemService 大概做了什么:

  • 保存寄存器现场和其他状态信息
  • 查SSDT表调用对应的系统服务
  • 恢复调用栈
  • 判断上次模式,如果是用户模式则执行 APC,之后恢复现场返回 r3 。如果是内核模式,则返回到调用 call KiSystemService 的下一条指令。

通过 KiFastSystemCall 进入内核(xp以上)

快速调用指令(Intel的是sysenter,AMD的是syscall)调用系统服务。

老式的cpu不支持、不提供sysenter指令,只能由int 2e模拟中断方式进入内核,调用系统服务,但是,那种方式有一个明显的缺点,就是速度慢!(如int 2e内部本身要保存5个寄存器的现场,然后还要去IDT中查找isr,这个过程消耗的时间太多),因此x86系列从奔腾2代开始为系统调用专门增设了一条sysenter指令以及相应的寄存器msr。

[Windows] 系统调用(R3 进入 R0)
Sysenter()
{
   Mov ss,msr_ss
   Mov esp,msr_esp            //关键
   Mov cs,msr_cs
   Mov eip,msr_eip            //关键
}

Sysexit
{
   Mov cs,msr_cs
   Mov ss,msr_ss
   Mov esp,ecx            // 换用用户空间中的栈
   Mov eip,edx            // 这样,就返回用户空间中了,所有系统调用总是先返回到NTDLL.dll中的某个固定位置,最后一路返回到NTDLL中发起系统调用的那个存根函数体内
}

// 快速系统调用总入口
KiFastCallEntry()  
{
    Mov fs,kpcr            // 一进入内核,就将fs改指向处理器描述符kpcr
    Mov esp,TSS.ESP         // 一进入内核,就换用内核栈(每个线程的内核栈地址保存在TSS中)
    Push ds
    Push edx               // edx为用户空间栈的栈顶地址,保存在这儿,方便以后回到用户空间时恢复
    Push eflags
    Push cs
    Push sysenter指令的后面那条指令的地址      // 将用户空间中的返回地址保存在这儿
    --------上面的5条push指令模拟中断、异常发生时cpu自动保存的那5个寄存器的现场------------
   Cli                              // 关中断,构造 Trap 现场帧的过程中需要暂时关中断
   Mov eflags,0x2
   SaveTrap();
   Sti                              // 开中断
   ---------------上面保存完寄存器等现场后,查SSDT表调用对应系统服务----------------------
   FindTableCall();
   ------------------------------------调用完系统服务函数后--------------------------------
   Move  esp,kthread.TrapFrame;            // 将栈顶回到trap帧结构体处
   Cli                              // 关中断
   …
   Call  KiDeliverApc                     // 遍历执行本线程的内核APC和用户APC队列中的所有APC函数
   …
   清理Trap帧,恢复寄存器现场
   Sti                              // 开中断
   -----------------------------------下面返回用户空间-------------------------------------
   Mov ecx,保存的用户空间栈顶地址
   Mov edx,保存的返回地址,也即sysenter指令的后面那条指令的地址
   sysexit                           // 可以把这条指令理解为一个fastcall调用约定函数
}      
  • 保存现场,并将环境切入内核
  • 查SSDT表调用对应系统服务
  • 恢复调用栈
  • 执行 APC
  • 返回 r3

继续阅读