Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html
存储物理页属性的PFN数据库
一、PFN的基础概念
页帧:即CPU的分页,常见的是4KB大小的分页。操作系统分页即用页帧为最小单位。
我们之前在保护模式中所学习到的PTE,其指向的就是页地址+页属性。(这个页地址是物理地址,唯一的)
页帧编号:4KB,即1000h,每个页大小1000h,即说明每个页的首地址必须是1000h的整数倍。
又因为页帧有唯一的物理地址,将该地址 x/1000h,所产生的一个唯一的编号,该编号被称为页帧编号。
PFN数据库:将每一个页帧的属性地址提取出来组成一数据结构PFN(Page Frame number),将连续的PFN组织在一起被称为页帧数据库。
这也就是说,我们只要获取某页的物理地址,就可以得知该页的页帧编号,通过页帧编号可以到PFN数据库中搜索到PFN,最后得到关于该页的属性。
二、PFN的数据结构
1. 通过wrk(windows search kernel)我们来搜索其PFN的数据结构
如下图,这个数据结构存在很多union的联合体。因为页帧有很多不同的属性(比如活动页/释放页等,后面会介绍),所以才有这么不同的union成员。
1 typedef struct _MMPFN { 2 union { 3 PFN_NUMBER Flink; 4 WSLE_NUMBER WsIndex; 5 PKEVENT Event; 6 NTSTATUS ReadStatus; 7 8 // 9 // Note: NextStackPfn is actually used as SLIST_ENTRY, however 10 // because of its alignment characteristics, using that type would 11 // unnecessarily add padding to this structure. 12 // 13 14 SINGLE_LIST_ENTRY NextStackPfn; 15 } u1; 16 PMMPTE PteAddress; 17 union { 18 PFN_NUMBER Blink; 19 20 // 21 // ShareCount transitions are protected by the PFN lock. 22 // 23 24 ULONG_PTR ShareCount; 25 } u2; 26 union { 27 28 // 29 // ReferenceCount transitions are generally done with InterlockedXxxPfn 30 // sequences, and only the 0->1 and 1->0 transitions are protected 31 // by the PFN lock. Note that a *VERY* intricate synchronization 32 // scheme is being used to maximize scalability. 33 // 34 35 struct { 36 USHORT ReferenceCount; 37 MMPFNENTRY e1; 38 }; 39 struct { 40 USHORT ReferenceCount; 41 USHORT ShortFlags; 42 } e2; 43 } u3; 44 #if defined (_WIN64) 45 ULONG UsedPageTableEntries; 46 #endif 47 union { 48 MMPTE OriginalPte; 49 LONG AweReferenceCount; 50 }; 51 union { 52 ULONG_PTR EntireFrame; 53 struct { 54 #if defined (_WIN64) 55 ULONG_PTR PteFrame: 57; 56 #else 57 ULONG_PTR PteFrame: 25; 58 #endif 59 ULONG_PTR InPageError : 1; 60 ULONG_PTR VerifierAllocation : 1; 61 ULONG_PTR AweAllocation : 1; 62 ULONG_PTR Priority : MI_PFN_PRIORITY_BITS; 63 ULONG_PTR MustBeCached : 1; 64 }; 65 } u4; 66 67 } MMPFN,
2. PFN分类
一个物理页面可能有这几种状态:活动状态、备用状态、已修改状态、已修改但不写出、转移状态、空闲状态、零化状态、坏状态。(因为篇幅限制,就先不介绍这些状态,以后有机会会补充)
因此,PFN就具有几种不同的种类,下面介绍一下。
1)活动页面的PFN数据结构
1> PFN中的PteAddress指向PTE所在的地址,ShareCount表示共享次数。只要ShareCount大于零,则就不能从内存中移除。
2> ReferenceCount表示其被引用的次数,比如锁等就是通过参考这个值来决定的。
3> u3中的e1(MMPFNENTNTRY) 表示物理页的状态,其是一个共享属性。(因为物理页被分成几类,而相同类都具有相同的属性,因此这个结构成员共享)
a. 这里面表示了一些物理页状态,比如其是否被修改,正在读或者正在写。
b. 重要的成员PageLocation(3位可以表示8种状态),其代表了当前页面状态类型,也指明了该页面在系统定义的哪个链表中。
c. MMPFN包含了指向此页面的PTE的原始内容,可能是一个原型PTE。
其用意是将一个物理页面分配给一个PTE,它记录页原来的PTE;此后当该物理页面不为它所用时,可以恢复原来的PTE
4. 最后一个成员U4指向了该页面的PTE所在的页表中的物理页帧编号,以及一些标志位。
2)备用状态或者已修改状态的PFN数据结构
1> 对于这种状态的物理页面,虽然不再有有效的PTE指向它,它页不再属于任何一个工作集,但它们就加入到系统的备用链表或修改链表中。
所以,MMPFN的第一个或者第三个4字节成员直接变为它再链表中的前后指针。
2>已修改但不写出状态的页面也是用同样的数据结构。
3)正在转移状态的PFN数据结构
a. 对于正在转移状态的,第一项要么指向一个同步时间,要么指向一个错误码。
b. 如果I/0正在继续进程中,则该事件会被通知到;如果在页面换入过程中产生错误,则ReadStatus包含了代表此I/O错误的状态码。
c.转移状态PFN项的用途是识别或消除冲突的页面错误。
3.物理页面的链表组织结构
我们之前了解过PFNDatabse,里面一个个PFN排列在一起,有不同种类。
但是这样存在一个弊端,搜索相同种类的PFN效率很顶不高,因此引出了其链表组织结构
MMLIST与MMPFNLIST数据类型
1) MMLISTS是枚举成员,其值代表物理页的属性,MMPFN数据结构的u3.e1.PageLocation域保存的就是MMLIST的某个枚举类型。
比如 PageLocatio为001,对应的物理页就是StandbyPageList类型的物理页面。
2)全局数组 mmPageLocationList将这些链表按照MMLISTS枚举值组织在一起。
a. 即开辟一个占八个元素的数据,每个成员都是PMMPFINLIST,表示一个链表头部。
b. 我们可以查看之前的MMPFN数据结构(Free类型),其u1代表前驱,u2代表后驱。
3)PFN数据库组织结构图
a.活动页面或转移状态的页面没有形成链表。
b. 零化页面或者空闲页面仅构成单链表。
c. 其余的为双循环链表。
4. 物理页面的状态变化
内存管理器根据内存的数量以及各个进程对于内存的需求,动态地调度这些物理页面地使用。
比如下列情况:
a. 当一个进程需要内存时,内存管理器可以从零化链表或者空闲链表中找到一个或多个页面来满足进程地需求。
b. 当进程退出时,内存管理器回收该进程地页面。
c. 当内存物理紧缺时,内存管理器按照特定地策略将有些进程中地页面换出到外存中,从而腾出物理内存以作它用。
三、通过windbg实战遍历PFN链表(以FreePageList为例)
1. 两个全局变量 PFNDataBase 与 nt!MmZeroedPageListHead
首先,我们需要获取两个存储在内存中的全局变量,一个是指向 PFNDataBase 数据库起始位置的指针,另一个是 FreePageList 链表。
如果不能记住名字,可以采用 x指令+通配符 模糊搜索的办法
1)PFNDataBase (其是一个指针,所以真正地址是其指向的首个地址)
kd> dd nt!MmPfnDatabase
80560b68 80c36000 0000ff00 00000000 0000000f
2 )nt!MmZeroedPageListHead
kd> dd nt!MmZeroedPageListHead
805523e0 0001111b 00000000 0001808d 00013a98
根据MMPFNLIST数据结构,对应结果为(内容与图片更改,但属性对应)
2. 获取PFN数据结构的大小
因为在PFNDATABASE数据库中这些数据连续排列,我们要知道单个数据大小从而有效拆分。
kd> dt _MMPFN -v
nt!_MMPFN
struct _MMPFN, 6 elements, 0x18 bytes
······
其总共占18个字节。
3. 通过公式查看链表
第一个地址*MMPfnDataBase+(nt!MmFreePageListHead.{Flink/Blink} * 0x18c) 获取搜寻的
之后的地址*MMPfnDataBase+(MMPFN.{u1(Flink)/u2(Blink)}*0x18c)
kd> dd 80c36000+0001808d*0x18 l6
80e76d38 00018093 00060234 ffffffff 00003000
80e76d48 0001809d 00000000
可以看到其因为是首个链表,故其Blink ffffffff(不存在)
Flink 00018093
四、通过驱动代码来遍历链表
这里全局变量的地址应该采用内存暴力搜索找到,但是技术还是不太成熟,就先强制定位到地址。
1 #include2 #include 3 #include 4 5 typedef struct _MMPFNLIST { 6 ULONG Total; 7 ULONG ListName; 8 ULONG Flink; 9 ULONG Blink; 10 }MMPFNLIST, *PMMPFNLIST; 11 12 typedef struct _MMPFN { 13 ULONG u1; 14 PULONG32 PteAddress; 15 ULONG u2; 16 ULONG u3; 17 ULONG OrininalPte; 18 ULONG u4; 19 } MMPFN, *PMMPFN; 20 21 // 查找全局变量 pfn数据库起始地址 22 // 注意: *MmPfnDataBase 为真正的起始地址 23 ULONG32 MmPfnDatabase = (ULONG32)0x80c36000; 24 // 查找全局变量 MmFreePageListHead 25 PMMPFNLIST MmFreePageListHead = (PMMPFNLIST)0x805523f0; 26 // 驱动卸载函数 27 NTSTATUS DriverUnload(PDRIVER_OBJECT Driver) { 28 return STATUS_SUCCESS; 29 } 30 31 // 驱动入口函数 32 NTSTATUS DriverEntry(PDRIVER_OBJECT Driver, PUNICODE_STRING RegPath) { 33 PMMPFN node; // 表示链表中各个结点 34 UINT32 index; 35 _asm { 36 int 3 37 } 38 // 查看链表的个数 39 ULONG32 total = MmFreePageListHead->Total; 40 // 设置好开始部分 41 index = MmFreePageListHead->Flink; 42 // 循环遍历链表 43 44 for (ULONG i = 0; i < total; i++) { 45 ULONG32 t = index * 0x18; 46 ULONG32 x = t + MmPfnDatabase; 47 // 获取结点信息 48 node = (PMMPFN)x; 49 DbgPrint("u1:%x | PteAddress:%x | u2:%x | u3:%x | OrininalPte:%x | u4: %x" 50 , node->u1, node->PteAddress, node->u2, node->u3, node->OrininalPte, node->u4); 51 DbgPrint("%x | %x", node, index); 52 // 遍历下一个结点 53 index = node->u1; 54 } 55 56 NTSTATUS status; 57 58 return STATUS_SUCCESS; 59 }