Tldr

通过 system mode 获取依赖,通过 user mode 加速 fuzzing。

我对这一篇文章还是蛮感兴趣的。看作者是 Firm-AFL 的团队做的,目的也是为了解决 FirmAFL 的痛点。

先看看概要:

  • EQLAFL:首先在 system mode 下执行程序,观察程序在 user mode 卡住/崩溃的关键点,根据观察到的信息,将 system mode 迁移为 user mode。然后 EQLAFL 使用 user mode 仿真重放网络相关的系统调用和资源管理行为。
    • 那么这个 system mode 是从什么时候开始的,操作系统执行的时候吗?
    • 为了迁移到 user mode,我需要同时保持一份 system mode 的镜像吗?
  • EQUAFL 采用观察重放策略。首先,EQUAFL 通过全系统仿真执行被测程序,并观察系统的关键行为,如设置启动变量、生成配置文件、网络操作等。
    • 关键行为怎么存呢
  • 然后,EQUAFL 将回放观察到的系统行为,以建立被测程序的执行环境。不同的系统行为需要不同的观察和重放方法,其中最复杂的两种行为是动态配置文件生成和网络交互。
    • 重放是类似于模拟执行并重放吗,在什么情况下的程序可以重放?如果是在启动的时候加载固件这种的该怎么做呢?
  • 动态配置文件生成和网络交互这两种行为最为复杂,前者需要在观察过程中感知进程,在重放过程中同步文件系统。后者则需要状态感知观察和重放。

看一下背景:

  • AFL+qemu user emulation:局限性比较大
  • AFL+qemu system emulation:速度很慢,作者认为不可行
    • 有人在现实中实现过吗
      • 似乎可以参考 WTF 做镜像 fuzz。
  • AFL+qemu hybrid emulation:
    • 例子:Firm-AFL,在 user mode 执行代码,将系统调用重定向到 system mode
      • 问题:频繁出现 syscall 速度还是很慢。

一些定义:

  • PUT: program under test,被测程序

工作流程:

两个主要步骤:观察和重放

  • 观察阶段,EQUAFL 通过 system mode 执行被测程序,观察系统的关键行为;
    • 作者认为的关键行为包括:
      • 设置启动变量
      • 动态配置文件生成(复杂)
      • NVRAM 相关操作
      • 网络交互(复杂)
        • 解决方案:状态感知,4.4 章
      • 进程资源限制
  • 重放阶段,EQUAFL 通过在主机上部署动态配置文件等系统资源或在用户模式仿真期间执行系统调用拦截来进行重放。
    • 看看具体是咋实现的吧

第四章 方法

4.1 设置启动变量

所谓的启动变量指的就是被测程序启动的时候的一堆依赖,在 IoT 设备上可能的依赖千奇百怪,有系统变量、进程约束、各种参数以及有可能包含其他进程(某个进程必须在运行状态才能启动被测程序)等等,看看作者怎么解决。

文章建议,通过对 Linux 内核做静态分析,对 system emulation 做运行时分析获取名称/参数/环境变量值。

  • 观察阶段
    • 检查 do_execve 函数转存启动程序的名称/参数/环境变量
      • 作者们总结出了一种二进制模式,可以在仿真固件时找到准确的转存这些变量的指令
        • 看来是得 Read The Friendly Code 了
        • 发现三条函数指令,一条调用 copy_strings_kernel,两条调用 copy_strings 包含可以用于计算参数地址的寄存器
        • 选择 qemu system mode 中 search_binary_handler 这一入口点
  • 重放阶段
    • 有了启动参数就可以启动程序了
      • 但是,如果依赖其他进程的怎么办呢?好像没有讲哎,尤其是如果和父进程共享内存的就更麻烦了。

4.2 文件系统状态同步

  • 作者们发现在 system mode 时固件会挂载临时文件系统,在初始化阶段更改文件状态,但是 user mode 是做不到的。
    • 但其实做不到的可能更多,如果是动态加载的镜像该怎么解决捏
  • 文中提出 observe-replay(观察-重放)策略同步 guest 和 host 之间的文件系统状态
    • 本质上是镜像 guest 的操作,guest 的操作同步在 host 上进行
    • 直到检测到 PUT 开始执行
    • 但是在主机上进行重放操作时,有些文件的系统调用参数可能是未知的,如下图所示:

  • 作者说,在上图中重新执行写系统调用时会无法指定 fd_host 的值
    • 我没看懂
    • 在观察阶段的时候,guest 存在三个操作:open、dup 和 fork。
      • open 打开 httpd.conf
      • dup 复制 fd_guestfd'_guest
      • fork 一个子进程 p'
    • 观察阶段的 host 只做了一个操作:open
    • 在复现阶段,guest 会写进程 p*fd*_guest
    • 但是 host 不知道写哪个 fd
    • 是这个意思吗?接着往后看
  • 作者们提出了一种进程感知的观察方法,在 guest 和 host 之间建立映射

