Windows XP 页保护(二)

ʕ •ᴥ•ʔ ɔ:

1 2-9-9-12分页

2-9-9-12分页结构(PAE,物理地址扩展):

  • 12:页的大小是确定的,4KB不能随便改,所以12确定了。
  • 9:如果想增大物理内存的访问范围,就需要增大PTE,增大到36位,考虑对齐的因素,增加到8个字节。4KB = 512*8 = 1024*4,512 = 2^ 9。PTI
  • 9:如果PTE所在物理地址超过4GB,则同样也需要一个超过32位的PDE来进行寻址,考虑内存对齐,则PDE也是8字节。PDI
  • 2:2 = 32 -(12+9+9),还剩2位,所以就再做一级叫PDPI

这里需要注意物理地址和线性地址:32位CPU的线性地址范围是32位,大小为4GB。在10-10-12分页模式下,物理地址和线性地址大小相等为4GB,那么在多进程情况下就需要不停的进行换物理页,为了节省换页的开销,Intel设计了2-9-9-12分页。但是Windows在这种分页模式下仅使用了36位来进行寻址方式,2^ 36 = 64GB。注意:

  • 2-9-9-12分页模式下,线性地址范围还是4GB。
  • 物理地址范围为64GB。
  • 物理地址范围和内存条大小无关,有可能此时内存条大小仅为2GB。
  • 物理地址大小由8字节的PTE中的36位来决定。
  • PDE也是8字节是因为PTE所在的物理页的物理地址可能超过4GB,同时考虑内存对齐。

25.png

其中:CR3、物理页数据是以32位存放,PDPTE、PDE、PTE为64位

1.1 开启2-9-9-12分页

  • 10-10-12分页:将C:\boot.ini文件中的noexecute改为execute重启。
  • 2-9-9-12分页:将C:\boot.ini文件中的execute改为noexecute重启。

1.2 PDPE(2)

由于CR3是32位的,所以PDPTE所在物理页的物理地址范围是0-4GB。

PDPT表(页目录指针表)是2-9-9-12分页模式下的线性地址的高两位,共组成四项PDPTE。PDPE结构如下:

26.png

每个PDPTE为8字节:

  1. 0~11:属性,也是PDE的偏移。
  2. 9~11:给操作系统用的,CPU不用。
  3. 12~35:PDT的基址,把低12位补0,0-35:共36位为PDT基址
  4. 36~63:保留位,保留位并不意味着填0就行了,而是CPU要用的,但是我们不能用,并且CPU用了还不会告诉我们怎么用的。

1.3 PDE(9)

每个PDT表(页目录表)大小为4KB,共512个PTE,每个PTE为8字节。

PDE根据PS位来区分是大页还是小页:

  • PS = 1:大页,剩下的9+12=21位,即2^ 21=2MB为 1 页。此时PDE中的G位才有意义:
    • G = 1,表示全局页,是多个进程共享的,与TLB相关。
    • G = 0,非全局页(独享页),如VirtualAlloc申请的内存。
  • PS = 0,PDE指向页表,每个物理页大小4KB,此时G位是无效位,一直为0

G=1,即为全局页,进程(CR3)切换时,TLB中的记录不会被刷新。

4KB物理页下:0-35:共36位为PTT基址

PDE大页、小页的结构图如下:(低36位为页表基址)

27.png

28.png

  • VirtualAlloc申请的内存是独享内存,仅当前进程可用

  • CreateFileMapping申请的是共享物理页,所有进程都可以使用,如放DLL的内存

1.4 PTE(9)

PTT表(页表),PTE结构如下:

29.png

PTE中35-12是物理页基址,24位,低12位补0:物理页基址+12位的页内偏移指向具体数据,共36位具体的物理数据是按32位存放的

1.5 XD标志位[63]

在PAE分页模式下,PDE与PTE的最高位为XD/NX位,AMD中称为NX位,即No Excetion。DEP就是PDE、PTE最高位X位。(DEP保护)

30.png

段的属性有可读、可写和可执行。
页的属性有可读、可写。
所以,Intel就做了硬件保护(保护数据),做了一个不可执行位:

  • XD = 1:该物理页数据不可执行。即使软件溢出了也没有关系,即使你的EIP蹦到了危险的“数据区”,也是不可以执行的!

1.6 PDPTE、PDE、PTE的属性

除了G位和XD位属性为OR外,其余属性为: PDPTE属性 & PDE属性 & PTE属性

1.7 PDT、PTT表

2-9-9-12分页下的PDT、PTT表结构如下:

