安全编码实践
本文介绍开发者和安全研究人员都需要了解的主题, 帮助编写安全代码并正确审计现有代码。
报告安全缺陷
如何报告安全缺陷,或询问潜在安全问题, 请参阅 安全流程 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 时报告的安全问题不会受理。