上一篇主要围绕 jazzer 在 specified target 下的流程,着重讲了多种场景下 (experimental mutator / libfuzzer by default) mutator 的产生和使用。

这篇主要分享的是 jazzer 在 autofuzz 场景下的工作流程,期望着重讲下 autofuzz 与 specified target 在选取 target 时的不同,以及 fuzz data provider 的使用。

1. autofuzz 的 target 定义在哪里

src/main/java/com/code_intelligence/jazzer/driver/Driver.java 文件下的 public static start 中,汇总了 autofuzz 和 specified target 两种 fuzzing 方法的启动点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!Opt.autofuzz.get().isEmpty()) {
AgentInstaller.install(Opt.hooks.get());
FuzzTargetHolder.fuzzTarget = FuzzTargetHolder.AUTOFUZZ_FUZZ_TARGET;
return FuzzTargetRunner.startLibFuzzer(args);
}

String targetClassName = FuzzTargetFinder.findFuzzTargetClassName();
if (targetClassName == null) {
Log.error("Missing argument --target_class=<fuzz_target_class>");
exit(1);
}

// ignore some code

// Installing the agent after the following "findFuzzTarget" leads to an asan error
// in it on "Class.forName(targetClassName)", but only during native fuzzing.
AgentInstaller.install(Opt.hooks.get());
FuzzTargetHolder.fuzzTarget = FuzzTargetFinder.findFuzzTarget(targetClassName);
return FuzzTargetRunner.startLibFuzzer(args);

这里需要提醒的是,因为启动了 startLibFuzzer,所以后面出现的方法(比如 fuzzerTestOneInput)全部都是在 libfuzzer 这个大 fuzz loop 里循环执行多次的。看起来一条线执行到最后,但其实在 libfuzzer 中 loop

由此可见,主要的区别在于 specified target 给出了 target class,并且需要用户很好地定义 driver 方法。但 autofuzz 并不需要。另外,相较于之前的 specified target,这里传入给 这里的 AUTOFUZZ_FUZZ_TARGET 是一个 static 的值,表示了一个名为 fuzzerTestOneInput 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final FuzzTarget AUTOFUZZ_FUZZ_TARGET = autofuzzFuzzTarget(() -> {
com.code_intelligence.jazzer.autofuzz.FuzzTarget.fuzzerInitialize(
Opt.targetArgs.get().toArray(new String[0]));
return null;
});

public static FuzzTarget autofuzzFuzzTarget(Callable<Object> newInstance) {
try {
Method fuzzerTestOneInput = com.code_intelligence.jazzer.autofuzz.FuzzTarget.class.getMethod(
"fuzzerTestOneInput", FuzzedDataProvider.class);
return new FuzzTargetHolder.FuzzTarget(fuzzerTestOneInput, newInstance, Optional.empty());
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
}
}

这里容易忽视的一点在于 autofuzzFuzzTarget 是一个名为 fuzzerTestOneInput,参数仅一个且类型为 FuzzedDataProvider 的方法。而当 FuzzTarget.Holder.FuzzTargetmethod 为满足这样格式的方法时,就会使 FuzzTarget.Holder.FuzzTarget usesFuzzedDataProvider 方法恒为 true:(这对后续 FuzzTargetRunner 的运行有一定影响)

1
2
3
4
public boolean usesFuzzedDataProvider() {
return this.method.getParameterCount() == 1
&& this.method.getParameterTypes()[0] == FuzzedDataProvider.class;
}

另外一个我认为容易被忽视的是 fuzzerInitialize 这个方法,其实这个方法中初始化了许多 FuzzTargetRunner 需要用来初始化其静态变量的变量。可能你会觉得很奇怪,难道我们刚才说了那么多 FuzzTargetRunner 相关的东西,展示了 startLibFuzzer 方法在 Driver 中的调用位置,难道现在还没初始化 FuzzTargetRunner 吗?

没错,确实是这样。在整个 jazzer 流程中,必须要在 FuzzTargetHolder 中的一系列变量初始化后才能初始化 FuzzTargetRunner,因为:

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
public final class FuzzTargetRunner {
static {
// 注意这个 fuzzTarget
FuzzTargetHolder.FuzzTarget fuzzTarget = FuzzTargetHolder.fuzzTarget;
Class<?> fuzzTargetClass = fuzzTarget.method.getDeclaringClass();

fuzzTarget.method.setAccessible(true);
try {
fuzzTargetMethod = MethodHandles.lookup().unreflect(fuzzTarget.method);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
useFuzzedDataProvider = fuzzTarget.usesFuzzedDataProvider();
// ignore some code about validation

fuzzerTearDown = fuzzTarget.tearDown.orElse(null);
reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider);

JazzerInternal.onFuzzTargetReady(fuzzTargetClass.getName());

try {
fuzzTargetInstance = fuzzTarget.newInstance.call();
} catch (Throwable t) {
Log.finding(t);
exit(1);
throw new IllegalStateException("Not reached");
}

// ignore some code about mutator
}

// ignore some code
}