33.png

根据示意图,0xC0000000是第一张页表的线性地址,0xC0600000是第一张页目录表的线性地址。

PDT表共4张,每张4KB。

2-9-9-12:
PDPTI-PDI-PTI-OFFSET

公式:
PDE = 0xc0600000 + (PDPTI*4KB) + (PDI*8)
PTE = 0xc0000000 + (PDPTI*2MB) + (PDI*4KB) + (PTI*8)

更高效的公式(MmIsAddressValid是这么干的)
PDE = 0xc0600000 + ((addr >> 18) & 0x3ff8)
PTE = 0xc0000000 + ((addr >> 9) & 0x7ffff8)

1.8 练习:找一个线性地址对应的物理地址

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

int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwNumber = 0x12345678;
printf("变量dwNumber的线性地址为:%p\n",&dwNumber);
getchar();
return 0;
}
  1. 开启2-9-9-12分页:将XPC:\boot.ini文件中的execute改为noexecute重启。
  2. 运行程序,线性地址为:0x0012FF7C,进行2-9-9-12拆分:
    • 2:00 = 0x0 * 8 = 0x0
    • 9:00 0000 000 = 0x0 * 8 = 0x0
    • 9:1 0010 1111 = 0x12F * 8 = 0x978
    • 12:0xF7C
  3. 在Windbg中使用!process 0 0获取当前进程CR3的值:0x06e40340(4字节物理地址)
    1. 查PDPTE:!dq 0x06e40340 + 0x0:00000000`18403801。
    2. 查PDE:取PDPTE的低36位作为基址查PDE:0x018403801,然后将低12位属性清零,!dq 0x018403000 + 0x0:00000000`184f1867。
    3. 查PTE:取PDE的低36位作为基址查PTE:0x0184f1867,然后将低12位属性清零,!dq 0x0184f1000 + 0x978:80000000`1844c867。
    4. 查物理页中的数据:取PTE的低36位作为基址查物理页数据:0x01844c867,然后将低12位属性清零。因为物理页是4字节的:!dd 0x01844c000 + 0xF7C :12345678。
  4. 使用 !vtop 指令验证线性地址转换为物理地址是否正确。!vtop CR3 线性地址

31.png

32.png

1.9 练习:给NULL挂上物理页

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

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

int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwNumber = 0x12345678;
PDWORD p = NULL;
printf("变量dwNumber的线性地址为:0x%P,请在windbg中给NULL挂物理页:\n",&dwNumber);
getchar();//修改NULL的PTE,给NULL挂物理页

DWORD dwNull = *p;
printf("线性地址NULL中的值为:%x\n",dwNull);
getchar();
return 0;
}
  1. 运行程序获取变量dwNumber的线性地址:0x0012FF7C。

  2. 获取CR3的值!process 0 0:0x06e401c0。

  3. 按2-9-9-12拆分线性地址NULL(0)、0x0012FF7C。

    1. NULL:!vtop 0x06e401c0 0x0

      1
      2
      3
      4
      5
      X86VtoP: Virt 00000000, pagedir 6e401c0
      X86VtoP: PAE PDPE 6e401c0 - 00000000`223d9801
      X86VtoP: PAE PDE 223d9000 - 00000000`224c7867
      X86VtoP: PAE PTE 224c7000 - 00000000`00000000
      X86VtoP: PAE zero PTE
    2. 0x0012FF7C:!vtop 0x06e401c0 0x0012FF7C

      1
      2
      3
      4
      5
      X86VtoP: Virt 0012ff7c, pagedir 6e401c0
      X86VtoP: PAE PDPE 6e401c0 - 00000000`223d9801
      X86VtoP: PAE PDE 223d9000 - 00000000`224c7867
      X86VtoP: PAE PTE 224c7978 - 80000000`2235c867
      X86VtoP: PAE Mapped phys 2235cf7c
  4. 可以看到NULL的PDE不为空,则直接修改PTE即可:由于!eq指令没法使用,则!ed 0x224c7000 0x2235c867 !ed 0x224c7004 0x80000000

  5. 此时再查看:

    1
    2
    3
    4
    5
    6
    kd> !vtop 0x06e401c0 0x0
    X86VtoP: Virt 00000000, pagedir 6e401c0
    X86VtoP: PAE PDPE 6e401c0 - 00000000223d9801
    X86VtoP: PAE PDE 223d9000 - 00000000224c7867
    X86VtoP: PAE PTE 224c7000 - 800000002235c867
    X86VtoP: PAE Mapped phys 2235c000
  6. 回到XP的VC6中继续执行代码。

    34.png35.png

1.a 练习:修改页属性读写高2G地址

除了G位和XD位属性为OR外,其余属性为: PDPTE属性 & PDE属性 & PTE属性

要想在3环读写高2G地址,除了段中学到的提权外,还可以使用页的手段:改U/S位,和PTE的G位

G位和TLB及PS位有关(PDE、PTE均包含G、PS位):

  1. PDE的PS = 0(小页4KB),G位在PDE中无效。
  2. 由于G位的属性是:PDE_G | PTE_G,当PTE的G = 1时,表示该物理页有缓存(使用了快表TLB),故要想使U/S修改后生效,要保证PTE中的G = 0

题目:在3环写代码读写线性地址:0x8003f048。

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

int _tmain(int argc, _TCHAR* argv[])
{
printf("请在windbg中修改线性地址0x8003f048 PDE、PTE的U/S位及PTE的G位:\n");
getchar();//修改NULL的PTE,给NULL挂物理页

PDWORD p = (PDWORD)0x8003f048;
printf("线性地址0x8003f048中的值为:%x\n",*p);//读
*p = 0x12345678;//写
printf("线性地址0x8003f048中的值为:%x\n",*p);//读
getchar();
return 0;
}
  1. 运行程序,并使用!process 0 0获取CR3的值:0x06e401c0。

  2. 拆分线性地址0x8003f048:!vtop 0x06e401c0 0x8003f048

    1
    2
    3
    4
    5
    X86VtoP: Virt 8003f048, pagedir 6e401c0
    X86VtoP: PAE PDPE 6e401d0 - 00000000`2556f801
    X86VtoP: PAE PDE 2556f000 - 00000000`00b17163
    X86VtoP: PAE PTE b171f8 - 00000000`0003f163
    X86VtoP: PAE Mapped phys 3f048
  3. 可以看到PDE、PTE下标2(低3位)的U/S为0(特权用户),需要修改为1。PTE下标8(低9位)G位为1,需要修改为0。

    • 修改PDE:!ed 0x2556f000 0x00b17167
    • 修改PTE:!ed 0x00b171f8 0x0003f067
  4. 回到XP的VC6继续执行代码。

    36.png

