页
X86模式下存在10-10-12分页和2-9-9-12分页。
10-10-12分页
在x86系统下,总说一个进程有4GB空间,那么按照这个说法来说,在windows上起一个进程就要占用4GB空间,两个进程就要占用8GB空间,但是实际上是我们电脑的物理内存往往只有8GB,16GB多一点的可能有32GB,我们却启动了几十个进程,这显然是矛盾的。
实际上,我们所说的进程有4GB内存空间,这个概念是虚拟的。cpu会经过一定算法从虚拟内存地址找到物理内存地址。
这里还有几个概念:线性地址、有效地址、物理地址
如下指令:
MOV eax,dword ptr ds:[0x12345678]
其中,0x12345678 是有效地址
ds.Base + 0x12345678 是线性地址
物理地址就是真正在内存条上的地址,不是虚拟出来的。
每个进程都有一个CR3,(准确的说是都一个CR3的值,CR3本身是个寄存器,一个核,只有一套寄存器)
CR3指向一个物理页,一共4096字节,从CR3到物理页的过程如图:
下面在10-10-12分页模式下从线性地址找到物理地址。要想当前xp系统是10-10-12分页,需要修改boot.ini文件。
将noexecute 改成 execute。
写入一句话到记事本,并通过CE找到他的线性地址。
采用10-10-12分页方式拆解这个线性地址。(十位,十位和十二位)
拆完以后CPU首先去找CR3寄存器,CR3寄存器是一个唯一存储物理地址的寄存器,CR3中存了一个值,这个值指向一个物理页,这个也有4096个字节,也就是他的第一级,第一部分分的高十位就是确定这个地址在第一级的哪个位置,第二个十位就是确定在第二级的哪个位置,最后12位就是确定在4096个字节的物理页的v哪个地址,4096 = 2 ^ 12;第一级中每个成员是4个字节,4096个字节可以存放1024 = 2 ^ 10个地址,同样第二级也是一样。
通过windbg获取notepad的cr3。
这里还得计算几个偏移。前面两个都是目录,由于一个是4个字节所以需要乘以4。
在!dd表示查看物理地址。这里第一层是0
kd> !dd 15d45000+0
第一级找到了,要去掉最后三位,这三位是属性。
kd> !dd 15bad000+2A8
第三级一样的。使用!db一个字节一个字节的查看。
kd> !dd 15ba8000+A40kd> !db 15ba8a40
物理地址就已经找到了。
在白皮书描述中整个过程如下(线性地址到物理地址):
Cr3寄存器起到了不可或缺的作用。那Cr3寄存器中存储的究竟是什么呢?
Cr3寄存器不同于其他寄存器,在所有的寄存器中,只有Cr3寄存器存储的地址是 物理地址,其他寄存器存储的都是 线性地址。
Cr3寄存器所存储的物理地址指向了一个页目录表(Page-Directory Table,PDT),也就是我们前面所说的查找时的第一级。在Windows中,一个页的大小通常为4KB(有4MB的),即一个页(页目录表)可以存储1024个页目录表项(PDE)。
而第二级为页表(PTT), 每个页表的大小为4KB,即一个页表可以存储1024个页表项(PTE)。
这种设计方式正是10-10-12分页的由来,由于前面两级是四个字节一组,那么索引为2的10次方就可以获取到每一项(整个是4096字节),也就是10位;而最后一级物理页,一个字节一组,所以需要4096组,索引也要指到4096,也就是2的12次方,正好12位。
上面说到10-10-12分页还有一个大页(4MB),实际上是没有页表(PTT)这一级,也就是PDE直接去索引物理页,那么就是2^10*2^12,正好是4MB。
页表项(PTE)具有以下特征:
-
PTE可以指向一个物理页,也可以不指向物理页
-
多个PTE可以指向同一个物理页
-
一个PTE只能指向一个物理页
我们都知道地址0是绝对不能写入的,如果写入回报0xC0000005错误,那么是什么原因不能写入呢?他的本质实际上就是0地址没有对应的物理页,也就是上面所说的“PTE可以指向一个物理页,也可以不指向物理页”,0地址实际上就没有对应的物理页。
那么我们可以自己将线性地址0的PTE挂载到物理页上,这样就可以读写了。运行这样一段代码:
#include "stdafx.h"int main(int argc, char* argv[]){ int x = 1; printf("x的地址:%xn",&x);
*(int*)0 = 123; printf("0地址数据:%dn",*(int*)0); return 0;
}
我们要做的就是将线性地址0的物理页挂载到局部变量x的物理页,让两个PTE指向的是同一个物理页。
还是先找到当前进程的cr3。
获取x的线性地址:0x0012ff7c,并对其经行分解。
然后找到其对应的物理地址
kd> !dd 1a9e9000 + 0kd> !dd 1a9b4000 + 4BCkd> !db 1a790000 + f7c
让线性地址0的PTE指向同一块物理地址。
如果线性地址为0,那么他就没有PTE,所以这里要写一个PTE。
kd> !dd 1a9b4000
由于二级偏移也是0,那么这里就把二级偏移直接写成物理页的首地址。也就是1a790067。
kd> !ed 1a9b4000 1a790067
回到程序重新执行,在线性地址0的位置已经写入了123。
此时用图形化表示为:
PDE和PTE的低12位实际上是表明属性,这个在之前的练习中已经了解过了。
物理页的属性 = PDE属性 & PTE属性
P位和段的P位是一样的,表示当前PDE或者PTE是否有效,所以PDE与PTE的P位 P=1 才是有效的物理页。
R/W属性
R/W位表示是否是可读可写的。R/W = 0 只读,R/W = 1 可读可写,只有当PDE和PTE的R/W位都为1的时候,该物理页才是可读可写的。
观察下面一段代码:
#include "stdafx.h"#include <windows.h>int main(int argc, char* argv[]){ char* str = "Hello World"; printf("线性地址:%xn",str);
getchar();
DWORD dwVal = (DWORD)str;
*(char*)dwVal = 'M'; printf("%s",str); return 0;
}
直接执行是会报错的,因为str指向的是常量区中的一个字符串,这是不可以写的,但是如果我们更改物理页对应的PDE和PTE的R/W属性,则可以成功改写。
直接执行Access Violation。
拆分线性地址:
通过Cr3找到PTE,发现最后12位属性中R/W位为0。(属性为025)
那么这里就需要让R/W位为1,属性变为027。
!ed 30b4088 19c48027
代码能够顺利执行,字符串成功被修改。
U/S属性
-
U/S = 0 特权用户
-
U/S = 1 普通用户
特权用户也就意味着只有高权限才能访问,普通用户普通权限即可访问。
观察这样一段代码,直接访问肯定是失败。
int main(int argc, char* argv[]){
PDWORD p = (PDWORD)0x8003F00C;
getchar(); printf("高2G地址:%xn",*p); return 0;
}
我们三环程序是无法直接访问高两G内存空间的,这里可以用之前的调用门提权访问,也可以通过修改页属性来访问。
这里具体细节和上面修改R/W差不多。
可以发现这个地址的PDE和PTE的U/S位都是0。
kd> !ed 1d50a800 0003b167kd> !ed 3b0fc 0003f167
这里一不小心把程序放过去了,直接结束了没截图,并没有报错,也就不重新做这个实验了。
P/S位
只对PDE有意义,PS == PageSize的意思 当PS==1的时候 PDE直接指向
物理页 无PTE,低22位是页内偏移。
线性地址只能拆成2段:大小为4MB 俗称“大页”
A 位
是否被访问(读或者写)过 访问过置1 即使只访问一个字节也会导致PDE PTE置1
D 位
脏位 是否被写过 0没有被写过 1被写过
页目录表(PDE)基址
如果系统要保证某个线性地址是有效的,那么必须为其填充正确的PDE与PTE,如果我们想填充PDE与PTE那么必须能够访问PDT与PTT。那么存在2个问题:
1、一定已经有“人”为我们访问PDT与PTT挂好了PDE与PTE,我们只有找到这个线性地址就可以了。
2、这个为我们挂好PDE与PTE的“人”是谁?
结论就是有一个特殊的地址:0xC0300000。存储的值就是PDT。
获取cr3
kd> !dd 131fb000 + c00kd> !dd 131fb000 + c00kd> !dd 131fb000
可以看到通过这个线性地址实际上是重新解析了cr3寄存器。
也就是说,以后不需要Cr3,只需在当前程序内,通过C0300000这个线性地址就可以得到当前程序PDT的首地址了。
那么PDT的首地址可以找到,PTT的首地址呢?
页表(PTT)基址
还是有个特殊的线性地址:0xC0000000
获取debugview的线性地址。
这个线性地址对应的就是PDT表,而PDE表中第一个地址为第一张PTT表。
kd> !dd 18ae9000kd> !dd 0bc7a000
PDT表中第二个地址为第二张PTT表。
kd> !dd 18ae9000kd> !dd 194b9000
然后我们拆分c0000000地址。
kd> !dd 18ae9000 + c00kd> !dd 18ae9000kd> !dd 0bc7a000
可以看到0xc0000000对应的物理地址就是第一张PTT表。
再拆分c0001000地址。
kd> !dd 18ae9000 + c00kd> !dd 18ae9000 + 4kd> !dd 0bc7a000
0xc0001000对应的物理地址就是第二张PTT表。
所以实际上的对应关系应该如下图所示:
根本就不存在什么PDT表,PDT表知识PTT表中的一个特殊的部分。
掌握了0xC0001000和0xC0300000,就掌握了一个进程所有的物理内存读写权限。
PDI和PTI分别指的是再PDT表和PTT表中的索引。
访问页目录表(PDT)的公式:
0xC0300000 + PDI*4
访问页表(PTT)公式:
0xC0000000 + PDI*4096 + PTI*4
1、页表被映射到了从0xC0000000到0xC03FFFFF的4M地址空间。
2、在这1024个表中有一张特殊的表:页目录表。
3、页目录被映射到了0xC0300000开始处的4K地址空间。
写入shellcode到0地址执行
这里直接看注释,要自己捋一下。
// CallGate0Address.cpp : Defines the entry point for the console application.//#include "stdafx.h"#include <stdio.h>#include <stdlib.h>#include <windows.h>char buf[] = {0x6a,0x00,0x6a,0,0x6a,0,0x6a,0,0xE8,0,0,0,0,0xc3};
__declspec(naked) void callGate(){
_asm
{
push 0x30;
pop fs;
pushad;
pushfd;
lea eax,buf;
mov ebx,dword ptr ds:[0xc0300000]; //当0xc0300000位置上的值是0时,表明地址0对应的PDE没有挂上,跳转代码为挂上buf对应的物理页。
//不是0挂PTE就行了
test ebx,ebx;
je __gPDE;
shr eax,12; and eax,0xfffff;
shl eax,2;
add eax, 0xc0000000;
mov eax,[eax];
mov dword ptr ds:[0xc0000000],eax;
jmp __retR;
__gPDE: //获取前10位偏移
shr eax,22; and eax,0x3ff; //乘以4
shl eax,2; //将buf对应的PDE挂到0地址
add eax, 0xc0300000;
mov eax,[eax];
mov dword ptr ds:[0xc0300000],eax;
__retR:
popfd;
popad;
retf;
}
}int main(int argc, char* argv[]){ unsigned int functionAddress = (unsigned int)MessageBox; //获取在物理页上的偏移,后12位。
int offset1 = ((unsigned int)buf) & 0xfff;
*((unsigned int*)&buf[9]) = functionAddress - (13 + offset1); char segmentGate[] = {0,0,0,0,0x48,0}; printf("MessageBox:%x callGate:%x buf:%xn",MessageBox,callGate,buf);
system("pause");
_asm
{
call fword ptr segmentGate;
push 0x3b;
pop fs;
mov eax,offset1;
call eax;
} return 0;
}
kd> eq 8003f048 0040ec00`0008100a
下一节进入2-9-9-12分页。
- 结尾 - 精彩推荐 【技术分享】RedGuard - Excellent C2 Front Flow Control tool 【技术分享】后门防御-Neural Cleanse分析及复现 【技术分享】从Java反序列化漏洞题看CodeQL数据流 戳“阅读原文”查看更多内容 本文由拔丝英语网 - buzzrecipe.com(精选英语文章+课程)收藏,供学习使用,分享转发是更大的支持!由 安全客原创,版权归原作者所有。
发表评论