0x01 4GB内存空间
通常我们所说,32位的系统在应用程序运行的时候就会分配4G的内存空间。那么问题来了,一般的电脑内存条总共也只有4-16G,那是不是意味着只能运行几个应用呢?此4G是否等于彼4G呢?
答案当然是否定的。实际上,进程被分配到的“4GB内存空间”只是虚拟的的内存空间,并不是指真正意义上的物理内存,虚拟内存与物理内存之间有一层转换关系。所以这个4G只是单独的一个应用程序内存最高能占用4G罢了。
0x02 有效地址-线性地址-物理地址
有效地址与线性地址
MOV eax,dword ptr ds:[0x12345678]
对于这样一条指令,其中有效地址是 0x12345678 ,而 ds.Base + 0x12345678 则是线性地址
也就是说,如果没有开启地址随机化的话,程序基址就会为0,那么这个时候 线性地址 == 有效地址
物理地址
而物理地址,就是字面上理解的,内存所在于物理设备上的真实地址。比如当我们Load了一个dll到程序中,dll它本身的内容自然会被载入到物理设备上,但是为了便于程序对dll内容的调用,系统会将dll的物理地址映射一份线性地址给程序,这样程序就能通过线性地址找到dll的物理地址。
这个真实例子是使用IDA调试某程序时所截,Path中包含着这个进程载入的所有程序和库。这个 0x6E220000 就是 CheatTools.dll 被载入到进程中,系统分配给进程的线性地址,通过调试器跳转到这个地址上就能看到这个dll的内容。但是这个 0x6E220000 并不是这个dll存在于内存设备中的绝对真实地址,它的绝对真实物理地址需要通过转化计算才可以得到
0x03 10-10-12分页方式
前面我们说到,内存线性地址到物理地址需要进行转化,而 10-10-12分页方式 就是一种转化方式。通常由线性地址到物理地址有两种转化方式,一种是10-10-12分页方式,一种是2-9-9-12方式。这里介绍一下10-10-12分页方式的转化步骤(具体实验可以参考文章底部的参考资料,这里就不转载了)
假设我们通过调试器确定我们所需的内存数据的线性地址为:0x06765140
先将这一地址转化为二进制
0x06765140
0000 0110 0111 0110 0101 0001 0100 0000
10-10-12分页方式其实就是将线性地址拆分为 高10位-中间10位-低12位,并作三次查找的过程,将0x06765140按这样的方式拆分得:
0000 0110 01 // 0x19
11 0110 0101 // 0x365
0001 0100 0000 // 0x140
拆分完后就是查找了。这里要引入一个新概念————CR3寄存器
每个进程都有一个CR3的值,CR3指向一个物理页一共4096字节,系统会从CR3的值开始以10-10-12拆分后的值作为索引得到物理地址,如下图:
拆完以后CPU首先去找CR3寄存器,CR3寄存器是一个唯一存储物理地址的寄存器,CR3中存了一个值,这个值指向一个物理页,这个也有4096个字节,也就是他的第一级,第一部分分的高十位就是确定这个地址在第一级的哪个位置,第二个十位就是确定在第二级的哪个位置,最后12位就是确定在4096个字节的物理页的v哪个地址,4096 = 2 ^ 12;第一级中每个成员是4个字节,4096个字节可以存放1024 = 2 ^ 10个地址,同样第二级也是一样。
0x04 CR3寄存器&PDT&PTE
根据前面所讲的线性地址在向物理地址转化的过程中,Cr3寄存器起到了不可或缺的作用。那Cr3寄存器中存储的究竟是什么呢?
Cr3寄存器不同于其他寄存器,在所有的寄存器中,只有Cr3寄存器存储的地址是 物理地址,其他寄存器存储的都是 线性地址。Cr3寄存器所存储的物理地址指向了一个页目录表(Page-Directory Table,PDT),也就是我们前面所说的查找时的第一级。在Windows中,一个页的大小通常为4KB,即一个页(页目录表)可以存储1024个页目录表项(PDE)。这样说可能有点绕,看一下这张物理页结构图就非常明了了。
PDT:页目录表 一个页的大小通常为4KB,即一个页可以存储1024个页目录表项(PDE)
PDE: 页目录表项 页目录表(PDT)的每一项元素称为页目录表项(PDE),每个页目录表项指向一个页表(PTT)
PTT:页表 每个页表的大小为4KB,即一个页表可以存储1024个页表项(PTE)
PTE:页表项 页表项所指向的才是真正的物理页
页表项(PTE)具有以下特征:
- PTE可以指向一个物理页,也可以不指向物理页
- 多个PTE可以指向同一个物理页
- 一个PTE只能指向一个物理页
明白了这个特征我们就不难理解10-10-12分页方式的由来了。一个物理页的大小为4096字节,即2的12次方,若要遍历整个物理页,则需要12个比特位存储。一个页表或一个页目录表均有1024个页表项或1024个页目录表项,1024等于2的十次方,即需要10个比特位。所以将32位的线性地址拆分成10-10-12的结构进行查找。
实验
下面看一个实例就能完全了解10-10-12的分页转化方式了
假设:某程序基址为0,x变量的地址为0x12ff7c, 现要通过修改页表的方式使得程序能够在0地址处进行读写
因为已经leak了一个线性地址,我们就可以通过这个线性地址一路找到它的物理页,然后通过修改PTE就可以实现对0地址的读写。
0x12ff7c
高10位: 0
中间10位: 0x12f
低12位: 0xf7c
这里要注意的是,低12位不是基址! 0-11位保存的是PDE/PTE的相关属性,是一些标记位,基址是12-31位,所以在寻址的时候一定要去掉它们。关于这些保存属性的标记位将在下一节介绍。
0x05 物理页属性
在研究物理页的属性之前应当先学习以下PDE和PTE的各标志位
P位:是否有效位 注意:当PDE或PTE中有一个的属性P=0时,物理页就是无效的
R/W位:读写位
R/W=0:只读
R/W=1:可读可写
U/S位:权限位
U/S=0:特权用户
U/S=1:普通用户
PS位:PDE特有
PS == PageSize
PS=1:PDE直接指向物理页,低22位=页内偏移,偏移最大值为4MB,俗称"大页"
PS=0:PDE指向PTE
A位:访问位
A=1:该PDE/PTE被访问过
A=0:该PDE/PTE未被访问过
D位:脏位
D=1:该PDE/PTE被写过
D=0:该PDE/PTE未被写过
物理页的属性由PDE属性和PTE属性共同决定:
物理页的属性=PDE属性 & PTE属性
0x06 页目录表基址
因为线性地址其实是一种虚拟地址,需要通过特定的转化方式才可变成物理地址,而其中的关键就是PDE和PTE。那如果我们人为的想填充PDE/PTE,那首先我们必须要能访问PDT和PTT。那么问题来了,因为Cr3存储的是物理地址,我们是不可以直接在程序内直接读取它(就算要读取也要先把Cr3的值挂载到PDT/PTT中再用线性地址间接访问)。那么我们要怎么才能通过线性地址访问PDT/PTT呢?
实际上,为了解决这个问题,已经有“人”为我们访问PDT与PTT挂好了PDE与PTE,我们只用找到这个线性地址就可以了。这个线性地址就是 页目录表基址
页目录表基址 = 线性地址:C0300000
这可以通过一个实验证明
0xC0300000
高10位: 1100 0000 00 = 0x300
中间10位: 1100 0000 00 = 0x300
低12位: 0000 0000 0000 = 0x000
可以发现 物理页的内容 与 PDT表的内容 完全相同!
结论:
- 线性地址C0300000对应的物理页就是页目录表
- 这个物理页即页目录表本身也是一张特殊的页表
- 这个物理页是一张特殊的页表,每一项PTE指向的不是普通的物理页,而是指向其它的页表
- 访问页目录表的公式:C0300000 + PDI*4(I=index)
0x07 页表基址
相同与页目录表基址,为了在程序内快速访问页表,也有一个页表基址
页表基址 = 线性地址:C0000000
这里不说实验验证过程,只记录结论(过程可以参考下面的学习资料
结论:
- 页表被映射到了从0xC0000000~0xC03FFFFF的4M地址空间
- 在这1024个表中有一张特殊的表:页目录表
- 页目录被映射到了0xC0300000开始处的4K地址空间
- 访问页表的公式:0xC0000000 + PDI4096 + PTI4(I=index)
至此,通过 0xC0300000 和 0xC0000000 这两个地址,我们就掌握了一个进程所有的物理内存读写权限
公式总结:
- 访问页目录表的公式:C0300000 + PDI*4(I=index)
- 访问页表的公式:0xC0000000 + PDI4096 + PTI4(I=index)