Although American Fuzzy Lop comes with a couple of nifty performance optimizations, it still relies on a fairly resource-intensive routine that is common to most general-purpose fuzzers: it continually creates new processes, feeds them a single test case, and then discards them to start over from scratch.
To avoid the overhead of the notoriously slow execve() syscall and the linking process, the fuzzer automatically leverages the forkserver optimization, where new processes are cloned from a copy-on-write master perpetually kept in a virgin state. This allows many targets to be fuzzed faster than with other, conventional tools. But even with this hack, each new input still incurs the cost of fork(). On all supported OSes with the exception of MacOS X, the fork() call is actually surprisingly fast - but certainly does not come free.
For some common fuzzing targets, such as zlib or libpng, the constant cycle of forking and initialization is a significant and avoidable bottleneck. In many cases, the underlying APIs are either stateless, or can be reliably reset to a nearly-pristine state across inputs - so at least in principle, you don't have to throw away the child process after every single run. That's where in-process fuzzing tends to shine: in this scheme, the test cases are generated inline and fed to the underlying API in a custom-written, single-process loop. The speed gains offered by in-process fuzzing can be as high as 10x, but the approach comes at a price; for example, it is easily derailed by accidental memory leaks or DoS conditions in the tested code.
Well, the good news is that starting with version 1.81b, afl-fuzz supports an optional "persistent" mode that combines the benefits of in-process fuzzing with the robustness of a more traditional multi-process tool. In this scheme, the fuzzer feeds test cases to a separate, long-lived process that reads the input data, passes it to the instrumented API, notifies the parent about successful run by stopping its own execution; eventually, when resumed by the parent, the process simply loops back to the start. You just need to write a minimalist harness to implement the loop, but AFL takes care of most of the tricky stuff, including crash handling, stall detection, and the usual instrumentation magic that AFL is designed for:
int main(int argc, char** argv) {
while (__AFL_LOOP(1000)) {
/* Reset state. */
memset(buf, 0, 100);
/* Read input data. */
read(0, buf, 100);
/* Parse it in some vulnerable way. You'd normally call a library here. */
if (buf[0] != 'p') puts("error 1"); else
if (buf[1] != 'w') puts("error 2"); else
if (buf[2] != 'n') puts("error 3"); else
abort();
}
}
For a more complete example, see experimental/persistent_demo/ and be sure to read the last section of llvm_mode/README.llvm. This feature is inspired by the work done by Kostya Serebryany on LibFuzzer (which is, in turn, inspired by AFL); additional credit goes to Christian Holler, who started a conversation that finally prompted me to integrate this mode with the tool.
0 nhận xét:
Đăng nhận xét