Windows XP 页保护(一)

ʕ •ᴥ•ʔ ɔ:

1 CR3和物理内存

如下指令:

1
mov eax,dword ptr ds:[0x12345678]

有效地址:0x12345678

线性地址:ds.Base + 0x12345678

物理地址:通过线性地址进行转换得到

存在内存的DLL会个每个EXE映射一个线性地址,要想通过线性地址找到真正的物理地址,就必须借助于CR3寄存器。该寄存器存的值是物理地址,每个进程一个CR3的值,不是一个线程一个CR3的值

物理内存的单位:页/4KB。

如下, CR3指向一个物理页,一共4096字节:

1.png

  • 第一级:叫做页目录表,大小1页/4KB/4096字节/1024项。
  • 第二级:叫做页表,大小1页/4KB/4096字节/1024项。
  • 第三级:真正的物理页内存。

2 10-10-12分页模式

X86模式下存在10-10-12分页和2-9-9-12分页来将线性地址转换为物理地址。

10-10-12分页转换规则如下:

2.png

转换步骤(0x000AAA40):

  1. 修改Windows XP启动配置,将noexecute改成execute,使用10-10-12分页模式。
  2. 将线性地址做10-10-12排列:
    • 10:对应第一级目录,0000 0000 00 = 0x0
    • 10:对应第二级页表,00 1010 1010 = 0xAA
    • 12:对应第三级物理页,1010 0100 0000 = 0xA40
  3. 将高地址的两段左移4位(第一和第二张表存的是地址,大小是4字节,所以要乘4):
    • 0x0*4 = 0x0
    • 0xAA*4 = 0x2A8
  4. 根据CR3寄存器的值进行查表,CR3是一个基址,$Dir=CR3+Offset_1*4$,指向第一级目录。
  5. 将步骤3得到的地址低12位(属性)置0,然后 PageTable = (Dir & 0xFFFFF000) + Offset_2 * 4,指向第二级的页表。
  6. 将步骤4得到的地址低12位(属性)置0,然后 Address = (PageTable & 0xFFFFF000) + Offset_3 ,即为物理地址。

实际上,把PDT、PTT表堪称两个 DWORD 数组:

1
2
3
4
DWORD PDT[Num_PDT];
DWORD PTT[Num_PTT];
1、每一项 PDT[x] 对应一张PTT表,也就是对应一个 PTT[Num_PTT] 数组
2、数组 PDT[Num_PDT] 起始地址为 CR3,32位线性地址的的高10位为数组索引,计算第x项PDE的地址为:CR3+((VirtualAddress>>22)&0x3FF)*4

3 练习:查找字符串的物理地址

题目:打开Notepad.exe输入字符串并查找其对应的物理地址。

  1. 修改Windows XP启动配置,将noexecute改成execute,使用10-10-12分页模式。

  2. CE查看字符串虚拟地址:0x000AAFF8

    3.png

  3. 按10-10-12分页拆分偏移地址:

    • 10:0x0*4 = 0x0
    • 10:AA*4 = 0x2A8
    • 12:0xFF8
  4. 在Windbg中使用!process 0 0获取当前进程CR3的值:0x15dd4000(物理地址)

    4.png

  5. 获取在第一级目录中的值!dd 0x15dd4000+0x0:$Dir=CR3+Offset_1*4$ = 0x15be1867。

    5.png

  6. 将第一级目录低12位(属性)置0后查偏移,PageTable = (Dir & 0xFFFFF000) + Offset_2 * 4 ,!dd 0x15be1000+0x2A8 = 0x1609b86。

    6.png

  7. 将页表查到的值低12位(属性)置0后查偏移,Address = (PageTable & 0xFFFFF000) + Offset_3 ,!db 0x1609b000 + 0xFF8

    7.png

4 PDT、PTT表

8.png

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的特征:

  1. PTE可以为空(内存还未分配)
  2. 多个PTE可以指向同一个物理页
  3. 一个PTE仅指向一个物理页
  4. 同一个进程中(一个CR3)两个线性地址只要高20位相同,则指向的物理页相同,只是页内偏移不同

