80x86 2-9-9-12分页
前言:80x86 2-9-9-12分页的学习笔记
在之前的课程中我们讲解了10-10-12分页方式,在这种分页方式下物理地址最多可达4GB。但随着硬件发展,4GB的物理地址范围已经无法满足要求,Intel在1996年就已经意识到这个问题了,所以设计了新的分页方式,也就是这节课要学习的2-9-9-12分页,又称为PAE(物理地址扩展)分页。
回顾下10-10-12的产生
首先10-10-12分页模式这个成品是基于设计者的想法而产生的,先要确定的肯定是页的大小也就是4kb大小,那么也就是2^12 -> 4KB大小
那么还剩下"10" "10" 接着又设计PDE和PTE,通过PTE的前20位再加上12位的偏移,那么也就是4G寻址范围(那当时的情况寻址能力已经能足够了),所以这里设计为PDE和PTE的大小都为4个字节,那么为什么PDE也需要4个字节呢?
原因就是为了迎合PTE,如果它不是4个字节的话,那么就没有前20位地址,如果PDE不是前20位基地址,那么它可能就找不到PTE的基地址,所以PDE的基地址也需要和PTE一样,所以"10","10"也就可以说明了
2-9-9-12的配置
之前的10-10-12的为/execute=optin
,这里改为/noexecute=optin
即可
2-9-9-12分页模式的产生
就是老师提出的问题,那么我有什么办法在32位cpu下如何能够提高物理地址的寻址范围呢?
这里就引入了2-9-9-12的分页模式,又被叫做为PAE物理地址扩展
1、页的大小是确定的,4KB不能随便改,所以低12位是可以确定的
2、如果想增大物理内存的访问范围,就需要增大PTE,增大多少了呢?需要找到64G,那么增大到36位,但是还需要考虑对齐的因素,所以对齐到8个字节
知识点:在Windows系统中,对齐大小有利于高效率的执行和搜索
3、同理PDI也是2的9次方 32 - 9 - 9 - 12 还差2位 所以就再做一级叫PDPI(页目录指针表)
知识点:这里需要让PDI也是2的9次方的原因,其实也是就是为了通过PDE同样可以定位到PTE,举个例子,此时2-9-9-12分页下,当前的PTE的前24位是4GB范围以外的,那么如果PDE不是跟PTE同样的结构,那么PDE的前24位就没有,也就是无法找到4GB范围以外,所以为了能够让PDE能够定位到4GB以为的PTE,所以也需要"9"
PDE(PS=1为大页的情况下)
当PS位为1的时候,该PDE指向的物理页是大页,35-21位是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。
PDE(PS=0)
2-9-9-12的9位,它是PDE,这个每项占8个字节,如下图所示
当PS=0时,第35-12位是页表基址,低12位补0,共36位。
PTE
2-9-9-12的9位,它是PTE,这个每项占8个字节,如下图所示
PTE中第35-12是物理页基址,24位,低12位补0物理页基址+12位的页内偏移指向具体数据
PDPTE(Page-Directory-Point-Table-Entry):
2-9-9-12最高的2位,它是页目录指针表项,这个每项占8个字节,如下图所示
整体的2-9-9-12架构
如下图所示
个人理解(不知道讲的对不对,仅供参考)
参考文章:https://www.cnblogs.com/kaleidopink/p/12672167.html
我感觉我自己还是没懂物理地址和一些知识点的概念,我自己的理解如下:
1、视频中学习的是,想要32位cpu下让物理内存能够识别的最多范围能够达到64G,那么是可以通过PAE物理地址拓展来实现的(其实本质也就是改变PTE的寻址位)?
我的理解就是改变了PTE的寻址位,让其能达到高36位的地址的寻址,那么也就能找到对应的64G的物理地址了
然后视频中有人提出来一个问题说 10-10-12的分页是 1024*1024*4096=4G
,如果我让它再多一份PDT表的话,那么也就是2*1024*1024*4096=8G
,那么它能找到的是不是就能有8G的物理地址了?
我觉得它说的是对的啊,但是我不知道是不是对的,我是这样理解的,这里多了一份PDT表的结果其实就是让寻址能力变大了,但是本质的问题没有解决,你的物理地址还是只有4G,所以说物理地址不变,你u寻址能力变大,那找到的物理地址还是只有4G,所以这个解决办法没有用
那么PAE它改变的是什么?我理解的就是增大了物理地址的范围,让其变成了64G,那么其实就是将PTE开刀了,让它变成8个字节大小,此时PTE的能找到的物理地址就变成了64G,所以这里的话才是真正让物理地址变大的原因,也就是物理地址的长度影响的
以上说法我不知道对不对,因为学习都是看视频,想问问题也不知道找谁问,自己理解就只能理解到这边...
在2-9-9-12分页模式下进行线性地址到物理地址的转换
代码测试,就需要创建一个变量即可,然后获取该变量的线性地址,找到该变量线性地址对应的物理地址即可
int main()
{
int x;
getchar();
return 0;
}
x变量的拆分地址如下所示
0012ff7c
->
00
00 0000 000 *8 -> 0
1 0010 1111 *8 -> 0x978
f7c
!process 0 0
,cr3为0x073402a0
接着!dq 073402a0
,内容为 00000000 31959801
(去掉了反引号)
因为PDPTE的第35-12 存储的是页目录表的基址,低12位补0,共36位,即页目录基址,!dq 031959000+8*0
,通过这个找到PDE
PDE结构中当PS=0时,35-12位是页表基址,低12位补0,共36位
!dq 0319b8000+0x978
PTE结构中35-12是物理页基址,24位,低12位补0,物理页基址+12位的页内偏移指向具体数据
!dq 0319ce000+f7c
知识点:为什么*8
,跟10-10-12中是*4
的,这里*8
的原因是一个PDE和PTE大小为8个字节
这里还可以通过!vtop 073402a0 0x12ff7c
来进行校验,看自己是不是计算的准确,如下图所示
XD标志位(AMD中称为NX,即No Excetion)
PDE/PTE结构
段的属性有可读、可写和可执行
页的属性有可读、可写
当RET执行返回的时候,如果我修改堆栈里面的数据指向一个我提前准备好的数据(把数据当作代码来执行,漏洞都是依赖这点,比如SQL注入也是)
所以,Intel就做了硬件保护,做了一个不可执行位,XD=1时。那么你的软件溢出了也没有关系,即使你的EIP蹦到了危险的"数据区",也是不可以执行的!
在PAE分页模式下,PDE与PTE的最高位为XD/NX位
这个XD/NX标志位我不知道在哪,因为文档中也没有标出来,高地址的话就是默认的为保留地址,应该指的是PDE或者PTE最高位,也就是第63位的值!
XD标志位模拟代码执行
X
2-9-9-12分页0线性地址挂物理页
测试代码如下:
#include<windows.h>
#include<stdio.h>
int main()
{
DWORD* pAddr = (DWORD*)VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
memset((DWORD*)pAddr,0,0x1000);
printf("VirtualAlloc addr: %x\n", pAddr);
printf("test addr: %x\n", test);
getchar();
*(DWORD*)0 = 0x12345678;
printf("null: %x", *(DWORD*)0);
return 0;
}
修改页属性,实现应用层读写高2G内存地址
这里拿线性地址0x8003F00C来进行举例
0x8003F00C
->
10 *8 -> 0x10
0 0000 0000 *8 -> 0
0 0011 1111 *8 -> 0x1F8
00C
测试代码:
#include<stdio.h>
#include<windows.h>
int main()
{
// 修改0x8003F00C页属性U/S,实现应用层读写高2G内存地址
getchar();
*(DWORD*)0x8003F00C = 0x12345678;
printf("%x", *(DWORD*)0x8003F00C);
getchar();
return 0;
}
这个测试有问题了,直接蓝屏了,我把G位设置为0也没有用,不知道是什么情况导致的,先放这里,知道了再来补上
我看了别人的文章,参考文章: https://blog.csdn.net/Kwansy/article/details/108965196 ,该作者修改的高2G的地址是8003f048
拆分8003f048
8003f048
->
10 *8 -> 0x10
0 0000 0000 *8 -> 0
0 0011 1111 *8 -> 1F8
048
可以发现,换了一个地址8003f048,就发现可以成功修改了,原因不太清楚,知道了再来补上
PDT页目录表基址0xC0600000
总结:PDE = 0xC0600000 + PDPTEI*0x1000 + PDI*0x8
关于2-9-9-12分页的MmIsAddressValid函数的逆向我放到了 https://www.cnblogs.com/zpchcbd/p/15915333.html 地址中了
然后这里通过windbg来进行验证相关的页目录基址,我在ida中找到的PDT的页目录为0xC0600000
拆分0xC0600000
11 *8 -> 0x18
0 0000 0011 *8 -> 0x18
0 0000 0000 *8 -> 0
000
然后再展示下0线性地址的PDE,如下所示,可以发现是一样的,所以0xC0600000是2-9-9-12页目录表的基址
PTT页表基址0xC0000000
总结:PDE = 0xC0000000 + PDPTEI*512*512*8 + PDI*8*512 + PTI*8
拆分0xC0000000
11 *8 -> 0x18
0 0000 0000 *8 -> 0
0 0000 0000 *8 -> 0
000
0xC0000000对应的物理地址为如下所示
这里就比如拆第一个PDPE的第三个PDE的第一个PTE,正常拆出来是如下地址
PROCESS 86abada0 SessionId: 0 Cid: 05d8 Peb: 7ffdf000 ParentCid: 03cc
DirBase: 073402c0 ObjectTable: e1e5a3d0 HandleCount: 13.
Image: 2_9_9_12.exe
kd> !dq 073402c0
# 73402c0 00000000`0f27a801 00000000`1217b801
# 73402d0 00000000`0f17c801 00000000`12039801
# 73402e0 00000000`1032e801 00000000`0ffaf801
kd> !dq 0f27a000
# f27a000 00000000`0ee24867 00000000`0ee12867
# f27a010 00000000`0e6e3867 00000000`00000000
# f27a020 00000000`00000000 00000000`00000000
kd> !dq 0e6e3000
# e6e3000 80000000`0fc36025 00000000`0e9e0025
# e6e3010 00000000`0e7fd025 00000000`0e6be025
# e6e3020 00000000`0dfbf025 00000000`10600025
接着我们这里假设0xC0000000是页表的基地址,那么通过公式来进行计算PDE = 0xC0000000+PDPTEI*512*512*8+PDI*8*512+PTI*8
PDE = 0xC0000000 + 0*512*512*8 + 2*8*512 + 0*8
为0xC0002000,这里拆分C0002000
C000 2000
->
11 -> 0x18
0 0000 0000 *8 -> 0
0 0000 0010 *8 -> 0x10
000
可以发现是这样的,那么0xc0000000是页表基址
PROCESS 86abada0 SessionId: 0 Cid: 05d8 Peb: 7ffdf000 ParentCid: 03cc
DirBase: 073402c0 ObjectTable: e1e5a3d0 HandleCount: 13.
Image: 2_9_9_12.exe
kd> !dq 073402c0+0x18
# 73402d8 00000000`12039801 00000000`1032e801
# 73402e8 00000000`0ffaf801 00000000`0fcf0801
kd> !dq 012039000+0
#12039000 00000000`0f27a863 00000000`1217b863
#12039010 00000000`0f17c863 00000000`12039863
kd> !dq 0f27a000+0x10
# f27a010 00000000`0e6e3867 00000000`00000000
# f27a020 00000000`00000000 00000000`00000000
kd> !dq 0e6e3000+0
# e6e3000 80000000`0fc36025 00000000`0e9e0025
# e6e3010 00000000`0e7fd025 00000000`0e6be025
代码实现2-9-9-12分页0线性地址挂物理页
这里我使用中断门来进行提权
eq 8003f500 0040EE00 00081005
(省略了反引号)
测试代码如下
#include<stdio.h>
#include<windows.h>
DWORD iAddr;
DWORD* getPDEAddr(DWORD addr)
{
return (DWORD*)(0xc0600000 + ((addr >> 18) & 0x3ff8));
}
DWORD* getPTEAddr(DWORD addr)
{
return (DWORD*)(0xc0000000 + ((addr >> 9) & 0x7ffff8));
}
void _declspec(naked) test()
{
__asm
{
int 3;
push ebp;
mov ebp, esp;
sub esp,0x1000;
pushad;
pushfd;
push fs;
}
*getPDEAddr(0) = *getPDEAddr(iAddr);
*getPTEAddr(0) = *getPTEAddr(iAddr) & 0xFFFFFEFF;
__asm
{
pop fs;
popfd;
popad;
add esp,0x1000;
mov esp,ebp;
pop ebp;
iretd;
}
}
int main()
{
iAddr = (DWORD)VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_READWRITE);
memset((DWORD*)iAddr,0,0x1000);
printf("VirtualAlloc addr: %x\n", iAddr);
printf("test addr: %x\n", test);
getchar();
__asm
{
int 0x20;
}
// test r/w
*(DWORD*)0 = 0x12345678;
printf("%x\n", *(DWORD*)0);
return 0;
}