(59)逆向分析 KiSwapContext 和 SwapContext —— 线程切换核心代码

一、前言

在前面的课程中,我们研究了模拟线程切换的代码,学习了 _KPCR,ETHREAD,EPROCESS 等内核结构体,这些都是为了学习Windows线程切换做的准备。

线程切换是操作系统的核心内容,几乎所有的内核API都会调用切换线程的函数。这次课我们就来逆向 KiSwapContext 和 SwapContext 这两个函数,看看Windows是怎么切换线程的。

我们要带着问题开始逆向:

  1. SwapContext 有几个参数,分别是什么?
  2. SwapContext 在哪里实现了线程切换
  3. 线程切换的时候,会切换CR3吗?切换CR3的条件是什么?
  4. 中断门提权时,CPU会从TSS得到ESP0和SS0,TSS中存储的一定是当前线程的ESP0和SS0吗?如何做到的?
  5. FS:[0]在3环指向TEB,但是线程有很多,FS:[0]指向的是哪个线程的TEB,如何做到的?
  6. 0环的 ExceptionList 在哪里备份的?
  7. IdleThread是什么?什么时候执行?找到这个函数.
  8. 如何找到下一个就绪线程?
  9. 模拟线程切换与Windows线程切换有哪些区别?

其中,问题 7,8 的答案暂时无法解答,因为相关的操作不在这两个函数里,我会在下一篇博客通过分析 KiSwapThread 函数来解答这些问题。

二、分析 KiSwapContext

这个函数调用了 SwapContext,我们通过逆它可以判断出 SwapContext 有几个参数。

KiSwapContext 做的工作是保存旧线程的寄存器到自己的栈顶,更新 KPCR 里的 CurrentThread 属性,然后调用 SwapContext 函数切换线程,SwapContext 返回后就已经完成线程切换的工作了。

所以说 KiSwapContext 函数做的事情其实不多,我们分析它主要是看看 SwapContext 接收了几个参数。

我这里已经分析完了,有3个参数:

ebx: _KPCR
esi: 新线程 _ETHREAD
edi: 旧线程 _ETHREAD

.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个,均通过寄存器保存
.text:0046A80D                                         ; ebx: _KPCR
.text:0046A80D                                         ; esi: 新线程 _ETHREAD
.text:0046A80D                                         ; edi: 旧线程 _ETHREAD
.text:0046A80D                                         ; cl: 旧线程的 WaitIrql,这个参数用来控制是否执行APC
.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 endp

三、分析 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_46A994

4. 中断门提权时,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环栈顶 ESP0

5. 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_46A9BD

6. 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 endp

SwapContext

.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

版权声明:本文为Kwansy原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。