PHP Fuzzing:利用libFuzzer攻击FFI接口和扩展
各位好,今天我们来探讨一个非常有趣且重要的主题:PHP Fuzzing,特别是如何利用libFuzzer对PHP的FFI接口和扩展进行攻击。Fuzzing,也称为模糊测试,是一种通过向程序输入大量随机或半随机数据,以寻找程序漏洞的技术。在PHP的上下文中,我们可以利用它来发现FFI接口和扩展中潜在的内存安全问题、逻辑错误或其他类型的安全缺陷。
1. 什么是Fuzzing?
Fuzzing是一种自动化测试技术,其核心思想是:
-
生成输入: 创建大量的、多样化的输入数据。这些数据可以是完全随机的,也可以是基于已知输入格式进行变异的。
-
执行程序: 将这些输入数据传递给目标程序。
-
监控状态: 监控程序在执行过程中的状态,例如崩溃、内存错误、断言失败等。
-
分析结果: 如果程序出现异常,记录导致异常的输入数据,并分析原因。
Fuzzing的优势在于其自动化和高效性,能够在短时间内覆盖大量的代码路径,发现一些人工测试难以发现的漏洞。
2. 为什么Fuzzing PHP FFI和扩展很重要?
-
FFI (Foreign Function Interface): FFI允许PHP代码直接调用C语言库,这极大地扩展了PHP的功能,但也引入了新的安全风险。如果FFI接口使用不当,可能导致内存损坏、代码执行等漏洞。
-
扩展: PHP扩展通常是用C/C++编写的,它们直接操作PHP的底层数据结构和API。扩展中的漏洞可能会影响整个PHP应用程序的安全性。
通过Fuzzing FFI接口和扩展,我们可以:
- 发现潜在的内存安全问题,例如缓冲区溢出、堆溢出、空指针解引用等。
- 发现输入验证方面的漏洞,例如格式化字符串漏洞、SQL注入等。
- 提高PHP应用程序的整体安全性。
3. libFuzzer简介
libFuzzer是一个基于覆盖率引导的模糊测试工具,它是LLVM项目的一部分。相比于传统的随机Fuzzer,libFuzzer具有以下优势:
- 覆盖率引导: libFuzzer会跟踪代码的覆盖率,并优先生成能够覆盖更多代码路径的输入数据。
- 高效性: libFuzzer采用了多种优化技术,例如突变算法、语料库管理等,以提高Fuzzing效率。
- 易于使用: libFuzzer提供了一套简单的API,可以很容易地集成到现有的项目中。
4. 如何使用libFuzzer Fuzz PHP FFI接口
以下是一个使用libFuzzer Fuzz PHP FFI接口的示例。假设我们有一个简单的C语言库 libexample.so,其中包含一个函数 process_data:
// libexample.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 假设这个函数存在缓冲区溢出漏洞
void process_data(const char* data, size_t size) {
char buffer[64];
if (size > 63) {
printf("Size too large, will cause a buffer overflow!n");
}
memcpy(buffer, data, size);
buffer[size] = ''; // Null-terminate the string
printf("Processed data: %sn", buffer);
}
首先,我们需要将这个C语言库编译成共享库:
gcc -shared -fPIC libexample.c -o libexample.so
接下来,我们需要编写一个PHP脚本,使用FFI调用process_data函数:
<?php
// ffi_example.php
$ffi = FFI::cdef(
"void process_data(const char* data, size_t size);",
"./libexample.so"
);
function fuzz_target(string $data): void {
global $ffi;
$ffi->process_data($data, strlen($data));
}
// This is just a placeholder. libFuzzer will call fuzz_target directly.
if (isset($argv[1])) {
fuzz_target($argv[1]);
}
?>
现在,我们需要创建一个C/C++ Fuzzer目标,这个目标将调用PHP脚本,并将Fuzzer生成的输入数据传递给它:
// fuzzer.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <cstdio>
#include <cstdlib>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// Create a temporary file to store the input data
std::ofstream tempFile("temp_input.txt");
tempFile.write((const char*)data, size);
tempFile.close();
// Construct the command to execute the PHP script
std::string command = "php ffi_example.php < temp_input.txt";
// Execute the PHP script using system()
int result = system(command.c_str());
// Clean up the temporary file
std::remove("temp_input.txt");
return 0;
}
这个C++代码创建了一个名为 LLVMFuzzerTestOneInput 的函数,该函数是 libFuzzer 的入口点。它接受一个字节数组 data 和其大小 size 作为输入。这个函数将输入数据写入一个临时文件,然后使用 system() 函数执行 PHP 脚本,并将临时文件作为标准输入传递给 PHP 脚本。最后,它会删除临时文件。
接下来,我们需要编译这个Fuzzer目标:
clang++ -std=c++11 -fsanitize=address,undefined -I/path/to/php/include -L/path/to/php/lib -lphp fuzzer.cpp -o fuzzer -lFuzzer
-fsanitize=address,undefined:启用地址消毒器和未定义行为消毒器,用于检测内存安全问题。-I/path/to/php/include:指定PHP头文件的路径。-L/path/to/php/lib:指定PHP库文件的路径。-lphp:链接PHP库。-lFuzzer:链接libFuzzer库。
最后,我们可以运行Fuzzer:
./fuzzer
libFuzzer将会生成大量的输入数据,并将它们传递给PHP脚本。如果PHP脚本由于FFI接口的漏洞而崩溃,libFuzzer将会记录导致崩溃的输入数据。
更高效的Fuzzer目标(推荐)
上面的Fuzzer目标使用 system() 函数执行 PHP 脚本,效率较低。一个更高效的方法是直接在 Fuzzer 目标中嵌入 PHP 解释器:
// fuzzer_direct.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <php.h>
#include <zend_exceptions.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
zend_string *script_text;
// Initialize the PHP interpreter if it hasn't been already
static bool initialized = false;
if (!initialized) {
php_embed_init(0, NULL);
initialized = true;
}
// Create a zend_string from the input data
script_text = zend_string_init((const char*)data, size, 0);
// Execute the PHP script
zend_eval_string(script_text, NULL, "fuzzed code");
// Clean up the zend_string
zend_string_release(script_text);
// Shutdown the PHP interpreter after fuzzing (optional, but recommended for stability)
// php_embed_shutdown(); // Uncomment this line to shutdown PHP after each run. This is slower.
return 0;
}
这个C++代码直接使用 PHP 的嵌入式 API 来执行 Fuzzer 生成的输入数据。这样可以避免使用 system() 函数带来的开销,从而提高 Fuzzing 效率。
编译这个更高效的Fuzzer目标:
clang++ -std=c++11 -fsanitize=address,undefined -I/path/to/php/include -L/path/to/php/lib -lphp -lzend -lFuzzer fuzzer_direct.cpp -o fuzzer_direct
修改ffi_example.php
需要修改 ffi_example.php,移除命令行参数的处理,因为现在 Fuzzer 直接将数据传递给 PHP 解释器。添加 fuzz_target 函数的调用代码:
<?php
// ffi_example.php
$ffi = FFI::cdef(
"void process_data(const char* data, size_t size);",
"./libexample.so"
);
function fuzz_target(string $data): void {
global $ffi;
$ffi->process_data($data, strlen($data));
}
// Call the fuzz target function directly
// This will be called by the fuzzer.
function php_fuzz_one_input(string $input) : void {
fuzz_target($input);
}
?>
编译完成后,您需要稍微修改 fuzzer_direct.cpp 文件,以便它包含并执行 ffi_example.php 文件中的 php_fuzz_one_input 函数。
// fuzzer_direct.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <php.h>
#include <zend_exceptions.h>
// Include the PHP script as a string
#include "ffi_example.php_string.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
zend_string *script_text;
zval retval;
// Initialize the PHP interpreter if it hasn't been already
static bool initialized = false;
if (!initialized) {
php_embed_init(0, NULL);
// Execute ffi_example.php to define the fuzz_target function
zend_eval_string(ZSTR_VAL(ffi_example_php_string), &retval, "ffi_example.php");
zval_ptr_dtor(&retval); // Clean up the return value
initialized = true;
}
// Create a zend_string from the input data
script_text = zend_string_init((const char*)data, size, 0);
// Call the php_fuzz_one_input function
zval function_name, input_param;
ZVAL_STRING(&function_name, "php_fuzz_one_input");
ZVAL_STR(&input_param, script_text);
zval *params[1] = {&input_param};
call_user_function(EG(function_table), NULL, &function_name, &retval, 1, params);
zval_ptr_dtor(&retval); // Clean up the return value
zval_dtor(&function_name); // Clean up the function name
zval_dtor(&input_param); // Clean up the input parameter
// Clean up the zend_string
zend_string_release(script_text);
return 0;
}
现在,您需要创建一个头文件 ffi_example.php_string.h,其中包含 ffi_example.php 文件的内容作为字符串。你可以使用以下命令生成这个头文件:
php -r '$code = file_get_contents("ffi_example.php"); echo "zend_string *ffi_example_php_string = zend_string_init("$code", strlen("$code"), 1);";' > ffi_example.php_string.h
解释:
#include "ffi_example.php_string.h": 包含包含 PHP 脚本字符串的头文件。php_embed_init(0, NULL);: 初始化 PHP 嵌入式解释器。zend_eval_string(ZSTR_VAL(ffi_example_php_string), &retval, "ffi_example.php");: 执行ffi_example.php脚本,这会定义php_fuzz_one_input函数。ZSTR_VAL获取zend_string的字符指针。zval function_name, input_param;: 声明zval变量来存储函数名称和输入参数。zval是 Zend Engine 中用于表示 PHP 变量的结构体。ZVAL_STRING(&function_name, "php_fuzz_one_input");: 使用字符串 "php_fuzz_one_input" 初始化function_namezval。ZVAL_STR(&input_param, script_text);: 使用script_text(包含 Fuzzer 提供的输入数据) 初始化input_paramzval。ZVAL_STR用于将zend_string赋值给zval。call_user_function(EG(function_table), NULL, &function_name, &retval, 1, params);: 调用用户定义的 PHP 函数。EG(function_table): 获取全局函数表。NULL: 表示没有对象。&function_name: 包含要调用的函数名称的zval。&retval: 用于存储函数返回值的zval。1: 参数的数量。params: 包含参数的zval数组。
zval_dtor(&function_name);和zval_dtor(&input_param);: 销毁zval结构以释放内存。
然后使用以下命令重新编译 Fuzzer:
clang++ -std=c++11 -fsanitize=address,undefined -I/path/to/php/include -L/path/to/php/lib -lphp -lzend -lFuzzer fuzzer_direct.cpp -o fuzzer_direct
使用相同的方法运行 Fuzzer:
./fuzzer_direct
5. 如何使用libFuzzer Fuzz PHP扩展
Fuzzing PHP扩展的过程与Fuzzing FFI接口类似,但需要直接操作PHP的扩展API。以下是一个简单的示例,假设我们有一个名为 myextension 的PHP扩展,其中包含一个函数 myextension_process_data:
// myextension.c
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "ext/standard/info.h"
PHP_FUNCTION(myextension_process_data);
ZEND_BEGIN_ARG_INFO_EX(arginfo_myextension_process_data, 0, 0, 1)
ZEND_ARG_INFO(0, data)
ZEND_END_ARG_INFO()
const zend_function_entry myextension_functions[] = {
PHP_FE(myextension_process_data, arginfo_myextension_process_data)
PHP_FE_END
};
zend_module_entry myextension_module_entry = {
STANDARD_MODULE_HEADER,
"myextension",
myextension_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
PHP_MYEXTENSION_VERSION,
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_MYEXTENSION
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(myextension)
#endif
PHP_FUNCTION(myextension_process_data)
{
char *data;
size_t data_len;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(data, data_len)
ZEND_PARSE_PARAMETERS_END();
// Vulnerable code: potential buffer overflow
char buffer[64];
if (data_len > 63) {
php_printf("Size too large, will cause a buffer overflow!n");
}
memcpy(buffer, data, data_len);
buffer[data_len] = ''; // Null-terminate the string
php_printf("Processed data: %sn", buffer);
}
创建 php_myextension.h 文件:
#ifndef PHP_MYEXTENSION_H
#define PHP_MYEXTENSION_H
#define PHP_MYEXTENSION_VERSION "0.1.0"
extern zend_module_entry myextension_module_entry;
#define phpext_myextension_ptr &myextension_module_entry
#ifdef PHP_WIN32
#define PHP_MYEXTENSION_API __declspec(dllexport)
#else
#define PHP_MYEXTENSION_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif
PHP_FUNCTION(myextension_process_data);
#endif /* PHP_MYEXTENSION_H */
为了编译这个扩展,你需要使用 phpize 和 php-config 工具。首先,创建 config.m4 文件:
PHP_ARG_ENABLE(myextension, whether to enable myextension support,
[--enable-myextension Enable myextension support])
if test "$PHP_MYEXTENSION" != "no"; then
PHP_NEW_EXTENSION(myextension, myextension.c, $ext_shared)
fi
然后执行以下命令:
phpize
./configure --enable-myextension
make
现在,我们需要创建一个C/C++ Fuzzer目标,这个目标将加载PHP扩展,并调用myextension_process_data函数:
// fuzzer_extension.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <php.h>
#include <zend_modules.h>
#include <ext/standard/info.h>
extern zend_module_entry myextension_module_entry; // Import the module entry
ZEND_DECLARE_MODULE_GLOBALS(myextension)
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
zend_string *input_data;
zval retval;
zval function_name;
zval params[1];
static bool initialized = false;
if (!initialized) {
php_embed_init(0, NULL);
// Load the extension
zend_module_entry* module = &myextension_module_entry;
if (zend_register_module(module) == FAILURE) {
std::cerr << "Failed to register module" << std::endl;
return 0; // Or handle the error as appropriate
}
initialized = true;
}
// Convert input data to zend_string
input_data = zend_string_init((const char*)data, size, 0);
ZVAL_STR(¶ms[0], input_data); // Set the parameter
// Call the PHP function
ZVAL_STRING(&function_name, "myextension_process_data");
if (call_user_function(EG(function_table), NULL, &function_name, &retval, 1, params) != SUCCESS) {
// std::cerr << "Function call failed" << std::endl;
}
zval_ptr_dtor(&retval); // Clean up return value
zval_dtor(&function_name);
zend_string_release(input_data);
return 0;
}
编译这个Fuzzer目标:
clang++ -std=c++11 -fsanitize=address,undefined -I/path/to/php/include -L/path/to/php/lib -lphp -lzend -lFuzzer fuzzer_extension.cpp -o fuzzer_extension -Wno-deprecated-declarations
-Wno-deprecated-declarations:抑制关于已弃用声明的警告。
最后,我们可以运行Fuzzer:
./fuzzer_extension
6. 优化Fuzzing过程
- 语料库: 提供一个初始的语料库,包含一些有效的输入数据。这可以帮助Fuzzer更快地找到有趣的代码路径。
- 字典: 提供一个字典,包含一些常见的字符串、数字或其他类型的token。这可以帮助Fuzzer生成更有效的输入数据。
- 代码覆盖率反馈: 使用代码覆盖率工具(例如gcov)来监控Fuzzing过程,并根据覆盖率结果调整Fuzzer的参数。
- 并行Fuzzing: 使用多个Fuzzer实例并行运行,以提高Fuzzing效率。
- 自定义突变: 根据目标程序的特点,自定义突变算法,以生成更有效的输入数据。
| 优化策略 | 描述 | 优势 | 适用场景 |
|---|---|---|---|
| 语料库 | 提供一组初始的、结构良好的输入样本,作为 Fuzzer 生成新输入的起点。 | 加速发现有效的代码路径,避免 Fuzzer 从完全随机的输入开始探索,提高效率。 | 适用于输入格式相对固定的程序,例如解析器、编译器。 |
| 字典 | 提供一组程序中常见的关键词、魔术数字、协议头等,帮助 Fuzzer 构造有意义的输入。 | 提高 Fuzzer 发现深层漏洞的可能性,因为这些关键词可能触发特定的代码逻辑。 | 适用于协议解析器、文件格式解析器等。 |
| 代码覆盖率 | 利用工具(如 gcov, lcov, llvm-cov)收集 Fuzzing 过程中代码的覆盖率信息,引导 Fuzzer 探索未覆盖的代码区域。 | 显著提高 Fuzzing 效率,确保尽可能多的代码被测试,降低遗漏漏洞的风险。 | 适用于任何类型的程序,尤其是大型、复杂的系统。 |
| 并行 Fuzzing | 同时运行多个 Fuzzer 实例,利用多核 CPU 的优势,大幅提升 Fuzzing 的速度。 | 显著缩短 Fuzzing 时间,增加发现漏洞的机会。 | 适用于计算资源充足的环境。 |
| 自定义突变 | 根据目标程序的特点,设计特定的输入突变策略,例如针对 JSON 格式数据的键值对修改、针对图像数据的像素变换等。 | 提高 Fuzzer 生成有效输入的能力,针对性地测试程序的薄弱环节。 | 适用于具有特定输入格式和处理逻辑的程序。 |
7. 调试Fuzzing结果
当Fuzzer发现崩溃时,我们需要分析崩溃原因,并修复漏洞。以下是一些调试Fuzzing结果的技巧:
- 重现崩溃: 使用Fuzzer提供的崩溃输入数据,重现崩溃。
- 使用调试器: 使用调试器(例如gdb)来分析崩溃时的堆栈信息和内存状态。
- 使用消毒器: 使用地址消毒器(AddressSanitizer)和未定义行为消毒器(UndefinedBehaviorSanitizer)来检测内存安全问题。
- 阅读源代码: 阅读源代码,理解崩溃相关的代码逻辑。
一些关键点
Fuzzing FFI和扩展是提高PHP应用安全性的重要手段。 libFuzzer 提供了一种高效且易于使用的方式来进行模糊测试。 通过合理的设置和优化,可以在短时间内发现大量的安全漏洞。
需要改进的方面
本文档只提供了一个入门级别的介绍。 实际应用中,需要根据目标程序的特点,选择合适的Fuzzing策略和工具。 持续进行Fuzzing,并及时修复发现的漏洞,是保证PHP应用安全性的关键。