101012分页模式配置

我们默认的分页模式为29912分页模式,需要修改为101012分页模式。

将原本的debug启动项拷贝一份

1
bcdedit /copy {current} /d 101012

img]

关闭DEP(数据执行保护)

1
bcdedit /set {dd64745e-0a47-11eb-bc4d-cd0e0677722b} nx AlwaysOff

关闭PAE(物理扩展内存)

1
bcdedit /set {dd64745e-0a47-11eb-bc4d-cd0e0677722b} pae ForceDisale

全部操作成功的话就可以看见这两项

img

并且重启后数据执行保护功能变灰

img

并且DirBase以K为单位进行变动,说明101012分页模式开启成功

img

windows7 x86的两种29912分页和101012分别由ntkrnlpa.exe和nroskrnl.exe两个内核文件进行管理。

什么是分页模式?

像29912或是101012这种分页模式的名字并不是cpu厂商起的,更不是操作系统厂商起的,只是为了好区分当前的分页模式。

为了更好的解释分页模式,这里先来一段汇编指令。

1
mov eax,dword ptr ds:[0x12345678]

众所周知,这里的0x12345678并不是线性地址(虚拟地址),而是一个逻辑地址(偏移)。

在正常的情况下,我们需要使用ds.base+逻辑地址才能转换得到真正的线性地址,只不过现在段的base都是0,就可以直接忽略掉他。

ds.base+逻辑地址得到的这个线性地址在101012的分页模式下就会被拆成10、10、12这三段。

怎么拆呢?首先,将0x12345678转换为二进制得到 0001 0010 0011 0100 0101 0110 0111 1000

0001 0010 0011 0100 0101 0110 0111 1000

后面这三位其实不用拆成二进制,因为3x4=12,刚刚好。

这三位678称为页内偏移

剩下0001 0010 0011 0100 0101这20位。

剩下的20位从右向左数10位进行分割得到:

1
2
0001 0010 00
11 0100 0101

不足4位的进行补0,得到:

0000 0100 1000 =0x48

0011 0100 0101 =0x345

拆份出来后,就可以通过这0x48、0x345、0x678这三个值来寻找到真正的物理地址。

在这,还要说明一个概念,就是一个页的大小为4096字节也就是4k,转换为16进制就是0x1000。

那么为什么是4096不能是别的数值呢?其实这就是根据页内偏移这后面三位决定的,后三位最大值为FFF,也就是4095,计算机都是从0开始算的,所以刚好是4096个,分页模式就是决定页大小为4096的原因。

寻找物理地址

当知道线性地址是如何根据分页模式进行拆分之后,就可以根据线性地址得到真正的物理地址了。

打开notepad.exe输入一段文本,然后使用ce搜索出该文本的线性地址0x00257FC0

ce

根据上面的拆解可以知道后12位FC0页内偏移不用拆,只需要拆前面的0x00257即可。

首先0x00257转换为二进制得到10 0101 0111按照4位将0补全得到0000 0000 0000 0010 0101 0111

拆开得:

0000 0000 0000 =0

0010 0101 0111 =0x257

现在得到了三层偏移(为什么是三层偏移这个下一小节再说),因为每个页的大小都为4096字节,那么里面每个元素的大小只能为4字节,所以偏移地址要乘4,最终得到:

1
2
3
0*4
0x257*4
0xFC0

接下来使用Windbg找到notepad.exe页基址(CR3) 0x71de9000

image-20201031145213470

使用这个CR3加上刚刚的第一层偏移0*4得到0x70da1867

image-20201031145701177

然后把0x70da1867的后三位去掉(因为这三位不是地址,是属性),得到0x70da1000,再用这个地址加上第二层偏移0x257*4得到0x71749867

image-20201031150757342

同样把0x71749867的后三位去掉,然后加上页内偏移0xFC0得到的就是文本内容的物理地址

image-20201031151430973

三层偏移是什么东西?

在这先附上一张Intel白皮书一张名为Segmentation and Paging的图片,这张图片详细说明了一个内存地址的分割和分页的关系。

image-20201031162406822

