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;
}

  

posted @ 2019-02-27 22:20  J坚持C  阅读(667)  评论(0编辑  收藏  举报