Windows XP 页保护(一)
ʕ •ᴥ•ʔ ɔ:
1 CR3和物理内存
如下指令:
1 | mov eax,dword ptr ds:[0x12345678] |
有效地址:0x12345678
线性地址:ds.Base + 0x12345678
物理地址:通过线性地址进行转换得到
存在内存的DLL会个每个EXE映射一个线性地址,要想通过线性地址找到真正的物理地址,就必须借助于CR3寄存器。该寄存器存的值是物理地址,每个进程一个CR3的值,不是一个线程一个CR3的值。
物理内存的单位:页/4KB。
如下, CR3指向一个物理页,一共4096字节:
- 第一级:叫做页目录表,大小1页/4KB/4096字节/1024项。
- 第二级:叫做页表,大小1页/4KB/4096字节/1024项。
- 第三级:真正的物理页内存。
2 10-10-12分页模式
X86模式下存在10-10-12分页和2-9-9-12分页来将线性地址转换为物理地址。
10-10-12分页转换规则如下:
转换步骤(0x000AAA40):
- 修改Windows XP启动配置,将
noexecute
改成execute
,使用10-10-12分页模式。 - 将线性地址做10-10-12排列:
- 10:对应第一级目录,0000 0000 00 = 0x0
- 10:对应第二级页表,00 1010 1010 = 0xAA
- 12:对应第三级物理页,1010 0100 0000 = 0xA40
- 将高地址的两段左移4位(第一和第二张表存的是地址,大小是4字节,所以要乘4):
- 0x0*4 = 0x0
- 0xAA*4 = 0x2A8
- 根据CR3寄存器的值进行查表,CR3是一个基址,$Dir=CR3+Offset_1*4$,指向第一级目录。
- 将步骤3得到的地址低12位(属性)置0,然后 PageTable = (Dir & 0xFFFFF000) + Offset_2 * 4,指向第二级的页表。
- 将步骤4得到的地址低12位(属性)置0,然后 Address = (PageTable & 0xFFFFF000) + Offset_3 ,即为物理地址。
实际上,把PDT、PTT表堪称两个 DWORD
数组:
1 | DWORD PDT[Num_PDT]; |
3 练习:查找字符串的物理地址
题目:打开Notepad.exe输入字符串并查找其对应的物理地址。
修改Windows XP启动配置,将
noexecute
改成execute
,使用10-10-12分页模式。CE查看字符串虚拟地址:0x000AAFF8
按10-10-12分页拆分偏移地址:
- 10:0x0*4 = 0x0
- 10:AA*4 = 0x2A8
- 12:0xFF8
在Windbg中使用
!process 0 0
获取当前进程CR3的值:0x15dd4000(物理地址)获取在第一级目录中的值
!dd 0x15dd4000+0x0
:$Dir=CR3+Offset_1*4$ = 0x15be1867。将第一级目录低12位(属性)置0后查偏移,PageTable = (Dir & 0xFFFFF000) + Offset_2 * 4 ,
!dd 0x15be1000+0x2A8
= 0x1609b86。将页表查到的值低12位(属性)置0后查偏移,Address = (PageTable & 0xFFFFF000) + Offset_3 ,
!db 0x1609b000 + 0xFF8
。
4 PDT、PTT表
CR3是唯一一个存储物理地址的寄存器,为第一级页目录表的基址。
- 第一级:PDT表,页目录表,总共1页(4KB=4096字节、1000h),每个成员占4字节,每个成员/每一项称为PDE,共1024项
- 第二级:PTT表,页表,总共1页,每一项称为PTE(4字节),共1024项
- 第三级:物理页,一个页的大小是4KB,在同一个物理页地址属性是相同的。
10-10-12分页原理:第一级和第二级共1024项,可以使用10位来表示。物理页大小为4KB=2^ 12,共需要12位来表示。
10-10-12内存寻址大小:1024*1024*4096=4GB(当前CPU能识别的物理内存大小)
PDE和PTE的特征:
- PTE可以为空(内存还未分配)
- 多个PTE可以指向同一个物理页
- 一个PTE仅指向一个物理页
- 同一个进程中(一个CR3)两个线性地址只要高20位相同,则指向的物理页相同,只是页内偏移不同
5 练习:挂靠物理页读写NULL地址
编程中,不能读写NULL,否则会报0xC0000005
错误,原因是NULL指针地址的PTE没有对应的物理页,因此,只要我们让NULL指针最终映射到一块可读写的物理页,就可以用NULL去读写数据了。
注意:如果NULL的PDE项不为0,则直接修改NULL的PTE即可。
只需要将线性地址0x00000000
的PTE挂载到变量x对应的物理页上,这样就可以读写了。
1 |
|
在XP中运行代码如下。
在Windbg中获取当前当前进程CR3的值并找到变量x的PDE、PTE、物理页地址。
按10-10-12拆分0x0012FF7C:
- 10:0000 0000 00 = 0x0 * 4 = 0x0;
- 10:01 0010 1111 = 0x12F * 4 = 0x4BC;
- 12:0xF7C。
使用
!process 0 0
获取CR3的值:0x216e8000。!dd 0x216e8000+0x0
获取变量x的PDE:PDEx = 0x21971867,PTEx = 0x21C86867。!dd 0x21971000+0x4BC
获取变量x的PTE:PTEx = 0x21C86000(去属性),物理地址 = 0x21c86F7C。!dd 0x21c86000+0xF7C
获取变量x的值:0x1。
按10-10-12拆分0x00000000,全是0。
!dd 0x216e8000+0x0
获取NULL地址的PTE:PDE0 = 0x21971867。!dd 0x21971000+0x0
获取NULL的物理地址:PTE0 = 0x0。
可以得出:PTEx = 0x21C86867,PTE0 = 0x0。则将变量x的PTE赋值给NULL的PTE即可,NULL的PDE去属性为:0x21971000,执行
!ed 0x21971000 0x21C86867
即可。流程如下:回到XP中继续执行代码如下:
结论:
现在x和NULL的PTE值相同,NULL和x处于同一个物理页,物理页地址范围 0x21C86000~0x21C86FFF,但是二者物理地址仍然是不同的,因为x在物理页内的偏移是 0xF7C,而NULL的偏移是0。
但是没有关系,NULL已经指向了一块可用的物理页了,现在可以对NULL进行读写了。
6 PDE、PTE属性
PDE和PTE的低12位表示物理页的属性。
物理页的属性 = PDE属性 & PTE属性(除了G位和2-9-9-12的最高XD位,为OR,)6.1 P位[0]
P位(Present)是存在位标志,该标志表明,该表项所指向的页或者页表当前是否在内存中。
- P = 1,这个页在物理内存中,将执行地址转换。
- P = 0,表示这个页不在物理内存中,如果处理器试图访问该页,将产生一个缺页异常(PF),
INT E
异常。
处理器并不置位或者清零该位;而是由操作系统来维护该标志的状态。
当P = 0时将会触发int e
缺页异常,接着操作系统会再去检查PDE和PTE的属性来决定采用以下4种换页方式中的哪一种:
如果处理器产生一个缺页异常,操作系统必须按序执行如下操作:
- 如果有必要,将该页从磁盘拷贝到内存中。
- 将该页地址装载入页表或者页目录项并设置它的存在标志。其他的位,比如脏
位(D位)和访问位,也必须同时被设置。 - 使TLB中的当前页表项失效。
- 从缺页异常处理程序返回,重新执行被中断的进程或任务。
6.2 R/W位[1]
R/W位(Read/Write)读写标志。
- R/W = 1,该页内存可读可写。
- R/W = 0,该页内存只读。
只有当PDE和PTE的R/W位都为1的时候,该物理页才是可读可写的。R/W标志与U/S标志和CR0寄存器中的WP标志共同起作用。
6.2.1 练习:修改常量区数据
C语言中,修改常量区的字符串是不允许的,原因是物理页不具有写权限。
只需要将常量区的线性地址转换后的物理地址的PDE_R/W & PTE_R/W = 1
即可。
如下代码:
1 |
|
在XP中运行VC6,得到常量字符串的地址:0x00423054。
按10-10-12拆分线性地址0x00423054:
- 10:0000 0000 01 = 0x1 * 4 = 0x4
- 10:00 0010 0011 = 0x23 * 4 = 0x8C
- 12:0x54
使用
!process 0 0
获取CR3的值:0x2d86f000。!dd 0x2d86f000+0x4
获取PDE:PDE = 0x2e24c867,R/W位为1。!dd 0x2e24c000+0x8C
获取PTE:PTE = 0x2d92f025,R/W位为0。将PTE的R/W位修改为1,
!ed 2e24c08c 0x2d92f027
。回到XP中继续执行代码。
6.3 U/S位[2]
U/S位(User/Supervisor),分别为普通用户/超级用户。
- U/S = 1,该页权限为普通用户可访问。
- U/S = 0,该页权限为特权用户可访问。
U/S标志与R/W标志和CR0寄存器中的WP标志共同起作用。
总结:
- 2G以上是内核才能访问的原因是U/S位的设置问题,如果将内核的某个页设置为1就可以在R3访问了。
- 0、1、2是系统环,可以访问系统页和用户页(0环是特权级环)
- 1、2环虽然不是特权级环,但是是系统环。
- 3环是用户环,仅可以访问用户页。
6.3.1 练习:在R3访问高2G内存
3环的代码仅能访问低2G的原因,是因为高2G的线性地址对应的物理页PDE_U/S & PDE_U/S = 0
,修改相应的位使与后的结果为1即可在3环访问。
练习:修改一个高2G线性地址的PDE/PTE属性,实现Ring3可读写。比如:0x8003F00C
1 |
|
按10-10-12分页拆分线性地址:0x8003F00C
- 10:1000 0000 00 = 10 0000 0000 = 0x200 * 4 = 0x800
- 10:00 0011 1111 = 0x3F * 4 = 0xFC
- 12:0xC
在XP的VC6中直接运行代码,报错如下:
重新在XP的VC6中运行代码。
!process 0 0
获取CR3的值:0x3493a000。!dd 0x3493a000+0x800
获取PDE的值:0x0003b163,U/S位为0。!dd 0x0003b000+0xFC
获取PTE的值:0x0003f163,U/S位为0。
将PDE、PTE的U/S位置1。
!ed 0x3493a800 0x0003b167
!ed 0x0003b0fc 0x0003f167
回到XP中继续执行代码。
结果仍然还是不能读写高2G的地址,重启了几遍还是没能成功,但是同样的方法,其他人是可以成功的。目前还没有找到解决方法。
已经找到方法解决:原因是TLB(可以看后面TLB的内容了解TLB),首先看到PDE、PTE中下标8即第9位的G = 1。
- PDE中的PS = 0,此时是小页,G位无效。
- PTE中的G = 1,说明原来的TLB中有线性地址0x8003F00C的缓存(物理地址)。
故这里修改后运行失败,若先将PTE的G位清零(
invlpg dword ptr ds:[0x8003F00C]
),再修改PTE即可成功。
6.4 A位[5]
A(Accessed)位是标记是否被访问(读或者写)过。访问过置1 即使只访问一个字节也会导致PDE PTE置1
- A = 1,该页物理内存被访问过。
- A = 0,该页物理内存未被访问过。
这个标志是个“粘性”标志,就是说一旦被设置,处理器不会隐式的给它清零。
只有软件能清零该位。内存管理软件使用访问位和脏位(D位)来调度页或者页表进出物理内存。
6.5 D位(PTE)[6]
D位(Dirty)位仅存在于PTE下标为6的位,PDE中该位保留(Reserve),为0。
D位指明该页是否曾经被写入过:
- D = 1,该页物理内存被写入过。
- D = 0,该页物理内存未被被写入过。
内存管理软件使用访问位(A)和脏位(D)来调度页或者页表进出物理内存。
这个标志是一个粘性标志,就是说,一旦被设置,处理器不会隐式的对它清零。只有软件可以对它清零。
6.6 PS位(PDE)、PAT(PTE)[7]
P/S位[PDE7]
P/S位(Page Size)确定页的大小,仅对PDE有意义。
- P/S = 1,页大小为4MB,此时不存在页表,低22位为一页(1024*4096)。
- P/S = 0,页大小为4KB。
P/S = 1时,线性地址只能拆成2段:大小为4MB ,俗称“大页”。
PAT位[PTE7]
PAT位(Page Table Attribute Index)页属性表,在奔腾I处理器中采用这个标志用来选择PAT项。
- PAT = 1,这个标志与PCD和PWT标志一起,被用来选取PAT项。
- PAT = 0,该位目前保留。
6.7 G[8]、PET[3]、PCD[4]
学完控制寄存器与TLB才能讲,此处略过。
6.8 有效位[9 10 11]
有效位在发生缺页时(PTE的P=0)使用。分以下四种情况(具体可见内存管理-无处不在的缺页异常)。关于这部分知识,等以后学习了内存管理的缺页异常就知道了,这里简单介绍一下。
7 页目录表基址
CR3中存储的是物理地址,不能在程序中直接读取的。如果想读取,也要把CR3的值挂到PDT和PTT中才能访问,那么怎么通过线性地址访问PDT和PTT呢?
在10-10-12分页模式下的PDT表:
- PDT表实际上是PTT表中的一张表,刚好是第0x300张。
- PDT表共1024个PDE项,每一项指向一张PTT表,其中第0x300项PDE指向自身。
- 在代码中可以通过线性地址
0xC0300000
来得到PDT表的基址。
关于PDT表的基址有以下结论:
- 通过线性地址0xC0300000找到的物理页就是页目录表。
- 这个物理页即是页目录表本身也是页表,PDT表实际上就是PTT表中的一张。
- 页目录表是一张特殊的页表,每一项PTE指向的不是普通的物理页,而是指向其他的页表。
- 如果我们要访问第N个PDE,那么有如下公式:$0xC0300000 + N*4 $ 。
8 页表基址
既然有线性地址0xC0300000指向PDT,且它是PTT表的第0x300张,一张表大小为1000h。则线性地址0xC0000000指向PTT表的基址。
则地址0xC0001000为第2张PTT表,到Windbg中查确实如此。
在10-10-12分页模式下的PTT表:
- 页表被映射到了从0xC0000000到0xC03FFFFF的4M地址空间。
- 1024张页表的线性地址是连续的,但物理地址不连续
- 在这1024个表中有一张特殊的表:页目录表。
- 页目录被映射到了0xC0300000开始处的4K地址空间。
关于PDT、PTT表的结论:
掌握了这两个地址,就掌握了一个进程所有的物理内存读写权限。
公式:
- 什么是PDI与PTI:10-10-12
- 10:PDI
- 10:PTI
- 访问页目录表项(PDE)的公式:$0xC0300000 + PDI*4$
- 访问页表项(PTE)的公式:$0xC0000000 + PDI*4096 + PTI*4$
9 练习:在NULL地址执行ShellCode
1 |
|