在线性地址转换的图中也清楚说明了101012分页怎么根据一个线性地址转换得到物理地址

线性地址转换

关于IA-32e模式下的内存管理也说明了各个表直接的关系

2.1.5.1 Memory Management in IA-32e Mode
In IA-32e mode, physical memory pages are managed by a set of system data structures. In compatibility mode and 64-bit mode, four levels of system data structures are used. These include:

  • The page map level 4 (PML4) — An entry in a PML4 table contains the physical address of the base of a page directory pointer table, access rights, and memory management information. The base physical address of the PML4 is stored in CR3.
  • A set of page directory pointer tables — An entry in a page directory pointer table contains the physical address of the base of a page directory table, access rights, and memory management information.
  • Sets of page directories — An entry in a page directory table contains the physical address of the base of a page table, access rights, and memory management information.
  • Sets of page tables — An entry in a page table contains the physical address of a page frame, access rights, and memory management information.

这里我画了一张表来进行说明

image-20201031171947780

首先,这里出现的这个DirBase,实际上是CR3(一个控制寄存器),里面存放着页基址

image-20201031145213470

其实CR3的结构也是一张表,大小为4096字节,每个元素为4字节,所以有1024个元素。

CR3里的页基址,指向一个PDT(Page Directory Table 页目录表),PDT的大小也是4096字节,能存放1024个元素。

PDT里每个元素称为PDE(Page Directory Entry 页目录项),每个PDE又指向一个PTT(Page Translation Table 页翻译表)表,PTT的大小也是4096字节,能存放1024个元素。

PTT里的元素叫PTE(page table entry 页表条目),通过PTE加上页内偏移也就是图中的偏移3,就可以来到线性地址对应的物理地址

0地址

系统在程序的逻辑地址空间的0x0位置,划分出一块64k的不可用空间,用来防止程序使用空指针,程序如果不意使用了空指针,再加上些结构元素偏移什么的,也鲜有超过 64K 的,所以就将这个区段空了出来,部分地址没有映射到物理内存,所以一访问就会出现page fault异常,操作系统捕捉到异常,根据访问的地址,就知道应用程序出现了空指针操作。

访问0地址

页内偏移为0的情况

先来撸一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Windows.h>

int p = 100;

int _tmain(int argc, _TCHAR* argv[])
{
int * xxx = (int *)0;
printf("%x\r\n",&p);
system("pause");
printf("%x\r\n",*xxx);
system("pause");
return 0;
}

因为读取的是0地址,运行肯定会报错

image-20201031232458065

先来看看为什么会报错。p的地址为0x13c5000

image-20201031232806936

拆分

0000 0000 0100 = 0x4

0011 1100 0101 = 0x3c5

使用Windbg查看CR3

image-20201101002111762

通过偏移得到PDE地址

image-20201101002146165

通过PDE加偏移得到PTE地址

image-20201101002236474

这个地址下存放的就是变量p里的值 100

image-20201101002324857

那么为什么0地址会访问不了呢?直接查看CR3不加偏移

image-20201101002411955

取0x00地址的值继续查看,可以看到PDE的0x00位置为空,也就是没用被挂上物理页

image-20201101002508227

尝试将他挂上0x57d3f867

image-20201101002706011

继续运行程序,成功输出100

image-20201101002838524

页内偏移不为0的情况

将全局变量移到main函数内,变成局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
int p = 100;
int * xxx = (int *)0;
printf("%x\r\n",&p);
system("pause");
printf("%x\r\n",*xxx);
system("pause");
return 0;
}

运行程序,这次的地址为0x33fac8

image-20201101145711827

ac8为页内偏移,把33f拆分一下

0000 0000 0000 =0

0011 0011 1111 =0x33f

使用WinDbg查看页基址

image-20201101150101783

跟着偏移走可以正常看见100

image-20201101150513121

将0x310b867写入0地址内,运行程序

image-20201101152506522

可以看到,因为*xxx取值取的是0地址的0号偏移,读到了一个0

image-20201101152636838

所以代码上取内容的时候要加上偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <Windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
int p = 100;
int * xxx = (int *)0;
printf("%x\r\n",&p);
system("pause");

