介绍今天的论文之前,先问下我们的读者,有多少看过电影《土拨鼠日》或者读过当年《科幻世界》杂志上一篇很有名的作品《一日囚》?如果你读过,那么一定知道今天我们讨论的主题是什么;如果没有读过也不要紧,让我们一起去看看 USENIX Security 2023 的这篇论文 No Linux, No Problem: Fast and Correct Windows Binary Fuzzing via Target-embedded Snapshotting
开门见山,我们先回答一下前面的问题。对于一个程序,如果ta有意识,且被人类控制着反复执行,每次还给ta更换一些初始条件,这是不是很像我们在科幻电影和科幻小说中看到的一种经典主题——时间循环?实际上,最近几年安全会议上最为热门的主题之一的 fuzzing 就是这样一个过程,而且恶毒的人类为了让 fuzzing 的效率更高,还想了很多办法去让程序的“时间循环”更为高效(或者轻量化),这样在物理世界的有限时间内,就可以探索更多的代码(和发现更多问题)。
上图是本文中非常经典的一幅图,展示了当前流行的 fuzzing 技术在减少程序重复执行(特别是减少程序的初始化)上做的一些努力。对于 Linux 平台,有一项非常有趣的技术 kernel based process snapshoting,通过修改内核相关的一些 API,减少进程启动、执行等诸多方面的开销(这里面假定 fuzzing 和正常执行不太一样,很多时候并不需要完整实现很多方面的细节,完全可以把它们都裁剪掉)。而实际上早在2017年的 CCS 会议上,我们就对这项技术做过了报道,详见当时我们撰写的《ACM CCS 2017 会议每日报道:Day 3 & 4》(内容见 https://zhuanlan.zhihu.com/p/30751150 论文的第一作者还是从 G.O.S.S.I.P 出发前往佐治亚理工读博的许文)。
不幸的是,在 Linux 系统上可以大放异彩的 kernel based process snapshoting 在 Windows 平台却行不通。闭源的 Windows 内核并不允许用户对其进行定制和裁剪,也就无法把一台安装 Windows 系统的计算机(或者虚拟机)变成高效的 fuzzing 系统。为了解决这个问题,本文的作者提出了 Target-embedded Snapshotting
这项新技术,目标就是让 Windows 上的应用程序(特别是闭源的二进制代码)也能够“享受”到高效的 fuzzing 过程。
既然前提条件是不允许对内核进行修改,那么 Target-embedded Snapshotting
的核心科技就只能在程序的用户态进程空间施展手脚。作者指出,对于一个用户态进程,其主要的状态(process state)实际上也就关乎内存和寄存器的状态,更具体一点,可以从四方面——寄存器状态(register state)、本地栈状态(local stack state)、堆状态(heap memory state)、全局数据状态(global data state)——来刻画一个进程的上下文(context)。
当你想让一个程序“回到过去”,只需要把之前保存过的这些状态恢复,就实现了时光倒流。作者为 Target-embedded Snapshotting
技术开发了名为 WinFuzz
的原型系统,利用程序插桩技术,在程序启动时(执行到 main()
的时候)记录下寄存器状态、本地栈状态和全局数据状态;而相对比较复杂的是对堆内存的记录,WinFuzz
通过对堆管理器的核心 API 进行 hook 来监视所有分配和释放的内存(地址)。这种快照(snapshot)会在 WinFuzz
重置程序的执行状态时覆盖掉那些改变的内容,让程序回到“新鲜”的状态。当然,其中还有很多的细节需要考虑。
首先看看对于栈的处理,这个是比较简单的(如下图所示):当 WinFuzz
命令被测试的程序开始一个新的“时间循环”时,直接把 BP 和 SP 恢复到初始值,就好像 main
函数从来没有执行过任何子函数。
对于寄存器状态,论文在这里用了很多看起来非常啰嗦的文字去描述如何恢复其状态,但是最后的实现还是非常基本的直接全覆盖(如下图)。不知道作者想要表达什么。
在全局变量的恢复这一步骤中,WinFuzz
需要记录那些修改过的全局内存页,最后只恢复这些页。根据作者的说法,这种策略能够带来比较大的性能改善。
最后是最复杂的堆内存状态恢复,这里涉及到一个内存泄露的问题。如果只是还原堆内存的值,那些从 OS 申请来的内存页就会吃掉系统的总内存,最后导致系统内存耗尽。所以 WinFuzz
在实践中对内存管理器进行了 hook,记录了所有分配过的内存页,在重置程序状态之前,先要把这些页都释放掉,把内存资源还给系统。
当然,WinFuzz
并不能完美地复原程序的状态,一些系统级别的资源状态在程序运行过一次后就会发生改变,这个是无法修改内核的劣势之一。作者在这里只能采取“鸵鸟战术”,表示这些都是 corner case,以后可以通过对更多的系统 API 进行 hook 来完善。这里编辑部要插一句,如果真要让执行做到“无痕”,这是另一个研究方向,大家可以去看看 OSDI 2012年的一篇论文 Eternal Sunshine of the Spotless Machine: Protecting Privacy with Ephemeral Channels
在实验评估部分,作者和两个主流的 Windows Fuzzer:Winnie
(高性能)和 WinAFL
(最多人使用)进行了对比。首先是兼容性对比:
其次是性能对比:
最后是代码覆盖率的对比:
不过,作者在论文中声称已经放出了代码,但是现在 USENIX Security 会议已经结束了,相关的 repo 甚至是 404 not found 的状态,这样的科研作风可是不怎么值得表扬:
https://github.com/FoRTE-Research/winfuzz
论文:https://www.usenix.org/system/files/usenixsecurity23-stone.pdf