您的位置:首页 > 其它

Meltdown与Spectre:处理器漏洞揭秘

2018-01-10 08:17 141 查看
最近披露的处理器重大安全漏洞,使得芯片巨头Intel遭受重大打击。而且漏洞不止一个,一个有两个:一个叫熔断(Meltdown),只涉及Intel处理器;一个叫幽灵(Spectre),影响Intel,AMD以及ARM处理器。这两个漏洞可能造成的后果很严重,给人们的教训也很深刻,我们在无限制追求芯片性能的同时也要反思一下,性能和安全性,到底谁才是最重要的?

来龙去脉



处理器上有许多运算和执行单元,分别完成加减乘除,移位,逻辑,测试和比较,地址生成和访问等各种功能。现代处理器为了充分利用这些资源,提高并行性能,采用了一种叫推测执行(Speculative Execution)的技术。具体说,在执行当前指令时,如果发现处理器上还有闲置资源可以使用,便“自作主张”尝试提前执行部分后续指令。随着程序的执行,一种情况是这部分推测执行的指令的确是需要执行的,于是就顺着已经执行的指令继续向前推进。但有时也没那么幸运,这些推测执行的指令由于条件发生变化而不允许执行。那也没关系,反正是使用的是“闲置资产”,大不了就废弃掉执行结果,转而执行应该执行的指令,也没额外浪费时间。

处理器在性能增强的同时,行为越来越智能,充分挖掘潜力,优化计算资源,这本来是件好事。Intel是这方面的先行者,包括超线程在内都做得很成功。然而如今Intel面临的局面,罪魁祸首也是这种激进的优化策略。因为对于这些推测执行的指令,由于不知道最终是否会采纳,处理器是在一种“试探”的方式下运行:先尝试着执行指令,并将结果临时保存在内部不可见的区域。如果被采纳则把这些结果输出到可见的区域(寄存器或内存),实际效果就是指令确实执行了;如果不被采纳则抛弃这些结果,不修改可见区域,相当于什么都没发生。现在问题就出在这个“试探”机制上:

首先,执行这些指令时,处理器没有做严格的权限验证。例如入侵者从用户空间读取内核地址。对于这样“明目张胆”的攻击,正常情况应该是在把数据读出之前就断然拒绝,不给入侵者任何机会。然而推测执行时竟然采用了首先把数据从内核里读出来,然后再检查这个读操作是否合法的危险方式。这就好比你发现有人闯入密室,慌忙打开灯,结果强盗把你的宝藏看得清清楚楚,你发现有强盗马上又把灯关上。即便这样,如果处理得当,也不会有问题。虽然内核数据被错误地读出来,处理器可以自己管理,不交给入侵者,让入侵者什么都得不到。就好比如果你把宝藏高高挂起来,强盗根本够不着,也没问题。

这就涉及到第二个问题,在这些推测执行的指令里,入侵者竟然能够在处理器察觉到越权之前的短暂一刻获得这些内核数据。这个时间窗口很短暂,只有纳秒数量级,但足够执行数十条指,发起一次攻击。还是那句话,如果处理得当也不会有问题。因为黑客偷来的这些数据没有别的地方可以藏,或者存放在寄存器里,或者存放在内存里。无论试图放在哪里,处理器总是有办法加以阻止。就如前面提到的,处理器发觉越权时不会将结果不输出到可见区(寄存器或内存),入侵者也是一无所获。这好比即使强盗拿到了宝藏,如果把所有出口都堵死,强盗也不能得逞。

于是第三个问题接踵而至:这些敏感数据,入侵者既不放在寄存器里,也不放在内存里,而是偷偷“藏匿”在Cache里。这个“藏匿”,不是把数据写进Cache里。写Cache就是写内存,如前述,处理器能够阻止这种操作。黑客利用短短的时间窗口,巧妙地在Cache里埋了个标记,偏偏Intel(公道地说,还有AMD和ARM)在抛弃计算结果时,没有把Cache里留下的“标记”清除掉。黑客揪住了这个“小辫子”,通过非常手段探测Cache里的标记,恢复了敏感数据。Cache是用来加速内存访问的,几乎没有人能想到黑客能够用来“走私”敏感数据。这好比强盗不走出口,而是挖条地道最终把宝藏运了出去。