int offset = ((ULONG)&p) & 0xFFF; //取后面12位的内容,也就是页内偏移

printf("%x\r\n",*(PULONG)((ULONG)xxx+offset)); //因为直接+的话是指针+1,所以要先转为ULONG,再转回指针取值
system("pause");
return 0;
}

把代码放入0地址中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <Windows.h>

char buf[0x1000] = {0};

int _tmain(int argc, _TCHAR* argv[])
{

printf("%x\r\n",&buf);
system("pause");

char code[]=
{
0x6a,0x00,
0x6a,0x00,
0x6a,0x00,
0x6a,0x00,
0xB8,0x78,0x56,0x34,0x12,
0xff,0xD0,
0xC3
};
char * p= (char *)0;
*(PULONG)&code[9] = (ULONG)MessageBoxA;

memcpy(p,code,sizeof(code));
typedef void (__stdcall *FunctionProc)();

HMODULE h = GetModuleHandleA("ntdll.dll");
FunctionProc func = (FunctionProc)GetProcAddress(h,"asdjasd");
func();

system("pause");
return 0;
}

效果:

image-20201101203558859

上面的代码会导致缓冲区溢出,位避免缓冲区溢出,使用VirtualAlloc申请内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
char * buf = (char*) VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
*buf = 1; //挂上物理页
printf("%x\r\n",&buf);
system("pause");

char code[]=
{
0x6a,0x00,
0x6a,0x00,
0x6a,0x00,
0x6a,0x00,
0xB8,0x78,0x56,0x34,0x12,
0xff,0xD0,
0xC3
};
char * p= (char *)0;
*(PULONG)&code[9] = (ULONG)MessageBoxA;

memcpy(p,code,sizeof(code));
typedef void (__stdcall *FunctionProc)();

HMODULE h = GetModuleHandleA("ntdll.dll");
FunctionProc func = (FunctionProc)GetProcAddress(h,"asdjasd");
func();

system("pause");
return 0;
}

页的属性

在拆分线性地址的时候,无论是PDE表还是PTE表里的元素,在使用的时候,都会把后三位忽略,因为这三位是页的属性,这一小节,将会详细介绍页的属性。

Intel白皮书中,有这样一张图详细介绍了PDE和PTE表的属性。

PDE和PTE的权限

P位(存在标志)

该标志表明,该表项所指向的页或者页表当前是否在内存中。当置位该标志时,这个页在物理内存中,将执行地址转换。当该标志清零时,表示这个页不在内存中,如果处理器试图访问该页,将产生一个缺页异常(PF)。处理器并不置位或者清零该位;而是由操作系统来维护该标志的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
char * buf = (char*) VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
*buf = 1;
VirtualLock(buf,0x1000);//将页面锁定在物理内存中。这样页面将不会被交换到硬盘上,防止蓝屏
printf("%x\r\n",&buf);

system("pause");
*buf;
system("pause");

return 0;
}

先确定地址没有错

image-20201102001756960

将PDE的P位置为0

image-20201102002323998

继续运行程序发现触发缺页异常,并蓝屏。

image-20201102002425233

image-20201102002645135

R/W位(读写标志位)

该标志确定对一个页或者一组页(比如,一个指向一个页表的页目录项)的读写权限。当这个标志为0时,该页是只读的;当这个标志为1时,该页是可读可写的。该标志与U/S标志和CRO寄存器中的WP标志共同起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
char *p ="123456";
printf("%p\r\n",p);
system("pause");
p[0]=5;
system("pause");
printf("%s\r\n",p);
system("pause");

return 0;
}

因为char *p ="123456";这行代码是定义了一个所谓的字符串常量(其实是被分配到一块没有写权限的内存空间中),接下来测试修改PTE的R/W位来让他获得写权限。

首先先确定PTE表的地址

image-20201102155200764

修改PTE表权限

image-20201102155611218

继续运行程序,成功修改

image-20201102155937005

U/S位 (User/Super)普通用户/超级用户 标志

