Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html
进程与线程
1. _EPROCESS结构体
2. _ETHREAD结构体
3. _KPCR结构体
4. 线程的等待/调度链表
5. 线程切换
6. 线程切换与TSS
7. FS:[0]三环指向TEB的切换
8. 进程挂靠
1. _EPROCESS结构体
+0x000 Header 可等待对象
+0x018 DirectoryTable 页目录表基址(进程最重要的地址)
+0x038 KernelTime 该进程在零环运行的时间
+0x03c UserTime 该进程在三环用到的时间
+0x05c Affinity 规定进程里的线程可以在哪个CPU跑(详细细节查询)
+0x062 BasePriority 该进程中的所有线程中最起始的优先级
+0x078 CreateTime 当前进程的创建时间
+0x078 ExitTime 当前进程的退出时间
+0x084 UniqueProcessId 进程编号,进程管理器的编号
+0x088 ActiveProcessLink 当前系统活动进程的编号(进程管理器就是查找该链表)
+0x090 QuotaUsage / +0x09c QuotaPeak 物理页统计的相关信息
+0x0a8 CommitCharge / +0x0ac PeakVirtualSize / +0x0b0 VirtualSize 虚拟内存相关的统计信息
+0x11c VadRoot 标识0-2G哪些地址被占用了
+0x0bc DebugPort / +0x0c0ExceptionPort 调试相关
+0x0c4 ObjectTable 句柄表
+0x174 ImageFileName 进程镜像文件名(最多16个字节)
+0x1a0 ActiveThreads 活动线程的数量
+0x1b0 PEB 在三环描述了相关信息
2. _ETHREAD结构体
+0x000 Header 可等待对象
+0x018 InitialStack / +0x01c StackLimit / +0x028 KernelStack 线程切换相关
+0x020 TEB 在三环描述了该线程相关信息(fs:[0]在三环指向TEB)
+0x02c DebugActive 如果为-1,则不能使用调试寄存器 Dr0~Dr7
+0x034 ApcState / +0x0e8 ApcQueueLock / +0x138 ApcStatePointer / +0x14c SavedApcState Apc相关
+0x02d State 线程状态:就绪、运行还是等待
+ 0x06c BasePriority 当前线程优先级
+0x070 WaitBlock 等待哪个对象
+0x0e0 ServiceTable 系统服务表机制
+0x134 TrapFrame 进零环时保存的环境
+0x140 PreviousMode 某些内核函数会判断程序是0环调用的还是3环调用的
+0x1b0 ThreadListEntry 双向链表,一个进程的所有线程,都挂在这样一个链表中(图解)。
+0x1ec Cid 当前线程和进程的编号
+0x220 ThreadProcess 当前线程所属的进程
+0x22c ThreadListEntry 双向链表,一个进程的所有线程,都挂在这样一个链表中(图解)。
3. _KPCR结构体
+0x000 ExceptionList 当前线程内核异常链表(SEH)
+0x004 StackBase / +0x008 当前内核线程栈的基址和大小
+0x018 self 指向自己(NT_TIB),方便查找
+0x01c SelfPcr 指向自己(KPCR),方便寻址
+0x020 Prcb 指向扩展结构体,PCRB
+0x038 IDT IDT表基址
+0x03c GDT GDT表基址
+0x040 TSS 指向TSS
+0x051 Number CPU编号:0,1,2,....
+0x120 PrcbData 拓展结构体
+0x004 CurrentThread 当前线程
+0x008 NextThread 即将切换的下一个线程
+0x00c IdleThread 空闲线程
4. 线程的等待/调度链表
KiWaitListHead - 等待链表
比如:线程调用了Sleep()或者WaitForSingleObject()等函数,就挂到这个链表中。
KiDispatcherReadyListHead - 调度链表
其存在32个链表,按不同调度级别来进行划分。
操作系统所有线程:当前KCPR正在跑的+等待链表+32个调度链表。
5. 线程切换
1)线程主动切换
线程切换依次调用 KiSwapThread-> KiSwapContext -> SwapContext,因此我们看其如何调用KiSwapThread调用。
该类函数有被其他很多函数调用,因此得出结论:内核函数绝大部分会引起线程切换。
2)线程被动切换
一个线程并不是必须自身调用API来实现线程切换,其他可以引起中断。
两类:①异常/中断;②时钟中断。
其中时钟中断走0x20号中断,系统每过20ms触发一次时钟中断来(可能)切换线程。
3)时钟中断的时间片管理
时钟中断可能触发线程切换,但并不一定触发线程切换。
在两类条件下,时钟中断会触发线程切换:
① 当前的线程CPU时间片到期
② 有备用线程(KPCR.PrcbData.NextThread)
这里之后会补充上······
6. 线程切换与TSS
1)如何实现线程切换
如下图,其KiSwapContext先保存几个最基本的寄存器,之后调用SwapContext,当从该函数出来之后,就会切换新的线程。
对于线程切换,以下要点是必须要理解的:
① 线程切换本质就是切换寄存器,堆栈地址。
② 虽然我们有虚拟内存的概念,但线程切换发生在内核中,其无论线程A还是B都公用一个函数,因此在0环,将_KPCR中指向的线程改变,其就达到线程切换的目的
2)内核栈的描述:
这里有一点需要注意,Trap_Frame是保存线程三环进零环时的寄存器环境,KernelStack是当前内核栈的栈顶。
千万不要以为用TrapFrame来保存原线程寄存器环境,其直接使用push压入栈顶,其_ETHREAD+0x134 TrapFrame的位置直接指向_TRAP_FRAME结构。
如果你理解了这部分,整个堆栈结构就很好理解了。
3)线程替换的具体细节 - SwapContext函数分析
首先,需要明确以下细节:
① TSS是一块固定内存,当初Intel把它作为任务切换,但实际上这么大块内存只有ESP0、SS0,用作从三环到零环时栈切换的具体位置。
② ESP0与新线程的ESP不是一个概念,新线程的ESP被保存到_KTRHEAD.KernelStack中,新线程的EBP同理。
③ 而ESP0只是作为该线程从三环到零环的起始位置,当线程切换时,其就计算得出该具体位置,计算方式是_KTHREAD.Initstack+0x210+0x10.
因此,当我们从三环进零环时,esp指向的就是Trap_Frame的初始值,因此你看SystemService直接压栈来存储TrapFrame结构。
④ N个线程仅使用一个TSS和一个KPCR结构体,因此在SwapContext中你会发现其找到该部分的地址,然后手动替换掉元素,并不会改变其位置。
如果你理解上面这些汇编代码,你再看下面的汇编代码,就会掌握线程替换的实质
7. FS:[0]三环指向TEB的切换
FS在三环时指向TEB,但在零环指向KPCR(地址固定是0FFDFF000h)。
FS是段描述符,其形式如下:
现在有两种方式:FS指向的段选择子改变;只改正段选择子的BaseAddress。
而Windows采用后面一种方式,TEB表对应的IDT[7],KPCR表对应着IDT[6],
因此三环进零环,仅修改段选择子的指向即可,只要保护模式熟练,这很容易理解。
8. 进程挂靠
我们在保护模式中学习过,当前进程的线性地址存储在Cr3中,进程为线程提供空间上的支持。
一个线程中存在两个位置指向_EPROCESS,分别是+0x220 _EPROCESS,+0x44 _KAPC_STATE._EPROCESS。
那线程切换时判断是否切换CR3依照什么呢?结论可能有点让你吃惊,通过 _KAPC_STATE 来切换CR3。
1)养父母与亲生父母
+0x220位置是作为线程的亲生父母,该进程创建了线程。
+0x44位置作为线程的养父母,其实对该线程负责。
因此线程切换,参照的是养父母,而不是亲生父母。
2)进程挂靠
将当前进程的CR3切换为其他进程,就叫做进程挂靠。
比如,Windows 提供 API 来读取其他进程的内存地址,这一定使用内存挂靠。
3)NtReadVirtualMemory实现路径
NtReadVirtualMemory->AttchProcess(修改养父母)->KiSwapProcess(修改Cr3)
进程挂靠的实现函数是Nt!SwapProcess,其代码过程如下:
另外,在进程挂靠时,SwapProcess的上层函数 ,Nt!KiAttachProcess也修改了Apc_State函数
4)进程挂靠为何要修改养父母的值
因为存在进程挂靠的缘故,其才会需要修改养父母的值,因为进程挂靠的过程中可能会产生进程切换,一旦发生进程切换,切换回来,根据的是养父母的值,
如果不修改养父母的值,则切换回来,就不是挂靠进程的值,而是自己了,如果此时再读,就会变成自己读自己,这样会发生错误。