nikic / php-fuzzer
Requires
- php: >= 7.4
- nikic/include-interceptor: ^0.1.1
- nikic/php-parser: ^4.3
- ulrichsg/getopt-php: ^4.0
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^9
Suggests
- ext-pcntl: Needed for timeout support
README
此库实现了一个用于PHP的模糊测试器,可以通过向其提供“随机”输入来查找库(尤其是解析库)中的错误。使用边缘覆盖率仪表反馈来指导“随机”输入的选择,以便访问新的代码路径。
安装
Phar(推荐):您可以从发行页面下载此库的phar包。使用phar是推荐的,因为它可以避免与使用PHP-Parser的库发生依赖冲突。
Composer: composer global require nikic/php-fuzzer
用法
首先,需要一个目标函数的定义。以下是一个用于查找microsoft/tolerant-php-parser中错误的示例目标
<?php // target.php /** @var PhpFuzzer\Config $config */ require 'path/to/tolerant-php-parser/vendor/autoload.php'; // Required: The target accepts a single input string and runs it through the tested // library. The target is allowed to throw normal Exceptions (which are ignored), // but Error exceptions are considered as a found bug. $parser = new Microsoft\PhpParser\Parser(); $config->setTarget(function(string $input) use($parser) { $parser->parseSourceFile($input); }); // Optional: Many targets don't exhibit bugs on large inputs that can't also be // produced with small inputs. Limiting the length may improve performance. $config->setMaxLen(1024); // Optional: A dictionary can be used to provide useful fragments to the fuzzer, // such as language keywords. This is particularly important if these // cannot be easily discovered by the fuzzer, because they are handled // by a non-instrumented PHP extension function such as token_get_all(). $config->addDictionary('example/php.dict');
模糊测试器针对一组初始的“有趣”输入运行,这些输入可以基于现有的单元测试进行初始化。如果没有指定语料库,将创建一个临时的语料库目录。
# Run without initial corpus php-fuzzer fuzz target.php # Run with initial corpus (one input per file) php-fuzzer fuzz target.php corpus/
如果模糊测试被中断,可以稍后通过指定相同的语料库目录来继续。
一旦找到崩溃,它将被写入到crash-HASH.txt
文件中。它以最初找到的形式提供,可能是不必要的复杂,并可能包含与崩溃不相关的片段。因此,您可能希望首先减小崩溃输入。
php-fuzzer minimize-crash target.php crash-HASH.txt
这将生成一系列连续更小的minimized-HASH.txt
文件。如果您想快速检查崩溃输入产生的异常跟踪,可以使用run-single
命令。
php-fuzzer run-single target.php minimized-HASH.txt
最后,可以生成HTML代码覆盖率报告,该报告显示在执行给定语料库的输入时,目标中哪些代码块被击中。
php-fuzzer report-coverage target.php corpus/ coverage_dir/
此外,可以通过php-fuzzer --help
显示配置选项。
错误类型
模糊测试器默认检测三种类型的错误
错误
异常由模糊测试目标抛出。虽然Exception
异常被视为无效输入的正常结果,但未捕获的Error
异常始终表示编程错误。它们通常由PHP本身产生,例如在调用null
上的方法时。- 抛出的通知和警告(除非它们被抑制)。模糊测试器注册了一个错误处理程序,将这些转换为
Error
异常。 - 超时。如果目标运行时间超过指定的超时时间(默认:3s),则假定目标已进入无限循环。这通过
pcntl_alarm()
和一个异步信号处理程序实现,该处理程序在超时时抛出Error
。
值得注意的是,这些都没有检查目标的输出是否正确,它们只确定目标没有进行过分的错误行为。检查输出正确性的方法之一是比较两个应该产生相同结果的实现。
$fuzzer->setTarget(function(string $input) use($parser1, $parser2) { $result1 = $parser1->parse($input); $result2 = $parser2->parse($input); if ($result1 != $result2) { throw new Error('Results do not match!'); } });
技术
这个模糊测试器的许多技术细节基于LLVM项目的libFuzzer。以下描述了一些实现细节。
仪表化
为了高效工作,模糊测试需要关于在测试特定模糊测试输入时执行了哪些代码路径的反馈。这种覆盖率反馈是通过“仪表化”模糊测试目标来收集的。使用include-interceptor库实时转换所有包含文件的代码。使用PHP-Parser库解析代码并找到需要插入额外仪表化代码的所有位置。
在每个基本块内部,插入以下代码,其中BLOCK_INDEX
是每个块的唯一整数
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX; \PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1; \PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;
这假设块索引最大为28位,并计算在执行期间观察到的(prev_block, cur_block)
对的数量。由于需要处理未初始化的边缘计数以及使用静态属性,生成的代码相当昂贵。将来,可以创建一个PHP扩展,它可以更高效地收集覆盖率反馈。
在某些情况下,基本块是表达式的一部分,在这种情况下我们无法轻松地插入额外的代码。在这种情况下,我们插入对包含上述代码的方法的调用
if ($foo && $bar) { ... } // becomes if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
将来,对比较进行仪表化也将是有益的,这样我们就可以从像$foo == "SOME_STRING"
这样的比较中自动确定字典条目。
特性
如果模糊测试输入包含尚未在语料库中观察到的其他输入中观察到的特征,则被视为“有趣的”。这个库使用粗粒度的边缘命中计数作为特征
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
近似命中计数将实际命中计数减少到8个类别(基于AFL)
0: 0 hits
1: 1 hit
2: 2 hits
3: 3 hits
4: 4-7 hits
5: 8-15 hits
6: 16-127 hits
7: >=128 hits
因此,每个输入都与一组整数相关联,这些整数代表特征。此外,它还有一个“唯一特征”集合,这些特征在测试输入时的其他语料库输入中尚未看到。
如果输入具有唯一特征,则将其添加到语料库中(NEW)。如果输入B是通过修改输入A创建的,但输入B更短且具有输入A的所有唯一特征,则用B替换语料库中的A(REDUCE)。
变异
在每次迭代中,从当前语料库中选择一个随机输入,然后使用一系列突变器对其进行变异。以下突变器(来自libFuzzer)目前实现
EraseBytes
:删除一定数量的字节。InsertByte
:插入一个新随机字节。InsertRepeatedBytes
:插入重复多次的随机字节。ChangeByte
:用一个随机字节替换一个字节。ChangeBit
:翻转一个比特。ShuffleBytes
:洗牌一个小子串。ChangeASCIIInt
:通过增加/减少/加倍/减半来更改ASCII整数。ChangeBinInt
:通过添加一个小随机数来更改二进制整数。CopyPart
:将字符串的一部分复制到另一部分,可以通过覆盖或插入来实现。CrossOver
:使用多种策略与另一个语料库条目交配。AddWordFromManualDictionary
:如果有的话,从字典中插入或覆盖一个单词。
变异受最大长度的限制。虽然可以通过目标指定整体最大长度(setMaxLength()
),但模糊测试器还执行自动长度控制(--len-control-factor
)。最大长度最初设置为一个非常低的值,然后在连续len_control_factor * log(maxlen)
次没有采取行动(NEW或REDUCE)时,通过log(maxlen)
增加。
长度控制因子越高,模糊测试器在允许更长的输入之前探索短输入的积极性就越高。这显著减小了生成的语料库的大小,但使初始探索变慢。