x64内核实验7-线程

x64内核实验7-线程

TOC
线程是比较重要的内核结构,思考一下其实可以想到线程结构体在64位下的变化应该不会很大最多只是扩充了一些内容,因为从我们之前分析段页时候会发现cpu更新的这些内容大部分不影响xp时候的线程切换机制,下面我们来验证一下

线程结构体介绍

ETHREAD和KPCR都有点大就不全贴出来了只说一些常用的字段,一般熟悉了内核机制的话看名字很多都能猜出来

KTHREAD + 0x000     struct _DISPATCHER_HEADER Header;   跟之前的进程结构体一样是可等待对象都有的头部结构体
KTHREAD + 0x018    VOID*        SListFaultAddress   上一次用户模式互锁单链表POP操作发生页面错误的地址。
KTHREAD + 0x028     VOID*        InitialStack;            内核栈的原始栈位置(高地址)
KTHREAD + 0x030     VOID*        StackLimit;             内核栈低地址
KTHREAD + 0x038    VOID*        StackBase;              内核栈的栈基址
KTHREAD + 0x058    VOID*        KernelStack;            内核调用栈开始位置
KTHREAD + 0x0C8     INT64        WaitStatus            等待的结果状态
KTHREAD + 0x0F0     VOID*        Teb                         三环使用的线程环境块
----------------------------------Apc相关的后面说Apc时候会讲
KTHREAD + 0x098      ApcState         _KAPC_STATE        ApcState结构体
KTHREAD + 0x258     SavedApcState   KAPC_STATE    备份ApcState结构体
KTHREAD + 0x24a     ApcStateIndex    UChar                索引ApcState时候用的
----------------------------------Apc相关的后面说Apc时候会讲
KTHREAD + 0x184     State             UChar                    线程当前状态
KTHREAD + 0x090     TrapFrame    _KTRAP_FRAME            指向Trap_Frame结构体
KTHREAD + 0x232     PreviousMode   Char                存储了当前线程之前的模式是内核模式还是用户模式
KTHREAD + 0x2f8     ThreadListEntry  _LIST_ENTRY            KTHREAD里的双向链表串起当前进程的所有线程



ETHREAD + 0x4e8     ThreadListEntry  _LIST_ENTRY         在Ethread里的这个链表也是圈起来了当前进程所有的线程
ETHREAD + 0x478     Cid              _CLIENT_ID        线程的Cid
ETHREAD + 0x220     Process          Ptr64 _KPROCESS        指向当前进程结构体


KPCR结构体介绍

KPCR是cpu控制区,一个核心一个KPCR对象,里面存放的大多是cpu相关的一些数据以及进程线程相关的一些常用数据
因为0环时候gs:0指向它所以无论在那个内核函数里都能很快的访问到这个结构体
KPCR和NTTIB比较小直接全贴出来了

0: kd> dt _NT_TIB
ntdll!_NT_TIB
   +0x000 ExceptionList    : Ptr64 _EXCEPTION_REGISTRATION_RECORD        当前的0环异常链表
   +0x008 StackBase        : Ptr64 Void                                   从线程里复制出来的栈位置
   +0x010 StackLimit       : Ptr64 Void                                    从线程里复制出来的栈低地址
   +0x018 SubSystemTib     : Ptr64 Void
   +0x020 FiberData        : Ptr64 Void
   +0x020 Version          : Uint4B
   +0x028 ArbitraryUserPointer : Ptr64 Void
   +0x030 Self             : Ptr64 _NT_TIB                            指向自己

0: kd> dt _KPCR
ntdll!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 GdtBase          : Ptr64 _KGDTENTRY64
   +0x008 TssBase          : Ptr64 _KTSS64                            指向Tss
   +0x010 UserRsp          : Uint8B                                    指向用户层的栈
   +0x018 Self             : Ptr64 _KPCR                                指向自己
   +0x020 CurrentPrcb      : Ptr64 _KPRCB                               指向自己的KPRCB的位置
   +0x028 LockArray        : Ptr64 _KSPIN_LOCK_QUEUE                    
   +0x030 Used_Self        : Ptr64 Void
   +0x038 IdtBase          : Ptr64 _KIDTENTRY64                            指向IDT表基址
   +0x040 Unused           : [2] Uint8B                            
   +0x050 Irql             : UChar                                         存储了当前的irql
   +0x051 SecondLevelCacheAssociativity : UChar        
   +0x052 ObsoleteNumber   : UChar    
   +0x053 Fill0            : UChar
   +0x054 Unused0          : [3] Uint4B
   +0x060 MajorVersion     : Uint2B
   +0x062 MinorVersion     : Uint2B
   +0x064 StallScaleFactor : Uint4B
   +0x068 Unused1          : [3] Ptr64 Void
   +0x080 KernelReserved   : [15] Uint4B
   +0x0bc SecondLevelCacheSize : Uint4B
   +0x0c0 HalReserved      : [16] Uint4B
   +0x100 Unused2          : Uint4B
   +0x108 KdVersionBlock   : Ptr64 Void
   +0x110 Unused3          : Ptr64 Void
   +0x118 PcrAlign1        : [24] Uint4B
   +0x180 Prcb             : _KPRCB                                下面是KPRCB一个很大的结构体

KPRCB有点大只介绍常用的字段了