1.b 逆向分析函数MmIsAddressValid

内核函数MmIsAddressValid的作用:验证一个线性地址是否有效。

在MSDN是这样说的:The MmIsAddressValid routine checks whether a page fault will occur for a read or write operation at a given virtual address.(MmIsAddressValid例程检查给定虚拟地址的读写操作是否会发生页面错误。)

找函数方法:

方法一:在windbg中输入 u MmIsAddressValid L41uf MmIsAddressValid

方法二:在C:\Windows\System32\ 中找到内核程序,用IDA分析。

  • 2-9-9-12分页内核:ntkrnlpa.exe
  • 10-10-12分页内核:ntoskrnl.exe

函数分析:

  1. mov edi,edi :正式分析前,先聊聊函数头的 mov edi,edi 指令。这条指令看起来什么也没做,但是很多系统函数开头都有这条指令,why?其实这是为了实现对函数行为的动态修改(热补丁,Hot-Patching),且执行一条MOV指令比执行两条NOP指令花费更少的时间。可以一下这篇文章 函数开始处的MOV EDI, EDI的作用
  2. align 8:数据8字节对齐。
  3. bp-based frame:使用 EBP 寻址。
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
.text:0043C921 ; ---------------------------------------------------------------------------
.text:0043C922 align 8 //数据8字节对齐
.text:0043C928 ; Exported entry 685. MmIsAddressValid
.text:0043C928
.text:0043C928 ; =============== S U B R O U T I N E =======================================
.text:0043C928
.text:0043C928 ; Attributes: bp-based frame //使用 EBP 寻址
.text:0043C928
.text:0043C928 ; BOOLEAN __stdcall MmIsAddressValid(PVOID VirtualAddress)
.text:0043C928 public MmIsAddressValid
.text:0043C928 MmIsAddressValid proc near ; CODE XREF: sub_41AD5A+2F↑p
.text:0043C928 ; sub_41ADAC+29↑p ...
.text:0043C928
.text:0043C928 var_8 = dword ptr -8 //局部变量2
.text:0043C928 var_4 = dword ptr -4 //局部变量1
.text:0043C928 VirtualAddress = dword ptr 8 //参数1,EBP+8
.text:0043C928
.text:0043C928 mov edi, edi //Hot-Patching
.text:0043C92A push ebp
.text:0043C92B mov ebp, esp
.text:0043C92D push ecx //保存 ECX 的值,同时为局部变量1留坑位
.text:0043C92E push ecx //保存 ECX 的值,同时为局部变量2留坑位
.text:0043C92F mov ecx, [ebp+VirtualAddress]//取参数VirtualAddress,线性地址,ecx = VAddr
.text:0043C932 push esi
.text:0043C933 mov eax, ecx //eax = VAddr
.text:0043C935 shr eax, 12h //VAddr>>18
.text:0043C938 mov esi, 3FF8h //0011 1111 1111 1000,2--0011左移12位,9--111111111左移3位
.text:0043C93D and eax, esi //eax = PDPTI * 4KB + PDI * 8
.text:0043C93F sub eax, 3FA00000h //eax = C0600000 + PDPTI * 4KB + PDI * 8
.text:0043C93F //eax指向PDE
.text:0043C944 mov edx, [eax] //edx = PDE低4字节
.text:0043C946 mov eax, [eax+4] //eax = PDE高4字节
.text:0043C949 mov [ebp+var_4], eax //局部变量1为PDE高4字节
.text:0043C94C mov eax, edx //eax = edx = PDE低4字节
.text:0043C94E push edi
.text:0043C94F and eax, 1 //取PDE的P位
.text:0043C952 xor edi, edi
.text:0043C954 or eax, edi //判断P = 0?
.text:0043C956 jz short loc_43C9B9 //if (P==0) 返回假,返回0
.text:0043C958 mov edi, 80h
.text:0043C95D and edx, edi //取PS位[7],判断大小页
.text:0043C95F push 0
.text:0043C961 mov [ebp+var_8], edx
.text:0043C964 pop eax //eax = 0
.text:0043C965 jz short loc_43C96B //and edx,edi与运算后如果结果为0,PS = 0,转到小页处理
.text:0043C967 test eax, eax
.text:0043C969 jz short loc_43C9BD //判定线性地址在大页,为有效地址,直接返回真,返回1
.text:0043C96B
.text:0043C96B loc_43C96B: ; CODE XREF: MmIsAddressValid+3D↑j
.text:0043C96B shr ecx, 9 //ecx = VAddr,VAddr>>9
//0111 1111 1111 1111 1111 1000--011`1 1111 1111`1111 1111 1`000
.text:0043C96E and ecx, 7FFFF8h //ecx = PDPTI * 2MB + PDI * 4KB + PTI * 8
.text:0043C974 mov eax, [ecx-3FFFFFFCh]//eax = PTE高4字节,C0000004
.text:0043C97A sub ecx, 40000000h //ecx = C0000000 + PDPTI * 2MB + PDI * 4KB + PTI * 8
//ecx指向PTE
.text:0043C980 mov edx, [ecx] //edx = PTE低4字节
.text:0043C982 mov [ebp+var_4], eax
.text:0043C985 push ebx
.text:0043C986 mov eax, edx //eax = edx = PTE低4字节
.text:0043C988 xor ebx, ebx
.text:0043C98A and eax, 1 //取PTE的P位
.text:0043C98D or eax, ebx //判断P = 0?
.text:0043C98F pop ebx
.text:0043C990 jz short loc_43C9B9 //if( p==0),返回假,返回0
.text:0043C992 and edx, edi //判断PTE的PAT = 0?
.text:0043C994 push 0
.text:0043C996 mov [ebp+var_8], edx
.text:0043C999 pop eax
.text:0043C99A jz short loc_43C9BD //PTE的PAT = 0,返回真,返回1,即有效地址
.text:0043C99C test eax, eax
.text:0043C99E jnz short loc_43C9BD //PAT = 1,以后再分析PAT的意义
.text:0043C9A0 and ecx, esi
.text:0043C9A2 mov ecx, [ecx-3FA00000h]
.text:0043C9A8 mov eax, 81h
.text:0043C9AD and ecx, eax
.text:0043C9AF xor edx, edx
.text:0043C9B1 cmp ecx, eax
.text:0043C9B3 jnz short loc_43C9BD
.text:0043C9B5 test edx, edx
.text:0043C9B7 jnz short loc_43C9BD
.text:0043C9B9
.text:0043C9B9 loc_43C9B9: ; CODE XREF: MmIsAddressValid+2E↑j
.text:0043C9B9 ; MmIsAddressValid+68↑j
.text:0043C9B9 xor al, al //eax为返回值
.text:0043C9BB jmp short loc_43C9BF
.text:0043C9BD ; ---------------------------------------------------------------------------
.text:0043C9BD
.text:0043C9BD loc_43C9BD: ; CODE XREF: MmIsAddressValid+41↑j
.text:0043C9BD ; MmIsAddressValid+72↑j ...
.text:0043C9BD mov al, 1
.text:0043C9BF
.text:0043C9BF loc_43C9BF: ; CODE XREF: MmIsAddressValid+93↑j
.text:0043C9BF pop edi
.text:0043C9C0 pop esi
.text:0043C9C1 leave
.text:0043C9C2 retn 4
.text:0043C9C2 MmIsAddressValid endp
.text:0043C9C2
.text:0043C9C2 ; ---------------------------------------------------------------------------

