PHP Fuzzing测试入门:针对扩展或FFI接口的随机输入安全测试

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 的核心思想是 "尝试触发错误"。它通过以下步骤工作:

  1. 目标识别: 确定需要进行 Fuzzing 测试的目标,例如特定的 PHP 扩展函数或 FFI 接口。
  2. 输入生成: 生成大量的、各种类型的输入数据。这些数据可以是完全随机的,也可以是基于已有数据进行变异的。
  3. 执行目标: 将生成的输入数据传递给目标程序,并执行。
  4. 监控: 监控目标程序的执行过程,检测是否发生了崩溃、异常或其他异常行为。
  5. 分析: 如果发现了异常,分析导致异常的输入数据,并尝试重现该异常,最终确定漏洞的根源。

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 准备工作

  1. 安装必要的工具: 确保你已经安装了 AFL 或 libFuzzer,以及 PHP 的开发环境。
  2. 创建扩展框架: 如果你还没有 myext 扩展,需要先创建一个基本的扩展框架。可以使用 phpizephp-config 工具来完成。
  3. 编写测试代码: 创建一个 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(&param, 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 = &param;
      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_infozend_fcall_info_cache:用于构建 PHP 函数调用信息。
  • zend_call_function:调用 PHP 函数。
  • sapi_startupsapi_shutdown:初始化和关闭 PHP 的 SAPI (Server Application Programming Interface) 环境。

编译 fuzz_myext.c:

使用 AFL 提供的 afl-gccafl-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(&param, 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 = &param;
    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 应用的安全性。记住,持续的测试和分析是确保系统安全的关键。

发表回复

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