原文很长;先转部分过来,有时间看一下;

一 windows 内核情景分析---说明

说明

本文结合《Windows内核情景分析》(毛德操著)、《软件调试》(张银奎著)、《Windows核心编程》
、《寒江独钓-Windows内核安全编程》、《Windows PE权威指南》、《C++反汇编与逆向分析揭秘》以及
ReactOS操作系统 (V0.3.12)源码,以《Windows内核情景分析》为蓝本,对Windows内核重要框架、函数
、结构体进行解析

由于工程庞大,我能理解到的只是冰山一角,但本文力求做到让每个读者都能从整体上理解Windows内核
的架构,并大量解释一些关键细节。

本文解读方式:1、源码、伪码结合,展示主流程,很多时候忽略权限、错误检查,多线程互斥等旁枝末

2、函数的参数没有严格排序,很多不重要的参数也省略了,要注意

3、结构体内的成员没有严格排序,成员名称也不严格对应,并只列出一些重要成员

4、一些清理工作,如关闭句柄、释放内存、释放互斥对象等工作省略

5、很多时候,函数体开头声明的那些没有初始值的局部变量我都略去了

翻看了毛老师的大作,受益匪浅,在基本理清了原理与细节后,特此做了一番总结,
,ReactOS本来就与Windows有一些小差别,

分析的部分项目截图:

本文术语约定:

描述符:指用来描述一件事物的“结构体”。如缓冲描述符,描述了一个缓冲的基址、长度等信息。中
断描述符,描述了那个中断向量对应的分配状态、isr等信息

Entry:指表中的表项、条目,有时也指函数入口

SSDT:基本系统服务表(其实全称应叫系统服务派遣表)

Shadow SSDT:GUI/GDI系统服务函数表,这是第二张SSDT