泄露了内核内存数据会有什么后果?后果非常严重。对于64位处理器,能访问内核内存意味着能够访问几乎所有物理内存,包括所有进程或应用的内存数据,这是root用户才拥有的权限。这就像有人在你家每个房间都装了摄像头,你的一举一动都看得清清楚楚。常用的调试器GDB,能够完全接管其它进程,设断点,看变量和调用栈,更别说截获用户名密码之类的小动作,其基础就是对其它进程内存访问和信号量截取。功能强大的strace,可以偷窥其它进程的系统调用,参数和返回值,也是因为能够读取其它进程的内存数据。但系统安全策略只允许这些工具跟踪和窥探你自己创建的进程,除非你是root用户。如今这两个漏洞,让一个普通用户就能对内核和其它进程的数据洞若观火,一览无余,能不严重吗?

推测执行包含两种技术,乱序执行(out-of-order execution)和跳转预测(branch prediction),分别对应熔断(Meltdown)漏洞和幽灵(Spectre)漏洞。

接下来具体分析两个漏洞是如何被利用,让黑客发威,让芯片巨头发怵的。

Meltdown漏洞

Meltdown漏洞只发生在Intel处理器上,利用了处理器对乱序(out-of-order)执行处理不当的缺陷。现代处理器为了提高各个运算单元的利用率,不是一次执行一条指令,而是综合考虑当前指令和后续几条指令,一次批量调度多条指令到空闲的运算单元并行执行,所以代码里指令的先后关系并不一定是指令执行的先后关系。

如果用户空间一条指令读取了内核的内存地址,正常情况下该指令会导致陷入(trap),从而终止该指令的执行。但由于乱序原因,该指令后续指令可能会在trap发生之前就得到执行。如果后续是精细策划的恶意代码,就能够抓住上面提到的处理器漏洞,把“一闪而过”的内核数据“藏匿”在Cache里,偷运出去。以下是代码实例:

 ; flush cache

 ; rcx = kernel address

 ; rbx = probe array

 retry:

 mov al, byte [rcx]

 shl rax, 0xc

 jz retry

 mov rbx, qword [rbx + rax]

 ; measure which of 256 cachelines were accessed

首先将256条cache line清空,即这些cache line不包含任何内存数据。这一点有很多办法可以做到。接下来,从rcx指向的内核空间读取一个字节,存放在寄存器al里:

mov al, byte [rcx]

不要天真地认为系统就这样被黑了--- al里就获得了想要的内核数据。实际上系统会感知到你处在ring-3-level,没有权限访问内核(ring-0-level)的数据,随即抛出陷入(trap),当然al(rax的低位)的值也不会有任何敏感数据。如果没有乱序执行,一切都完美结束,一次低级的攻击企图被击破。但是乱序出英雄。由于乱序,在trap真正发生之前,处理器自作聪明地执行了后续几条指令,而且还犯了前面提到的错误,导致无可挽回的后果。

为了支持乱序执行,处理器里实际上有上百个不可见的寄存器,尽管只有16个寄存器是可见的。所以即便寄存器al(即rax)不包含内核数据,但是有一个不可见的寄存器与rax是对应的,并且处理器处理不当,把内核数据写进了这个隐藏的rax里。如果一切正常,最终这个隐藏rax会变成rax,从而完成赋值操作。但在此场景里这不会发生,因为trap的出现会终止赋值。尽管如此,在乱序执行过程中,隐藏的rax还是拥有和rax一样的地位,故后续指令里用到的rax实际上就是隐藏的rax,已经包含了内核数据。这正是黑客梦寐以求的,接下来要做的就是在trap发生前,尽快将数据转手。首先藏匿在Cache里:

 shl rax, 0xc 

 mov rbx, qword [rbx + rax]

所谓“藏匿”在Cache里,就是对这个字节对应的Cache Line发起一次读操作。首先将内核数据平移12位,从而指向一条Cache Line的边界(起点)。通常一条Cache Line大小是64字节,只需要平移6位就行了。平移12位是考虑到处理器的prefetch可能会一次加载进多条相邻的Cache Line,造成混淆。后面读操作的目的是把内存里的数加载到内核数据值对应的Cache Line里,这就是个标记:Cache里其它Cache
Line都是空的,唯独这个Cache Line和内存一致(up-to-date)。接下来等处理器恍然大悟,触发了trap,为时已晚,标记已经做好了,而且由于处理器的缺陷,没有清除Cache里留下的痕迹。接下来黑客可以遍历所有256根Cache Line(一个字节有256种可能性),找到刚刚做过标记的那根,比如第45根,那么刚才“窃取”的内核数据就是45。

Spectre漏洞

