好久没写过 blog 了,这篇主要分享 Jazzer 的 Mutator 设计。Jazzer 是一款 libfuzzer 套壳 fuzzer,在我看来的主要功能是提供了对 Java 的 fuzzing 能力。

1. libfuzzer

Libfuzzer 允许用户自定义 LLVMFuzzerCustomMutator / LLVMFuzzerMutate

src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp 中,定义了 LLVMFuzzerCustomMutatorLLVMFuzzerCustomCrossOver。Mutate 和 CrossOver 都属于高中生物的概念,这里只是一个借用,大概意思也差不多。因此可以注意到 Mutate 总是对一个 uint8_t *Data 操作,而 CrossOver 需要两条 DNA:

1
2
3
4
5
6
7
extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
size_t MaxSize, unsigned int Seed);

extern "C" size_t LLVMFuzzerCustomCrossOver(const uint8_t *Data1, size_t Size1,
const uint8_t *Data2, size_t Size2,
uint8_t *Out, size_t MaxOutSize,
unsigned int Seed);

2. Jazzer —experimental_mutator

1
2
3
4
5
6
// We always define LLVMFuzzerCustomMutator, but only use it when --experimental_mutator is
// specified. libFuzzer contains logic that disables --len_control when it finds the custom
// mutator symbol:
// https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L202-L207
// We thus have to explicitly set --len_control to its default value when not using the new
// mutator.

只有 --experimental_mutator 参数指定了之后才会使用 LLVMFuzzerCustomMutator,否则一直都是使用 LLVMFuzzerMutate。但是这块逻辑并不在 jazzer 的 java 代码中,而是在 c++ 部分。我们继续看 LLVMFuzzerCustomMutator 代码,这次贴出全部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
size_t MaxSize, unsigned int Seed) {
if (gUseExperimentalMutator) {
JNIEnv &env = *gEnv;
jint jsize =
std::min(Size, static_cast<size_t>(std::numeric_limits<jint>::max()));
jint jmaxSize = std::min(
MaxSize, static_cast<size_t>(std::numeric_limits<jint>::max()));
jint jseed = static_cast<jint>(Seed);
jint newSize = env.CallStaticLongMethod(gRunner, gMutateOneId, Data, jsize,
jmaxSize, jseed);
if (env.ExceptionCheck()) {
env.ExceptionDescribe();
_Exit(1);
}
return static_cast<uint32_t>(newSize);
} else {
return LLVMFuzzerMutate(Data, Size, MaxSize);
}
}

可见,是否执行默认的 LLVMFuzzerMutate 是由 gUseExperimentalMutator 这个全局变量决定的,而这个全局变量的赋值也在同一个文件下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[[maybe_unused]] jint
Java_com_code_1intelligence_jazzer_runtime_FuzzTargetRunnerNatives_startLibFuzzer(
JNIEnv *env, jclass, jobjectArray args, jclass runner,
jboolean useExperimentalMutator) {
gUseExperimentalMutator = useExperimentalMutator;
gEnv = env;
env->GetJavaVM(&gJavaVm);
gRunner = reinterpret_cast<jclass>(env->NewGlobalRef(runner));
gRunOneId = env->GetStaticMethodID(runner, "runOne", "(JI)I");
gMutateOneId = env->GetStaticMethodID(runner, "mutateOne", "(JIII)I");
gCrossOverId = env->GetStaticMethodID(runner, "crossOver", "(JIJIJII)I");

// ignore some code

return LLVMFuzzerRunDriver(&argc, const_cast<char ***>(&argv), testOneInput);
}

可见这里的 useExperimentalMutator 是从 java 那边传过来的。我们可以追踪到调用这个函数的 java 代码位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Starts libFuzzer via LLVMFuzzerRunDriver.
*
* @param args command-line arguments encoded in UTF-8 (not null-terminated)
* @return the return value of LLVMFuzzerRunDriver
*/
private static int startLibFuzzer(byte[][] args) {
return FuzzTargetRunnerNatives.startLibFuzzer(
args, FuzzTargetRunner.class, useExperimentalMutator);
}

/*
* Starts libFuzzer via LLVMFuzzerRunDriver.
*/
public static int startLibFuzzer(List<String> args) {
// We always define LLVMFuzzerCustomMutator, but only use it when --experimental_mutator is
// specified. libFuzzer contains logic that disables --len_control when it finds the custom
// mutator symbol:
// https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L202-L207
// We thus have to explicitly set --len_control to its default value when not using the new
// mutator.
if (!useExperimentalMutator) {
// args may not be mutable.
args = new ArrayList<>(args);
// https://github.com/llvm/llvm-project/blob/da3623de2411dd931913eb510e94fe846c929c24/compiler-rt/lib/fuzzer/FuzzerFlags.def#L19
args.add("-len_control=100");
}

for (String arg : args.subList(1, args.size())) {
if (!arg.startsWith("-")) {
Log.info("using inputs from: " + arg);
}
}

if (!IS_ANDROID) {
SignalHandler.initialize();
}
return startLibFuzzer(
args.stream().map(str -> str.getBytes(StandardCharsets.UTF_8)).toArray(byte[][] ::new));
}

