来具体学习一下页属性

看前必读

本文所述的第 x 位均表示下标从 0 开始的计数制。

1
2
0000100010001
*

例如上面星号所指示的位置表示第 1 位。

有效属性

可以关注内核函数 MmIsAddressValid 实现原理,取出虚拟机 C:\Windows\System32\ntoskrnl.exe 内核文件,找到该函数,F5可得以下逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool __fastcall sub_48DCB8(unsigned int a1)
{
int v1; // eax
int v3; // eax

v1 = *(_DWORD *)(((a1 >> 20) & 0xFFC) - 0x3FD00000);
if ( (v1 & 1) == 0 )
return 0;
if ( (v1 & 0x80u) != 0 )
return 1;
v3 = *(_DWORD *)(((a1 >> 10) & 0x3FFFFC) - 0x40000000);
if ( (v3 & 1) == 0 )
return 0;
return (v3 & 0x80) != 0x80;
}

看到第一个表达式 (a1 >> 20) & 0xFFC),可以认为是将地址右移了 22 位(取高10位),再左移了 2 位(*4),然后将该值减去 0x3FD00000,其实转换成加法就是 +0xC0300000,也就是我们的页目录表的线性地址。

如果该地址的最低位为 0,则返回 0(p位为0,无效)。

如果该地址的第7位为 1,说明是个大页,那么直接返回 1,整个页都是有效的,否则进行后续判断。

这里就是判断 PDE 的最低为是否为 0,若为 0 则还是无效。

否则返回第7位是否为 1 (这里存疑,不明白为什么PAT位为1才表示有效)。

总体,该函数的实现就是通过两个关键的线性地址 0xC00000000xC0300000,检查页表的属性来实现的功能。

读写属性

我是这么认为的:一个虚拟页,只要挂上了对应的物理页且有效,它必定可读,是否可写根据第 1 位标志位确定。

举个例子

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

const char *s="123456789";
int main(int argc,char * argv[])
{
printf("%x\n",s);
__asm{
mov eax,ds:dword ptr[s]
int 3
mov ds:byte ptr[eax],0x66
}
puts(s);
system("pause");
return 0;
}

尝试向字符串常量指向的地址进行写入则必定会出一个写入错误,那么在中间下个断点,然后修改挂的物理页。

拆分得到 PDI,PTI分别为 1,3,页内偏移为 0x12C

可以发现 PTE 的第一位为 0,即该页不可写。

同时也可以验证一下该物理页的正确性

将该物理页设为可写,使用命令 !ed a6fab00C 0b8e2027

可以发现这个字符修改成功了

特权位

当我们处于三环的权限,我们无法访问 U/S 位为 0 的页,通常,32位地址下,我们无法访问 80 开头的地址,因为只有 0 环态下可以访问。

例如,我修改 gdt 所属的页,让gdt变得三环状态可读,参考以下步骤:

  • 通过 gdtr 找到 gdt 所在的页。
  • 拆分地址找到对应的物理页对应的 PTE。
  • 将物理页的 U/S 位改为 1。

这里我的 gdtr 为 80b93800

1
2
3
4
5
6
PDI 1000000010
0x202
PTI 1110010011
0x393
off 100000000000
0x800

按照同样的方法,找到了 gdt 的物理页。

图中反的主要原因时物理页输出是 dd,虚拟页输出是 dq,会小端序的反转一下。

我们看到物理页的 PTE,结果是 00b93163,很明显第二位,也就是 U/S 位为 0,表明是 0 环才可以访问的,那么将它改为三环可访问,使用命令 !ed 0018a000+0x393*4 00b93167

结果发现好像还是不可读,是为什么呢?

对啦,原来PDE对应的U/S位也要改,因为它们是与的关系,所以还需要再增加一个命令 !ed 0000000000185000+0x202*4 0018a067,做完这些,再验证一下三环程序能否读到 gdt 表。

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

DWORD val=0;
int main(int argc,char * argv[])
{
val=0;
__asm{
mov eax,ds:dword ptr[0x80b93808]
mov ds:dword ptr[val],eax
}
printf("%x\n",val);
system("pause");
return 0;
}

这里需要注意的是,PTE基本最后映射的都是同一个,但是PDE不一样,因此需要这么操作才能实验成功:

  • 运行程序,中断。
  • 找到该程序 CR3 的值,得到 PDE,将PDE的U/S位改为1(每次重新运行必须做这件事)
  • 根据PDE找到对应的PTE,将PTE的U/S位改为 1(只需要改一次就可以)

可以看到,程序能够成功读取gdt表的四个字节。

访问位

这个属性不太好做实验验证,只需要知道:访问(读或写)过了则为 1,否则为0。

脏位

写过了则为 1,否则为0,且只有PTE具有这个属性。

其余位

其余位都跟缓存相关,实验自己不太会设计了,咕咕吧,等后面有能力了再来验证。

参考文献