x64保护模式

1.分段机制

1.1 强制平坦化

​ 在x64中,gdt表中普通段描述符中不再描述基址Base和界限Limit,基址永远是0界限永远是最大的,只有属性还在使用。

fsgs没有强制平坦,而是在msr寄存器中记录了基址:

名称 地址 内容 用途
IA32_FS_BASE MSR[0xC0000100] fs基址 x86 teb
IA32_GS_BASE MSR[0xC0000101] gs基址 x64 teb/kpcr
IA32_KERNEL_GS_BASE MSR[0xC0000102] kpcr/teb基址 x64暂存kpcr或者teb

swapgs特权指令:交换 IA32_GS_BASE 和 IA32_KERNEL_GS_BASE 的值,使gs在系统调用后指向KPCR

1.2 描述符宽度

  • cs、ds段描述符仍是64位

  • TSS段描述符扩展到128位

  • 中断门描述符扩展到128位

1.3 数据/代码段描述符

数据/代码段描述符

L:置1表示指令按照64位解读。

G:粒度,置1单位4kb,0单位1byte

D:置1表示32位操作数,0表示16位操作数

P:1表示有效,0表示无效

DPL:描述符权限(0-3)

S:1表示数据/代码段,0表示系统段,系统段就是TSS之类的

Type:属性,具体属性如下:

段属性

1.4 手动拆分gdt表

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
kd> dq fffff8044b890fb0
fffff804`4b890fb0 00000000`00000000 00000000`00000000
fffff804`4b890fc0 00209b00`00000000 00409300`00000000
fffff804`4b890fd0 00cffb00`0000ffff 00cff300`0000ffff
fffff804`4b890fe0 0020fb00`00000000 00000000`00000000
fffff804`4b890ff0 4b008b88`f0000067 00000000`fffff804
fffff804`4b891000 0040f300`00003c00 00000000`00000000
fffff804`4b891010 00000000`00000000 00000000`00000000
fffff804`4b891020 00000000`00000000 00000000`00000000

============================================================================================

kd> dg 10
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0010 00000000`00000000 00000000`00000000 Code RE Ac 0 Nb By P Lo 0000029b r0 代码段
kd> dg 18
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0018 00000000`00000000 00000000`00000000 Data RW Ac 0 Bg By P Nl 00000493 r0 数据段
kd> dg 20
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0020 00000000`00000000 00000000`ffffffff Code RE Ac 3 Bg Pg P Nl 00000cfb r3 代码段
kd> dg 28
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0028 00000000`00000000 00000000`ffffffff Data RW Ac 3 Bg Pg P Nl 00000cf3 r3 数据段
kd> dg 30
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0030 00000000`00000000 00000000`00000000 Code RE Ac 3 Nb By P Lo 000002fb r3 代码段
kd> dg 40
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0040 00000000`4b88f000 00000000`00000067 TSS32 Busy 0 Nb By P Nl 0000008b TSS32段
kd> dg 48
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0048 00000000`0000ffff 00000000`0000f804 <Reserved> 0 Nb By Np Nl 00000000 TSS的一部分基址
kd> dg 50
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0050 00000000`00000000 00000000`00003c00 Data RW Ac 3 Bg By P Nl 000004f3 r3 数据段