其实由此也可以得知,相较于 specified target 中需要自己传入定义好的、名为 fuzzerTestOneInput 的 target,jazzer 其实也在 FuzzTarget 这个 class 里面定义了一个 fuzzerTestOneInput

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
public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Throwable {
AutofuzzCodegenVisitor codegenVisitor = null;
if (Meta.IS_DEBUG) {
codegenVisitor = new AutofuzzCodegenVisitor();
}
fuzzerTestOneInput(data, codegenVisitor);
if (codegenVisitor != null) {
Log.println(codegenVisitor.generate());
}
}

private static void fuzzerTestOneInput(FuzzedDataProvider data, AutofuzzCodegenVisitor codegenVisitor)
throws Throwable {
Executable targetExecutable;
if (FuzzTarget.targetExecutables.length == 1) {
targetExecutable = FuzzTarget.targetExecutables[0];
} else {
targetExecutable = data.pickValue(FuzzTarget.targetExecutables);
}
Object returnValue = null;
try {
if (targetExecutable instanceof Method) {
if (targetInstance != null) {
returnValue =
meta.autofuzz(data, (Method) targetExecutable, targetInstance, codegenVisitor);
} else {
returnValue = meta.autofuzz(data, (Method) targetExecutable, codegenVisitor);
}
} else {
// No targetInstance for constructors possible.
returnValue = meta.autofuzz(data, (Constructor<?>) targetExecutable, codegenVisitor);
}
executionsSinceLastInvocation = 0;
} catch (AutofuzzConstructionException e) {
} catch (AutofuzzInvocationException e) {
} catch (Throwable t) {
} finally {
}
}

2. autofuzz 中的方法参数填充 - consume

间接调用的 fuzzerTestOneInput 同样需要区分 static method 和 instance method,甚至 fuzz target 可能会是 constructor,所以在 Meta.java 中有多种 meta.autofuzz 的方法重载。这里首先列出 instance method 的(因为 static method 的 autofuzz 只是 Object thisObject == null 的特殊情况):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Object autofuzz(
FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) {
if (visitor != null) {
visitor.pushGroup(String.format("%s(", method.getName()), ", ", ")");
}
Object[] arguments = consumeArguments(data, method, visitor);
if (visitor != null) {
visitor.popGroup();
}
try {
return method.invoke(thisObject, arguments);
} catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) {
// We should ensure that the arguments fed into the method are always valid.
throw new AutofuzzError(getDebugSummary(method, thisObject, arguments), e);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof HardToCatchError) {
throw new AutofuzzInvocationException();
}
throw new AutofuzzInvocationException(e.getCause());
}
}

这里可以看到 method 在本方法中就已经 invoked 了,所需要的 arguments 通过 consumeArguments 获得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object[] consumeArguments(
FuzzedDataProvider data, Executable executable, AutofuzzCodegenVisitor visitor) {
Object[] result;
try {
result = Arrays.stream(executable.getGenericParameterTypes())
.map(type -> consume(data, type, visitor))
.toArray();
return result;
} catch (AutofuzzConstructionException e) {
// Do not nest AutofuzzConstructionExceptions.
throw e;
} catch (AutofuzzInvocationException e) {
// If an invocation fails while creating the arguments for another invocation, the exception
// should not be reported, so we rewrap it.
throw new AutofuzzConstructionException(e.getCause());
} catch (Throwable t) {
throw new AutofuzzConstructionException(t);
}
}

而对于这个 consume,其是一个 400 多行的方法,内部包含了各种类型的参数构造方法,包括 Long / Float / Byte / Boolean / CharSequence / String / Array / InputStream / Map 等常用的参数类型,也包括 Enum / Class / Constructor / Interface / Abstract class 等等。

3. FuzzedDataProvider 全流程

i. 初始化阶段

如果我没理解错的话,FuzzedDataProvider 的初始化阶段应该是在 FuzzTargetRunner 的 static block 执行时:

1
2
private static final FuzzedDataProviderImpl fuzzedDataProvider =
FuzzedDataProviderImpl.withNativeData();

这个 withNativeData 是一个 static method,这会触发初始化时的 static block 执行:

1
2
3
4
5
6
7
8
public class FuzzedDataProviderImpl implements FuzzedDataProvider, AutoCloseable {
static {
RulesJni.loadLibrary("jazzer_fuzzed_data_provider", "/com/code_intelligence/jazzer/driver");
nativeInit();
}

// ignore some code
}