该标志确定一个页或者一组页(比如,一个指向一个页表的页目录项)的用户权限。当这个标志被清零,该页的用户权限为超级用户的权限;该标志置位时,该页的用户权限为普通用户权限。这个标志与R/W标志和CRO寄存器中的WP标志共同起作用。

像R3不能访问R0地址就是这个位控制的。

随便拿GDT表的一个地址进行测试,这里我使用0x80dd2ba0

image-20201102163612515

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
int * p = (int *)0x80dd2ba0;
printf("%p\r\n",p);
system("pause");
*p = 0x100;
printf("%x\r\n",*p);
system("pause");

return 0;
}

先确定偏移正确

image-20201102174810373

修改权限

image-20201102175212456

继续运行程序,成功修改

image-20201102175347884

在windbg中也可以看见修改成功

image-20201102175626105

A位(访问标志)

指明这个页或页表是否曾经被访问过。内存管理软件通常会在这个页或者页表被载入内存时,清零该位。当该页或者页表第一次被访问以后,处理器会置位该标志。
​ 这个标志是个“粘性”标志,就是说一旦被设置,处理器不会隐式的给它清零。只有软件能清零该位。内存管理软件使用访问位和脏位来调度页或者页表进出物理内存。

在程序中申请的线性地址,在没有被访问过的情况下,是不会被挂上物理页的。当第一次被访问的时候,会出现一个页异常,然后对页进行修复(简单来说挂上物理页)。并把PDT和PTE的A位置1。

D位(脏位)

指明该页是否曾经被写入过(在指向页表的页目录项(PDE)中,不使用该标志)。通常,内存管理软件在该页刚被载入内存时,将该标志清零。当该页的第一次写操作完成后,处理器置位该标志。这个标志是-一个粘性标志,就是说,一旦被设置,处理器不会隐式的对它清零。只有软件可以对它清零。内存管理软件使用访问位和脏位来调度页或者页表进出物理内存。

写入过一次一次后,该位被置1。

PS标志 (页尺寸标志)

如果PDE.ps = 0 代表是一个小页,也就是正常的101012分页,PDE后面还有PTE。

如果PDE.ps = 1 代表是一个大页,只有前10位有效,后面22位都是页内偏移。2^22=4194304,也就是大页有4M。

image-20201103001341715

缓存位

缓存位是由三个位组合进行生效的,一共会有8种情况,

image-20201107145755210

这8种情况对应下图的几种效果

image-20201107150347871

00(UC):没有缓存(读写都直接操作内存条)

07 (UC-):读的时候可能会读缓存,并不一定会读缓存

下面这种带W的,读的时候统统都是读缓存

01(WC):有可能直写有可能回写

04 (WT):同时写入缓存与内存中

05 (WP):写保护,一写就触发异常

06 (WB):先写到缓存,然后再等刷新机制刷新到内存中

这8种类型并不是固定的,是可以随意设置的。有一个PAT MSR的寄存器,编号为277。在Windbg中可以使用rdmsr+编号查看:

image-20201107161639697

可以看到,一共是8个字节,也就是8种情况

1
00 07 01 06 00 07 01 06

PAT=0

PWT=0

PCD=0

需要查询第00个索引,也就是这里的06

需要注意的是,PAT是需要设置的,默认是没有的。在不支持PAT的时候,只使用PCD和PWD这;两个标志组合成两位的0~3这四种组合

image-20201107154049695

PAT位(页面属性表Page Attribute Table)

pat系统默认的是0,如果人为的置1的话,系统就会认为这个是无效的。

PWT位 (Page Write Through)

PWT=1:写Cache的时候也要将数据写入内存
PWT=0:写Cache的时候就只是写Cache,是否要映射到内存由CPU缓存控制器自己决定

1没有缓存,0可以有缓存

PCD位 (Page Cache Disable)

PCD=1:禁止某个页写入缓存(直接写入内存)
比如:做页表用的页,已经存储在TLB中,可能就不需要再做缓存,而它的PCD一定为1

G位 (全局页)

当G位等于1,全局页进入TLB之后切换CR3(切换进程)TLB也不会被刷新(具体在TLB中的如何锁定缓存这一小节演示)。