SSDTDT:系统服务表描述符表,表中每个元素是一个SSDT描述符(注意内核中有两张SSDT和两张SSDTDT

IDT:中断描述符表,每个cpu一个。(每个表项是一个描述符,可以简单视为isr)

ISR:中断服务例程,IDT表中的中断描述符所描述的中断处理函数

EPR:异常处理例程,IDT表中的异常描述符所描述的异常处理函数

VA:虚拟地址,    PA:物理地址,   LA:线性地址,   RVA:相对虚拟地址    foa:文件偏移

PDE:页目录中的表项,保存着对应二级页表的物理地址,又叫“二级页表描述符”

PTE:二级页表中的表项,真正记录着每个虚拟页面的映射情况以及其他信息,又叫“映射描述符”

页目录:(又叫一级页表、总页表),一个PDE数组,这个数组的大小刚好占据一个页面

二级页表:一个PTE数组,这个数组的大小也刚好占据一个页面(进程有一个总页表+1024个二级页表)

AREA:地址空间中的一块连续的区段,VirtualAlloc分配内存都是以区段为单位

内存分配:表示从地址空间中用VirtualAlloc预定或者提交映射一块内存,不是指malloc、new、
HeapAlloc

PID:进程ID、进程号。(其实也是个句柄)

TID:线程ID、线程号。(其实也是个句柄)

PDO:物理设备对象,相对于fdo而言。Pdo并不一定是最底层的那个硬件pdo

FDO:功能设备对象,相对于pdo而言。Fdo也可能直接访问硬件芯片。fdo与pdo只是一种相对概念。

栈底pdo:又叫‘基石pdo’,‘硬件pdo’,指用作堆栈基石的那个pdo,它是由相应的总线驱动内部创
建的 。

端口设备对象:端口驱动或者小端口驱动中创建的设备对象(他下面是硬件pdo)

总线驱动:用来驱动总线的驱动(总线本身也是一种特殊的设备),如pci.sys总线驱动

端口驱动:由厂家提供的真正用来直接访问硬件芯片的驱动,位于总线驱动上层

功能驱动:指类驱动。如鼠标类驱动mouseclass.sys,磁盘类驱动disk.sys

上层过滤驱动:位于功能类驱动上面的驱动

下层过滤驱动:位于功能驱动下面,端口驱动上面的驱动

顶层驱动:指位于栈顶的驱动

中间驱动:intermediate drivers,凡是夹在顶层驱动与端口驱动之间的那些驱动都叫中间驱动

设备树:由PnP管理器构造的一颗用来反映物理总线布局的‘硬件设备树’。

设备节点:设备树中的节点。每个节点都表示一个真正的‘硬件pdo’

老式驱动:即NT式驱动,指不提供AddDevice或通过NtLoadDriver加载的驱动

WDM驱动:指提供了AddDevice并且不是通过NtLoadDriver加载的驱动

IRP派遣例程:又叫分发例程、派遣函数。驱动程序中用来响应处理irp的函数。(Dispatch)

设备绑定:指将设备‘堆栈’到原栈顶设备上面,成为新的栈顶设备。

文件:指物理介质上的文件(磁盘、光盘、U盘)

文件对象:每次打开设备时生成一个文件对象(文件对象不是文件,仅仅表示对设备的一次打开上下文
,因此文件对象又叫打开者)

套接字驱动:afd.sys

套接字设备:\Device\Afd\Endpoint

套接字文件对象:每打开一次套接字设备生成一个套接字文件对象

套接字FCB:每个套接字文件对象关联的FCB,用来描述套接字的其他信息

地址文件对象:每次打开传输层的tdi设备时生成的一个文件对象,用于套接字绑定

地址对象:传输层中为每个地址文件对象创建一个地址对象,用来描述一个地址(IP、端口号、协议等

Socket irp:发往afd套接字设备(即\Device\Afd\Endpoint)的irp

Tdi irp:发往传输层设备(即\Device\Tcp,\Device\Udp,\Device\RawIp)的irp

物理卷设备:指磁盘卷、光盘卷、磁带卷等物理卷设备,由相应类型的硬件驱动创建

磁盘卷设备:指磁盘分区,设备对象名为\Device\HarddiskN\PartitionN 形式(N从0开始)

文件卷设备:由文件系统内部创建的挂载(即绑定)在物理卷上的匿名设备

Cdo:控制设备对象。一个驱动通常创建有一个cdo,用来与外界通信。

FSD:文件系统驱动,File System Driver缩写。

簇:文件以簇为分配单位。一个文件包含N个簇,簇之间不必物理连续,一个簇一般为4KB

扇区:系统以扇区为单位进行磁盘IO。一个簇包含N个扇区,一个扇区一般为512B

文件块:磁盘文件中的文件块,对应于内核中的文件缓冲段

缓冲段:文件块在内核中的缓冲

ACL:访问控制表。每个Ntfs文件、内核对象都有一份ACL,记录了各用户、组的访问权限

Token:访问令牌。每个线程、进程都有一个Token,记录了包含的特权、用户、组等信息

SID:指用户ID、组ID、机器ID,用来唯一标识。

主令牌:进程自己的令牌

客户令牌:也即模拟令牌。每个线程默认使用进程的令牌,但也可模式使用其他进程的令牌

二 windows内核情景分析--系统调用

Windows的地址空间分用户模式与内核模式,低2GB的部分叫用户模式,高2G的部分叫内核模式,位于用户空
间的代码不能访问内核空间,位于内核空间的代码却可以访问用户空间

一个线程的运行状态分内核态与用户态,当指令位于用户空间时,就表示当前处于内核态,当指令位于内核
空间时,就处于内核态.

一个线程由用户态进入内核态的途径有3种典型的方式:

1、 主动通过int 2e(软中断自陷方式)或sysenter指令(快速系统调用方式)调用系统服务函数,主
动进入内核

2、 发生异常,被迫进入内核

3、 发生硬件中断,被迫进入内核

现在讨论第一种进入内核的方式:(又分为两种方式)

1、 通过老式的int 2e指令方式调用系统服务(因为老式cpu没提供sysenter指令)

如ReadFile函数调用系统服务函数NtReadFile

Kernel32.ReadFile()  //点号前面表示该函数的所在模块

{

//所有Win32 API通过NTDLL中的系统服务存根函数调用系统服务进入内核
NTDLL.NtReadFile();

}

NTDLL.NtReadFile()

{

Mov eax,152   //我们要调用的系统服务函数号,也即SSDT表中的索引,记录在eax中
   If(cpu不支持sysenter指令)
   {
      Lea edx,[esp+4] //用户空间中的参数区基地址,记录在edx中
      Int 2e  //通过该自陷指令方式进入KiSystemService,‘调用’对应的系统服务
   }
   Else
   {
      Lea edx,[esp +4] //用户空间中的参数区基地址,记录在edx中
      Sysenter //通过sysenter方式进入KiFastCallEntry,‘调用’对应的系统服务
   }
   Ret 36 //不管是从int 2e方式还是sysenter方式,系统调用都会返回到此条指令处
}

Int 2e的内部实现原理:

该指令是一条自陷指令,执行该条指令后,cpu会自动将当前线程的当前栈切换为本线程的内核栈(栈分
用户栈、内核栈),保存中断现场,也即那5个寄存器。然后从该cpu的中断描述符表(简称IDT)中找到
这个2e中断号对应的函数(也即中断服务例程,简称ISR),jmp 到对应的isr处继续执行,此时这个ISR
本身就处于内核空间了,当前线程就进入内核空间了

Int 2e指令可以把它理解为intel提供的一个内部函数,它内部所做的工作如下

Int 2e
{
   Cli  //cpu一中断,立马自动关中断
   Mov esp, TSS.内核栈地址 //切换为内核栈,TSS中记录了当前线程的内核栈地址
   Push SS
   Push esp
   Push eflags
   Push cs
Push eip  //这5项工作保存了中断现场【标志、ip、esp】
Jmp  IDT[中断号]  //跳转到对应本中断号的isr
}
 
IDT的整体布局:【异常->空白->5系->硬】(推荐采用7字口诀的方式重点记忆)

异常:前20个表项存放着各个异常的描述符(IDT表不仅可以放中断描述符,还放置了所有异常的异常处
理描述符,0x00-0x13)

保留:0x14-0x1F,忽略这块号段

空白:接下来存放一组空闲的保留项(0x20-0x29),供系统和程序员自己分配注册使用

5系:然后是系统自己注册的5个预定义的软中断向量(软中断指手动的INT指令)

(0x2A-0x2E  5个系统预注册的中断向量,0x2A:KiGetTickCount, 0x2B:KiCallbaclReturn

0x2C:KiRaiseAssertion,  0x2D:KiDebugService,  0x2E:KiSystemService)

硬:  最后的表项供驱动程序注册硬件中断使用和自定义注册其他软中断使用(0x30-0xFF)
......
......
参见《寒江独钓》一书P93页注册键盘中断时,搜索空闲未用表项是从0x20开始,到0x29结束的,就知道
为什么寒江独钓是在这段范围内搜索空白表项了(其实我们也完全可以从0x14开始搜索)
......
明白了IDT,就可以看到0x2e号中断的isr为KiSystemService,顾名思义,这个中断号专用于提供系统服
务。

在正式分析KiSystemService,前,先看下几个辅助函数

SaveTrap()  //这个函数用来保存寄存器现场和其他状态信息

{
Push 0   //LastError
Push ebp
Push ebx
Push esi
Push edi
Push fs   //此时的fs若是从用户空间自陷进来的就指着TEB,反之指着kpcr
Push kpcr.ExceptionList
Push kthread.PreviousMode
Sub esp,0x48 //腾给调式寄存器保存用
-----------至此,上面的这些语句连同int 2e中的语句在栈上构造了一个trap帧-----------------

Mov CurTrapFrame,esp  //当前Trap帧的地址
Mov CurTrapFrame.edx, kthread.TrapFrame //将上次的trap帧地址记录到edx成员中
Mov kthread.TrapFrame, CurTrapFrame, //修改本线程当前trap帧的地址
Mov kthread.PreviousMode,GetMode(进入内核前的CS)  //根据CS自动确定上次模式
Mov kpcr.ExceptionList,-1  //表示刚进入内核时,尚未安装seh
Mov fs,kpcr   //一进入内核就让fs改指向当前cpu的描述符kpcr,不再指向TEB
If(当前线程处于调试状态)
   保存DR0-DR7到trap帧中
}

FindTableCall() //这个函数用来查表,拷贝参数,调用系统服务
{
Mov edi,eax  //系统函数号,低12位为索引,第13为表示是哪张系统服务表中的索引
Mov eax, edi.低12位 //eax=真正的服务号
If(edi.第13位=1)  //if这是shadow SSDT中的系统函数号
{
   If(当前线程.服务描述符表!=shadow)
      当前线程.服务描述符表=shadow  //换用另外一张描述符表
}

服务表描述符=当前线程.服务描述符表[edi.第13位]
Mod edi=服务表描述符.base //这个系统服务表的地址
Mov ebx,[edi+eax*4]  //查表获得这个函数的地址
Mov ecx=服务表描述符.Number[eax]  //查表获得的这个系统函数的参数大小
Mov esi,edx   //esi=用户空间中的参数地址
Mov edi,esp  //esp已经为内核栈的栈顶地址
Rep movsb  //将所有参数从用户空间复制到内核空间,相当于N个连续push压参
Call  ebx  //调用对应的系统服务函数
}

......
Struct KSERVICE_TABLE_DESCRIPTOR

{
   ULONG* base;//系统服务表的地址
   ULONG* CountTable;//该系统服务表中每个函数的历史调用次数统计表
   ULONG limit;//该系统服务表的大小,也即容量
   BYTE* ArgSizeTable;//记录该系统服务表中每个函数参数大小的表   
}

2、 通过快速调用指令(Intel的是sysenter,AMD的是syscall)调用系统服务

老式的cpu不支持、不提供sysenter指令,只能由int 2e模拟中断方式进入内核,调用系统服务,

但是,那种方式有一个明显的缺点,就是速度慢!(如int 2e内部本身要保存5个寄存器的现场,然后还
要去IDT中查找isr,这个过程消耗的时间太多),因此x86系列从奔腾2代开始为系统调用专门增设了一
条sysenter指令以及相应的寄存器msr。同样,sysenter指令也可看做intel提供的一个内部函数,它做
的工作如下:

Sysenter()
{
   Mov ss,msr_ss
   Mov esp,msr_esp //关键
   Mov cs,msr_cs
   Mov eip,msr_eip //关键
}
系统在启动初始化过程中,会将上面四个msr寄存器设为固定的值,其中msr_esp为DPC函数专用堆栈,

Msr_eip则固定为KiFastCallEntry
......
......
KeGetPreviosMode()
{
Return kthread.PreviousMode;
}
这样:内核API KeGetPreviosMode的返回值就是内核模式了
......

三 windows内核情景分析--内存管理

32位系统中有4GB的虚拟地址空间

每个进程有一个地址空间,共4GB,(具体分为低2GB的用户地址空间+高2GB的内核地址空间)

各个进程的用户地址空间不同,属于各进程专有,内核地址空间部分则几乎完全相同

虚拟地址如0x11111111,  看似这8个数字是一个整体,其实是由三部分组成的,是一个三维地址,将这
个32位的值拆开,高10位表示二级页表号,中间10位表示二级页表中的页号,最后12位表示页内偏移
(2^12=4kb),因此,一个虚拟地址实际上是一个三维地址,指明了本虚拟地址在哪个二级页表,又在哪
个页以及页内偏移是多少  这三样信息!

【虚拟地址 = 二级页表号.页号.页内偏移】:口诀【页表、页号、页偏移】

Cpu访问物理内存的原理介绍:

如高级语言

DWORD  g_var;  //假设这个全局变量被编译器编译为0x00000004

g_var=100;

那么这条赋值语句编译后对应的汇编语句为:mov DWORD PTR[0x00000004],100

这里0x00000004就是一个虚拟地址,简称VA,那么这条mov 指令究竟是如何寻址的呢?

寻址过程为:CPU中的虚拟地址转换器也即MMU,将虚拟地址0x00000004转换为物理地址

具体转换过程为:

根据CR3寄存器中记录的当前进程页表的物理地址,找到总页表也即页目录,再根据虚拟地址中的页表号
,以页表号为索引,找到总页表中对应的PDE,再根据PDE,找到对应的二级页表,再以虚拟地址中的页
号部分为索引,找到二级页表中的对应PTE,再根据这个PTE记录的映射关系,找到这个虚拟页面对应的
物理页面,最后加上虚拟地址中的页内偏移部分,加上这个偏移值,就得出最后的物理地址。具体用下
面的函数可以形象表达寻址转换过程:

mov DWORD PTR[0x00000004],100 //这条指令的内部原理(没考虑二级缓冲情况)
{
va=0x00000004;//页表号=0,页号=0,页内偏移=4
      总页表=CR3;  //本进程的总页表的物理地址固定保存在cr3寄存器中
      PDE=总页表[va.页表号];  //PDE为对应的二级页表描述符
      二级页表=PDE.PageAddr;  //得出本二级页表的地址
      PTE=二级页表[va.页号];   //得出到该虚拟地址所在页面的PTE映射描述符
      If(PTE空白)  //PTE为空表示该虚拟页面尚未建立映射
         触发0x0e号页面访问异常(具体为缺页异常)
      Else
      If(PTE.bPresent==false) //PTE的这个字段表示该虚拟页面当前是否映射到了物理内存
         触发0x0e号页面访问异常(具体为缺页异常)
      Else
      If(CR0.wp==1  &&  PTE.Writable==false) //已开启页面写保护功能,就检查这个页面是否可写
         触发0x0e号页面访问异常(具体为页面访问保护越权异常)
      Else
         物理地址pa =cs.base + PTE.PageAddr + va.页内偏移  //得出对应的物理地址
      将得到的pa放到地址总线上,100放在数据总线上,经由FSB->北桥->内存总线->内存条 写入内存
}
PTE是二级页表中的表项,记录了对应虚拟页面的映射情况,这个PTE实际上可以看做一个描述符。

上面的过程比较简单,由于每次访问内存都要先访问一次PTE获取该虚拟页面对应的物理页面,再访问物
理页面读得对应的数据,因此实际上访问了两次物理内存,如果类似于每条这样的Mov指令都要访问物理
内存两次,才能获得数据,效率就很低。因此,cpu芯片中专门开辟了一个二级缓冲,用来保存那些频繁
访问的PTE,这样,cpu每次去查物理页面时,就先尝试在二级缓冲中查找对应的PTE,如果找不到,再才
去访问内存中的PTE。这样,效率就比较高,实际上绝大数情况就可以在二级缓冲中一次性找到对应的
PTE。

另外有一个问题需要说明下:va---->pa的转换过程实际上是va->la->pa,实际上PTE.PageAddr表示的是
相对于cs段的偏移,加上cs段的base基址,就得到了该页面的la线性地址。

(线性地址=段.基地址 + 段内偏移),但是由于Windows采取了Flat也即所谓的平坦分段机制,使得每
个段的基地址都在0x00000000处,长度为4GB,也即相当于Windows没有采取分段机制。前面讲过,cs是
GDT表中的索引,指向GDT表中的cs段描述符,由于Windows不分段,因此GDT中每个段描述符的基址=0,
长度=4GB,是固定的!这样一来,由于不分段,线性地址就刚好是物理地址,所以本来是由虚拟地址->
线性地址->物理地址的转换就可以直接看做虚拟地址->物理地址。

(注:在做SSDT hook、IDT hook时,由于SSDT与IDT这两张表各自所在的页面都是只读的,也即他们的
PTE中标志位标示了该页面不可写。因此,一修改SSDT、IDT就会报异常,一个简单的处理方法是是关闭
CRO中的wp即写保护位,这样就可以修改了)

前文说了,每个进程有两个地址空间,一个用户地址空间,一个内核地址空间,该地址空间的内核结构
体定义为:

Struct  MADDRESS_SPACE  //地址空间描述符
{
   MEMORY_AREA*  MemoryRoot;//本地址空间的已分配区段表(一个AVL树的根)
   VOID*  LowestAddress;//本地址空间的最低地址(用户空间是0,内核空间是0x80000000)
   EPROCESS* Process;//本地址空间的所属进程
/*一个表,表中每个元素记录了本地址空间中各个二级页表中的PTE个数,一旦某个二级页表中的PTE个
数减到了0,就自动释放该二级页面表本身,体现为稀疏数组特征*/
   USHORT* PageTableRefCountTable; 
   ULONG PageTableRefCountTableSize;//上面那个表的大小
}
地址空间中所有已分配的区段都记录在一张表中,这个表不是简单的数组,而是一个AVL树,用来提高查
找效率。每个区段的基址都对齐64KB或4KB(指64KB整倍数),各个区段之间可以有空隙,

区段的分布是很零散的!各个区段之间,夹杂的空隙就是尚未分配的虚拟内存。

注:所谓已分配区段,是指已经过VirtualAlloc预订(reserve)或提交(commit)后的虚拟内存

区段的描述符如下:

Struct  MEMORY_AREA    //区段描述符
{
   Void* StartingAddress; //开始地址,普通区段对齐64KB,其它类型区段对齐4KB
   Void* EndAddress;//结尾地址,EndAddress – StartingAddress就是该区段的大小
   MEMORY_AREA*  Parent;//AVL树中的父节点
   MEMORY_AREA*  LeftChild;//左边的子节点
   MEMORY_AREA*  RightChild;//右边的子节点

//常见的区段类型有:普通型区段、视图型区段、缓冲型区段(后面文件系统中会讲到)等

ULONG type;//本区段的类型
   ULONG protect;//本区段的保护权限,可读、可写、可执行的组合
   ULONG flags;//当初分配本区段时的分配标志
   BOOLEAN DeleteInProgress;//本区段是否标记为了‘已删除’
   ULONG PageOpCount;

Union

{

Struct //这个Struct专用于视图型区段

{

//凡是含有ROS字样的函数与结构体都表示是ReactOS与Windows中不同的实现细节
       ROS_SECTION_OBJECT*  section; 
       ULONG ViewOffest;//指本视图型区段在所在Segment内部的偏移
       MM_SECTION_SEGMENT* Segment;//所属Segment
       BOOLEAN WriteCopyView;//本视图区段是不是一个写复制区段   
    }SectionData;

LIST_ENTRY  RegionListHead;//本区段内部的所有Region区块,放在一个链表中

}Data;

}//end
浅谈区段类型:

MEMORY_AREA_VIRTUAL_MEMORY://普通型区段,由VirtuAlloc应用层用户分配的区段都是普通区段

MEMORY_AREA_SECTION_VIEW://视图型区段,用于文件映射、共享内存

MEMORY_AREA_CACHE_SEGMENT://用于文件缓冲的区段(一个簇大小)

MEMORY_AREA_PAGED_POOL://内核分页池中的区段

MEMORY_AREA_KERNEL_STACK://用于内核栈中的区段

MEMORY_AREA_PEB_OR_TEB://用于PEB、TEB的区段

MEMORY_AREA_MDL_MAPPING://内核中专用于建立MDL映射的区段

MEMORY_AREA_CONTINUOUS_MEMORY://对应的物理页面也连续的区段

MEMORY_AREA_IO_MAPPING://内核空间中用于映射外设内存(如显存)的区段

MEMORY_AREA_SHARED_DATA://内核空间中用于与用户空间共享的区段

Struct  MM_REGION  //区块描述符

{
   ULONG type;//指本区块的分配类型(预定型分配、提交型分配),又叫映射状态(已映射、尚未映
射)

ULONG protect;//本区块的访问保护权限,可读、可写、可执行的组合

ULONG length;//区块长度,对齐页面大小(4KB)

LIST_ENTRY RegionListEntry;//用来挂入所在区段的区块链表

}
内存以区段为分配单位,一个区段内部,又按分配类型、保护属性划分区块。一个区块包含一到多个内
存页面,分配类型相同并且保护权限相同的区域组成一个个的区块,因此,称为“同属性区块”。一个
区段内部,相邻区块之间的属性肯定是不相同的(分配类型或保护权限不同),若两个相邻区块的属性
相同了,会自动合并成一个新的区块。

......
......
......
创建好了section对象后,就可以让任意进程拿去映射了,不过映射是以视图为单位进行的

【section.  segment.  视图. 页面】,这是这四者之间的层级关系,请牢记

NtMapViewOfSection(hSection, ViewOffset, ViewSize,   AllocType, protect,  hProcess, void**
BaseAddr )
{
   PreviousMode=ExGetPreviousMode();
   If(PreviousMode == UserMode)
       参数检查;
   ViewOffset=Align4kb(ViewOffset);
   ViewSize=Align4kb(ViewSize);
   ObReferenceObjectByHandle(hSection---> Section);//获得对应的对象
   MmMapViewOfSection(Section, ViewOffset,ViewSize, AllocType, protect, hProcess, void**
BaseAddr );
}
MmMapViewOfSection(Section, ViewOffset, ViewSize , AllocType, protect, hProcess, void**
BaseAddr )

{

AddressSpace=process->VadRoot;//那个进程的用户地址空间

//若是PE文件的section,则加载映射文件中的每个segment,注意此时的ViewOffset和ViewSize参数不

起作用,将自动把每个完整segment当做一个视图来映射。

If(Section->AllocationAttribute  &  SEC_IMAGE)

{

ULONG i;

ULONG NrSegments;

ULONG_PTR ImageBase;

ULONG ImageSize;

PMM_IMAGE_SECTION_OBJECT ImageSectionObject;

PMM_SECTION_SEGMENT SectionSegments;

ImageSectionObject = Section->ImageSection;

SectionSegments = ImageSectionObject->Segments;//节数组

NrSegments = ImageSectionObject->NrSegments;//该pe文件中的节数

ImageBase = (ULONG_PTR)*BaseAddress;

if (ImageBase == 0)

ImageBase = ImageSectionObject->ImageBase;

ImageSize = 0;

//下面的循环遍历该pe文件中所有需要加载的节,计算所有节的大小总和

for (i = 0; i < NrSegments; i++)

{

if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD))//所需要加载这个

{

ULONG_PTR MaxExtent;

//该节的rva+该节的对齐4KB长度

MaxExtent=SectionSegments[i].VirtualAddress + SectionSegments[i].Length;

ImageSize = max(ImageSize, MaxExtent);

}

}

ImageSectionObject->ImageSize = ImageSize;

//如果该pe文件期望加载的区域中有任何一个地方被占用了,重定位,dll文件一般都会重定位

if (MmLocateMemoryAreaByRegion(AddressSpace, ImageBase,PAGE_ROUND_UP(ImageSize)))

{

if ((*BaseAddress) != NULL)//如果用户的要求是必须加载到预期地址处,返回失败!

return(STATUS_UNSUCCESSFUL);

ImageBase = MmFindGap(AddressSpace, ImageSize, PAGE_SIZE, FALSE);//重定位,找空闲

}

//一次性加载映射该pe文件中的所有节

for (i = 0; i < NrSegments; i++)

{

//注意pe文件中有的节是不用加载的

if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD))

