Windows XP x86分页机制分析
前言
32位的x86 CPU支持两种分页方式,分别是10 10 12分页和2 9 9 12分页,其中2 9 9 12分页模式称为PAE分页。10 10 12分页模式下,PDE和PTE的每一项只占用4字节,此模式下CPU不支持数据执行保护。而PAE分页中PAPTE、PDE、PTE中的每一项都占用8字节,这8字节能够拿出更多的位作为页保护属性和物理地址访问。所以PAE分页模式相对10 10 12分页模式增加了数据执行保护和更多的物理地址访问能力。也正因此PAE分页模式更被广泛的使用。
PAE分页模式
PAE分页模式将32位的线性地址按2 9 9 12位方式进行拆分,作为各级页目录项的索引,PAE分页模式使用3级页表,分别是PAPTE、PDE、PTE。
总共有2^2=4项PAPTE,每一项占8字节,总共占32字节
每项PAPTE有2^9=512项PDE,每一项占8字节,每一个PDE页表总共占4KB字节
PTE有2^9=512项每一项占8字节,总共占4KB字节
当目标进程获得被执行权限时,cpu的cr3寄存器保存了该进程的PAPTE表的基地址的物理地址。
实验验证
现在通过一例将线性地址转换为物理地址的实验,来完成对PAE分页的认识。首先任务是在目标进程中找到一个线性地址(注意:线性地址=虚拟地址+段内偏移,由于段内偏移基本位为0,所以通常情况下虚拟地址=线性地址,为方便描述,之后的虚拟地址都用线性地址来称呼)然后对其进行分析得到对应的物理地址,通过两处地址中的内容比对来验证PAE分页机制。
这里被调试的XP系统是一台采用2 9 9 12分页的虚拟机,而物理机则是安装有windbg调试器的Windows10操作系统。
对DebugView.exe进行内存搜索,发现显示字符串“8322200”的线性地址为9b400f。
使用CheatEngine.exe工具将该地址内容修改为“helloWord.”,发现WinDebug.exe进程的内容也变成“helloWord.”,这表明使用CheatEngine.exe搜索到的地址是正确的。
接下来在windbg中使用!process 0 0查看所有进程的信息,这里主要查看DebugView的进程信息:
得到DebugView.exe的进程信息后,使用.process /i 82061da0切换到该进程的空间中。
使用db 9b400f命令以byte单位查看DebugView.exe进程的9b400f虚拟内存的数据:
现在开始通过cr3寄存器查看9b400f地址的物理地址,也就是说使用物理地址方式查看该部分的数据。
首先,通过之前的!process 0 0命令我们已经查看到DebugView.exe的分页使用的基地址的物理地址是2b40300,另外对9b400f按二进制拆分成2 9 9 12方式:
拆分值 | 十六进制值 |
00 | 0 |
000000 100 | 4 |
11011 0100 | 1b4 |
0000 0000 1111 | f |
由于开始的偏移是0,那么开始使用命令!dq 2b40300+0*8获得PDPTE 值:
显示的是1d825801,将后三位替换成0后是1d825000,偏移是4,则使用命令!dq 1d825000+4*8获得了PDE的值:
(注意:1d825801中低12位是页属性位,前面的30位是页针编号。页针编号是物理内存从低到高每4KB一个单位指定的编号,每一级页表都是如此)
显示的值是1d6b2867,将后三位替换成0后是1d6b2000,偏移是1b4,则使用命令!dq 1d6b2000+1b4*8获得了PTE的值:
显示的值是1daa3825,将后三位替换成0后是1daa3000,而最后的页内偏移是f,所以使用命令!db 1daa3000+f*1获得保存的数据:
所以在当前运行下,DebugView.exe进程内线性地址9b400f对应的物理地址是1daa300f。
对于以上步骤,可以通过在windbg使用命令!vtop 进程的cr3值 线性地址 查看其将线性地址转译成物理地址的过程,这里也即执行命令!vtop 02b40300 9b400查看线性地址到物理地址的转译过程:
上图显示线性地址转换位物理地址的转译过程。
通过以上过程了解了线性地址转译成物理地址的过程。但是,在保护模式下,程序无法直接访问物理内存,只能通过虚拟地址进行访问数据。那么如何才能通过虚拟地址访问内存方式获得各级页表的数据呢?
在Windbg中使用命令!pte 虚拟地址 可以获得该虚拟地址的各级页目录项所在的虚拟地址:
上图表示虚拟地址009b400f的PDE项即PDT在虚拟地址C0600020中保存,PTE项即PTT在虚拟地址C0004DA0中保存。
从对0地址的查看可以知道,实际上微软故意的将32位操作系统的PAE分页模式的PDE基址放在c0600000处,而将PTE放在C0000000开始的位置。
微软将PTE按顺序放在起始线性地址为C0000000的内存区域,那么所有的pte占用的内存空间为4*512*512*8=0x800000所以[c0000000,c0800000)保存了所有的PTE,很容易发现这里面包含c0600000(前面说到的其中一个PDE),那么说明每一个PDE也是其中一个PTE。
看看PDT需要多少个字节呢?需要512*4*8=0x4000,所以[C0600000,c0604000)保存了四个PDT表,但是他们仍然是PTE。
虽然[c0000000,c0800000)是属于内核区域的,但是它并不是所有进程中数据都一样的,因为它保存了这个进程的页表。
使用虚拟地址访问页表
通过以上分析,了解了进程的页表就在进程的空间当中。这样一来,就可以通过访问进程空间的[c0000000,c0800000)区域来修改页表内容。
为了能够修改,需要知道一个由虚拟地址A快速计算A的PDE和PTE所在虚拟地址呢?
VA[PDE]=C060 0000+A[31:30]*0x1000+A[29:21]*8
VA[PTE]=C000 0000+ A[31:30]*0x1000*0x200+ A[29:21]*0x1000+A[20:12]*8
例如:
若A=0x12345678= 00010010 00110100 01010110 01111000
那么A[31:30]=0,A[29:21]=0x91,A[20:12]=0x145
PDE= 0xC0600000+0*0x1000+0x91*8= 0xC0600488
PTE= 0xC0000000+0*0x1000*0x200+0x91*0x1000+0x145*0x8= C0091A28
若A=0x87504832=10000111 01010000 01001000 00110010
那么A[31:30]=2,A[29:21]=0x3A,A[20:12]=0x104
PDE= 0xC0600000+2*0x1000+0x3a*8= 0xC06021D0
PTE= 0xC0000000+2*0x1000*0x200+0x3a*0x1000+0x104*0x8= 0xc043a820
若A=0xf0483215= 11110000 01001000 00110010 00010101
那么A[31:30]=3,A[29:21]=0x182,A[20:12]=0x83
PDE= 0xC0600000+3*0x1000+0x182*8= 0xC0603C10
PTE= 0xC0000000+3*0x1000*0x200+0x182*0x1000+0x83*0x8= 0xC0782418
附:
Windows x64下虚拟地址下所在页对应的描述页信息的虚拟地址的关系:
#include <iostream> #include <iomanip> #include <windows.h> using namespace std; int main(int argc, TCHAR* argv[], TCHAR* envp[]) { //x64下的CPU实现了36位(64GB),40位(1TB->服务器CPU、AMD的CPU实现)寻址,有52位寻址的最大标准但没有厂家实现 //对于上面这么多情况,无论如何,CR3寄存器的值低12位,多出来位到第52位在CR4不同场景具有不同含义 //Windwos的x64分页模式是 9 9 9 9 12分页模式,总共256TB空间。 //Windows系统分配前43位给用户层,共计8TB空间,内核层使用剩余的248TB空间。 //Windwos遵循着虚拟高地址为内核层保留,因此将48位地址映射为64位,即高16位为1。 UINT64 addr; cout << "ADDR="; cin >> hex >> addr; UINT64 a0, a1, a2, a3; UINT64 PXE_BAS = 0xFFFFF6FB7DBED000; UINT64 PPE_BAS = 0xFFFFF6FB7DA00000; UINT64 PDE_BAS = 0XFFFFF6FB40000000; UINT64 PTE_BAS = 0XFFFFF68000000000; UINT64 USR_MAX = 0X000007FFFFFFFFFF;//2^43是用户空间,占8TB UINT64 KER_MIN = 0XFFFF080000000000;//2^48-2^43是内核空间,占248TB[(2^8-2^3)*2^40=248TB,变化的有8个位,原先有3个变化位给用户层] UINT64 PXE, PPE, PDE, PTE; cout << setiosflags(ios::uppercase) << endl; if (addr <= USR_MAX || addr >= KER_MIN)//用户层8TB,内核层248TB { a0 = addr >> (12 + 9 + 9 + 9); a1 = addr >> (12 + 9 + 9); a2 = addr >> (12 + 9); a3 = addr >> (12); a0 = a0 & 0x1ff; a1 = a1 & 0x1ff; a2 = a2 & 0x1ff; a3 = a3 & 0x1ff; PXE = PXE_BAS + a0 * 8; PPE = PPE_BAS + a1 * 8 + a0 * 0x1000; PDE = PDE_BAS + a2 * 8 + a1 * 0X1000 + a0 * 0x1000 * 0x200; PTE = PTE_BAS + a3 * 8 + a2 * 0x1000 + a1 * 0x1000 * 0x200 + a0 * 0x1000 * 0x200 * 0x200; cout << "PXE=0x" << hex << PXE << endl; cout << "PPE=0x" << hex << PPE << endl; cout << "PDE=0x" << hex << PDE << endl; cout << "PTE=0x" << hex << PTE << endl; } else { cout << "ERROR : ADDR INPUT IS WRONG!" << endl; } return 0; }
Windows x86下 2 9 9 12分页模式下,虚拟地址下所在页对应的描述页信息的虚拟地址的关系:
#include <iostream> #include <iomanip> #include <windows.h> using namespace std; int main(int argc, TCHAR* argv[], TCHAR* envp[]) { UINT32 addr; cout << "ADDR="; cin >> hex >> addr; UINT32 a0, a1, a2; UINT32 PDE_BAS = 0XC0600000; UINT32 PTE_BAS = 0XC0000000; UINT32 PDE, PTE; cout << setiosflags(ios::uppercase) << endl; a0 = addr >> (12 + 9 + 9); a1 = addr >> (12 + 9); a2 = addr >> (12); a0 = a0 & 0X03; a1 = a1 & 0x1ff; a2 = a2 & 0x1ff; PDE = PDE_BAS + a1 * 8 + a0 * 0x1000; PTE = PTE_BAS + a2 * 8 + a1 * 0x1000 + a0 * 0x1000 * 0x200; cout << "PDE=0x" << hex << PDE << endl; cout << "PTE=0x" << hex << PTE << endl; return 0; }
Windows x86下 10 10 12分页模式下,虚拟地址下所在页对应的描述页信息的虚拟地址的关系:
#include <iostream> #include <iomanip> #include <windows.h> using namespace std; int main(int argc, TCHAR* argv[], TCHAR* envp[]) { UINT32 addr; cout << "ADDR="; cin >> hex >> addr; UINT32 a0, a1; UINT32 PDE_BAS = 0xC0300000; UINT32 PTE_BAS = 0xC0000000; UINT32 PDE, PTE; cout << setiosflags(ios::uppercase) << endl; a0 = addr >> (12 + 10); a1 = addr >> (12); a0 = a0 & 0X3ff; a1 = a1 & 0x3ff; PDE = PDE_BAS + a0 * 4; PTE = PTE_BAS + a1 * 4 + a0 * 0x1000; cout << "PDE=0x" << hex << PDE << endl; cout << "PTE=0x" << hex << PTE << endl; return 0; }