手动拆分r0代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
00209b00`00000000

2 -> 0010
G:0 byte
D:0 16位操作数
L:1 x64
AVL:0

9 -> 1001
P:1 有效
DPL:0 r0

b -> 1011
Type: 1011 代码段

1.5 TSS段

TSS段描述符长度为128位,结构如下:

TSS段描述符

TSS段需要描述符中的基址,定位方式如下:

TSS内存定位

TSS段不用来任务切换,主要保存一些rsp备用指针(用于快速切换r0,r1,r2的堆栈),内存结构如下:

TSS内存结构

1.6 中断门

1.6.1 描述符

中断门描述符长度为128位,结构如下:

中断门描述符

IST位:进0环时,会根据这个值去找TSS中的对应的rsp,为0表示默认使用rsp0。

Windbg指令:

  • !idt:输出当前系统全部中断信息
  • !idt 3:指定查看某个中断信息

1.6.2 中断现场

int中断发生时,会做出一下动作:

​ TSS.rsp0 -> rsp

​ 中断门描述符 -> rip

​ 中断门描述符 -> cs

​ cs + 16 -> ss

同时将r3的ss、rsp、rflags、cs、rip压栈,此时的rsp处在IDT表的末尾

1.6.3 int3入口分析

int3入口

上述代码主要完成以下的是:

  • 完成0环cr3的切换,KPTI机制
  • 完成0换堆栈的切换,并将旧堆栈数据拷贝过去,使此时的栈如int指令发生时一致
  • 跳转到中断处理例程

1.6.4 代码框架

Win10 1904中断门提权代码:

Entry.cpp

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

// Debug x64
// 关闭随机基址
// 关闭增量链接

// !idt 0x21 (获取idt首地址)
// eq fffff800`2fa77210 4000ee00`00101000 00000000`00000001

// 清空cr4的SMEP和SMAP位

// 修改进程pte属性--可以执行r3代码
// !pte 140001000

UINT64 read_val = 0;

extern "C" void IDTEntry(UINT64* read);
extern "C" void GoTrap();

extern "C" ULONG64 g_rsp;
ULONG64 g_rsp;
extern "C" ULONG64 g_cs;
ULONG64 g_cs;
extern "C" WORD g_ss;
WORD g_ss;

int main()
{
if ((UINT64)IDTEntry != 0x0140001000)
{
printf("Wrong fun addr. %p\n", (UINT64)IDTEntry);
system("pause");
return -1;
}

system("pause");

GoTrap();

printf("g_rsp: %x\r\n", g_rsp);
// printf("g_cs: %x\r\n", g_cs);
// printf("g_ss: %x\r\n", g_ss);

system("pause");
return 0;
}

fun.asm

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
option casemap:none

EXTERN g_rsp: qword
EXTERN g_cs: qword
EXTERN g_ss: word

.data

.code

IDTEntry proc

stac
swapgs
lfence
bt dword ptr gs:[9018h], 1
jb change_cr3

mov rsp, gs:[9000h]
mov cr3, rsp

change_cr3:

mov rsp, gs:[9008h]
mov gs:[10h], rsi
mov rsi, gs:[38h]
add rsi, 4200h
push qword ptr [rsi-8]
push qword ptr [rsi-10h]
push qword ptr [rsi-18h]
push qword ptr [rsi-20h]
push qword ptr [rsi-28h]
mov rsi, gs:[10h]
and qword ptr gs:[10h], 0

mov rax, rsp
mov g_rsp, rax

mov rax, gs:[188h]
mov rax, [rax+220h]
mov rax, [rax+388h]
bt eax, 0
jnb recover_cr3
bts rax, 03fh
recover_cr3:

mov cr3, rax

swapgs
iretq

IDTEntry endp

GoTrap proc
int 21h
ret
GoTrap endp

END

1.7 SMEP和SMAP

控制寄存器结构如图所示:

控制寄存器结构

x64下,Cr4寄存器的20和21bit分别代表SMEP和SMAP。

  • SMEP:不允许内核权限执行用户代码
  • SMAP:不允许内核权限读写用户内存

stac指令用来关闭SMAP检测,本质修改rflags寄存器的AC位,详情见intel手册。

2.分页机制

2.1 简介

​ IA-32e模式下,虚拟地址宽度为64位,但只有低48位有效,最多可以寻址256TB,高16位用作符号拓展(全0或全1),物理页面仍为4KB。CPU 分页机制变为4级,分别对应 PML4、PDPT、PD、PT表,Cr3 寄存器中的物理地址指向 PML4 表的首地址,每个表项8字节,并将48位虚拟地址按 9-9-9-9-12 索引格式划分。如下图所示:

分页简介

2.2 手动拆分