{

PVOID SBaseAddress =  ((char*)ImageBase + (SectionSegments[i].VirtualAddress);

//把该节整体当做一个view进行映射。由此可见,pe文件中的每个节也是一个视图型区段

MmMapViewOfSegment(AddressSpace,

Section,

&SectionSegments[i],//该视图所在的第一个节

&SBaseAddress,//该节的预期映射地址

SectionSegments[i].Length,//ViewSize=整个节的长度

SectionSegments[i].Protection,

0,//ViewOffset=0

0);

}

}

*BaseAddress = (PVOID)ImageBase;//返回该PE文件实际加载映射的地址

}

Else//普通数据文件和页文件的section,都只有一个segment

{

MmMapViewOfSegment(AddressSpace,  section, section->segmen,   ViewOffset, ViewSize ,  
AllocType & MEM_TOPDOWN,  protect,  hProcess,  void** BaseAddr);

}

}
MmMapViewOfSegment(AddressSpace,  section, segmen,   ViewOffset, ViewSize ,  AllocType,  
protect,  hProcess,  void** BaseAddr);

{

MEMORY_AREA*  Area;

MmCreateMemoryArea(AddressSpace,  视图型区段,  BaseAddr,ViewSize,  protect, AllocType,
&Area);

//记录本视图区段映射的是哪个section的哪个segment中的哪个位置

Area->Data.SectionData.Section=Section;

Area->Data.SectionData.Segment=segment;

Area->Data.SectionData.ViewOffset=ViewOffset;

Area->Data.SectionData..WriteCopyView=FALSE;//视图型区段默认是不‘写复制’的

初始化Area区段中的区块链表;//初始时,整个区段中就一个区块

}
......
   if (NumberOfBytes > PageSize-BlockHeadSize)//超出一个页面

