利用Spectre(幽灵)实现Meltdown攻击

利用Spectre(幽灵)实现Meltdown攻击

Meltdown攻击通过利用Intel微处理器结构的设计缺陷,通过乱序执行时的计算痕迹建立信道,从而实现越界访问,进而攻击内核空间的地址。然而朴素的Meltdown攻击由于会频繁触发缺页错误,导致攻击程序提前退出,无法持续的对靶地址进行持续的访问。所以,常见的解决方式包括:

  • 使用自定义的SEGV处理函数,接住系统抛出的异常,从而不退出攻击程序
  • 利用Intel的TSX,建立事务同步扩展,在子线程里完成攻击,子线程退出后仍然不终止攻击

对这两种感兴趣的话可以看:

在这篇笔记中,主要介绍第三种处理异常退出攻击的方式:Spectre(幽灵)攻击。具体的说,通过处理器分支预测算法的设计缺陷,在错误的分支预测被系统发现之前的间隙,完成meltdown攻击。

分支预测

分支预测是Spectre攻击(准确的说是Spectre_v1)的核心。简单来说,为了提升性能,系统不会等到分支条件语句的计算结果得出后,再开始执行之后的代码。相反的,处理器会预测一个(或多个)分支的可能结果,并且提前准备它们的执行。如果随后验证发现预测正确,那么就节省了等待的时间,反之,如果预测失败,那么处理器会消除之前运行的痕迹,并且从头执行正确的指令,相反降低了效率。虽然分支预测算法有利有弊,但是实际中,由于用户的代码中的分支结果其实很容易被预测,比如for循环10000次,只会在最后一次分支的时候跳出循环,所以简单预测“不跳出循环”即可获得99.99%的准确度。诸如此类。也正是因为分支预测带来的性能提升,处理器公司不会选择主动放弃它带来的市场优势,即便在知道算法背后的安全隐患。

那么如何利用分支预测攻击呢?以下代码为例,secret和addr分别是攻击者的目标信息和目标地址。直接对地址进行访问(*addr)会触发系统警报,甚至可能直接退出程序,因为用户没有取得这个信息的权限。但是,如果我们对外层套一个分支的壳子作为伪装,情况就不一样了:

if false:
	*addr // addr = &secret;
  • 首先,处理器分支预测,猜测之后会执行(*addr)的命令,于是在还没有判断出if结果的情况下就提前执行了。
  • 随后,处理器得到false的结果,发现之前判断错误,于是自己偷偷删除了之前的运行过程,由于系统以为是自己犯的错,于是不会触发对于用户一开始是否“有权限执行该语句”的判断。
  • 最后,用户顺利浑水摸鱼的执行了一条有害指令。

总结起来,幽灵攻击的过程分为以下3步:

  1. 首先,诱骗系统执行恶意的分支
  2. 其次,在恶意的分支内运行有害代码
  3. 最后,确保在分支错误发现之前完成攻击

当然,尽管过程非常的简单易懂,但是想要高效稳定的建立信道,需要攻击者有足够的技术,使得以上的三步都可以顺利执行。如果处理器可以(1)识别出,并且避免不会发生的分支,或者(2)提前发现错误并且在攻击者取走信息之前关闭通道,那么攻击会立即失效。所以,我们的攻击需要针对这两点进行重点突破。

误导分支预测

目前我们的攻击(上述代码)其实显然并不会成功,原因之一就是系统不会傻到看不出if语句的结果。所以攻击者首先要做的就是提升分支预测的难度。比如以下的代码,当处理器在执行x的前n-1次分支时得到的结果都是true,那么会自然而然的觉得下一次结果也会进入循环,从而被我们误导。

for x in 0, ..., n:
	if x != n:
		...

利用这个技巧,我们只需要让前n-1次被执行的操作看上去无害,并且把有害的操作放到最后一次,如下。这样被处理器正常执行的语句都是无害的(&val),在诱导处理器犯错并自我修改时,攻击者偷偷的执行了最后一个有害的指令&secret

array = [&val, &val, ... &val, &secret]
for x in 0, ..., n:
	if x != n:
		*array[x]

加速有害指令

之前提到,如果在处理器修复错误之前,攻击者未能成功取到信息,那么将错过进攻机会。于是解决的方向在于,(1)如何可以让系统慢点发现问题,以及(2)如何快速取到信息。两个问题有统一的解决方案,那就是缓存。当处理器执行计算的时候,首先需要获取计算对应的输入。这个过程需要处理器向存储器提交读取指令。而缓存的存在,让这个过程可快可慢:如果读取的指令在缓存里,那么读取可以在60个时钟左右完成,反之,如果读取的指令不在缓存,而在内存,那么读取需要300个左右的时钟。相似的,尽管缓存的存在提供了一条信息泄露的渠道,厂商也不会因为安全问题而放弃它带来的巨大性能提升,由此失去市场。这样的取舍给了进攻者可乘之机。

flush(x)
flush(n)
cache(addr)
	if x != n:
		*addr

考虑以上代码,攻击者利用缓存创造时间差,将分支判断所需要的数据从缓存中删除flush,并且将攻击需要的地址提前读取至缓存cache,那么在计算分支结果之前,攻击者有大于300个时钟的间隔可以进行攻击,其中只需要花费60个读取目标,剩余的时间差即可用于如何将其偷偷运走。

利用Meltdown运走数据

虽然到目前为止攻击者已经可以成功的执行语句,但是简单的访问依然无法奏效,因为处理器会在发现后试图改正预测失败产生的副作用。这其中就包括寄存器状态,堆栈指针等等。但是,处理器的设计缺陷使得,并不是所有的痕迹都可以被轻松的擦除,其中最明显的就是缓存记录。由于清除缓存成本极高,不可能在每次分支后追踪哪些记录是新添加的,哪些记录是之前就存在的。于是通过对于缓存的攻击,黑客可以反向推断出处理器在改错过程中的行为。代码如下:

if x != n:
	array[*addr] // *addr = secret
for N in 0, ... 
	time array[N]

可以发现,攻击者并不直接取走得到的secret数值,而是用它作为序号,去敲击对应的缓存位置(array[secret])。通过这样的方法,即便攻击者没有在有限的时间差内完成攻击,也会在处理器内留下一条缓存记录。这样,即便之后处理器成功的恢复了寄存器状态,攻击者只需要对于所有缓存地址进行遍历,分析哪个地址在过去被访问过(被访问过的地址再次访问需要的时间减少),由此在随后取走所需信息。

在这里插入图片描述
最终,结合以上三步,攻击者即可在缓存记录中,分析出所需数据。如上图,红色箭头对应的缓存位置由于再次读取所需时间减少,说明最近被处理器访问过,并且留下了痕迹。

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VOlD-NULL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值