安全编码实践

本文介绍开发者和安全研究人员都需要了解的主题, 帮助编写安全代码并正确审计现有代码。

报告安全缺陷

如何报告安全缺陷,或询问潜在安全问题, 请参阅 安全流程 Wiki。

通用 C 安全编码实践

QEMU 中报告的大多数 CVE 并非虚拟化或模拟特有问题,而是普通 C 编程缺陷。 因此,了解常见安全缺陷类别非常重要。

有关 C 安全编码的资料很多,例如 CERT C 编码标准 涵盖最重要的安全缺陷类别。

这里不再详细展开,只列出最重要的类别:

  • 缓冲区溢出

  • 释放后使用和重复释放

  • 整数溢出

  • 格式字符串漏洞

部分问题可以通过分析工具发现。Coverity 定期进行静态分析, 最明显的问题连编译器也会报告。动态分析可使用 valgrind、tsan 和 asan。

输入验证

来自客户机或网络、文件等外部来源的输入不可信,可能无效。 任何可能导致程序崩溃、向客户机暴露主机内存,或被攻击者利用的操作, 都必须先验证输入。

最敏感的攻击面是设备模拟。 所有硬件寄存器访问,以及从客户机内存读出的数据,都必须验证。 典型例子是具有多个单元、由客户机通过索引寄存器选择的设备:

typedef struct {
    ProcessingUnit unit[2];
    ...
} MyDeviceState;

static void mydev_writel(void *opaque, uint32_t addr, uint32_t val)
{
    MyDeviceState *mydev = opaque;
    ProcessingUnit *unit;

    switch (addr) {
    case MYDEV_SELECT_UNIT:
        unit = &mydev->unit[val];   <-- this input wasn't validated!
        ...
    }
}

若 val 不在 [0, 1] 范围内,解引用 unit 时就会越界访问内存。 代码必须检查 val 为 0 或 1,并处理无效值。

非预期设备访问

客户机可能以异常顺序或在意外时刻访问设备寄存器。 模拟代码不能假定客户机遵循驱动开发手册中的典型操作流程。 例如,客户机可能在设备完全初始化前就启动操作。

模拟代码还必须应对异步操作进行期间的意外寄存器访问。 行为良好的客户机会等完成中断后才访问某些寄存器, 但代码仍需处理请求完成前覆盖寄存器或提交更多请求的情况。 这些非预期访问不得造成 QEMU 内存损坏或泄漏。

可通过 qemu_log_mask(LOG_GUEST_ERROR, ...) 报告无效寄存器访问, 使用 -d guest_errors 启用相应日志。

热迁移

设备状态可保存到磁盘镜像并与其他用户共享。 热迁移代码加载状态时必须验证输入,防止攻击者构造无效状态来取得控制。 因此,即使通常由 QEMU 自身生成,设备状态也应视为不可信。

客户机内存访问竞争

多 vCPU 客户机可能在设备模拟代码运行期间修改 RAM。 模拟代码必须复制描述符及其他客户机内存结构,只处理本地副本。 这可防止检查后使用前(TOCTOU)竞争: 设备处理内存时若 vCPU 同时修改它,可能导致 QEMU 崩溃。

使用 null-co 块驱动

null-co 块驱动面向性能,默认不初始化读取缓冲区。 若用于安全研究,必须使用 read-zeroes=on,使读取缓冲区填零。 使用默认 read-zeroes=off 时报告的安全问题不会受理。