{

//大于一个页面大小的分配特殊处理;

Retun  MiAllocatePoolPages(PoolType, NumberOfBytes);;

}

For(遍历空闲块表)

{

If(找到了一个合乎大小的空闲块)

{

从空闲块链表中摘下一个合乎大小的块;

前后合并相邻块;//是在一个页面内分隔、合并

Return 找到的块地址;

}

}

//如果已有的空闲链表找不到这样一个大小的块

在池中分配一个新的页面;

在新页面中把前面的部分割出来,后面剩余的部分挂入池中的空闲块表中;

Return 分得的块地址

}
内核中池的分配原理同用户空间中的堆一样,都是先用VirtuAllocate去分配一个页面,然后在这个页面

寻找空闲块,分给用户。每个池块的块头含有一些附加信息,如这个池块的大小,池类型,该池块的tag
标记等信息。用户空间中的malloc,new堆块分配函数,都是调用HeapAlloc API函数从堆管理器维护的N
个虚拟页面中分出一些零散的块出来,每个堆块的块头、块尾也含有一些附加信息,如堆块大小,防止
堆块溢出的cookie等信息。堆管理器则在底层调用VirtualAlloc  API分配,增长虚拟页面,提供底层服
务。

......

四 Windows内核情景分析---内核对象

写过Windows应用程序的朋友都常常听说“内核对象”、“句柄”等术语却无从得知他们的内核实现到底是怎样的, 本篇文章就揭开这些技术的神秘面纱。