5 练习:挂靠物理页读写NULL地址

编程中,不能读写NULL,否则会报0xC0000005错误,原因是NULL指针地址的PTE没有对应的物理页,因此,只要我们让NULL指针最终映射到一块可读写的物理页,就可以用NULL去读写数据了。

注意:如果NULL的PDE项不为0,则直接修改NULL的PTE即可。

只需要将线性地址0x00000000的PTE挂载到变量x对应的物理页上,这样就可以读写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "stdafx.h"
#include <tchar.h>
#include <stdlib.h>

int _tmain(int argc, _TCHAR* argv[])
{
int x = 0x1;
printf("变量x的线性地址为:%X\n",&x);
getchar(); //在Windbg中获取CR3的值并获取线性地址x的PTE和NULL的PTE

*(int*)NULL = 0x123;
printf("NULL地址的值为:%X\n",*(int*)NULL);
getchar();
system("Pause");
return 0;
}
  1. 在XP中运行代码如下。

    9.png

  2. 在Windbg中获取当前当前进程CR3的值并找到变量x的PDE、PTE、物理页地址。

    1. 按10-10-12拆分0x0012FF7C:

      • 10:0000 0000 00 = 0x0 * 4 = 0x0;
      • 10:01 0010 1111 = 0x12F * 4 = 0x4BC;
      • 12:0xF7C。
    2. 使用!process 0 0获取CR3的值:0x216e8000。

    3. !dd 0x216e8000+0x0获取变量x的PDE:PDEx = 0x21971867,PTEx = 0x21C86867。

    4. !dd 0x21971000+0x4BC获取变量x的PTE:PTEx = 0x21C86000(去属性),物理地址 = 0x21c86F7C。

    5. !dd 0x21c86000+0xF7C获取变量x的值:0x1。

      10.png

  3. 按10-10-12拆分0x00000000,全是0。

    1. !dd 0x216e8000+0x0获取NULL地址的PTE:PDE0 = 0x21971867。

    2. !dd 0x21971000+0x0获取NULL的物理地址:PTE0 = 0x0。

      11.png

  4. 可以得出:PTEx = 0x21C86867,PTE0 = 0x0。则将变量x的PTE赋值给NULL的PTE即可,NULL的PDE去属性为:0x21971000,执行!ed 0x21971000 0x21C86867即可。流程如下:

    12.png

  5. 回到XP中继续执行代码如下:

    13.png

结论:

现在x和NULL的PTE值相同,NULL和x处于同一个物理页,物理页地址范围 0x21C86000~0x21C86FFF,但是二者物理地址仍然是不同的,因为x在物理页内的偏移是 0xF7C,而NULL的偏移是0。

但是没有关系,NULL已经指向了一块可用的物理页了,现在可以对NULL进行读写了。

6 PDE、PTE属性

14.png

15.png

16.png

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种换页方式中的哪一种:

如果处理器产生一个缺页异常,操作系统必须按序执行如下操作:

  1. 如果有必要,将该页从磁盘拷贝到内存中。
  2. 将该页地址装载入页表或者页目录项并设置它的存在标志。其他的位,比如脏
    位(D位)和访问位,也必须同时被设置。
  3. 使TLB中的当前页表项失效。
  4. 从缺页异常处理程序返回,重新执行被中断的进程或任务。

6.2 R/W位[1]

14.png

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "stdafx.h"
#include <tchar.h>

int _tmain(int argc, _TCHAR* argv[])
{
char* szBuffer = "Const Zone Read/Write";
printf("常量字符串的地址为:0x%08X\n",szBuffer);
printf("常量字符串为:%s\n",szBuffer);
getchar(); //修改改线性地址转换后的物理地址的PDE、PTE的R/W位为1

szBuffer[4] = 'T';
printf("常量字符串为:%s\n",szBuffer);
getchar();
return 0;
}
  1. 在XP中运行VC6,得到常量字符串的地址:0x00423054。

    17.png

  2. 按10-10-12拆分线性地址0x00423054:

    • 10:0000 0000 01 = 0x1 * 4 = 0x4
    • 10:00 0010 0011 = 0x23 * 4 = 0x8C
    • 12:0x54
  3. 使用!process 0 0获取CR3的值:0x2d86f000。

  4. !dd 0x2d86f000+0x4获取PDE:PDE = 0x2e24c867,R/W位为1。

  5. !dd 0x2e24c000+0x8C获取PTE:PTE = 0x2d92f025,R/W位为0。

    18.png

  6. 将PTE的R/W位修改为1,!ed 2e24c08c 0x2d92f027

  7. 回到XP中继续执行代码。

    19.png