KPRCB + 0x008 CurrentThread    : Ptr64 _KTHREAD  当前线程
KPRCB + 0x004 LegacyNumber     : UChar             是否是兼容模式,兼容模式时候启动是是32位内核了
KPRCB + 0x010 NextThread       : Ptr64 _KTHREAD    下一个线程
KPRCB + 0x018 IdleThread       : Ptr64 _KTHREAD    空闲线程,一般cpu空闲时候就会执行这个线程
KPRCB + 0x028 RspBase          : Uint8B            内核栈
KPRCB + 0x8e88 RspBaseShadow    : Uint8B            kpti开启时候使用的跳板0环栈
KPRCB + 0x8e90 UserRspShadow    : Uint8B            3环栈
KPRCB + 0x7e9a DeepSleep        : UChar             深睡眠模式,在线程切换时候会查询不过不用太多关注跟硬件也有关系
KPRCB + 0x7e80 InterruptCount   : Uint4B            中断次数,在下面的逆向代码里能看到增加这个中断次数的代码
KPRCB + 

寻找线程切换函数

根据白皮书里描述30号中断为时钟中断,线程切换一定跟时钟中断相关,那么我们就先找一下时钟中断的函数叫什么

1: kd> !idt 30

Dumping IDT: ffffba81de9d5000

30:	fffff80281402230 nt!KiHvInterrupt

通过在windbg里查看可以知道时钟中断函数是KiHvInterrupt,我们到ida里搜一下可以搜到下面几个
x64内核实验7-线程_第1张图片

因为我的虚拟机环境默认是没开kpti的所以中断函数直接指向了KiHvInterrupt如果开了的话则是指向KiHvInterruptShadow,不过不要紧我们之前分析过int 3的那个Shadow函数这个KiHvInterruptShadow跟那个基本一样,我这里只贴个图上来就不详细说这个跳板函数了
x64内核实验7-线程_第2张图片

下面我们就看一下这个KiHvInterrupt函数
一开始就还是熟悉的保存trapframe流程
x64内核实验7-线程_第3张图片

中间是硬件相关的一堆调用不管
x64内核实验7-线程_第4张图片

然后就是存浮点相关,之后是一些检测然后增加中断次数跳到KiHvInterruptDispatch
我们再看一下KiHvInterruptDispatch
x64内核实验7-线程_第5张图片

我们再看一下KiDpcInterruptBypass
x64内核实验7-线程_第6张图片

又调用了KiDispatchInterrupt
x64内核实验7-线程_第7张图片

在跟进去会发现我们要找的函数,swapContext
x64内核实验7-线程_第8张图片

swapContext就是我们要找的线程切换函数
现在我们记录一下win10系统下时钟中断进入线程切换的函数调用流程吧

  1. KiHvInterruptShadow(开了kpti的话有这一步)
  2. KiHvInterrupt
  3. KiHvInterruptDispatch
  4. KiDpcInterruptBypass
  5. KiDispatchInterrupt
  6. KxDispatchInterrupt
  7. SwapContext

这个流程里有大量的代码有兴趣深入研究的可以按照这个流程看一下,线程切换涉及到了很多系统内核的其他内容我们这里下面直接分析SwapContext

线程切换函数逆向分析

先看一下进入SwapContext之前都传了那些参数进来,可以看到先是调用KiQueueReadyThread找到要切换的线程
大家可以自己到这个函数里分析一下
从KiDpcInterruptBypass这里开始看

x64内核实验7-线程_第9张图片

现在的寄存器值是
rsp = trapframe
rbp =TRAP_FRAME + 80
rcx = CurrentThread
然后走到KiDispatchInterrupt
x64内核实验7-线程_第10张图片

看图中圈出来的位置,rsp在调用KxDispatchInterrupt之前又恢复成了trapframe,所以现在的寄存器状态还是
rsp = trapframe
rbp = TRAP_FRAME + 80
rcx = CurrentThread
rbx = kpcr + 20

x64内核实验7-线程_第11张图片

再看一下SwapContext都干了什么,我这里不一行一行去说了这个函数超级长,我把主要流程截图出来大家最好自己去逆一下会有自己的理解
先是判断要切换的线程是不是就是当前线程,是的话就不处理了,不是的话走下面更改线程状态
x64内核实验7-线程_第12张图片

x64内核实验7-线程_第13张图片

这里是切换线程的栈
x64内核实验7-线程_第14张图片

判断俩线程是不是同一个进程不是的话要切换cr3
x64内核实验7-线程_第15张图片

x64内核实验7-线程_第16张图片

走到这里再往下就是收尾的动作了,就是复制进程和线程内容到kpcr里的过程
x64内核实验7-线程_第17张图片

总结

我们最后总结一下
线程切换的流程(这个总体流程跟xp时候差不多只不过调用的函数链路变了而且多了不少的检测和动作):

  1. 当前线程保存上下文环境到内核栈
  2. 找到一个就绪线程
  3. 切换线程的内核栈
  4. 如果需要切换cr3就切换
  5. 复制信息到kpcr,比如线程结构体里的内核栈位置或者往tss里存进0环时候要用的ist0
  6. 从刚切换的线程内核栈里恢复上下文环境

触发线程切换的条件(这里只带大家看了时钟中断其他的几个场景大家可以自己去验证一下):

  1. 时钟中断
  2. 缺页异常
    环境到内核栈
  3. 找到一个就绪线程
  4. 切换线程的内核栈
  5. 如果需要切换cr3就切换
  6. 复制信息到kpcr,比如线程结构体里的内核栈位置或者往tss里存进0环时候要用的ist0
  7. 从刚切换的线程内核栈里恢复上下文环境

触发线程切换的条件(这里只带大家看了时钟中断其他的几个场景大家可以自己去验证一下):

  1. 时钟中断
  2. 缺页异常
  3. 系统api出0环

你可能感兴趣的:(windows内核,windows,x64,逆向,cpu)