树莓派是如何免疫 Meltdown 和 Spectre 漏洞的

前段时间,对Meltdown和spectre安全漏洞的讨论非常激烈,该漏洞影响了所有的现代intel处理器,一开始ARM还声称这些漏洞不会影响ARM系列的处理器,但后面的事实证明AMD处理器和ARM内核都没有免遭其害。

spectre漏洞使得攻击者可以绕过软件检查,读取当前地址空间中的任意位置数据,meltdown漏洞使得攻击者可以读取操作系统核地址空间的任意位置数据(用户通常不可访问该数据)。这两种漏洞皆通过边信道攻击利用很多现代处理器都有的性能特征(缓存和推测执行)来泄露数据。但树莓派称不受这些漏洞的影响。

meltdown影响了intel处理器,打破了用户应用程序和操作系统之间最基本的隔离(地址空间隔离),这种攻击允许程序访问其他程序和操作系统的内存,这会导致数据泄露。(通常而言,目前操作系统都采用了虚拟内存管理方式,这样可以让内存需求超过实际物理内存限制的进程或线程能够运行,加上页面置换即可。虚拟内存管理也可以实现对不同内存区域的保护)

而spectre除了能够影响intel还能影响AMD和ARM架构的大量处理器,也就是说除了PC,手机等终端也会受到影响,几乎所有现代计算机处理器均无法幸免。

但是类似树莓派等廉价计算设备可能并不会受到影响。

接下来介绍一些现在处理器设计的概念,使用简单的python来解释这些概念:

t=a+b

u=c+d

v=e+f

w=v+g

x=h+i

y=j+k


标量处理器

最简单的现代处理器每次循环执行一个指令,称之为标量处理器,也就是说上面的语句在标量处理器中需要执行6次循环。

树莓派1和zero中使用的intel486和arm11767都是标量处理器


超标量处理器

很明显,加速标量处理器的方式就是提供其时钟频率,但是这样很快就会达到处理器内部逻辑门运行的极限,因此处理器设计者开始寻找一次性处理多件事情的方式。

有序超标量处理器尝试在一个pipeline中一次性执行多个指令,这取决于指令之间的依赖关系,依赖关系很重要,你或许认为双向超标量处理器可以将6个指令配置执行:

t,u = a+b, c+d

v,w = e+f, v+g

x,y = h+i, j+k

但这没有作用,我们必须先计算v再计算w,也就是指令三和四无法同时执行,因此会执行四个循环:

t,u =a+b , c+d

v=e+f #第二个pipe没有

w,x=v+g, h+i

y=j+k

超标量处理器包括intel pentium以及树莓派2,3使用的ARM cortex-A7和cortex-A53,树莓派3的时钟频率只比2高33%,但性能大约是后者的2倍,部分原因在于A53超出A7的对大量指令的配对执行能力


无序处理器(与原子操作概念相关)

即使v和w存在依赖关系,我们仍能找到其他独立指令来填补第二次循环中的空pipe。无序超标量处理器能够打乱指令执行的顺序(当然同样受限于指令之间的依赖关系)以保持每个pipe处于忙碌状态。

无序处理器可以交换w和x的顺序:

t=a+b

u=c+d

v=e+f

x=h+i

w=v+g

y=j+k

这样就允许执行三次循环:

t,u = a+b, c+d

v,x = e+f, h+i

w,y = v+g, j+k

无序处理器包括了intel pentium2(以及大部分后序intel和AM x86处理器)还有近期的ARM处理器,如cortex-A9,A15,A17等。


分支预测器

上述的示例是直线式代码块,真正运行的程序不是这样的:他们还包括正向分支(if语句),反向分支(用于实现loop)。

在获取指令时,处理器可能遇到依赖于计算值的条件分支(而该值目前尚未计算出),为了避免停顿,处理器必须猜测出下一个要获得的指令。分支预测器通过收集某一个分支之前被采用频率的相关统计数据,帮助处理器猜测该分支是否被采用。

现在分支预测器非常复杂,可以生成非常准确的预测,树莓派3的额外性能是由于cortex-A7和A53之间分支预测的改进。


推测

重排序顺序指令是一种恢复指令级别并行化的强大方法,但是由于处理器变得更宽(能够一次执行3-4个指令),保证所有pipeline处于忙碌就更难了,因此现在处理器提高了推测能力,推测执行可以处理并不需要的指令:这样可以保证pipeline处理忙碌状态,如果最后该指令没有被执行,我们只需要放弃结果就可以了。