而这个 nativeInit 将所有的 data methods 都通过 env->RegisterNatives 反向注册到 java 代码中的 FuzzedDataProviderImpl 里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const JNINativeMethod kFuzzedDataMethods[]{
{(char *)"consumeBoolean", (char *)"()Z", (void *)&ConsumeBool},
{(char *)"consumeByte", (char *)"()B", (void *)&ConsumeIntegral<jbyte>},
{(char *)"consumeByteUnchecked", (char *)"(BB)B",
(void *)&ConsumeIntegralInRange<jbyte>},
// ignore some data methods
};

const jint kNumFuzzedDataMethods =
sizeof(kFuzzedDataMethods) / sizeof(kFuzzedDataMethods[0]);

[[maybe_unused]] void
Java_com_code_1intelligence_jazzer_driver_FuzzedDataProviderImpl_nativeInit(
JNIEnv *env, jclass clazz) {
env->RegisterNatives(clazz, kFuzzedDataMethods, kNumFuzzedDataMethods);
gDataPtrField = env->GetFieldID(clazz, "dataPtr", "J");
gRemainingBytesField = env->GetFieldID(clazz, "remainingBytes", "I");
}

FuzzedDataProviderImpl 你可以看到 24 个 public native methods (有几个不是 consume method,但大部分是) 和 7 个 private native consume methods。这和 fuzzed_data_provider.cpp 中是对的上的。

总之,在执行了初始化阶段后,现在 java 部分的 FuzzedDataProviderImpl 和 cpp 部分的 fuzzed_data_provider.cpp 建立了联系,之后可以相互调用了。

ii. 单个 consume 的执行意图

public native String consumeString(int maxLength) 为例,该函数在 cpp 一侧是:

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
jstring JNICALL ConsumeString(JNIEnv &env, jobject self, jint max_length) {
return ConsumeStringInternal(env, self, max_length, false, true);
}

jstring ConsumeStringInternal(JNIEnv &env, jobject self, jint max_length,
bool ascii_only, bool stop_on_backslash) {
// 这里的 dataPtr 和 remainingBytes 都是定义在 java 中的 field
const auto *dataPtr =
reinterpret_cast<const uint8_t *>(env.GetLongField(self, gDataPtrField));
jint remainingBytes = env.GetIntField(self, gRemainingBytesField);

// 如果 remain 长度只有 1 了,那就只返回一个空字符串
if (remainingBytes == 1) {
env.SetIntField(self, gRemainingBytesField, 0);
return env.NewStringUTF("");
}

std::string str;
jint consumed_bytes;
std::tie(str, consumed_bytes) = jazzer::FixUpModifiedUtf8(
dataPtr, remainingBytes, max_length, ascii_only, stop_on_backslash);
// 否则调用 FixUpModifiedUtf8 获得 str 和这次 consume 掉的 byte 数,并用来更新 java 中的 dataPtr 和 remainingBytes
env.SetLongField(self, gDataPtrField, (jlong)(dataPtr + consumed_bytes));
env.SetIntField(self, gRemainingBytesField, remainingBytes - consumed_bytes);
return env.NewStringUTF(str.c_str());
}

由此也可知,java 中定义的 dataPtrremainingBytes 很大程度上是 cpp 这边在用。java 那边基本没赋过值。就连取值都是通过 native method 转到 cpp,再由 RemainingBytes 这种通过 JNI env.GetIntField(self, gRemainingBytesField) 反向取 java 中 field 的值来完成的。

iii. 梳理 consume 的执行前后

consume 都是在 meta.autofuzz 里面调用的,而这仅是 --autofuzz 作用时才会调用的,所以可以说只有在 autofuzz 场景下才会由 consume 填充。

consume 受 fuzzerTestOneInputmeta.autofuzz 调用,目的是为了填充 target method 执行所需的参数。也就是说,autofuzz 场景会在 java 一侧执行代码。fuzzerTestOneInput 这个方法在 autofuzzFuzzTarget 已经被指定为 FuzzTarget method 了。因此,libfuzzer 会在 fuzz loop 中持续地调用该方法,调用过程是:

  • LLVMFuzzerRunDriver:libfuzzer 大循环的开启者;
  • testOneInput (cpp):传给 libfuzzer fuzz loop 的 callback;
  • runOne (java):callback 反向调用的 java 方法,里面调用了 fuzzTargetMethod.invoke (java),而这对应的 method 恰好是 AUTOFUZZ_FUZZ_TARGET 填入的 fuzzerTestOneInput
  • fuzzerTestOneInput:最主要的目标就是调用了 meta.autofuzz
  • meta.autofuzz:调用了 consumeArgumentsmethod.invoke(thisObject, arguments)