如何模糊测试 API 作为一个整体而不是文件输入?

How to fuzz test API as a whole and not with file inputs?

我正在学习模糊测试 C 应用程序的方法。据我了解,大多数时候在进行模糊测试时,都有一个 takes/reads 文件的 C 函数。给模糊器一个有效的样本文件,随机或使用覆盖启发式对其进行变异,并使用这个新输入执行函数。

但现在我不想对接受文件输入的函数进行模糊测试,而是对几个共同构成 API 的函数进行模糊测试。例如:

int setState(int state);
int run(void); // crashes when previous set state was == 123

这个想法是作为一个整体来测试 API 并检测是否误用和以错误的顺序调用函数(这里:调用 setState(123) 后跟 run())是否在某处崩溃了.

怎么能做出这样的事?我正在搜索模糊测试框架(不一定是 C)、一般概念和示例。

我尝试使用 LLVM 中的 libFuzzer 并逐字节“消耗”其模糊器数据。我读取一个字节以确定要调用的函数,然后在需要时读取一个参数,最后调用该函数。然后我重复直到不再剩下模糊器输入数据。它看起来像这样:

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    while(/* not end of fuzzer data reached */)
        switch (fuzzerConsumeByte()) {
        case 0:
            setState(fuzzerConsumeInt());
            break;
        case 1:
            run();
            break;
        default:
            break;
        }
    }
    return 0;
}

我发现的提到这种模糊测试风格的来源是这样的:

[...] randomly select functions from your Public API and call them in random order with random parameters. code-intelligence

这似乎不是对基于输入文件的模糊器的良好或高效使用。使用 libFuzzer 进行模糊测试会在几秒钟后发现错误。但我认为如果我用多个其他功能扩展 API 可能会花费很长时间。

回答我自己的问题:

是的,这就是 API 模糊测试的方式。 为了按字节消费数据,可以使用 libFuzzer #include <fuzzer/FuzzedDataProvider.h> (C++) 提供的函数。这个问题:故障转储和模糊器语料库将无法被人类阅读。

对于更具可读性的模糊器,为 libFuzzer 实施 structure aware 自定义数据修改器是有益的。

我使用预制数据修改器 libprotobuf-mutator (C++) 对示例 API 进行模糊测试。它根据协议缓冲区定义生成有效的输入数据,而不仅仅是(半)随机字节。不过,它确实使模糊测试变慢了一点。给定人为示例 API 中的错误是在 ~2 分钟后发现的,而基本字节消耗设置为 ~30 秒。但我确实认为它对于更大的(真实的)API 会更好。

以这种方式对有状态应用程序的 API 进行模糊测试时要记住的另一件事是,您应该确保在每次模糊测试之间重置您的应用程序或使用 AFL 而不是 libFuzzer 来分叉将测试的每个新输入。否则,您发现的崩溃可能无法通过故障转储重现,因为崩溃取决于早期测试用例对目标应用程序所做的某些更改。

我还想提一下,我们正在使用 "[...] 从您的 Public API 中随机 select 函数并调用它们具有随机参数的随机顺序。 模糊测试方法也在更大的现实生活 API 秒(最多几百个函数)上实现了良好的代码覆盖率并在合理的时间内找到结果。

关于崩溃转储不是人类可读的,你是对的,但是使用一些 Feedback-based 模糊测试工具,你不仅会得到崩溃输入的转储,还会得到额外的信息,比如堆栈跟踪,可以帮你分析根源。

编辑:
这里有一个使用这种模糊测试方法并使用 FuzzedDataProvider 的模糊测试示例:

#include <stdint.h>
#include <stddef.h>

#include "FuzzedDataProvider.h"

#include "GPS_module_1.h"
#include "crypto_module_1.h"
#include "crypto_module_2.h"
#include "key_management_module_1.h"
#include "time_module_1.h"