6.3 U/S位[2]

14.png

U/S位(User/Supervisor),分别为普通用户/超级用户。

  • U/S = 1,该页权限为普通用户可访问。
  • U/S = 0,该页权限为特权用户可访问。

U/S标志与R/W标志和CR0寄存器中的WP标志共同起作用。

总结:

  1. 2G以上是内核才能访问的原因是U/S位的设置问题,如果将内核的某个页设置为1就可以在R3访问了。
  2. 0、1、2是系统环,可以访问系统页和用户页(0环是特权级环)
  3. 1、2环虽然不是特权级环,但是是系统环。
  4. 3环是用户环,仅可以访问用户页

6.3.1 练习:在R3访问高2G内存

3环的代码仅能访问低2G的原因,是因为高2G的线性地址对应的物理页PDE_U/S & PDE_U/S = 0,修改相应的位使后的结果为1即可在3环访问。

练习:修改一个高2G线性地址的PDE/PTE属性,实现Ring3可读写。比如:0x8003F00C

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "stdafx.h"
#include <tchar.h>
#include <windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
PDWORD pR3_Number = (PDWORD)0x8003F00C;
getchar(); //进入Windbg修改线性地址0x8003F00C对应的PDE、PTE的U/S位为1
*pR3_Number = 12345678;
printf("地址0x8003F00C存储的值为:%d\n",*pR3_Number);
getchar();
return 0;
}
  1. 按10-10-12分页拆分线性地址:0x8003F00C

    • 10:1000 0000 00 = 10 0000 0000 = 0x200 * 4 = 0x800
    • 10:00 0011 1111 = 0x3F * 4 = 0xFC
    • 12:0xC
  2. 在XP的VC6中直接运行代码,报错如下:

    20.png

  3. 重新在XP的VC6中运行代码。

    1. !process 0 0获取CR3的值:0x3493a000。
    2. !dd 0x3493a000+0x800获取PDE的值:0x0003b163,U/S位为0。
    3. !dd 0x0003b000+0xFC获取PTE的值:0x0003f163,U/S位为0。
  4. 将PDE、PTE的U/S位置1。

    • !ed 0x3493a800 0x0003b167
    • !ed 0x0003b0fc 0x0003f167

    21.png

  5. 回到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]

14.png

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]

14.png

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)使用。分以下四种情况(具体可见内存管理-无处不在的缺页异常)。关于这部分知识,等以后学习了内存管理的缺页异常就知道了,这里简单介绍一下。

14.png

22.png

7 页目录表基址

CR3中存储的是物理地址,不能在程序中直接读取的。如果想读取,也要把CR3的值挂到PDT和PTT中才能访问,那么怎么通过线性地址访问PDT和PTT呢?

在10-10-12分页模式下的PDT表:

  1. PDT表实际上是PTT表中的一张表,刚好是第0x300张。
  2. PDT表共1024个PDE项,每一项指向一张PTT表,其中第0x300项PDE指向自身。
  3. 在代码中可以通过线性地址0xC0300000来得到PDT表的基址。

23.png

关于PDT表的基址有以下结论:

  1. 通过线性地址0xC0300000找到的物理页就是页目录表
  2. 这个物理页即是页目录表本身也是页表,PDT表实际上就是PTT表中的一张。
  3. 页目录表是一张特殊的页表,每一项PTE指向的不是普通的物理页,而是指向其他的页表。
  4. 如果我们要访问第N个PDE,那么有如下公式:$0xC0300000 + N*4 $ 。

