背景
一个线程由用户态进入内核态的途径有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),至此进入内核代码空间。
// 这个函数用来保存寄存器现场和其他状态信息
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。
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调用约定函数
}
总结下 KiFastCallEntry 大概做了什么:
- 保存现场,并将环境切入内核
- 查SSDT表调用对应系统服务
- 恢复调用栈
- 执行 APC
- 返回 r3