常见的内核对象有:

Job、Directory(对象目录中的目录)、SymbolLink(符号链接),Section(内存映射文件)、Port(LPC端口)、IoCompletion(Io完成端口)、File(并非专指磁盘文件)、同步对象(Mutex、Event、Semaphore、Timer)、Key(注册表中的键)、Token(用户/组令牌)、Process、Thread、Pipe、Mailslot、Debug(调试端口)等

内核对象就是一个数据结构,就是一个struct结构体,各种不同类型的对象有不同的定义,本片文章不专门介绍各个具体对象类型的结构体定义,只讲述一些公共的对象管理机制。

至于各个具体对象类型的结构体定义,后文逐步会有详细介绍。

所有内核对象都遵循统一的使用模式:
第一步:先创建对象;
第二步:打开对象,得到句柄(可与第一步合并在一起,表示创建时就打开)
第三步:通过API访问对象;
第四步,关闭句柄,递减引用计数;
第五步:句柄全部关完并且引用计数降到0后,销毁对象。

句柄就是用来维系对象的把柄,就好比N名纤夫各拿一条绳,同拉一艘船。每打开一次对象就可拿到一个句柄,表示拿到该对象的一次访问权。

内核对象是全局的,各个进程都可以访问,比如两个进程想要共享某块内存来进行通信,就可以约定一个对象名,然后一个进程可以用CreatFileMapping(”SectionName”)创建一个section,而另一个进程可以用OpenFileMapping(”SectionName”)打开这个section,这样这个section就被两个进程共享了。

(注意:本篇说的都是内核对象的句柄。像什么hWnd、hDC、hFont、hModule、hHeap、hHook等等其他句柄,并不是指内核对象,因为这些句柄值不是指向进程句柄表中的索引,而是另外一种机制)

各个对象的结构体虽然不同,但有一些通用信息记录在对象头中,看下面的结构体定义

typedef struct _OBJECT_HEADER
{
    LONG PointerCount;//引用计数
    union
    {
        LONG HandleCount;//本对象的打开句柄计数(每个句柄本身也占用一个对象引用计数)
        volatile VOID* NextToFree;//下一个要延迟删除的对象
    };

OBJECT_TYPE* Type;//本对象的类型,类型本身也是一种内核对象,因此我习惯叫‘类型对象’
    UCHAR NameInfoOffset;//对象名的偏移(无名对象没有Name)
    UCHAR HandleInfoOffset;//各进程的打开句柄统计信息数组
    UCHAR QuotaInfoOffset;//对象本身实际占用内存配额(当不等于该类对象的默认大小时要用到这个)
    UCHAR Flags;//对象的一些属性标志

union
    {
        OBJECT_CREATE_INFORMATION* ObjectCreateInfo;//来源于创建对象时的OBJECT_ATTRIBUTES
        PVOID QuotaBlockCharged;
    };

PSECURITY_DESCRIPTOR SecurityDescriptor;//安全描述符(对象的拥有者、ACL等信息)
    QUAD Body;//通用对象头后面紧跟着真正的结构体(这个字段是后面真正结构体中的第一个成员)

} OBJECT_HEADER, *POBJECT_HEADER;

如上,Body就是对象体中的第一个字段,头部后面紧跟具体对象类型的结构体定义

typedef struct _OBJECT_HEADER_NAME_INFO
{
    POBJECT_DIRECTORY Directory;//对象目录中的父目录(不一定是文件系统中的目录)
    UNICODE_STRING Name;//相对于Directory的路径或者全路径
ULONG QueryReferences;//对象名查询操作计数

} OBJECT_HEADER_NAME_INFO, *POBJECT_HEADER_NAME_INFO;

typedef struct _OBJECT_HEADER_CREATOR_INFO
{
    LIST_ENTRY TypeList;//用来挂入所属‘对象类型’中的链表(也即类型对象内部的对象链表)
PVOID CreatorUniqueProcess;//表示本对象是由哪个进程创建的

} OBJECT_HEADER_CREATOR_INFO, *POBJECT_HEADER_CREATOR_INFO;

对象头中记录了NameInfo、HandleInfo、QuotaInfo、CreatorInfo这4种可选信息。如果这4种可选信息全部都有的话,整个对象的布局从低地址到高地址的内存布局为:

QuotaInfo-> HandleInfo->NameInfo->CreatorInfo->对象头->对象体;这4种可选信息的相对位置倒不重要,但是必须记住,他们都是在对象头中的上方(也即对象头上面的低地址端)。以下为了方便,不妨叫做“对象头中的可选信息”、“头部中的可选信息”。

于是有宏定义:

//由对象体的地址得到对象头的地址

#define OBJECT_TO_OBJECT_HEADER(pBody)    CONTAINING(pBody,OBJECT_HEADER,Body)

//得到对象的名字

#define OBJECT_HEADER_TO_NAME_INFO(h)

h->NameInfoOffset?(h - h->NameInfoOffset):NULL

//得到对象的创建者信息

#define OBJECT_HEADER_TO_CREATOR_INFO(h)

