Skip to content

Mutator 方面

Jiageng edited this page Jul 17, 2023 · 1 revision

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:

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

// 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 代码,这次贴出全部:

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 这个全局变量决定的,而这个全局变量的赋值也在同一个文件下:

[[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 代码位置:

/**
 * 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 逻辑:

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:

./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 默认只取一个方法:

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 中的两种执行方式:

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 中调用:

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,而上面已经介绍了其内部代码:

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 的初始化:

    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 也在下面使用:

@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 的方法。