
مختبر توجيهي بتغطية الحواف لمكتبات PHP يكتشف الأخطاء من خلال الأعطال، وانتهاء المهلات، والتحذيرات. يدعم إدارة المجموعة النصية، وتقليل الأعطال، وتقارير تغطية الشفرة.
تقوم هذه المكتبة بتنفيذ مختبر عشوائي (Fuzzer) بلغة 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';
// مطلوب: الهدف يقبل سلسلة إدخال واحدة ويشغلها من خلال المكتبة المختبرة.
// يُسمح للهدف بإلقاء الاستثناءات العادية (Exception) (التي يتم تجاهلها)،
// لكن استثناءات الخطأ (Error) تُعتبر خطأ مكتشفاً.
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// اختياري: العديد من الأهداف لا تُظهر أخطاءً على مدخلات كبيرة لا يمكن إنتاجها أيضاً
// بمدخلات صغيرة. تحديد الطول الأقصى قد يحسن الأداء.
$config->setMaxLen(1024);
// اختياري: يمكن استخدام قاموس لتزويد المختبر العشوائي بأجزاء مفيدة،
// مثل كلمات اللغة المفتاحية. هذا مهم بشكل خاص إذا لم يتمكن المختبر من اكتشافها
// بسهولة، لأنها تُعالج بواسطة دالة إضافة PHP غير موجهة مثل token_get_all().
$config->addDictionary('example/php.dict');
يتم تشغيل المختبر العشوائي على مجموعة أولية من المدخلات "المثيرة للاهتمام"، والتي يمكن مثلاً تزويدها بناءً على اختبارات الوحدة الموجودة. إذا لم يتم تحديد مجموعة، فسيتم إنشاء مجلد مجموعة مؤقت بدلاً من ذلك.
# تشغيل بدون مجموعة أولية
php-fuzzer fuzz target.php
# تشغيل مع مجموعة أولية (مدخل واحد لكل ملف)
php-fuzzer fuzz target.php corpus/
إذا تمت مقاطعة عملية الاختبار العشوائي، يمكن استئنافها لاحقاً عن طريق تحديد نفس مجلد المجموعة.
بمجرد العثور على تعطل (crash)، يتم كتابته في ملف 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.
أثناء تشغيل المختبر العشوائي، يقوم بالإبلاغ عن حالته باستمرار باستخدام سطر إخراج واحد. يتكون هذا السطر من الأجزاء التالية بالترتيب التالي:
NEW أو REDUCED: الإجراء الذي تسبب في سطر الحالة هذا. يشير NEW إلى أنه تمت إضافة مدخل جديد إلى المجموعة بينما يشير REDUCED إلى أنه تم استبدال إدخال موجود في المجموعة بمدخل أقصر.run: N: العدد الإجمالي لتكرارات الاختبار العشوائي (عمليات تنفيذ الهدف) التي تمت منذ بدء المختبر(N/s): سرعة التنفيذ الحالية، مقاسة بعدد مرات التشغيل في الثانيةft: N: العدد الإجمالي للميزات الفريدة المكتشفة حتى الآن(N/s): متوسط عدد الميزات الجديدة المكتشفة في الثانية منذ بدء المختبرcorp: N: عدد المدخلات المثيرة للاهتمام المخزنة حالياً في المجموعة(%s): الحجم الإجمالي لجميع المدخلات في المجموعةlen: %d/%d: الرقم الأول هو طول (بالبايت) المدخل الحالي الذي تسبب في الإجراء، والرقم الثاني هو الحد الأقصى المسموح به حالياً لطول المدخلt: إجمالي الوقت المنقضي منذ بدء المختبر، بالثوانييكتشف المختبر العشوائي بشكل افتراضي ثلاثة أنواع من الأخطاء:
Error التي يلقيها الهدف المُختبر. بينما تعتبر استثناءات Exception نتيجة طبيعية لـ
المدخلات غير الصالحة، فإن استثناءات Error غير المعالجة تشير دائماً إلى خطأ برمجي. وهي تُنتج بشكل
شائع بواسطة PHP نفسها، على سبيل المثال عند استدعاء دالة على null.Error.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('النتائج غير متطابقة!');
}
});
تعتمد العديد من التفاصيل التقنية لهذا المختبر العشوائي على libFuzzer من مشروع LLVM. فيما يلي وصف لبعض تفاصيل التنفيذ.
لكي يعمل الاختبار العشوائي بكفاءة، فإنه يحتاج إلى تغذية راجعة بشأن مسارات الكود التي تم تنفيذها أثناء اختبار مدخل معين. يتم جمع تغطية التغذية الراجعة هذه عن طريق "توجيه" الهدف المُختبر. يتم استخدام مكتبة include-interceptor لتحويل كود جميع الملفات المضمنة أثناء التنفيذ. يتم استخدام مكتبة PHP-Parser لتحليل الكود و العثور على جميع الأماكن التي تحتاج إلى إدراج كود توجيه إضافي.
داخل كل كتلة أساسية (basic block)، يتم إدراج الكود التالي، حيث يكون 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) { ... }
// يصبح
if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
في المستقبل، سيكون من المفيد أيضاً توجيه المقارنات، بحيث يمكننا تحديد إدخالات القاموس تلقائياً
من مقارنات مثل $foo == "SOME_STRING".
تعتبر مدخلات الاختبار العشوائي "مثيرة للاهتمام" إذا كانت تحتوي على ميزات جديدة لم يتم ملاحظتها مع مدخلات أخرى موجودة بالفعل في المجموعة. تستخدم هذه المكتبة عدد مرات الوصول إلى الحواف ذات الحبيبات الخشنة كميزات:
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
يقلل عدد الزيارات التقريبي من عدد الزيارات الفعلي إلى 8 فئات (بناءً على AFL):
0: 0 زيارة
1: زيارة واحدة
2: زيارتان
3: 3 زيارات
4: 4-7 زيارات
5: 8-15 زيارة
6: 16-127 زيارة
7: >=128 زيارة
وبالتالي، يرتبط كل مدخل بمجموعة من الأعداد الصحيحة التي تمثل الميزات. بالإضافة إلى ذلك، لديه مجموعة من "الميزات الفريدة"، وهي ميزات لم تُشاهد في أي مدخلات أخرى في المجموعة في وقت اختبار المدخل.
إذا كان للمدخل ميزات فريدة، فسيتم إضافته إلى المجموعة (NEW). إذا تم إنشاء مدخل B عن طريق تحوير مدخل A، ولكن المدخل B أقصر ولديه جميع الميزات الفريدة للمدخل A، فسيتم استبدال A بـ B في المجموعة (REDUCE).
في كل تكرار، يتم اختيار مدخل عشوائي من المجموعة الحالية، ثم تحويره باستخدام سلسلة من المحورات. المحورات التالية (مأخوذة من libFuzzer) منفذة حالياً:
EraseBytes: إزالة عدد من البايتات.InsertByte: إدراج بايت عشوائي جديد.InsertRepeatedBytes: إدراج بايت عشوائي مكرر عدة مرات.ChangeByte: استبدال بايت ببايت عشوائي.ChangeBit: قلب بت واحد.ShuffleBytes: خلط سلسلة فرعية صغيرة.ChangeASCIIInt: تغيير عدد صحيح ASCII عن طريق الزيادة/النقصان/المضاعفة/التنصيف.ChangeBinInt: تغيير عدد صحيح ثنائي عن طريق إضافة كمية عشوائية صغيرة.CopyPart: نسخ جزء من السلسلة إلى جزء آخر، إما بالكتابة فوقه أو إدراجه.CrossOver: التهجين مع مدخل آخر من المجموعة باستخدام استراتيجيات متعددة.AddWordFromManualDictionary: إدراج أو الكتابة فوق كلمة من القاموس (إن وجد).يخضع التحوير لقيود الطول الأقصى. على الرغم من أنه يمكن تحديد طول أقصى إجمالي بواسطة الهدف
(setMaxLength())، إلا أن المختبر العشوائي يقوم أيضاً بالتحكم التلقائي في الطول (--len-control-factor).
يتم تعيين الطول الأقصى مبدئياً إلى قيمة منخفضة جداً ثم زيادته بمقدار log(maxlen) كلما لم يتم
اتخاذ أي إجراء (NEW أو REDUCE) لآخر len_control_factor * log(maxlen) تشغيلة.
كلما زاد عامل التحكم في الطول، كلما كان المختبر العشوائي أكثر عدوانية في استكشاف المدخلات القصيرة قبل السماح بمدخلات أطول. هذا يقلل بشكل كبير من حجم المجموعة المولدة، لكنه يجعل الاستكشاف الأولي أبطأ.
mem: استخدام الذاكرة الحالي لعملية PHP