CPU 推测执行漏洞及缓解 via KVA Shadow

😄

写完本章内容后,才发现《Windows Internals 7 Part2》有很好的讲解这个 CPU 推测执行的漏洞细节。

0 CPU 推测执行漏洞

虽然 Meltdown(熔毁,CVE-2017-5754)、Spectre(幽灵,CVE-2017-5753&CVE-2017-5715)漏洞在发现时间上存在前后关系,但是在后来的研究以及漏洞缓解实施过程中,都将这些漏洞统称为 CPU 推测执行的变体(CPU Speculative execution Variant)。

参考:

变体(Variant) CVE 原理 缓解措施
1 CVE-2017-5753(Spectre) 在有分支判断的地方,代码编译为 JCC 条件跳转指令。攻击者可以通过循环训练 CPU 对跳转的训练,最后利用 CPU 来推测执行 JCC 的目的地。 软件缓解:Microsoft Visual C/C++ 编译器提供了一个编译选项 Qspectre,通过在潜在易受攻击的代码位置插入 lfence 序列化指令来启用缓解。
2 CVE-2017-5715(Spectre) 在有间接跳转或调用,如 jmp rax/jmp [rax]/call rax/call [rax]/ret间接分支时,CPU 利用 BTB、RSB 中的信息进行预测并跳转。攻击者可以污染 BTB、RSB 来修改跳转目的地,从而实施攻击。 软件缓解:
1、使用 Retpoline 将易受攻击的间接分支替换为非易受攻击的指令序列,强制 CPU 跳转到真实目的地而不是 BTB/RSB 建议的预测目标。
2、使用 lfence 指令(可选择)。
3、RSB 填充 + BTB 刷新。
硬件缓解:
1、IBPB:同一权限级别下,之前的跳转地址不会影响之后的跳转预测。
2、IBRS:内核权限代码不得使用分支预测器中的用户代码,用户权限代码也不得使用分支预测器中的内核代码。
3 CVE-2017-5754(Meltdown) 在指令中,如果前后指令不存在依赖关系,如果前面一条指令需要访问内存或导致异常时,CPU 有可能已经执行了乱序执行了后面的指令。攻击者可以利用瞬态指令来泄露内核数据。 软件缓解:KVA Shadow、lfence 指令(乱序之行)。

1 Meltdown 和 Spectre

Meltdown 和 Spectre 是两个 CPU 漏洞,对应的 CVE 编号是 Meltdown(熔毁,CVE-2017-5754)、Spectre(幽灵,CVE-2017-5753&CVE-2017-5715)。目前已经衍生出多种变种利用方式

这两个 CPU 缺陷几乎影响了自 Pentium Pro(1995)以来开发的所有现代处理器。这些缺陷源于两个关键的CPU功能:乱序执行和推测执行。

  • Spectre:基于分支预测的推测执行(Speculative execution from branch prediction)。
  • Meltdown:基于乱序执行(Out-of-order execution)的推测执行。

1.png

两个漏洞非常相似,都是组合 CPU 设计缺陷、侧信道攻击、内存访问越界来实施攻击,以破坏地址空间隔离泄露内核数据

这两个漏洞的研究报告:《Meltdown and Spectre》。

Spectre 影响的 CPU 范围较 Meltdown 广,用于缓解 Meltdown 的 KAISER 机制不能防止 Spectre。

CVE-2017-5753、CVE-2017-5715、CVE-2017-5754 都是 Spectre 的变体:

  • 变体1,有条件的分支误判:CVE-2017-5753(Spectre)。
  • 变体2,间接分支误判:CVE-2017-5715(Spectre)。
  • 变体3,乱序执行:CVE-2017-5753(Meltdown)。

2 漏洞原理

2.1 Spectre(幽灵)

漏洞原理

几十年前,为了加快处理器的执行速度以提高性能,其中一项技术就是推测执行(Speculative Execution):在遇到分支判断时,让处理器猜测可能执行的方向,并在这个路径上提前执行指令

举个示例,在分支判断的地方访问了未缓存的内存,由于访问内存要比 CPU 执行慢得多,通常需要数百个时钟周期才能取到内存中的值,CPU 不会空闲下来而浪费这些周期时间,而是尝试去猜测该控制流的方向。CPU 先将当前点(Checkpoint)的寄存器状态保存起来,然后在猜测的路径上继续往下执行。当从内存传送过来的值到达处理器后,CPU 会检查初始猜测的正确性。如果猜测错误,CPU 会通过将寄存器状态恢复为存储的检查点来丢弃不正确的推测执行(称为回滚),这种情况下与空闲是相当的性能;如果猜测正确则继续往下执行,从而产生性能的提升。

而幽灵漏洞就是利用不正确的猜测执行,幽灵攻击诱使 CPU 推测执行正确控制流不应该执行的指令。漏洞发现者将这部分正确控制流不会执行,而 CPU 能推测执行的指令称为 Transient instructions(瞬态指令)。利用 Transient instructions 从受害者的内存地址空间中泄露内核信息。

不幸的是,尽管架构状态被回滚了,仍然有些副作用,比如 TLB 或缓存状态并没有被回滚。这些副作用随后可以被黑客通过侧信道攻击(Side Channel Attack)的方式获取到缓存的内容。如果攻击者能触发推测执行去访问指定的敏感数据区域的话,就可能可以读取到原本是其它用户或更高特权级的敏感数据。

基于这样的推测执行能够衍生出多种不同的变体利用方式,如利用条件分支的推测执行(CVE-2017-5753)、利用间接分支的推测执行(CVE-2017-5715)。

漏洞还原

本文以其中一种变体——利用条件分支的推测执行来泄露内核数据。

主要参考Meltdown Reloaded: Breaking Windows KASLR by Leaking KVA Shadow MappingsSpectre Attacks: Exploiting Speculative ExecutionSpectre and Meltdown explained: A comprehensive guide for professionals

一、触发漏洞

如下代码:

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 array_size = 1
unsigned char array1[256] = { 0 };
unsigned char array2[256] = { 0 };

Leaker_function ( unsigned __int64 pos )
{
if ( pos < array_size )
{
temp_value = array1[ array2[ pos ] ];
}
}

参数 pos 完全由攻击者控制,如果此时 pos >255,CPU 没有等待条件判断,同时还会猜测数组边界检查为真(实际上 CPU 直接访问了越界数组指向的内存),假设此时 array2[pos] = 0x41,则接下来 CPU 会访问 array1[0x41],并将访问到的数据缓存 Cache。当最终确定条件分支及数组越界检查的结果后,CPU 会发现其错误并进行回滚。但是访问 array1[0x41] 的数据已缓存 Cache,CPU 没有刷新该缓存,攻击者就可以分析该缓存的内容,从而得到越界访问的数据。

注意0 <= array2[pos] <= 255,所以数组 array1[X] 不会越界,方便后面分析缓存的值(无符号字符为1字节8位,2^8=256)。除此之外,还要要求 array1 数组之前没有被访问过也就没有相关缓存信息。

所以可以构造如下代码:

1
array1[ array2[0xffff8000`12345678 - &array2] ];

这样就可以将 CPU 推测执行 array2[0xffff8000'12345678] 的地址缓存起来,然后分析缓存,从而得到地址 0xffff8000'12345678 泄露的值。

关于缓存

现代 CPU 一般都会在内部设置缓存结构,然后将高速缓存划分为固定大小的块(或叫做行Lines),典型的每行 64/128 字节,内存是按行的粒度进行缓存的。档处理器需要内存中的数据时,首先就要去高速缓存中进行检索,检索不到才会去访问物理内存。

Intel 处理器通常具有三级缓存,每个核心都有自己的 L1、L2 缓存,还有一个所有核心共享的 L3 缓存(也称为LLC)。

处理器必须使用缓存一致性协议确保每个内核的 L1和L2缓存是一致的,通常基于 MESI 协议。特别是,使用 MESI 协议或其某些变体意味着对一个内核的内存写入操作将导致其他内核的 L1 和 L2缓存中的相同数据副本被标记为无效,这意味着将来访问其他内核上的这些数据将无法从L1或 L2 缓存中快速加载数据。

二、分析缓存

由于 CPU 使用缓存来存储最近访问的内存的内容,并且缓存比常规内存快得多,因此我们有机会通过观察不同缓存行的访问时间来推断访问了哪些元素。

推断访问了哪个缓存行的典型方法是(Flush+Reload技术):

  1. 首先刷新所有缓存行,即刷新数组每个元素的 CPU 缓存。

    1
    2
    3
    4
    5
    for ( i = 0 ; i < 256 ; i ++ )
    {
    // Invalidating cache
    _mm_clflush ( &array1 [ i ] );
    }
  2. 然后触发受害者内存访问,将访问的数据进行缓存。

    1
    2
    3
    4
    5
    6
    7
    Leaker_function ( unsigned __int64 pos )
    {
    if ( pos < array_size )
    {
    temp_value = array1[ array2[0xffff8000`12345678 - &array2] ];
    }
    }

    假设 array2[0xffff8000'12345678 - &array2] = 0x41,则在下面使用 __rdtscp 检索数组 array1 每个元素访问时间时,应该是 array1[0x41] 使用的时间最少。

  3. 为访问每个缓存行计时(依次计时访问数组 arry1 每个元素),受害者访问的元素将产生最低的时间。

三、训练

为诱导分支预测推测执行,在错误的推测执行前应该尽可能的训练 CPU 推测正确的分支判断,使得 CPU 在类似的分支判断前按照之前的经验进行推测执行。

完整利用代码(参考Meltdown Reloaded: Breaking Windows KASLR by Leaking KVA Shadow Mappings):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Kernel address to be read
malicious_pos = kernel_address - &array1;

// Looping 33 times
for ( i = 0 ; i <= 33 ; i ++ )
{
// Enabling speculator
pos = ( ( i % 33 ) - 1 ) & ~0xFFFF;
pos = ( pos | ( pos >> 16 ) );
pos = pos & malicious_pos;

// Leaking data when branch predictor is working
leaker_function ( pos );
}

Github 利用项目Meltdown KVA Shadow Leak

总结

幽灵(Spectre)漏洞利用 CPU 的分支预测+推测执行。推测执行时不会对内存访问权限检查,以及数组越界等检查。似乎 CPU 推测执行时都会推测执行条件”满足“而继续往下推测执行,等最后判断条件不满足时再回滚寄存器内容或已访问内存的内容。但是此时已访问的内存内容已经缓存至高速缓存了,后面就可以利用侧信道攻击(根据访问内存时间从而判断缓存信息)。

2.2 Meltdown(熔断)

熔断也是利用 CPU 的推测执行,只不过不像幽灵漏洞那样是在分支预测时进行推断执行,而是在乱序执行中进行的推测执行。

在用户空间执行的代码可以利用熔断漏洞来泄露内核空间的数据。

如下代码:

1
2
3
int a = 1;
int b = a + 1;
int c = 1;

由于 b 的值依赖于 a 的值,所以 CPU 在执行时可能会进行乱序执行如下:

1
2
3
int a = 1;
int c = 1;
int b = a + 1;

下面推测执行的指令不依赖于前面的指令。

论文指出可乱序执行的指令:当遇到分支条件时,位于该路径上且没有任何依赖关系的指令可以提前执行,如果预测正确,则立即使用它们的结果。如果预测不正确,重新初始化统一保留站来回滚到正常状态(Instructions that lie on that path and do not have any dependencies can be executed in advance and their results immediately used if the prediction was correct. If the prediction was incorrect, the reorder buffer allows to rollback to a sane state by clearing the reorder buffer and re-initializing the unified reservation station.)。

可以构造以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned char array1[256] = { 0 };
unsigned char array2[256] = { 0 };

Leaker_function ( unsigned __int64 pos )
{
unsigned char ErrorValue = 0;
unsigned char temp_value = 0;

try
{
ErrorValue = 0xFFFF8000`00000000;
}
__except(1)
{
printf(”Hit Exception!“);
return;
}

temp_value = array1[ array2[0xffff8000`12345678 - &array2] ];
}

在用户空间的程序访问 __try 中的内核地址导致异常,在进行异常处理前,由于乱序执行,CPU 可能已经执行了下面的访问数组的指令(瞬态指令,Transient instructions)。因为访问数组指令不依赖于前面的指令,然后访问到的 array1 数据已经缓存至告诉缓存。

在正常程序流程中,瞬态指令部分不应该被执行到。

然后和上面的幽灵漏洞一样,使用侧信道攻击,借助 Flush+Reload 技术来分析缓存。假设地址 0xffff8000'12345678 中的数据为 84,则高速缓存中缓存的应该是 array1[84]。Flush+Reload 技术分析访问数组 array1 每个元素访问时间如下:

2.png

2.3 缓解措施

可参考:《幽灵・熔毁・预兆》。

  • 硬件:重新设计 CPU 以确保在发射读取指令之前进行权限检查,英特尔已于 Coffee Lake Refresh 及后续微架构中修补漏洞,但之前的 CPU 就只能软件修补了;
  • 软件:各大操作系统都推出了内核页表隔离补丁来抵御漏洞:
    • Linux 4.15 已部署 Kernel Page-Table Isolation(内核页表隔离,KPTI);
    • Windows 10 build 17035 已部署 KVA Shadow;
    • macOS 10.13.2 / iOS 11.2 已部署 Double Map;
    • ……

https://www.4hou.com/posts/jL6y

https://www.anquanke.com/post/id/93599

https://blog.yzsun.me/spectre-meltdown-foreshadow/

http://www.qfrost.com/WindowsKernel/KVAS/

https://zhuanlan.zhihu.com/p/33427373

https://zerosum0x0.blogspot.com/2019/11/fixing-remote-windows-kernel-payloads-meltdown.html

https://blog.csdn.net/xuandao_ahfengren/article/details/112135179

https://xz.aliyun.com/t/3072

https://key08.com/usr/uploads/2022/05/910489819.pdf

3 Meltdown 缓解措施

微软在 2018.1.3 发布更新 以缓解如下的漏洞,并给出了相关的说明Mitigating speculative execution side channel hardware vulnerabilities。其中提到 Hypervisor address space segregation、Split user and kernel page tables 等诸多措施。

其中关于 Split user and kernel page tables:

这种缓解措施被称为 Windows 上的内核虚拟地址(KVA)阴影(Windows 下的内核页表隔离),它通过每个进程创建两个页面目录基来缓解被称为 Meltdown(CVE-2017-5754)的推测技术。第一个映射“用户”页面表,该表仅包含用户模式映射和少量内核过渡页面。第二个映射了“内核”页面表,其中包含该过程的用户和内核映射。也就是每个进程提供两个 CR3,一个用于用户空间,一个用于内核空间。专门用来缓解 Meltdown 漏洞。关于 KVA Shadow 的缓解措施,微软也给了说明文档KVA Shadow: Mitigating Meltdown on Windows

本章节主要先研究 KVA Shadow 缓解措施,然后研究在 Windows 内核中的一些具体的缓解 Spectre 代码示例。

KVA Shadow 用来缓解 Meltdown,并不能缓解 Spectre。

3.1 KVA Shadow

利用 Meltdown 漏洞可以在用户空间泄露内核空间的数据,这是因为每一个进程共享系统内核空间,且用户空间可以通过系统调用或中断处理等方式进入内核。这些实现的基础就是线性地址的连续性映射,如在一个进程中,用户空间地址范围 0~0x00007FFF'FFFFFFFF,内核空间地址范围 0xFFFF800'00000000~0xFFFFFFFF'FFFFFFFF

KVA Shadow 的核心原理就是:使用 2 个 CR3 来构建两套页表,一个 CR3 来映射用户空间使用的线性地址,另一个 CR3 来映射内核空间使用的线性地址。前者叫做 Shadow CR3 或 Shadow PLM4 或 User CR3,后者叫做 Kernel CR3,以实现页表隔离。

  • Kernel CR3:映射用户空间及内核空间地址,仅在内核模式下使用。
  • User CR3:仅映射用户空间地址及一小部分内核空间地址。

这一小部分内核空间地址实际上存放的是 GDT、IDT,及一些用来提供处理系统调用、中断处理函数的资源,在 ntoskrnl.exe 这部分代码存放在 .KVASCODE 段中,而非 .text 段。

3.png

当选择启用 KVA Shadow 机制后,应用程序会有两个 CR3,一个为内核CR3,能够访问内核物理页,而三环的 CR3只映射了内核的.KVASCODE 区段的物理页(少数r3进入r0的入口),而没有映射其他区段的,因此通过 3 环的 Cr3 寻找内核 TEXT 段的物理页,最多只能找到 PPE,而从 PDE 开始就没有映射了。

如上图,当 CPL = 3 时,用户 CR3 可访问的地址空间及访问权限只是整个线性地址空间的一部分。两个 CR3 的位置如下:

  • User CR3:KPROCESS.UserDirectoryTableBase
  • Kernel CR3:KTHREAD.SavedApcState.Process.DirectoryTableBaseKPRCR.KernelDirectoryTableBase

KVA Shadow 起作用的地方主要在:

  1. 系统调用、用户空间触发的中断处理入口:用于将用户 UserDirectoryTableBase 切换到内核 KernelDirectoryTableBase
  2. 从内核空间返回至用户空间出口:用于将内核 KernelDirectoryTableBase 切换至用户 UserDirectoryTableBase

下面以 ntoskrnl.exe.KVASCODE.text 两个区段中的地址来解释两个 CR3:

4.png

  1. 获取 CR3。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    kd> !process 0 0 notepad.exe
    PROCESS ffffdb04799bd080
    SessionId: 1 Cid: 0f94 Peb: ac473ef000 ParentCid: 0d28
    DirBase: 2e269002 ObjectTable: ffffa88876245140 HandleCount: 238.
    Image: notepad.exe

    kd> dt nt!_EPROCESS ffffdb04799bd080 ImageFileName Pcb.DirectoryTableBase Pcb.UserDirectoryTableBase
    +0x000 Pcb :
    +0x028 DirectoryTableBase : 0x2e269002
    +0x388 UserDirectoryTableBase : 0x66468001
    +0x5a8 ImageFileName : [15] "notepad.exe"

    可以看到,这两个 CR3 下 PCID 值不一样,后面会讲到,这将用于性能优化。

    • DirectoryTableBase:PCID = 2
    • UserDirectoryTableBase:PCID = 1
  2. 获取 ntoskrnl.exe.KVASCODE.text 两个区段不同函数的地址。

    1
    2
    3
    4
    kd> ? nt!KiBreakpointTrapShadow
    Evaluate expression: -8794360622400 = fffff800`674252c0
    kd> ? nt!KiBreakpointTrap
    Evaluate expression: -8794366969856 = fffff800`66e17800
  3. UserDirectoryTableBase 解析这两个地址(注意:!vtop 指令不识别十六进制地址的 ' 符号)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // KiBreakpointTrapShadow
    kd> !vtop 0x66468000 0xfffff800674252c0
    Amd64VtoP: Virt fffff800674252c0, pagedir 0000000066468000
    Amd64VtoP: PML4E 0000000066468f80
    Amd64VtoP: PDPE 0000000079768008
    Amd64VtoP: PDE 00000000797679d0
    Amd64VtoP: PTE 000000007976e128
    Amd64VtoP: Mapped phys 00000000036122c0
    Virtual address fffff800674252c0 translates to physical address 36122c0.

    // KiBreakpointTrap
    kd> !vtop 0x66468000 0xfffff80066e17800
    Amd64VtoP: Virt fffff80066e17800, pagedir 0000000066468000
    Amd64VtoP: PML4E 0000000066468f80
    Amd64VtoP: PDPE 0000000079768008
    Amd64VtoP: PDE 00000000797679b8
    Amd64VtoP: zero PDE
    Virtual address fffff80066e17800 translation fails, error 0xD0000147.

    可以看到使用 UserDirectoryTableBase 时,位于 .text 区段的函数 KiBreakpointTrap 地址没有对应的 PTE,没有进行映射。

  4. DirectoryTableBase 解析这两个地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // KiBreakpointTrapShadow
    kd> !vtop 0x2e269000 0xfffff800674252c0
    Amd64VtoP: Virt fffff800674252c0, pagedir 000000002e269000
    Amd64VtoP: PML4E 000000002e269f80
    Amd64VtoP: PDPE 0000000000c89008
    Amd64VtoP: PDE 0000000000c8a9d0
    Amd64VtoP: PTE 0000000000c1c128
    Amd64VtoP: Mapped phys 00000000036122c0
    Virtual address fffff800674252c0 translates to physical address 36122c0.

    // KiBreakpointTrap
    kd> !vtop 0x2e269000 0xfffff80066e17800
    Amd64VtoP: Virt fffff80066e17800, pagedir 000000002e269000
    Amd64VtoP: PML4E 000000002e269f80
    Amd64VtoP: PDPE 0000000000c89008
    Amd64VtoP: PDE 0000000000c8a9b8
    Amd64VtoP: PTE 0000000000c190b8
    Amd64VtoP: Mapped phys 0000000003004800
    Virtual address fffff80066e17800 translates to physical address 3004800.

    可以看到,使用 DirectoryTableBase 时,两个不同区段的函数都进行了映射。

注意一个细节:.KVASCODE 区段中的虚拟地址、物理地址在两个 CR3 下面都是相同的,但是 PLM4E、PDPTE、PDE、PTE 不相同

3.2 初始化KVA Shadow

KVAS 在启动(Boot)过程中就被初始化,会在操作系统初始化过程中被启用。NT 内核的入口点称为 KiSystemStartup,由操作系统加载器(OS Loader)调用。KiSystemStartup 依次执行一些基础的初始化步骤,其中的一个步骤就是调用 KilnitializeBootStructures

KiInitializeBootStructures 将会调用 KiEnableKvaShadowingKiEnableKvaShadowing 的功能是负责启用 KVAS,并设置 KiKvaShadow = 1

KiEnableKvaShadowing 函数中(处理成功则返回 TRUE):

  1. 如果 KiIsKvaShadowDisabled = 1,表示禁用 KVA Shadow,则不会将 KiKvaShadow = 1,直接返回 1。如果 KiIsKvaShadowDisabled = 0,则会将 KiKvaShadow = 1

  2. 如果 KPRCB.Number = 0,即当前 CPU 编号为 0 时进行下面的步骤 3。否则会调用 KiShadowProcessorAllocation 映射 IDT.base、KernelDirectoryTableBase 的共享内存区对象,并设置 KPRCB.ShadowFlags = 2,然后跳到步骤 6。

  3. 调用 KiInitializeIdt 函数,使用 KiInterruptInitTable 中的函数填充 KiDebugTraps[KiDebugTrapIndex++] = v11,然后将中断处理例程(ISR)全部替换成 Shadow 版本。

    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
    kd> !idt

    Dumping IDT: fffff80069877000

    00: fffff80067425100 nt!KiDivideErrorFaultShadow
    01: fffff80067425180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF8006987B9D0
    02: fffff80067425240 nt!KiNmiInterruptShadow Stack = 0xFFFFF8006987B7D0
    03: fffff800674252c0 nt!KiBreakpointTrapShadow
    04: fffff80067425340 nt!KiOverflowTrapShadow
    05: fffff800674253c0 nt!KiBoundFaultShadow
    06: fffff80067425440 nt!KiInvalidOpcodeFaultShadow
    07: fffff800674254c0 nt!KiNpxNotAvailableFaultShadow
    08: fffff80067425540 nt!KiDoubleFaultAbortShadow Stack = 0xFFFFF8006987B3D0
    09: fffff800674255c0 nt!KiNpxSegmentOverrunAbortShadow
    0a: fffff80067425640 nt!KiInvalidTssFaultShadow
    0b: fffff800674256c0 nt!KiSegmentNotPresentFaultShadow
    0c: fffff80067425740 nt!KiStackFaultShadow
    0d: fffff800674257c0 nt!KiGeneralProtectionFaultShadow
    0e: fffff80067425840 nt!KiPageFaultShadow
    10: fffff800674258c0 nt!KiFloatingErrorFaultShadow
    11: fffff80067425940 nt!KiAlignmentFaultShadow
    12: fffff800674259c0 nt!KiMcheckAbortShadow Stack = 0xFFFFF8006987B5D0
    13: fffff80067425ac0 nt!KiXmmExceptionShadow
    14: fffff80067425b40 nt!KiVirtualizationExceptionShadow
    15: fffff80067425bc0 nt!KiControlProtectionFaultShadow
    1f: fffff80067425c40 nt!KiApcInterruptShadow
    20: fffff80067425cc0 nt!KiSwInterruptShadow
    29: fffff80067425d40 nt!KiRaiseSecurityCheckFailureShadow
    2c: fffff80067425dc0 nt!KiRaiseAssertionShadow
    2d: fffff80067425e40 nt!KiDebugServiceTrapShadow
    2f: fffff80067425f40 nt!KiDpcInterruptShadow
    30: fffff80067425fc0 nt!KiHvInterruptShadow
    31: fffff80067426040 nt!KiVmbusInterrupt0Shadow
    32: fffff800674260c0 nt!KiVmbusInterrupt1Shadow
    33: fffff80067426140 nt!KiVmbusInterrupt2Shadow
    34: fffff800674261c0 nt!KiVmbusInterrupt3Shadow
    35: fffff80067426468 nt!HalpInterruptCmciService (KINTERRUPT fffff80067705fc0)
  4. 先将 KiFlushPcid = 1。如果 KPRCB.FeatureBits & 240000000000h = 240000000000h 则将 KiFlushPcid = 2

  5. 设置 KiKvaShadowMode 的值。

    1
    2
    3
    4
    5
    6
    7
    if(KiFlushPcid != 0)
    {
    KiKvaShadowMode = 1;
    }else
    {
    KiKvaShadowMode = 2;
    }
  6. 设置 KPRCB.KernelDirectoryTableBase 的 Bit63。

    1
    2
    3
    4
    5
    // 则将 bit63 = 1,不刷新 TLB 和分页结构缓存
    if(KiFlushPcid != 0)
    {
    KPRCB.KernelDirectoryTableBase &= 63;
    }

上述初始化好后,返回到函数 KiInitializeBootStructures 继续进行初始化系统调用的入口函数。

在已经开启 KVA Shadow 的情况下:

  • 先将 KiSystemCall32Shadow/KiSystemCall64Shadow 保存到全局指针 KiDebugTraps 指向索引为 6/5 的位置。
  • 然后修改 MSR 的 IA32_STAR/IA32_LSTAR/IA32_FMASK 寄存器,将系统调用入口全部替换为 Shadow 版本(如果不开启KVAS则不替换为Shadow版本)。
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
63
64
65
66
67
68
rbx = &KiSystemCall32;
rsi = &KiSystemCall64;
KiEnableKvaShadowing(&_KPRCB, IDT.Base);

if(KiKvaShadow != 0)
{
rbx = &KiSystemCall32Shadow;
rsi = &KiSystemCall64Shadow;
}

KiDebugTraps[KiDebugTrapIndex++] = &KiSystemCall64Shadow;
KiDebugTraps[KiDebugTrapIndex++] = &KiSystemCall32Shadow;
/*
kd> dqs KiDebugTraps
fffff800`67814900 fffff800`674252c0 nt!KiBreakpointTrapShadow
fffff800`67814908 fffff800`67425340 nt!KiOverflowTrapShadow
fffff800`67814910 fffff800`67425d40 nt!KiRaiseSecurityCheckFailureShadow
fffff800`67814918 fffff800`67425dc0 nt!KiRaiseAssertionShadow
fffff800`67814920 fffff800`67425e40 nt!KiDebugServiceTrapShadow
fffff800`67814928 fffff800`67427180 nt!KiSystemCall64Shadow
fffff800`67814930 fffff800`67426e40 nt!KiSystemCall32Shadow
fffff800`67814938 00000000`00000000
fffff800`67814940 00000400`00000007
fffff800`67814948 fffff800`66c894f0 nt!KiDispatchException
*/

// IA32_STAR, 提供CS、SS
__asm
{
xor eax, eax;
mov edx, 230010h; // 1、低16位是内核空间的 CS = 0x10(SS = CS + 8 = 0x18)
// 2、高16位是用户空间的 CS = 0x23(SS = CS + 8 = 0x2B)
mov ecx, 0C0000081h;
wrmsr;
}

// IA32_LSTAR, 提供系统调用的 RIP
__asm
{
mov rdx, rsi; // 使用 KiSystemCall64Shadow 函数填充
mov rax, rsi;
shr rdx, 20h;
mov ecx, 0C0000082h;
wrmsr;
}

// IA32_FMASK, 屏蔽 RFLAGS 的某些位
__asm
{
mov eax, 4700h; // 注意在 syscall 中是将 IA32_FMASK 先取反再与上 RFLAGS
// 所以系统调用进入内核时会将 TF、IF、DF、NT 位都清零
xor edx, edx;
mov ecx, 0C0000084h;
wrmsr;
}

// IA32_CSTAR, Intel64 的 syscall 指令不使用,给 ADM CPU 使用
// 用 KiSystemCall32Shadow 填充 IA32_CSTAR
// 在 Intel64 CPU 中的 syscall 指令不使用该寄存器,所以也就用不到 KiSystemCall32Shadow 函数
// 这是给 AMD CPU 使用的,因为 Intel 下面 32 位程序先切换至 64 位再进入内核
__asm
{
mov rdx, rbx;
mov rax, rbx;
shr rdx, 20h;
mov ecx, 0C0000083h;
wrmsr;
}

3.3 CR3 切换分析

KVA Shadow: Mitigating Meltdown on Windows建议了解 KVAS最好是从中断处理、系统调用开始。与 KVA Shadow 最相关的事件是那些称为“内核进入”和“内核退出”事件的事件。这些事件分别涉及从用户模式过渡到内核模式,以及从内核模式过渡到用户模式。

如下图,系统调用中断处理从用户模式刚进入内核模式阶段的状态。需要在关中断的状态下立即从 UserDirectoryTableBase 切换到内核 KernelDirectoryTableBase

5.png

5

下面就以 3 号中断 KiBreakpointTrapShadow 处理函数为例进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kd> dqs KiDebugTraps
fffff800`67814900 fffff800`674252c0 nt!KiBreakpointTrapShadow
fffff800`67814908 fffff800`67425340 nt!KiOverflowTrapShadow
fffff800`67814910 fffff800`67425d40 nt!KiRaiseSecurityCheckFailureShadow
fffff800`67814918 fffff800`67425dc0 nt!KiRaiseAssertionShadow
fffff800`67814920 fffff800`67425e40 nt!KiDebugServiceTrapShadow
fffff800`67814928 fffff800`67427180 nt!KiSystemCall64Shadow
fffff800`67814930 fffff800`67426e40 nt!KiSystemCall32Shadow
fffff800`67814938 00000000`00000000
fffff800`67814940 00000400`00000007
fffff800`67814948 fffff800`66c894f0 nt!KiDispatchException

kd> !idt

Dumping IDT: fffff80069877000

00: fffff80067425100 nt!KiDivideErrorFaultShadow
01: fffff80067425180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF8006987B9D0
02: fffff80067425240 nt!KiNmiInterruptShadow Stack = 0xFFFFF8006987B7D0
03: fffff800674252c0 nt!KiBreakpointTrapShadow
04: fffff80067425340 nt!KiOverflowTrapShadow

KiBreakpointTrapShadow 函数:

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
KVASCODE:0000000140A122C0 // =============== S U B R O U T I N E =======================================
KVASCODE:0000000140A122C0
KVASCODE:0000000140A122C0 // 中断门硬件压栈:SS3、RSP3、RFLAGS、CS3、RIP3、ErrorCode
KVASCODE:0000000140A122C0 //
KVASCODE:0000000140A122C0
KVASCODE:0000000140A122C0 KiBreakpointTrapShadow proc near // DATA XREF: .rdata:00000001400091F0↑o
KVASCODE:0000000140A122C0 // .pdata:000000014012C468↑o ...
KVASCODE:0000000140A122C0
KVASCODE:0000000140A122C0 arg_0 = byte ptr 8
KVASCODE:0000000140A122C0
KVASCODE:0000000140A122C0 test [rsp+arg_0], 1
KVASCODE:0000000140A122C5 jz short loc_140A1232E // CS3,判断先前模式
KVASCODE:0000000140A122C5 // 这里不是 ErrorCode,3 号中断不会像堆栈压入 错误码
KVASCODE:0000000140A122C5 // // ErrorCode.EXT = 1,代表外部硬件触发的异常;
KVASCODE:0000000140A122C5 // // ErrorCode.EXT = 0 为软件触发异常(INT n)。
KVASCODE:0000000140A122C7 swapgs
KVASCODE:0000000140A122CA lfence // 缓解推测执行
KVASCODE:0000000140A122CD bt gs:_KPCR.Prcb.ShadowFlags, 1
KVASCODE:0000000140A122D7 jb short loc_140A122E5 // ShadowFlags < 1,表示没有开启 KVAS,不用切换 CR3
KVASCODE:0000000140A122D9 mov rsp, gs:_KPCR.Prcb.KernelDirectoryTableBase // 当前的 CR3 = UserDirectoryTableBase
KVASCODE:0000000140A122E2 mov cr3, rsp // 将当前的 CR3 切换到 KernelDirectoryTableBase
KVASCODE:0000000140A122E5
KVASCODE:0000000140A122E5 loc_140A122E5: // CODE XREF: KiBreakpointTrapShadow+17↑j
KVASCODE:0000000140A122E5 mov rsp, gs:_KPCR.Prcb.RspBaseShadow // -----
KVASCODE:0000000140A122E5 // kd> dt nt!_KPRCB Rsp*
KVASCODE:0000000140A122E5 // +0x028 RspBase : Uint8B
KVASCODE:0000000140A122E5 // +0x8e88 RspBaseShadow : Uint8B
KVASCODE:0000000140A122E5 // -----------------------------------
KVASCODE:0000000140A122EE mov gs:10h, rsi
KVASCODE:0000000140A122F7 mov rsi, gs:_KPCR.IdtBase
KVASCODE:0000000140A12300 add rsi, 4200h // kd> dqs 0xfffff800`69877000+0x4200-0x40
KVASCODE:0000000140A12300 // fffff800`6987b1c0 00000000`00000000
KVASCODE:0000000140A12300 // fffff800`6987b1c8 00000000`00000000
KVASCODE:0000000140A12300 // fffff800`6987b1d0 00000000`00000014
KVASCODE:0000000140A12300 // fffff800`6987b1d8 00007ff8`9f0e0510 KERNELBASE!CreateThreadpoolTimer
KVASCODE:0000000140A12300 // fffff800`6987b1e0 00000000`00000033
KVASCODE:0000000140A12300 // fffff800`6987b1e8 00000000`00010246
KVASCODE:0000000140A12300 // fffff800`6987b1f0 0000008f`6d37f2d8
KVASCODE:0000000140A12300 // fffff800`6987b1f8 00000000`0000002b
KVASCODE:0000000140A12300 // fffff800`6987b200 00000000`00000000
KVASCODE:0000000140A12300 // fffff800`6987b208 00000000`00000000
KVASCODE:0000000140A12307 push qword ptr [rsi-8] // ss3
KVASCODE:0000000140A1230A push qword ptr [rsi-10h] // rsp3
KVASCODE:0000000140A1230D push qword ptr [rsi-18h] // rflags3
KVASCODE:0000000140A12310 push qword ptr [rsi-20h] // cs3
KVASCODE:0000000140A12313 push qword ptr [rsi-28h] //rip3
KVASCODE:0000000140A12316 mov rsi, gs:10h
KVASCODE:0000000140A1231F and qword ptr gs:10h, 0
KVASCODE:0000000140A12329 jmp KiBreakpointTrap
KVASCODE:0000000140A1232E // ---------------------------------------------------------------------------
KVASCODE:0000000140A1232E
KVASCODE:0000000140A1232E loc_140A1232E: // CODE XREF: KiBreakpointTrapShadow+5↑j
KVASCODE:0000000140A1232E lfence
KVASCODE:0000000140A12331 jmp KiBreakpointTrap
KVASCODE:0000000140A12336 // ---------------------------------------------------------------------------
KVASCODE:0000000140A12336 retn
KVASCODE:0000000140A12336 KiBreakpointTrapShadow endp

KiEnableKvaShadowing 函数:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
PAGELK:000000014099CFDC // =============== S U B R O U T I N E =======================================
PAGELK:000000014099CFDC
PAGELK:000000014099CFDC
PAGELK:000000014099CFDC KiEnableKvaShadowing proc near // CODE XREF: KxInitializeProcessorState+1EC↑p
PAGELK:000000014099CFDC // KiInitializeBootStructures+32D↑p
PAGELK:000000014099CFDC // DATA XREF: ...
PAGELK:000000014099CFDC
PAGELK:000000014099CFDC // FUNCTION CHUNK AT PAGELK:00000001409A46E0 SIZE 00000041 BYTES
PAGELK:000000014099CFDC
PAGELK:000000014099CFDC push rbx
PAGELK:000000014099CFDE sub rsp, 20h
PAGELK:000000014099CFE2 mov r11, rdx // rdx = IDT.Base
PAGELK:000000014099CFE5 mov rbx, rcx // rbx = rcx = KPRCB
PAGELK:000000014099CFE8 call KiIsKvaShadowDisabled // nt!KiFeatureSettings = 0
PAGELK:000000014099CFED test al, al // rax = 0
PAGELK:000000014099CFEF jnz loc_1409A46E0 // 已禁用 KVAS,并返回 1
PAGELK:000000014099CFF5 mov rax, cs:KeFeatureBits2 // 已启用 KVAS, nt!KeFeatureBits2 = 0
PAGELK:000000014099CFFC and eax, 18000h
PAGELK:000000014099D001 cmp rax, 8000h
PAGELK:000000014099D007 jz loc_1409A46EC
PAGELK:000000014099D00D
PAGELK:000000014099D00D loc_14099D00D: // CODE XREF: KiEnableKvaShadowing+771B↓j
PAGELK:000000014099D00D call KiIsKvaLeakSimulated // 0
PAGELK:000000014099D012 test al, al
PAGELK:000000014099D014 jnz loc_1409A46FC
PAGELK:000000014099D01A
PAGELK:000000014099D01A loc_14099D01A: // CODE XREF: KiEnableKvaShadowing+7727↓j
PAGELK:000000014099D01A cmp cs:KiKvaLeakage, 0 // KiKvaLeakage = 00001000`00000001
PAGELK:000000014099D021 jz loc_1409A4708
PAGELK:000000014099D027
PAGELK:000000014099D027 loc_14099D027: // CODE XREF: KiEnableKvaShadowing+7739↓j
PAGELK:000000014099D027 mov rax, cr3
PAGELK:000000014099D02A mov [rcx+_KPRCB.KernelDirectoryTableBase], rax
PAGELK:000000014099D031 mov rax, [rdx+1004h]
PAGELK:000000014099D038 mov [rdx+1078h], rax
PAGELK:000000014099D03F call KiInitializeDescriptorIst
PAGELK:000000014099D044 lea rcx, [r11+4200h]
PAGELK:000000014099D04B mov [r11+1004h], rcx
PAGELK:000000014099D052 cmp [rbx+_KPRCB.Number], 0
PAGELK:000000014099D056 jz short loc_14099D0A6
PAGELK:000000014099D058 mov rdx, r11
PAGELK:000000014099D05B mov rcx, rbx
PAGELK:000000014099D05E call KiShadowProcessorAllocation // 给IDT.base、KernelDirectoryTableBase创建内存区对象
PAGELK:000000014099D063 test eax, eax
PAGELK:000000014099D065 jz loc_1409A471A
PAGELK:000000014099D06B xor ecx, ecx
PAGELK:000000014099D06D call KeGetPrcb
PAGELK:000000014099D072 movzx ecx, [rax+_KPRCB.VerwSelector]
PAGELK:000000014099D079 or [rbx+_KPRCB.ShadowFlags], 2
PAGELK:000000014099D080 mov [rbx+_KPRCB.VerwSelector], cx
PAGELK:000000014099D087
PAGELK:000000014099D087 loc_14099D087: // CODE XREF: KiEnableKvaShadowing+189↓j
PAGELK:000000014099D087 cmp cs:KiFlushPcid, 0
PAGELK:000000014099D08E jz short loc_14099D09A
PAGELK:000000014099D090 lock bts [rbx+_KPRCB.KernelDirectoryTableBase], 3Fh // -------
PAGELK:000000014099D090 // KiFlushPcid != 0
PAGELK:000000014099D090 // 则将 bit63 = 1,不刷新 TLB 和分页结构缓存
PAGELK:000000014099D090 // ------------------------------
PAGELK:000000014099D09A
PAGELK:000000014099D09A loc_14099D09A: // CODE XREF: KiEnableKvaShadowing+B2↑j
PAGELK:000000014099D09A // KiEnableKvaShadowing+770B↓j ...
PAGELK:000000014099D09A mov eax, 1
PAGELK:000000014099D09F
PAGELK:000000014099D09F loc_14099D09F: // CODE XREF: KiEnableKvaShadowing+7740↓j
PAGELK:000000014099D09F add rsp, 20h
PAGELK:000000014099D0A3 pop rbx
PAGELK:000000014099D0A4 retn
PAGELK:000000014099D0A4 // ---------------------------------------------------------------------------
PAGELK:000000014099D0A5 align 2
PAGELK:000000014099D0A6
PAGELK:000000014099D0A6 loc_14099D0A6: // CODE XREF: KiEnableKvaShadowing+7A↑j
PAGELK:000000014099D0A6 mov dl, 1
PAGELK:000000014099D0A8 mov rcx, r11 // IDT.Base
PAGELK:000000014099D0AB call KiInitializeIdt // 使用 KiInterruptInitTable 中的函数填充 KiDebugTraps[KiDebugTrapIndex++] = v11//
PAGELK:000000014099D0AB // 全都替换为 Shadow 版本
PAGELK:000000014099D0B0 mov rax, gs:_KPCR.Prcb.CurrentThread
PAGELK:000000014099D0B9 mov rcx, [rax+_KTHREAD._union_90.ApcState.Process]
PAGELK:000000014099D0C0 mov [rcx+_KPROCESS.AddressPolicy], 1
PAGELK:000000014099D0C7 mov cs:byte_140D24D90, 1
PAGELK:000000014099D0CE lock or cs:dword_140D2527C, 4000h
PAGELK:000000014099D0D9 mov ecx, 1
PAGELK:000000014099D0DE call KiSetAddressPolicy
PAGELK:000000014099D0E3 mov eax, 18h
PAGELK:000000014099D0E8 mov rcx, 40000000000h
PAGELK:000000014099D0F2 mov [rbx+_KPRCB.VerwSelector], ax
PAGELK:000000014099D0F9 mov rax, [rbx+_KPRCB.FeatureBits]
PAGELK:000000014099D100 test rcx, rax
PAGELK:000000014099D103 jz short loc_14099D12D
PAGELK:000000014099D105 mov rax, cr4
PAGELK:000000014099D108 btr rax, 7 // CR4.PGE = 1 ,启用分页模式
PAGELK:000000014099D10D bts rax, 11h // UMIP = 1,禁止在 CPL>0 时使用 SGDT, SIDT, SLDT, SMSW, 和STR 指令。
PAGELK:000000014099D112 mov cr4, rax
PAGELK:000000014099D115 mov rax, cr3
PAGELK:000000014099D118 or rax, 2
PAGELK:000000014099D11C mov cr3, rax
PAGELK:000000014099D11F or cs:KiFlushPcid, 1
PAGELK:000000014099D126 mov rax, [rbx+_KPRCB.FeatureBits]
PAGELK:000000014099D12D
PAGELK:000000014099D12D loc_14099D12D: // CODE XREF: KiEnableKvaShadowing+127↑j
PAGELK:000000014099D12D mov rcx, 240000000000h
PAGELK:000000014099D137 and rax, rcx
PAGELK:000000014099D13A cmp rax, rcx
PAGELK:000000014099D13D jnz short loc_14099D146
PAGELK:000000014099D13F or cs:KiFlushPcid, 2
PAGELK:000000014099D146
PAGELK:000000014099D146 loc_14099D146: // CODE XREF: KiEnableKvaShadowing+161↑j
PAGELK:000000014099D146 call HvlRescindEnlightenments
PAGELK:000000014099D14B mov al, cs:KiFlushPcid
PAGELK:000000014099D151 neg al // CF = 1
PAGELK:000000014099D153 mov cs:KiKvaShadow, 1
PAGELK:000000014099D15A sbb ecx, ecx // ecx = -1
PAGELK:000000014099D15C add ecx, 2 // ecx = 1
PAGELK:000000014099D15F mov cs:KiKvaShadowMode, ecx
PAGELK:000000014099D165 jmp loc_14099D087
PAGELK:000000014099D165 KiEnableKvaShadowing endp

通过以上两个函数的分析,GDT、IDT 表在内存中的起始地址有如下关系,在中断处理中硬件压栈的 5 个参数(ErrorCode仅对8,17号中断压栈,但是在堆栈上空间预留)会保存在如下 IDT.base + 0x4200 - 8 开始的位置。

在中断处理函数,如函数 KiBreakpointTrapShadow 中会将中断时硬件压栈的这 5 个参数拷贝至新的堆栈中。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 先进行堆栈切换
KVASCODE:0000000140A122E5 mov rsp, gs:_KPCR.Prcb.RspBaseShadow
// 然后获取到 IDT.base
KVASCODE:0000000140A122F7 mov rsi, gs:_KPCR.IdtBase
// 找到硬件压栈的参数
KVASCODE:0000000140A12300 add rsi, 4200h
// 将硬件压栈的参数拷贝至新的堆栈中
// 因为 IDT.base + 0x4200 这个位置后面的中断还会再用到
KVASCODE:0000000140A12307 push qword ptr [rsi-8] // ss3
KVASCODE:0000000140A1230A push qword ptr [rsi-10h] // rsp3
KVASCODE:0000000140A1230D push qword ptr [rsi-18h] // rflags3
KVASCODE:0000000140A12310 push qword ptr [rsi-20h] // cs3
KVASCODE:0000000140A12313 push qword ptr [rsi-28h] //rip3

10.png

3.4 性能加速

一、支持 PCID 机制的 CPU(CR4.PCIDE = 1)

由于系统调用频繁地发生,UserDirectoryTableBaseKernelDirectoryTableBase 的切换会在一定程度上损耗性能。所以在支持PCID 并需要 KVA Shadow 的硬件上,Windows 内核使用两个不同的 PCID 值,内部称为 PCID_KERNEL 和 PCID_USER,从上面的分析可以看到:

  • PCID_KERNEL = 2,即 KiFlushPcid = 2,CR3[bit 63] = 1。
  • PCID_USER = 1,即 KiFlushPcid = 1,CR3[bit 63] = 1。

这样在模式切换后,先前模式下的 TLB、Page struct-cache、全局页面不用刷新。但是注意:不同进程的线程切换时,CPU 上的 TLB 必须刷新

二、不支持 PCID 机制的 CPU(CR4.PCIDE = 0)

不支持 PCID 机制的 CPU 上,也就没有 Page struct-cache。只能依靠 TLB 上的全局页面来进行性能加速。传统方式下,一般是共享的内核页面才更有可能作为全局页面地址转换信息缓存在 TLB 中,但是在支持 KVA Shadow 的系统中,而是将用户页面标记为全局页面

三、特权应用程序加速

当程序以管理员身份(特权级)运行时,只有一个 KernelDirectoryTableBase,不会使用到 UserDirectoryTableBase

ntoskrnl 中,切换上下文的函数 KiSwapContext 调用 SwapContext

KiKvaShadow = 1 下:

  • AddressPolicy > 0,特权权限程序 —> ShadowFlags = 1。

  • AddressPolicy = 0,一般权限程序 —> ShadowFlags = 3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SwapContext
.text:00000001403FFB96 mov rcx, [r14+_KPROCESS.DirectoryTableBase]
.text:00000001403FFB9A test cs:KiKvaShadow, 1
.text:00000001403FFBA1 jz short loc_1403FFBDC
.text:00000001403FFBA3 cli
.text:00000001403FFBA4 bt ecx, 1
.text:00000001403FFBA8 jnb short loc_1403FFBB6 // PCID = 2,内核CR3
.text:00000001403FFBAA bts rcx, 3Fh
.text:00000001403FFBAF or dword ptr [rbx+8E98h], 1
.text:00000001403FFBB6
.text:00000001403FFBB6 loc_1403FFBB6: // CODE XREF: SwapContext+258↑j
.text:00000001403FFBB6 mov [rbx+_KPRCB.KernelDirectoryTableBase], rcx // PCID = 2
.text:00000001403FFBBD btr rcx, 3Fh
.text:00000001403FFBC2 and [rbx+_KPRCB.ShadowFlags], 0FFFFFFFDh // ShadowFlags[bit 1] = 0
.text:00000001403FFBC9 bt dword ptr [r14+_KPROCESS.AddressPolicy], 0
.text:00000001403FFBD2 jnb short loc_1403FFBDB
.text:00000001403FFBD4 xor [rbx+_KPRCB.ShadowFlags], 3 // KiKvaShadow = 1 下:
.text:00000001403FFBD4 // AddressPolicy > 0 特权权限程序 ---> ShadowFlags = 1
.text:00000001403FFBD4 // AddressPolicy = 0 一般权限程序 ---> ShadowFlags = 3
.text:00000001403FFBDB
.text:00000001403FFBDB loc_1403FFBDB: // CODE XREF: SwapContext+282↑j
.text:00000001403FFBDB sti

参考:

4 CPU 推测执行漏洞

虽然 Meltdown(熔毁,CVE-2017-5754)、Spectre(幽灵,CVE-2017-5753&CVE-2017-5715)漏洞在发现时间上存在前后关系,但是在后来的研究以及漏洞缓解实施过程中,都将这些漏洞统称为 CPU 推测执行的变体(CPU Speculative execution Variant)。

参考:

变体(Variant) CVE 原理 缓解措施
1 CVE-2017-5753(Spectre) 在有分支判断的地方,代码编译为 JCC 条件跳转指令。攻击者可以通过循环训练 CPU 对跳转的训练,最后利用 CPU 来推测执行 JCC 的目的地。 软件缓解:Microsoft Visual C/C++ 编译器提供了一个编译选项 Qspectre,通过在潜在易受攻击的代码位置插入 lfence 序列化指令来启用缓解。
2 CVE-2017-5715(Spectre) 在有间接跳转或调用,如 jmp rax/jmp [rax]/call rax/call [rax]/ret间接分支时,CPU 利用 BTB、RSB 中的信息进行预测并跳转。攻击者可以污染 BTB、RSB 来修改跳转目的地,从而实施攻击。 软件缓解:
1、使用 Retpoline 将易受攻击的间接分支替换为非易受攻击的指令序列,强制 CPU 跳转到真实目的地而不是 BTB/RSB 建议的预测目标。
2、使用 lfence 指令(可选择)。
3、RSB 填充 + BTB 刷新。
硬件缓解:
1、IBPB:同一权限级别下,之前的跳转地址不会影响之后的跳转预测。
2、IBRS:内核权限代码不得使用分支预测器中的用户代码,用户权限代码也不得使用分支预测器中的内核代码。
3 CVE-2017-5754(Meltdown) 在指令中,如果前后指令不存在依赖关系,如果前面一条指令需要访问内存或导致异常时,CPU 有可能已经执行了乱序执行了后面的指令。攻击者可以利用瞬态指令来泄露内核数据。 软件缓解:KVA Shadow、lfence 指令(乱序之行)。

5 Spectre 缓解措施

5.1 lfence 指令

lfencelfence 前的指令必须在它之后指令前执行

该指令常用在分支预测 JCC 之后防止推测执行,在文《Bypassing KPTI Using the Speculative Behavior of the SWAPGS Instruction》中报告了集中利用 CPU 在 swapgs 指令执行时推测执行后面代码的漏洞,所以一般在 swapgs 指令前后都会有 lfence 指令。

如下代码,swapgs 必须在 bt 指令前执行。

1
2
3
KVASCODE:0000000140A122C7                 swapgs
KVASCODE:0000000140A122CA lfence
KVASCODE:0000000140A122CD bt gs:_KPCR.Prcb.ShadowFlags, 1

8.png

可以用来缓解 CPU 的推测执行,包括缓解熔断和幽灵漏洞,但是会有稍稍的性能影响。

5.2 RSB 刷新 + BTB 刷新(v2)

在有如 jmp rax/jmp [rax]/call rax/call [rax]/ret间接分支时,跳转地址在运行时才能确定。但是 CPU 中使用 BTB、RSB 两个结构来进行预测跳转。

  • 分支目标缓冲区(Branch Target Buffer,BTB):保存预测指令预测的跳转地址。处理器甚至可以在解码分支指令之前使用 BTB 来预测未来的代码地址。
  • 返回堆栈缓冲区(Return Stack Buffer,RSB):维护最近使用的调用堆栈返回地址的副本,专门用于 call/ret 指令。如果 RSB 中没有可用数据,不同的处理器将停止执行或使用 BTB 作为后备。

一、RSB 刷新:

因为 RSB 是一个 32 个槽的循环 buffer,因此只需要 32 个虚假的 call 指令就可以把整个 RSB 清理一遍。

在函数 KiSystemCall64Shadow 中就有使用(其中 add rsp, 8 用来模拟 ret):

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
KVASCODE:0000000140A14291 loc_140A14291:                          // CODE XREF: KiSystemCall64Shadow+FC↑j
KVASCODE:0000000140A14291 test edx, 2
KVASCODE:0000000140A14297 jz loc_140A143CC
KVASCODE:0000000140A1429D test byte ptr gs:279h, 4
KVASCODE:0000000140A142A6 jnz loc_140A143CC
KVASCODE:0000000140A142AC call loc_140A143BF
KVASCODE:0000000140A142B1
KVASCODE:0000000140A142B1 loc_140A142B1: // CODE XREF: KiSystemCall64Shadow+13E↓p
KVASCODE:0000000140A142B1 add rsp, 8
KVASCODE:0000000140A142B5 call loc_140A143C8
KVASCODE:0000000140A142BA
KVASCODE:0000000140A142BA loc_140A142BA: // CODE XREF: KiSystemCall64Shadow+147↓p
KVASCODE:0000000140A142BA add rsp, 8
KVASCODE:0000000140A142BE call loc_140A142B1
KVASCODE:0000000140A142C3
KVASCODE:0000000140A142C3 loc_140A142C3: // CODE XREF: KiSystemCall64Shadow+150↓p
KVASCODE:0000000140A142C3 add rsp, 8
KVASCODE:0000000140A142C7 call loc_140A142BA
KVASCODE:0000000140A142CC
KVASCODE:0000000140A142CC loc_140A142CC: // CODE XREF: KiSystemCall64Shadow+159↓p
KVASCODE:0000000140A142CC add rsp, 8
KVASCODE:0000000140A142D0 call loc_140A142C3
KVASCODE:0000000140A142D5
KVASCODE:0000000140A142D5 loc_140A142D5: // CODE XREF: KiSystemCall64Shadow+162↓p
KVASCODE:0000000140A142D5 add rsp, 8
KVASCODE:0000000140A142D9 call loc_140A142CC
KVASCODE:0000000140A142DE
KVASCODE:0000000140A142DE loc_140A142DE: // CODE XREF: KiSystemCall64Shadow+16B↓p
KVASCODE:0000000140A142DE add rsp, 8
KVASCODE:0000000140A142E2 call loc_140A142D5
KVASCODE:0000000140A142E7
KVASCODE:0000000140A142E7 loc_140A142E7: // CODE XREF: KiSystemCall64Shadow+174↓p
KVASCODE:0000000140A142E7 add rsp, 8
KVASCODE:0000000140A142EB call loc_140A142DE
KVASCODE:0000000140A142F0
KVASCODE:0000000140A142F0 loc_140A142F0: // CODE XREF: KiSystemCall64Shadow+17D↓p
KVASCODE:0000000140A142F0 add rsp, 8
KVASCODE:0000000140A142F4 call loc_140A142E7
KVASCODE:0000000140A142F9
KVASCODE:0000000140A142F9 loc_140A142F9: // CODE XREF: KiSystemCall64Shadow+186↓p
KVASCODE:0000000140A142F9 add rsp, 8
KVASCODE:0000000140A142FD call loc_140A142F0
KVASCODE:0000000140A14302
KVASCODE:0000000140A14302 loc_140A14302: // CODE XREF: KiSystemCall64Shadow+18F↓p
KVASCODE:0000000140A14302 add rsp, 8
KVASCODE:0000000140A14306 call loc_140A142F9
KVASCODE:0000000140A1430B
KVASCODE:0000000140A1430B loc_140A1430B: // CODE XREF: KiSystemCall64Shadow+198↓p
KVASCODE:0000000140A1430B add rsp, 8
KVASCODE:0000000140A1430F call loc_140A14302
KVASCODE:0000000140A14314
KVASCODE:0000000140A14314 loc_140A14314: // CODE XREF: KiSystemCall64Shadow+1A1↓p
KVASCODE:0000000140A14314 add rsp, 8
KVASCODE:0000000140A14318 call loc_140A1430B
KVASCODE:0000000140A1431D
KVASCODE:0000000140A1431D loc_140A1431D: // CODE XREF: KiSystemCall64Shadow+1AA↓p
KVASCODE:0000000140A1431D add rsp, 8
KVASCODE:0000000140A14321 call loc_140A14314
KVASCODE:0000000140A14326
KVASCODE:0000000140A14326 loc_140A14326: // CODE XREF: KiSystemCall64Shadow+1B3↓p
KVASCODE:0000000140A14326 add rsp, 8
KVASCODE:0000000140A1432A call loc_140A1431D
KVASCODE:0000000140A1432F
KVASCODE:0000000140A1432F loc_140A1432F: // CODE XREF: KiSystemCall64Shadow+1BC↓p
KVASCODE:0000000140A1432F add rsp, 8
KVASCODE:0000000140A14333 call loc_140A14326
KVASCODE:0000000140A14338
KVASCODE:0000000140A14338 loc_140A14338: // CODE XREF: KiSystemCall64Shadow+1C5↓p
KVASCODE:0000000140A14338 add rsp, 8
KVASCODE:0000000140A1433C call loc_140A1432F
KVASCODE:0000000140A14341
KVASCODE:0000000140A14341 loc_140A14341: // CODE XREF: KiSystemCall64Shadow+1CE↓p
KVASCODE:0000000140A14341 add rsp, 8
KVASCODE:0000000140A14345 call loc_140A14338
KVASCODE:0000000140A1434A
KVASCODE:0000000140A1434A loc_140A1434A: // CODE XREF: KiSystemCall64Shadow+1D7↓p
KVASCODE:0000000140A1434A add rsp, 8
KVASCODE:0000000140A1434E call loc_140A14341
KVASCODE:0000000140A14353
KVASCODE:0000000140A14353 loc_140A14353: // CODE XREF: KiSystemCall64Shadow+1E0↓p
KVASCODE:0000000140A14353 add rsp, 8
KVASCODE:0000000140A14357 call loc_140A1434A
KVASCODE:0000000140A1435C
KVASCODE:0000000140A1435C loc_140A1435C: // CODE XREF: KiSystemCall64Shadow+1E9↓p
KVASCODE:0000000140A1435C add rsp, 8
KVASCODE:0000000140A14360 call loc_140A14353
KVASCODE:0000000140A14365
KVASCODE:0000000140A14365 loc_140A14365: // CODE XREF: KiSystemCall64Shadow+1F2↓p
KVASCODE:0000000140A14365 add rsp, 8
KVASCODE:0000000140A14369 call loc_140A1435C
KVASCODE:0000000140A1436E
KVASCODE:0000000140A1436E loc_140A1436E: // CODE XREF: KiSystemCall64Shadow+1FB↓p
KVASCODE:0000000140A1436E add rsp, 8
KVASCODE:0000000140A14372 call loc_140A14365
KVASCODE:0000000140A14377
KVASCODE:0000000140A14377 loc_140A14377: // CODE XREF: KiSystemCall64Shadow+204↓p
KVASCODE:0000000140A14377 add rsp, 8
KVASCODE:0000000140A1437B call loc_140A1436E
KVASCODE:0000000140A14380
KVASCODE:0000000140A14380 loc_140A14380: // CODE XREF: KiSystemCall64Shadow+20D↓p
KVASCODE:0000000140A14380 add rsp, 8
KVASCODE:0000000140A14384 call loc_140A14377
KVASCODE:0000000140A14389
KVASCODE:0000000140A14389 loc_140A14389: // CODE XREF: KiSystemCall64Shadow+216↓p
KVASCODE:0000000140A14389 add rsp, 8
KVASCODE:0000000140A1438D call loc_140A14380
KVASCODE:0000000140A14392
KVASCODE:0000000140A14392 loc_140A14392: // CODE XREF: KiSystemCall64Shadow+21F↓p
KVASCODE:0000000140A14392 add rsp, 8
KVASCODE:0000000140A14396 call loc_140A14389
KVASCODE:0000000140A1439B
KVASCODE:0000000140A1439B loc_140A1439B: // CODE XREF: KiSystemCall64Shadow+228↓p
KVASCODE:0000000140A1439B add rsp, 8
KVASCODE:0000000140A1439F call loc_140A14392
KVASCODE:0000000140A143A4
KVASCODE:0000000140A143A4 loc_140A143A4: // CODE XREF: KiSystemCall64Shadow+231↓p
KVASCODE:0000000140A143A4 add rsp, 8
KVASCODE:0000000140A143A8 call loc_140A1439B
KVASCODE:0000000140A143AD
KVASCODE:0000000140A143AD loc_140A143AD: // CODE XREF: KiSystemCall64Shadow+23A↓p
KVASCODE:0000000140A143AD add rsp, 8
KVASCODE:0000000140A143B1 call loc_140A143A4
KVASCODE:0000000140A143B6
KVASCODE:0000000140A143B6 loc_140A143B6: // CODE XREF: KiSystemCall64Shadow+243↓p
KVASCODE:0000000140A143B6 add rsp, 8
KVASCODE:0000000140A143BA call loc_140A143AD
KVASCODE:0000000140A143BF
KVASCODE:0000000140A143BF loc_140A143BF: // CODE XREF: KiSystemCall64Shadow+12C↑p
KVASCODE:0000000140A143BF add rsp, 8
KVASCODE:0000000140A143C3 call loc_140A143B6
KVASCODE:0000000140A143C8
KVASCODE:0000000140A143C8 loc_140A143C8: // CODE XREF: KiSystemCall64Shadow+135↑p
KVASCODE:0000000140A143C8 add rsp, 8
KVASCODE:0000000140A143CC
KVASCODE:0000000140A143CC loc_140A143CC: // CODE XREF: KiSystemCall64Shadow+117↑j
KVASCODE:0000000140A143CC // KiSystemCall64Shadow+126↑j
KVASCODE:0000000140A143CC lfence
KVASCODE:0000000140A143CF
KVASCODE:0000000140A143CF loc_140A143CF: // CODE XREF: KiSystemCall64Shadow+10C↑j
KVASCODE:0000000140A143CF mov byte ptr gs:853h, 0
KVASCODE:0000000140A143D8 jmp KiSystemServiceUser
KVASCODE:0000000140A143DD // ---------------------------------------------------------------------------
KVASCODE:0000000140A143DD retn
KVASCODE:0000000140A143DD KiSystemCall64Shadow endp // sp-analysis failed

二、BTB 刷新

BTB 的槽从 1K 到 16K 不等,而且从虚拟地址到 BTB 索引的映射函数f(x)还不公开,因此要清除 BTB,需要首先使用逆向工程方法找到 f(x),然后根据 f(x) 找到 1k 到 16K 的虚拟地址来对应每个 BTB 的槽。最后发起 1K 到 16K 个虚假的 call/jmp 把所有 BTB 的槽清空(如下图)。需要指出,该方案的性能 overhead 很大。在内核代码中也没有看到这种大消耗的代码片段,所以只有在代码中尽量少使用间接跳转/调用(可以使用 Retpoline 技术缓解)。

9.png

参考:

Meltdown Spectre 攻击及缓解措施(一)

5.3 Retpoline 技术(v2)

该技术是 Google 提出的,以毒攻毒的解法。

两个示例,对于间接跳转 jmp r11/call r11

  1. jmp r11。使用 Retpoline 技术可以修改为:

    1
    2
    3
    4
    5
    6
    7
    call set_up_target;		(1)
    capture_spec: (4)
    pause;
    jmp capture_spec;
    set_up_target:
    mov qword ptr [rsp], r11; (2)
    ret; (3)

    由于(3)处的 ret 也是潜在的被攻击目标(基于RSB预测目标的注入),因此 indirect branch thunk 需要将解决方案闭环。

    • (1):是一个近跳转(near jmp),跳转目标在代码编译时硬编码写好的,不会使用到 BTB。然后将 capture_spec 压栈,同时(4)的地址也会压入 RSB 中。
    • (2):该指令访问内存不存在分支,也就不会进行预测执行。此时会将栈顶的返回地址修改为真正的跳转目标 r11,但这个操作不会改变 RSB 中的值。
    • (3):此时存在推测执行,推测执行的目的地是 capture_spec,推测执行的瞬态指令是一个死循环。当 ret 真正执行时,会跳转至 r11,然后丢弃推测执行的结果。(4)中没有访问内存(fetch指令不是read Memory),也就没有存高速缓存的过程。
  2. call r11。使用 Retpoline 技术可以修改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
      jmp set_up_return; (1)

    inner_indirect_branch:(3)
    call set_up_target; }
    capture_spec: }
    pause; }
    jmp capture_spec; } Indirect branch
    set_up_target: } sequence.
    mov qword ptr [rsp], r11; }
    ret; }

    set_up_return:
    call inner_indirect_branch (2)

    本质上,indirect call thunk 的内部实现还是 indirect branch thunk,只不过为了维持 call 指令的语义,必须增加(1)这样的跳转指令调到(2)处,将 call 指令真正的返回地址,即利用(2)将原先返回地址压入栈顶(原先call r11的返回地址),同时跳转到(3)处开始执行 indirect branch thunk。