笑死,这不正是写上面注释的地方吗。

3. libfuzzer 中的 CustomMutator

上面只是说 LLVMFuzzerCustomMutator 被执行到且 useExperimentalMutator == true 的时候,会执行 custom 的部分。但也有可能不会执行到这个 LLVMFuzzerCustomMutator 里,这是因为 libfuzzer 的 MutatationDispatcher 逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
MutationDispatcher::MutationDispatcher(Random &Rand,
const FuzzingOptions &Options)
: Rand(Rand), Options(Options) {
DefaultMutators.insert(
DefaultMutators.begin(),
{
{&MutationDispatcher::Mutate_EraseBytes, "EraseBytes"},
{&MutationDispatcher::Mutate_InsertByte, "InsertByte"},
{&MutationDispatcher::Mutate_InsertRepeatedBytes,
"InsertRepeatedBytes"},
{&MutationDispatcher::Mutate_ChangeByte, "ChangeByte"},
{&MutationDispatcher::Mutate_ChangeBit, "ChangeBit"},
{&MutationDispatcher::Mutate_ShuffleBytes, "ShuffleBytes"},
{&MutationDispatcher::Mutate_ChangeASCIIInteger, "ChangeASCIIInt"},
{&MutationDispatcher::Mutate_ChangeBinaryInteger, "ChangeBinInt"},
{&MutationDispatcher::Mutate_CopyPart, "CopyPart"},
{&MutationDispatcher::Mutate_CrossOver, "CrossOver"},
{&MutationDispatcher::Mutate_AddWordFromManualDictionary,
"ManualDict"},
{&MutationDispatcher::Mutate_AddWordFromPersistentAutoDictionary,
"PersAutoDict"},
});
if(Options.UseCmp)
DefaultMutators.push_back(
{&MutationDispatcher::Mutate_AddWordFromTORC, "CMP"});

if (EF->LLVMFuzzerCustomMutator)
Mutators.push_back({&MutationDispatcher::Mutate_Custom, "Custom"});
else
Mutators = DefaultMutators;

if (EF->LLVMFuzzerCustomCrossOver)
Mutators.push_back(
{&MutationDispatcher::Mutate_CustomCrossOver, "CustomCrossOver"});
}

首先上面这段代码是 libfuzzer 中的,现在 libfuzzer 已经不在 llvm-project 的 lib 目录下了,而是在 compiler-rt/lib/fuzzer 下面。这里给出的是 MutationDispatcher 的构造函数。我们可以看到:无论是 Mutate 还是 CrossOver,在 libfuzzer 中都还是视作 Mutator。另外,如果外部定义了 LLVMFuzzerCustomMutator,按道理说就不会加入上面那些 libfuzzer 中定义的 mutator,比如 Mutate_EraseBytes

而在 jazzer 中,LLVMFuzzerCustomMutator 可以说是默认定义的,只是用不用要看 experimental_mutator 这个参数而已。

4. Jazzer 中的 Mutators

Jazzer 中有两种模式,分别是 autofuzzspecified target。字面意思来讲,jazzer 倾向于指定具体的 “类” 来完成 fuzzing,这段对应的描述可见项目 README:

Run the jazzer binary (jazzer.exe on Windows), specifying the classpath and fuzz test class:

1
./jazzer --cp=<classpath> --target_class=<fuzz test class>

请注意这里的 target_class 的说明是:“fuzz test class”,也就是说这里指定的 class 其实是一个 Driver,这也是 “jazzer 指定的是 target_class 而非 target_method” 的原因。因为这里指定的就是 Driver

而反观 autofuzz 的要求是给出一个 “方法”,所以我们自然可以得知,jazzer 其实是可以根据方法签名来自己生成 Driver 的。于是我们可以总结为:

  • autofuzz:只需要指定方法名,jazzer 会根据方法名自动生成 Driver
  • specified target:只需要指定驱动类,jazzer 会去执行这个驱动类。

那么在知道这个信息的基础上,我们接下来以 specified target 为例,看下 jazzer 是如何对 fuzz test class 中的方法参数变异的。

i. 选择哪一个方法