分析结果:

2-9-9-12:
PDPTI-PDI-PTI-OFFSET

公式:
PDE = 0xc0600000 + (PDPTI*4KB) + (PDI*8)
PTE = 0xc0000000 + (PDPTI*2MB) + (PDI*4KB) + (PTI*8)

更高效的公式(MmIsAddressValid是这么干的)
PDE = 0xc0600000 + ((addr >> 18) & 0x3ff8)
PTE = 0xc0000000 + ((addr >> 9) & 0x7ffff8)

1.c 练习:代码实现读写高2G地址

题目:在3环写代码读写线性地址:0x8003f048。

方法:通过构造调用门在0环修改线性地址0x8003f048对应PDE_U/S = 1、PTE_U/S = 1PTE_G = 0

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
#include "stdafx.h"
#include <tchar.h>
#include <windows.h>

VOID __declspec(naked) CallGate_RW8003f048()
{
__asm
{
pushad;
pushfd;
//先找PDE
//int 3;
mov eax,0x8003f048;
push eax;
shr eax,18; //eax = dwAddr>>18
and eax,0x3ff8; //eax = (dwAddr>>18) & 0x3ff8 = PDPTI * 4KB + PDI * 8
add eax,0xc0600000; //eax = C0600000 + PDPTI * 4KB + PDI * 8
//eax 指向PDE
mov esi,eax;
mov eax,[eax]; //eax = PDE低4字节
mov ebx,eax; //ebx = eax = PDE低4字节
//再找PTE
pop eax;
shr eax,9; //eax =dwAddr>>9
and eax,0x7ffff8; //eax = (dwAddr >> 9) & 0x7ffff8 = PDPTI * 2MB + PDI * 4KB + PTI * 8
add eax,0xc0000000; //eax = 0xc0000000 + PDPTI * 2MB + PDI * 4KB + PTI * 8
//eax指向PTE
mov edi,eax;
mov eax,[eax]; //eax = PTE低4字节
mov ecx,eax; //ecx = eax = PTE低4字节

//判断PDE的P位和PS位(省略),PTE P、PAT位(省略)
//修改PDE、PTE的U/S[2] = 1,修改PTE的G[8] = 0。(如果PDE_PS = 0(小页),PDE_G位无效)
or ebx,0x4; //PDE_U/S = 1
or ecx,0x4; //PTE_U/S = 1
and ecx,0xFFFFFEFF; //PTE_G = 0
mov [esi],ebx; //将修改后的PDE低4字节写回
mov [esi+4],0x0; //将修改后的PDE高4字节写回
INVLPG dword ptr ds:[0x8003f048]; //清空线性地址0x8003f048在TLB的缓存
mov [edi],ecx; //将修改后的PTE低4字节写回
mov [edi+4],0x0; //将修改后的PTE高4字节写回

popfd;
popad;
retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
BYTE CallGate[] = {0,0,0,0,0x48,0};
PDWORD p = (PDWORD)0x8003f048;
printf("请在windbg执行: eq 8003f048 %04xec00`0008%04x\n", ((DWORD)CallGate_RW8003f048>>16) & 0x0000FFFF,\
(DWORD)CallGate_RW8003f048 & 0xFFFF);

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

printf("线性地址0x8003f048中的值为:%x\n",*p);//读
*p = 0x12345678;//写
printf("线性地址0x8003f048中的值为:%x\n",*p);//读
getchar();
return 0;
}

39.png

注意:代码中如果不使用INVLPG dword ptr ds:[0x8003f048];清空线性地址0x8003f048在TLB缓存的话,eq 8003f048 0040ec00·00081005修改后到VC6中第一次运行时报错不能读写高2G内存(可不要退出程序再按F5继续执行即可成功),如果使用INVLPG清空0x8003f048在TLB缓存,则可直接运行成功。