
مختبر إرشادي بالتغطية لرمز Ruby الخالص وامتدادات Ruby C
مُختبر تغطية موجّه لرمز Ruby الخالص وإضافات Ruby C extensions.
Ruzzy مستوحى بشدة من Google's Atheris، وهو مختبر Python. يستخدم Ruzzy libFuzzer (أو LibAFL) لآلية تغطية التتبع ومحرك الاختبار. كما يدعم Ruzzy AddressSanitizer و UndefinedBehaviorSanitizer عند اختبار إضافات C. إذا كنت ترغب في معرفة المزيد عن الدافع وراء Ruzzy، طالع ورقتنا: Design and Implementation of a Coverage-Guided Ruby Fuzzer.
جدول المحتويات:
Ruzzy يدعم Linux (x86-64, AArch64/ARM64) و macOS (Apple Silicon). على Windows، يمكنك بناء Dockerfile و/أو استخدام بيئة التطوير. يتطلب Ruzzy إصدارًا حديثًا من clang (تم اختباره حتى 14.0.0)، ويفضل أحدث إصدار. للإعداد الخاص بـ macOS، انظر ملاحظات لمستخدمي macOS.
قم بتثبيت Ruzzy باستخدام الأمر التالي:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
gem install ruzzy
هناك الكثير يحدث هنا، لذا دعنا نوضحه:
MAKE يتحكم في أمر make عند تجميع إضافة Ruzzy C. هذا يخبر make باحتترام متغيرات البيئة التالية عند تجميع الإضافة.clang. ذلك يضمن توفر أحدث ميزات clang، الضرورية للاختبار المناسب.إذا واجهت مشاكل في التثبيت، يمكنك تشغيل الأمر التالي للحصول على مخرجات تصحيح:
RUZZY_DEBUG=1 gem install --verbose ruzzy
إذا كان هدف الاختبار الخالص لـ Ruby يستخدم تعبيرات regex بشكل كثيف، فقم بتثبيت regexp_parser:
gem install regexp_parser
بمجرد التثبيت، سيستخدم Ruzzy تلقائيًا هذه الوظيفة لأخذ عينات (أي حل) من التعبيرات النمطية التي يصادفها أثناء الاختبار. هذا يسمح للمختبر بتجاوز شروط التعبير النمطي وفتح تغطية إضافية.
Ruzzy يتضمن مثالًا توضيحيًا لشرح كيفية عمله. أولاً، قم بتعيين متغير البيئة التالي:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
ASAN_OPTIONSيمكنك بعد ذلك تشغيل المثال باستخدام الأمر التالي:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
LD_PRELOAD مطلوب لنفس الأسباب كما في Atheris. ومع ذلك، على عكس ASAN_OPTIONS، ربما لا ترغب في تصديرها (export) لأنها قد تتداخل مع برامج أخرى.
يجب أن ينتج بسرعة تعطلًا مثل التالي:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2527961537
...
==45==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c0009bab80 at pc 0xffff99ea1b44 bp 0xffffce8a67d0 sp 0xffffce8a67c8
...
SUMMARY: AddressSanitizer: heap-use-after-free /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/ext/dummy/dummy.c:18:24 in _c_dummy_test_one_input
...
==45==ABORTING
MS: 4 EraseBytes-CopyPart-CopyPart-ChangeBit-; base unit: 410e5346bca8ee150ffd507311dd85789f2e171e
0x48,0x49,
HI
artifact_prefix='./'; Test unit written to ./crash-253420c1158bc6382093d409ce2e9cff5806e980
Base64: SEk=
يمكننا رؤية أنه وجد بشكل صحيح الإدخال ("HI") الذي أنتج انتهاكًا للذاكرة. لمزيد من المعلومات، انظر dummy.c لمعرفة سبب حدوث هذا الانتهاك.
يمكنك إعادة تشغيل حالة التعطل باستخدام الأمر التالي:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
المختبرات التالية متاحة:
Ruzzy::ASAN_PATH لـ AddressSanitizerRuzzy::UBSAN_PATH لـ UndefinedBehaviorSanitizerلنختبر سكربت Ruby صغير كمثال. اختبار كود Ruby الخالص يتطلب سكربتين: سكربت تتبع وسكربت تسخير. سكربت التتبع مطلوب بسبب تفاصيل تنفيذ مترجم Ruby.
أولاً، سكربت التتبع، لنسميه test_tracer.rb:
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
ثانيًا، سكربت التسخير، لنسميه test_harness.rb:
# frozen_string_literal: true
require 'ruzzy'
def fuzzing_target(input)
if input.length == 4
if input[0] == 'F'
if input[1] == 'U'
if input[2] == 'Z'
if input[3] == 'Z'
raise
end
end
end
end
end
end
test_one_input = lambda do |data|
fuzzing_target(data) # Your fuzzing target would go here
return 0
end
Ruzzy.fuzz(test_one_input)
يمكنك تشغيل هذا الملف وبدء الاختبار باستخدام الأمر التالي:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
يجب أن ينتج بسرعة تعطلًا مثل التالي:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2311041000
...
/app/ruzzy/bin/test_harness.rb:12:in `block in <top (required)>': unhandled exception
from /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/lib/ruzzy.rb:15:in `c_fuzz'
from /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/lib/ruzzy.rb:15:in `fuzz'
from /app/ruzzy/bin/test_harness.rb:35:in `<top (required)>'
from bin/test_tracer.rb:7:in `require_relative'
from bin/test_tracer.rb:7:in `<main>'
...
SUMMARY: libFuzzer: fuzz target exited
MS: 1 CopyPart-; base unit: 24b4b428cf94c21616893d6f94b30398a49d27cc
0x46,0x55,0x5a,0x5a,
FUZZ
artifact_prefix='./'; Test unit written to ./crash-aea2e3923af219a8956f626558ef32f30a914ebc
Base64: RlVaWg==
يمكننا رؤية أنه وجد بشكل صحيح الإدخال ("FUZZ") الذي أنتج استثناءً.
لاختبار هدفك الخاص، قم بتعديل lambda لـ test_one_input لاستدعاء دالتك المستهدفة.
لنختبر مكتبة msgpack-ruby كمثال. أولاً، قم بتثبيت الجيم:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
CFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
CXXFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
gem install msgpack
بالإضافة إلى متغيرات البيئة المستخدمة عند تجميع Ruzzy، نقوم بتحديد CFLAGS و CXXFLAGS. هذه العلامات تساعد في عملية الاختبار. تمكن وظائف مفيدة مثل محدد العناوين ومعلومات تتبع أفضل. لمزيد من المعلومات، انظر AddressSanitizerFlags.
بعد ذلك، نحتاج إلى سكربت تسخير لـ msgpack. التالي قد يكون مألوفًا لمن لديه خبرة مع libFuzzer:
# frozen_string_literal: true
require 'msgpack'
require 'ruzzy'
test_one_input = lambda do |data|
begin
MessagePack.unpack(data)
rescue Exception
# We're looking for memory corruption, not Ruby exceptions
end
return 0
end
Ruzzy.fuzz(test_one_input)
لنسمي هذا الملف fuzz_msgpack.rb. يمكنك تشغيل هذا الملف وبدء الاختبار باستخدام الأمر التالي:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
يمكن تمرير خيارات libFuzzer إلى سكربت Ruby مثل هذا:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
راجع خيارات libFuzzer لمزيد من المعلومات.
لاختبار هدفك الخاص، قم بتعديل lambda لـ test_one_input لاستدعاء دالتك المستهدفة.
الوحدة Ruzzy تعرض نقاط الدخول الرئيسية.
Ruzzy::FuzzedDataProvider يقسم البايتات الخام للمختبر إلى قيم Ruby مكتوبة.
test_one_input = lambda do |data|
fdp = Ruzzy::FuzzedDataProvider.new(data)
name = fdp.consume_random_length_string(50)
age = fdp.consume_int_in_range(0, 150)
score = fdp.consume_float_in_range(0.0, 100.0)
role = fdp.pick_value_in_list(['admin', 'user', 'guest'])
User.new(name: name, age: age, score: score, role: role).validate!
end
Ruzzy.fuzz(test_one_input)
جميع الطرق تعيد القيم الافتراضية (0, "", false, min) عندما تنفد البيانات.
Ruzzy على macOS يتطلب LLVM المثبت عبر Homebrew (Apple Clang لا يتضمن libFuzzer) و Ruby غير النظامي (Ruby النظامي في /usr/bin/ruby محمي بواسطة SIP، الذي يزيل متغيرات البيئة DYLD_* قبل بدء Ruby).
brew install llvm ruby
أي Ruby غير النظامي يعمل (brew, rbenv, asdf)، لكن انظر التحذيرات أدناه لمديري الإصدارات القائمة على الواجهات.
استخدم مسارات Homebrew Clang وعلماء الروابط المناسبة لـ macOS:
MAKE="make --environment-overrides V=1" \
CC="$(brew --prefix llvm)/bin/clang" \
CXX="$(brew --prefix llvm)/bin/clang++" \
LDSHARED="$(brew --prefix llvm)/bin/clang -dynamic -bundle -undefined dynamic_lookup" \
LDSHAREDXX="$(brew --prefix llvm)/bin/clang++ -dynamic -bundle -undefined dynamic_lookup" \
gem install ruzzy
استخدم DYLD_INSERT_LIBRARIES بدلاً من LD_PRELOAD:
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
Ruzzy::ASAN_PATH و Ruzzy::UBSAN_PATH يتم حلهما إلى ملفات .dylib على macOS.
asdf, rbenv) تزيل متغيرات البيئة DYLD_*. هذه الواجهات تستخدم #!/usr/bin/env bash و /usr/bin/env محمي بواسطة SIP، لذا macOS يزيل DYLD_INSERT_LIBRARIES قبل بدء Ruby. إما استخدم Ruby من Homebrew (بدون واجهة) أو استدع المسار المطلق لثنائية Ruby المثبتة:
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIES للمكتبة الديناميكية ASan يعلق العملية أثناء بدء التشغيل. إذا رأيت Ruzzy يتعطل عند الإطلاق، قم بتحديث Homebrew LLVM (brew upgrade llvm).أخطاء تم اكتشافها باستخدام Ruzzy:
toml: #76toml-rb: #150ox: #351, #410Marshal: #20941redcarpet: #813يمكن إجراء التطوير محليًا، أو باستخدام Dockerfile المقدم في هذا المستودع.
يمكنك بناء صورة Docker لـ Ruzzy باستخدام الأمر التالي:
docker build --tag ruzzy .
ثم، يمكنك الدخول إلى الحاوية باستخدام الأمر التالي:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
نستخدم rake-compiler لتجميع إضافات Ruzzy C.
يمكنك تجميع إضافات C داخل الحاوية باستخدام الأمر التالي:
rake compile
نستخدم اختبارات الوحدة rake لاختبار كود Ruby.
يمكنك تشغيل الاختبارات داخل الحاوية باستخدام الأمر التالي:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
نستخدم rubocop لتدقيق كود Ruby.
يمكنك تشغيل rubocop داخل الحاوية باستخدام الأمر التالي:
rubocop
Ruzzy يتم إصداره تلقائيًا إلى RubyGems عند دفع علامة git جديدة.
لإصدار إصدار جديد، قم بتشغيل الأوامر التالية:
git tag vX.X.X
git push --tags
| الطريقة | الوصف |
|---|
Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS) | اختبر test_one_input (إجراء/لامبدا يأخذ بايتات خام). |
Ruzzy.trace(harness_script) | قم بلف harness_script بأداة تتبع تغطية الفروع في Ruby، ثم require. مطلوب لاختبار Ruby الخالص. |
Ruzzy.dummy | اختبر أداة التسخير التجريبية المرفقة (عرض لانتهاك heap-use-after-free). |
Ruzzy.dummy_test_one_input(data) | أداة التسخير التجريبية نفسها. |
| الثابت | الوصف |
|---|
Ruzzy::ASAN_PATH | المسار إلى غلاف ASan + المختبر. استخدم مع LD_PRELOAD (Linux) / DYLD_INSERT_LIBRARIES (macOS). |
Ruzzy::UBSAN_PATH | نفسه، لـ UBSan. |
Ruzzy::EXT_PATH | المسار إلى مجلد بناء ext/cruzzy. |
Ruzzy::DEFAULT_ARGS | الوسائط الافتراضية التي يتم تمريرها إلى المختبر. |
| الطريقة | الوصف | الإرجاع |
|---|
remaining_bytes | عدد البايتات غير المستهلكة المتبقية | Integer |
consume_bytes(count) | استهلك حتى count بايت خام | String ثنائي |
consume_random_length_string(max_length) | استهلك سلسلة ذات طول متغير؛ تنتهي عند \ + بايت غير \ | String |
consume_remaining_bytes | استهلك جميع البايتات المتبقية | String ثنائي |
consume_remaining_as_string | اسم مستعار لـ consume_remaining_bytes | String ثنائي |
consume_uint(count) | عدد صحيح غير موقّع من count بايت | Integer |
consume_int(count) | عدد صحيح موقّع (مكمل ثنائي) من count بايت | Integer |
consume_int_in_range(min, max) | عدد صحيح موزع بشكل موحد في [min, max] | Integer |
consume_bool | منطقي من بايت واحد (البت الأقل أهمية) | true/false |
consume_float | عائم يغطي نطاق double الكامل | Float |
consume_float_in_range(min, max) | عائم في [min, max] | Float |
consume_probability | عائم في [0.0, 1.0] | Float |
pick_value_in_list(list) | عنصر عشوائي من list | عنصر |