Effective file format fuzzing (2)

来源:互联网 发布:李洋演过过网络剧 编辑:程序博客网 时间:2024/06/10 12:06

AFL the first to introduce and ship this at large

From lcamtuf’s technical whitepaper:

cur_location = <COMPILE_TIME_RANDOM>;shared_mem[cur_location ^ prev_location]++;prev_location = cur_location >> 1;

Implemented in the fuzzer’s own custom instrumentation.

Extending the idea even further

In a more abstract sense, recording edges is recording the current block + one previous.

  • What if we recorded N previous blocks instead of just 1?
  • Provides even more context on the program state at a given time, and how execution arrived at that point.
  • Another variation would be to record the function call stacks at each basic block.

In my experience N = 1 (direct edges) has worked very well, but more experimentation is required and encouraged.

Counters and bitsets

Let’s abandon the “basic block” term and use “trace” for a single unit of code coverage we are capturing (functions, basic blocks, edged, etc.).
In the simplest model, each trace only has a Boolean value assigned in a coverage log: REACHED or NOTREACHED.
More useful information can be found in the specific, or at least more precise(精确) number of times it has been hit.

  • Especially useful in case of loops, which the fuzzer could progress through by taking into account the number of iterations.
  • Implemented in AFL, as shown in the previous slide.
  • Still not perfect, but allows some granular information related to |program states| to be extracted and used for guiding.

Extracting all this information

For closed-source programs, all aforementioned data can be extracted by some simple logic implemented on top of Intel Pin or DynamoRIO.

  • AFL makes use of modified qemu-user to obtain the necessary data.

For open-source, the gcc and clang compilers offer some limited support for code coverage measurement.

  • Look up gcov and llvm-cov.
  • I had trouble getting them to work correctly in the past, and quickly moved to another solution…

SanitizerCoverage!

Enter the SanitizerCoverage

Anyone remotely interested in open-source fuzzing must be familiar with the mignty AddressSanitizer.

  • Fast, reliable C/C++ instrumentation for detecting memory safety issues for clang and gcc (mostly clang).
  • Also a ton of other run time sanitizers by the same authors: MemorySanitizer (use of uninitialized memory), ThreadSanitizer (race conditions), UndefinedBehaviorSanitizer, LeakSanitizer (memort leaks).

A definite must-use tool, compile your targets with it whenever you can.

ASAN, MSAN and LSAN together with SanitizerCoverage can now also record and dump code coverage at a very small overhead, in all the different modes mentioned before.

  • Thanks to the combination of a sanitizer and coverage recorder, you can have both error detection and coverage guidance in your fuzzing session at the same time.

LibFuzzer, Kostya’s own fuzzer, also uses SanitizerCoverage (via the inprocess programmatic API).

Sanitizer Coverage usage

% cat -n cov.cc    1 #include <stdio.h>    2 _attribute_((noinline))    3 void foo() { printf("foo\n"); }    4     5 int main(int arcg, char **argv) {    6     if (argc == 2)    7         foo();    8     printf("main\n");    9 }% clang++ -g cov.cc -fsanitize=address -fsanitize-coverage=func% ASAN_OPTIONS=coverage=1 ./a.out; ls -l *sancovmain-rw-r----- 1 kcc eng 4 Nov 27 12:21 a.out.22673.sancov% ASAN_OPTIONS=coverage=1 ./a.out foo ; ls -l *sancovfoomain-rw-r----- 1 kcc eng 4 Nov 27 12:21 a.out.22673.sancov-rw-r----- 1 kcc eng 8 Nov 27 12:21 a.out.22679.sancov

So, we can measure coverage easily.

Just measuring code coverage isn’t a silver bullet by itself.

  • But still extremely useful, even the simplest implementation is better then no coverage guidance.

There are still many code constructs which are impossible to cross with a dumb mutation-based fuzzing.

  • One-instruction comparisons of types larger than a byte (uint32 etc.), especially with magic values.
  • Many-byte comparisons performed in loops, e.g. memcmp(), strcmp() calls etc.

Hard code constructs: examples

uint32_t value = load_from_input();if (value == 0xDEADBEEF) {    // Special branch.}

Comparison with a 32-bit constant value

char buffer[32];load_from_input(buffer, sizeof(buffer));if (!strcmp(buffer, "Some long expected string")) {    // Special branch.}

Comparison with a long fixed string

The problems are somewhat approachable

Constant values and strings being com pared against may be hard in a completely context-free fuzzing scenario, but are easy to defeat when some program/formatspecific knowledge is considered.

  • Both AFL and LibFuzzer support “dictionsries”.
  • A dictionary may be created manually by feeding all known format signatures, etc. Can be then easily reused for fuzzing another implementation of the same format.
  • Can also be generate automatically, e.g. by disassembling the target program and recording all constants used in instructions such as:
cmp r/m32, imm32

Compiler flags may come helpful… or not

A somewhat intuitive approach to building the target would be to disable all code optimizations.

  • Fewer hacky expressions in assembly, compressed code constructs, folded basic blocks, complicated RISC-style x86 instructions etc. -> more granular coverage information to analyze.
  • On the contrary, lcamtuf discovered that using -03 -funroll-loops may result in unrolling short fixed-string comparisons suchas strcmp(buf, “foo”) to:
cmpb $0x66,0x200c32(%rip) # 'f'jne 4004b6cmpb $0x6f,0x200c2a(%rip) # 'o'jne 4004b6cmpb $0x6f,0x200c22(%rip) # 'o'jne 4004b6cmpb $0x0,0x200c1a(%rip)  # NULjne 4004b6

It is quite unclear which compilation flags are most optimal for coverage-guided fuzzing.

  • Probably depends heavily on the nature of tested software, requiring case-by-case adjustments.

Past encounters

原创粉丝点击