如何模糊测试 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;
}
我正在学习模糊测试 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;
}