Windbg 中手动拆分64位虚拟地址,并按照上面的分页规则计算出物理地址。实验选用 idt 表首地址进行拆分。(在计算物理地址时,需要对页表项的低12位属性位清0。)

1
2
3
4
5
6
7
8
9
10
11
kd> r idtr
idtr=fffff8037888e000
kd> dq fffff8037888e000
fffff803`7888e000 761e8e00`00107e00 00000000`fffff803
fffff803`7888e010 761e8e04`00108140 00000000`fffff803
fffff803`7888e020 761e8e03`00108600 00000000`fffff803
fffff803`7888e030 761eee00`00108ac0 00000000`fffff803
fffff803`7888e040 761eee00`00108e00 00000000`fffff803
fffff803`7888e050 761e8e00`00109140 00000000`fffff803
fffff803`7888e060 761e8e00`00109680 00000000`fffff803
fffff803`7888e070 761e8e00`00109b80 00000000`fffff803

将虚拟地址按照 9-9-9-9-12 格式划分(注意低48位有效)

1
2
3
4
5
6
7
fffff803`7888e000 -> f803`7888e000

1 1111 0000 0x1f0 PML4I
0 0000 1101 0xd PDPTI
1 1100 0100 0x1c4 PTI
0 1000 1110 0x8e PDI
000000000000 0x0 Offset

访问 Cr3 + PML4I * 8 指向的物理地址得到 PDPTE 的物理地址

1
2
3
4
5
6
7
8
9
10
11
kd> r cr3
cr3=0000000052c76000
kd> !dq 52c76000+1f0*8
#52c76f80 00000000`00c08063 00000000`00000000
#52c76f90 00000000`00000000 00000000`00000000
#52c76fa0 00000000`00000000 00000000`00000000
#52c76fb0 0a000000`0bafc863 00000000`00000000
#52c76fc0 00000000`00000000 00000000`00000000
#52c76fd0 00000000`00000000 00000000`00000000
#52c76fe0 00000000`00000000 00000000`00000000
#52c76ff0 00000000`00000000 00000000`00ca8063

访问 PDPTE + PDPTI * 8 指向的物理地址得到 PTE 的物理地址

1
2
3
4
5
6
7
8
9
kd> !dq c08000+d*8
# c08068 00000000`00c09063 00000000`00000000
# c08078 00000000`00000000 00000000`00000000
# c08088 00000000`00000000 00000000`00000000
# c08098 00000000`00000000 00000000`00000000
# c080a8 00000000`00000000 00000000`00000000
# c080b8 00000000`00000000 00000000`00000000
# c080c8 00000000`00000000 00000000`00000000
# c080d8 00000000`00000000 00000000`00000000

访问 PTE + PTI * 8 指向的物理地址得到 PDE 的物理地址

1
2
3
4
5
6
7
8
9
kd> !dq c09000+1c4*8
# c09e20 00000000`00ca7063 0a000000`03996863
# c09e30 0a000000`0f5bc863 0a000000`0f5bd863
# c09e40 0a000000`0f5be863 0a000000`0f5bf863
# c09e50 0a000000`032c0863 0a000000`032c1863
# c09e60 0a000000`040c3863 0a000000`02bc4863
# c09e70 0a000000`02bc5863 0a000000`02bc6863
# c09e80 0a000000`02bc7863 0a000000`02bc8863
# c09e90 0a000000`02bc9863 0a000000`02bca863

访问 PDE + PDI * 8 指向的物理地址得到物理页面

1
2
3
4
5
6
7
8
9
kd> !dq ca7000+8e*8
# ca7470 89000000`0588e121 89000000`0588f963
# ca7480 89000000`05890963 89000000`05891963
# ca7490 89000000`05892963 89000000`05893963
# ca74a0 00000000`00000000 89000000`05895963
# ca74b0 89000000`05896963 89000000`05897963
# ca74c0 89000000`05898963 89000000`05899963
# ca74d0 89000000`0589a963 89000000`0589b963
# ca74e0 00000000`00000000 89000000`0589d963

最终,访问 物理页面 + Offset 指向的物理地址得到内容

