上一篇主要围绕 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 ); } 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.FuzzTarget
的 method
为满足这样格式的方法时,就会使 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 { 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(); 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" ); } } }
其实由此也可以得知,相较于 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 { 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) { 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) { throw e; } catch (AutofuzzInvocationException e) { 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(); } }
而这个 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>}, }; 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) { const auto *dataPtr = reinterpret_cast <const uint8_t *>(env.GetLongField(self, gDataPtrField)); jint remainingBytes = env.GetIntField(self, gRemainingBytesField); 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); env.SetLongField(self, gDataPtrField, (jlong)(dataPtr + consumed_bytes)); env.SetIntField(self, gRemainingBytesField, remainingBytes - consumed_bytes); return env.NewStringUTF(str.c_str()); }
由此也可知,java 中定义的 dataPtr
和 remainingBytes
很大程度上是 cpp 这边在用。java 那边基本没赋过值。就连取值都是通过 native method 转到 cpp,再由 RemainingBytes
这种通过 JNI env.GetIntField(self, gRemainingBytesField)
反向取 java 中 field 的值来完成的。
iii. 梳理 consume 的执行前后 consume 都是在 meta.autofuzz
里面调用的,而这仅是 --autofuzz
作用时才会调用的,所以可以说只有在 autofuzz 场景下才会由 consume 填充。
consume 受 fuzzerTestOneInput
中 meta.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
:调用了 consumeArguments
和 method.invoke(thisObject, arguments)
。