(59)逆向分析 KiSwapContext 和 SwapContext —— 线程切换核心代码
一、前言
在前面的課程中,我們研究了模擬線程切換的代碼,學習了 _KPCR,ETHREAD,EPROCESS 等內核結構體,這些都是為了學習Windows線程切換做的準備。
線程切換是操作系統的核心內容,幾乎所有的內核API都會調用切換線程的函數。這次課我們就來逆向 KiSwapContext 和 SwapContext 這兩個函數,看看Windows是怎么切換線程的。
我們要帶著問題開始逆向:
其中,問題 7,8 的答案暫時無法解答,因為相關的操作不在這兩個函數里,我會在下一篇博客通過分析 KiSwapThread 函數來解答這些問題。
二、分析 KiSwapContext
這個函數調用了 SwapContext,我們通過逆它可以判斷出 SwapContext 有幾個參數。
KiSwapContext 做的工作是保存舊線程的寄存器到自己的棧頂,更新 KPCR 里的 CurrentThread 屬性,然后調用 SwapContext 函數切換線程,SwapContext 返回后就已經完成線程切換的工作了。
所以說 KiSwapContext 函數做的事情其實不多,我們分析它主要是看看 SwapContext 接收了幾個參數。
我這里已經分析完了,有3個參數:
ebx: _KPCR
esi: 新線程 _ETHREAD
edi: 舊線程 _ETHREAD
三、分析 SwapContext
這個函數是切換線程最終發生的地方,代碼也比較長,我也不是每一句都看懂了,所以要跟著問題分析。我最后再貼出完整的注釋。
2. SwapContext 在哪里實現了線程切換?
找給 esp 賦值的語句就是了。
mov esp, [esi+_ETHREAD.Tcb.KernelStack] ; 此處是切換線程,切換線程本質是切換堆棧3. 線程切換的時候,會切換CR3嗎?切換CR3的條件是什么?
如果新舊線程屬于同一個進程,就不換 cr3,;否則就要換。
判斷是否屬于同一進程的代碼:
mov eax, [edi+_ETHREAD.Tcb.ApcState.Process] ;; 通常情況下,ApcState.Process 和 _ETHREAD.ThreadsProcess 是同一個; 但是當A進程調用API訪問B進程的內存時,ApcState.Process 存的就是B進程 cmp eax, [esi+_ETHREAD.Tcb.ApcState.Process] mov [edi+_ETHREAD.Tcb.IdleSwapBlock], 0 jz short loc_46A994 ; 如果是同一個進程內的線程切換,就跳轉;; 如果不是同一個進程的,那么就要做額外的工作,主要就是切換CR3切換 cr3 的代碼:
loc_46A975: ; 修改 LDT 寄存器 lldt ax xor eax, eax mov gs, eax ; gs 寄存器清零; 這就是 Windows 不使用 gs 的依據 assume gs:GAP mov eax, [edi+_EPROCESS.Pcb.DirectoryTableBase] mov ebp, [ebx+_KPCR.TSS] mov ecx, dword ptr [edi+_EPROCESS.Pcb.IopmOffset] mov [ebp+TSS.CR3], eax mov cr3, eax ; 關鍵步驟:切換 cr3 mov [ebp+TSS.IOMap], cx jmp short loc_46A9944. 中斷門提權時,CPU會從TSS得到ESP0和SS0,TSS中存儲的一定是當前線程的ESP0和SS0嗎?如何做到的?
往 _KPCR.TSS 存 ESP0 的代碼就在線程切換上面幾句,但是并沒有存 SS0 的代碼,因為所有線程的 SS0 的值是固定不變的,系統啟動時已經填到 TSS 里,不需要在這里改了。
.text:0046A940 loc_46A940: ; CODE XREF: SwapContext+11F↓j .text:0046A940 test dword ptr [eax-1Ch], 20000h ; SegCs & 20000h .text:0046A940 ; 判斷是否是虛擬8086模式,如果不是,直接減掉 .text:0046A940 ; +0x07c V86Es : Uint4B .text:0046A940 ; +0x080 V86Ds : Uint4B .text:0046A940 ; +0x084 V86Fs : Uint4B .text:0046A940 ; +0x088 V86Gs : Uint4B .text:0046A940 ; .text:0046A940 ; 如果是,那么就不減 .text:0046A940 ; .text:0046A940 ; 這樣做了之后,eax 就指向了0環棧頂,接下來就會存儲到 TSS 里 .text:0046A940 ; 以后這個線程進0環,不論是中斷門還是快速調用,都會從 TSS 里獲取 ESP0 .text:0046A947 jnz short loc_46A94C .text:0046A949 sub eax, 10h .text:0046A94C .text:0046A94C loc_46A94C: ; CODE XREF: SwapContext+67↑j .text:0046A94C mov ecx, [ebx+_KPCR.TSS] ; .text:0046A94C ; ecx 指向 TSS .text:0046A94C ; TSS 的用途是3環進0環時,要從 TSS 取 SS0 和 ESP0 .text:0046A94F mov [ecx+_KTSS.Esp0], eax ; 更新 TSS 中存儲的0環棧頂 ESP05. FS:[0]在3環指向TEB,但是線程有很多,FS:[0]指向的是哪個線程的TEB,如何做到的?
loc_46A94C: ; mov ecx, [ebx+_KPCR.TSS] ; ecx 指向 TSS; TSS 的用途是3環進0環時,要從 TSS 取 SS0 和 ESP0 mov [ecx+TSS.ESP0], eax ; 更新 TSS 中存儲的0環棧頂 ESP0 mov esp, [esi+_ETHREAD.Tcb.KernelStack] ; 此處是切換線程,切換線程本質是切換堆棧; 將 esp 修改為新線程的棧頂,然后就可以從堆棧里取數據恢復現場了 mov eax, [esi+_ETHREAD.Tcb.Teb] mov [ebx+_KPCR.NtTib.Self], eax ; 暫時存儲 TEB 到 ffdff000這里把新線程的 TEB 暫存到 ffdff000,在 SwapContext 快結束的地方又取了出來,填充了 GDT表 0x3B 對應那項的基址,因為3環FS的選擇子就是 0x3B,所以這樣3環才能通過 FS 找到當前線程的 TEB:
loc_46A994: ; mov eax, [ebx+_KPCR.NtTib.Self] ; 此時 eax 指向了 TEB mov ecx, [ebx+_KPCR.GDT] ; 假設 GDT表在 0x8003f000; ecx = 0x8003f000; 3環 FS = 0x3B; 所以 FS 在 GDT表里的地址是 0x8003f03B; 下面的操作是修改 FS 的段描述符,這樣3環 FS 就能找到 TEB 了; ; mov [ecx+3Ah], ax ; BaseAddress 15:00 shr eax, 10h ; eax 指向 TEB 的地址高16位 mov [ecx+3Ch], al ; BaseAddress 23:16 mov [ecx+3Fh], ah ; BaseAddress 31:24 inc [esi+_ETHREAD.Tcb.ContextSwitches] inc [ebx+_KPCR.PrcbData.KeContextSwitches] pop ecx mov [ebx], ecx cmp [esi+_ETHREAD.Tcb.ApcState.KernelApcPending], 0 jnz short loc_46A9BD6. 0環的 ExceptionList 在哪里備份的?
在 SwapContext 開頭附近保存的,從 _KPCR 里取出來,存到舊線程的棧頂了。
loc_46A8E8: ; mov ecx, [ebx+_KPCR.NtTib.ExceptionList] ; 保存本線程切換時的內核seh鏈表 cmp [ebx+_KPCR.PrcbData.DpcRoutineActive], 0 ; 是否有DPC,有就藍屏 push ecx jnz loc_46AA2D四、完整的逆向注釋
KiSwapContext
.text:0046A7E4 ; __fastcall KiSwapContext(x) .text:0046A7E4 @KiSwapContext@4 proc near ; CODE XREF: KiSwapThread()+41↑p .text:0046A7E4 .text:0046A7E4 var_10 = dword ptr -10h .text:0046A7E4 var_C = dword ptr -0Ch .text:0046A7E4 var_8 = dword ptr -8 .text:0046A7E4 var_4 = dword ptr -4 .text:0046A7E4 .text:0046A7E4 sub esp, 10h ; 使用寄存器傳參,因此要將使用到的寄存器暫時保存到堆棧中 .text:0046A7E4 ; 這里和 push 是等效的 .text:0046A7E7 mov [esp+10h+var_4], ebx .text:0046A7EB mov [esp+10h+var_8], esi .text:0046A7EF mov [esp+10h+var_C], edi .text:0046A7F3 mov [esp+10h+var_10], ebp ; ebp 沒用 .text:0046A7F6 mov ebx, ds:0FFDFF01Ch ; _KPCR.Self .text:0046A7FC mov esi, ecx ; ecx:新線程的 _ETHREAD .text:0046A7FE mov edi, [ebx+_KPCR.PrcbData.CurrentThread] ; edi:當前線程的 _ETHREAD .text:0046A804 mov [ebx+_KPCR.PrcbData.CurrentThread], esi ; 修改 _KPCR,更新當前線程 .text:0046A80A mov cl, [edi+_ETHREAD.Tcb.WaitIrql] .text:0046A80D call SwapContext ; 參數有4個,但實際使用的只有3個,均通過寄存器保存 .text:0046A80D ; ebx: _KPCR .text:0046A80D ; esi: 新線程 _ETHREAD .text:0046A80D ; edi: 舊線程 _ETHREAD .text:0046A80D ; cl: 舊線程的 WaitIrql,這個參數沒用,一進去 eax 就被覆蓋了 .text:0046A80D ; .text:0046A80D ; 調用 SwapContext 后,已經完成了線程切換 .text:0046A80D ; 后面就是新線程從它自己的堆棧里恢復寄存器的值的過程 .text:0046A812 mov ebp, [esp+10h+var_10] .text:0046A815 mov edi, [esp+10h+var_C] .text:0046A819 mov esi, [esp+10h+var_8] .text:0046A81D mov ebx, [esp+10h+var_4] .text:0046A821 add esp, 10h .text:0046A824 retn .text:0046A824 @KiSwapContext@4 endpSwapContext
.text:0046A8E0 ; 參數有4個,均通過寄存器保存 .text:0046A8E0 ; ebx: _KPCR .text:0046A8E0 ; esi: 新線程 _ETHREAD .text:0046A8E0 ; edi: 舊線程 _ETHREAD .text:0046A8E0 ; cl: 舊線程的 WaitIrql,貌似用不到,直接覆蓋了 .text:0046A8E0 .text:0046A8E0 SwapContext proc near ; CODE XREF: KiUnlockDispatcherDatabase(x)+72↑p .text:0046A8E0 ; KiSwapContext(x)+29↑p ... .text:0046A8E0 or cl, cl .text:0046A8E2 mov es:[esi+_ETHREAD.Tcb.State], 2 ; 修改新線程狀態為 2 .text:0046A8E2 ; 1 就緒 .text:0046A8E2 ; 2 運行 .text:0046A8E2 ; 5 等待 .text:0046A8E7 pushf .text:0046A8E8 .text:0046A8E8 loc_46A8E8: ; CODE XREF: KiIdleLoop()+5A↓j .text:0046A8E8 mov ecx, [ebx+_KPCR.NtTib.ExceptionList] ; .text:0046A8E8 ; 保存本線程切換時的內核seh鏈表 .text:0046A8EA cmp [ebx+_KPCR.PrcbData.DpcRoutineActive], 0 ; 是否有DPC,有就藍屏 .text:0046A8F1 push ecx .text:0046A8F2 jnz loc_46AA2D .text:0046A8F8 cmp ds:_PPerfGlobalGroupMask, 0 .text:0046A8FF jnz loc_46AA04 .text:0046A905 .text:0046A905 loc_46A905: ; CODE XREF: SwapContext+12C↓j .text:0046A905 ; SwapContext+13D↓j ... .text:0046A905 mov ebp, cr0 ; cr0 控制寄存器可以判斷當前環境是實模式還是保護模式,是否開啟分頁模式,寫保護 .text:0046A908 mov edx, ebp ; edx = ebp = cr0 .text:0046A90A mov cl, [esi+_ETHREAD.Tcb.DebugActive] .text:0046A90D mov [ebx+_KPCR.DebugActive], cl ; 更新 _KPCR 中當前線程的調試狀態位,此時存的是新線程的值 .text:0046A910 cli ; 屏蔽時鐘中斷 .text:0046A911 mov [edi+_ETHREAD.Tcb.KernelStack], esp .text:0046A914 mov eax, [esi+_ETHREAD.Tcb.InitialStack] .text:0046A917 mov ecx, [esi+_ETHREAD.Tcb.StackLimit] .text:0046A91A sub eax, 210h ; 線程堆棧的前 0x210 字節是浮點寄存器 .text:0046A91A ; 此時 eax 指向 _KTRAP_FRAME.V86Gs .text:0046A91F mov [ebx+_KPCR.NtTib.StackLimit], ecx .text:0046A922 mov [ebx+_KPCR.NtTib.StackBase], eax .text:0046A925 xor ecx, ecx .text:0046A927 mov cl, [esi+_ETHREAD.Tcb.NpxState] .text:0046A92A and edx, 0FFFFFFF1h ; 判斷 NpxState 是否支持浮點 .text:0046A92A ; .text:0046A92A ; 根據判斷結果決定是否更新 cr0 .text:0046A92D or ecx, edx .text:0046A92F or ecx, [eax+20Ch] .text:0046A935 cmp ebp, ecx .text:0046A937 jnz loc_46A9FC .text:0046A93D lea ecx, [ecx+0] .text:0046A940 .text:0046A940 loc_46A940: ; CODE XREF: SwapContext+11F↓j .text:0046A940 test dword ptr [eax-1Ch], 20000h ; SegCs & 20000h .text:0046A940 ; 判斷是否是虛擬8086模式,如果不是,直接減掉 .text:0046A940 ; +0x07c V86Es : Uint4B .text:0046A940 ; +0x080 V86Ds : Uint4B .text:0046A940 ; +0x084 V86Fs : Uint4B .text:0046A940 ; +0x088 V86Gs : Uint4B .text:0046A940 ; .text:0046A940 ; 如果是,那么就不減 .text:0046A940 ; .text:0046A940 ; 這樣做了之后,eax 就指向了0環棧頂,接下來就會存儲到 TSS 里 .text:0046A940 ; 以后這個線程進0環,不論是中斷門還是快速調用,都會從 TSS 里獲取 ESP0 .text:0046A947 jnz short loc_46A94C .text:0046A949 sub eax, 10h .text:0046A94C .text:0046A94C loc_46A94C: ; CODE XREF: SwapContext+67↑j .text:0046A94C mov ecx, [ebx+_KPCR.TSS] ; .text:0046A94C ; ecx 指向 TSS .text:0046A94C ; TSS 的用途是3環進0環時,要從 TSS 取 SS0 和 ESP0 .text:0046A94F mov [ecx+TSS.ESP0], eax ; 更新 TSS 中存儲的0環棧頂 ESP0 .text:0046A952 mov esp, [esi+_ETHREAD.Tcb.KernelStack] ; 此處是切換線程,切換線程本質是切換堆棧 .text:0046A952 ; 將 esp 修改為新線程的棧頂,然后就可以從堆棧里取數據恢復現場了 .text:0046A955 mov eax, [esi+_ETHREAD.Tcb.Teb] .text:0046A958 mov [ebx+_KPCR.NtTib.Self], eax ; 暫時存儲 TEB 到 ffdff000 .text:0046A95B sti .text:0046A95C mov eax, [edi+_ETHREAD.Tcb.ApcState.Process] .text:0046A95F cmp eax, [esi+_ETHREAD.Tcb.ApcState.Process] .text:0046A962 mov [edi+_ETHREAD.Tcb.IdleSwapBlock], 0 .text:0046A966 jz short loc_46A994 ; 如果是同一個進程內的線程切換,就跳轉 .text:0046A966 ; .text:0046A966 ; 如果不是同一個進程的,那么就要做額外的工作,主要就是切換CR3 .text:0046A968 mov edi, [esi+_ETHREAD.Tcb.ApcState.Process] ; edi: 新線程所屬進程 .text:0046A96B test [edi+_EPROCESS.Pcb.LdtDescriptor.LimitLow], 0FFFFh ; 判斷 LDT .text:0046A971 jnz short loc_46A9CE .text:0046A973 xor eax, eax .text:0046A975 .text:0046A975 loc_46A975: ; CODE XREF: SwapContext+117↓j .text:0046A975 lldt ax ; 修改 LDT 寄存器 .text:0046A978 xor eax, eax .text:0046A97A mov gs, eax ; gs 寄存器清零 .text:0046A97A ; 這就是 Windows 不使用 gs 的依據 .text:0046A97C assume gs:GAP .text:0046A97C mov eax, [edi+_EPROCESS.Pcb.DirectoryTableBase] .text:0046A97F mov ebp, [ebx+_KPCR.TSS] .text:0046A982 mov ecx, dword ptr [edi+_EPROCESS.Pcb.IopmOffset] .text:0046A985 mov [ebp+TSS.CR3], eax .text:0046A988 mov cr3, eax ; 關鍵步驟:切換 cr3 .text:0046A98B mov [ebp+TSS.IOMap], cx .text:0046A98F jmp short loc_46A994 .text:0046A98F ; --------------------------------------------------------------------------- .text:0046A991 align 4 .text:0046A994 .text:0046A994 loc_46A994: ; CODE XREF: SwapContext+86↑j .text:0046A994 ; SwapContext+AF↑j .text:0046A994 mov eax, [ebx+_KPCR.NtTib.Self] ; .text:0046A994 ; 此時 eax 指向了 TEB .text:0046A997 mov ecx, [ebx+_KPCR.GDT] ; 假設 GDT表在 0x8003f000 .text:0046A997 ; ecx = 0x8003f000 .text:0046A997 ; 3環 FS = 0x3B .text:0046A997 ; 所以 FS 在 GDT表里的地址是 0x8003f03B .text:0046A997 ; 下面的操作是修改 FS 的段描述符,這樣3環 FS 就能找到 TEB 了 .text:0046A997 ; ; .text:0046A99A mov [ecx+3Ah], ax ; BaseAddress 15:00 .text:0046A99E shr eax, 10h ; eax 指向 TEB 的地址高16位 .text:0046A9A1 mov [ecx+3Ch], al ; BaseAddress 23:16 .text:0046A9A4 mov [ecx+3Fh], ah ; BaseAddress 31:24 .text:0046A9A7 inc [esi+_ETHREAD.Tcb.ContextSwitches] .text:0046A9AA inc [ebx+_KPCR.PrcbData.KeContextSwitches] .text:0046A9B0 pop ecx .text:0046A9B1 mov [ebx], ecx .text:0046A9B3 cmp [esi+_ETHREAD.Tcb.ApcState.KernelApcPending], 0 .text:0046A9B7 jnz short loc_46A9BD .text:0046A9B9 popf .text:0046A9BA xor eax, eax .text:0046A9BC retn總結
以上是生活随笔為你收集整理的(59)逆向分析 KiSwapContext 和 SwapContext —— 线程切换核心代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (58)模拟线程切换——添加挂起、恢复线
- 下一篇: (60)逆向分析 KiSwapThrea