8 页表基址

既然有线性地址0xC0300000指向PDT,且它是PTT表的第0x300张,一张表大小为1000h。则线性地址0xC0000000指向PTT表的基址。

则地址0xC0001000为第2张PTT表,到Windbg中查确实如此。

在10-10-12分页模式下的PTT表:

  1. 页表被映射到了从0xC0000000到0xC03FFFFF的4M地址空间。
  2. 1024张页表的线性地址是连续的,但物理地址不连续
  3. 在这1024个表中有一张特殊的表:页目录表
  4. 页目录被映射到了0xC0300000开始处的4K地址空间。

关于PDT、PTT表的结论:

掌握了这两个地址,就掌握了一个进程所有的物理内存读写权限。

公式:

  1. 什么是PDI与PTI:10-10-12
    • 10:PDI
    • 10:PTI
  2. 访问页目录表项(PDE)的公式:$0xC0300000 + PDI*4$
  3. 访问页表项(PTE)的公式:$0xC0000000 + PDI*4096 + PTI*4$

9 练习:在NULL地址执行ShellCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "stdafx.h"
#include <tchar.h>
#include <windows.h>

BYTE ShellCode[] = {0x6A,0x00,0x6A,0x00,0x6A,0x00,0x6A,0x00,0xE8,0x00,0x00,0x00,0x00};

VOID __declspec(naked) CallGate_SetPDEPTE()
{
__asm
{
pushad
pushfd

lea eax,ShellCode;
mov ebx,eax;
mov ecx,dword ptr ds:[0xC0300000];
test ecx,ecx; //该处如果去挂PDE会发生失败,不知道为啥,AND
je __SetPDE; //实际代码运行的时候是jne,因为PDE不是0

//挂PTE
//PTE = 0xC0000000 + PDI*4096 + PTI*4
//4096 = 2^12
shr ebx,22;
and ebx,0x3FF; //PDI
shl ebx,12; //PDI*4096
lea eax,ShellCode;
shr eax,12;
and eax,0x3FF; //PTI
shl eax,2; //PTI*4
add eax,0xC0000000; //0xC0000000 + PTI*4
add eax,ebx; //eax = PTE = 0xC0000000 + PTI*4 + PDI*4096

mov eax,dword ptr ds:[eax];
mov dword ptr ds:[0xC0000000],eax;//将ShellCode_PTE挂到NULL的PTE上去

jmp __RET3;

__SetPDE:
//将线性地址ShellCode的PDE挂到NULL地址的PDE上去
//ShellCode_PDE = 0xC0300000 + PDI*4
//取线性地址高10位(PDI)
shr eax,22;
and eax,0x3FF; //将多余的位清零仅保留最高10位
shl eax,2; //PDI*4
add eax,0xC0300000; //0xC0300000 + PDI*4

mov edx,dword ptr ds:[eax];
mov dword ptr ds:[0xC0300000],edx;//将ShellCode_PDE挂到NULL的PDE上去

__RET3:
popfd;
popad;
retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
//填充E8 Call的代码 = 目的地址 - 返回地址(当前代码长度+当前代码基址)
DWORD dwOffset = (DWORD)ShellCode & 0xFFF;
*(PDWORD)&ShellCode[9] = (DWORD)MessageBox - (13+dwOffset);
BYTE CallGate[] = {0,0,0,0,0x48,0};
DWORD dwShellCode = (DWORD)ShellCode;
//*(PDWORD)&ShellCode[9] = (DWORD)MessageBox - (13+dwShellCode);

printf("CallGate_SetPDEPTE地址:%08X,MessageBox的地址:%08X\n",(DWORD)CallGate_SetPDEPTE,(DWORD)MessageBox);
printf("请在windbg执行: eq 8003f048 %04xec00`0008%04x\n", ((DWORD)CallGate_SetPDEPTE>>16) & 0x0000FFFF,\
(DWORD)CallGate_SetPDEPTE & 0xFFFF);

getchar();
__asm
{
call fword ptr ds:[CallGate];
call dwOffset;
}
getchar();
return 0;
}

24.png