保护模式下操作系统是如何访问到物理内存的?

微软设计了构造了一个很巧妙的线性地址C0300000,用来查找自身的CR3,接下来就来介绍一下这个地址是怎么来的。

Windows的开机大致流程:

image-20201103141348712

刚开始进入系统的时候,并没有进程这个概念,因为是多任务的操作系统,有进程于线程的概念,系统也要用同样的方式来管理自己,就需要先把进程空间创建好。然后虚拟出一个进程。

image-20201103180028661

但是,进程空间创建出来之后,操作系统只有虚拟地址,没有物理地址。要怎么管理呢?

首先,x86下一个进程空间为4G,一个页为4k。

1
2
最大虚拟地址/页大小=PTE个数
PTE个数*指针大小=管理这块虚拟地址所要用到的内存大小

0x100000000/0x1000 = 0x100000

0x100000000的10进制为1,048,576

1,048,576/1024=1,024 也就是需要1024个pte来来管理内存

每个PTE为4字节,1024*4=4096,所以需要4096k也就是4M的内存来管理这4G的进程空间

在知道4G的进程空间中会分出4M来管理进程空间之后,我们假定这个规划出来的4M地址首地址为0xC0000000,注意!这个地址是可以随意变化的。

0xC0000000 / 4G *4M

0xC0000000 / 0x100000000 * 0x400000 = C * 0x40000 = 0x300000

0xC0000000+0x300000 = 0xC0300000

0xC0300000就是PDE的基址

现在就得到了两个范围

0xC0000000 - 0xC0300000 都是PTE

0xC0300000 - 0xC0400000 都是PDE

把C0000 000 拆分转一下二进制得到1100 也就是300*4

接下来去Windbg看一个神奇的东西,随便找一个进程,取页基址,这里我用system进程

image-20201103182108006

用基址加上300*4,又指回了自己,只是多个属性位了点东西

image-20201103182255741

也就是这样,这就是系统用来管理自己的CR3的算法,设计一个地址C0300c00回绕两次获取自身CR3,这就是自映射

image-20201103200500668

其实世界上本没有PDE,当PTE多到需要专门管理了,PDE就有了。

程序获得自己的PDE与PET

当然,系统上运行的普通程序也可以使用这种方法来取得自己的PDE与PET。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
int x = 0;
printf("%x\r\n",&x);
system("pause");
return 0;
}

运行程序,得到x的地址

image-20201104134650563

拆分25f得到

0000 0000 0000 = 0

0010 0101 1111 = 25f

先取得PDE与PTE,作对照

image-20201104150102176

image-20201104141444212

PDE的基址为C0300000,根据公式C0300000+i*4可以得到第i个PDE

使用C0300000加上第一层偏移0*4结果还是C0300000

接着再将C0300拆分

1100 0000 00 = C00

11 0000 0000 = C00

为了安全起见,使用.process /i PID让进程在执行的时候断下

image-20201104150020295

通过C00得到PDE,可以看到是一样的

image-20201104150418930

接下来求PTE,PTE的基址为C0000000,根据公式C0300000+i*4可以得到第i个PTE

使用C0000000加上第二层偏移0*4结果是C000025f,然后对这个结果进行拆分

1100 0000 00 = C00

00 0000 0000 = 0

验证一下,成功得到自身PTE0bfe4867

image-20201104151858580

系统是如何判断地址是否挂上物理页的

在101012分页模式下,是由ntoskrnl.exe对分页进行管理的

路径C:\Windows\System32\ntoskrnl.exe

把这个文件拖入IDA,搜索MmIsAddressValid函数,记得先设置好符号,否则是无法搜到的。

image-20201105150340874

来到函数对应的反汇编处,可以看到把地址传给了ECX后又Call了一个MiIsAddressValid函数

image-20201105151207521

跟进去就是系统判断物理页的全部操作

image-20201105194848991

(0x80000000 >> 12) = 80000 去掉后三位每位4字节3x4=12所以>>12

1000 0000 0000 0000 0000

3FF = 0011 1111 1111

&3FF就是取后面10位

