PHP代码的模糊测试(Fuzzing):利用libFuzzer对FFI接口或扩展输入进行攻击

PHP Fuzzing:利用libFuzzer攻击FFI接口和扩展

各位好,今天我们来探讨一个非常有趣且重要的主题:PHP Fuzzing,特别是如何利用libFuzzer对PHP的FFI接口和扩展进行攻击。Fuzzing,也称为模糊测试,是一种通过向程序输入大量随机或半随机数据,以寻找程序漏洞的技术。在PHP的上下文中,我们可以利用它来发现FFI接口和扩展中潜在的内存安全问题、逻辑错误或其他类型的安全缺陷。

1. 什么是Fuzzing?

Fuzzing是一种自动化测试技术,其核心思想是:

  1. 生成输入: 创建大量的、多样化的输入数据。这些数据可以是完全随机的,也可以是基于已知输入格式进行变异的。

  2. 执行程序: 将这些输入数据传递给目标程序。

  3. 监控状态: 监控程序在执行过程中的状态,例如崩溃、内存错误、断言失败等。

  4. 分析结果: 如果程序出现异常,记录导致异常的输入数据,并分析原因。

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_name zval。
  • ZVAL_STR(&input_param, script_text);: 使用 script_text (包含 Fuzzer 提供的输入数据) 初始化 input_param zval。 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 */

为了编译这个扩展,你需要使用 phpizephp-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(&params[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应用安全性的关键。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注