背景
一個線程由使用者态進入核心态的途徑有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調用約定函數
}
- 儲存現場,并将環境切入核心
- 查SSDT表調用對應系統服務
- 恢複調用棧
- 執行 APC
- 傳回 r3