和Meltdown不同,Intel,AMD和ARM都有Spectre漏洞。该漏洞利用了处理器在Branch Prediction失效处理不当的缺陷。在讲述out-of-order时提到,处理器在执行当前指令的同时,还有可能把后续指令提前执行。问题是如果当前是条跳转指令,那么后续指令到底从哪个分支取呢?于是就出现了branch prediction技术:用Branch Target Buffer(BTB)记录最近几次跳转指令的目的地址,利用前一次(或几次)的跳转历史预测下一次的跳转目的地址。一旦执行到跳转指令,就要根据BTB里记录的跳转地址去获取out-of-order要执行的后续指令。如果预测正确,系统将会以很高的效率和并发度运转。这对采用流水线结构的处理器也是很有意义的:正确的预测会使流水线一直处于高负荷状态,不会被中途打断。

不过既然是推测执行就一定会出错。万一预测错误,正确的做法是,那些不该执行的指令要被抛弃掉,不能生效,甚至不能留下任何痕迹,转而去从新的跳转地址取指令。但恰恰出现了和Meltdown一样的问题:这些推测执行的指令在权限检查之前就能够让黑客有机会读取敏感数据,同时也不是“不留下任何痕迹”,Cache里的标记就没有被清除,为黑客转手敏感数据创造了条件。还是用代码来说明:

if (x < array1_size)

    y = array2[array1[x] * 256];

C语句y = array2[array1[x] * 256]和Meltdown的汇编代码很类似:

array1[x]对应指令mov al, byte [rcx],触发敏感数据的读取;

array1[x] * 256对应指令shl rax, 0xc,将敏感数据大小平移到Cache Line边界;

array2[array1[x] * 256]对应指令mov rbx, qword[rbx+rax],在Cache里做标记

不同的是开始时的条件语句:if (x < array1_size)。这条语句是用来触发和“欺骗”branch prediction的,具体过程是:

首先给x赋与合法值,即x < array1_size,然后执行条件语句,此时会执行if下面的分支。接下来再执行几次x合法值得场景,从而训练BTB将if的跳转目的地址指向if下面的分支。接下来无论x取什么值,都会预测执行if下面的分支。诱骗处理器“上当”后,攻击开始了。

和Meltdown一样,先把Cache  Line清空,不包含array2指向的内存里的任何数据。同时也把array1_size从cache里清除掉,确保访问它时需要耗费很长的时间(从内存里读取比较慢)。接下来给x赋一个非法值x>array1_size,这不是随便给的非法值,这个值的选择使得array1[x]正好读出想要的内核地址(C语言里数组实际上是基地址array1加上偏移量x)。然后攻击开始,先执行条件判断if (x < array1_size)。由于条件不满足,理论上if下面的指令不应该执行。但是由于array1_size不在Cache里,需要从内存读取,所以要花费上百个clock获得array1_size的值后才能知道条件是否满足。“聪明”的处理器等不及了,一查BTB,发现以前都是执行下面语句的,这次应该也不例外,于是便尝试着执行if下面的语句。接下来发生的事和Meltdown一样了,array1[x]偷取了内核数据,array2[array1[x]
* 256]把内核数据标记在Cache Line里,泄露出来。

暗度陈仓

无论Meltdown还是Spectre,最终都要把敏感数据记录在Cache Line里,然后才能窃取出来。这个标记是这样做的:假设Cache里有256根Cache Line,分别对应敏感数据(一个字节)256种可能的取值。这256根Cache Line中,有一根的数据是和内存数据是一致的(up to date),其余255根是空的。之所以会有这样的差别,是因为推测执行时,恶意代码结合敏感数据发起了一次内存读操作,例如上面array2[array1[x] * 256],读内存块array2,其中array1[x]包含敏感数据;以及mov
rbx, qword [rbx + rax],读内存块rbx,rax包含敏感数据。这次“闪电般”的读操作更新了敏感数据对应的Cache,这就是所谓的“标记”。这个标记的识别方式是:已经读取过的Cache Line,再次读取花费的时间比没有读取过的Cache Line少很多,因为前者直接就可以使用Cache里的数据,后者还需要从内存里搬运。找到读取最快的Cache Line就能恢复敏感数据,代码如下:

/* Time reads. Order is lightly mixed up to prevent stride prediction */

for (i = 0; i < 256; i++) { 

    mix_i = ((i * 167) + 13) & 255; 

    addr = &array2[mix_i * 512]; 

    time1 = __rdtscp(&junk); /* READ TIMER */ 

    junk = *addr; /* MEMORY ACCESS TO TIME */ 

    time2 = __rdtscp(&junk) - time1; /* READ TIMER & COMPUTE ELAPSED TIME */ 

    if (time2 <= CACHE_HIT_THRESHOLD && mix_i != array1[tries % array1_size]) 

        results[mix_i]++; /* cache hit - add +1 to score for this value */ 

}