h->Flags & OB_FLAG_CREATOR_INFO?h-sizeof(OBJECT_HEADER_CREATOR_INFO):NULL

所有有名字的对象都会进入内核中的‘对象目录’中,对象目录就是一棵树。内核中有一个全局指针变量ObpRootDirectoryObject,就指向对象目录树的根节点,根节点是一个根目录。

对象目录的作用就是用来将对象路径解析为对象地址。给定一个对象路径,就可以直接在对象目录中找到对应的对象。就好比给定一个文件的全路径,一定能从磁盘的根目录中向下一直搜索找到对应的文件。

如某个设备对象的对象名(全路径)是”\Device\MyCdo”,那么从根目录到这个对象的路径中:

Device是根目录中的子目录,MyDevice则是Device目录中的子节点。

对象有了名字,应用程序就可以直接调用CreateFile打开这个对象,获得句柄,没有名字的对象无法记录到对象目录中,应用层看不到,只能由内核自己使用。

内核中各种类型的对象在对象目录中的位置:

目录对象:最常见,就是对象目录中的目录节点(可以作为叶节点)

普通对象:只能作为叶节点

符号链接对象:只能作为叶节点

注意文件对象和注册表中的键对象看似有文件名、键名,但此名非对象名。因此,文件对象与键对象是无名的,无法进入对象目录中

根目录也是一种目录对象,符号链接对象可以链接到对象目录中的任何节点,包括又链向另一个符号链接对象。

对象目录中,每个目录节点下面的子节点可以是

1、 普通对象节点

2、 子目录

3、 符号链接

该目录中的所有子节点对象都保存在该目录内部的目录项列表中。不过,这个列表不是一个简单的数组,而是一个开式hash表,用来方便查找。根据该目录中各个子对象名的hash值,将对应的子对象挂入对应的hash链表中,用hash方式存储这些子对象以提高查找效率

目录本身也是一种内核对象,其类型就叫“目录类型”,现在就可以看一下这种对象的结构体定义:

typedef struct _OBJECT_DIRECTORY
{
    struct _OBJECT_DIRECTORY_ENTRY*  HashBuckets[37];//37条hash链
    EX_PUSH_LOCK Lock;
    struct _DEVICE_MAP *DeviceMap;
    …
} OBJECT_DIRECTORY, *POBJECT_DIRECTORY;

如上,目录对象中的所有子对象按hash值分门别类的安放在该目录内部不同的hash链中

其中每个目录项的结构体定义为:

typedef struct _OBJECT_DIRECTORY_ENTRY
{
    struct _OBJECT_DIRECTORY_ENTRY * ChainLink;//下一个目录项(即下一个子节点)
    PVOID Object;//对象体的地址
    ULONG HashValue;//所在hash链
} OBJECT_DIRECTORY_ENTRY, *POBJECT_DIRECTORY_ENTRY;

看到没,每个目录项记录了指向的对象的地址,同时间接记录了对象名信息

下面这个函数用来在指定的目录中查找指定名称的子对象

VOID*

ObpLookupEntryDirectory(IN POBJECT_DIRECTORY Directory,

IN PUNICODE_STRING Name,

IN ULONG Attributes,

IN POBP_LOOKUP_CONTEXT Context)

{

BOOLEAN CaseInsensitive = FALSE;
    PVOID FoundObject = NULL;

//表示对象名是否严格大小写匹配查找

if (Attributes & OBJ_CASE_INSENSITIVE) CaseInsensitive = TRUE;

HashValue=CalcHash(Name->Buffer);//计算对象名的hash值

HashIndex = HashValue % 37;//获得对应的hash链索引

//记录本次是在那条hash中查找

Context->HashValue = HashValue;

Context->HashIndex = (USHORT)HashIndex;

if (!Context->DirectoryLocked)

ObpAcquireDirectoryLockShared(Directory, Context);//锁定目录,以便在其中进行查找操作

//遍历对应hash链中的所有对象

AllocatedEntry = &Directory->HashBuckets[HashIndex];

LookupBucket = AllocatedEntry;

while ((CurrentEntry = *AllocatedEntry))

{

if (CurrentEntry->HashValue == HashValue)

{
            ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentEntry->Object);

HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);

if ((Name->Length == HeaderNameInfo->Name.Length) &&

(RtlEqualUnicodeString(Name, &HeaderNameInfo->Name, CaseInsensitive)))

{
                break;//找到对应的子对象
            }

}

AllocatedEntry = &CurrentEntry->ChainLink;

}

if (CurrentEntry)//如果找到了子对象
    {

if (AllocatedEntry != LookupBucket)

将找到的子对象挂入链表的开头,方便下次再次查找同一对象时直接找到;

FoundObject = CurrentEntry->Object;

}

if (FoundObject) //如果找到了子对象

{

ObjectHeader = OBJECT_TO_OBJECT_HEADER(FoundObject);

ObpReferenceNameInfo(ObjectHeader);//递增对象名字的引用计数

ObReferenceObject(FoundObject);//注意递增了对象本身的引用计数

if (!Context->DirectoryLocked)

ObpReleaseDirectoryLock(Directory, Context);

}

//检查本次函数调用前,查找上下文中是否已有一个先前的中间节点对象,若有就释放

if (Context->Object)

{

ObjectHeader = OBJECT_TO_OBJECT_HEADER(Context->Object);

HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);

ObpDereferenceNameInfo(HeaderNameInfo);

ObDereferenceObject(Context->Object);

}

Context->Object = FoundObject;

return FoundObject;//返回找到的子对象

}

如上,hash查找子对象,找不到就返回NULL。

注意由于这个函数是在遍历路径的过程中逐节逐节的调用的,所以会临时查找中间的目录节点,记录到Context中。

......

五 windows内核情景分析---进程线程

本篇主要讲述进程的启动过程、线程的调度与切换、进程挂靠

进程的启动过程:

BOOL CreateProcess

(

LPCTSTR lpApplicationName,                 //

LPTSTR lpCommandLine,                      // command line string

LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD

LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD

BOOL bInheritHandles,                     //

DWORD dwCreationFlags,                    // creation flags

LPVOID lpEnvironment,                     // new environment block

LPCTSTR lpCurrentDirectory,               // current directory name

LPSTARTUPINFO lpStartupInfo,               // startup information

LPPROCESS_INFORMATION lpProcessInformation // process information

);

这个Win32API在内部最终调用如下:

CreateProcess(…)

{

NtCreateProcess(…);//间接调用这个系统服务,先创建进程

NtCreateThread(…);//间接调用这个系统服务,再创建该进程的第一个线程(也即主线程)

}

进程的4GB地址空间分两部分,内核空间+用户空间

看下面几个定义:

#define MmSystemRangeStart  0x80000000 //系统空间的起点

#define MM_USER_PROB_ADDRESS  MmSystemRangeStart-64kb  //除去高端的64kb隔离区

#define MM_HIGHEST_USER_ADDRESS   MmUserProbAddress-1 //实际的用户空间中最高可访问地址

#define MM_LOWEST_USER_ADDRESS  64kb  //实际的用户空间中最低可访问地址

#define KI_USER_SHARED_DATA  0xffdf0000   //内核空间与用户空间共享的一块区域

