模糊测试器开发:基于Bochs模拟器的内存加载与执行

本文详细介绍了如何开发一个基于Bochs模拟器的模糊测试器,包括ELF文件的内存加载、堆栈构建以及执行环境设置,旨在实现对复杂目标(如Linux内核)的全系统模糊测试。

模糊测试器开发1:新机器的灵魂

引言与致谢

长期以来,我一直想在博客上利用周末和空闲时间开发一个模糊测试器,但由于种种原因,始终未能构思出一个既有教育价值又能为模糊测试社区提供实用工具的项目。最近,出于Linux内核漏洞利用的需求,我对Nyx产生了浓厚兴趣。Nyx是一款基于KVM的虚拟机模糊测试器,可用于对传统上难以测试的目标进行快照模糊测试。

面对模糊测试中的目标复杂性(暂时抛开输入生成和细节问题),通常有两种方法:

  1. 目标简化:将目标的一部分隔离出来进行测试,例如将内核子系统的一小部分提取出来编译成用户态应用程序。但这种方法限制了可探索的目标状态。
  2. 全系统沙盒:将目标完全沙盒化,以便控制其执行环境并测试整个目标。这是Nyx等模糊测试器采用的方法。

在了解Nyx工作原理的过程中,我重新观看了Gamozolabs(Brandon Falk)对Nyx论文的流媒体评审。这次流媒体不仅展示了Nyx的强大功能,还提到了Gamozo之前构建的一个利用Bochs模拟器进行快照模糊测试的架构。这个架构与我正在设计的一个沙盒工具有很多共同之处,因此我决定在此基础上开发自己的模糊测试器。

项目选择标准

这个模糊测试架构满足了我对博客项目的多个标准:

  • 设计相对简单
  • 允许添加几乎无限的内省工具
  • 适合迭代开发周期
  • 可扩展并可用于我购买的服务器
  • 可测试Linux内核和其他操作系统(Windows、macOS)的用户态和内核组件
  • 设计与现有开源模糊测试工具相比非常独特
  • 可从零开始设计,与LibAFL等现有灵活工具良好配合
  • 没有公开源代码,可自由实现
  • 可移植到Windows等其他平台
  • 允许进行大量学习和底层计算研究

Bochs简介

Bochs是一款x86全系统模拟器,能够在软件模拟的硬件设备上运行整个操作系统。与QEMU相比,Bochs更小、更简单,但性能较差且用例较少。Bochs采用完全软件模拟的方式,不关心性能,这使得它成为运行x86系统的理想工具。我们将使用Bochs作为目标执行引擎,目标代码将在Bochs内部运行。

模糊测试器架构

根据Gamozo在流媒体中描述的信息,我们的模糊测试器设计如下:

  • 创建一个“模糊测试器”进程,该进程将执行Bochs,而Bochs又执行我们的测试目标。
  • 通过快照和恢复Bochs(包含目标及其所有模拟状态)来实现目标的快照和恢复。
  • 将Bochs沙盒化,使其无法与操作系统交互,从而确保其确定性。

内存加载ELF

为了将Bochs加载到内存中,我选择将其编译为-static-pie ELF文件。这种ELF文件没有加载位置的期望,其_start例程包含执行自身重定位所需的逻辑。

ELF头解析

我解析了ELF头和相关程序头,提取了加载段的信息。关键数据结构包括:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#[derive(Debug)]
pub struct ElfHeader {
    pub entry: u64,
    pub phoff: u64,
    pub phentsize: u16,
    pub phnum: u16,
    // ... 其他字段
}

#[derive(Debug)]
pub struct ProgramHeader {
    pub typ: u32,
    pub flags: u32,
    pub offset: u64,
    pub vaddr: u64,
    pub filesz: u64,
    pub memsz: u64,
    pub align: u64,
}

内存映射与段加载

使用mmap分配足够的内存来容纳可加载段,然后遍历程序头,将段数据从文件复制到分配的内存中,并使用mprotect设置适当的内存权限。

设置Bochs的堆栈

设置堆栈是中最复杂的部分。我需要构建一个包含参数向量(argv)、环境变量(envp)和辅助向量(auxv)的堆栈。辅助向量包含程序的关键信息,如入口点(AT_ENTRY)、程序头地址(AT_PHDR)和程序头数量(AT_PHNUM)。

堆栈构建逻辑

  1. 分配堆栈内存:使用mmap分配1MB的堆栈空间。
  2. 构建堆栈数据:从堆栈底部开始,依次放置结束标记、参数字符串、辅助向量、环境变量终止符、参数向量终止符和参数指针。
  3. 计算绝对地址:根据堆栈长度和字符串偏移量计算参数字符串的绝对地址。

辅助向量成员

我包含了以下辅助向量成员:

  • AT_ENTRY:程序入口点
  • AT_PHDR:程序头数据指针
  • AT_PHNUM:程序头数量
  • AT_RANDOM:随机种子指针(用于栈金丝雀值)
  • AT_NULL:辅助向量终止符

执行加载的程序

一切就绪后,通过汇编代码跳转到程序入口点并开始执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pub fn start_bochs(bochs: Bochs) {
    unsafe {
        asm!(
            "mov rax, {0}",
            "mov rsp, {1}",
            "xor rdx, rdx",
            "jmp rax",
            in(reg) bochs.entry,
            in(reg) bochs.rsp,
        );
    }
}

测试结果

测试程序成功运行,解析了命令行参数并正常退出。随后,我编译了Bochs作为-static-pie ELF并成功加载执行:

1
2
3
4
5
6
7
8
9
========================================================================
                        Bochs x86 Emulator 2.7
              Built from SVN snapshot on August  1, 2021
                Timestamp: Sun Aug  1 10:07:00 CEST 2021
========================================================================
Usage: bochs [flags] [bochsrc options]
  -n               no configuration file
  -f configfile    specify configuration file
  // ... 其他选项

下一步

接下来的步骤包括:

  1. 开发上下文切换例程,用于在模糊测试器和Bochs执行之间切换。
  2. 熟悉Bochs,尝试在 vanilla Bochs 中运行目标。
  3. 在模糊测试器中运行目标。

资源与致谢

  • 感谢Faster Than Lime的博客文章,帮助我了解如何在内
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计