得到的内容要x4所以是<<2

FFC =1111 1111 1100

可以简化成(80000000 >>10)& FFC

因为少右移两位所以&取到的值要往前两位

PDE = (80000000>>20)& ffc

根据上面逆向得到的内容,可以写出一份修改所有高地址US位的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include "stdafx.h"
#include <Windows.h>

void updateAddress()
{
unsigned int pdeBase = 0xC0300000;
unsigned int pteBase = 0xC0000000;
for (unsigned int i = 0x80000000;i<=0xFFFFE000;i+=0x1000)
{
unsigned int * pde = (unsigned int *)(((i>>20) & 0xFFC)+pdeBase);
unsigned int pdephy = *pde;

if ((pdephy & 1)==0) continue;

pdephy |=0x7;
*pde = pdephy;

if ((pdephy & 0x80)==0x80) continue;

//判断PTE
unsigned int * pte = (unsigned int *)(((i>>10) & 0x3FFFFC)+pteBase);
unsigned int ptephy = *pte;

if ((ptephy & 1)==0) continue;
if ((pdephy & 0x80)==0x80) continue;

ptephy |=0x7;
*pte = ptephy;

}
}

__declspec(naked) void callGate()
{
//__asm
//{
// push ebp;
// mov ebp,esp;
// sub esp,0x100;

//}

//如果要在这里写的话要保存现场

//__asm
//{
// add esp,0x100;
// mov esp,ebp;
// pop ebp;
// retf;
//}

__asm
{
pushfd;
pushad;
push fs;
mov ax,0x30;
mov fs,ax;
call updateAddress;
pop fs;
popad;
popfd;
mov eax,cr3;
mov cr3,eax;
retf;
};
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%x\r\n",callGate);
system("pause");
char bufcode[]={0,0,0,0,0x48,0};
__asm
{
call fword ptr bufcode
};
int * x = (int *)0x80b99010;
printf("%x\r\n",*x);
system("pause");
return 0;

}

29912分页

在使用32根地址总线的系统中,最大只支持4G的内存,使用101012分页没有任何问题。后来因为性能提升,地址总线扩展到了36根,也就是支持64G的内存。而101012分页的PDE和PTE的地址都是4个字节。已经不足以存放36根地址总线的地址。所以PDE和PTE就变成了占用8个字节。但是虚拟地址依然是4G。

在上一小节中介绍101012分页的时候有这样一张图说明了101012分页的寻址方式。

在101012分页中,PDT表和PTE表的大小都是4096字节。也就是最多只能存放1024个元素。

image-20201031171947780

与10-10-12不同,CR3不直接指向PDT表,而是指向一张新的表,叫做PDPT表(Page-Directory-Point-Table页目录指针表)

PDPT表中的每一个成员叫做PDPTE(Page-Directory-Point-Table Entry,页目录指针表项),每项占8个字节

PDPT表只有4个成员,因为2位比特位只能满足2^2=4种情况

image-20201106165257811

首先最后的12位是属性位,不能动。

如果想增大物理内存的访问范围,就需要增大PTE,增大了多少呢?考虑对齐的因素,增加到8个字节

由于PTE增大了,而PTT表的大小没变,依然是4KB,所以每张PTT表能放的PTE个数由原来的1024个减少到512个,512等于2的9次方,因此PTI=9

由于2的9次方个PDE就能找到所有的PTT表,因此PDI=9

分配到这里时,还剩下前2位未分配。于是就设计存放PDPTE表。

这就是29912的分页模式。

管理这块内存所需要的空间:4*512*512*8=800000=8M

29912的PDE基址:C0000000/100000000*800000+C0000000 = 800000*C = C0600000

页的属性

image-20201106174456355

后面12位的属性与101012分页的PDEPTE的属性基本相同,需要注意的是最高位XD位。

101012分页下,并没有管理执行的位,我们申请一块内存无论是可读还是可写,他都可以把数据当作代码来执行。

所以为了避免这个漏洞,在29912分页下,增加了DEP(数据执行保护),就是这个XD位来进行管理的。

XD=1时,这个页不能执行代码