将上面的解决方案进行封装:

先将 indirect thunk 封装:

1
2
3
4
5
6
7
8
retpoline_r11_trampoline:
call set_up_target
capture_spec:
pause
jmp capture_spec
set_up_target:
mov qword ptr [rsp], r11
ret

然后将 jmp r11/call r11 分别替换成 jmp/call retpoline_r11_trampoline 即可。

在函数 _guard_icall_handler__guard_retpoline_indirect_cfg_rax 中有使用到:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
RETPOL:0000000140A152A0 __guard_retpoline_indirect_cfg_rax proc near
RETPOL:0000000140A152A0 ; DATA XREF: .pdata:000000014012C714↑o
RETPOL:0000000140A152A0 ; __unwind { // __guard_retpoline_icall_handler
RETPOL:0000000140A152A0 mov r11, offset _guard_icall_bitmap
RETPOL:0000000140A152AA mov r11, [r11]
RETPOL:0000000140A152AD test rax, rax
RETPOL:0000000140A152B0 jge loc_140A15358
RETPOL:0000000140A152B6 test r11, r11
RETPOL:0000000140A152B9 jz short __guard_retpoline_indirect_rax
RETPOL:0000000140A152BB mov r10, rax
RETPOL:0000000140A152BE shr r10, 9
RETPOL:0000000140A152C2 mov r11, [r11+r10*8]
RETPOL:0000000140A152C6 mov r10, rax
RETPOL:0000000140A152C9 shr r10, 3
RETPOL:0000000140A152CD test al, 0Fh
RETPOL:0000000140A152CF jnz loc_140A15341
RETPOL:0000000140A152D5 bt r11, r10
RETPOL:0000000140A152D9 jnb loc_140A15358
RETPOL:0000000140A152DF nop
RETPOL:0000000140A152E0
RETPOL:0000000140A152E0 __guard_retpoline_indirect_rax: ; CODE XREF: __guard_retpoline_indirect_cfg_rax+19↑j
RETPOL:0000000140A152E0 ; __guard_retpoline_indirect_cfg_rax+B6↓j
RETPOL:0000000140A152E0 mov r11, offset _retpoline_image_bitmap
RETPOL:0000000140A152EA mov r11, [r11]
RETPOL:0000000140A152ED test r11, r11
RETPOL:0000000140A152F0 jz short loc_140A1533B
RETPOL:0000000140A152F2 mov r10, rax
RETPOL:0000000140A152F5 shr r10, 10h
RETPOL:0000000140A152F9 bt [r11], r10
RETPOL:0000000140A152FD jnb short loc_140A15325
RETPOL:0000000140A152FF call loc_140A15320
RETPOL:0000000140A15304 int 3 ; Trap to Debugger
RETPOL:0000000140A15304 ; ---------------------------------------------------------------------------
RETPOL:0000000140A15305 align 20h
RETPOL:0000000140A15320
RETPOL:0000000140A15320 loc_140A15320: ; CODE XREF: __guard_retpoline_indirect_cfg_rax+5F↑p
RETPOL:0000000140A15320 mov [rsp+0], rax
RETPOL:0000000140A15324 retn
RETPOL:0000000140A15325 ; ---------------------------------------------------------------------------
RETPOL:0000000140A15325
RETPOL:0000000140A15325 loc_140A15325: ; CODE XREF: __guard_retpoline_indirect_cfg_rax+5D↑j
RETPOL:0000000140A15325 or byte ptr gs:853h, 1
RETPOL:0000000140A1532E test byte ptr gs:853h, 2
RETPOL:0000000140A15337 jnz short loc_140A1533B
RETPOL:0000000140A15339 jmp short __guard_retpoline_exit_indirect_rax
RETPOL:0000000140A1533B ; ---------------------------------------------------------------------------
RETPOL:0000000140A1533B
RETPOL:0000000140A1533B loc_140A1533B: ; CODE XREF: __guard_retpoline_indirect_cfg_rax+50↑j
RETPOL:0000000140A1533B ; __guard_retpoline_indirect_cfg_rax+97↑j
RETPOL:0000000140A1533B lfence
RETPOL:0000000140A1533E jmp rax
RETPOL:0000000140A15341 ; ---------------------------------------------------------------------------
RETPOL:0000000140A15341
RETPOL:0000000140A15341 loc_140A15341: ; CODE XREF: __guard_retpoline_indirect_cfg_rax+2F↑j
RETPOL:0000000140A15341 btr r10, 0
RETPOL:0000000140A15346 bt r11, r10
RETPOL:0000000140A1534A jnb short loc_140A15358
RETPOL:0000000140A1534C or r10, 1
RETPOL:0000000140A15350 bt r11, r10
RETPOL:0000000140A15354 jnb short loc_140A15358
RETPOL:0000000140A15356 jmp short __guard_retpoline_indirect_rax
RETPOL:0000000140A15358 ; ---------------------------------------------------------------------------
RETPOL:0000000140A15358
RETPOL:0000000140A15358 loc_140A15358: ; CODE XREF: __guard_retpoline_indirect_cfg_rax+10↑j
RETPOL:0000000140A15358 ; __guard_retpoline_indirect_cfg_rax+39↑j ...
RETPOL:0000000140A15358 mov rcx, rax
RETPOL:0000000140A1535B mov rax, offset _guard_icall_bugcheck
RETPOL:0000000140A15365 call loc_140A15380
RETPOL:0000000140A1536A int 3 ; Trap to Debugger
RETPOL:0000000140A1536A ; ---------------------------------------------------------------------------
RETPOL:0000000140A1536B align 20h
RETPOL:0000000140A15380
RETPOL:0000000140A15380 loc_140A15380: ; CODE XREF: __guard_retpoline_indirect_cfg_rax+C5↑p
RETPOL:0000000140A15380 mov [rsp+0], rax
RETPOL:0000000140A15384 retn
RETPOL:0000000140A15384 ; } // starts at 140A152A0
RETPOL:0000000140A15384 __guard_retpoline_indirect_cfg_rax endp