由此可见,用户地址空间的范围实际上是从  64kb---->0x80000000-64kb 这块区域。

(访问NULL指针报异常的原因就是NULL(0)落在了最前面的64kb保留区中)

内核中提供了一个全局结构变量,该结构的类型是KUSER_SHARED_DATA。内核中的那个结构体变量所在的虚拟页面起始地址为:0xffdf0000,大小为一个页面大小。这个内核页面对应的物理内存页面也映射到了每个进程的用户地址空间中,而且是固定映在同一处:0x7ffe0000。这样,用户空间的程序直接访问用户空间中的这个虚拟地址,就相当于直接访问了内核空间中的那个公共页面。所以,那个内核页面称之为内核空间提供给各个进程的一块共享之地。(事实上,这个公共页面非常有用,可以在这个页面中放置代码,应用程序直接在r3层运行这些代码,如在内核中进行IAT hook)
......

六 Windows内核情景分析 --APC

明白了APC大致原理后,现在详细看一下APC的工作原理。

APC分两种,用户APC、内核APC。前者指在用户空间执行的APC,后者指在内核空间执行的APC。

先看一下内核为支持APC机制提供的一些基础结构设施。

Typedef struct _KTHREAD

{

KAPC_STATE  ApcState;//表示本线程当前使用的APC状态(即apc队列的状态)

KAPC_STATE  SavedApcState;//表示保存的原apc状态,备份用

KAPC_STATE* ApcStatePointer[2];//状态数组,包含两个指向APC状态的指针

UCHAR ApcStateIndex;//0或1,指当前的ApcState在ApcStatePointer数组中的索引位置

UCHAR ApcQueueable;//指本线程的APC队列是否可插入apc

ULONG KernelApcDisable;//禁用标志

//专用于挂起操作的APC(这个函数在线程一得到调度就重新进入等待态,等待挂起计数减到0)

KAPC SuspendApc;

}KTHREAD;

Typedef struct _KAPC_STATE //APC队列的状态描述符

{

LIST_EBTRY  ApcListHead[2];//每个线程有两个apc队列

PKPROCESS Process;//当前线程所在的进程

BOOL KernelApcInProgress;//指示本线程是否当前正在 内核apc

BOOL KernelApcPending;//表示内核apc队列中是否有apc

BOOL UserApcPending;//表示用户apc队列中是否apc

}

Typedef enum _KAPC_ENVIRONMENT

{

OriginalApcEnvironment,//0,状态数组索引

AttachedApcEnvironment;//1,状态数组索引

CurrentApc Environment;//2,表示使用当前apc状态

CurrentApc Environment;//3,表示使用插入apc时那时的线程的apc状态

}

七 Windows内核情景分析---线程同步

基于同步对象的等待、唤醒机制:

一个线程可以等待一个对象或多个对象而进入等待状态(也叫睡眠状态),另一个线程可以触发那个等待对象,唤醒在那个对象上等待的所有线程。

一个线程可以等待一个对象或多个对象,而一个对象也可以同时被N个线程等待。这样,线程与等待对象之间是多对多的关系。他们之间的等待关系由一个队列和一个‘等待块’来控制,等待块就是线程与等待目标对象之间的纽带。

WaitForSingleObject可以等待那些“可等待对象”,哪些对象是‘可等待’的呢?进程、线程、作业、文件对象、IO完成端口、可等待定时器、互斥、事件、信号量等,这些都是‘可等待’对象,可用于WaitForSingleObject等函数。

‘可等待’对象又分为‘可直接等待对象’和‘可间接等待对象’

互斥、事件、信号量、进程、线程这些对象由于内部结构中的自第一个字段是DISPATCHER_HEADER结构(可以看成是继承了DISPATCHER_HEADER),因此是可直接等待的。而文件对象不带这个结构,但文件对象内部有一个事件对象,因此,文件对象是‘可间接等待对象’。

比如:信号量就是一种可直接等待对象,它的结构如下:

Struct KSEMAPHORE
{
   DISPATCHER_HEADER Header;//公共头
   LONG Limit;//最大信号量个数
}

Struct DISPATCHER_HEADER
{


   LONG SignalState;//信号状态量(>0表示有信号,<=0表示无信号)
   LIST_ENTRY WaitListHead;//等待块队列
   …
}

WaitForSingleObject内部最终调用下面的系统服务

NTSTATUS
NtWaitForSingleObject(IN HANDLE ObjectHandle,//直接或间接可等待对象的句柄
                      IN BOOLEAN Alertable,//表示本次等待操作是否可被吵醒(即被强制唤醒)
                      IN PLARGE_INTEGER TimeOut  OPTIONAL)//超时
{

PVOID Object, WaitableObject;
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    LARGE_INTEGER SafeTimeOut;

NTSTATUS Status;

if ((TimeOut) && (PreviousMode != KernelMode))
    {
        _SEH2_TRY
        {
            SafeTimeOut = ProbeForReadLargeInteger(TimeOut);
            TimeOut = &SafeTimeOut;
        }

_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            _SEH2_YIELD(return _SEH2_GetExceptionCode());
        }

_SEH2_END;

}

Status = ObReferenceObjectByHandle(ObjectHandle,SYNCHRONIZE,NULL,PreviousMode,
                                       &Object,NULL);

if (NT_SUCCESS(Status))
{

//得到那个对象的‘可直接等待对象’DefaultObject
        WaitableObject = OBJECT_TO_OBJECT_HEADER(Object)->Type->DefaultObject;

if (IsPointerOffset(WaitableObject))//if DefaultObject是个偏移,不是指针
        {
            //加上偏移值,获得内部的‘可直接等待对象’
            WaitableObject = (PVOID)((ULONG_PTR)Object + (ULONG_PTR)WaitableObject);
        }

_SEH2_TRY
        {
            Status = KeWaitForSingleObject(WaitableObject,//这个函数只能等待‘直接等待对象’
                                           UserRequest,PreviousMode,Alertable,TimeOut);
        }

_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            Status = _SEH2_GetExceptionCode();
        }

_SEH2_END;
        ObDereferenceObject(Object);
    }
    return Status;
}

#define IsPointerOffset(Ptr) ((LONG)(Ptr) >= 0)
如上,每个对象的对象类型都有一个默认的可直接等待对象,要么直接指向对象,要么是个偏移值。

如果是个偏移值,那么DefaultObject值的最高位为0,否则为1。
......

八 windows内核情景分析--窗口消息

消息与钩子

众所周知,Windows系统是消息驱动的,现在我们就来看Windows的消息机制.

早期的Windows的窗口图形机制是在用户空间实现的,后来为了提高图形处理效率,将这部分移入内核空间,在Win32k.sys模块中实现。这个模块作为一个扩展的内核模块,提高了一个扩展额系统服务表,专用于窗口图形操作,相应的,这个模块中添加了一个扩展系统调用服务表Shadow SSDT,以及一个扩展的系统调用服务表描述符表:KeServiceDescriptorTableShadow.(系统中 不仅有两张SSDT,还有两张系统服务表描述符表)。当一个线程首次调用这个模块中的系统服务函数时,这个线程就自然变成了GUI线程。GUI线程结构的ServiceTable指向的就是这个shadow描述符表。