准确的流程识别

  • 这一步识别 guest 中正在执行的进程,分为两步
    1. 进程收集:收集模拟过程中所有进程的信息
      • 在 fork 和 execve 结束后收集新生成的进程信息
      • 遍历 task_struct 找到新生成的进程
        • 收集 PGD(page global directory)、PID(process identifier)、PPID(PID of the parent process) 等信息
    2. 进程推理
      • 监控特定寄存器或内存区域获取当前执行进程的 PGD

过程感知的观察阶段

  • 这一步是用来观察文件相关的系统调用执行情况
    • 首先过滤不影响文件状态/不修改文件内容的系统调用
    • 然后根据参数将剩余的系统调用分为两类,如下表所示
      1. 直接处理文件路径
      2. 处理文件描述符
Argument TypeSystem Calls
File pathsmount, mkdir, rmdir, mkdirat, link, symlink, unlink
File descriptoropen, read, write, dup, dup2, dup3, create, fcntl, pipe
  • 对于文件描述符,无法在 host 上获取相应文件描述符
    • 作者们通过文件与进程感知的关系解决,以上面的图 2 为例:
      • 监控创建文件的 syscall(如 open),获取 fd_guest
      • 确定 guest 上执行系统调用的进程 p
      • 在 host 上重新执行相同的系统调用,获取 fd_host
      • 如果 guest 执行 dup 之类的系统调用创建文件描述符的副本,将相关的文件描述符加入 fd_guest 集合中
      • 然后将相关的子进程都关联到集合 p
      • 接着创建映射 M,如果某个进程属于 p,并且它在文件描述符 fd_guest 上执行了其他与文件相关的系统调用,就在宿主机的 fd_host 上镜像执行系统调用
        • 算是解决了图 2 的疑惑
        • 但还是要看具体实现咯
  • 重放阶段
    • 在 host 中执行和 guest 相同的系统调用
      • 感觉真没必要把一个字符串拼接算法写成伪代码……

4.3 设置 NVRAM

  • 观察阶段
    • 先进的全系统仿真技术(如 FIRMADYNE)分配常规文件存储 NVRAM 配置的数据
      • 将相关 API 重定向到常规文件实现对 NVRAM 访问的模拟
      • 由于 4.2 章已经实现了文件系统的同步,因此 NVRAM 配置也会在 host 上生成
  • 重放阶段
    • 因为最终是在 user mode 执行,因此将对 NVRAM 的访问重定向到 host 中
      • 使用 LD_PRELOAD 技术 hook

4.4 网络行为

  • 通过观察到的网络相关 syscall 序列学习状态机
    • 基于状态机模拟 PUT 的网络行为
  • 状态感知的观察阶段
    • 监视 socket 参数筛选出网络相关的 socket
    • Kernel 会按一定顺序发出 syscall,通过下图所示的状态机直到模拟网络行为:

  • 同样也可以模拟 I/O 复用,对图上 poll/select 的解释:
    1. 当程序第一次在 fd_net 上执行 poll 时,fd_net 已经准备好连接并设置了 ready 状态。新的描述符 fd'_net 被接受;
    2. 程序在 fd'_net 上执行轮询时,fd'_net 设置为 ready 状态并等待数据
    3. 第三次执行 poll 时,fd'_net 已经收到了数据,大多数情况下可以终止当前的模糊测试,进入下一轮
  • 重放阶段
    • 按照上面图 3 的状态机模拟网络调用

4.5 进程资源限制

  • 某些情况下 PUT 会遍历所有系统描述符
    • guest 的设置与 host 不同
    • 过大的 RLIMIT_NOFILE 会影响效率
  • 还是通过观察-重放获取 guest 程序的限制,最后 hook 掉。

第五章 实现

  • 模拟仿真实现
    • 观察阶段:利用 FIRMADYNE 执行 system emulation(以及系统调用的监视)
    • 重放阶段:
      • 既然说了是用户模式,那就在 host 上运行
      • 利用 IDA Pro 找到转存启动变量的准确位置(这是人工逆向喂)
      • binwalk+chroot 提取/切换文件系统
  • fuzzing 实现
    • 何时结束程序?
      • 监视网络请求,如果已经准备好接收新请求就结束
    • 何时开始 fuzzing?
      • 检测接收网络输入的系统调用发送输入

第六章 实验

  • 数据集:
    • 第一组:nbench 和 lmbench
    • 第二组:来自 D-Link、TrendNet、NetGear 的系统镜像

好了,talk is cheap,我去读代码了。

参考资料