PHP Fuzzing 测试入门:针对扩展或 FFI 接口的随机输入安全测试
大家好,今天我们来聊聊 PHP Fuzzing 测试,特别是针对扩展和 FFI 接口的随机输入安全测试。Fuzzing 是一种通过向目标程序提供大量的随机、畸形或半畸形输入,来发现潜在的安全漏洞的技术。在 PHP 的语境下,这对于确保扩展和 FFI 接口的健壮性和安全性至关重要。
1. 为什么要做 PHP Fuzzing 测试?
PHP 作为一种广泛使用的脚本语言,其安全性和可靠性直接影响着无数 Web 应用。而 PHP 扩展通常是用 C 或 C++ 编写的,直接与底层系统交互,一旦出现漏洞,往往会带来严重的后果,例如代码执行、信息泄露、拒绝服务等。FFI (Foreign Function Interface) 允许 PHP 直接调用 C 函数,同样引入了潜在的安全风险,因为 PHP 代码不再完全处于自身的沙盒环境中。
Fuzzing 测试能有效地发现这些隐藏在扩展或 FFI 接口中的漏洞,它比传统的人工代码审计更高效,而且能够覆盖更多的输入场景。
2. Fuzzing 的基本原理
Fuzzing 的核心思想是 "尝试触发错误"。它通过以下步骤工作:
- 目标识别: 确定需要进行 Fuzzing 测试的目标,例如特定的 PHP 扩展函数或 FFI 接口。
- 输入生成: 生成大量的、各种类型的输入数据。这些数据可以是完全随机的,也可以是基于已有数据进行变异的。
- 执行目标: 将生成的输入数据传递给目标程序,并执行。
- 监控: 监控目标程序的执行过程,检测是否发生了崩溃、异常或其他异常行为。
- 分析: 如果发现了异常,分析导致异常的输入数据,并尝试重现该异常,最终确定漏洞的根源。
3. PHP Fuzzing 的工具和技术
目前,常用的 PHP Fuzzing 工具和技术包括:
- AFL (American Fuzzy Lop): AFL 是一个基于覆盖率引导的 Fuzzing 工具,它能够根据程序执行路径的覆盖率来优化输入数据的生成,从而更有效地发现漏洞。虽然 AFL 最初是为 C/C++ 程序设计的,但通过一些技巧,也可以用于 Fuzzing PHP 扩展。
- libFuzzer: libFuzzer 是一个 Clang 编译器提供的 Fuzzing 库,它可以与 AFL 或其他的 Fuzzing 引擎配合使用,提供代码覆盖率和变异策略。
- Peach Fuzzer: Peach Fuzzer 是一种基于模型的 Fuzzer,它允许你定义输入数据的格式和约束,从而生成更有针对性的测试数据。
- 自定义脚本: 使用 PHP 脚本结合随机数据生成函数,对特定函数进行 Fuzzing 测试。这种方法更灵活,但需要更多的手动工作。
4. Fuzzing PHP 扩展:以一个简单的例子为例
假设我们有一个简单的 PHP 扩展,名为 myext,它提供了一个函数 myext_process_data,用于处理用户提供的数据。该函数的原型如下:
PHP_FUNCTION(myext_process_data);
该函数接受一个字符串作为输入,并进行一些处理。现在我们想要对这个函数进行 Fuzzing 测试。
4.1 准备工作
- 安装必要的工具: 确保你已经安装了 AFL 或 libFuzzer,以及 PHP 的开发环境。
- 创建扩展框架: 如果你还没有
myext扩展,需要先创建一个基本的扩展框架。可以使用phpize和php-config工具来完成。 - 编写测试代码: 创建一个 C 代码文件,用于调用
myext_process_data函数,并提供 AFL 或 libFuzzer 所需的接口。
4.2 使用 AFL 进行 Fuzzing
首先,我们需要编写一个 C 代码文件 fuzz_myext.c,该文件包含 main 函数,用于从标准输入读取数据,并将其传递给 myext_process_data 函数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "php.h"
#include "ext/standard/info.h"
#include "myext.h"
int main(int argc, char **argv) {
zend_string *input_str;
char *data;
size_t len;
// 初始化 PHP 环境
sapi_startup(&sapi_module);
zend_hash_init(&EG(symbol_table), 50, NULL, ZVAL_PTR_DTOR, 0);
// 从标准输入读取数据
while (__AFL_LOOP(10000)) {
data = __AFL_FUZZ_TESTCASE_BUF;
len = __AFL_FUZZ_TESTCASE_LEN;
if (len > 0) {
input_str = zend_string_init(data, len, 0);
// 调用 myext_process_data 函数
zval retval;
zval param;
ZVAL_STR(¶m, input_str);
zend_fcall_info fci;
zend_fcall_info_cache fcc;
fci.size = sizeof(fci);
ZVAL_NULL(&fci.function_name);
fci.object = NULL;
fci.retval = &retval;
fci.param_count = 1;
fci.params = ¶m;
fci.no_separation = 1;
if (zend_hash_find_ex(EG(function_table), ZSTR_KNOWN(ZEND_STR_MYEXT_PROCESS_DATA), (void **)&fcc.function_handler, 1) == NULL) {
fprintf(stderr, "Function myext_process_data not found.n");
exit(1);
}
fcc.initialized = 1;
fcc.function_handler = ZSTR_KNOWN(ZEND_STR_MYEXT_PROCESS_DATA);
fcc.calling_scope = NULL;
fcc.called_scope = NULL;
if (zend_call_function(&fci, &fcc) == FAILURE) {
fprintf(stderr, "Error calling myext_process_data.n");
}
zval_dtor(&retval);
zend_string_release(input_str);
}
}
zend_hash_destroy(&EG(symbol_table));
sapi_shutdown();
return 0;
}
代码解释:
__AFL_LOOP(10000):AFL 的宏,用于循环执行 Fuzzing 过程。__AFL_FUZZ_TESTCASE_BUF和__AFL_FUZZ_TESTCASE_LEN:AFL 的宏,用于获取 AFL 生成的测试数据及其长度。zend_string_init:将 C 字符串转换为 Zend 字符串,PHP 内部使用 Zend 字符串来表示字符串。zend_fcall_info和zend_fcall_info_cache:用于构建 PHP 函数调用信息。zend_call_function:调用 PHP 函数。sapi_startup和sapi_shutdown:初始化和关闭 PHP 的 SAPI (Server Application Programming Interface) 环境。
编译 fuzz_myext.c:
使用 AFL 提供的 afl-gcc 或 afl-clang 编译器来编译 fuzz_myext.c:
afl-gcc -I/path/to/php/include -I/path/to/php/include/main -I/path/to/php/include/Zend -o fuzz_myext fuzz_myext.c -lphp -lmyext
参数解释:
-I/path/to/php/include:指定 PHP 头文件所在的目录。你需要根据你的 PHP 安装路径进行修改。-lphp:链接 PHP 库。-lmyext:链接myext扩展库。
运行 AFL:
mkdir afl_in afl_out
echo "test" > afl_in/seed
afl-fuzz -i afl_in -o afl_out ./fuzz_myext
参数解释:
-i afl_in:指定输入目录,AFL 会从该目录读取初始的测试用例。-o afl_out:指定输出目录,AFL 会将发现的崩溃和新的测试用例保存到该目录。./fuzz_myext:指定要 Fuzzing 的程序。
AFL 会不断生成新的测试用例,并监控 fuzz_myext 程序的执行情况。如果发现了崩溃,它会将导致崩溃的测试用例保存到 afl_out/crashes 目录中。
4.3 使用 libFuzzer 进行 Fuzzing
使用 libFuzzer 的方法类似,但需要编写一个不同的 main 函数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "php.h"
#include "ext/standard/info.h"
#include "myext.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
zend_string *input_str;
// 初始化 PHP 环境 (只在第一次调用时初始化)
static bool initialized = false;
if (!initialized) {
sapi_startup(&sapi_module);
zend_hash_init(&EG(symbol_table), 50, NULL, ZVAL_PTR_DTOR, 0);
initialized = true;
}
if (size > 0) {
input_str = zend_string_init((char *)data, size, 0);
// 调用 myext_process_data 函数
zval retval;
zval param;
ZVAL_STR(¶m, input_str);
zend_fcall_info fci;
zend_fcall_info_cache fcc;
fci.size = sizeof(fci);
ZVAL_NULL(&fci.function_name);
fci.object = NULL;
fci.retval = &retval;
fci.param_count = 1;
fci.params = ¶m;
fci.no_separation = 1;
if (zend_hash_find_ex(EG(function_table), ZSTR_KNOWN(ZEND_STR_MYEXT_PROCESS_DATA), (void **)&fcc.function_handler, 1) == NULL) {
fprintf(stderr, "Function myext_process_data not found.n");
exit(1);
}
fcc.initialized = 1;
fcc.function_handler = ZSTR_KNOWN(ZEND_STR_MYEXT_PROCESS_DATA);
fcc.calling_scope = NULL;
fcc.called_scope = NULL;
if (zend_call_function(&fci, &fcc) == FAILURE) {
fprintf(stderr, "Error calling myext_process_data.n");
}
zval_dtor(&retval);
zend_string_release(input_str);
}
return 0;
}
代码解释:
LLVMFuzzerTestOneInput:libFuzzer 要求的入口函数,它接受一个指向输入数据的指针和一个表示数据长度的参数。- PHP 环境的初始化只需要进行一次,所以使用
static bool initialized来保证只初始化一次。
编译 fuzz_myext.c:
clang -fsanitize=address -I/path/to/php/include -I/path/to/php/include/main -I/path/to/php/include/Zend -c fuzz_myext.c -o fuzz_myext.o
clang -fsanitize=address fuzz_myext.o -o fuzz_myext -lphp -lmyext -llz4 -lstdc++
运行 libFuzzer:
mkdir libfuzzer_in
echo "test" > libfuzzer_in/seed
./fuzz_myext libfuzzer_in
libFuzzer 也会不断生成新的测试用例,并监控 fuzz_myext 程序的执行情况。如果发现了崩溃,它会将导致崩溃的测试用例保存到当前目录中。
4.4 分析崩溃
当 AFL 或 libFuzzer 发现崩溃时,你需要分析导致崩溃的输入数据,找到漏洞的根源。可以使用 GDB 等调试器来分析崩溃现场。
5. Fuzzing FFI 接口
Fuzzing FFI 接口的方法与 Fuzzing 扩展类似,但需要使用 PHP 代码来调用 FFI 函数,并使用 PHP 的 Fuzzing 工具或自定义脚本来生成测试数据。
例如,假设你有一个 C 函数 process_data,你想通过 FFI 在 PHP 中调用它:
// process_data.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int process_data(const char *data, size_t len) {
if (len > 1024) {
return -1; // 数据过长
}
char *buffer = (char *)malloc(len + 1);
if (buffer == NULL) {
return -2; // 内存分配失败
}
memcpy(buffer, data, len);
buffer[len] = '';
printf("Processing data: %sn", buffer);
free(buffer);
return 0;
}
你可以使用以下 PHP 代码来调用 process_data 函数:
<?php
$ffi = FFI::cdef(
"int process_data(const char *data, size_t len);",
"./process_data.so" // 编译后的共享库
);
// 生成随机数据
function generate_random_string(int $length): string {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$randomString .= $characters[$index];
}
return $randomString;
}
// Fuzzing 循环
for ($i = 0; $i < 10000; $i++) {
$length = rand(0, 2048); // 随机长度
$data = generate_random_string($length);
try {
$result = $ffi->process_data($data, strlen($data));
// 可以根据返回值进行一些检查
} catch (FFIException $e) {
echo "FFI Exception: " . $e->getMessage() . "n";
// 记录导致异常的数据
file_put_contents("ffi_crashes/crash_" . $i . ".txt", $data);
exit(1);
}
}
echo "Fuzzing completed.n";
?>
代码解释:
FFI::cdef:定义 FFI 接口,指定 C 函数的原型和共享库的路径。generate_random_string:生成随机字符串。try...catch:捕获 FFI 异常。file_put_contents:将导致异常的数据保存到文件中。
编译 process_data.c:
gcc -shared -fPIC -o process_data.so process_data.c
运行 PHP 脚本:
mkdir ffi_crashes
php ffi_fuzz.php
6. Fuzzing 测试的最佳实践
- 代码覆盖率: 使用代码覆盖率工具来评估 Fuzzing 测试的有效性,确保测试覆盖了尽可能多的代码路径。
- 输入数据多样性: 生成各种类型的输入数据,包括随机数据、边界值、特殊字符等。
- 持续集成: 将 Fuzzing 测试集成到持续集成流程中,定期进行测试,及时发现和修复漏洞。
- 漏洞报告: 详细记录发现的漏洞,包括漏洞的描述、重现步骤、影响范围等。
- 针对性测试: 根据程序的特点和已知的漏洞,进行针对性的 Fuzzing 测试。
表格:常用 Fuzzing 工具对比
| 工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| AFL | 基于覆盖率引导,能够有效地发现漏洞;社区活跃,资料丰富。 | 学习曲线较陡峭,需要一定的 C/C++ 基础;配置较为复杂。 | C/C++ 程序,特别是需要高覆盖率的场景。 |
| libFuzzer | 与 Clang 集成,使用方便;支持多种 Sanitizer,能够检测多种类型的漏洞。 | 需要 Clang 编译器;对输入数据的格式要求较高。 | C/C++ 程序,特别是需要快速集成和使用 Sanitizer 的场景。 |
| Peach Fuzzer | 基于模型,能够生成更有针对性的测试数据;支持多种数据格式。 | 学习曲线较陡峭,需要定义输入数据的模型;配置较为复杂。 | 需要精确控制输入数据格式的场景。 |
| 自定义脚本 | 灵活,可以根据程序的特点进行定制;易于学习和使用。 | 需要手动编写代码,工作量较大;测试效果取决于脚本的质量。 | 简单的测试场景,或需要对特定函数进行测试的场景。 |
总结一些需要注意的点
PHP Fuzzing 测试是一个重要的安全实践,可以帮助我们发现扩展和 FFI 接口中的潜在漏洞。 选择合适的工具,掌握基本的技术,并遵循最佳实践,能够有效地提高 PHP 应用的安全性。记住,持续的测试和分析是确保系统安全的关键。