参考:

深入分析Spectre变体2漏洞缓解方案:Google Retpoline构造技术

Retpoline: a software construct for preventing branch-target-injection

5.4 IBRS、IBPB

硬件缓解:

1、IBPB:同一权限级别下,之前的跳转地址不会影响之后的跳转预测。

2、IBRS:内核权限代码不得使用分支预测器中的用户代码,用户权限代码也不得使用分支预测器中的内核代码。

CPUID.(EAX=7H,ECX=0):EDX[26] = 1 则表示支持 IBRS 和 IBPB(需要在较新的 CPU 上)。其中 IBRS 位于 IA32_SPEC_CTRL[0](48H) MSR ,IBPB 位于 IA32_SPEC_CTRL[0](49H) MSR。

参考:

幽灵漏洞(二)

Speculative Execution Side Channel Mitigations—Revision 2.0

Speculative Execution Side Channel Mitigations—Revision 3.0

6 SMEP、SMAP、NX

CR4.SMEP(Supervisor Mode Executions Protection Enable)

  • SMEP = 1,在 R0 权限的程序不可以执行 R3 中 PTE.XD = 0 的页面上的代码。
  • SMEP = 0,在 R0 权限的程序可以执行 R3 中 PTE.XD = 0 的页面上的代码。

CR4.SMAP(Supervisor Mode Access Protection Enable)

  • SMEP = 1,在 R0 权限的程序不可以访问 R3 中 PTE.XD = 0 的页面上的代码。
  • SMEP = 0,在 R0 权限的程序可以访问 R3 中 PTE.XD = 0 的页面上的代码。
  1. 从用户模式构造中断门进入内核后,由于当前的 CR3 = UserDirectoryTableBase,但是 CPL = 0,默认情况下 CR4.SMEP = 1, CR4.SMAP = 1。提前在 3 环构造的函数在 UserDirectoryTableBase 下具有执行权限,也就是 PLM4E.NX | PDPTE.NX | PDE.NX | PTE.NX = 0
  2. 但是如果从 UserDirectoryTableBase 切换到 KernelDirectoryTableBase 后,该 CR3 下对用户空间的所有地址都是没有执行权限的。如下图。

3.png