XD=0时,这个页可以执行代码

注意!PDE和PTE都有XD位,并且是与的关系,当一个为1一个为0时,还是可以执行,当两个都为1时才不可执行。

大页

在101012中的大页为4M是因为只用了前面10位,后面22位没用。

在29912中前面11位是有值的,后面的21位没用,所以大小为2^22=2M。

29912的拆分

80b99000

先去掉后12位剩余的转换为二进制

1000 0000 1011 1001 1001

从右往左取9位

1 1001 1001= 199

第二部分

00 0000 101 = 5

最后两位

10 = 2

而且需要注意的是,寻址的时候要乘8,而且取值要取7位。

image-20201107021131822

TLB (Translation Lookaside Buffer)

在当天普遍的分页模式下,CPU中的MMU模块要查询到物理地址,需要经过这几个步骤:

1
CR3->PDPT->PDE->PTE->物理地址

MMU就是根据页表基地址寄存器从CR3一路查到PTE,最终找到物理地址(PTE页表中存储物理地址)。这样一级一级找下去,非常繁琐。每一次页表查找过程都需要进行内存访问。延时可想而知,非常影响性能。

为了解决这个问题,TLB就出现了。

TLB里一共有4张表,两张小表,两张大表。

表中存放的线性地址为PTE或是PDE对应的偏移部分,属性则是用PDE与上PTE,统计是用来计算地址使用频率高低,需要向表中加入新元素时,会将使用频率低的地址移除。

小表的物理页帧存放的是4k的物理页帧。

img

大表的物理存放的是2M(29912)的物理页帧。

image-20201109154629542

什么是页帧?

页帧全称物理页帧,就是PTE中存放的一个个物理页的首地址,或是PDE中存放的一个个物理大页的首地址。

分页结构缓存

分页结构缓存TLB是互补的,MMU在查询一个物理地址的时候,首先会拿着线性地址去TLB中查找,如果找不到,就会拿着线性地址去分页结构缓存中查找。

在32位的分页模式下,只有PDE的缓存项会被建立,PTE是不会被缓存的。如果PDE指向的是一个物理页帧(大页),那么PDE的缓存项也不会建立。

因此,在32位分页模式下只有使用4k的页面才会建立PDE的缓存项。

验证缓存的作用

首先,取到p1的地址右移9位去除属性位(原本是右移动12位,但是后面要乘8相当于还要<<3所以直接右移9位即可)

把剩余的位与上0x7ffff8

and 0x7ffff8 右移9差三位,所以最后的8是把三位给抹了

29912去掉了12,还有20位,7ffff8正好取20位

29912页中每个元素是8个字节,所以需要 eax和eax+4一起组成了PDE eax+4是高4字节

所以他要取两次才是一个完整的

接下来就是访问一下0地址,然后保存结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "stdafx.h"
#include <Windows.h>
PCHAR p1 = NULL;
PCHAR p2 = NULL;

int temp1 = 0;
int temp2 = 0;

void __declspec(naked) test()
{
__asm
{
pushad;
pushfd;
push fs;
mov ax,0x30;
mov fs,ax;

mov eax,dword ptr ds:[p1];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp1],eax;

mov eax,cr3;
mov cr3,eax

mov eax,dword ptr ds:[p2];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp2],eax;

pop fs;
popfd;
popad;
retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
p1 = (PCHAR)VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
p2 = (PCHAR)VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
*(PULONG)p1 = 0x1000;
*(PULONG)p2 = 0x2000;

temp1 = 111;
temp2 = 222;
printf("p1 = %x,p2 = %x,Func addr:%x\r\n",p1,p2,test);
system("pause");

char bufcode[]={0,0,0,0,0x48,0};

__asm
{
call fword ptr bufcode;
}

printf("temp1:%x\r\n temp2:%x\r\n",temp1,temp2);
system("pause");
return 0;
}

虽然在代码中

p1 = 0x1000;

p2 = 0x2000;

运行的情况下该p1p2的值是一样的,这就是因为MMU去查找了缓存

image-20201110130906617

如何刷新缓存

方法1:操作CR3刷新缓存

