执行器 Executor
在不同的fuzzers中,这种执行被测程序的概念意味着每次运行都是一样的。 例如,对于像 libFuzzer 这样的内存模糊器来说,执行是对一个被测试函数的调用,而对于像 kAFL 这样的基于管理程序的模糊器来说,每次运行都会从一个快照启动整个操作系统。
在我们的模型中,执行者(Executor) 是一个实体,它不仅定义了如何执行目标,还定义了所有与目标的单一运行有关的易失性操作。
因此,执行者负责告知程序模糊器要在运行中使用的输入,例如写到一个内存位置或作为参数传递给约束函数。
在我们的模型中,它还可以持有一组与每个执行程序相关的观察者。
在 Rust 中,我们将这个概念与 Executor
trait 绑定。实现这个特性的结构如果想持有一组观察者,也必须实现 HasObservers
。
默认情况下,我们实现了一些常用的执行器,如 InProcessExecutor
,其目标是提供进程中崩溃检测的线程函数。另一个执行器是 ForkserverExecutor
,它实现了一个类似于 AFL 的机制,用来生成子进程进行模糊处理。
在创建执行器时,一个常见的模式是包装现有的执行器,例如 TimeoutExecutor
包装了一个执行器,并在调用被包装的执行器的原始运行函数之前安装一个超时回调。
InProcessExecutor
让我们从基本情况开始: InProcessExecutor
。
这个执行器使用 SanitizerCoverage 作为其后端,你可以在 libafl_targets/src/sancov_pcguards
中找到相关代码。在这里,我们分配了一个名为 EDGES_MAP
的 map,然后我们的编译器包装器编译约束函数,将覆盖率写入这个 map 中。
当你想尽可能快地执行约束函数时,你很可能想使用 InprocessExecutor
。
这里需要注意的是,当你的约束函数有可能出现堆损坏的问题时,你要使用另一个分配器,这样损坏的堆就不会影响到模糊器本身。(例如,我们在一些模糊器中采用 MiMalloc) 。另外,你也可以启用 AddressSanitizer 来编译你的约束函数,以确保你能捕捉到这些堆的错误。
ForkserverExecutor
接下来,我们来看看 ForkserverExecutor
。在这种情况下,是 afl-cc
(来自AFLplus/AFLplus) 在编译约束函数代码,因此,我们不能再使用 EDGES_MAP
。希望我们有 a way 告诉 forkserver 哪个 map 来记录覆盖率。
你可以从 forkserver 的例子中看到:
//Coverage map shared between observer and executor
let mut shmem = StdShMemProvider::new().unwrap().new_shmem(MAP_SIZE).unwrap();
//let the forkserver know the shmid
shmem.write_to_env("__AFL_SHM_ID").unwrap();
let mut shmem_buf = shmem.as_mut_slice();
这里我们建立一个共享内存区域: shmem
,并将其写入环境变量 __AFL_SHM_ID
。然后,被检测的二进制文件或 forkerver 会找到这个共享内存区域 (来自上述环境变量) 来记录其覆盖范围。在你的fuzzer方面,你可以将这个shmem map传递给你的 Observer
,以获得与任何 Feedback
相结合的覆盖率反馈。
ForkserverExecutor
的另一个特点是共享内存测试案例。在正常情况下,突变的输入是通过 .cur_input
文件在 forkerver 和被测二进制之间传递。你可以通过用共享内存传递输入来提高你的 forkserver 模糊器的性能。
参见 AFL++ 的 documentation 或 forkserver_simple/src/program.c
中的fuzzer例子以供参考。
这很简单,当你调用 ForkserverExecutor::new()
并将 use_shmem_testcase
设为true时,ForkserverExecutor
会将事情设置好,你的约束函数就可以从 __AFL_FUZZ_TESTCASE_BUF
获取输入。
InprocessForkExecutor
最后,我们来谈谈 InProcessForkExecutor
。
InProcessForkExecutor
与 InprocessExecutor
只有一个区别: 它在运行约束函数之前进行 fork,仅此而已。
但为什么我们要这样做呢?好吧,在某些情况下,你可能会发现你的约束函数非常不稳定,或者你的约束函数对全局状态造成了破坏。在这种情况下,你想在子进程中执行约束函数运行之前将其 fork,这样就不会破坏事情。
然而,我们必须照顾到共享内存,是子进程在运行约束函数代码,并将覆盖范围写到 map 上。
我们必须使 map 在父进程和子进程之间共享,所以我们将再次使用共享内存。你应该用 pointer_maps
(用于 libafl_targes
) 功能来编译你的约束函数,这样,我们可以有一个指针: EDGES_MAP_PTR
,可以指向任何覆盖图。
在你的fuzzer方面,你可以分配一个共享内存区域,让 EDGES_MAP_PTR
指向你的共享内存。
let mut shmem;
unsafe{
shmem = StdShMemProvider::new().unwrap().new_shmem(MAX_EDGES_NUM).unwrap();
}
let shmem_buf = shmem.as_mut_slice();
unsafe{
EDGES_PTR = shmem_buf.as_ptr();
}
同样,你可以把这个 shmem
map 传递给你的 Observer
和 Feedback
以获得覆盖率反馈。