本文主要是介绍Spectre Attacks Exploiting Speculative Execution-1,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Spectre Attacks: Exploiting Speculative Execution
-
摘要:
- 推测式执行在如何执行方面是不可靠的,因为它可以访问受害者的内存和寄存器,并且可以执行具有可测量的副作用的操作
- 幽灵攻击:诱导受害者在执行在正确的程序执行过程中不会发生的推测性操作,并通过一个侧信道将受害者的敏感数据泄露出去
- 论文提出了一种实用的攻击方法,通过结合侧信道攻击,fault攻击和ROP攻击,可以从受害者进程中读取任意位置的数据
-
介绍:
-
论文提出的一些实验方法
- 利用推测执行:错误训练处理器的分支预测器,使其执行在正确执行过程中不会执行的代码,同时这些代码中包括一些获取敏感数据的操作,并且会将这些数据利用微结构的状态传递出来,最终实用侧信道攻击获取这些数据
- 利用本地的代码进行攻击:所有受害者程序的二进制文件和OS的共享库,寻找可以用于从受害者地址空间泄露信息的指令序列,然后编写攻击程序,利用CPU的推测执行,将之前发现的序列作为临时指令执行,从而读取整个受害者的内存地址空间
- 使用javascript攻击:通过可移植的javascript代码破坏浏览器沙箱
-
幽灵攻击的过程:攻击者首先在进程地址空间中找到一系列指令,这些指令能够充当隐蔽信道发射器,泄露受害者的敏感数据。然后攻击者欺骗CPU,使其猜测并错误的执行这个序列。最终攻击者通过隐蔽信道获取受害者的信息
-
欺骗CPU进行推测执行的方式:
-
利用条件分支指令:使得分支预测器错误的预测分支方向,然后CPU推测执行该方向上的代码,这些代码能够泄露敏感数据。
//x之前一直满足分支条件,突然给一个恶意的x,此时CPU会推测执行分支之后的操作 //需要保证在给出恶意的x之后,分支必须很慢做完,例如array1_size不在cache中等 if(x<array1_size)y=array2[array1[x]*256];
-
利用间接分支指令:主要借鉴ROP的思想,从受害者的地址空间中寻找一个小工具,并尽可能使得受害者推测执行该小工具。此时和ROP不同之处在于,幽灵使用的训练BTB来执行小工具。训练过程在攻击者的地址空间完成,用于训练的分支的目标虚拟地址和小工具的地址相同。
-
其它方式:更改推测执行的方式和泄露信息的方式。前者包括mistraining返回指令或者中断返回,后者可以是通过对ALU的竞争
-
-
幽灵可以攻击的硬件:Intel的(Ivy Bridge, Haswell, Skylake), AMD(Ryzen CPUs), Sumsung(ARM)
-
幽灵和熔断的不同:
- 熔断使用指令引发的tap而达到推测执行的目的,幽灵则是分支预测
- 熔断利用是Intel特权升级漏洞(推测式执行的指令能够跨过存储保护)
-
BTB:分支目标缓冲区,用于保存最近执行的分支指令到目标地址的映射。问题:处理器甚至可以在解码分支指令之间使用BTB来预测未来的跳转地址。
-
-
微结构的侧信道攻击
- 当多个程序同时或者通过分时在同一硬件上执行时,由一个程序的行为引起的微体系结构状态变化可能会影响到其它程序,从而可能导致从一个程序到另一个程序的信息泄露
- 已有的侧信道攻击可以利用BTB,分支历史,cache进行信息泄露
- Flush+reload和Evict+reload的不同:前者使用特定的机器指令,例如x86的clflush指令清除某个cache line或者数据;后者则是利用访问其它数据,强行将之前cache set中的数据替换出去
-
ROP攻击(return-oriented programming,返回导向编程)
- 利用缓存区溢出漏洞实现攻击
- 首先在受害者的二进制代码中找到称为 gadget(小工具)的机器代码片段(以返回指令结尾)来执行之后的工作,然后利用缓冲区溢出漏洞将 gadget 的一系列地址写入受害者程序栈
- 每个gadget在执行返回指令之前执行一些计算,然后返回指令从堆栈中获取返回地址,由攻击者控制该地址,从而跳到另一个gadget继续执行
-
攻击的整体过程
- 第一步:设置阶段
- 使用一些操作错误训练CPU,从而保证之后其可以进行推测执行
- 准备一些帮助工作,例如延迟分支中的判断
- 攻击者准备侧信道,例如使用flush或者evict完成cache的设置
- 第二步:CPU开始推测执行指令,将敏感数据从受害者上下文中传输到微架构的侧信道中
- 该过程可能是攻击者请求受害者的某个操作触发的(例如syscall,socket,file等)
- 在某些场景中,攻击者也可以使用自己的代码从自己的进程中获取敏感数据(例如沙箱的数据)
- 第三步:恢复敏感数据。使用flush+reload/evict+reload
- 幽灵假设推测执行的指令只会访问正常进程可以访问的数据,不会触发fault/exception。即使处理器不允许推测执行的指令访问内核数据,攻击仍然可以工作
- 第一步:设置阶段
-
利用条件分支指令的错误预测
if(x<array1_size)y=array2[array1[x]*256];
- 上述代码是一个函数中的代码(例如kernel中的syscall或者加密库中的函数),会接受一个无符号整型x,来源不一定可信
- 在推测执行期间,边界检查的条件分支可能会进入错误的路径(提前的分支训练)
- 当x和array1_size比较时,此时array1_size由于cache miss,需要从DRAM中获取,需要相当大的延迟
- 在这个过程中,分支预测器推测if为真,如果此时array1[x]能够快速命中,则之后的执行会出地址,然后将数据读入cache中(本来在DRAM中)
- 尽管之后,发现分支预测错误,但是cache的状态将不会回滚
- 在完成之前的步骤之后,攻击者需要检查缓存状态的变化,然后恢复秘密数据K(一个字节)。
- 如果array2是攻击者可以访问的,则遍历一遍即可
- 否则,使用prime-and-probe攻击,通过检测由于读取array2数据而导致的cache替换,推断出k
- 或者,攻击者立即再次调用目标函数,使用合法的x值,测量第二次调用的时间。通过多次测量该过程的时间变化来求解k
- 再或者寻找另一个推测指令序列,来判断array2[k*256]是否被缓存
-
利用间接分支实现幽灵攻击
- 间接分支跳转的优势:可以跳转到更多的地方,条件分支指令只能跳转到两个地方
- 如果由于cache miss而延迟了间接分支的目标地址确定,并且间接分支预测器被使用恶意的目标地址错误训练,则就可能会在攻击者选择的地址进行推测执行。此时即使没有可利用的条件分支错误预测的情况下,也会暴露受害者内存
- 攻击前提:当间接分支跳转发生时,攻击者可以控制两个寄存器的值(R1,R2)
- 攻击前提的一种实际情况:处理外部接收到的数据的函数内部进行函数调用,传入的数据(可以控制)在寄存器(R1,R2)中,寄存器(R1,R2)在调用函数时会被压入堆栈中,在结束时恢复。
- 攻击者首先在受害者的可执行内存(二进制代码或者库函数中)中寻找一个gadget。
- 一个gadget的示例:包含两条指令(不一定相邻),一条根据R1寄存器寻址内存,然后将结果加到R2中,另一条是根据R2的数据作为地址,访问内存。(对于某些gadget,攻击者控制单个寄存器、堆栈上的值或内存值就足够了)
- 这种攻击方式很类似于ROP,但是它不需要gadget正确的返回,因为推测错误最终会终止它的执行
- BTB mistraining实际测试:在intel x86处理器的一个超线程上执行代码能够错误训练分支预测器,使得另一个在同一个CPU的超线程也受到影响。在Skylake处理器上测试的结果表明,在相同的vCPU上的进程之间也可以相互影响分支预测器
- 一些攻击实现的硬件选择和OS选择:
- gadgets需要在受害者的可执行的内存范围内,否则无法进行推测执行
- 多个windows程序共享一个dll时,通常会加载一个副本并且映射到所有进程的相同虚拟地址上。几乎所有的windows程序包含的DLL,都包括足够搜索gadget的可执行代码
- 分支预测器只关注分支目标的虚拟地址,其它的类似于指令源地址,物理地址,时间和进程ID似乎无关紧要(意味着其它进程可以mistraining BTB)
- 分支预测器只需要使用虚拟地址的低若干位进行索引,因此攻击者并不需要一定使用固定的一样的分支地址进行训练。同时低0-15位并不会被ASLR影响
- 在攻击者中训练分支预测器,尽管由于跳转到非法目的地址会引发异常,但是可以简单的捕获异常,并不会终止程序等
- 对于分支预测器的错误训练影响无法传递到其它的CPU,即分支预测器在每个CPU上是独立的
- DLL的代码和常量数据区域可以被任何使用DLL的进程读取和刷新,意味着可以使用这些区域作为flush+probe攻击的探测表(类似于array2)
- DLL使用了copy-on-write机制,即本来共享的DLL,如果某个进程需要写DLL,则会将其复制一份,然后再写,并且该进程只可见复制之后的副本,其它进程仍旧看到未修改之前的DLL
-
在Windows上实现利用BTB的幽灵攻击
-
一个简单的受害者程序:
- 生成一个随机键值,然后死循环调用{Sleep(0), load文件的前几个字节(文件头),调用windows的cryto函数(加密使用)计算SHA-1哈希值,最后打印hash值,如果文件头发生变化,则打印出hash值}
- 在优化编译此程序的时候,会使用寄存器ebx,edi中的文件数据(load的数据)调用Sleep函数
-
寻找一些共享DLL的memory,用于之后的flush-and-probe检测使用
-
寻找gadget,可以推测执行使用ebx和edi,这两个值可以被攻击者控制
-
寻找到的结果(ntdll.dll中)
adc edi,dword ptr [ebx+edx+13BE13BDh] adc dl,byte ptr [edi]
-
当攻击者设定ebx=m - 0x13BE13BD - edx (edx=3),第一条指令会从m处读取32位值,将加到edi上(不考虑进位,提前被清除了)
-
攻击者控制edi的值,第二条指令会利用计算的地址将数据加载到cache中。因为edi已知,因此最终判断cache中是否有该地址范围的数据即可
-
-
错误训练sleep函数的第一条指令:jmp dword ptr ds:[76AE0078h] ,使其可以跳转到gadget的位置。同时需要保证指令中原本的目的地址没有被放到cache中
- 将带有跳转指令的内存页设置为可写的状态(copy-on-write),然后将跳转的目的地址修改为gadget的地址。然后将包含gadget的内存页也设置为可写状态,然后在gadget指令序列结尾增加ret指令,以返回跳转指令的地方
- 使用一个单独的线程,返回清除受害者包含跳转指令的目的地址的内存地址
- 在mistraining的线程中,会有一个循环,不断将映射地址(gadget的地址)压入堆栈,然后初始化时设置的返回指令将会根据堆栈的内容跳转到gadget,然后从gadget的返回指令再跳转回来。训练使用的返回指令被初始化在一个1MB的可执行区域中(20位,可能是为了ASLR带来的影响)。通过这样的训练,BTB则会被错误训练指向gadget的地址。
-
-
幽灵的其它变种
-
使用Evict+time带来flush+reload
if (false but mispredicts as true)read array1[R1] read [R2]
- 测量操作执行的时间,并且这种操作依赖于cache的状态
- 在上述的代码中,R1是敏感数据,如果array1[R1]在cache中,则读取R2地址的操作将会很快完成,如果不在cache中,则读取R2的操作将会更慢。因此可以通过测量这些操作的时间,来判断R1是哪一个(即cache中只包括一个)
- 这种方式,如果推测执行无法修改cache的状态,也仍旧可以泄露数据
-
指令执行的时间(instruction timing)
-
某些指令的执行时间依赖于操作数,这些指令就可以用于泄露数据。在代码中,推测执行中使用了乘法器计算R1*R2,通过测量之后乘法器多久后可用的时间,来揭示R1和R2的信息
if (false but mispredicts as true)multiply R1, R2 multiply R3, R4
-
-
寄存器堆的争用:使用CPU中用于保存检查点的寄存器的变化来泄露数据。在下面的代码中,如果第二条if为真,则需要在多增加一个检查点,如果为假,则不需要。攻击者通过检查检查点存储的变化来判断R1的值
if (false but mispredicts as true)if (condition on R1)if (condition)
-
-
防御措施:
- 串行化指令,但是不是针对所有处理器或者系统配置的有效对策。可以在编译的时候使用静态分析,增加FENCE指令,但是问题在于可能无法全面的阻止(如果全部插入,性能会下降很多),同时代码也需要重新编译
- 为了降低间接分支带来的问题,在上下文切换期间,禁用超线程和刷新分支预测器状态(似乎没有体系结构定义这种方法)。但是无法解决所有问题
- 限制cache的对策并不是很充分,因为在推测执行的过程中也可能会泄露信息。此时需要考虑来自内存总线的竞争,DRAM行地址选择状态,虚拟寄存器的可用性(例如保存检查点的寄存器),ALU的可用性,以及分支预测器本身的状态所带来的时间影响。除此之外,还包括power等传统侧信道的影响因素
- AMD表示,Ryzen处理器有一个人工智能神经网络,可以根据过去的运行行为推测未来的程序路径,这意味着更加复杂的推测行为
这篇关于Spectre Attacks Exploiting Speculative Execution-1的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!