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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
|
\chapter{针对 Spectre 攻击的微体系结构设计}\label{sec:mywork}
本章讲解本文提出的一种防御 Spectre 攻击的方法,该方法使用动态信息流追
踪的方法,检测 Spectre 组件指令流中可能泄露秘密数据的访存指令,并使用
一种安全的方法执行这些访存指令。
这种微体系结构设计不需要软件的支持,未修改的软件和操作系统可以直接在采用这
种微体系结构改进的处理器中执行。
\section{威胁模型}
本文对攻击者做如下假设:攻击者知道受害者程序的代码、进程中的地址分布信
息,攻击者和受害者可以在同一进程,共享同一处理器核,或在不同处理器核上。
本文设计的方法用于防御利用控制流推测式执行的 Spectre-PHT, Spectre-BTB,
Spectre-RSB 攻击,此方法可以扩展至 Spectre-STL 攻击。攻击者在 Spectre
攻击中使用高速缓存作为隐蔽信道,其他信道不在本文的考虑范围。本文不考虑
其他的侧信道攻击。本文针对攻击者通过 Spectre 攻击获取内存中秘密数据的
情形,不考虑攻击者通过攻击获取寄存器中秘密数据的情形。
\section{基于动态信息流追踪的 Spectre 检测方法}
动态信息流追踪(Dynamic Information Flow Tracking)\supercite{dift}是
一种硬件安全策略,通过识别可疑的信息流,并限制可疑信息的使用,保护程序
的安全。它最早用于防止攻击者利用缓冲区溢出攻击执行恶意代码,也可以用于
检测跨站脚本攻击、SQL注入等攻击。\supercite{raksha}
DIFT 可以作为 Spectre 攻击的检测手段之一。Spectre 的论文中指出处理器可
以追踪数据是否在推测式执行中获取,进而阻止在后续可能泄露这个数据的操作
中使用,作为阻止数据进入隐蔽信道的方法\supercite{spectre},本文所用的方
法则是这种观点的一种具体设计。在其他使用了 DIFT 的防御 Spectre 的设计中,
所追踪的对象有所不同。CSF\supercite{context-sensitive-fencing} 中的译码
级信息流追踪框架 DIFT,用于追踪处理器使用的数据是否来源于用户输入,从而
处理器可以根据此信息判断是否需要插入 fence 微码。有的指令系
统\supercite{oisa}在定义中即包含了 DIFT 技术,用于追踪一个数据是否为秘
密数据。
和 Conditional Speculation
\supercite{conditional-speculation} 的 TPBuf 过滤器一样,本文的工作同样
实现了一个用于识别一条指令是否泄露数据的方法。和 TPBuf 不同的是,基
于DIFT 的方法并不检测不同的访存指令之间的地址关系,而是直接寻找读取数据
的指令和泄露数据的指令之间直接或间接的依赖关系。TPBuf 过滤器不能防范非
共享内存区域的缓存侧信道攻击,但是基于 DIFT 的方法可以。
而SpectreGuard\supercite{spectreguard} 中可以不依赖软件实现
的 SG(Full)方案,目的也是阻止推测式执行中从存储系统读取的数据不被泄露,
方法是直接禁止读出的数据被后续的推测式执行的指令使用,本文的方法允许这
样的数据被使用,但是使用这个数据的指令需要用不产生侧信道的方式执行。
在本文的工作中,DIFT 的作用是在运行时动态识别可能泄露秘密数据的指令。以
图\ref{lst:spectre_v1}的 Spectre v1 组件代码为例。攻击者希望通
过 Spectre攻击泄露 \verb|array1[x]| 的值,方法是让处理器在分支的推测式
执行中,访问 \verb|array2[array1[x] * 4096]|,从而将一个依赖
于 \verb|array1[x]| 的地址处的数据写入了缓存中。使用了基于 DIFT 的检测
手段后,可以识别出 \verb|array2[array1[x] * 4096]| 的地址依赖
于 \verb|array1[x]| 的值,从而处理器可以阻止对这个地址的访问。
为了达到这个目的,我们为所有的物理寄存器都添加一个标记,用于表示它的值
是否来源于推测式执行中,从内存中读出的值。在上述例子中,有一条将内
存 \verb|array1[x]| 读取至寄存器的指令,该指令的目的寄存器的标记将会被
设为1.
\begin{figure}
\begin{minted}[frame=single,linenos=true]{nasm}
xor eax, eax
cmp qword [rip + 0x2b157f], rdi
jbe loc.funcret
lea rax, [rip + 0x2b14ae]
add rax, rdi
movzx eax, byte [rax]
shl eax, 12
lea rdx, [rip + 0x2b425d]
mov eax, dword [rdx + rax]
loc.funcret: ret
\end{minted}
\caption{Spectre 组件的汇编代码}
\label{lst:spectre_v1_asm}
\centering
\end{figure}
上述 Spectre v1 的组件代码可以产生图\ref{lst:spectre_v1_asm}所示的指令。
我们用表\ref{tab:spectre_dift}展示 DIFT 在推测式执行分支中的指令的行为,
其中 T 表示寄存器的标记,在分支推测式执行前,所有寄存器的标记为0,为了
描述方便,这里用体系结构寄存器进行描述,实现中为物理寄存器的标记。
\begin{table}
\begin{tabular}{|c|c|c|}
\hline
指令 & 指令的语义 & DIFT 行为\tabularnewline
\hline
\hline
lea rax, {[}rip + 0x2b14ae{]} & rax <- rip + 0x2b14ae & T{[}rax{]} <- T{[}rip{]} = 0\tabularnewline
\hline
add rax, rdi & rax <- rax + rdi & T{[}rax{]} <- T{[}rax{]} | T{[}rdi{]} = 0\tabularnewline
\hline
\multirow{2}{*}{movzx eax, byte {[}rax{]}} & \multirow{2}{*}{eax <- (uint8\_t){[}eax{]}} & T{[}rax{]} = 0, 指令安全\tabularnewline
\cline{3-3}
& & T{[}rax{]} <- 1\tabularnewline
\hline
shl eax, 12 & eax <- eax {*} 4096 & T{[}rax{]} <- T{[}rax{]} = 1\tabularnewline
\hline
lea rdx, {[}rip + 0x2b425d{]} & rdx <- rip + 0x2b425d & T{[}rdx{]} <- T{[}rip{]} = 0\tabularnewline
\hline
\multirow{2}{*}{eax, dword {[}rdx + rax{]}} & \multirow{2}{*}{eax <- {[}rdx + rax{]}} & (T{[}rdx{]} | T{[}rax{]}) = 1, 指令不安全 \tabularnewline
\cline{3-3}
& & T{[}rax{]} <- 1\tabularnewline
\hline
\end{tabular}
\caption{分支中代码产生的 DIFT 行为}
\label{tab:spectre_dift}
\centering
\end{table}
以下描述这个检测方法的具体细节。
标记的设置:在推测式执行时,对于所有从内存读取数据的指令,将其所有目的
寄存器的标记设为1.
标记的传播:标记的传播在处理器的执行阶段进行,在这个阶段,处理器在读源
寄存器的同时,将它们对应的标记读出。对于不同类型的指令,标记传播的方式
如下:
\begin{itemize}
\item 对于算术类指令,它的源操作数均来源于寄存器,其目的寄存器的标记设
为所有源寄存器标记做或运算的结果。
\item 对于装载类指令,它从内存中读取数据,这是标记设置的基本条件,因此
目的寄存器的标志设为1.
\item 对于存储类指令,它没有目的寄存器。和一般的 DIFT 实现不同,我们不
对任何内存进行标记。因为要使用存入内存中的数据,需要有一套指令将其重
新从内存中读出,这个操作会设置这条指令目的寄存器的标记。
\item 对于转移类指令,我们为每个指令添加一位标记,如果源寄存器中有标记
为1的指令,则设置这个指令的标记为1,否则设置为0.转移指令的的标记将在
下文描述。
\end{itemize}
标记的清除:当一条指令不再处于推测式执行的状态,即该指令此前的所有分支
都执行完成并通过验证,则要将这条指令所有目的寄存器的标记重置为0.对于转
移类指令,则清除该转移指令的标记。
寄存器中的标记可以处理读取内存中数据的指令,和泄露内存中数据的指令,存
在直接或间接数据相关的情形。读取内存的指令使得存放它的寄存器被标记,此
后和它存在直接或间接数据相关的指令,会将这个标记传播至这些指令的目的寄
存器。
考虑攻击者在 Spectre 攻击中利用缓存侧信道泄露内存中的数据,则
在 Spectre 的组件代码中,泄露数据的指令为装载指令,该装载指令的地址直接
或间接依赖于内存中的数据。在加入上述的 DIFT 检测机制后,由于该装载指令
依赖于内存中的数据,它必定有一个被标记的源寄存器,从而处理器可以得知该
指令不安全。
转移指令的标记用于处理可能泄露数据的指令,和内存中数据的值存在控制相关
的情形,如图\ref{lst:victim_v10}的例子\supercite{msvc}:
\begin{figure}[htbp]
\begin{minted}[frame=single,linenos=true]{C}
void victim(size_t x, uint8_t k) {
if (x < array1_size) {
if (array1[x] == k)
temp &= array2[0];
}
}
\end{minted}
\caption{泄露数据的指令和内存中的数据存在控制相关的情形}
\label{lst:victim_v10}
\centering
\end{figure}
这个例子读取的 \verb|array1[x]| 和攻击者猜测的值 \verb|k| 进行比较,如
果两个值相等,则会访问 \verb|array2[0]|,即访问 \verb|array2[0]| 的指令
和访问 \verb|array1[x]| 的指令存在控制相关,攻击者可以通过探
测\verb|array2[0]| 是否在缓存中,判断 \verb|array1[x]| 的值是否
和\verb|k| 相等。只使用寄存器的标记,并不能检测出读取 \verb|array2[0]|
的指令和 \verb|array1[x]| 的值存在依赖关系。
因此我们需要转移指令中的标记。如果一条指令在被标记的转移指令之后执行,
则该指令和一个被标记的数据存在依赖关系,如果指令为装载指令,由于它改变
了缓存的状态,攻击者可以通过缓存信道得知该指令被执行过,推测出被标记转
移指令的执行结果,从而推测出内存中秘密数据的值,因此这样的装载指令也是
不安全的指令。
这种检测方案的微体系结构实现如图\ref{fig:spectre_dift}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.8\textwidth]{spectre_dift.eps}
\caption{基于 DIFT 的 Spectre 检测方法的微体系结构示意图}
\label{fig:spectre_dift}
\end{figure}
\section{不安全指令的执行方式}
对于检测到的不安全的指令,最简单的方式是阻止它的执行,直到指令因推测式
执行错误回卷,或确认为安全状态,再重新调度至执行单元执行。这种做法会导
致推测式执行中,后续所有依赖于这条指令的指令都会被推迟,降低流水线的利
用率,导致性能下降。
一种方法是使用 InvisiSpec \supercite{invisispec},和 Spectre 攻击的检测
机制结合后,可以作为单条不安全指令的执行机制。它将不安全指令从内存中读
到的数据放入推测式执行缓冲区中,后续的指令可以使用这条指令的执行结果,
确保指令流的继续执行。在推测式执行错误时,推测式执行缓冲区的内容会被丢
弃,缓存状态不会改变,因此不会产生侧信道。推测式执行被验证为安全时,缓
冲区中的数据将会更新至存储系统。为类确保不发生访存违例 InvisiSpec 需要
在确认指令安全后,对读到的数据做一次验证,因此需要再次访问存储系统。
由于缓存命中的指令不会改变缓存的内容,因此还有一种方法是在推测式执行时,
如果一级缓存命中,则允许该指令继续执行,否则中止该指令的执行,等待指令
被回卷或转为安全的指令。如果在推测式执行时,缓存命中率高,则这种方案不
会阻止后续指令的执行,如果程序在推测式执行中缓存命中率低,则性能损失较
大。这种方法和 Conditional Speculation
\supercite{conditional-speculation} 中的基于缓存命中的过滤器基本相同。
\section{针对 Spectre 攻击的微体系结构在 gem5 中的实现}
以下介绍这种可抵抗 Spectre 攻击的微体系结构在 gem5 模拟器中的实现。首先分
析 gem5 中乱序执行处理器的实现,然后分别介绍 InvisiSpec 和本文使用的
DIFT 方案在 gem5 中的实现。
\subsection{gem5 的乱序执行处理器}
%\Todo: 做一个 gem5 流水线的示意图?
gem5 的乱序执行处理器实现在 FullO3CPU 类中,它又用类实现类处理器的以下
流水级:取指(Fetch)、译码(Decode)、重命名(Rename)、发射/执行/回
写(IEW)、提交(Commit)。
gem5 的取指和译码阶段由 DefaultFetch 和 DefaultDecode 两个类实现。在
DefaultFetch 中,取指部件从指令缓存中取出处理器 PC 位置的指令,并用指
令系统对应的译码器进行译码,再取出指令对应的微指令,将微指令传至译码阶
段,译码阶段再将其传到重命名阶段。取指阶段取出的指令在 DynInst 类的实
例中保存。
gem5 的重命名阶段由 DefaultRename 类实现,它对指令的源寄存器和目的寄存
器进行重命名。重命名后,指令的 DynInst 实例中的源寄存器和目的寄存器均
保存它们对应的物理寄存器,同时还保存目的寄存器原来对应的物理寄存器用于
恢复。
gem5 的发射、执行和回写三个阶段由一个类 DefaultIEW 实现,它模拟了处理
器将指令发射至功能单元和处理器执行指令的过程。gem5 中用一个专门的语言
定义了每个指令系统的指令的语义,为每个指令和微指令生成一个 StaticInst
类,里面定义了指令的执行方式。对于存储访问类指令,gem5 用 LSQ 类定义处
理器中的装载和存储指令队列,这些指令在执行时添加至队列中,进行存储访问
操作。
gem5 的提交阶段由 DefaultCommit 类实现,它提交 ROB 队列头部的指令,更
新 ROB 的状态。
\subsection{安全执行装载指令方案的实现}
图\ref{fig:load_exec}是 gem5 中执行一条装载指令的总体流程。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.8\textwidth]{load_exec.eps}
\caption{gem5 O3CPU 执行装载指令的过程}
\label{fig:load_exec}
\end{figure}
在执行阶段,处理器将装载指令传递至 LSQ,装载指令将会按序进入 LQ. gem5
用指令系统特定的 initiatAcc 方法模拟该指令系统中装载指令访问存储系统的
方式。最后 LSQ 将装载指令的信息封装为一个请求,用 sendTimingReq 将这个
请求发送至存储系统。
发送至存储系统的读请求并不会立刻得到结果,需要等待来自存储系统的响应。
图\ref{fig:load_exec}的右半部分是处理器接收响应的流程,
在 recvTimingResp 后,处理器可以得到此前发送的请求对应的结果,根据得到
的结果,LSQ 执行回写操作,将结果写入至指令的目的寄存器,并将指令送至提
交阶段,完成指令的执行。
对于不安全指令的执行,可以为其增加一种 SpecLoad 请求,不同于普通的Load
请求,SpecLoad 执行后,不会改变任何缓存状态和缓存一致性状态,因此不会留
下缓存的侧信道。
% 一致性协议
对于 InvisiSpec,在指令确认安全后,还需要向存储系统再发一个请求,以验
证 SpecLoad 读取的结果正确,或直接确认 SpecLoad 的结果,因此还需要再增
加一个 Expose 请求,用于确认或验证结果。
在 gem5 中,多核系统的缓存和缓存一致性模型由 Ruby 存储系统模拟。本文模
拟一个带二级缓存的处理器,缓存一致性协议为 MESI,需要修改的相关文件
为 src/mem/protocol 下以 MESI\_Two\_Level 开头的文件。
对于一级缓存,为了增加对不安全内存读取的支持,我们修改状态机文
件 MESI\_Two\_Level-L1cache.sm. 除了添加 SpecLoad 和 Expose 两个请求外,
还要处理从其他处理器转发的响应的请求。此外,在处理器的实现中,状态机接
收到处理器的请求,到从存储系统得到数据之间,状态机处于一个过渡状态,因
此在状态机中添加一个状态 IX,表示接收了 SpecLoad 之后缓存缺失,向下级存
储系统发送读请求,还未得到响应。
InvisiSpec 中,一级缓存处理 SpecLoad 和 Expose 的部分状态如
图\ref{fig:invisispec_mesi}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.8\textwidth]{invisispec_mesi.eps}
\caption{InvisiSpec 一级缓存处理 SpecLoad 和 Expose 请求}
\label{fig:invisispec_mesi}
\end{figure}
其中,对于任意一种状态,接收到 Expose 请求后,处理器都会更新缓存状态,
将指令从存储系统中读到的数据写入缓存。对于 SpecLoad 缓存命中的情
形,spec\_load\_hit 取出缓存行,返回至处理器,和 load\_hit 的区别是推测
式执行中访问缓存,不会更新缓存替换算法的状态,避免产生侧信道信息。
% hit based
% 而对于只在缓存命中时执行装载指令的方案,设计相对简单。它只需要处理
% SpecLoad 请求。
% 在这种情形下,不需要添加额外状态。缓存行在任一状态下,接收 SpecLoad 时
% 状态不变。缓存命中时,则执行 spec\_load\_hit 将缓存行中的内容返回至处
% 理器。
% 而在缓存缺失时,则在其中添加一个 spec\_load\_miss 操作,它向处理器返回
% 一个缓存缺失的响应。LSQ 在写回的过程中处理这个响应,为其对应的缓存缺失
% 的指令清除表\ref{tab:inst_status}中的 ReadyToExpose 属性,并通知指令队列需
% 要推迟这条访存指令的执行。
\subsection{执行流水线的修改}
我们在 DynInst 类为指令添加新的状态和属性,添加的主要的指令状态和属性
见表\ref{tab:inst_status}。
\begin{table}
\begin{tabular}{|p{0.25\textwidth}|p{0.75\textwidth}|}
\hline
指令状态和属性 & 含义\tabularnewline
\hline
\hline
PrevBrsResolved & 记录指令之前的分支是否得出结果\tabularnewline
\hline
IsTainted & 在基于 DIFT 的检测方案中,记录指令是否依赖于被标记的寄存器\tabularnewline
\hline
AfterTaintedBranch & 在基于 DIFT 的检测方案中,记录指令是否在被标记的转
移指令之后\tabularnewline
\hline
L1Hit & 记录指令的 SpecLoad 请求是否命中一级缓存并且缓存内容不变,如果
是,则不需要发送验证请求\tabularnewline
\hline
ExposeSent & 已发送 Expose 请求\tabularnewline
\hline
ExposeCompleted & Expose 已执行完成\tabularnewline
\hline
ValidationCompleted & 数据已验证完成\tabularnewline
\hline
NeedPostFetch & 需要再次发送 Expose 请求\tabularnewline
\hline
ReadyToExpose & 指令已确认安全,可以发送 Expose 请求,或直接用基本的方
式执行指令\tabularnewline
\hline
NeedExposeOnly & 只需要发送 Expose,而不需要通过再次读取数据用于验证\tabularnewline
\hline
FenceDelay & 指令被阻止执行,直到确认安全,在 InvisiSpec 之外的执行方
案中使用\tabularnewline
\hline
\end{tabular}
\caption{新增的指令状态}
\label{tab:inst_status}
\centering
\end{table}
在每一周期,处理器扫描 ROB 中的每条指令,更新流水线中每条指令的
PrevBrsResolved 状态。在 LSQ 中,对于每一个未完成的装载指令,根据指令
之前是否存在未决分支,判断指令是否安全,设定 ReadyToExpose 属性,取消
FenceDelay 标志。
\subsection{动态信息流追踪的实现}
动态信息流追踪的实现分为标记设置、传播、清除和使用四个部分,以下分别介
绍它们在 gem5 中的实现。
对所有推测式执行中的装载指令,其目的寄存器都需要设置标记。在扫描 LSQ
中指令的时候,指令前存在未决分支,则对这个指令的目的寄存器设置标记。
标志的传播在执行阶段进行。对于非访存类指令,如果指令有设置了标记的源寄
存器,则设置该指令的目的寄存器的标记。同时也设置指令的 IsTainted 属性,
它作为转移指令的标记。
指令的 IsTainted 属性和目的寄存器的标记在更新 ROB 中每条指令
的 PrevBrsResolved 状态的时候清除,这个过程同时更新指令
的 AfterTaintedBranch 属性。
标记的使用在更新 LSQ 中指令状态的时候发生,在加入动态信息流追踪的设计中,
指令的 ReadyToExpose 属性设定的条件有:(1)指令已设置
了 PrevBrsResolved 属性,或者(2)AfterTaintedBranch 未设置,并且源寄存
器均未被标记。
\section{小结}
本章前两节介绍本文设计的基于信息流追踪的 Spectre 组件的检测方法,讨论
了执行可能泄露数据的装载指令的几种方法。第三节介绍了在 gem5 中实现这些
技术的方法,包括对 gem5 流水线的分析,gem5 装载指令执行流程的分析,对
执行流水线作出的修改,实现装载指令安全执行的方法。
|