本文是基于meltdown/spectre论文与维基的一个科普小结。
meltdown
meltdown是基于处理器的乱序执行 + 高速缓存技术,结合现代操作系统进程中内存管理技术制造的一个安全漏洞。
乱序执行
所谓乱序执行,指的是处理器在处理指令时,并不是严格按照指令原顺序执行的。例如在读取数据时,应该先检查读取者是否有权限读取数据,如果有权限读取数据,再真正读取数据。
但是为了提高运行效率,处理器往往会首先从内存中读取一批指令,然后再按照自己认为最优的方式处理指令。
在上述读取数据的指令序列处理过程中,处理器的一般做法是首先读取数据,然后再检查读取者是否有权限读取数据,如果没有权限读取数据,则再返回相应的错误码,例如无权限等。但是被读取的数据仍然在处理器的高速缓存中,没有被清空。
处理器之所以要先读取数据,是因为对比执行指令而言,从内存读取的时间实在太长了。如果等待权限检查完毕之后再读取数据,往往会导致数据读取指令被堵塞很长时间,处理器无事可做,使得整体处理性能下降。例如对一颗工作频率2G Hz的处理器而言,一个指令的处理时间约为ns级别,但是访问ddr4主存的延迟可能会达到15 ~ 20ns级别(仅计内存控制器发出请求到取回数据延迟的理想情况),但实际上如果计入行冲突与缓存缺失处理等实际事项,其延迟往往高达近百ns。
高速缓存
如上所述,由于处理器访问主存很慢,因此在处理器内部内置了多级缓存(最快的是一级缓存),以缓存最近访问的主存数据,方便处理器快速访问,处理器访问一级缓存的延迟也在ns级别。
因此一般来说,当程序访问一段数据的时候,如果数据在高速缓存内,访问延迟将很短,如果数据在主存但是不在高速缓存内,访问延迟将较长。在Intel处理器上,可以通过rdtsc
(即ReaD TimeStamp Counter)指令获取高精度的时间戳(以时钟周期为单位),用来判断数据读取的延迟,如果延迟在几个时钟周期之内,则数据在高速缓存中,如果在近百个时钟周期之内,则在主存中。
进程的内存管理
现代操作系统对进程进行了虚拟化管理,其目的是为了简化应用程序的开发,让进程能透明有序地共享处理器、内存、磁盘等资源。
在内存管理上,内核与处理器的MMU单元进行协作,通过TLB提供了高速的MMU缓存,从而可以实现快速的内存分页管理,给每个进程不同的页表,从而既通过进程隔离保障了安全,又给进程提供了连续内存的假象。
在Linux操作系统的实现上,每个进程的地址空间实际上都映射了内核空间的内容,以及物理内存的。因此,从原理上来说,如果没有处理器的MMU/TLB协作进行权限判断,进程内是可以随意访问(读取与写入)包括内核与物理内存的任何一段的内容的。
综合利用
结合上述多个方面的技术,meltdown产生了。其原理如下:
- 恶意代码读取某块它感兴趣,但是没有权限访问的数据
- 处理器读取这段高权限数据,将其放入高速缓存
- 恶意代码随之读取对应的数据,如果发现访问时间长,则其将得知对应的数据在缓存里,否则就不在缓存里
当然,在以上的第3步中,恶意代码只能得知对应地址的数据是否在缓存中,但是并不能读取到真正的数据内容。为此,可以使用间接寻址的方式猜测数据的具体内容。
例如假设地址为2000的数据无法被恶意代码直接读取,但其值可能为1~5。此时,如果执行执行下列指令:“读取地址为5000+(地址为2000的数据)的数据”,则处理器可以读取的数据地址范围即为5001 ~ 5005。如果我们现在发起了对此地址的读取,然后再依次读取5001 ~ 5005地址的数据,而且发现5004地址的数据读取最快,则显然地址为2000的内存中存放的数据就是4了。
上述例子来自于参考资料中的Meltdown的Wikipedia。
影响
meltdown主要利用了Intel处理器的乱序执行,其权限检查延迟导致数据被访问且被缓存了,再通过高精度时钟来获取访问时差以猜测数据内容,而不是直接获取数据内容,这是典型的侧信道攻击(side channel attack)。
meltdown可以绕过权限检查访问数据,所以可以“熔断”(meltdown)安全边界,因此而得名。
由于缓存的刷新频度较快,因此meltdown攻击需要一定的时间间隙,而且需要进行多次猜测以确定数据具体内容,有一定攻击难度。
想要纠正这种问题,可以采取以下措施:
- 处理器在进行数据访问之前先检查权限,通过了再访问数据(AMD号称是这样处理的,因此不受meltdown的影响)
- 如果不做上述处理,在发现权限不符时应该清除缓存数据
- 操作系统及时刷新TLB防止用户态程序直接访问内核态地址空间(KPTI)
处理器来解决上述问题会更高效,但是可能需要对硬件进行修改(召回)。操作系统解决问题更快速,但是性能下降则较为明显,现在看来。对于浮点、定点运算等性能影响应该低于1%,但是对于数据密集型应用,如文件操作与数据库等,则会带来至少15%以上的性能损耗。
不过在桌面系统中,恶意程序往往有更多的手段读取到敏感数据,因此meltdown的实际危害有限。
spectre
spectre攻击利用的是处理器的分支猜测 + 高速缓存技术。
在执行指令的过程中,当遇到分支时(例如if
、else
判断这种),为了避免等到分支判断结束后再堵塞读取内存中的后续指令(记住,对于处理器来说,内存访问实在太慢了~),处理器会进行分支猜测。
变种1
例如下面这段代码:
if (x < array1_size)
y = array2[array1[x] * 256];
按照正常逻辑,应该是首先判断x
是否小于array1_size
,如果是才能使用x
来访问array1
中对应索引的数据。下面,假设array1
是一段敏感数据,攻击者希望能获取其信息。
上述代码是常见的一种代码,例如在某函数(系统调用)的入口处,根据用户传入的参数x
进行参数校验,通过了校验再使用x
来访问对应的内部数据。
处理器内部实现分支猜测的逻辑基本是根据这段代码最近分支的跳转结果来预取最可能成功的分支,并预先执行分支,当然,获取到的数据也将放在高速缓存里。如果发现分支预测错误了,则将会把相应的指令结果作废,重新执行其它的分支。
恶意代码可以首先通过自己可以控制的x
来训练此段代码,使得if
条件总是被满足,从而在下次分支预测时会直接执行y = array2[array1[x] * 256];
这行代码。
要使得对应的代码被执行和利用,技术上还需要做到下面几点:
-
x
的值需要选择一个特殊的值,使得array2[array1[x] * 256]
超出了array2
的实际范围,假设array1[x]
等于k
-
array1_size
与array2
不在高速缓存中,因此无法做到快速比较(这样处理器就无法进行快速的if
判断,只能猜测分支了)
要刷新array1_size
与array2
,只要不停地读取垃圾数据,让高速缓存中原来的数据清空就可以了,或者调用x86的clflush
指令也可以。
这样,通过分支预测,对应的数据就可以被放在高速缓存中了。为了得到对应k
的值,攻击者可以再次调用原函数,这次输入合法的y
,如果array1[y]
等于k
,则由于k
就在高速缓存中,则对应的访存将很快,不然将明显较慢。如果攻击者能直接访问array2
,那通过顺序访问array2
,比较访问时间,就可以更快知道对应的k
值了。
上述变种1攻击方式利用了数组越界。
变种2
spectre的变种2攻击方式是利用了处理器的间接跳转指令,例如jmp eax
表示跳转到寄存器eax
对应的地址去。
第二种攻击方式的原理是在这些跳转指令发生的地方,找到对应一些寄存器(或者栈、或者堆内存地址),例如寄存器r1与r2。若在代码中可以搜索到两片代码,使得第一片代码能把r1寄存器中存放的地址的数据进行数学逻辑运算(如异或、加减等),将得到的结果放入r2中,第二片代码能通过访问r2寄存器中的数据对应的内存,而且r1中存放的内容是攻击者感兴趣的内容,则攻击者可以对分支预测进行训练,使得程序在运行时跳转到第一片代码中,再在第二片代码执行完后访问感兴趣的数据,进行逆推得到原始数据。
在论文中举的例子是在一段Windows二进制代码中定位函数调用,然后发现在函数调用前edi与ebx寄存器会保存自己感兴趣的数据,继而在Windows动态库ntdll.dll(Windows的核心库)中找到对edi与ebx访问的片段,继而训练分支预测,使其跳转到对应的代码片段,最后获取到对应的敏感数据。
这种攻击方法可以通过Google提出的retpoline技术(即return trampoline,基于函数返回技术的跳床)解决,例如将jmp eax
换为一个call
与ret
指令的组合,如下:
call set_up_target;
capture_spec:
pause;
jmp capture_spec;
set_up_target:
mov %eax, (%rsp);
ret;
通过处理器的返回缓存(x86上的RSB、AMD的RAS与ARM的return stack等)可以避免被诱导,从而防御这种攻击方法。
影响
当然,由于spectre没有跨越数据权限,因此从表面上来看,这种工具没有什么意义,因为只是读取本进程本来就可以读取的数据可以有其它的技术方法。但是在这里,我们还可以利用各种JIT来得到敏感数据,例如在同一个进程内的某网站的javascript
代码可以通过JIT得到同在一个进程内的另一个网站的敏感数据。或者在容器环境下通过bcc
生成eBPF
代码注入宿主机的内核,再利用eBPF
的JIT来读取本来无法读取到的信息。
spectre的攻击技巧更高,因为它需要在现有代码中查找可利用的代码片段,而且对代码进行训练,以及同时利用好时间间隙和访存测量时差。但是其危害范围更广,简单地加强权限隔离是不够的,需要在编译器、JIT等技术方面进行修正(例如加上数组边界检查),涉及到所有软件。
小结
不管是meltdown,还是spectre,都只能被用来读取内存中的数据,而且攻击技术难度较高。因此在部分人看来,其危害很有限,但是我们需要注意到下面几点:
- 内存中的数据包含大量的文件系统缓存(page cache),因此,保存在磁盘上的敏感文件数据是可能被读取出来的
- 安全攻击往往是组合式的,单个漏洞的作用是有有限的,有效攻击是多个漏洞的综合利用造成的
- 虽然尚未有明确根据上述漏洞展开的实质性攻击,但是有了PoC就已经有充分的理由进行修补了,不能置之不理
上述漏洞根植于处理器的微架构设计中。其中,meltdown可以被认为是处理器的bug,而spectre则可能真会像幽灵一般徘徊多年。
在程序上,可能(未测试 ;))可以通过内存屏障(如lfence
等指令)暂时消除指令处理乱序的问题,从而保证指令严格按序执行来消除漏洞,但是这样的话估计性能会下降较多。
此外,上述漏洞已经很早就报告给Intel了,但是Intel的处理措施让人失望,比如它发布的微码导致系统不稳定。只能说,这个漏洞的完善修复显然超出了Intel的能力。