KiFastCallEntry() 機制分析
1. 概述
從 windows xp 和 windows 2003 開始使用了快速切入核心的方式提供系統服務例程的呼叫。
KiFastCallEntry() 的實現是直接使用匯編語言,C 語言不能直接表達某些操作。我從 windows 2003 裡反彙編出來,寫成 C 偽碼形式,點選這裡察看:KiFastCallEntry()
在下面的篇章裡,我將分析從 Win32 子系統 API WriteFile() 的呼叫為例,介紹如何切入到 nt 模組的 NtWriteFile() 系統服務例程。
2. Win32 子系統 API 呼叫
當我向一個檔案或 device 使用 WriteFile() API 來寫入一些資料時,像下面的呼叫:
// // 往裝置裡寫資料 // if (WriteFile(hFile, _T("Hello, world!"), 20, &nCount, NULL) == FALSE) { _tprintf(_T("fail: WriteFile for device, ErrorCode: %d\n"), GetLastError()); CloseHandle(hFile); return -2; }
象這樣的 API 會切入到 kernel 執行系統服務例程(Service Routine),在切入 kernel 前會經過一些 stub 函式轉發。
如下圖所示:
在使用者程式碼裡,對於 WriteFile() 函式的呼叫,編譯器在使用者程式碼裡會生成對子系統 DLL 的 ntdll 模組的 ZwWriteFile() 函式的呼叫,如下程式碼所示:
Status = ZwWriteFile(hFile, // file handle 0, // event handle NULL, // APC routine NULL, // APC context &IoStatus, // IO_STATUS_BLOCK 塊 lpBuffer, // buffer nNumberOfBytesToWrite, // write bytes NULL, // Byte Offset, PLARGE_INTEGER 指標 NULL); // key: ULONG 指標
這個真實的 ntdll!ZwWriteFile() 是 9 個引數,ntdll!NtWriteFile() 會定向到 ntdll!ZwWriteFile() 裡,因此:它們是完全一樣的,只是名字不同而已!
3. ntdll!ZwWriteFile() 函式
ZwWriteFile() 是一個 stub 函式,作用也只是轉發一下,因此它很簡單:
ntdll!ZwWriteFile:
7c957b9d b81c010000 mov eax,11Ch ; 系統服務例程號
7c957ba2 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300) ; 取得 KiFastCallEntry() stub 函式
7c957ba7 ff12 call dword ptr [edx] ; 呼叫這個 stub 函式
7c957ba9 c22400 ret 24h
ZwWriteFile() 在 eax 暫存器裡傳遞一個系統服務例程號,NtWriteFile() 服務例程這號碼是 11Ch。從 UserSharedData 結構裡得到另一個 stub 函式。
這個 stub 函式是 KiFastCallEntry() 的 stub 函式。
4. ntdll!KiFastSystemCall() 函式
根據上面的程式碼,ZwWriteFile() 函式從一個叫 UserSharedData 結構區域裡得到一個函式地址值,它就是 KiFastSystemCall() 函式。
這個函式是最後一個執行在使用者層的 stub 函式,它將會轉入 kernel 層:
ntdll!KiFastSystemCall:
7c958458 8bd4 mov edx,esp ; 傳送 caller 的 stack frame pointer
7c95845a 0f34 sysenter ; 快速切入到 kernel
7c95845c c3 ret ; 注意:實際上這是一個獨立的 ntdll!KiFastSystemCallRet() 例程
值得注意的是:這三行程式碼實際上包含了兩個 ntdll 例程,最後一條 ret 指令,它是一個獨立的 ntdll!KiFastSystemCallRet() 例程,我們可以在後面介紹_KUSER_SHERED_DATA 結構時看到。
KiFastSystemCall() 使用 sysenter 指令快速切入到核心的 nt!KiFastCallEntry() 程式碼裡。
注意:給 edx 暫存器傳送當前的 esp 值,這一點很重要,看看下圖的 stack 佈局:
在核心層的 KiFastCallEntry() 程式碼裡,將 edx + 8 來獲得傳遞給 ZwWriteFile() 的引數地址,從而讀取完整的引數。
5. _KUSER_SHARED_DATA 結構
在 User 層和 Kernel 層分別定義了一個 _KUSER_SHARED_DATA 結構區域,用於 User 層和 Kernel 層共享某些資料,在 sysenter 快速切入機制裡就使用了這個區域。
它們使用固定的地址值對映,_KUSER_SHARED_DATA 結構區域在 User 和 Kernel 層地址分別為:
- User 層地址為:0x7ffe0000
- Kernnel 層地址為:0xffdf0000
值得注意的是: User 層和 Kernel 層的 _KUSER_SHARED_DATA 區域都對映到同一個實體地址,下面是在 windbg 裡得到 windows 2003 裡的資訊:
kd> !pte 7ffe0000
VA 7ffe0000
PDE at C0601FF8 PTE at C03FFF00
contains 00000000108AD067 contains 0000000000041025
pfn 108ad ---DA--UWEV pfn 41 ----A--UREV
kd> !pte ffdf0000
VA ffdf0000
PDE at C0603FF0 PTE at C07FEF80
contains 0000000000513063 contains 0000000000041163
pfn 513 ---DA--KWEV pfn 41 -G-DA--KWEV
可以看到:它們都對映到物理頁面 0x41000 上。因此:User 層和 Kernel 層的 _KUSER_SHARED_DATA 區域內容是完全一樣的。基於這種設計可以方便地在 User 層和 Kernel 層共享某些資料。
另一點:在 User 層裡 _KUSER_SHARED_DATA 區域是只讀的,只有在 Kernel 層才是可寫的。因此:Kernel 在初始化階段某個時刻對 _KUSER_SHHARED_DATA 區進行設定。User 層只能讀取其中的值,如前面的 stub 函式所示:
ntdll!ZwWriteFile:
7c957b9d b81c010000 mov eax,11Ch ; 系統服務例程號
7c957ba2 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300) ; 取得 KiFastCallEntry() stub 函式
7c957ba7 ff12 call dword ptr [edx] ; 呼叫這個 stub 函式
7c957ba9 c22400 ret 24h
下面,來看看 _KUSER_SHARED_DATA 區域是些什麼內容(User 層和 Kernel 層是一樣的),在 windbg 用 dt 命令來檢視:
kd> dt _KUSER_SHARED_DATA 0x7ffe0000
ntdll!_KUSER_SHARED_DATA
+0x000 TickCountLowDeprecated : 0
+0x004 TickCountMultiplier : 0xfa00000
+0x008 InterruptTime : _KSYSTEM_TIME
+0x014 SystemTime : _KSYSTEM_TIME
+0x020 TimeZoneBias : _KSYSTEM_TIME
+0x02c ImageNumberLow : 0x14c
+0x02e ImageNumberHigh : 0x14c
+0x030 NtSystemRoot : [260] 0x43
+0x238 MaxStackTraceDepth : 0
+0x23c CryptoExponent : 0
+0x240 TimeZoneId : 0
+0x244 LargePageMinimum : 0x200000
+0x248 Reserved2 : [7] 0
+0x264 NtProductType : 3 ( NtProductServer )
+0x268 ProductTypeIsValid : 0x1 ''
+0x26c NtMajorVersion : 5
+0x270 NtMinorVersion : 2
+0x274 ProcessorFeatures : [64] ""
+0x2b4 Reserved1 : 0x7ffeffff
+0x2b8 Reserved3 : 0x80000000
+0x2bc TimeSlip : 0
+0x2c0 AlternativeArchitecture : 0 ( StandardDesign )
+0x2c8 SystemExpirationDate : _LARGE_INTEGER 0x0
+0x2d0 SuiteMask : 0x112
+0x2d4 KdDebuggerEnabled : 0x3 ''
+0x2d5 NXSupportPolicy : 0x2 ''
+0x2d8 ActiveConsoleId : 0
+0x2dc DismountCount : 0
+0x2e0 ComPlusPackage : 0xffffffff
+0x2e4 LastSystemRITEventTickCount : 0x239f29d
+0x2e8 NumberOfPhysicalPages : 0x17f1b
+0x2ec SafeBootMode : 0 ''
+0x2f0 TraceLogging : 0
+0x2f8 TestRetInstruction : 0xc3
+0x300 SystemCall : 0x7c958458 <--------- System Call stub 函式
+0x304 SystemCallReturn : 0x7c95845c <--------- System Call return 函式
+0x308 SystemCallPad : [3] 0
+0x320 TickCount : _KSYSTEM_TIME
+0x320 TickCountQuad : 0x2481d8
+0x330 Cookie : 0xa4a0f27b
+0x334 Wow64SharedInformation : [16] 0
其中 +0x300 位置上就是 KiFastSystemCall() stub 函式地址,而 +0x304 位置上就是返回函式地址:
ntdll!KiFastSystemCall:
7c958458 8bd4 mov edx,esp ; 傳送 caller 的 stack frame pointer
7c95845a 0f34 sysenter ; 快速切入到 kernel
7c95845c c3 ret ; 注意:實際上這是一個獨立的 ntdll!KiFastSystemCallRet() 例程
地址 0x7c958458 是 ntdll!KiFastSystemCall() 函式地址,地址 0x7c95845c 是 ntdll!KiFastSystemCallRet() 函式地址。
6. 切入 KiFastCallEntry()
在使用者層的 stub 函式會使用 sysenter 指令切入到核心層的 KiFastCallEntry() 函式,再由 KiFastCallEntry() 函式分發到相應的系統服務例程執行。
如下圖所示:
KiFastCallEntry() 函式是使用匯編語言來實現的,我將寫成 C 偽碼形式,點選這裡察看:KiFastCallEntry(),這裡不再重複貼出。
6.1 讀取 TSS 資訊
在 x86 體系的 sysenter/sysexit 指令快速切入機制裡 IA32_SYSENTER_ESP 暫存器(MSR 地址為 175h)提供了 ESP 值。但是,在 windows 裡並沒有使用這個值,而是使用KPCR 結構內TSS 塊裡的 ESP 值。
// // 得到當前 TSS 塊,並讀取 0 級的 esp 值 // 注意:這個 Esp0 指向一個 KTRAP_FRAME 結構的 V86Es 成員! // Esp0 值減去 0x7c 就等於 KTRAP_FRAME 結構地址,trap 用於 context 資訊 // esp 被賦予 KTRAP_FRAME 結構地址:esp = KtrapFrame,它以 push 的方式儲存 context 資訊 // PKTSS Tss = GetCurrentTss(); PKTRAP_FRAME KtrapFrame = (PKTRAP_FRAME)(Tss->Esp0 - 0x7c);
在 KPCR(Processor Cotnrol Region)區域的 +0x40 位置是 TSS 指標(指向一個 KTSS 結構),KPCR 結構的地址在0xffdff000:
kd> dt _kpcr 0xffdff000
ntdll!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : 0xf6ac85b8 _EXCEPTION_REGISTRATION_RECORD
+0x004 Used_StackBase : (null)
+0x008 PerfGlobalGroupMask : (null)
+0x00c TssCopy : 0x80042000 Void
+0x010 ContextSwitches : 0x10d344b
+0x014 SetMemberCopy : 1
+0x018 Used_Self : 0x7ffdf000 Void
+0x01c SelfPcr : 0xffdff000 _KPCR
+0x020 Prcb : 0xffdff120 _KPRCB
+0x024 Irql : 0 ''
+0x028 IRR : 0
+0x02c IrrActive : 0
+0x030 IDR : 0xffffffff
+0x034 KdVersionBlock : 0x8088e3b8 Void
+0x038 IDT : 0x8003f400 _KIDTENTRY
+0x03c GDT : 0x8003f000 _KGDTENTRY
+0x040 TSS : 0x80042000 _KTSS <------ TSS 結構地址
+0x044 MajorVersion : 1
+0x046 MinorVersion : 1
+0x048 SetMember : 1
+0x04c StallScaleFactor : 0x95a
+0x050 SpareUnused : 0 ''
+0x051 Number : 0 ''
+0x052 Spare0 : 0 ''
+0x053 SecondLevelCacheAssociativity : 0 ''
+0x054 VdmAlert : 0
+0x058 KernelReserved : [14] 0
+0x090 SecondLevelCacheSize : 0
+0x094 HalReserved : [16] 0
+0x0d4 InterruptMode : 0
+0x0d8 Spare1 : 0 ''
+0x0dc KernelReserved2 : [17] 0
+0x120 PrcbData : _KPRCB
我們看到這個 KTSS 結構地址在 0x80042000 裡,這個 KTSS 結構如下:
kd> dt _ktss 0x80042000
ntdll!_KTSS
+0x000 Backlink : 0xc45
+0x002 Reserved0 : 0x4d8a
+0x004 Esp0 : 0xf649bde0 <------- 0 級的 Esp 值,這指向一個 KTRAP_FRAME 結構 V86Es 成員
+0x008 Ss0 : 0x10
+0x00a Reserved1 : 0xb70f
+0x00c NotUsed1 : [4] 0x5031ff00
+0x01c CR3 : 0x8b55ff8b
+0x020 Eip : 0xc75ffec
+0x024 EFlags : 0xe80875ff
+0x028 Eax : 0xfffffbdd
+0x02c Ecx : 0x1b75c084
+0x030 Edx : 0x8b184d8b
+0x034 Ebx : 0x7d8b57d1
+0x038 Esp : 0x2e9c110
+0x03c Ebp : 0xf3ffc883
+0x040 Esi : 0x83ca8bab
+0x044 Edi : 0xaaf303e1
+0x048 Es : 0xeb5f
+0x04a Reserved2 : 0x6819
+0x04c Cs : 0x24fc
+0x04e Reserved3 : 0x44
+0x050 Ss : 0x75ff
+0x052 Reserved4 : 0xff18
+0x054 Ds : 0x1475
+0x056 Reserved5 : 0x75ff
+0x058 Fs : 0xff10
+0x05a Reserved6 : 0xc75
+0x05c Gs : 0x75ff
+0x05e Reserved7 : 0xe808
+0x060 LDT : 0
+0x062 Reserved8 : 0xffff
+0x064 Flags : 0
+0x066 IoMapBase : 0x20ac
+0x068 IoMaps : [1] _KiIoAccessMap
+0x208c IntDirectionMap : [32] "???"
KTSS 結構內的 Esp0 指向 KTRAP_FRAME 結構的 V86Es 成員,如下圖所示:
這個 Esp0 值被賦值給 esp 暫存器,那麼 KiFastCallEntry() 將會使用這個值來壓入 context 資訊,如下程式碼所示:
8088387d 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h] ; 讀取 KTSS 結構
80883883 8b6104 mov esp,dword ptr [ecx+4] ; 讀取 Esp0,Esp0 指向 KTRAP_FRAME 的 V86Es 成員
80883886 6a23 push 23h ; 壓入 HardwareSegSs 值,也就是 SS 值
esp 指向 KTRAP_FRAME 結構 V86Es,當 push 23h 時則等於將 HardwareSegSs 賦值為 23h 值。我們將在後面瞭解到 KTRAP_FRAME 結構
6.2 KTRAP_FRAME 結構
在 KiFastCallEntry() 中將 context 資訊儲存在一個被稱為 KTRAP_FRAME 的結構裡,在前面我們看到 KTRAP_FRAME 結構的地址被賦予 esp 暫存器,因此:KTRAP_FRAME 結構就是 KiFastCallEntry() 函式的 stack 區域。KTRAP_FRAME 結構如下面所示:
kd> dt _ktrap_frame 0xf649bde0-0x7c <--- KTRAP_FRAME 結構基址等於 Esp0 值減 0x7c
ntdll!_KTRAP_FRAME
+0x000 DbgEbp : 0x12fa74
+0x004 DbgEip : 0x7c95845c
+0x008 DbgArgMark : 0xbadb0d00
+0x00c DbgArgPointer : 0x12fa30
+0x010 TempSegCs : 0
+0x014 TempEsp : 0
+0x018 Dr0 : 0
+0x01c Dr1 : 0
+0x020 Dr2 : 0
+0x024 Dr3 : 0
+0x028 Dr6 : 0
+0x02c Dr7 : 0
+0x030 SegGs : 0
+0x034 SegEs : 0x23
+0x038 SegDs : 0x23
+0x03c Edx : 0xc
+0x040 Ecx : 2
+0x044 Eax : 0x12f6a0
+0x048 PreviousPreviousMode : 1
+0x04c ExceptionList : 0xffffffff _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : 0x3b
+0x054 Edi : 1
+0x058 Esi : 0
+0x05c Ebx : 0
+0x060 Ebp : 0x12fa74
+0x064 ErrCode : 0
+0x068 Eip : 0x7c95845c
+0x06c SegCs : 0x1b
+0x070 EFlags : 0x213
+0x074 HardwareEsp : 0x12fa28
+0x078 HardwareSegSs : 0x23
+0x07c V86Es : 0
+0x080 V86Ds : 0
+0x084 V86Fs : 0
+0x088 V86Gs : 0
注意:KTRAP_FRAME 結構的基址是 Esp0 值減 0x7c 而來,因為:Esp0 指向 V86Es 成員。在執行 push 23h 指令後,HardwareSegSs 就等於 23h。上面顯示的 KTRAP_FRAME 結構的內容是 KiFastCallEntry() 在已經儲存好 context 資訊後的內容,將要轉入執行真正的系統服務例程(nt!NtWriteFile)。
KiFastCallEntry() 在 KTRAP_FRAME 裡儲存下面的內容:
// // 注意:下面儲存 context 的操作是以 push 方式壓入 TrapFrame 為 esp 的棧中 // KtrapFrame->HardwareSegSs = 0x23; // 儲存原 R3 的 SS 值 KtrapFrame->HardwareEsp = edx; // edx 是原 R3 的 ESP 值 KtrapFrame->EFlags = eflags; // 儲存原 eflags 值 KtrapFrame->EFlags.IF = 1; // context 中的 eflags.IF 置位 eflags = 0; // 當前的 eflags 清為 0 // // 當前 edx 儲存著 sysenter 進入前的 esp 值 // 加上 8 後:edx 指向 native API 呼叫中的第 1 個引數 // PVOID ArgumentPointer = edx + 8; KtrapFrame->SegCs = 0x1b; // 儲存原 R3 的 CS 值 KtrapFrame->Eip = UserSharedData->SystemCallReturn; // 儲存返回函式 KtrapFrame->ErrCode = 0; // 錯誤碼為 0 KtrapFrame->Ebp = ebp; KtrapFrame->Ebx = ebx; KtrapFrame->Esi = esi; KtrapFrame->Edi = edi; KtrapFrame->SegFs = 0x3b; // 原 R3 的 FS 值 PKPCR Kpcr = (PKPCR)0xffdff000; // 也就是 fs.base KtrapFrame->ExceptionList = Kpcr->NtTib.ExceptionList; // 儲存原 SEH 鏈底 Kpcr->->NtTib.ExceptionList = -1; // 設定為空 SEH 鏈 PKTHREAD Thread = Kpcr->PrcbData.CurrentThread; // 得到當前執行緒結構 PVOID InitialStack = Thread->InitialStack; // 得到初始的 stack 地址 KtrapFrame->PreviousPreviousMode = 1; // 1 值是原 MODE_MASK 值 KtrapFrame = (PKTRAP_FRAME)((ULONG)KtrapFrame - 0x48); // 計算出 Ktrap_frame 基地址 // // 計算初始 stack 的 ktrap_frame 基址: // 這個 0x29c 值等於:NPX_FRAME_LENGTH + KTRAP_FRAME_LENGTH // NPX_FRAME_LEGNTH = 0x210 // KTRAP_FRAME_LENGTH = 0x8c // InitialStack = (PVOID)((ULONG)InitialStack - 0x29c); Thread->PreviousMode = 1; // // 假如這兩個 stack 基址值不同 // if (KtrapFrame != InitialStack) { goto 3869; } // // 此時 InitialStack 指向 KtrapFrame 基址,也就是:InitialStack == KtrapFrame // InitialStack->Dr7 = 0; // 清 Dr7 值 Thread->TrapFrame = InitialStack; // // 檢測是否需要儲存 debug context 資訊(debug 暫存器) // if (Thread->Header.DebugActive != 0xff) { goto 372c; } ebx = KtrapFrame->Ebp; // 讀取原 ebp 值 edi = KtrapFrame->Eip; // 讀取 UserSharedData->SystemCallReturn KtrapFrame->DbgArgPointer = ArgumentPointer; // native API 呼叫的第 1 個引數 KtrapFrame->DbgArgMark = 0xbadb0d00; // // 實際上:當前 KtrapFrame 值等價於當前 esp // 因此,下面兩行程式碼是構建一個標準的 call 返回流程 // 1. push UserSharedData->SystemCallReturn(返回地址) // 2. push ebp(原 stack frame base ) // // 當前: // 1. ebp == esp // 2. ebp 指向 KtrapFrame->DbgEbp 值(stack top) // KtrapFrame->DbgEbp = ebx; KtrapFrame->DbgEip = edi;
7. 系統服務例程號與 ServiceTable
KiFastCallEntry() 儲存好相關的 context 資訊後,接下來要重的一步是分析系統服務例程號以便讀取系統服務例程地址。如下圖所示:
32 位的系統服務例程號,實際只使用低 12 位,bit12 位是 index 值,用來在 Service table 裡查詢自己的系統服務例程表。
補充:這個 ServiceTable 的內容貌似就是 SDT(Service Descirptor Table)!
7.1 ServiceTable
windows 的 Service Table 來自於 KTHREAD 結構內的 ServiceTable 成員,ServiceTable 的定址是:
PKPCR Pcr = (PKPCR)0xffdff000; // 核心中的 Processor Cotnrol Region 地址為 0FFDFF000h PKTHREAD Thread = Pcr->PrcbData.CurrentThread; // 得到 kernel 中的 KTHREAD 結構 PVOID ServiceTable = Thread->ServiceTable; // 得到 KTHREAD 結構中的 ServiceTable 地址
實際上 KTHREAD 的地址在 0x880c7330 上,在 windbg 上觀察如下:
kd> dt _kthread 0x880c7330
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY [ 0x880c7340 - 0x880c7340 ]
+0x018 InitialStack : 0xf649c000 Void
+0x01c StackLimit : 0xf6495000 Void
+0x020 KernelStack : 0xf649b914 Void
+0x024 ThreadLock : 0
+0x028 ApcState : _KAPC_STATE
+0x028 ApcStateFill : [23] "Xs???"
+0x03f ApcQueueable : 0x1 ''
+0x040 NextProcessor : 0 ''
+0x041 DeferredProcessor : 0 ''
+0x042 AdjustReason : 0 ''
+0x043 AdjustIncrement : 2 ''
+0x044 ApcQueueLock : 0
+0x048 ContextSwitches : 0x14cda
+0x04c State : 0x2 ''
+0x04d NpxState : 0xa ''
+0x04e WaitIrql : 0 ''
+0x04f WaitMode : 1 ''
+0x050 WaitStatus : 0n0
+0x054 WaitBlockList : 0x880c73d8 _KWAIT_BLOCK
+0x054 GateObject : 0x880c73d8 _KGATE
+0x058 Alertable : 0x1 ''
+0x059 WaitNext : 0 ''
+0x05a WaitReason : 0x6 ''
+0x05b Priority : 12 ''
+0x05c EnableStackSwap : 0x1 ''
+0x05d SwapBusy : 0 ''
+0x05e Alerted : [2] ""
+0x060 WaitListEntry : _LIST_ENTRY [ 0xffdffb70 - 0xffdffb70 ]
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
+0x068 Queue : (null)
+0x06c WaitTime : 0x5378
+0x070 KernelApcDisable : 0n0
+0x072 SpecialApcDisable : 0n0
+0x070 CombinedApcDisable : 0
+0x074 Teb : 0x7ffdf000 Void
+0x078 Timer : _KTIMER
+0x078 TimerFill : [40] "???"
+0x0a0 AutoAlignment : 0y0
+0x0a0 DisableBoost : 0y0
+0x0a0 GuiThread : 0y0
+0x0a0 VdmSafe : 0y0
+0x0a0 ReservedFlags : 0y0000000000000000000000000000 (0)
+0x0a0 ThreadFlags : 0n0
+0x0a8 WaitBlock : [4] _KWAIT_BLOCK
+0x0a8 WaitBlockFill0 : [23] "???"
+0x0bf SystemAffinityActive : 0 ''
+0x0a8 WaitBlockFill1 : [47] "???"
+0x0d7 PreviousMode : 1 ''
+0x0a8 WaitBlockFill2 : [71] "???"
+0x0ef ResourceIndex : 0x1 ''
+0x0a8 WaitBlockFill3 : [95] "???"
+0x107 LargeStack : 0x1 ''
+0x108 QueueListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x110 TrapFrame : 0xf649bd64 _KTRAP_FRAME
+0x114 CallbackStack : (null)
+0x118 ServiceTable : 0x8089f440 Void <--- 這是 ServiceTable 地址
+0x11c ApcStateIndex : 0 ''
+0x11d IdealProcessor : 0 ''
+0x11e Preempted : 0 ''
+0x11f Proce***eadyQueue : 0 ''
+0x120 KernelStackResident : 0x1 ''
+0x121 BasePriority : 8 ''
+0x122 PriorityDecrement : 2 ''
+0x123 Saturation : 0 ''
+0x124 UserAffinity : 1
+0x128 Process : 0x87d2e958 _KPROCESS
+0x12c Affinity : 1
+0x130 ApcStatePointer : [2] 0x880c7358 _KAPC_STATE
+0x138 SavedApcState : _KAPC_STATE
+0x138 SavedApcStateFill : [23] "ht???"
+0x14f FreezeCount : 0 ''
+0x150 SuspendCount : 0 ''
+0x151 UserIdealProcessor : 0 ''
+0x152 CalloutActive : 0 ''
+0x153 Iopl : 0 ''
+0x154 Win32Thread : 0xe10b2350 Void
+0x158 StackBase : 0xf649c000 Void
+0x15c SuspendApc : _KAPC
+0x15c SuspendApcFill0 : [1] "??? 0$"
+0x15d Quantum : 32 ' '
+0x15c SuspendApcFill1 : [3] "???"
+0x15f QuantumReset : 0x24 '$'
+0x15c SuspendApcFill2 : [4] "???"
+0x160 KernelTime : 0x21e
+0x15c SuspendApcFill3 : [36] "???"
+0x180 TlsArray : (null)
+0x15c SuspendApcFill4 : [40] "???"
+0x184 LegoData : (null)
+0x15c SuspendApcFill5 : [47] "???"
+0x18b PowerState : 0 ''
+0x18c UserTime : 0x133
+0x190 SuspendSemaphore : _KSEMAPHORE
+0x190 SuspendSemaphorefill : [20] "???"
+0x1a4 SListFaultCount : 0
+0x1a8 ThreadListEntry : _LIST_ENTRY [ 0x88204888 - 0x87d2e9a8 ]
+0x1b0 SListFaultAddress : (null)
7.2 ServiceTable entry
由 index 值在 ServiceTable 裡定位 Service Table entry 結構,它是 16 位元組大,它看起來包括:
- ServiceRoutineTable:提供真正的系統服務例程的地址
- unknow 未知的元素
- MaxServiceNumber:最大的系統服務例程號
- ArgumentSizeTable:提供每個例程所需要的引數大小,這個值將要用來從 caller 裡複製多少個引數。
當 index = 1 時,是指向 GUI 類的系統服務例程表,它們在 GUI 類系統核心驅動模組 win32k.sys 模組。例如:win32k!NtGdiBitBlt() 例程,index = 0 時,使用普通的系統例程表。
在 windows service 2003 系統上,兩個系統服務例程表的 MaxServiceNumber 值分別是:
- index = 0:MaxServiceNumber 為 0x128
- index = 1:MaxServiceNumber 為 0x299
我們可以在 windbg 裡檢視這些 entry 值是多少,如下所示:
kd> dd 0x8089f440
8089f440 80830f84 00000000 00000128 80831428 ; index = 0
8089f450 bf9a7000 00000000 00000299 bf9a7d08 ; index = 1
下面的情況下是屬於超出例程號:
if (ServiceNumber >= ServceTableEntry->MaxServiceNumber) { goto nt!KiBBTUnexpectedRange (80883662) }
當提供的服務例程號大小等於 MaxServiceNumber 時就屬於超限!因此:普通的系統服務例程號最大為 0x127 號,而 GUI 類例程號最大為 0x298 號
7.3 讀取目標例程地址和引數 size
根據 ServiceNumber 號在 ServiceRoutineTable 裡找到最終的系統服務例程地址值,例如 WriteFile() 的服務例程號是 0x11c,那麼將在地址[ServiceRoutineTable + 0x11c * 4] 的位置上就 nt 模組的 WriteFile() 地址。
而 ArgumentSize 值被讀取後,用它來複制引數在 KiFastCallEntry() 的棧上:
80883949 8a0c18 mov cl,byte ptr [eax+ebx] ; 讀取 ArgumentSize 值
8088394c 8b3f mov edi,dword ptr [edi] ; 讀取 ServiceRoutineTable
8088394e 8b1c87 mov ebx,dword ptr [edi+eax*4] ; 讀取服務例程地址
80883951 2be1 sub esp,ecx ; 在當前棧上開闢空間容納引數
80883953 c1e902 shr ecx,2
80883956 8bfc mov edi,esp
80883958 3b35e8588980 cmp esi,dword ptr [nt!MmUserProbeAddress (808958e8)] ; 是否屬於使用者空間
8088395e 0f83f0010000 jae nt!KiSystemCallExit3+0x90 (80883b54)
nt!KiFastCallEntry+0xf4:
80883964 f3a5 rep movs dword ptr es:[edi],dword ptr [esi] ; 複製引數到當前棧上
80883966 ffd3 call ebx ; 呼叫最終的服務例程
在複製前,KiFastCallEntry() 還會判斷 caller 的地址是否屬於使用者空間,當大於等於 MmUserProbeAddress 值時,屬於核心空間,那麼會進行另外的處理。
這個 MmUserProbeAddress 值為 0x7FFF0000,它是使用者空間最大可用的地址值。在複製完引數後,緊接著就呼叫最終的系統服務例程!
8. KiFastCallEntry() 的返回處理
KiFastCallEntry() 的返回處理很複雜,需要檢測多種情況,主要是檢查出呼叫者屬於 0 級,還是 3 級情況下。
之所以需要過多的檢測,是因為進入 KiFastCallEntry() 的途徑可能有多種,下面我們看看返回前的一些處理。
8.1 檢查 APC 和提交 APC
// // 假如呼叫者是 ring 3 並且需要檢查 APC // if ((KiEnableApcCheck & 0x01) && (KtrapFrame->SegCs.RPL == 3)) { KIRQL Irql = KeGetCurrentIrql(); // // 如果當前 Irql 不是 PASSIVE_LEVEL 級別:丟擲 BugCheck 錯誤! // if (Irql != PASSIVE_LEVEL) { Kpcr->Irql = PASSIVE_LEVEL; KeBugCheck2(0x4A, NtWriteFile, Irql, 0, 0, InitialStack); } Thread = Kpcr->CurrentThread; // // 檢查 process 是否 attached ? // 或者 APCs 是否被 disable ? // 如果是的話:丟擲 BugCheck 錯誤! // if ((Thread->ApcStateIndex & 0xff) || (Thread->CombinedApcDisable != 0)) { KeBugCheck2(1, NtWriteFile, Thread->ApcStateIndex, Thread->CombinedApcDisable, 0, InitialStack); } } // // 恢復 stack frame // esp = ebp; PKTRAP_FRAME OldTrapFrame = KtrapFrame->Edx; // 找到原 esp 值(進入 sysenter 之前) Thread->TrapFrame = OldTrapFrame; // 儲存在 Thread 的 TrapFrame 域裡 cli(); if ((KtrapFrame->EFlags.VM == 1) || (KtrapFrame->SegCs.RPL == 3)) { Thread->Alerted = 0; while (Thread->ApcStateFill.AsUserApcPending != 0) { KtrapFrame->Eax = eax; // 儲存返回值 KtrapFrame->SegFs = 0x3b; KtrapFrame->SegDs = 0x23; KtrapFrame->SegEs = 0x23; KtrapFrame->SegGs = 0; // // 下面程式碼將 IRQL 提升到 APC_LEVEL 級別 // 然後提交 APC 排隊處理(需要開中斷) // 完成後恢復原 IRQL 級別並關閉中斷許可 // OldIrql = KfRaiseIrql(APC_LEVEL); sti(); KiDeliverApc(1, 0, KtrapFrame); // 提交 APC 處理 KfLowerIrql(OldIrql); cli(); Thread->Alerted = 0; } }
當呼叫者是 user 層的話,如果需要檢查 APC,則檢查:
- 當前的 IRQL 必須在 PASSIVE_LEVEL 級別,否則會引發 BugCheck(死亡藍屏)。
- 檢查 process 是否 attached,或者 kernel APCs 是否被 disable 掉,這兩種情況都會引發 BugCheck!
並且當有 APC 在 pending 狀態時,需要提交完所有的 APC 進行處理。
8.2 檢查是否開啟除錯功能
if (KtrapFrame->Dr7 & 0xffff23ff) { // // 如果開啟了 DR7 除錯功能 // if ((KtrapFrame->EFlags.VM == 1) || (KtrapFrame->SegCs.RPL == 3)) { dr7 = 0; // 先關閉除錯功能 dr0 = KtrapFrame->Dr0; dr1 = KtrapFrame->Dr1; dr2 = KtrapFrame->Dr2; dr3 = KtrapFrame->Dr3; dr6 = KtrapFrame->Dr6; dr7 = KtrapFrame->Dr7; } }
如果是的話:恢復原來的 debug 暫存器值。
8.3 恢復部分 context 資訊
if (KtrapFrame->EFlags.VM == 1) { // // 如果開啟了 V8086 模式 // edx = KtrapFrame->Edx; ecx = KtrapFrame->Ecx; eax = KtrapFrame->Eax; } else if (KtrapFrame->SegCs & 0xFFF9 == 0) { // // 如果 CS selector 為 0 值(0級下的 NULL selector) // KtrapFrame->SegCs = KtrapFrame->TempSegCs; // // ErrCode 指向構造的 TempStack 結構 // PVOID TempStack = KtrapFrame->TempEsp - 0x0c; KtrapFrame->ErrCode = TempStack; // // 下面構造一個 stack 結構(TempStack)以便使用 iretd 指令返回: // // Eflags // SegCs // esp ------> Eip // TempStack->Eflags = KtrapFrame->EFlags; TempStack->SegCs = KtrapFrame->SegCs; TempStack->Eip = KtrapFrame->Eip; edi = KtrapFrame->Edi; esi = KtrapFrame->Esi; ebx = KtrapFrame->Ebx; ebp = KtrapFrame->Ebp; // // 基於構造的 TempStack 來中斷返回 // esp = &TempStack; iretd; } else if (KtrapFrame->SegCs.RPL == 3) { // // 恢復 ring 3 的暫存器值 // eax = KtrapFrame->Eax; edx = KtrapFrame->Edx; ecx = KtrapFrame->Ecx; gs = KtrapFrame->SegGs; es = KtrapFrame->SegEs; ds = KtrapFrame->SegDs; fs = KtrapFrame->SegFs; } else if (KtrapFrame->SegCs != 8) { fs = KtrapFrame->SegFs; }
上面的程式碼中,其中一項:KtrapFrame->SegCs & 0xFFF9 == 0,意圖是檢查呼叫者的 CS 是否為 0,因為 windows 只使用 0 和 3 級的許可權執行級別。
這段程式碼比較奇怪,注意,我不能完全理解它的用意!它在 ErrCode 的地方,構造一個 32 位寬的 far pointer,用來載入到 16 位的 SP 暫存器和 SS 暫存器。我想是為了返回到 16 位程式碼
8.4 完成一個典型的中斷呼叫 stack 結構
在返回前,經過一系列的 pop 操作之後,形成一個典型的中斷呼叫棧結構:
//
// 現在:esp 指向 KtrapFrame->Edi 域
// 下一步工作是:pop 出相關的值
//
esp = &KtrapFrame->Edi;
//
// 下面恢復原部分暫存器 context 資訊
// 也就是執行:
// pop edi ----> 此時 esp 指向 edi 儲存的地址
// pop esi
// pop ebx
// pop ebp
//
edi = KtrapFrame->Edi;
esi = KtrapFrame->Esi;
ebx = KtrapFrame->Ebx;
ebp = KtrapFrame->Ebp;
//
// 此時 stack frame 內的值為:
//
// esp ----> KtrapFrame->ErrCode
// KtrapFrame->Eip
// KtrapFrame->SegCs
// KtrapFrame->EFlags
// ktrapFrame->HardwareEsp
// KtrapFrame->HardwareSegSs
//
// 這是一個標準的呼叫中斷 handler 入棧的情形,esp 指向 ErrorCode
//
它形成的 stack 結構如下圖所示:
同樣這個 stack 結構是基於 KtrapFrame 來構造的,因為此時 esp 指向 KtrapFrame 結構的 ErrCode 成員。形成這樣一個典型的中斷呼叫 stack 目的是:出於另一個呼叫途徑可以使用 iret 指令來執行中斷返回!
8.5 根據呼叫者的許可權級別來決定如何返回
最後,KiFastCallEntry() 根據呼叫者是 Ring0 還是 Ring3 來使用何種方式返回。
- 當屬於 ring0 時:
//
// 下面進行判斷兩種情形:
// 1. 當呼叫者屬於 Ring 0 時,直接使用 jmp 指令返回到目標返回地址
// 2. 當呼叫者屬於 Ring 3 時,使用 sysexit 指令返回
//
esp = esp + 4; // 跳過 ErrCode
if (KtrapFrame->SegCs.RPL == 0)
{
//
// 屬於 ring0 的呼叫,下面操作是:手動銷棧,讀出 Eip 值到 edx 暫存器
// 等價於下面的操作:
// pop edx
// pop ecx
// popfd
// jmp edx
//
edx = KtrapFrame->Eip; // 得到返回地址
ecx = KtrapFrame->SegCs; // SegCs 值
eflags = KtrapFrame->Eflags; // pop 出 eflags 暫存器
//
// 跳轉到 edx 地址上,也就是跳到:ntdll!KiFastSystemCallRet() 例程
// 這個 ntdll!KiFastSystemCallRet() 例程只有一條 ret 指令
// 通過這種方式返回到 API 的呼叫者(而非執行 sysexit 指令)
//
jmp edx
}返回到 ring0 時,分別主動 pop 出 Eip,SegCs 以及 Eflags 值到 edx,ecx 以及 eflags 暫存器,然後使用 jmp edx 指令直接跳轉到返回地址上。當然在 pop 操作之前需要先跳過 ErrCode 碼。
顯然這種情形發生在:在 ring0 裡通過 KiFastCallEntry() 來分發系統服務例程時使用。
- 當屬於 User 層時:
else
{
//
// 當呼叫者是使用者程式碼時,返回的地址屬於 3 級的使用者層程式碼
//
if (KtrapFrame->EFlags.TF == 1)
{
//
// 如果開啟單步除錯,則使用 iretd 指令返回
//
iretd;
}
else
{
//
// 下面的處理,目的是:
// 1. 銷掉部分 stack
// 2. 找到返回地址到 EDX 暫存器
// 3. 找到返回的 esp 值到 ECX 暫存器
// 4. 使用 sysexit 指令返回
//
edx = KtrapFrame->Eip; // pop 出 EIP 值
KtrapFrame->EFlags.IF = 0; // 清掉 stack 的中 IF 標誌位
eflags = KtrapFrame->EFlags; // pop 出 eflags 值
ecx = ktrapFrame->HardwareEsp; // pop 出 Esp 值到
sti(); // 返回前開啟中斷
sysexit; // 執行 sysexit 指令返回
}
}在返回 User 層裡,當呼叫者的 Eflags.TF = 1 時(開啟了單步除錯)時,直接使用 iretd 指令返回!
最後返回正常途徑下的 User 層時這是一個典型地通過 sysexit 指令返回的情形:將 pop 出 Eip 值到 edx 暫存器,pop 出 EFlags 值到 eflags 暫存器,還有 pop 出 Esp 值到 ecx 暫存器,構造一個 sysexit 指令的執行環境,通過 sysexit 指令返回!
至此:我對 KiFastCallEntry() 的大致流程分析完畢。
後記:事實上可能會多種途徑通過 KiFastCallEntry() 來分發系統服務例程,以及返回呼叫者,這裡的分析只是基於一種常用的使用途徑。這裡的分析並不代表全部!敬請留意。
轉載地址:http://www.mouseos.com/windows/kernel/KiFastCallEntry.html
轉載於:https://blog.51cto.com/whatday/1382202