既然是给定了一个类,那难道 jazzer 把这个类中所有方法都 fuzzing 一个遍吗?这肯定不是,都说了是 Driver,jazzer 默认只取一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private static final String FUZZER_TEST_ONE_INPUT = "fuzzerTestOneInput";
private static FuzzTarget findFuzzTargetByMethodName(Class<?> clazz) {
Method fuzzTargetMethod;
if (Opt.experimentalMutator.get()) {
List<Method> fuzzTargetMethods =
Arrays.stream(clazz.getMethods())
.filter(method -> "fuzzerTestOneInput".equals(method.getName()))
.filter(method -> Modifier.isStatic(method.getModifiers()))
.collect(Collectors.toList());
if (fuzzTargetMethods.size() != 1) {
throw new IllegalArgumentException(
String.format("%s must define exactly one function of this form:%n"
+ "public static void fuzzerTestOneInput(...)%n",
clazz.getName()));
}
fuzzTargetMethod = fuzzTargetMethods.get(0);
} else {
Optional<Method> bytesFuzzTarget =
targetPublicStaticMethod(clazz, FUZZER_TEST_ONE_INPUT, byte[].class);
Optional<Method> dataFuzzTarget =
targetPublicStaticMethod(clazz, FUZZER_TEST_ONE_INPUT, FuzzedDataProvider.class);
if (bytesFuzzTarget.isPresent() == dataFuzzTarget.isPresent()) {
throw new IllegalArgumentException(String.format(
"%s must define exactly one of the following two functions:%n"
+ "public static void fuzzerTestOneInput(byte[] ...)%n"
+ "public static void fuzzerTestOneInput(FuzzedDataProvider ...)%n"
+ "Note: Fuzz targets returning boolean are no longer supported; exceptions should be thrown instead of returning true.",
clazz.getName()));
}
fuzzTargetMethod = dataFuzzTarget.orElseGet(bytesFuzzTarget::get);
}

// ignore some code
}

总的来说,二者都是只取一个叫做 fuzzerTestOneInputpublic static 方法。findFuzzTargetByMethodName 这个方法会返回一个 FuzzTarget,这个结果现在确实就包含了 Class / Method / Params 等等信息了。

ii. 如何 fuzz 这个 public static fuzzerTestOneInput

即便是用户指定的,也有两种情况,就是这个 Driver 可能用 byte[]FuzzedDataProvider 两种参数,这也代表了 FuzzTargetRunner.java 中的两种执行方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private static int runOne(long dataPtr, int dataLength) {
// ignore some code
byte[] data;
Object argument;
if (useExperimentalMutator) {
byte[] buf = copyToArray(dataPtr, dataLength);
boolean readExactly = mutator.read(new ByteArrayInputStream(buf));

// All inputs constructed by the mutator framework can be read exactly, existing corpus files
// may not be valid for the current fuzz target anymore, though. In this case, print a warning
// once.
if (!(invalidCorpusFileWarningShown || readExactly || isFixedLibFuzzerInput(buf))) {
invalidCorpusFileWarningShown = true;
Log.warn("Some files in the seed corpus do not match the fuzz target signature. "
+ "This indicates that they were generated with a different signature and may cause issues reproducing previous findings.");
}
data = null;
argument = null;
} else if (useFuzzedDataProvider) {
fuzzedDataProvider.setNativeData(dataPtr, dataLength);
data = null;
argument = fuzzedDataProvider;
} else {
data = copyToArray(dataPtr, dataLength);
argument = data;
}
try {
if (useExperimentalMutator) {
// No need to detach as we are currently reading in the mutator state from bytes in every
// iteration.
mutator.invoke(false);
} else if (fuzzTargetInstance == null) {
fuzzTargetMethod.invoke(argument);
} else {
fuzzTargetMethod.invoke(fuzzTargetInstance, argument);
}
} catch (Throwable uncaughtFinding) {
finding = uncaughtFinding;
}
// ignore some code
}

至于怎么使用 DataDataProvider,那就是 Driver 自己要考虑的内容了。

需要额外说明的是,这个 runOne 方法同样并非在 java 中调用,而是在 fuzz_target_runner.cpp 中调用:

1
2
3
4
5
6
7
8
9
10
11
12
// gRunOneId = env->GetStaticMethodID(runner, "runOne", "(JI)I");
int testOneInput(const uint8_t *data, const std::size_t size) {
JNIEnv &env = *gEnv;
jint jsize =
std::min(size, static_cast<size_t>(std::numeric_limits<jint>::max()));
int res = env.CallStaticIntMethod(gRunner, gRunOneId, data, jsize);
if (env.ExceptionCheck()) {
env.ExceptionDescribe();
_Exit(1);
}
return res;
}

这里的 gRunnerFuzzTargetRunnergRunOneIdrunOne。而这个 testOneInput 函数是作为 LLVMFuzzerRunDriver 的 callback 传入的。

于是,startLibFuzzer 间接调用了 LLVMFuzzerRunDriver,这相当于开启了 fuzz loop,而它又调用了 testOneInput,这个 CB 会调用 runOne,每次都会执行一次用户自定义的 Driver。而 CB 的 data 是谁给的?那自然是 fuzz loop 中调用 LLVMFuzzerCustomMutator 时给的了。

