Python Fuzz Testing:利用 AFL 或 Hypothesis 对 C 扩展接口进行健壮性测试
各位朋友,大家好!今天我们来探讨一个非常重要的软件测试领域:Fuzz Testing,特别是如何利用 Fuzz Testing 技术来提高 Python C 扩展接口的健壮性。
Python 以其易用性和强大的生态系统而闻名,但为了性能优化或利用底层系统资源,Python 经常需要通过 C 扩展与原生代码交互。然而,C 扩展引入了潜在的风险,例如内存泄漏、段错误、缓冲区溢出等,这些问题在纯 Python 代码中不容易出现。因此,对 C 扩展进行健壮性测试至关重要。Fuzz Testing,也称为模糊测试,是一种有效的自动化测试技术,通过向程序输入大量的、随机的、畸形的数据,以期发现程序中的漏洞和错误。
1. 什么是 Fuzz Testing?
Fuzz Testing 的核心思想很简单:向目标程序提供非预期的输入,观察程序是否崩溃或出现异常行为。这些非预期的输入通常是随机生成的,但也可能基于已知的漏洞模式或数据格式进行变异。
传统的单元测试通常针对特定的输入和预期输出进行验证,而 Fuzz Testing 则侧重于探索输入空间,发现未知的边界情况和错误。Fuzz Testing 的优势在于:
- 自动化: Fuzzing 工具可以自动生成大量的测试用例,无需人工编写。
- 发现未知漏洞: Fuzzing 可以发现单元测试难以覆盖的边界情况和错误。
- 高性价比: 通过自动化测试,可以以较低的成本发现大量的潜在问题。
2. 为什么需要对 Python C 扩展进行 Fuzz Testing?
Python C 扩展直接与底层系统交互,因此更容易受到内存管理、类型转换、数据边界等问题的困扰。以下是一些常见的问题:
- 内存泄漏: C 代码中未正确释放的内存会导致程序逐渐消耗完系统资源。
- 段错误: 访问未分配或不允许访问的内存区域会导致程序崩溃。
- 缓冲区溢出: 向固定大小的缓冲区写入超出其容量的数据会导致数据覆盖和潜在的安全漏洞。
- 类型混淆: 将一种类型的数据错误地解释为另一种类型可能会导致数据损坏或程序崩溃。
- 整数溢出: 算术运算的结果超出整数类型的范围会导致不可预测的行为。
这些问题在 Python 代码中通常会被 Python 解释器捕获并抛出异常,但在 C 扩展中,它们可能直接导致程序崩溃或更糟糕的情况,例如远程代码执行。
3. Fuzz Testing 工具:AFL 和 Hypothesis
目前有很多 Fuzz Testing 工具可供选择,其中 AFL (American Fuzzy Lop) 和 Hypothesis 是两个比较流行的选择。
-
AFL (American Fuzzy Lop): 是一种基于覆盖率引导的 Fuzzing 工具。它通过在程序中插入插桩代码来跟踪程序的执行路径,并根据覆盖率反馈来调整输入生成策略,从而更有效地探索代码。AFL 特别适合于测试 C/C++ 代码,因为它可以直接对编译后的二进制文件进行 Fuzzing。
-
Hypothesis: 是一种基于属性的测试框架。它允许你定义输入数据的属性,然后 Hypothesis 会自动生成满足这些属性的测试用例。Hypothesis 适用于测试各种类型的代码,包括 Python 代码和 C 扩展。它特别适合于测试具有复杂输入格式或依赖关系的代码。
| 工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| AFL | 覆盖率引导,高效;适用于 C/C++ 代码;易于集成到构建系统中。 | 需要编译目标程序;学习曲线相对较陡峭;对输入格式的先验知识要求不高,可能会生成大量无效输入。 | 测试 C/C++ 代码,特别是需要高效探索代码覆盖率的场景;对输入格式没有太多限制的场景。 |
| Hypothesis | 基于属性的测试,可以精确控制输入生成;适用于各种类型的代码;易于使用;方便与 Python 代码集成。 | 需要定义输入数据的属性;可能需要编写大量的测试代码;覆盖率引导能力相对较弱;对输入格式的先验知识要求较高,需要仔细设计属性。 | 测试各种类型的代码,特别是需要精确控制输入生成的场景;需要对输入格式有深入了解的场景。 |
4. 使用 AFL 进行 Python C 扩展的 Fuzz Testing
要使用 AFL 对 Python C 扩展进行 Fuzz Testing,需要进行以下步骤:
-
准备 C 扩展: 首先,需要准备一个 C 扩展。例如,我们可以创建一个简单的 C 扩展,该扩展接受一个字符串作为输入,并将其转换为大写。
#include <Python.h> #include <string.h> #include <ctype.h> static PyObject * string_toupper(PyObject *self, PyObject *args) { const char *input; char *output; Py_ssize_t len; int i; if (!PyArg_ParseTuple(args, "s#", &input, &len)) return NULL; output = (char *)PyMem_Malloc(len + 1); if (output == NULL) return PyErr_NoMemory(); for (i = 0; i < len; i++) { output[i] = toupper(input[i]); } output[len] = ''; return PyUnicode_FromString(output); } static PyMethodDef StringMethods[] = { {"toupper", string_toupper, METH_VARARGS, "Convert a string to uppercase."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; static struct PyModuleDef stringmodule = { PyModuleDef_HEAD_INIT, "string", /* name of module */ NULL, /* module documentation, may be NULL */ -1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */ StringMethods }; PyMODINIT_FUNC PyInit_string(void) { return PyModule_Create(&stringmodule); } -
编写 Fuzzing Harness: 接下来,需要编写一个 Fuzzing Harness,该 Harness 将负责将 AFL 生成的输入传递给 C 扩展,并执行相应的函数。
#include <Python.h> #include "string.h" // 包含 C 扩展的头文件 int main(int argc, char **argv) { Py_Initialize(); PyInit_string(); // 初始化 C 扩展 // 获取 string.toupper 函数 PyObject *module = PyImport_ImportModule("string"); if (!module) { PyErr_Print(); return 1; } PyObject *toupper_func = PyObject_GetAttrString(module, "toupper"); if (!toupper_func || !PyCallable_Check(toupper_func)) { PyErr_Print(); return 1; } // 从 stdin 读取输入数据 char *buf = NULL; size_t buf_size = 0; ssize_t len = getline(&buf, &buf_size, stdin); if (len > 0) { // 创建 Python 字符串对象 PyObject *arg = PyUnicode_FromStringAndSize(buf, len); if (!arg) { PyErr_Print(); free(buf); return 1; } // 调用 string.toupper 函数 PyObject *result = PyObject_CallFunctionObjArgs(toupper_func, arg, NULL); if (result) { Py_DECREF(result); } else { PyErr_Print(); } Py_DECREF(arg); } free(buf); Py_DECREF(module); Py_DECREF(toupper_func); Py_Finalize(); return 0; }关键点解释:
Py_Initialize()和Py_Finalize(): 初始化和反初始化 Python 解释器。PyInit_string(): 初始化我们创建的 C 扩展模块。PyImport_ImportModule("string"): 导入 Python 模块,这里的 "string" 是 C 扩展的名字。PyObject_GetAttrString(module, "toupper"): 获取模块中的toupper函数。getline(&buf, &buf_size, stdin): 从标准输入读取数据,AFL 会将生成的测试用例通过标准输入传递给这个 harness。PyUnicode_FromStringAndSize(buf, len): 创建 Python 字符串对象,准备传递给 C 扩展函数。PyObject_CallFunctionObjArgs(toupper_func, arg, NULL): 调用 C 扩展函数。Py_DECREF(...): 释放 Python 对象的引用计数,避免内存泄漏。
-
编译 Fuzzing Harness: 使用 AFL 提供的编译器 (afl-gcc 或 afl-clang) 编译 Fuzzing Harness。
afl-gcc -o fuzzer fuzz_harness.c -I/usr/include/python3.8 -lpython3.8注意: 需要根据你的 Python 版本调整
-I和-l参数。/usr/include/python3.8是 Python 头文件的路径,-lpython3.8是链接 Python 库。 -
准备初始语料库: 为 AFL 提供一些初始的输入样本,以帮助 AFL 更快地找到有趣的输入。
mkdir in echo "hello" > in/hello echo "world" > in/world echo "12345" > in/numbers -
运行 AFL: 使用 AFL 运行 Fuzzing Harness。
afl-fuzz -i in -o out ./fuzzer-i in: 指定初始语料库的目录。-o out: 指定输出目录,AFL 会将发现的崩溃和有趣的输入保存在这个目录中。./fuzzer: 指定要 Fuzzing 的程序,也就是我们编译的 Fuzzing Harness。
-
分析结果: AFL 运行一段时间后,会生成大量的输出。你需要分析这些输出,找到导致程序崩溃的输入,并修复相应的漏洞。AFL 会将崩溃的输入保存在
out/crashes目录中。
代码示例:修复内存泄漏漏洞
假设 AFL 发现了一个崩溃,通过分析崩溃的输入,我们发现 string_toupper 函数中存在内存泄漏,因为在某些情况下,PyUnicode_FromString 可能会失败,导致 output 指向的内存没有被释放。
修复后的代码如下:
#include <Python.h>
#include <string.h>
#include <ctype.h>
static PyObject *
string_toupper(PyObject *self, PyObject *args)
{
const char *input;
char *output;
Py_ssize_t len;
int i;
PyObject *result = NULL; // 初始化 result
if (!PyArg_ParseTuple(args, "s#", &input, &len))
return NULL;
output = (char *)PyMem_Malloc(len + 1);
if (output == NULL)
return PyErr_NoMemory();
for (i = 0; i < len; i++) {
output[i] = toupper(input[i]);
}
output[len] = '';
result = PyUnicode_FromString(output); // 存储结果
if (result == NULL) {
PyMem_Free(output); // 释放内存,避免泄漏
return NULL;
}
PyMem_Free(output); // 函数成功执行后也要释放 output 指向的内存
return result;
}
static PyMethodDef StringMethods[] = {
{"toupper", string_toupper, METH_VARARGS,
"Convert a string to uppercase."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef stringmodule = {
PyModuleDef_HEAD_INIT,
"string", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
StringMethods
};
PyMODINIT_FUNC
PyInit_string(void)
{
return PyModule_Create(&stringmodule);
}
5. 使用 Hypothesis 进行 Python C 扩展的 Fuzz Testing
要使用 Hypothesis 对 Python C 扩展进行 Fuzz Testing,需要进行以下步骤:
-
安装 Hypothesis: 使用 pip 安装 Hypothesis。
pip install hypothesis -
编写测试用例: 使用 Hypothesis 编写测试用例,定义输入数据的属性。
import unittest import hypothesis.strategies as st from hypothesis import given import string # 导入 C 扩展 class TestStringExtension(unittest.TestCase): @given(st.text()) # 使用 st.text() 生成任意字符串 def test_toupper(self, s): result = string.toupper(s) self.assertEqual(result, s.upper()) # 验证结果是否正确 @given(st.binary()) # 使用 st.binary() 生成任意字节串 def test_toupper_bytes(self, b): # 处理字节串, 因为 C 扩展可能不期望接收字节串 try: s = b.decode('utf-8') # 尝试解码为 UTF-8 字符串 result = string.toupper(s) self.assertEqual(result, s.upper()) except UnicodeDecodeError: # 如果解码失败,则跳过测试 pass if __name__ == '__main__': unittest.main()关键点解释:
@given(st.text()): 使用given装饰器将test_toupper函数标记为 Hypothesis 测试用例。st.text()是一个策略,用于生成任意的 Unicode 字符串。st.binary(): 生成任意的字节串。s.upper(): Python 内置的字符串方法,用于将字符串转换为大写。self.assertEqual(result, s.upper()): 断言string.toupper(s)的结果与 Python 内置的s.upper()的结果是否相等。try...except UnicodeDecodeError: 处理字节串解码错误,避免程序崩溃。
-
运行测试用例: 使用 Python 运行测试用例。
python test_string.pyHypothesis 会自动生成大量的测试用例,并根据你定义的属性进行验证。如果发现违反属性的输入,Hypothesis 会自动简化输入,并将其报告给你。
代码示例:发现类型错误漏洞
假设 AFL 发现了一个崩溃,通过分析崩溃的输入,我们发现 string_toupper 函数没有正确处理非字符串输入,例如整数或列表。
修复后的代码如下:
#include <Python.h>
#include <string.h>
#include <ctype.h>
static PyObject *
string_toupper(PyObject *self, PyObject *args)
{
const char *input;
char *output;
Py_ssize_t len;
int i;
PyObject *result = NULL; // 初始化 result
if (!PyArg_ParseTuple(args, "s#", &input, &len)) {
PyErr_SetString(PyExc_TypeError, "Expected a string argument"); // 设置错误信息
return NULL;
}
output = (char *)PyMem_Malloc(len + 1);
if (output == NULL)
return PyErr_NoMemory();
for (i = 0; i < len; i++) {
output[i] = toupper(input[i]);
}
output[len] = '';
result = PyUnicode_FromString(output); // 存储结果
if (result == NULL) {
PyMem_Free(output); // 释放内存,避免泄漏
return NULL;
}
PyMem_Free(output); // 函数成功执行后也要释放 output 指向的内存
return result;
}
static PyMethodDef StringMethods[] = {
{"toupper", string_toupper, METH_VARARGS,
"Convert a string to uppercase."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef stringmodule = {
PyModuleDef_HEAD_INIT,
"string", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
StringMethods
};
PyMODINIT_FUNC
PyInit_string(void)
{
return PyModule_Create(&stringmodule);
}
6. Fuzz Testing 的最佳实践
- 选择合适的 Fuzzing 工具: 根据你的项目需求和代码类型选择合适的 Fuzzing 工具。AFL 适用于 C/C++ 代码,Hypothesis 适用于各种类型的代码。
- 编写高质量的 Fuzzing Harness 或测试用例: Fuzzing Harness 或测试用例的质量直接影响 Fuzzing 的效果。确保 Fuzzing Harness 或测试用例能够覆盖目标代码的各个方面。
- 提供初始语料库: 提供一些初始的输入样本,以帮助 Fuzzing 工具更快地找到有趣的输入。
- 监控 Fuzzing 过程: 监控 Fuzzing 过程,观察程序的覆盖率和崩溃情况。
- 分析 Fuzzing 结果: 分析 Fuzzing 结果,找到导致程序崩溃的输入,并修复相应的漏洞。
- 持续 Fuzzing: 将 Fuzzing 集成到持续集成流程中,定期对代码进行 Fuzzing。
- 结合静态分析: 将 Fuzzing 与静态分析工具结合使用,可以更有效地发现漏洞。
7. 总结一下
Fuzz Testing 是一种强大的自动化测试技术,可以有效地提高 Python C 扩展接口的健壮性。通过使用 AFL 或 Hypothesis 等 Fuzzing 工具,我们可以发现 C 扩展中潜在的内存泄漏、段错误、缓冲区溢出等问题,并及时修复这些问题,从而提高软件的质量和安全性。选择正确的工具、编写高质量的 harness/测试用例以及持续进行 Fuzzing 是保证有效性的关键。
总而言之,Fuzzing 是一种发现代码深层问题的强大技术,而对 Python C 扩展进行 Fuzzing 是确保 Python 应用安全性和稳定性的重要手段。
更多IT精英技术系列讲座,到智猿学院