挂物理页的一些细节

线性地址有效性判断

之前我们分析过 MmIsValidAddress 函数,在 10-10-12 分页模式下就是去拿到页表的线性地址,然后判断 PTE 和 PDE 的P位是否都有效。一般来说,如果都有效说明进程在这个线性地址这里挂上了物理页。

零地址挂页

考虑以下代码:

1
2
int *x=NULL;
*x=100;

通常情况下我们会认为这两条语句执行之后必然出错,这就是所谓的空指针错误,但是空指针真的不能写值吗?未必,只是通常情况下不会在上面挂物理页,而对一个线性地址的内存进行读或写操作都会校验这个线性地址是否有效,如果无效则会抛出异常 crash 程序。

以 10-10-12 分页为例,写出以下的程序:

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>


int main(int argc,char * argv[])
{
PVOID addr=VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_READWRITE);
memset(addr,0,0x1000);
printf("addr=%08x\n",addr);
system("pause");
int *x=NULL;
*x=0x12345678;
printf("%x\n",*x);
system("pause");
return 0;
}

等待第一次分配输出地址

计算 PDI,PTI 的时候不要偷懒

1
2
3
4
5
6
7
00000000000101100000000000000000
0000000000
0
0101100000
0x160
000000000000
0

注意上面的代码中加入了 memset,这是因为线性地址只有第一次读/写的时候会挂物理页,如果只申请没有读写则不会挂物理页。

我们尝试把这个页挂在零地址上,这里查看一下 PDI 为 0 的物理地址,如上图所示的值为 0x5a1eb000,这个物理地址必然没有被挂上物理页,我们把刚刚那个页的PTE写到线性地址 0 上。

使用 windbg 命令 !ed 0x5a1eb000 2b13a847

现在 0 地址挂上了物理页,我们再看看能否继续运行。

发现没有问题,完美运行通过,而如果此时去读刚刚 addr 分配的线性地址就会发现,值同样也是 0x12345678,而对应的物理页肯定也是这个值,这里对代码稍作修改再次输出。

这点也论证了同一个物理页是可以被挂上不同的线性地址,这里展示的是同一个进程不同的线性地址。而不同进程的线性地址同样也能挂同一个物理页,也就是所谓的共享内存(一个进程修改,另一个进程会得到修改的结果)。

零地址写shellcode

Windows 的 shellcode,通常情况下,shellcode 藏在 0 线性地址是比较有效的,因为大部分扫描器通常不会去动 0 这一片的地址,而挂页的时候,是否可执行是通过 PTE 决定的。也就是说,同样的一个物理页,你可以以只读形式挂在 0x400000 这样的一个线性地址,同时还可以以读写的形式挂在 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
#include "stdafx.h"
#include <Windows.h>
#include <stdlib.h>

typedef void (*funcptr)();

int main(int argc,char * argv[])
{
PVOID addr=VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_READWRITE);

printf("addr=%08x\n",addr);
BYTE shellcode[]={
0x6A,0x00, //push 0
0x6A,0x00, //push 0
0x6A,0x00, //push 0
0x6A,0x00, //push 0
0xB8,0x00,0x00,0x00,0x00, //mov eax,xxx
0xFF,0xD0, //call eax
0xC3, //ret
};
*(DWORD *)(&shellcode[9])=(DWORD)MessageBox;
memcpy(addr,shellcode,sizeof(shellcode));
system("pause");
funcptr func=0;
func();
system("pause");
return 0;
}

同时这个shellcdoe也是比较简单的,仅仅是弹框而已。

这里因为用的是 10-10-12 分页,所以说 PTE 没有哪个位表示页是否可以执行,也就是说所有的页都可以执行,在 2-9-9-12 分页我们只需要把最高位设置为 0 即可。

参考文献