iii. Mutator 如何工作

我们知道在 fuzz loop 中会调用到 LLVMFuzzerCustomMutator,而上面已经介绍了其内部代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gMutateOneId = env->GetStaticMethodID(runner, "mutateOne", "(JIII)I");

extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
size_t MaxSize, unsigned int Seed) {
if (gUseExperimentalMutator) {
JNIEnv &env = *gEnv;
jint jsize =
std::min(Size, static_cast<size_t>(std::numeric_limits<jint>::max()));
jint jmaxSize = std::min(
MaxSize, static_cast<size_t>(std::numeric_limits<jint>::max()));
jint jseed = static_cast<jint>(Seed);
jint newSize = env.CallStaticLongMethod(gRunner, gMutateOneId, Data, jsize,
jmaxSize, jseed);
if (env.ExceptionCheck()) {
env.ExceptionDescribe();
_Exit(1);
}
return static_cast<uint32_t>(newSize);
} else {
return LLVMFuzzerMutate(Data, Size, MaxSize);
}
}

根据这段代码,可以非常清楚地理解 “jazzer 为什么是 libfuzzer 套壳”,毕竟默认就是用的全套 libfuzzer。但当 UseExperimentalMutator 时,将返回去调用 FuzzTargetRunnermutateOne。这里需要首先来到 FuzzTargetrRunner 的 static 块中,块中不仅有上文 “对 FuzzTarget 的指定”,还有 mutator 的初始化:

1
2
3
4
5
6
7
8
9
10
if (useExperimentalMutator) {
if (Modifier.isStatic(fuzzTarget.method.getModifiers())) {
mutator = ArgumentsMutator.forStaticMethodOrThrow(fuzzTarget.method);
} else {
mutator = ArgumentsMutator.forInstanceMethodOrThrow(fuzzTargetInstance, fuzzTarget.method);
}
Log.info("Using experimental mutator: " + mutator);
} else {
mutator = null;
}

可见,如果是 useExperimentalMutator 下,代码会根据 fuzzTarget 中的 method signature 得到一系列的 mutators。具体可在 ArgumentsMutator 中看到。

这里要多说一句,在 jazzer 当中,有 SerializingMutator / ProductMutator / ArgumentsMutator 这三层抽象,它们的意思分别是:

名称 含义
SerializingMutator 是一个 abstract class,需要去实现 ValueMutator 这个接口,该接口包含 init / mutate / crossOver。Jazzer 中所有 mutator 都是这个 abstract class 的子类
ProductMutator 这里的 Product 可能取自 “积” $(\prod)$ 这个概念,因为用 product 表示函数签名也是一种通常的表达方式。这个类中里面有一个 SerializingMutator 数组,由此可见一个 ProductMutator 就可以表示 对一个方法进行 mutate 的抽象层
ArgumentsMutator 算是一层外包装,提供了一系列 static 方法来创建自己。里面有一个 ProductMutator。这几乎可以说是对 FuzzTargetRunner 提供的最外层抽象

上面提到的 LLVMFuzzerCustomMutator 调用的就是下面的 mutateOne,而上面通过 ArgumentsMutator 的 static method 生成的 mutator 也在下面使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SuppressWarnings("unused")
private static int mutateOne(long data, int size, int maxSize, int seed) {
mutate(data, size, seed);
return writeToMemory(mutator, data, maxSize);
}

private static void mutate(long data, int size, int seed) {
// libFuzzer sends the input "\n" when there are no corpus entries. We use that as a signal to
// initialize the mutator instead of just reading that trivial input to produce a more
// interesting value.
if (size == 1 && UNSAFE.getByte(data) == '\n') {
// 这里的 mutator 就是上面的 ArgumentsMutator.forStaticMethodOrThrow / forInstanceMethodOrThrow
mutator.init(seed);
} else {
mutator.read(new ByteArrayInputStream(copyToArray(data, size)));
mutator.mutate(seed);
}
}

如果我们跟着 ArgumentsMutatormutate 一层层进去,就可以追溯到在确切执行 mutate 方法的 “SerializingMutator 的实现类” 们,比如:

  • BooleanMutator
  • ByteArrayMutator
  • FloatMutator
  • DoubleMutator

这些 MutatorFactory 生成的 mutator 要么是定义了类型,要么是匿名(反正只要一个 instance)。

5. 总结

也就是说,jazzer 在 --experimental_mutator 没开之前确实是一个 libfuzzer 的完全套壳。用的是 libfuzzer 的 LLVMFuzzerMutate

但加上这个之后,jazzer 会去使用定义在 java 代码里面的 mutator 们,这通过 LLVMFuzzerCustomMutate 反向调用 java 中 FuzzTargetRunner中的 mutate 提供了一种比较方便的定义 mutator 的方法。