指向这个表的系统服务号的bit12位(也即第13位)为1,如0x1XXX表示使用的是shadow服务表。

每个线程创建时都是普通线程,但是只要那个线程在运行的过程中发起了一次对win32k.sys模块中的系统调用,就会转变成GUI线程,下面的函数就是这个用途。

NTSTATUS  PsConvertToGuiThread(VOID)
{
    ULONG_PTR NewStack;
    PVOID OldStack;
    PETHREAD Thread = PsGetCurrentThread();
    PEPROCESS Process = PsGetCurrentProcess();
    NTSTATUS Status;

if (KeGetPreviousMode() == KernelMode) return STATUS_INVALID_PARAMETER;

ASSERT(PspW32ProcessCallout != NULL);//确保win32k.sys模块已加载到内存

if (Thread->Tcb.ServiceTable != KeServiceDescriptorTable)
        return STATUS_ALREADY_WIN32;//表示先前已经转换为GUI线程了

if (!Thread->Tcb.LargeStack)//if 尚未换成大内核栈
    {

NewStack = (ULONG_PTR)MmCreateKernelStack(TRUE, 0);//分配一个64KB的大内核栈
        //更为大内核栈
        OldStack = KeSwitchKernelStack(NewStack, (NewStack - KERNEL_STACK_SIZE));
        MmDeleteKernelStack(OldStack, FALSE);//销毁原来的普通内核栈
    }

if (!Process->Win32Process)//if 尚未分配W32PROCESS结构(也即if是该进程中的第一个GUI线程)
        Status = PspW32ProcessCallout(Process, TRUE);//分配Win32Process结构(表示GUI进程)

Thread->Tcb.ServiceTable = KeServiceDescriptorTableShadow;//关键。更改描述符表

//为当前线程分配一个W32THREAD结构
    Status = PspW32ThreadCallout(Thread, PsW32ThreadCalloutInitialize);
    if (!NT_SUCCESS(Status)) Thread->Tcb.ServiceTable = KeServiceDescriptorTable;//改为原来的
    return Status;

}
如上,每个线程在转换为GUI线程时,必须换用64KB的大内核栈,因为普通的内核栈只有12KB大小,不能支持开销大的图形任务。然后分配一个W32PROCESS结构,将进程转换为GUI进程,然后分配W32THREAD结构,更改系统服务表描述符表。上面的PspW32ProcessCallout和PspW32ThreadCallout函数都是回调函数,分别指向win32k.sys模块中的Win32kProcessCallback、Win32kThreadCallback函数。
......

windows 内核情景分析相关推荐

  1. [14]Windows内核情景分析 --- 文件系统

    文件系统 一台机器上可以安装很多物理介质来存放资料(如磁盘.光盘.软盘.U盘等).各种物理介质千差万别,都配备有各自的驱动程序,为了统一地访问这些物理介质,windows设计了文件系统机制.应用程序要 ...

  2. windows内核情景分析 --- 文件系统

    文件系统 一台机器上可以安装很多物理介质来存放资料(如磁盘.光盘.软盘.U盘等).各种物理介质千差万别,都配备有各自的驱动程序,为了统一地访问这些物理介质,windows设计了文件系统机制.应用程序要 ...

  3. windows内核情景分析读书笔记-----HYPERSPACE

    主要介绍HYPERSPACE的创建映射函数 赏光看我这一系列文章的朋友最好结合毛德操老师的书来看,具体的细节我这里就不阐述了 简单说下这个函数功能 Windows内核有时候需要把某些物理页面临时映射到 ...

  4. Windows内核情景分析-概述

    现在的Windows 现在的windows内核包含了两大部分,一部分是本来意面上的操作系统内核,另一部分则是移到了内核中的视窗服务,前者对应ntoskrnl.exe后者win32k.sys:后者部分为 ...

  5. windows内核情景分析---进程线程2

    二.线程调度与切换 众所周知:Windows系统是一个分时抢占式系统,分时指每个线程分配时间片,抢占指时间片到期前,中途可以被其他更高优先级的线程强制抢占. 背景知识:每个cpu都有一个TSS,叫'任 ...

  6. windows内核情景分析---进程线程1

    本篇主要讲述进程的启动过程.线程的调度与切换.进程挂靠 一.进程的启动过程: BOOL CreateProcess ( LPCTSTR lpApplicationName,               ...

  7. Windows内核情景分析 笔记

    803页:WDK文档强调IoRegisterDriverReinitialization 主要用于同时支持Non-PNP和PNP下层的驱动.大概原因是:只依赖Legacy下层的驱动可以通过LoadOr ...

  8. [6]Windows内核情景分析 --APC

    APC:异步过程调用.这是一种常见的技术.前面进程启动的初始过程就是:主线程在内核构造好运行环境后,从KiThreadStartup开始运行,然后调用PspUserThreadStartup,在该线程 ...

  9. [9]Windows内核情景分析 --- DPC

    DPC不同APC,DPC的全名是'延迟过程调用'. DPC最初作用是设计为中断服务程序的一部分.因为每次触发中断,都会关中断,然后执行中断服务例程.由于关中断了,所以中断服务例程必须短小精悍,不能消耗 ...

最新文章

  1. java如何重写_java中如何重写一个方法
  2. springmvc处理ajax请求
  3. [蓝桥杯2019初赛]质数-质数筛or 水题
  4. springboot+shiro+jwt实现token认证登录
  5. RANSAC算法与原理(二)
  6. Dreamweaver cc 2019
  7. oracle及操作系统对于文件大小的限制
  8. win10无法打开匿名级安全令牌_无法打开匿名级安全令牌
  9. jmeter压测学习11-模拟浏览器访问web页面
  10. Ajax异步请求之设置Content-Type
  11. netty tcp空闲设置
  12. python菜鸟教程 | 打印菱形
  13. 旋转矩阵、变换矩阵,李群(Lie Group)、李代数(Lie Algebra)及扰动模型
  14. 硅谷领军行动:两大诺贝尔得主同时空降,黑石摩根解密晋级风控,斯坦福专家点睛区块链全图谱...
  15. 有限个无穷小的和也是无穷小
  16. IBM公司长久不衰的秘密是什么?
  17. RenPy今天更新到6.4.0
  18. Android编程实现修改设备WiFi名称
  19. 电脑显示问题:问题描述:1、显卡风扇声音大2、显示器显示模糊3、显示器自动息屏4、双屏设置问题
  20. etcdctl 基本使用

热门文章

  1. TensorFlow patch块划分(transpose and reshape)
  2. 何杰月c语言课程,北京西城区教育科研月:学科核心素养的教学探索
  3. js 获取字符串中最后一个斜杠前面/后面的内容
  4. Learn About Salesforce Flow for Service
  5. GridBagLayout布局管理器应用详解
  6. 8个独立按键控制LED
  7. CTFshow 反序列化 web275
  8. 2015年蓝桥杯省赛第5题--九数组分数
  9. 参考文献找不全页码?
  10. 计算机在线平方,完全平方数批量判断在线计算器_三贝计算网_23bei.com