// Wrapper function for FuzzedDataProvider.h
// Writes |num_bytes| of input data to the given destination pointer. If there
// is not enough data left, writes all remaining bytes and fills the rest with zeros.
// Return value is the number of bytes written.
void ConsumeDataAndFillRestWithZeros(void *destination, size_t num_bytes, FuzzedDataProvider *fuzz_data) {
  if (destination != nullptr) {
    size_t num_bytes_with_fuzz_data = fuzz_data->ConsumeData(destination, num_bytes);
    if (num_bytes > num_bytes_with_fuzz_data) {
      size_t num_bytes_with_zeros = num_bytes - num_bytes_with_fuzz_data;
      std::memset((char*)destination+num_bytes_with_fuzz_data, 0, num_bytes_with_zeros);
    }
  }
}

extern "C" int FUZZ(const uint8_t *Data, size_t Size) {
    // Ensure a minimum data length
    if(Size < 100) return 0;

    // Setup FuzzedDataProvider
    FuzzedDataProvider fuzz_data_provider(Data, Size);
    FuzzedDataProvider *fuzz_data = &fuzz_data_provider;

    // Reset the state of the target software
    // to ensure that crashes are reproducible
    crypto::init();

    int number_of_functions = fuzz_data->ConsumeIntegralInRange<int>(1,100);
    for (int i=0; i<number_of_functions; i++) {
      int func_id = fuzz_data->ConsumeIntegralInRange<int>(0, 15);
      switch(func_id) {
         case 0: {
            // Create a struct and fill it with fuzz data
            GPS::position struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            GPS::get_current_position(&struct_0);
            break;
          }
         case 1: {
            GPS::get_destination_position();
            break;
          }
         case 2: {
            GPS::init_crypto_module();
            break;
          }
         case 3: {
            GPS::position struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            GPS::set_destination_position(struct_0);
            break;
          }
         case 4: {
            // Create a vector of "random" size
            // and fill it with fuzz data
            std::vector<uint8_t> fuzz_data_0 = fuzz_data->ConsumeBytes<uint8_t>(fuzz_data->ConsumeIntegral<uint8_t>());
            size_t fuzz_size_0 = fuzz_data_0.size();
            crypto::hmac struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            crypto::calculate_hmac(fuzz_data_0.data(), fuzz_size_0, &struct_0);
            break;
          }
         case 5: {
            crypto::get_state();
            break;
          }
         case 6: {
            crypto::init();
            break;
          }
         case 7: {
            crypto::key struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            crypto::set_key(struct_0);
            break;
          }
         case 8: {
            crypto::nonce struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            crypto::set_nonce(struct_0);
            break;
          }
         case 9: {
            std::vector<uint8_t> fuzz_data_0 = fuzz_data->ConsumeBytes<uint8_t>(fuzz_data->ConsumeIntegral<uint8_t>());
            size_t fuzz_size_0 = fuzz_data_0.size();
            crypto::hmac struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            crypto::verify_hmac(fuzz_data_0.data(), fuzz_size_0, &struct_0);
            break;
          }
         case 10: {
            crypto::key struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            crypto::verify_key(struct_0);
            break;
          }
         case 11: {
            crypto::nonce struct_0 = {0};
            ConsumeDataAndFillRestWithZeros(&struct_0, sizeof(struct_0), fuzz_data);
            crypto::verify_nonce(&struct_0);
            break;
          }
         case 12: {
            std::vector<uint8_t> fuzz_data_0 = fuzz_data->ConsumeBytes<uint8_t>(fuzz_data->ConsumeIntegral<uint8_t>());
            size_t fuzz_size_0 = fuzz_data_0.size();
            key_management::create_key(fuzz_data_0.data(), fuzz_size_0);
            break;
          }
         case 13: {
            std::vector<uint8_t> fuzz_data_0 = fuzz_data->ConsumeBytes<uint8_t>(fuzz_data->ConsumeIntegral<uint8_t>());
            size_t fuzz_size_0 = fuzz_data_0.size();
            key_management::create_nonce(fuzz_data_0.data(), fuzz_size_0);
            break;
          }
         case 14: {
            std::vector<uint8_t> fuzz_data_0 = fuzz_data->ConsumeBytes<uint8_t>(fuzz_data->ConsumeIntegral<uint8_t>());
            size_t fuzz_size_0 = fuzz_data_0.size();
            key_management::generate_random_bytes(fuzz_data_0.data(), fuzz_size_0);
            break;
          }
         case 15: {
            time_management::current_time();
            break;
          }
      }
    }

    return 0;
}