1
2
3
4
5
6
7
8
9
kd> !dq 0588e000
# 588e000 761e8e00`00107e00 00000000`fffff803
# 588e010 761e8e04`00108140 00000000`fffff803
# 588e020 761e8e03`00108600 00000000`fffff803
# 588e030 761eee00`00108ac0 00000000`fffff803
# 588e040 761eee00`00108e00 00000000`fffff803
# 588e050 761e8e00`00109140 00000000`fffff803
# 588e060 761e8e00`00109680 00000000`fffff803
# 588e070 761e8e00`00109b80 00000000`fffff803

2.3 页表属性

页表属性

2.4 页表自映射

2.4.1 简介

​ 在64位模式下,高等级页表项都指向低等级页表项的物理地址,依次类推,直到最低级别页表项,即可获取物理页面进而读取内容。在此过程中 Cr3 寄存器中存储了最高级页表(PML4)的表基物理地址。为了更好的管理这些页表,微软采取了最高级页表基址自映射的方式实现仅仅利用8字节物理内存,就可以在每次访问分页管理相关的内存时,少做一次页表查询操作来优化速度。

2.4.2 原理

​ 在四级页表的最高级 PML4 页表中存在一项,里面保存了 PML4 页表的表基物理地址,即 Cr3 。假设这一项在 PML4 表中的索引为 0x100,如下图所示:

页表自映射原理

此时满足:( ![物理地址] 表示读取物理地址的内容 )

1
![Cr3 + 0x100 * 8]  =  Cr3

用于分页管理的物理页面大小总计 512 * 512 * 512 * 4KB = 512GB,而一个 PML4 表项恰好可以管理512GB内存。

PML4 表中索引位置0x100的元素用于内存管理且满足上述关系,那么此时用于内存管理的虚拟地址空间为:

1
0xFFFF8000`00000000 ~ 0xFFFF807F`FFFFF000

