提权读写空指针

"利用中断门提权并挂载物理页"

Posted by Chris on June 17, 2018

最近在学习x86的保护模式,学到了调用门,陷阱门,任务门,中断门,及分页相关知识点,这里将所学知识融汇一下,演示一下如何利用中断门提权为空指针挂载物理页。

  • 实验环境: WinDBG + win7 x86 双机调试
  • VC6.0编译环境

0x00 知识准备

1,中断门(interruptgate)

包含段选择符和中断或异常处理程序的段内偏移量.当控制权转移到一个适当的段时,处理器 清IF标志,从而关闭将来会发生的可屏蔽中断.

中断门描述符结构 pic1

  • offset:高低16位拼在一起是跳转地址.
  • Attributes属性位 : / P位 P=1:该描述符有效 P=0:该描述符无效 / DPL位:段权限 / S位: 该位为 1 表示这是一个数据段或者代码段。为 0 表示这是一个系统段(比如调用门,中断门等) / TYPE域: 根据 S 位的结果,再次对段类型进行细分。
  • Segment Selector:段选择子,根据它查GDT表中的系统段描述符,确定跳转段的基址,属性,权限等信息。

通常中断门是以 —-ee00 0008—-这种形式出现的,当然了,第 5 个十六进制数不一定就是 e,如果 DPL = 0 的话,那这个数就是 8. 后面的 0008 是段选择子,而左右两侧的占位符是要跳转的地址。

2,分页(Page)

先说说最基本的未开启PAE时的10-10-12分页 (PDE为页表地址,PTE为物理页地址)

CR3存有页目录表的物理地址,页目录表有1024项,存有页表的物理地址,每张页表有1024项,存有物理页的地址。由于系统的内存通常都是按照4kb对齐,所以页目录表和页表中的地址的低12位都是保留位,另做他用。

但其实上面这种结构方式是错误的,正确的的结构方式是这样的:

其实页目录表(PDT)本不存在,是抽象出来的.

  • 页表被映射到了从0xC0000000到0xC03FFFFF的4M地址空间
  • 在这1024个表中有一张特殊的表:页目录表
  • 页目录被映射到了0xC0300000开始处的4K地址空间

所以我们就能根据线性地址计算所在物理页地址

访问页目录表的公式: 0xC0300000 + PDI*4

访问页表的公式: 0xC0000000 + PDI*4096 + PTI*4

(PDI为页目录表偏移是线性地址高10位,PTI为页表偏移是线性地址的高12-21位)

但本次实验环境 win7 x86 默认分页方式为 2-9-9-12分页,四页式。

1
2
3
4
5
根据 CR3 找到 Page Directory Pointer Table
根据一级索引在 Page Directory Pointer Table 中查询到 Page Directory
根据二级索引在 Page Directory 中查询到 Page Table
根据三级索引在 Page Table 中查询到普通 4KB 物理页
在物理页中查找第四段偏移。

如下图: pic2

根据以上描述,第一段索引其实就是 Page Directory Pointer Table(PDPT) 这张表的索引。 下面的彩图也许能帮助你理解。

pic3

这里就直接给出该分页模式下 PDE 和 PTE 地址计算公式

addr 存放的是线性地址

1
2
pPDE = (int*)(0xc0600000 + ((addr >> 18) & 0x3ff8));
pPTE = (int*)(0xc0000000 + ((addr >> 9) & 0x7ffff8));

0x01 原理简述

用windbg在系统IDT(中断描述符表)中安装一个中断门,程序通过int指令调用。通过调用,跳转到我们自己写的函数,通过公式:0xc0000000 + ((addr » 9) & 0x7ffff8),计算出PTE地址,为0线性地址的PTE地址赋值为已存在物理页的PTE地址,这样就为0地址挂载物理页,可访问。

0x02 实验步骤

先贴出完整代码。

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
#include <windows.h>
#include<stdio.h>

__declspec(naked) 
void MyPage() {
    __asm {
        push ebp
			mov ebp, esp
			sub esp, 0x100
			push ebx
			push esi
			push edi
    }
	
    DWORD* pPTE;    
    DWORD* pNullPTE; 
	
    pNullPTE = (DWORD*)0xc0000000; // #0地址的PTE线性地址
    
    pPTE = (DWORD*)(0xc0000000 + ((0x60000000 >> 9) & 0x7ffff8));   // #目标线性地址的PTE线性地址
	
    *pNullPTE = *pPTE;  // #为0地址的PTE挂上目标线性地址的PTE(物理页)
	
    __asm {
        pop edi
			pop esi
			pop ebx
			mov esp, ebp
			pop ebp
			iretd   // #注意返回
    }
}


__declspec(naked)
void Page() {
    __asm {
        int 0x20   
			ret
    }
}


int main(int argc, char* argv[])
{
	char *str="hello world";
    DWORD* addr = (DWORD*)VirtualAlloc((LPVOID)0x60000000, 4, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);  
    *addr = (DWORD)str;
	
	 #/*为0x60000000线性地址开辟堆内存,里面存有str的地址*/
	
    if (addr ==NULL) {
        printf("Error alloc!\n");
        return -1;
    }
	
    Page();
    printf("读 0 地址数据:\n");
    printf("*NULL = %s\n", *(DWORD*)0);
	getchar();
    return 0;
}

1,在 main 函数起始位置下断点,在 VC6.0 中观察到函数 MyPage的函数地址,在我的环境里是 0x00401030。

2,构造中断门描述符 0040ee00`00081030 (构造原理这里就不细说了)

3,中断到 WinDbg 中去,执行以下命令安装中断门(实际偏移以IDT表位置为准)

pic4

4,在 WinDbg 中执行命令g 回到 win7 系统中。

5,继续执行 VC6.0 中的程序。

运行结果

  • 如果没有提权操作,也就是没有执行MyPage函数的话 运行结果会是如下:

pic6

  • 如果有提权操作,也就是执行了MyPage函数的话,运行结果如下:

pic5

0x03 后记

这个实验看起来虽然很无聊,但是覆盖的操作系统保护模式的知识面却很宽广,有兴趣的小伙伴可以亲自动手试试哦。