.. Kenneth Lee 版权所有 2018-2020 :Authors: Kenneth Lee :Version: 1.0 给程序员解释Spectre和Meltdown漏洞 ********************************* 我犹豫了很久要不要写这篇东西。伦理上我当然不想鼓励攻击行为,所以更好的方法是不 要讨论它。问题是这玩意儿论文都出来了,不讨论它似乎又是掩耳盗铃。所以,不轻不重 地讨论一下吧。 Spectre和Meltdown是缓冲时延旁路攻击的两种实际攻击方法。 什么叫旁路(Side Channel)攻击呢?就是说,在你的程序正常通讯通道之外,产生了一 种边缘特征,这些特征反映了你不想产生的信息,这个信息被人拿到了,你就泄密了。这 个边缘特征产生的信息通道,就叫旁路。比如你的内存在运算的时候,产生了一个电波, 这个电波反映了内存中的内容的,有人用特定的手段收集到这个电波,这就产生了一个旁 路了。基于旁路的攻击,就称为旁路攻击。这个论文对这种攻击有一个归纳: https://csrc.nist.gov/csrc/media/events/physical-security-testing-workshop/documents/papers/physecpaper19.pdf 。读者可以体会一下可能的攻击方法:时延,异常(Fault),能耗,电磁,噪声,可见光 ,错误消息,频率,JTag等等,反正你运行总是有边缘特征的,一不小心这个边缘特征就 成了泄密的机会。 缓冲时延(Cache Timing)旁路是通过内存访问时间的不同来产生的旁路。假设你访问一 个变量,这个变量在内存中,这需要上百个时钟周期才能完成,但如果你访问过一次,这 个变量被加载到缓冲(Cache)中了,下次你再访问,可能几个时钟周期就可以完成了。这 样,如果我攻击一个对象(比如一个进程,或者内核),要得到其中某个地址ptr的内容, 我只要和它共享一个数组,然后诱导它用ptr的内容作为下标访问这个数组,然后我检查这 个数组每个成员的访问时间,我就可以知道ptr的值了。 你一定觉得这是不可能的,对吗?在Cache Timing Side Channel刚被提出来的时候,大家 也是认为出问题的机会是很小的,只是理论上有效而已——直到Spectre和Meltdown被提出来 (其实之前已经有人用这种方法来对内核地址随机化(KSLR)进行攻击了,Spectre和 Meltdown只是综合利用了预执行的漏洞而已)…… 这个事情坏就坏在现代的CPU基本上都支持指令预执行。比如,下面这段代码::: if(condition) do_sth(); 你以为condition不成立,do_sth就不会执行,但condition存在内存上,从内存中把 condition读出来,可能要几百个时钟周期,CPU闲着也是闲着,于是,它好死不死,它偷 偷把do_sth()给它执行了!CPU本来想得好好的:我先偷偷执行着,如果最终condition不 成立,我把动过的寄存器统统放弃掉就可以了。 问题是,大部分CPU在执行do_sth()的时候,如果有数据被加载到Cache中了,它是不会把 它清掉的(因为这个同样不影响功能),这样就制造了一个“不同”了,旁路就产生了。 现在我们来看看Meltdown是怎么构造的。假设我现在在用户态执行一个程序,我可以在程 序中制造这样一段代码::: raise_exception(); access(probe_data[data*4096]); 其中,raise_exception()表示制造一个异常,比如你除零错,或者访问非法地址之类的。 后面那个数组是我(攻击程序)自己创建,我可以通过访问另一个一样大的数组一类的手 段,导致这个数组的Cache全部被清掉。这样,理论上我访问这个数组的每个成员的时间都 应该要数百时钟周期。 然后data是内核的一个地址(我想攻击的那个地址。另,为了避免部分人误会,严格来说 ,ptr的值是内核的一个地址,而data=*ptr),理论上这个地址我是没有权限访问的,但 第一句话产生一个异常后,系统已经陷入到内核了。又“照理说”,access那一句是不应该 执行的,但CPU又把它预执行了,这样,数组probe_data中的其中一个(下标等于data的) 成员就被Load进Cache了。 等异常从内核返回后,我检查一下probe_data每个成员的加载速度(如果data的大小是字 节,这个数组只要256项(乘4096是为了让cacheline隔开而已),我就足以偷到内核中的 一个字节了)。然后重复这个过程,我就可以读出内核中的所有数据,包括你的root密码 了。 按Meltdown论文的说法,他们在Intel的CPU上可以用五百多K每秒的速度Dump内核映像! 还是那句话,恐怖不恐怖?惊喜不惊喜? 为了解决这个问题,Linux上现在提出的解决手段是KPTI(通用技术称为Kaiser),内核和 用户态不共享页表,每次你异常、IO、系统调用,都要把内核页表重新装进来。 现在我们来看看Spectre攻击。Meltdown只能从用户态攻击内核,Spectre攻击就灵活多了 ,它可以攻击任何有缺陷的对象。它要求被攻击的对象里面有如下Pattern的代码::: if(index1