1.分段机制 1.1 强制平坦化 在x64中,gdt表中普通段描述符中不再描述基址Base和界限Limit,基址永远是0 ,界限永远是最大的 ,只有属性还在使用。
fs 、gs 没有强制平坦,而是在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段不用来任务切换,主要保存一些rsp备用指针(用于快速切换r0,r1,r2的堆栈),内存结构如下:
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入口分析
上述代码主要完成以下的是:
完成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> 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); 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 页表基址有两个特点:
假设该虚拟地址按照 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,二者映射状态如下所示:
其中KVASCODE是两个cr3都映射的区域,用于在r3切换到r0时完成cr3转换工作。r0所使用的cr3保存在KPRCB.KernelDirectoryTableBase中。