网络攻防-熔断与幽灵攻击,涉及实验:SEED Labs – Spectre Attack Lab ,代码见:https://github.com/Seanxz401/seed-labs
理论
侧信道攻击:如果CPU访问Cache中并 不存在的数据时,则将会产生时间延迟。测量这种时间延迟有可能让攻击者确定出Cache访问失败的发生 和频率。
可以用CPU缓存来记忆秘密、
Flush-Reload技术:(具体见实验)
- 刷新处理器缓存
- 访问存储器秘密s位置
- 重新加载,检查缓存中的s是哪一个
熔断攻击
程序指令的无序执行,导致可能没有机会执行的语句也会执行,程序会消除执行的结果并回滚,但是CPU缓存中还是会留下痕迹。
对策
- CPU硬件(修理费用昂贵)
- 在操作系统中开发变通方法
- KASLR(内核地址空间布局随机化)
幽灵攻击
因为无序执行,在遇到if判断时,可能执行判断条件不成立的分支,通过多次执行if的同一分支可以训练出CPU的惯性(?),然后访问条件不成立时,CPU会很大概率执行之前训练的那个分支,虽然最后会回滚,但是CPU缓存中会有记录。
- 训练CPU去往正确条件下的分支
- 刷新CPU缓存
- 访问存储器秘密s位置(在正常情况下不能通过分支判断)
- 重新加载,检查缓存
实验
Task1 CacheTime.c
代码解释:
定义10个内存块(一个内存块64Byte,4096bit)大小的数组并初始化:
uint8_t array[10*4096];
将数组从CPU缓存中清除:
for(i=0; i<10; i++) _mm_clflush(&array[i*4096]);
访问一下内存块3和内存块7
array[3*4096] = 100;array[7*4096] = 200;
重新访问所有内存块,并计算访问每个内存块使用的时间(CPU时钟)
1
2
3
4
5
6for(i=0; i<10; i++) {
addr = &array[i*4096];
time1 = __rdtscp(&junk); junk = *addr;
time2 = __rdtscp(&junk) - time1;
printf("Access time for array[%d*4096]: %d CPU cycles\n",i, (int)time2);
}
编译代码:gcc -march=native CacheTime.c -o t1
执行结果如下图所示,可以看到内存块3和内存块7所用时间明显少于其他内存块。因为清空高速缓存后,再次访问了内存块3和内存块7,因此这两块被再次放进了高速缓存,而其他的在内存中,因此访问这两块的时间更短。
存在的问题:执行了10次,部分结果不理想,有时候输出没有array[3*4096],有时候访问其他内存块比3和7更快,尤其是第一次经常用时很短。
Task2 FlushReload.c
代码解释:
- 建立数组内存块并写入内容。
DELTA
是使读写内容偏移到一块的中间位置。 - 将数组从CPU缓存中清除。
- 从RAM中读取数组中的一个值(攻击目标、Secret),此时这一块被放入CPU缓存。
- 重新访问所有内存块,并计算访问每个内存块使用的时间(CPU时钟)。如果使用的时间小于某个阈值(根据上一个Task的观察和自己机器的实际情况自定义),就认为这一块已在CPU缓存中。
编译执行结果如图,缓存中的Secret被探测出来了
问题:阈值设置的80,实验20次成功率100%,但是Task1得出的结果范围都远大于80.
Task3 SpectreExperiment.c
代码解释:
创建数组,清除缓存。
训练CPU。使CPU连续十次在if判读语句中选择同一个分支。
1
2
3
4for (i = 0; i < 10; i++) {
if (x < size) {
temp = array[x * 4096 + DELTA];
}再次清除缓存,包括判断条件size和数组。
再次访问一个明显大于size的值,然后重新加载整个数组并计算时间。
编译执行结果如图,97是一个大于size的值,但是他已经存在于缓存中,即说明执行了if中的语句。
task:
- 注释掉对size的缓存清除:
_mm_clflush(&size);
,编译执行后无法获得上图结果,我觉得是因为size在缓存中比较语句很快就会执行。 - 将访问值x改为x+20,编译执行后无法获得上图结果。因为被训练成不执行这个分支的情况了。
Task4 SpectreAttack.c
代码解释:
- 创建数组并清除缓存
- 训练CPU。此时要使CPU能走向正常访问的if分支。
- 清除if判断条件的缓存和数组缓存
- 访问一个超出限制条件的值,再重新加载数组并计算时间。
编译执行结果如图所示,通过训练CPU可以实现对数组中限制区的访问。
Task5 SpectreAttackImproved.c
将Task4升级为了自动化多次尝试,并统计每次命中的内存块号,命中次数最多的就是Secret。编译执行结果如图:
task:
- 上图中显示的secret是0,可能是程序总是会把数组的首地址加载进缓存吧。多试几次总会有成功的
- 注释掉
printf("*****\n");
,攻击不成功,获取的值为0; - 修改usleep的值,增加到1000,准确率提高。
Task6
之前的只是输出命中的内存块的索引,通过改进Task4的代码我们可以输出字符串,除了第一个字符其他都能打印出来。
具体改进是通过for循环从secret头指针开始偏移,对每一个字符发起幽灵攻击然后再重新加载并计算时间就可以得到命中的地址及其具体值,转为字符,连起来就是secret字符串,如图所示