上述代码遍历所有Cache Line,对每条Cache Line产生一个读操作,并计算这个读操作花费的时间time2。如果这个时间小于某个门限值就认为找到了up to date的Cache Line,对应到Cache Line的位置就是敏感数据的数值。

解决办法

目前网上已经有了通过Meltdown漏洞盗取浏览器密码的演示。而Spectre漏洞,由于触发条件较难,暂时还没有造成信息泄露,但这也只是暂时的。由于漏洞发生在处理器层面,几乎所有的安全措施都失效,但还是有几种方案可以缓解或规避。

一种方案是地址布局随机化,包括内核地址随机化和进程堆栈地址随机化。随机化使得攻击难度变大:入侵者不知道哪个内存地址包含敏感数据,没办法一举就把系统攻破。但这种方案只能延缓或降低攻破可能性:家里的小秘密还是暴露在偷窥者的摄像头下,只是你不停地把家具从一个房间搬到另一个房间,总有一天会被识破。

另一种方案是把内核地址映射表和用户地址映射表分开。这两个漏洞发生的根本原因是从用户空间读出了内核空间的内存数据,而这种情况发生的根本原因是在用户空间运行时依然有内核空间的映射。我们知道,当操作系统运行起来后,软件访问的地址都是虚拟地址,无论用户空间还是内核空间都是这样。虚实地址转换通过页表(Page Table)来管理。为了有效利用存储空间,页表组织成树形分层结构:位于顶层的是page global directory (PGD);下面是page upper directory (PUD),然后分别是page
middle directory (PMD)和page-table entries (PTE)。目前一个进程只有一个PGD,对应的页表是个全集,包含用户空间和内核空间的映射。但这不意味着从用户空间就可以随意访问内核空间的内容了,因为PTE记录了访问权限,用户空间无法通过页表访问内核空间的数据。如果没有Meltdown和Spectre两个漏洞,这样的设计是没有缺陷的,反正无法越权访问,映不映射内核空间都无所谓。但这两个漏洞的存在,导致操作系统要作出修补,每个进程使用两个PGD:原来全映射的PGD还保留,在内核态下用;另一个PGD只包含用户空间映射,在用户空间下使用。这样一来,用户空间下根本就没有内核空间的映射,即使漏洞存在,越权访问指令也得不到任何敏感数据。Linux内核的KAISER机制就是为了这个目的。

但是这样做付出的代价是性能下降。树形结构的页表意味着处理器需要经历多次查找才能实现虚实地址转换,为了加速这个过程处理器开辟了一张快速查找表(TLB),将最近使用的虚实地址对应起来,从而减少页表访问次数。但切换PGD会使得TLB失效,处理器不得不重新访问页表,建立TLB,这是个很耗时的过程。两个PGD意味着每次系统调用或中断发生时都会切换PGD,刷新TLB,这是个很可观的开销。在多数情况下会增加5%的负载,在有些场景下甚至会达到30%。在支持PCID的处理器上情况会有很大的改善,但总一些场景无法避免开销的增加。

后记

硬件漏洞靠软件修补,总会付出代价。最终的解决办法还是要芯片巨头们从根本上修复这个漏洞。通过这次事件我们认识到:

1. 当我们沉醉于主频,带宽,Cache,处理器个数,跑分等性能指标和各种优化技术时,不要忘了安全性也是一个很重要的因素;

2. 这次的漏洞姑且可以认为是无意为之,至少没有恶意。但谁能保证处理器里就没有故意植入的后门呢?面对处理器级的漏洞我们几乎束手无策,甚至都无法察觉,这不能不说是个安全隐患。试想在一个月黑风高的夜晚,无数个手持通关文牒的报文跨过路由器,穿过防火墙,悄无声息地钻入处理器,激活芯片巨头固化在处理器上的“幽灵”...... 一切看起来都很正常,没有报警,没有日志,也没有留下任何痕迹......

参考资料
https://en.wikipedia.org/wiki/Kernel_page-table_isolation http://blog.erratasec.com/2018/01/lets-see-if-ive-got-metldown-right.html#.WlGKYFWWbX4 https://spectreattack.com/spectre.pdf https://en.wikipedia.org/wiki/Speculative_execution https://lwn.net/Articles/741878/ https://www.theregister.co.uk/2018/01/02/intel_cpu_design_flaw/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  安全