进程与线程

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)进程挂靠为何要修改养父母的值

    因为存在进程挂靠的缘故,其才会需要修改养父母的值,因为进程挂靠的过程中可能会产生进程切换,一旦发生进程切换,切换回来,根据的是养父母的值,

    如果不修改养父母的值,则切换回来,就不是挂靠进程的值,而是自己了,如果此时再读,就会变成自己读自己,这样会发生错误。

    

posted @ 2020-03-31 09:36  OneTrainee  阅读(594)  评论(0编辑  收藏  举报