\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 的方法可以。同时,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, [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,为了描述方便,这里用体系结构寄存器进行描述,实际的微处理器中,数据可存放在物理寄存器、保留站、ROB等结构。 \begin{table} \caption{分支中代码产生的 DIFT 行为} \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}{*}{mov eax, {[}rdx + rax{]}} & \multirow{2}{*}{eax <- {[}rdx + rax{]}} & (T{[}rdx{]} | T{[}rax{]}) = 1, 指令不安全 \tabularnewline \cline{3-3} & & T{[}rax{]} <- 1\tabularnewline \hline \end{tabular} \label{tab:spectre_dift} \centering \end{table} 以下描述这个检测方法的具体细节。 标记的设置:在推测式执行时,对于所有从内存读取数据的指令,读取的数据均 标记为1. 标记的传播:标记的传播在处理器的执行阶段进行,在这个阶段,处理器在读源 操作数的同时,将它们对应的标记读出。对于立即数和非流水线中产生的数据, 如直接从寄存器中读取的数据,标记均为0。对于不同类型的指令,标记传播的 方式如下: \begin{itemize} \item 对于算术类指令,它的源操作数均为寄存器操作数或立即数操作数,计算 结果的标记设为所有源寄存器操作数对应的数据的标记做或运算的结果。 \item 对于装载类指令,它从内存中读取数据,这是标记设置的基本条件,因此 计算结果的标志设为1. \item 对于存储类指令,它没有目的寄存器。和一般的 DIFT 实现不同,用于检 测 Spectre 组件中泄露数据的指令的 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]| 的值存在依赖关系。 因此这个 DIFT 方案需要控制指令中的标记。如果一条指令在被标记的控制指令之后执行, 则该指令和一个被标记的数据存在控制依赖关系,如果指令为装载指令,由于它改变 了缓存的状态,攻击者可以通过缓存信道得知该指令被执行过,推测出被标记转 移指令的执行结果,从而推测出内存中秘密数据的值,因此这样的装载指令也是 不安全的指令。 %这种检测方案的微体系结构实现如图\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} 中的基于缓存命中的过滤器基本相同。 本文使用的方案是使用 InvisiSpec 作为不安全的指令的执行机制,它可以保证 在执行装载指令时,无论它是否命中缓存,都可以获取数据,使得依赖于它的指 令可以继续执行。作为对比,本文同时评估了推迟指令执行的方式。 \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{安全执行装载指令方案的实现} \begin{figure}[htbp] \centering \includegraphics[width=0.8\textwidth]{load_exec.eps} \caption{Gem5 O3CPU 执行装载指令的过程} \label{fig:load_exec} \end{figure} 图 \ref{fig:load_exec} 是 Gem5 中执行一条装载指令的总体流程。 在执行阶段,处理器将装载指令传递至 LSQ,装载指令将会按序进入装载队列(LQ)。 Gem5 用指令系统特定的 initiatAcc 方法模拟该指令系统中装载指令访问存储系统的 方式。最后 LSQ 将装载指令的信息封装为一个请求,用 sendTimingReq 将这个 请求发送至存储系统。 发送至存储系统的读请求并不会立刻得到结果,需要等待来自存储系统的响应。 图 \ref{fig:load_exec} 的右半部分是处理器接收响应的流程, 在 recvTimingResp 后,处理器可以得到此前发送的请求对应的结果,根据得到 的结果,LSQ 执行回写操作,将结果写入至指令的目的寄存器,并将指令送至提 交阶段,完成指令的执行。 对于不安全指令的执行,可以为其增加一种 SpecLoad 请求,不同于普通的 Load 请求,SpecLoad 执行后,不会改变任何缓存状态和缓存一致性状态,因此不会留 下缓存的侧信道。 % 一致性协议 对于 InvisiSpec,在指令确认安全后,还需要向存储系统再发一个请求,以验 证 SpecLoad 读取的结果正确,或直接确认 SpecLoad 的结果,因此还需要再增 加一个 Expose 请求,用于确认或验证结果。 在 Gem5 中,多核系统的缓存和缓存一致性模型由 Ruby 存储系统模拟。本文模 拟一个带二级缓存的处理器,缓存一致性协议为 MESI,Gem5 中相关文件 为 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}。 在每一周期,更新流水线中每条指令的 PrevBrsResolved 状态。在 LSQ 中,对于每一个未完成的装载指令,根据指令 之前是否存在未决分支,判断指令是否安全,设定 ReadyToExpose 属性,取消 FenceDelay 标志。 执行单元执行一条装载指令时,根据 ReadyToExpose 属性判断是否需要用 InvisiSpec 等安全的方式执行。修改后的执行流程如图 \ref{fig:is-load}, LSQ 每周期都要将已转为安全指令的装载指令的验证或曝光,发送请求的 LSQUnit::read 过程需要支持 SpecLoad 请求的发送,对地址翻译的过程做一个 微小的修改,对 TLB 不命中的推测式执行的指令要延迟执行,防止产生 TLB 的 侧信道。 \begin{table}[htbp] \centering \caption{新增的指令状态} \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} \label{tab:inst_status} \end{table} \begin{figure}[htbp] \centering \includegraphics[width=0.6\textwidth]{is-load.eps} \caption{安全执行指令的流程} \label{fig:is-load} \end{figure} \subsection{动态信息流追踪的实现} 动态信息流追踪的实现分为标记设置、传播、清除和使用四个部分,以下分别介 绍它们在 Gem5 中的实现。 Gem5 的处理器模型使用显式寄存器重命名机制,Gem5 模拟指令执行后,结果均 存放在物理寄存器中,此后执行阶段将指令目的寄存器结果可用的信息,通知至 相关的指令。因此,在 Gem5 中模拟 DIFT,只需要为所有物理寄存器添加一个 对应的标记。 对所有推测式执行中的装载指令,其目的寄存器都需要设置标记。在扫描 LSQ 中指令的时候,指令前存在未决分支,则对这个指令的目的寄存器设置标记。 标志的传播在执行阶段进行。对于非访存类指令,如果指令有设置了标记的源寄 存器,则设置该指令的目的寄存器的标记。同时也设置指令的 IsTainted 属性, 它作为控制指令的标记。 指令的 IsTainted 属性和目的寄存器的标记在更新 ROB 中每条指令 的 PrevBrsResolved 状态的时候清除,这个过程同时更新指令 的 AfterTaintedBranch 属性。 标记的使用在更新 LSQ 中指令状态的时候发生,在加入动态信息流追踪的设计中, 指令的 ReadyToExpose 属性设定的条件有:(1)指令已设置 了 PrevBrsResolved 属性,或者(2)AfterTaintedBranch 未设置,并且源寄存 器均未被标记。 \section{小结} 本章前两节介绍本文设计的基于信息流追踪的 Spectre 组件的检测方法,讨论 了执行可能泄露数据的装载指令的几种方法。第三节介绍了在 Gem5 中实现这些 技术的方法,包括对 Gem5 流水线的分析,Gem5 装载指令执行流程的分析,对 执行流水线作出的修改,实现装载指令安全执行的方法。