这个操作是强制把所有TLB中的缓存移除,除了带G位的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
__asm
{
pushad;
pushfd;
push fs;
mov ax,0x30;
mov fs,ax;

mov eax,dword ptr ds:[p1];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp1],eax;

mov eax,cr3;
mov cr3,eax;

mov eax,dword ptr ds:[p2];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp2],eax;

pop fs;
popfd;
popad;
retf;
}

运行后结果正确

image-20201110133542472

方法2:int3刷新缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
__asm
{
pushad;
pushfd;
push fs;
mov ax,0x30;
mov fs,ax;

mov eax,dword ptr ds:[p1];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp1],eax;

int 3;

mov eax,dword ptr ds:[p2];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp2],eax;

pop fs;
popfd;
popad;
retf;
}

如何锁定缓存

方法1:通过设置G位(全局页)锁定缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
__asm
{
pushad;
pushfd;
push fs;
mov ax,0x30;
mov fs,ax;

mov eax,dword ptr ds:[p1];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

or ecx,0x100;G位是第9位,把第9位置1

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp1],eax;

mov eax,cr3;尝试通过操作cr3刷新缓存
mov cr3,eax;

mov eax,dword ptr ds:[p2];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp2],eax;

pop fs;
popfd;
popad;
retf;
}

运行可以看到TLB依旧没有刷新

image-20201110134932715

如何强制刷新锁定的缓存

方法1:使用int3刷新全局页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
__asm
{
pushad;
pushfd;
push fs;
mov ax,0x30;
mov fs,ax;

mov eax,dword ptr ds:[p1];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

or ecx,0x100;G位是第9位,把第9位置1

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp1],eax;

int 3;

mov eax,dword ptr ds:[p2];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp2],eax;

pop fs;
popfd;
popad;
retf;
}

方法2:使用invlpg指令将某地址从TLB中移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
__asm
{
pushad;
pushfd;
push fs;
mov ax,0x30;
mov fs,ax;

mov eax,dword ptr ds:[p1];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

or ecx,0x100;G位是第9位,把第9位置1

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp1],eax;

mov eax,dword ptr ds:[p2];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

invlpg dword ptr ds:[0];将0地址从TLB中移除

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp2],eax;

pop fs;
popfd;
popad;
retf;
}

方法3:通过CR4的PGE位禁止全局页面生效

CR4中有一个PGE位(Page Global Enable 全局页面启用)

在G位中可以设置全局页,但是生不生效取决与CR4中的PGE位

PGE = 1 全局页生效

PGE = 0 全局页无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
__asm
{
pushad;
pushfd;
push fs;
mov ax,0x30;
mov fs,ax;

mov eax,dword ptr ds:[p1];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;

or ecx,0x100;G位是第9位,把第9位置1

mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp1],eax;

//mov eax,cr4;
__emit 0x0f;
__emit 0x20;
__emit 0xe0;

mov ebx,0x80;
not ebx;把0x80取反
and eax,ebx;把第8位置0

//mov cr4,eax;
__emit 0x0f;
__emit 0x22;
__emit 0xe0;

mov eax,dword ptr ds:[p2];
shr eax,9;
and eax,0x7ffff8;
add eax,0xc0000000;
mov ecx,dword ptr ds:[eax];
mov ebx,dword ptr ds:[eax+4];
mov dword ptr ds:[0xc0000000],ecx;
mov dword ptr ds:[0xc0000004],ebx;


mov eax,dword ptr ds:[0];
mov dword ptr ds:[temp2],eax;

//mov eax,cr4;
__emit 0x0f;
__emit 0x20;
__emit 0xe0;

mov ebx,0x80;
or eax,ebx;

//mov cr4,eax;
__emit 0x0f;
__emit 0x22;
__emit 0xe0;


pop fs;
popfd;
popad;
retf;
}

References:

《牛逼的火哥》

《IA-32卷3:系统编程指南》(中文版)

《x86_x64体系探索及编程》(邓志)

《64-ia-32-architectures-software-developer-vol-3a-part-1-manual》

以及 My classmates 大佬的博客