windows核心情景分析--系統呼叫
Windows的地址空間分使用者模式與核心模式,低2GB的部分叫使用者模式,高2G的部分叫核心模式,位於使用者空間的程式碼不能訪問核心空間,位於核心空間的程式碼卻可以訪問使用者空間
一個執行緒的執行狀態分核心態與使用者態,當指令位於使用者空間時,就表示當前處於核心態,當指令位於核心空間時,就處於核心態.
一個執行緒由使用者態進入核心態的途徑有3種典型的方式:
1、 主動通過int 2e(軟中斷自陷方式)或sysenter指令(快速系統呼叫方式)呼叫系統服務函式,主動進入核心
2、 發生異常,被迫進入核心
3、 發生硬體中斷,被迫進入核心
現在討論第一種進入核心的方式:(又分為兩種方式)
1、 通過老式的int 2e指令方式呼叫系統服務(因為老式cpu沒提供sysenter指令)
如ReadFile函式呼叫系統服務函式NtReadFile
Kernel32.ReadFile() //點號前面表示該函式的所在模組
{
//所有Win32 API通過NTDLL中的系統服務存根函式呼叫系統服務進入核心
NTDLL.NtReadFile();
}
NTDLL.NtReadFile()
{
Mov eax,152 //我們要呼叫的系統服務函式號,也即SSDT表中的索引,記錄在eax中
If(cpu不支援sysenter指令)
{
Lea edx,[esp+4] //使用者空間中的引數區基地址,記錄在edx中
Int 2e //通過該自陷指令方式進入KiSystemService,‘呼叫’對應的系統服務
}
Else
{
Lea edx,[esp +4] //使用者空間中的引數區基地址,記錄在edx中
Sysenter //通過sysenter方式進入KiFastCallEntry,‘呼叫’對應的系統服務
}
Ret 36 //不管是從int 2e方式還是sysenter方式,系統呼叫都會返回到此條指令處
}
Int 2e的內部實現原理:
該指令是一條自陷指令,執行該條指令後,cpu會自動將當前執行緒的當前棧切換為本執行緒的核心棧(棧分使用者棧、核心棧),儲存中斷現場,也即那5個暫存器。然後從該cpu的中斷描述符表(簡稱IDT)中找到這個2e中斷號對應的函式(也即中斷服務例程,簡稱ISR),jmp 到對應的isr處繼續執行,此時這個ISR本身就處於核心空間了,當前執行緒就進入核心空間了
Int 2e指令可以把它理解為intel提供的一個內部函式,它內部所做的工作如下
Int 2e
{
Cli //cpu一中斷,立馬自動關中斷
Mov esp, TSS.核心棧地址 //切換為核心棧,TSS中記錄了當前執行緒的核心棧地址
Push SS
Push esp
Push eflags
Push cs
Push eip //這5項工作儲存了中斷現場【標誌、ip、esp】
Jmp IDT[中斷號] //跳轉到對應本中斷號的isr
}
IDT的整體佈局:【異常->空白->5系->硬】(推薦採用7字口訣的方式重點記憶)
異常:前20個表項存放著各個異常的描述符(IDT表不僅可以放中斷描述符,還放置了所有異常的異常處理描述符,0x00-0x13)
保留:0x14-0x1F,忽略這塊號段
空白:接下來存放一組空閒的保留項(0x20-0x29),供系統和程式設計師自己分配註冊使用
5系:然後是系統自己註冊的5個預定義的軟中斷向量(軟中斷指手動的INT指令)
(0x2A-0x2E 5個系統預註冊的中斷向量,0x2A:KiGetTickCount, 0x2B:KiCallbaclReturn
0x2C:KiRaiseAssertion, 0x2D:KiDebugService, 0x2E:KiSystemService)
硬: 最後的表項供驅動程式註冊硬體中斷使用和自定義註冊其他軟中斷使用(0x30-0xFF)
下面是中斷號的具體的分配情況:
0x00-0x13固定分配給異常:
0x00: Divide error(故障)
0x01: Debug (故障或陷阱)
0x02: 保留未用(為非遮蔽中斷保留的,NMI)
0x03: breakpoint(陷阱)
0x04: Overflow(陷阱)
0x05: Bounds check(故障)
0x06: Invalid Opcode(故障)
0x07: Device not available(故障)
0x08: Double fault(異常中止)
0x09: Coprocessor segment overrun(異常中止)
0x0A: Invalid TSS(故障)
0x0B: Segment not present(故障)
0x0C: Stack segment(故障)
0x0D: General protection(故障)
0x0E: Page fault(故障)
0x0F: Intel保留
0x10: Floating point error(故障)
0x11: Alignment check(故障)
0x12: Machine check(異常中止)
0x13: SIMD floating point(故障)
0x14-0x1f:Intel保留給他公司將來自己使用(OS和使用者都不要試圖去使用這個號段,不安全)
----------------------以下的號段可用於自由分配給OS、硬體、使用者使用-----------------------
linux等其他系統是怎麼劃分這塊號段的,不管,我們只看Windows的情況
0x20-0x29:Windows沒佔用,因此這塊號段我們也可以自由使用
0x2A-0x2E:Windows自己本身使用的5箇中斷號
0x30-0xFF:Windows決定把這塊剩餘的號段讓給硬體和使用者使用
參見《寒江獨釣》一書P93頁註冊鍵盤中斷時,搜尋空閒未用表項是從0x20開始,到0x29結束的,就知道為什麼寒江獨釣是在這段範圍內搜尋空白表項了(其實我們也完全可以從0x14開始搜尋)
Windows系統中,0x30-0xFF這塊號段讓給了硬體和使用者自己使用。事實上,這塊號段的開頭部分預設都是讓給硬體IRQ使用的,也即是分配給硬體IRQ的。IRQ N預設對映到中斷號0x30+N,如IRQ0用於系統時鐘,系統時鐘中斷號預設對應就是0x30。當然程式設計師也可以修改APIC(可程式設計中斷控制器)將IRQ對映到自定義的中斷號。
IRQ對外部裝置分配,但IRQ0,IRQ2,IRQ13必須如下分配:
IRQ0 ---->間隔定時裝置
IRQ2 ---->8259A晶片
IRQ13 ---->外部數學協處理器
其餘的IRQ可以任意分配給外部裝置。
雖然一個IRQ只對應一箇中斷號,但是由於IRQ數量有限,而裝置種類成千上萬,因此多個裝置可以使用同一個IRQ,進而,多個裝置可以分配同一個中斷號。因此,一箇中斷號可以共享給多個裝置同時使用。
明白了IDT,就可以看到0x2e號中斷的isr為KiSystemService,顧名思義,這個中斷號專用於提供系統服務。
在正式分析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 //呼叫對應的系統服務函式
}
KiSystemService()//int 2e的isr,核心服務函式總入口,注意這個函式可以巢狀、遞迴!!!
{
SaveTrap();
Sti //開中斷
---------------上面儲存完暫存器等現場後,開始查SSDT表呼叫系統服務------------------
FindTableCall();
---------------------------------呼叫完系統服務函式後------------------------------
Move esp,kthread.TrapFrame; //將棧頂回到trap幀結構體處
Cli //關中斷
If(上次模式==UserMode)
{
Call KiDeliverApc //遍歷執行本執行緒的核心APC和使用者APC佇列中的所有APC函式
清理Trap幀,恢復暫存器現場
Iret //返回使用者空間
}
Else
{
返回到原call處後面的那條指令處
}
}
上面所說的trap幀(TrapFrame)是指一個結構體,用來儲存系統呼叫、中斷、異常發生時的暫存器現場,方便以後回到使用者空間/回到中斷處時,恢復那些暫存器的值,繼續執行
Trap幀中除了儲存了所有暫存器現場外,還附帶儲存了一些其他資訊,如seh連結串列的地址等
必須說一下trap幀的結構體佈局定義:
typedef struct _KTRAP_FRAME //Trap現場幀
{
------------------這些是KiSystemService儲存的---------------------------
ULONG DbgEbp;
ULONG DbgEip;
ULONG DbgArgMark;
ULONG DbgArgPointer;
ULONG TempSegCs;
ULONG TempEsp;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
ULONG SegGs;
ULONG SegEs;
ULONG SegDs;
ULONG Edx;//xy 這個位置不是用來儲存edx的,而是用來儲存上個Trap幀,因為Trap幀是可以巢狀的
ULONG Ecx; //中斷和異常引起的自陷要儲存eax,系統呼叫則不需儲存ecx
ULONG Eax;//中斷和異常引起的自陷要儲存eax,系統呼叫則不需儲存eax
ULONG PreviousPreviousMode;
struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh連結串列的開頭地址
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Ebp;
----------------------------------------------------------------------------------------
ULONG ErrCode;//發生的不是中斷,而是異常時,cpu還會自動在棧中壓入對應的具體異常碼在這兒
-----------下面5個暫存器是由int 2e內部本身儲存的或KiFastCallEntry模擬儲存的現場---------
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp;
ULONG HardwareSegSs;
---------------以下用於用於儲存V86模式的4個暫存器也是cpu自動壓入的-------------------
ULONG V86Es;
ULONG V86Ds;
ULONG V86Fs;
ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;
KPCR與KPRCB結構,都是用來描述處理器的,前者叫處理器描述符,後者叫處理器控制塊
Struct KPCR
{
KPCR_TIB Tib;//類似於TEB.TIB,內部第一個欄位都是ExceptionList
KPCR* self;//自身結構體的地址,方便直接定址
KPRCB* kprcb;//處理器控制塊的地址、
KIRQL irql;//當前cpu的irql
USHORT* IDT;//本cpu的IDT地址,一有中斷/異常就去這個表找isr、epr
USHORT* GDT;//全域性描述符表地址
KTSS* TSS;//記錄了本cpu上當前執行執行緒的狀態資訊,重要欄位有核心棧地址,IO許可權點陣圖
……
}
Struct KPRCB
{
KTHREAD* CurrentThread;//本cpu上當前執行的執行緒
KTHREAD* NextThread;//本cpu上將搶佔當前執行緒的下個執行緒(搶佔式排程核心)
BYTE CpuID;//不多說
ULONG KernelTime,UserTime;//本cpu的累計執行時間統計資訊
……
}
系統中有兩張“系統服務表”,即SSDT和shadow SSDT。同樣,系統中也有兩張“系統服務表描述符表”,每個表都包含兩個描述符。兩張表中第一個描述符都是SSDT的描述符,第二個描述符都是shadow SSDT的描述符。但是第一個表的第二個描述符是空白的,因此,第一張表實際上只能描述SSDT表,第二張表可以描述SSDT表和shadow SSDT表。所以一旦呼叫的是shadow SSDT表中系統服務函式,這個執行緒就會自動換用第二張服務表描述符表,具體為:
Mov kthread.ServiceTable, 第二張服務表描述符表,這樣,這個執行緒就變為一個GUI執行緒,以後都使用第二張“系統服務表描述符表”了
“系統服務表描述符”是一個結構體,用來描述一張系統服務表的各種資訊,如下定義:
Struct KSERVICE_TABLE_DESCRIPTOR
{
ULONG* base;//系統服務表的地址
ULONG* CountTable;//該系統服務表中每個函式的歷史呼叫次數統計表
ULONG limit;//該系統服務表的大小,也即容量
BYTE* ArgSizeTable;//記錄該系統服務表中每個函式引數大小的表
}
2、 通過快速呼叫指令(Intel的是sysenter,AMD的是syscall)呼叫系統服務
老式的cpu不支援、不提供sysenter指令,只能由int 2e模擬中斷方式進入核心,呼叫系統服務,
但是,那種方式有一個明顯的缺點,就是速度慢!(如int 2e內部本身要儲存5個暫存器的現場,然後還要去IDT中查詢isr,這個過程消耗的時間太多),因此x86系列從奔騰2代開始為系統呼叫專門增設了一條sysenter指令以及相應的暫存器msr。同樣,sysenter指令也可看做intel提供的一個內部函式,它做的工作如下:
Sysenter()
{
Mov ss,msr_ss
Mov esp,msr_esp //關鍵
Mov cs,msr_cs
Mov eip,msr_eip //關鍵
}
系統在啟動初始化過程中,會將上面四個msr暫存器設為固定的值,其中msr_esp為DPC函式專用堆疊,
Msr_eip則固定為KiFastCallEntry
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呼叫約定函式
}
Sysexit指令也可理解為一個函式,它做的工作如下:
Sysexit
{
Mov cs,msr_cs
Mov ss,msr_ss
Mov esp,ecx //換用使用者空間中的棧
Mov eip,edx //這樣,就返回使用者空間中了,所有系統呼叫總是先返回到NTDLL.dll中的某個固定位置,最後一路返回到NTDLL中發起系統呼叫的那個存根函式體內
}
前面講過,執行緒的核心結構KTHREAD中,有一個欄位記錄了PreviousMode,這個“上一模式”指的就是,進入本次系統呼叫前的模式,也即指進入SSDT表中的服務函式前的模式是在使用者空間還是核心空間。Windows不僅支援由使用者空間發起系統呼叫,也支援由核心空間發起系統呼叫,為此,Windows專門配備了Zw系列的核心服務封裝函式,如:
Ntoskrnl.ZwCreateFile() //模擬構造一個Trap現場,然後呼叫系統服務
{
Mov eax,系統服務號
Lea edx,[esp+4]
Push eflags
Push cs //關鍵。根據cs的值設定KTHREAD.PreviousMode欄位
//注意在呼叫本函式前,此處不再模擬中斷、異常時自動儲存的ss、esp、eip暫存器
Call KiSystemService
Ret //這樣,呼叫完系統服務後,就返回到這兒了,不再返回到NTDLL中的sysenter指令後面了
}
NTSTATUS NtReadFile(…)
{
…
KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
…
}
KeGetPreviosMode()
{
Return kthread.PreviousMode;
}
這樣:核心API KeGetPreviosMode的返回值就是核心模式了
上面這個NtReadFile系統服務函式需要獲得‘上次模式’,而這個‘上次模式’是在構造TrapFrame中的過程中根據cs的值設定的。因此,凡是需要讀取‘上次模式’的系統服務函式,都必須有一個“正確的TrapFrame”。因此ZwXXX系列的系統服務封裝函式會在內部Push eflags,Push cs,Call KiSystemService,這三條指令就恰好偽造了一個“正確的TrapFrame”,使得系統服務能夠正確執行。
換言之:凡是需要讀取“正確TrapFrame”的系統服務函式都不能直接手工呼叫,必須呼叫他們的ZwXXX封裝函式。反之,就可以直接呼叫。
附:cs,ds,es,fs,gs,ss這六個段暫存器的介紹:
fs在使用者態間接指向TEB,在核心態間接指向kpcr
其他5個段暫存器都可以理解為一個描述符
如struct cs
{
BOOL bInGDT;//指示下面的idx是在GDT表中還是LDT表中的索引,一般為TRUE
Int idx;// GDT/LDT描述符表中,本cs段描述符的索引位置
Int rpl://本段的特權級別:0或者3
}
簡單的講,可以將他們視為GDT或LDT中的段描述符索引
更多基礎資訊參考:張銀奎 -《軟體除錯》