avatar

Catalog
2-9-9-12分页

在前面的文章中主要介绍了10-10-12分页方式,在这种分页方式下,物理地址最多可达4GB。随着硬件发展,4GB的物理地址范围已经无法满足要求,于是Intel设计了一种新的分页方式:2-9-9-12分页(又称PAE)分页。下面就来了解一下这种分页方式是如何运作的吧。

PAE分页

为什么是2-9-9-12

PAE(Physical Address Extension,物理地址扩展)页,一定会涉及到2个结构,就是PDE和PTE。以PTE来说,它可以直接定位到某个物理页上的物理地址,在10-10-12分页下,由于PTE的大小是4字节(32位),因此PTE能够寻址的范围仅有4GB。设想,若PTE有33位,那便可以寻址8GB;34位就能寻址16GB……以此类推。Intel考虑到对齐的因素,就干脆直接让PTE的长度达到64位了。这样一个PTE的大小就8字节,又因为一个PTT表的大小是4KB(4096字节),因此原本一个PTT表里能装下1024个4字节的PTE,现在只能装下512个8字节的PTE了。2的9次方等于512,所以PTI的值为9。

同理,PDI的值也为9,这样2-9-9-12中还剩下最前面的2位。

设置PAE分页

设置PAE分页比较简单,进入C盘打开boot.ini文件修改启动项,将execute改成noexecute即可,然后重启虚拟机即可进入PAE分页。

PDPTE

PDPTE(Paga-Directory-Point-Table Entry)页目录指针表项,顾名思义,这是一个指向PDT表(在10-10-12分页下,Cr3指向PDT表的首地址)的元素,且位于PDPT表(PAE分页下Cr3指向PDPT表首地址)中。由于仅剩2位,所以PDPTE只有4个,同样PDPTE每项占8个字节,来看下这个结构。

  • Avail:下标9~11,共3位,这是留给操作系统使用的位,CPU本身并不使用
  • Base Address:下标12~35,寻址时,低12位补0,共36位(达到36位,与PTE保持一致,寻址空间达到64GB),即PDT基址

至于PCD和PWT,留到控制寄存器和TLB部分详解。

PDE

PAE分页下,PDE扩展到了64位,其余属性变化不大。

  • PS = 1:大页,下标35-21是大页的物理地址,低21位填0,大页的大小为2MB(10-10-12的大页为4MB),按照2MB对齐。
  • PS = 0:下标35~12是页表(PTT)基址,低12位补0,共36位。
  • Avail:同PDPTE

PTE

与PDE一样,PAE分页下的PTE,也是扩展到了64位,其余变化不大

PTE中下标35-12是物理页基址,共24位(10-10-12分页下是下标31~12,共20位),低12位补0。

物理页基址+12位的页内偏移指向具体数据。

在了解这些结构后,来看一下PAE分页的大致模型

XD位

在Intel新系列的CPU中,在下标63的地方多了一个属性位XD位(AMD中称为NX,即No Execetion)

我们知道段的属性有可读、可写和可执行,但是页的属性只有可读、可写。

当ret执行返回语句时,如果堆栈里的数据指向一个提前准备好的数据(把数据当作代码来执行,漏洞很多都是依赖这点,比如SQL注入),这个位的作用就是在硬件上实现一种保护,防止数据可执行的情况发生。

查找物理页

PAE分页下查找对应的物理页和10-10-12差不多,拆分线性地址后,再根据PDPI、PDI、PTI偏移去找,由于每项均是8字节,所以在Windbg中使用dq指令进行查看。来看下面这个例子:

变量a的线性地址为:0x12ff7c。按照2-9-9-12进行拆分后得到0-0-12f-f7c。接着通过Cr3一步步查找,具体如下:

变量a的存的值为0x123,通过拆分线性地址,成功在找到变量a对应的物理地址。

0地址挂物理页

在学习10-10-12分页时,通过0地址挂物理页的实验,加深对物理页的理解,这里我们通过这个实验进一步熟悉PAE分页。

