内核-③进程线程
1. EPROCESS
- 每个进程在0环都对应着一个进程结构体,里面包含了进程所有重要信息:kd> dt _EPROCESS
![]()
1.1 +0x0 Pcb
- 类型:_KPROCESS结构体
- 只要0环(结构体)对象是以_DISPATCHER_HEADER开头的,就是可等待对象,可以使用WaitForSingleObject等待,如Mutex互斥体,Event时间等
![]()
1.1.1 0x18 DirectoryTableBase
- 这就是每个进程中的页目录基址,Cr3中的值就是从这里获取的
- 所谓的进程切换:将DirectoryTableBase的值取出,放入到Cr3中
1.1.2 0x38 KernelTime
- 当前进程在3环运行的时间
1.1.3 0x3C UserTime
- 当前进程在0环运行的时间
1.1.4 0x50 ThreadListEntry
- 类型:_LIST_ENTRY
- 线程双向链表的第一个,里面存储着当前进程的所有线程的链表
1.1.5 0x5c Affinity
- 规定进程里面的所有线程能在哪个CPU上跑,如果值为1,那这个进程的所有线程只能在0号CPU上跑(00000001) ,如果值为3,那这个进程的所有线程能在0、1号CPU上跑(000000011)
- 4个字节共32位 所以最多32核 Windows64位 就64核
- 如果只有一个CPU 把这个设置为4 那么这个进程就死了
1.1.6 0x62 BasePriority
- 基础优先级或最低优先级,该进程中的所有线程最起码的优先级
1.2 0x70 CreateTime
- 当前进程的创建时间
1.3 0x78 ExitTime
- 当前进程的退出时间
1.4 0x84 UniqueProcessId
- 进程ID
1.5 0x88 ActiveProcessLinks
- 类型:_LIST_ENTRY
- 活动进程列表,是一个双向列表,所有活动进程都连接在一起,构成了一个链表
- 第一个成员指向下一个进程的ActiveProcessLinks
- 第二个成员指向上一个进程的ActiveProcessLinks
- 全局变量PsActiveProcessHead(8字节)指向全局链表头,全局变量的第一个成员指向了当前系统的第一个进程结构体_EPROCESS
查看第一个进程结构体:dt _EPROCESS 863b78b8-0x88因为63b78b8 指向的位置是_EPROCESS的ActiveProcessLinks,需要-0x88才回到进程结构体的头部任务管理器中的进程就是通过系统提供的API对这个双向列表进行查询得到的
- 拓展:可以将双向列表中的某一个进程断掉,达到隐藏进程的目的
- +0x090 QuotaUsage : [3] Uint4B +0x09c QuotaPeak : [3] Uint4B
- 物理页相关的统计信息
- +0x0a8 CommitCharge : Uint4B +0x0ac PeakVirtualSize : Uint4B +0x0b0 VirtualSize : Uint4B
- 虚拟内存相关的统计信息
1.6 +0x11c VadRoot
- 类型: Ptr32 Void
- 标识0-2G哪些地址没占用,可以隐藏模块
1.7 +0x0bc DebugPort
- +0x0c0 ExceptionPort
- 类型: Ptr32 Void
- 调试相关
- 反调试:DebugPort清零,DebugPort里面存的是一个结构体,可以通过它与调试器进行通讯,但是别人也可以自己建立一个DebugPort结构体,通过它来进行调试
1.8 +0x0c4 ObjectTable
- 类型: Ptr32 _HANDLE_TABLE
- 句柄表
- 当前进程可能会用到很多其他的内核对象,句柄表会存放这些内核对象的句柄
- 反调试:遍历系统所有进程的句柄表,查看自己的句柄(_EPROCESS的地址)是否在该进程的句柄表中,若自己的句柄在某进程的句柄表中,说明自己被该进程打开了,说明自己很可能正在被调试
- +0x174 ImageFileName : [16] UChar
- 进程镜像文件名 最多16个字节
- +0x1a0 ActiveThreads : Uint4B
- 活动线程的数量
1.9 +0x1b0 Peb
- 00c~01c是三个双向列表,记录了当前进程有多少个模块
- 0x00c:模块加载的顺序
- 0x014:模块在内存中的顺序
- 0x01c:模块初始化的顺序
- 模块隐藏:对这几个链表进行断链
1.10 总结
- 1._EPROCESS
- 1.每个进程在0环都对应一个_EPROCESS结构体,存储了该线程的所有重要信息
- 2.DISPATCHER_HEADER
- 2._EPROCESS的第一个成员是_KPROCESS,因为_KPR....的第一个成员是DISPATCHER_HEADER,所以是可等待对象
- 3.页目录基址表PDT
- 3._KPROCESS结构体的0x18是DirectoryTableBase,是页目录表基址(PDT),Cr3寄存器的值就来自这里
- 4.线程双向链表
- 4._KPROCESS的x50是ThreadListEntry,当前进程的线程双向链表头,EPROCESS的0x190也是当前进程的线程双向列表
- 5.活动进程双向链表
- 5._EPROCESS的0x88是ActiveProcessLinks,是所有活动进程的双向链表,全局变量PsActiveProcessHeader指向链表头
- 6.当前进程打开的句柄表
- 6._EPROCESS的0xc4是ObjectTable,是当前进程所打开的句柄表
- 7.0~2GB空闲位置
- 7._EPROCESS的0x11c是VadRoot,标识了0-2GB中,没有被占用的地址
- 8.调试桥梁
- 8._EPROCESS的0xbc是DebugPort,存储一个结构体指针,这个结构体是调试器与当前进程通讯的桥梁
- 9.进程隐藏
- 9.通过对_EPROCESS的0x88的ActiveProcessLinks进行断链,可以实现简单的进程隐藏
- 10.ObjectTable
- 10._EPROCESS的0xc4存储着当前进程打开的句柄表
- 11.反调试
- 11.句柄表和jDebugPort都可以做反调试
2. ETHREAD
- 每个线程在0环都对应着一个线程结构体,里面包含了线程所有重要信息:kd> dt _ETHREAD
2.1 ThreadListEntry
- 线程双向链表的第一个链表在进程的0x50处,1.1.4中可以观察到,里面存储着当前进程的所有线程的链表
2.2 练习
- 1.一个进程里面所有线程都在线程个双向链表中,若把线程双向链表断链,还能不能在OD里面看到这个线程,若看不到,这个线程是否还能继续执行?
- 1.通过全局变量PsActiveProcessHeader找到第一个进程
- 2.遍历线程,找到线程的父进程,通过进程的0x50:ThreadListEntry(或0x190)遍历线程,找到需要隐藏的线程A
- 3.将A的上一个线程程的双向列表指向A的下一个线程,线程链表断链之后,发现线程序依然可以跑,说明问题:
- ①查询线程的API都是在0x50,0x190这里面查询的
- ②CPU在调度线程的时候,并不使用这俩个双向链表
- 思考:
- CPU调度线程的时候会去哪里找这些线程?
2.3 总结
- 1._ETHREAD和_EPROCESS一样,存储线程的重要信息
- 2._DISPATCHER_HEADER
- _KTHREAD的第一个成员也是指向_DISPATCHER_HEADER,也是可等待对象
- 3.TEB
- _KTHREAD的0x20指向TEB,是线程环境模块,TEB在3环是可以由FS:[0]访问的
- 4.调试状态
- _KTHREAD的0x2c是DebugActive表明当前线程是否处于调试状态,非调试状态时值为-1
- 5.等待链表。调度链表
- _KTHREAD的0x60指向的是:情况①等待链表,情况②调度链表,因此0x60有两个名字
- 6.优先级
- _KTHREAD的0x6c是BasePriority是线程的优先级,从父进程的BasePriority继承的,KeSetBasePriorityThread()可修改
- 7.系统服务表
- _KTHREAD的0xe0是ServiceTable,指向系统服务表基址(3环通过系统服务号在系统服务表找到函数)
- 8._TRAPFRAME
- _KTHREAD的0x134是TrapFrame,当前线程的_TRAMPFRAME,进0环时保存的(原3环的会保存在堆栈)
- 9.先前模式
- _KTHREAD的0x140是PreviousMode,保存先前模式,cpu以此判断调用函数前的权限是0环还是3环
- 10.线程双向链表
- KTHREAD的0x1b0是ThreadListEntry,双向链表头,将当前进程的所有线程圈起来了,EPROCESS的0x50也指向它
- 11.进程线程编号
- _ETHREAD的0x1ec是Cid,存储了当前线程的进程编号和线程标号
- 12.线程所属的进程
- _ETHREAD的0x220是ThreadProcess,存储了当前线程所属线程的_EPROCESS
- 13.线程双向列表
- _ETHREAD的0x22c是ThreadListEntyr,线程双向链表头,将当前进程的所有线程圈起来了
- 14.隐藏线程
- 通过线程结构体,可以实现隐藏线程,只需要将ThreadListEntyr断链,断链之后,线程仍然能运行,只是OD找不到这个线程了,说明普通的API是通过ThreadListEntyr找到线程的,但是CPU不是通过它调用线程的(而是通过调度链表)
- 15.线程反调试
- _KTHREAD的0x2c是DebugActive,可以做反调试
3. KPCR
3.1 KPCR
- 当线程进入0环的时候,CPU第一件事情就是切换FS段寄存器,3环的时候FS指向TEB(描述当前线程信息),进入0环后,FS指向KPCR结构体,描述当前CPU的信息,并将0环异常链表清空。(cpu进了0环才会使用kpcr,在0环查kpcr,在3环查teb)
- 每个CPU都有一个KPCR结构体(一核一个)
- KPCR中存储了CPU需要用到的一些重要数据:GDT、IDT、以及一些线程相关的信息,其实就是一些进程与线程信息的副本,用起来直接去KPCR拿,不用再去查询
- windbg查看
- kd> dt _KPCR
获取当前线程信息:fs:[124]当前线程的栈底和栈的边界是线程切换的时候拷贝到KRCP的
3.2 总结
- 1._KPCR
- _KPCR里面存储着CPU经常需要用到的重要数据,虽然其他地方也有这些数据,但是存在这里可以提高效率
- 2._KPCR的0x1c
- _KPCR的0x1c指向_KPCR自己本身(为了方便)
- 3.PrcbData
- _KPRC的0x120指向_KPRC的0x120:PrcbData,是为了防止结构体发生变化0x120位置改变,找不到PrcData
- 3.IDT表基址
- _KPRC的0x38指向IDT表基址(IDT存储了三种门),一个CPU对应一个IDT
- 4.GDT表基址
- _KPRC的0x3c指向GDT表基址,一个CPU对应一个GDT表(里面存储段描述符)
- 5.TSS
- _KPRC的0x40存储TSS(TSS存储一堆寄存器)
- 6.CPU编号
- _KPRC的0x51是当前CPU编号,只有一个CPU的话,编号就是0
- 7._KPRCB
- _KPRC的0x120指向_KPRCB结构体,里面存储CPU的一些重要信息
- 8.CurrentThread
- _KPCB的0x4是CurrentThread,是当前CPU执行线程的_ETHREAD
- 9.NexThread
- _KPRCB的0x8是NexThread,是CPU要执行的下一个线程_ETHREAD
- 10.IdleThread
- _KPRCB的0xc是IdleThread,也是一个_ETHREAD,当所有线程执行完毕后,执行它
- 11.ExceptionList
- KPCR的0x0指向ExceptionList,是一个异常处理链表(3环时是3环的异常处理,0环时是0环的异常处理SEH)只要写了异常处理:_tyr{} _exept,就会向ExceptionList挂上一个函数
4. 等待链表、调度链表
4.1 线程状态
- ①运行
- ②就绪(随时可以跑)
- ③等待(正在阻塞)
- 正在运行中的线程存储在KPCR中
- 就绪和等待线程存储在另外的33个链表中,其中32个就绪链表,一个等待链表
- 这些链表都使用到了_KTHREAD的0x60这个位置
4.2 等待链表
- 查看等待链表:kd> dd KiWaitListHead
- KiWaitListHeads是一个全局变量,指向一个双向链表头,里面存储的是阻塞状态的线程_ETHREAD,如:调用了Sleep()或WaitForSingleObject()函数的线程
- 8626c080就是指向线程结构体ETHREAD也可以说是指向KTHREAD的0x60的位置
- 查看当前的阻塞线程的_ETHREAD:kd> dt _ETHREAD 8626C080-0x60
查看当前阻塞线程所属进程的_EPROCESSkd> dt _EPROCESS 0x860b1de0,然后在0x174的位置可以看到ImageFileName进程名
4.3 调度链表
- 查看调度链表(也叫就绪链表):kd> dd KiDispatcherReadyListHead L70
- KiDispatcherReadyListHead 是一个全局变量,里面存储了32个双向链表的链表头,32个链表头分别存储了32种线程优先级别的_ETHREAD
- 只要找到了KiDispatcherReadyListHead 这个全局变量,意味着整个系统的所有调度状态的线程都被我们找到了
- 第一个值=第二个值=当前地址说明该链表是空的
- 第一个值=第二个值!=当前地址说明只有一个
4.4 版本差异
- 等待链表和调度链表32位情况下,不管是几核的CPU,都是只有1个KiWaitListHead,1个KiDispatcherReadyListHead
- XP的KiDispatcherReadyListHead有32个调度链表,64位WIN7的KiDispatcherReadyListHead ,有64个调度链表
- 但是服务器的版本不同:
- 等待链表(KiWaitListHeads)只有一个
- 调度链表(KiDispatcherReadyListHead )的数量和cpu一致
5. 线程切换
5.1 模拟线程切换代码ThreadSwitch
- 步骤:
- 1.定义线程结构体
- 2.编写线程函数
- 3.注册线程:在结构体中找到需要注册的线程
- 4.初始化线程结构体
- 5.在main函数中调用切换线程函数
- 实现
- 1.定义线程结构体
//线程信息的结构(模仿ETHREAD) typedef struct { char* name; //线程名,相当于线程ID int Flags; //线程状态,(windows的是存储在等待链表,就绪链表,和KPCR) int SleepMillsecondDot; //休眠时间 void* initialStack; //线程堆栈起始位置 void* StackLimit; //线程堆栈界限 void* KernelStack; //线程堆栈当前位置,也就是ESP void* lpParameter; //线程函数的参数 void(*func)(void* lpParameter); //线程函数 }GMThread_t; //第0个结构体就是主线程的线程结构体- 2.编写线程函数
- 线程函数
void Thread1(void*) { while(1){ printf("Thread1\n"); GMSleep(500); } } - GMSleep():【主动切换】设置线程函数Sleep时间,以便切换线程
void GMSleep(int MilliSeconds){ GMThread_t* GMThreadp; //获取当前线程结构体,设置线程状态以及休眠时间 GMThreadp = &GMThreadList[CurrentThreadIndex]; if (GMThreadp->Flags != 0) { GMThreadp->Flags = GMTHREAD_SLEEP; GMThreadp->SleepMillsecondDot = GetTickCount() + MilliSeconds; } //再次启动线程 Scheduling(); return; }- 3.注册线程
- 在结构体中找到需要注册的线程
//将一个函数注册为单独线程执行 int RegisterGMThread(char* name, void(*func)(void*lpParameter), void* lpParameter){ int i; for (i = 1; GMThreadList[i].name; i++) { if (0 == _stricmp(GMThreadList[i].name, name)) { break; } } //初始化线程结构体 initGMThread(&GMThreadList[i], name, func, lpParameter); return (i & 0x55AA0000); } - 4.初始化线程结构体
- 1.定义堆栈指针
- 2.申请堆空间作为堆栈空间
- 3.将申请得到的地址+申请的大小作为堆栈的栈低
- 4.对这个堆栈空间压入数据
- 5.压入数据完毕后,设置栈顶指针
- 注意,新的EIP应指向线程入口函数,线程的入口函数,通过参数线程结构体来调用线程函数
- 代码
//线程入口减数: //参数是线程结构体,入口函数会调用结构体中的线程函数,从结构体中取出函数参数 void GMThreadStartup(GMThread_t* GMThreadp){ GMThreadp->func(GMThreadp->lpParameter); //调用线程函数,参数是线程结构体中的参数 GMThreadp->Flags = GMTHREAD_EXIT; Scheduling(); return; } //初始化线程的信息 void initGMThread(GMThread_t* GMThreadp, char* name, void (*func)(void* lpParameter), void* lpParameter) { unsigned char* StackPages; unsigned int* StackDWordParam; //结构体初始化赋值 GMThreadp->Flags = GMTHREAD_CREATE; GMThreadp->name = name; GMThreadp->func = func; GMThreadp->lpParameter = lpParameter; //申请堆栈空间 StackPages = (unsigned char*)VirtualAlloc(NULL, GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE); ZeroMemory(StackPages, GMTHREADSTACKSIZE); //模拟堆栈效果(高-->低) GMThreadp->initialStack = StackPages + GMTHREADSTACKSIZE; //设置堆栈边界 StackDWordParam = (unsigned int*)GMThreadp->initialStack; //入栈 PushStack(&StackDWordParam, (unsigned int)GMThreadp); //当前线程结构体指针,可以找到线程函数,参数 PushStack(&StackDWordParam, (unsigned int)0); //平衡堆栈:切换进程的时,最后pop的是ebp,为了使ebp+8就是参数 PushStack(&StackDWordParam, (unsigned int)GMThreadStartup);//线程入口函数,负责调用线程函数 PushStack(&StackDWordParam, (unsigned int)5); //ebp,第一次创建线程,这些寄存器还不需要用到,可以随便填 PushStack(&StackDWordParam, (unsigned int)7); //edi PushStack(&StackDWordParam, (unsigned int)6); //esi PushStack(&StackDWordParam, (unsigned int)3); //ebx PushStack(&StackDWordParam, (unsigned int)2); //ecx PushStack(&StackDWordParam, (unsigned int)1); //edx PushStack(&StackDWordParam, (unsigned int)0); //eax //当前线程的栈顶ESP GMThreadp->KernelStack = StackDWordParam; GMThreadp->Flags = GMTHREAD_READY; return; } - 5.在main函数中调用切换线程函数
- 1.遍历结构体,找到处于GMTHREAD_READY状态的线程
//先判断线程休眠时间是否已经全部休眠完了(系统启动时间-线程休眠时间) //这个函数会让出cpu,从队列里重新选择一个线程执行 void Scheduling(void){ int i; int TickCount; GMThread_t* NowGMThreadp; GMThread_t* NextGMThreadp; TickCount = GetTickCount(); NowGMThreadp = &GMThreadList[CurrentThreadIndex]; NextGMThreadp = &GMThreadList[0]; //找到第一个处于GMTHREAD_READY状态的线程 for (i = 1; GMThreadList[i].name; i++) { if (GMThreadList[i].Flags & GMTHREAD_SLEEP) { if (TickCount > GMThreadList[i].SleepMillsecondDot) { GMThreadList[i].Flags = GMTHREAD_READY; } } if (GMThreadList[i].Flags & GMTHREAD_READY) { NextGMThreadp = &GMThreadList[i]; break; } } //当前线程索引 CurrentThreadIndex = NextGMThreadp - GMThreadList; //参数:当前线程结构体指针,需要切换的线程结构体指针 SwitchContext(NowGMThreadp, NextGMThreadp); return; } - 2.【被动切换线程】Scheduling()
__declspec(naked) void SwitchContext(GMThread_t* NowGMThreadp, GMThread_t* NextGMThreadp){ __asm { push ebp mov ebp, esp //提升堆栈 push edi //保存寄存器 push esi push ebx push ecx push edx push eax mov esi, NowGMThreadp mov edi, NextGMThreadp mov [esi+GMThread_t.KernelStack], esp //或:mov [esi+20], esp //保存当前线程的栈顶 //经典线程切换,另外一个线程复活 mov esp, [edi+GMThread_t.KernelStack] //设置新的线程栈顶,此时堆栈变成的新的线程的堆栈 pop eax //将新线程堆栈中的的寄存器弹出到寄存器中 pop edx pop ecx pop ebx pop esi pop edi pop ebp ret //pop eip,eip就是之前push的GMThreadStartup,所以会执行线程函数 } } - 6.代码
int main(){ //初始化线程环境 RegisterGMThread("Thread1", Thread1, NULL); RegisterGMThread("Thread2", Thread2, NULL); RegisterGMThread("Thread3", Thread3, NULL); RegisterGMThread("Thread4", Thread4, NULL); //模仿windows线程切换 while(TRUE) { Sleep(20); Scheduling(); } return 0; }
5.1.1 总结
- 1.线程不是被动切换的,是主动让出CPU的
- 2.线程切换并没有使用TSS来保存寄存器,而是使用堆栈来保存
- 3.线程切换的过程其实就是堆栈切换的过程
5.2 主动切换
- 在5.1中,得知通过调用SwitchContext()函数,可以达到切换线程的目的,在Windows下,也有类似的函数:KiSwapContext()
- 思考:
- IDA分析:
5.3 时钟中断切换
5.3.1 如何中断一个正在执行的程序
- 1.异常,如:缺页,或INT N指令
- 2.中断,如:时钟中断
5.3.2 时钟中断
5.3.2.1 执行流程
- 1.搜索IDT
- 2.找到0x30对应的函数(0x2E的是_KiSystemService)
- 3.2E+4+4=30,找到0x30对应的函数_KiStartUnexpectedRange@0
- 4.跟进去查看时钟中断做了什么
- ①发现调用了其他模块的函数HalBeginSystemInterrupt
- 去导入表窗口ALT+G搜索该函数,发现函数是属于HAL的Lib里面的一个函数,跟进去查看,没有调用其他函数
- ②发现还调用了一个其他模块的函数HalEndSystemInterrupt
- 导入表搜索该函数,发现也是HAL的Lib里面的一个函数,跟进去查看:发现调用了KiDispatchInterrupt,在导入表中搜索,发现是ntoskrnl模块的函数
- ③在ntoskrnl搜索查看KiDispatchInterrupt函数
- 发现这里调用了SwapContext函数
5.3.2.2 总结
- 线程切换的几种情况:
- 1.主动调用API函数
- 2.时钟中断
- ①当前线程的CPU时间片到期
- ②有备用线程(KPCR.PrcbData.NextThread存储了另外一个线程的值时)
- 3.异常处理(例如缺页异常)
- 一个线程如果不调用API,在代码中屏蔽中断(CLI指令可以清空IF标志寄存器,达到屏蔽可屏蔽中断的目的),并且不会出现异常,那么这个线程将永久占用CPU,单核占用率100%,双核就是50%
5.4 时间片管理
5.4.1 回顾
- 时钟中断会导致线程切换,但并不是只要有时钟中断就一定会切换线程,存在以下两种情况才会切换线程:
- ①当前线程的CPU时间片到期
- ②有备用线程时(KPCR.PrcbData.NextThread存储了另外一个线程的值时)
5.4.2 cpu时间片到期
- 1) 当一个新的线程开始执行时,初始化程序会在_KTHREAD.Quantum(当前线程时间片大小)赋初始值,该值的大小由_KPROCESS.ThreadQuantum决定(观察ThreadQuantum大小)
- 2) 每次时钟中断会调用KeUpdateRunTime函数,用于更新当前线程的CPU时间,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd的值设置为非0,KPCR.PrcbData.QuantumEnd是一个标志,标志当前CPU时间片是否用完。
- 3)系统时钟执行完毕后,调用 KiDispatchInterrupt()判断时间片是否到期,若到期:
- ①将KPCR.PrcbData.QuantumEnd设置为0,
- ②调用KiQuantumEnd
- 重新设置_KTHREAD.Quantum时间片
- 调用KiFindReadyThread函数找到要运行的线程,存储在eax中
- 4)线程切换:切换KPCR中的CurrentThread(切换为eax的线程)
- 5)将之前的线程挂到就绪链表中:调用KiReadyThread
- 6)调用SwapContext,切换线程
5.4.3 存在备用线程
- 当存在备用线程(KPCR.PrcbData.NextThread)时
- 5.4.2中,第3)步,若时间片未到期,但是KPCR.PrcbData.NextThread不为0时
- ①将当前PKCR的CurrentThread设置为KPCR.PrcbData.NextThread
- ②将当前线程挂在就绪链表中:调用KiReadyThread
- ③调用SwapContext,切换线程、堆栈
5.4.4 总结
- (1)当前线程主动调用API:
- API函数-->KiSwapThread-->KiSwapContext-->SwapContext
- (2)当前线程时间片到期:
- KiDispatchInterrupt-->KiQuantumEnd-->SwapContext
- (3)有备用线程(KPCR.PrcbData.NextThread)
- KiDispatchInterrupt-->SwapContext
- (4)时间片没有到期,没有备用线程
- 直接return,不做处理
5.5 TSS
5.5.1 线程的内核堆栈
- _Trap_Frame
5.5.2 TSS
5.5.3 快速调用与非快速调用
- 非快速调用:通过TSS.ESP0得到0环堆栈
- 快速调用:从MSR得到一个临时0环栈以及EIP,执行EIP后仍然通过TSS.ESP0得到当前线程0环堆栈,然后才对堆栈压入那5个寄存器
5.5.4 SwapContext线程切换时TSS的变化
5.5.5 总结
- 线程切换时,TSS实际并没有发生切换,只是TSS的ESP(TSS+0x4)和Cr3的值发生了改变
- ESP:
- 获取目标线程的栈底,减去0x210后,指向TrapFrame结构体,减去0x10后,指向真正有用的数据(减去了保护模式下不使用的4个成员),然后将这个值赋值给了TSS的ESP0
- Cr3:
- 将目标进程的Cr3赋值给了当前的TSS中的Cr3
5.6 FS寄存器
- FS:[0]寄存器在3环时指向TEB,进入0环后FS:[0]指向KPCR,系统中同时存在很多个线程,这就意味着FS:[0]在3环时指向的TEB要有多个(每个线程一份)。但在实际的使用中我们发现,当我们在3环查看不同线程的FS寄存器时,FS的段选择子都是相同的,那是如何实现通过一个FS寄存器指向多个TEB呢?
5.6.1 段寄存器
5.6.2 IDA分析SwapContext函数
5.6.3 总结
- 在切换线程的时候,SwapContext函数会将目标线程的TEB取出,将TEB的32位地址分别赋值给FS指向的段描述符的3个Base位,因此,一个FS可以指向多个TEB,在SwapContext时完成的
5.7 线程优先级
- 之前的内容讲过,有三种情况会导致线程切换:
- (1)当前线程主动调用API:
- API函数 KiSwapThread KiSwapContext SwapContext
- (2)当前线程时间片到期:
- KiDispatchInterrupt KiQuantumEnd SwapContext
- (3)有备用线程(KPCR.PrcbData.NextThread)
- KiDispatchInterrupt SwapContext
- 在KiSwapThread与KiQuantumEnd函数中都是通过KiFindReadyThread来找下一个要切换的线程,KiFindReadyThread是根据什么条件来选择下一个要执行的线程呢?
5.7.1 调度链表
kd> dd KiDispatcherReadyListHead8055bc20 8055bc20 8055bc20 8055bc28 8055bc288055bc30 8055bc30 8055bc30 8055bc38 8055bc388055bc40 8055bc40 8055bc40 8055bc48 8055bc488055bc50 8055bc50 8055bc50 8055bc58 8055bc588055bc60 8055bc60 8055bc60 8055bc68 8055bc688055bc70 8055bc70 8055bc70 8055bc78 8055bc788055bc80 8055bc80 8055bc80 8055bc88 8055bc888055bc90 8055bc90 8055bc90 8055bc98 8055bc98
- KiFindReadyThread查找方式:
- 按照优先级别进行查找:31..30..29..28.....
- 也就是说,在【本次查找】中,如果级别31的链表里面有线程,那么就不会查找级别为30的链表!
5.7.2 高效查找调度链表
- 全局变量_kiReadySummary
- 判断当前调度链表是否存在线程
- 32个双向调度链表头,每一个双向链表占用8字节
- windows判断方式:判断上图的①、②和当前链表的地址
- ①=②!=当前地址,说明当前调度链表有一个线程
- ①=②=当前地址,说明当前调度链表没有调度线程
- PrcbData
5.7.3 总结
- 系统会通过KiFindReadyThread查找下一个调度线程,若调度链表中没有调度线程了,那么会找到KRCP.PrcbData.IdleThread空闲线程(PrcbData+0xC),调用这个线程
- 1.windows定义了一个变量_kiReadySummary,用于记录调度链表是否空闲
- 2.windows判断方式调度链表是否有调度线程:
- ①=②!=当前地址,说明当前调度链表有一个线程
- ①=②=当前地址,说明当前调度链表没有调度线程
- 3.线程优先级有32个,最高优先级是31,最低是0,切换线程的时候,先找31的调度链表
6. 进程挂靠
6.1 进程挂靠
6.1.1 进程线程回顾
- 一个进程可以包含多个线程,一个进程至少要有一个线程
- 进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址,Cr3确定了,线程能访问的内存也就确定了。
6.1.2 _ETHREAD中,默认使用Cr3的进程
- IDA分析SwapContext函数
- 小结
6.1.3 进程挂靠原理
- 目的是为了使当前线程可以访问其他进程的内存空间
6.1.4 NtReadVirtualMemory(读取其他进程内存)
6.1.5 总结
- 正常情况下,当前线程使用的Cr3是由其所属进程提供的(ETHREAD 0x44偏移处指定的EPROCESS),正是因为如此,A进程中的线程只能访问A的内存。
- 如果要让A进程中的线程能够访问B进程的内存,就必须要修改Cr3的值为B进程的页目录表基址(B.DirectoryTableBase),这就是所谓的“进程挂靠”。
6.2 跨进程读写内存
6.2.1 跨进程读取内存思考
6.2.2 跨进程读取内存流程
windows实现起来写了几百行代码,但是如果我们懂得了原理,自己写的话,20-30行代码即可完成


































浙公网安备 33010602011771号