在前面的文章中主要介绍了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的部分
1 | 80511987 8b4d08 mov ecx,dword ptr [ebp+8] //获取参数 |
- 右移18位后,进行了一次与运算,保留的位相当于PDPI x 4KB + PDI x 8(看不明白的可以参考这篇)
- sub eax, 0x3FA00000和add eax, 0xC0600000,因此可以推测,PAE分页下PDT的基址为0xC0600000
接着分析查找PTE的部分
1 | 805119c3 c1e909 shr ecx,9 //右移9位 |
- 重点还是在与运算这,右移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的公式:
- 利用MmIsAddressValid内的手法
1 | pPDE = (int*)(0xc0600000 + ((addr >> 18) & 0x3ff8)) |
- 通过拆分线性地址
1 | pPDE = (int*)(0xc0600000 + (PDPTI<<12) + (PDI<<3)) |
修改常量区
在10-10-12分页学习的时候,我们知道通过修改物理页属性,可以使得程序能够修改常量区里的内容。并且在基址小实验那篇中,通过代码实现了修改常量区的操作,利用了基址,从而可以在代码中通过线性地址找到PDE和PTE。这里,在PAE分页下,重做一遍那个实验,原理一样,就不在此赘述了。
首先,直接修改位于常量区的“protect”失败
这里采用拆分线性地址的方式计算PDE/PTT的具体位置,拆分“protect”所在的线性地址0x423034 -> 0-2-0x23-0x34
接着在裸函数里实现通过基址修改PDE/PTE(执行这部分要先提权,然后在调用门内实现)
1 | temp = *(int*)(0xC0600000 + 0*0x1000 + 2*0x8); |
在做这部分实现时,我踩了一个大坑:temp = temp|0x2,在执行这条语句时,运算符”|”的两侧千万不能加空格,不然就死机了,我也不知道是什么原因,只要运算的结果对原操作数有影响,就会死机,若没有影响,会继续执行,这个坑也导致我停顿了好一会。
修改完PDE/PTE的属性后,执行代码,便可以成功修改常量区的内容。
完整代码
1 |
|
总结
PAE分页下,整体流程和10-10-12分页差别不大,在理解了10-10-12分页的基础上,学习PAE分页是不困难的,个人觉得如果能真正理解PDT/PTT基址的原理和使用方法,对PAE分页能掌握的更好。PDT的基址C0600000还是很好理解的,但是PTT的就有点困难了,尽管计算上的结果是正确的,但是在拆分后带入Cr3跟进时,可能会踩一些小坑。
目前为止,保护模式中最主要的段和页的知识,就介绍的差不多了,至于64-bit的9-9-9-9-12分页,就不再去细说了,有兴趣的小盆友们可以自己查看Intel白皮书第三卷了解更多,接下来还会有几篇琐碎的知识,然后将会进入下一个模块,系统调用。