按照 9-9-9-9-12 分页方式去拆分上述边界物理地址:( 只使用低48位 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 起始地址
0x8000`00000000
1 0000 0000 0x100
0 0000 0000 0x0
0 0000 0000 0x0
0 0000 0000 0x0
0000 0000 0000 0x0

// 结束地址
0x807F`FFFFF000
1 0000 0000 0x100
1 1111 1111 0x1FF
1 1111 1111 0x1FF
1 1111 1111 0x1FF
0000 0000 0000 0x0

常规查询流程:

1
2
3
4
5
6
7
8
9
10
11
// 起始地址
![Cr3 + 0x100 * 8] = PDPTE
![PDPTE + 0x0 * 8] = PDE
![PDE + 0x0 * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

// 结束地址
![Cr3 + 0x100 * 8] = PDPTE
![PDPTE + 0x1FF * 8] = PDE
![PDE + 0x1FF * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

根据上述等式,![Cr3 + 0x100 * 8] = Cr3,所以查询流程变为:

1
2
3
4
5
6
7
8
9
// 起始地址
![Cr3 + 0x0 * 8] = PDE
![PDE + 0x0 * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

// 结束地址
![Cr3 + 0x1FF * 8] = PDE
![PDE + 0x1FF * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

很神奇,查询页表操作由四次变成了三次,效率大大提升。而且只是使用了8字节的物理地址空间来保存 Cr3 。下图展示了优化后的查询过程:

优化后的查询过程

2.4.3 规律

为了写代码方便读写页表属性,四级页表都应该有自己的表基虚拟地址,以便访问其中的元素。

a.推导 PML4 基址

PML4 页表基址有两个特点:

  • 属于虚拟地址
  • 虚拟地址的内容是Cr3

假设该虚拟地址按照 9-9-9-9-12 分页规则拆分得到的索引依次为 x、y、z、r,根据页表解析规则:

1
2
3
4
![Cr3 + x * 8]   = PDPTE
![PDPTE + y * 8] = PDE
![PDE + z * 8] = PTE
![PTE + r * 8] = 物理页面 = Cr3

还需要满足 ![Cr3 + x * 8] = Cr3,所以当 x = y = z = r 的时候上述条件均满足。

b.推导 PDPT 基址

PDPT 页表基址有两个特点:

  • 属于虚拟地址
  • 虚拟地址的内容不再是Cr3,而是 ![Cr3 + 0 * 8] 指向的物理地址。

假设该虚拟地址按照 9-9-9-9-12 分页规则拆分得到的索引依次为 x、y、z、r,根据页表解析规则:

1
2
3
4
![Cr3 + x * 8]   = PDPTE
![PDPTE + y * 8] = PDE
![PDE + z * 8] = PTE
![PTE + r * 8] = ![Cr3]

还需要满足 ![Cr3 + x * 8] = Cr3,所以当 x = y = z 且 r = 0 的时候上述条件均满足。

c.推导 PD、PT 基址

​ 方法同理。

d.结论

页内偏移均为0时:

  • PML4:PML4i == PDTi == PDi == PTi == Index
  • PDPT:PML4i == PDTi == PDi == Index && PTi == 0
  • PD: PML4i == PDTi == Index && PDi == 0 && PTi == 0
  • PT: PML4i == Index && PDTi == 0 && PDi == 0 && PTi == 0

2.5 基址随机化

2.5.1 原理

​ 上面得到结论中的 Index 就是自映射表项在 PML4 表中的索引,这个值的变化就是造成各级页表基址变化的原因。

系统重启前的 PML4 基址:

1
2
3
4
5
6
7
8
0xFB7DBEDF6000
1 1111 0110 0x1F6 PML4
1 1111 0110 0x1F6 PDPT
1 1111 0110 0x1F6 PD
1 1111 0110 0x1F6 PT
000000000000 0x0

Index为:0x1F6

系统重启后的PML4基址:

1
2
3
4
5
6
7
8
0x8D46A351A000 
1 0001 1010 0x11A
1 0001 1010 0x11A
1 0001 1010 0x11A
1 0001 1010 0x11A
000000000000 0

Index为:0x11A

2.5.2 定位

​ 页表基址随机化导致写代码读写页表属性变得不方便,但可以利用页表自映射的一些结论来获取 PML4 表基址。PML4 表基址的内容为Cr3的值,并且位于 PML4 表所在的页面内。因为Cr3里保存了 PML4 的表基物理地址,所以可以通过映射Cr3物理地址的虚拟地址,遍历这个虚拟地址页面的512个地址,哪个地址符合上述条件,哪个地址就是 PML4 表基址。驱动代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ULONG64 GetPml4Base(ULONG64 ulNtBase)
{
PHYSICAL_ADDRESS pCr3 = { 0 };
pCr3.QuadPart = __readcr3();
PULONG64 pCmpArr = MmGetVirtualForPhysical(pCr3);

int count = 0;
while ((*pCmpArr & 0xFFFFFFFFF000) != pCr3.QuadPart)
{
if (count++ >= 512)
{
return -1;
}
pCmpArr++;
}
return (ULONG64)pCmpArr & 0xFFFFFFFFFFFFF000;
}

得到了 PML4 表基址,就可以得到 Index 索引值,其他各级页表基址也就都可以得到了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ULONG64 GetPdptBase(ULONG64 ulPml4Base)
{
return (ulPml4Base >> 21) << 21;
}

ULONG64 GetPdBase(ULONG64 ulPml4Base)
{
return (ulPml4Base >> 30) << 30;
}

ULONG64 GetPtBase(ULONG64 ulPml4Base)
{
return (ulPml4Base >> 39) << 39;
}

2.6 KPTI

为了避免r3利用漏洞越权访问r0内存,cpu为r3和r0各分配一个cr3,二者映射状态如下所示:

cr3映射关系

其中KVASCODE是两个cr3都映射的区域,用于在r3切换到r0时完成cr3转换工作。r0所使用的cr3保存在KPRCB.KernelDirectoryTableBase中。


x64保护模式
http://helloymf.github.io/2022/10/25/x64-bao-hu-mo-shi/
作者
JNZ
许可协议