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里搜一下可以搜到下面几个
因为我的虚拟机环境默认是没开kpti的所以中断函数直接指向了KiHvInterrupt如果开了的话则是指向KiHvInterruptShadow,不过不要紧我们之前分析过int 3的那个Shadow函数这个KiHvInterruptShadow跟那个基本一样,我这里只贴个图上来就不详细说这个跳板函数了
下面我们就看一下这个KiHvInterrupt函数
一开始就还是熟悉的保存trapframe流程
中间是硬件相关的一堆调用不管
然后就是存浮点相关,之后是一些检测然后增加中断次数跳到KiHvInterruptDispatch
我们再看一下KiHvInterruptDispatch
我们再看一下KiDpcInterruptBypass
又调用了KiDispatchInterrupt
在跟进去会发现我们要找的函数,swapContext
swapContext就是我们要找的线程切换函数
现在我们记录一下win10系统下时钟中断进入线程切换的函数调用流程吧
- KiHvInterruptShadow(开了kpti的话有这一步)
- KiHvInterrupt
- KiHvInterruptDispatch
- KiDpcInterruptBypass
- KiDispatchInterrupt
- KxDispatchInterrupt
- SwapContext
这个流程里有大量的代码有兴趣深入研究的可以按照这个流程看一下,线程切换涉及到了很多系统内核的其他内容我们这里下面直接分析SwapContext
线程切换函数逆向分析
先看一下进入SwapContext之前都传了那些参数进来,可以看到先是调用KiQueueReadyThread找到要切换的线程
大家可以自己到这个函数里分析一下
从KiDpcInterruptBypass这里开始看
现在的寄存器值是
rsp = trapframe
rbp =TRAP_FRAME + 80
rcx = CurrentThread
然后走到KiDispatchInterrupt
看图中圈出来的位置,rsp在调用KxDispatchInterrupt之前又恢复成了trapframe,所以现在的寄存器状态还是
rsp = trapframe
rbp = TRAP_FRAME + 80
rcx = CurrentThread
rbx = kpcr + 20
再看一下SwapContext都干了什么,我这里不一行一行去说了这个函数超级长,我把主要流程截图出来大家最好自己去逆一下会有自己的理解
先是判断要切换的线程是不是就是当前线程,是的话就不处理了,不是的话走下面更改线程状态
这里是切换线程的栈
判断俩线程是不是同一个进程不是的话要切换cr3
走到这里再往下就是收尾的动作了,就是复制进程和线程内容到kpcr里的过程
总结
我们最后总结一下
线程切换的流程(这个总体流程跟xp时候差不多只不过调用的函数链路变了而且多了不少的检测和动作):
- 当前线程保存上下文环境到内核栈
- 找到一个就绪线程
- 切换线程的内核栈
- 如果需要切换cr3就切换
- 复制信息到kpcr,比如线程结构体里的内核栈位置或者往tss里存进0环时候要用的ist0
- 从刚切换的线程内核栈里恢复上下文环境
触发线程切换的条件(这里只带大家看了时钟中断其他的几个场景大家可以自己去验证一下):
- 时钟中断
- 缺页异常
环境到内核栈 - 找到一个就绪线程
- 切换线程的内核栈
- 如果需要切换cr3就切换
- 复制信息到kpcr,比如线程结构体里的内核栈位置或者往tss里存进0环时候要用的ist0
- 从刚切换的线程内核栈里恢复上下文环境
触发线程切换的条件(这里只带大家看了时钟中断其他的几个场景大家可以自己去验证一下):
- 时钟中断
- 缺页异常
- 系统api出0环