推测执行不必要的指令需要耗费大量能源,但是在很多情况下为了获得单线程性能的提升,该方法是值得的。

为了展示推测的好处,看另一个示例:

t=a+b

u=t+c

v=u+d

if v:

    w=e+f

    x=w+g

    y=x+h

现在我们有从t到u到v,从w到x到y的依赖关系,那么没有推测的双向无序处理器无法填充第二个pipeline,它会用三次循环来计算t,u和v,之后处理器知道if语句是否被执行,然后再用三次循环来计算w,x和y。假设if使用了一次循环,那么该示例可以执行4次(v为0)或7次循环(v不是0),如果分支预测器表明if语句的主体很可能被执行,那么推测可以有效打乱程序顺序,如下:

t=a+b

u=t+c

v=u=d

w_=e+f

x_=w_+f

y_=x_+h

if v:

w,x,y=w_,x_,y_

循环计数在推测无序处理器中变得不太明确,但w,x和y的分支和条件更新几乎是空闲的,因此上述示例几乎执行三个循环。


什么是缓存?

在过去处理器速度与内存访问速度成正比,但是现在,处理器已经变得非常快,但内存几乎没有变化,树莓派3只需要0.5ns执行一次指令,但可能需要100ns才能访问主存。

在实践中,程序倾向于以相对可预测的方式访问内存,同时展示时间局部性(如果我访问一个定位,我可能很快会再次访问)和空间局部性(如果访问一个定位,很可能会很快访问附近的位置),缓存利用这些属性来降低访问内存的平均成本。

缓存是一个小的片上内存,接近于处理器,存储最近使用的位置还有近邻内容的副本,以便随后的访问中可以快速获取(最快的存储器当然是CPU内部的寄存器,与CPU同一种材质制成,访问速度与CPU一样。但是大小极其有限)。


什么是边信道攻击?

边信道攻击是基于从密码系统的物理实现获得的信息的任何攻击,而不是算法中的蛮力或者理论弱点。例如定时信息,功耗等都可以提供额外的信息。

meltdown和spectre是通过定时观察缓存中是会否有另一个可访问的位置,以推测内存位置的内容,这些内容通常不应该被访问。


把这些概念放在一起

现在让我们看看如何结合推测和缓存以允许类似meltdown的攻击。

考虑下面这个示例,该程序时一个读取非法(内核)地址的用户程序,会导致错误:

t=a+b

u=t+c

v=u+d

if v:

    w=kern_mem[addr]  //如果到了这,由于非法访问内核地址,会导致程序错误

    x=w&0x100

    y=user_mem[x]

现在,假设我们可以训练分支预测器,使其相信v很可能是非0的,那么我们的无序双向超标量处理器就会混洗程序:

t,w_=a+b, kern_mem[addr]

u,x_=t+c, w&0x100

v,y_=u+d, user_mem[x]

if v:

    #fault

    w,x,y=w_,x_,y_ #we never get here

即使处理器总是推测性读取内核地址,它必须推测产生的错误,直到知道v是非0,也就是说当v为非0,意味着if中的语句要被执行,也就是要用户可以得到内核地址中的内容了,这个时候会程序报错。也就是说,从表面上看,这是安全的:

1.若v是0,,非法读取的结果不会提交给w

2.若v是非0,但在读取结果被提交给w之前发生了错误

然而,假设在执行代码前刷新了缓存,并排列a,b,c,d使得v实际为0,第三个循环中的推测性读取为:

v,y_=u+d, user_mem[x_]

将其依赖非法读取结果的第八位获取用户地址0x000或0x100,并把地址及其近邻加载进缓存,由于v是0,推测性指令的结果将被摒弃,执行将继续。如果我们随后访问其中一个地址,就可以决定哪个地址在缓存之中,那么恭喜:你刚刚从内核地址空间读取了一个位。并且你知道了该位的具体地址。


真正的meltdown实际上更为复杂(因为我们实际上优先执行了非法读取,并处理产生的异常),但原理是相同的。



结论:

现代处理器竭尽全力保持抽象,从而成为直接访问内存的有序标量机器,而事实上,使用包括缓存,指令重排序以及推测在内的大量技术来提供比简单处理器更高的性能成为了现实。meltdown和spectre就是当我们在发展过程中,我们的理想和现实的细微差别导致的漏洞。

树莓派使用的ARM1176, cortex-A7和cortex-A53内核中推测功能的缺失使得树莓派免疫此类攻击。

posted on 2018-04-13 22:28  sichenzhao  阅读(141)  评论(0编辑  收藏  举报

导航