先运行程序,发现访问违例,运行失败

查看0地址对应的物理页,发现物理页是空的。

然后查看局部变量a对应的物理页,并将a对应的物理页挂到0的位置(注意,挂物理页时,用两次!ed指令而不是!eq指令)

接着运行程序发现可以正确的打印出0地址上的内容

PAE分页下PDT/PTT的基址

新增加的结构,PDPTE,并没有R/W位,US位等属性,真正决定物理页属性的还是PDE和PTE。相比10-10-12分页可以通过PDT/PTT基址修改物理页属性,在PAE分页下同样可以做到,这部分我们来研究下PAE分页下PDT和PTT的基址。

逆向分析MmIsAddressValid

在前一篇文章中我们分析了10-10-12分页下的MmIsAddressValid函数,它在找到PDE/PTE后会判断下标为0(P位)和下标为7(PDE对应PS位,PTE对应PAT位)的位置的值,进行一些处理工作。而这个函数找到PDE/PTE的过程就使用了PDT/PTT的基址。这次通过分析PAE分页下的MmIsAddressValid函数,来找到PAE分页下PDT/PTT基址。

先分析查找PDE的部分

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
80511987 8b4d08          mov     ecx,dword ptr [ebp+8]	//获取参数
8051198a 56 push esi
8051198b 8bc1 mov eax,ecx
8051198d c1e812 shr eax,12h //右移18位
80511990 bef83f0000 mov esi,3FF8h
80511995 23c6 and eax,esi //进行与运算,余下11位有效位
80511997 2d0000a03f sub eax,3FA00000h //相当于add eax, 0xC0600000
8051199c 8b10 mov edx,dword ptr [eax] //取PDE低四字节
8051199e 8b4004 mov eax,dword ptr [eax+4] //取PDE高四字节
805119a1 8945fc mov dword ptr [ebp-4],eax //高四字节保存到局部变量
805119a4 8bc2 mov eax,edx
805119a6 57 push edi //保存edi原本的值
805119a7 83e001 and eax,1 //保留P位的值
805119aa 33ff xor edi,edi
805119ac 0bc7 or eax,edi
  • 右移18位后,进行了一次与运算,保留的位相当于PDPI x 4KB + PDI x 8(看不明白的可以参考这篇
  • sub eax, 0x3FA00000和add eax, 0xC0600000,因此可以推测,PAE分页下PDT的基址为0xC0600000

接着分析查找PTE的部分

Code
1
2
3
4
5
6
7
8
9
10
11
12
805119c3 c1e909          shr     ecx,9	//右移9位
805119c6 81e1f8ff7f00 and ecx,7FFFF8h //进行与运算,余下20位有效位
805119cc 8b81040000c0 mov eax,dword ptr [ecx-3FFFFFFCh] //相当于mov eax, [ecx+0xC0000004]
805119d2 81e900000040 sub ecx,40000000h //相当于add ecx, 0xC0000000
805119d8 8b11 mov edx,dword ptr [ecx] //取PTE低四字节
805119da 8945fc mov dword ptr [ebp-4],eax //将PTE高四字节保存至局部变量
805119dd 53 push ebx //保存ebx原本的值
805119de 8bc2 mov eax,edx
805119e0 33db xor ebx,ebx
805119e2 83e001 and eax,1 //保留P位的值
805119e5 0bc3 or eax,ebx
805119e7 5b pop ebx
  • 重点还是在与运算这,右移9位后跟0x7FFFF8进行与运算,相当于PDPI x 2MB + PDI x 4KB + PTI x 8
  • sub ecx,0x40000000相当于add ecx, 0xC0000000,可以推测,PAE分页下PTT的基址仍然为0xC0000000

公式总结

根据MmIsAddressValid函数,可以得到PAE分页下PDT和PTT的基址分别为0xC0600000和0xC0000000

我们可以采纳MmIsAddressValid的方法总结出找到任意一个PDE /PTE的公式:

  1. 利用MmIsAddressValid内的手法
c
1
2
pPDE = (int*)(0xc0600000 + ((addr >> 18) & 0x3ff8))
pPTE = (int*)(0xc0000000 + ((addr >> 9) & 0x7ffff8))
  1. 通过拆分线性地址
c
1
2
pPDE = (int*)(0xc0600000 + (PDPTI<<12) + (PDI<<3))
pPTE = (int*)(0xc0000000 + (PDPTI<<21) + (PDI<<12) + (PTI<<3))

修改常量区

在10-10-12分页学习的时候,我们知道通过修改物理页属性,可以使得程序能够修改常量区里的内容。并且在基址小实验那篇中,通过代码实现了修改常量区的操作,利用了基址,从而可以在代码中通过线性地址找到PDE和PTE。这里,在PAE分页下,重做一遍那个实验,原理一样,就不在此赘述了。

首先,直接修改位于常量区的“protect”失败

这里采用拆分线性地址的方式计算PDE/PTT的具体位置,拆分“protect”所在的线性地址0x423034 -> 0-2-0x23-0x34

接着在裸函数里实现通过基址修改PDE/PTE(执行这部分要先提权,然后在调用门内实现)

c
1
2
3
4
5
6
temp = *(int*)(0xC0600000 + 0*0x1000 + 2*0x8);
temp = temp|0x2; //将R/W位置1
*(int*)(0xC0600000 + 0*0x1000 + 2*0x8) = temp;
temp = *(int*)(0xC0000000 + 0 + 2*0x1000 + 0x23*0x8);
temp = temp|0x2; //将R/W位置1
*(int*)(0xC0000000 + 0 + 2*0x1000 + 0x23*0x8) = temp;

在做这部分实现时,我踩了一个大坑:temp = temp|0x2,在执行这条语句时,运算符”|”的两侧千万不能加空格,不然就死机了,我也不知道是什么原因,只要运算的结果对原操作数有影响,就会死机,若没有影响,会继续执行,这个坑也导致我停顿了好一会。

修改完PDE/PTE的属性后,执行代码,便可以成功修改常量区的内容。

完整代码

c
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
#include "stdafx.h"

int temp;

__declspec(naked) void ModifyConst() {
_asm {
pushad
pushfd
}

temp = *(int*)(0xC0600000 + 0*0x1000 + 2*0x8);
temp = temp|0x2;
*(int*)(0xC0600000 + 0*0x1000 + 2*0x8) = temp;
temp = *(int*)(0xC0000000 + 0 + 2*0x1000 + 0x23*0x8);
temp = temp|0x2;
*(int*)(0xC0000000 + 0 + 2*0x1000 + 0x23*0x8) = temp;

_asm {
popfd
popad
retf
}
}

int main(int argc, char* argv[])
{
char* str = "protect";
char buffer[] = {0, 0, 0, 0, 0x4B, 0};
printf("addr: %x", str);
getchar();
_asm {
call fword ptr buffer
}

*str = 'a';

// printf("%x\n", temp);
printf("str: %s", str);
getchar();
return 0;
}

总结

PAE分页下,整体流程和10-10-12分页差别不大,在理解了10-10-12分页的基础上,学习PAE分页是不困难的,个人觉得如果能真正理解PDT/PTT基址的原理和使用方法,对PAE分页能掌握的更好。PDT的基址C0600000还是很好理解的,但是PTT的就有点困难了,尽管计算上的结果是正确的,但是在拆分后带入Cr3跟进时,可能会踩一些小坑。

目前为止,保护模式中最主要的段和页的知识,就介绍的差不多了,至于64-bit的9-9-9-9-12分页,就不再去细说了,有兴趣的小盆友们可以自己查看Intel白皮书第三卷了解更多,接下来还会有几篇琐碎的知识,然后将会进入下一个模块,系统调用。

Author: cataLoc
Link: http://cata1oc.github.io/2020/03/22/2-9-9-12%E5%88%86%E9%A1%B5/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