构建新一代快照模糊测试工具与IDA实战

本文详细介绍了从零开发基于快照的模糊测试工具wtf的完整过程,涵盖Bochscpu、WHV和KVM三种后端实现,以及针对IDA Pro的实战模糊测试,涉及代码覆盖率、内存追踪、崩溃检测等核心技术。

构建新一代快照模糊测试工具与IDA实战

引言

2020年1月,我决定开发自定义模糊测试工具。在与yrp604讨论后,我们决定构建一个可用于模糊测试任何Windows目标(用户/内核模式、应用/服务、内核/驱动)的工具。计划开发基于Windows快照的模糊测试器,在VM或模拟器中运行目标代码,允许用户通过断点进行插桩,并提供现代模糊测试器应有的基本功能:代码覆盖率、崩溃检测、通用变异器、跨平台支持、快速恢复等。

为了验证工具实用性,我选择IDA Pro作为目标,原因包括:它是复杂的Windows用户模式应用、解析多种二进制文件、启动缓慢(快照方法可加速测试)、且存在漏洞赏金计划。

架构设计

用户使用流程:

  1. 在目标中寻找接近处理攻击者控制数据的位置,使用Windows内核调试器中断并置目标于所需状态
  2. 生成内核转储并提取CPU状态
  3. 编写模块告知wtf如何插入测试用例,定义停止条件

核心组件:

  • kdmp-parser: C++库解析Windows内核转储
  • bdump.js: Windbg扩展用于提取CPU状态和MSR
  • bochscpu: Bochs的CPU组件,通过Rust库提供C绑定

Bochscpu基础

内存管理

Bochscpu采用惰性内存加载机制,通过回调处理未映射的物理内存访问:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void StaticGpaMissingHandler(const uint64_t Gpa) {
  const Gpa_t AlignedGpa = Gpa_t(Gpa).Align();
  uint8_t *Page = (uint8_t *)aligned_alloc(Page::Size, Page::Size);
  
  if(DmpPage) {
    memcpy(Page, DmpPage, Page::Size);
  } else {
    memset(Page, 0, Page::Size);
  }
  
  bochscpu_mem_page_insert(AlignedGpa.U64(), Page);
}

CPU状态加载

通过bochscpu_cpu_set_state设置完整的CPU状态,包括寄存器、MSR、段描述符等。

执行钩子

注册多种钩子函数获取执行信息:

1
2
3
4
5
Hooks_.ctx = this;
Hooks_.after_execution = StaticAfterExecutionHook;
Hooks_.before_execution = StaticBeforeExecutionHook;
Hooks_.lin_access = StaticLinAccessHook;
// ... 其他钩子

基础功能构建

内存访问设施

提供物理和虚拟内存读写能力。虚拟内存访问需要模拟MMU和解析页表。

执行流插桩

通过指令执行通知实现断点功能:

1
2
3
4
5
6
void BochscpuBackend_t::BeforeExecutionHook() {
  const Gva_t Rip = Gva_t(bochscpu_cpu_rip(Cpu_));
  if(Breakpoints_.contains(Rip)) {
    Breakpoints_.at(Rip)(this);
  }
}

无限循环处理

通过指令计数限制测试用例执行:

1
2
3
4
5
6
7
8
void BochscpuBackend_t::AfterExecutionHook() {
  RunStats_.NumberInstructionsExecuted++;
  if(InstructionLimit_ > 0 && 
     RunStats_.NumberInstructionsExecuted > InstructionLimit_) {
    TestcaseResult_ = Timedout_t();
    bochscpu_cpu_stop(Cpu_);
  }
}

代码覆盖率追踪

每次指令执行时记录地址:

1
2
3
4
5
6
7
void BochscpuBackend_t::BeforeExecutionHook() {
  const Gva_t Rip = Gva_t(bochscpu_cpu_rip(Cpu_));
  const auto &Res = AggregatedCodeCoverage_.emplace(Rip);
  if(Res.second) {
    LastNewCoverage_.emplace(Rip);
  }
}

脏内存追踪

通过内存访问钩子跟踪写入的内存:

1
2
3
4
5
6
7
void BochscpuBackend_t::LinAccessHook() {
  if(MemAccess != BOCHSCPU_HOOK_MEM_WRITE &&
     MemAccess != BOCHSCPU_HOOK_MEM_RW) {
    return;
  }
  DirtyGpa(Gpa_t(PhysicalAddress));
}

通用变异器

集成libfuzzer和honggfuzz的变异策略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class LibfuzzerMutator_t {
  fuzzer::Random Rand_;
  fuzzer::MutationDispatcher Mut_;
  // ...
};

class HonggfuzzMutator_t {
  honggfuzz::dynfile_t DynFile_;
  // ...
};

IDA实战测试

测试用例插入

通过文件系统钩子层处理IDA的文件I/O操作,将实际文件读入缓冲区并通过钩子传递字节。

遇到的问题与解决方案

问题1: 预加载DLL 解决方案:在生成快照前使用inject工具将DLL注入IDA进程。

问题2: 换出内存 解决方案:使用lockmem工具锁定进程工作集中的所有虚拟内存范围。

问题3: 手动触发页错误 通过虚拟到物理地址转换检查页表状态,必要时插入页错误。

问题4: KVA Shadow 通过注册表编辑禁用KVA Shadow缓解措施。

问题5: 性能瓶颈识别 使用Intel V-Tune Profiler分析性能问题,发现内存访问错误处理耗时过长。

WHV后端实现

代码覆盖率

使用基本块起始处的一次性软件断点,需要预先生成断点地址JSON文件。

脏内存追踪

使用WHVQueryGpaRangeDirtyBitmap接口跟踪脏内存。

确定性执行

通过断点钩子使nt!ExGenRandom的rdrand指令行为确定化。

性能优化

实现Ram_t类通过内存换CPU时间的方式优化断点恢复性能。

KVM后端实现

共享内存GPR访问

通过mmap映射共享内存区域访问Guest寄存器。

按需分页

使用userfaultfd实现按需分页:

1
2
3
4
5
6
void KvmBackend_t::UffdThreadMain() {
  while(!UffdThreadStop_) {
    struct pollfd PoolFd = {.fd = Uffd_, .events = POLLIN};
    // ... 处理页错误
  }
}

超时机制

通过PMU(性能监控单元)在指定指令数后触发中断。

分布式架构

将wtf重构为客户端-服务器架构,服务器维护覆盖率、语料库和变异器,客户端作为运行器返回结果,充分利用硬件资源进行语料库最小化。

成果总结

通过wtf发现了IDA多个组件(libdwarf64.dll、dwarf64.dll、elf64.dll、pdb64.dll)中的数十个独特崩溃,包括空指针解引用、栈溢出、除零错误、无限循环、释放后使用和越界访问等问题。

根据lighthouse数据,实现了elf64.dll约80%、dwarf64.dll约50%、libdwarf64.dll约26%的代码覆盖率,使用约2.4k个文件(总计17MB)的最小语料集。

工具开发过程中还创建并开源了多个辅助项目:lockmem、inject、kdmp-parser和symbolizer。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计