چکیده – WebAssembly، یک استاندارد باز برای کدهای باینری میباشد که به سرعت در وب و فراتر از آن در حال گسترش و پذیرش است. از آنجا که باینریها اغلب با زبانهای سطح پایینی مانند C و ++C نوشته میشوند، میتوانند مملو از همان باگهایی باشند که در نمونههای سنتیِ متناظر آنها نیز وجود دارد. با این حال، ابزارهای محدودی برای کشف این باگها در باینریهای WebAssembly وجود دارد. ما در این مقاله، WAFL ؛ فازری برای باینریهای WebAssembly را معرفی میکنیم. WAFL مجموعهای از وصلهها (Patches) را به محیط اجرای WebAssembly یعنی WAVM اضافه میکند تا دادههای پوشش (Coverage Data) را برای فازر محبوب ++AFL تولید کند. به لطف قابلیت کامپایل پیش از اجرا (Ahead-of-Time یا AOT) در WAVM، WAFL از همان ابتدا عملکرد بسیار بالایی دارد. WAFL همچنین اسنپشاتهای سبک (Lightweight VM Snapshots) را اضافه میکند. با جایگزین کردن Forkها، که بهطور سنتی در مهارگرهای (Harnesses) ++AFL مورد استفاده قرار میگیرند، با اسنپشاتهای WAFL، مهارگرهای WAFL حتی میتوانند از نظر عملکرد خام فازینگ، از مهارگرهای بومی (Native Harnesses) مجهز به ابزارگذاری در زمان کامپایل (Compile-Time Instrumentation) نیز بهتر عمل کنند. تا آنجا که میدانیم، WAFL نخستین فازر هدایتشده با پوشش (Coverage-Guided Fuzzer) برای WebAssembly صرفاً باینری است که برای کار کردن به کد منبع نیاز ندارد.
کلیدواژهها: فازینگ (Fuzzing)، WebAssembly، AFL، صرفاً باینری (Binary-Only)
۱. مقدمه (INTRODUCTION)
وب از یک خواننده منفعل ابرمتن (Passive Hypertext Reader) به بستری برای برنامههای کاربردی سمتکلاینت (Client-Side Applications) بسیار تعاملی تکامل یافته است. اگرچه عملکرد جاوااسکریپت (JavaScript) در موتورهای مدرن در زمان اجرا (Just-in-Time یا JIT) به طور چشمگیری بهبود یافته است، اما اغلب چارچوبهایی که دارای کارایی بالا برای پردازش تصویر و بازیسازی میباشند توسط زبانهای برنامهنویسی بومی (Native Programming Languages) نوشته شدهاند. WebAssembly به منظور آوردن این برنامهها به وب و نزدیکتر کردن سرعت اجرای وظایف پیچیده به عملکرد بومی، معرفی شد [3].
WebAssembly یک استاندارد باینری باز میباشد که توسط مرورگرهای مدرن پذیرفته شده است، در کاربردهای متنوع دیگری نیز مورد استفاده قرار میگیرد و به عنوان یک هدف کامپایل (Compilation Target) برای بسیاری از زبانهای برنامهنویسی کامپایلشونده (Compiled Programming Languages) پشتیبانی میشود. چارچوبهای جدید مرورگر (browser frameworks) مانند Yew [8] و Blazor [13] حتی جاوااسکریپت (JavaScript) را بهطور کامل از فرایند توسعه وب کنار میگذارند. توسعهدهندگان میتوانند برنامههای وب را مستقیماً با زبانهایی مانند Rust و #C بنویسند و سپس این چارچوبها، زبان مربوطه را برای اجرا به WebAssembly هدفگذاری میکنند.
با یک گام فراتر بردن ایده قابلیت حمل (Portability)، استاندارد باز WASI [4] امکان اجرای برنامههای مستقل WebAssembly را فراهم میکند که حتی خارج از مرورگر نیز اجرا میشوند. هدف، ایجاد یک بستر باینری واقعاً جهانشمول است. زیرساخت پیرامون WASI هنوز جوان است، اما در حال رشد است؛ برای مثال، از طریق مدیر بسته WebAssembly (WebAssembly Package Manager یا wapm) [23]. با استفاده از wapm، کاربران میتوانند باینریهای WebAssembly را دانلود کنند که روی ماشینهای مجازی (VMs) مجهز به رابط سیستم WebAssembly (WebAssembly System Interface یا WASI) اجرا میشوند. برنامههایی که به صورت باینریهای WebAssembly توزیع میشوند، روی هر پلتفرمی که یک Runtime (محیط اجرا) برای آن در دسترس باشد، اجرا خواهند شد.
از آنجا که WebAssembly نیز مانند هر هدف کامپایل (Compilation Target) دیگری است، آسیبپذیریهای حافظه موجود در زبانهای مبدأ ناامن (Unsafe Source Languages)، مانند C، به WebAssembly منتقل میشوند و همچنان بصورت بالقوه آسیبپذیر باقی میمانند. اگرچه این بستر با در نظر گرفتن ملاحظات امنیتی توسعه یافته و از سازوکارهای مقابلهای مدرن (Modern Mitigations) پشتیبانی میکند، بااینحال ممکن است باگها همچنان قابل بهرهبرداری باشند و به اجرای کد (Code Execution) منجر شوند؛ همانطور که Lehman و همکاران نشان دادهاند [9]. تاکنون، ابزارهای موجود برای کشف خرابیهای حافظه (Memory Corruptions) در باینریهای WebAssembly محدود بودهاند.
در این مقاله، WAFL را معرفی میکنیم؛ یک فازر برای باینریهای WebAssembly با نرخ پردازش (Throughput) مناسب. WAFL برای تولید ورودی از فازر شناخته شده ++AFL و برای بهبود عملکرد از اسنپشاتهای سبک ماشین مجازی (Lightweight VM Snapshots) استفاده میکند. با ساخت WAFL بر پایه ماشین مجازی WebAssembly (WebAssembly Virtual Machine یا WAVM)، میتوانیم باینریهای WebAssembly را بدون دسترسی به کد منبع فاز کنیم. سرعت فازینگ حاصل را ارزیابی میکنیم و نشان میدهیم که WAFL به لطف سازوکار اسنپشاتگیری (Snapshotting Mechanism) خود، حتی از مهارگرهای بومی (Naive Harnesses) کامپایلشده از کد منبع برای معماری x86-64 نیز عملکرد بهتری دارد.
دستاوردها:
- ما WAFL را توسعه دادهایم؛ یک فازر متنباز (Open-Source) و صرفاً باینری (Binary-Only) برای WebAssembly.
- چندین بهبود را بر پایه یک پیادهسازی اولیه مبتنی بر wasm3 اعمال و عملکرد آنها را ارزیابی (Benchmark) میکنیم.
- در نسخه نهایی، این فازر مبتنی بر اسنپشات (Snapshot Fuzzer) و WAVM که از کامپایل پیش از اجرا (Ahead-of-Time یا AOT) استفاده میکند، حتی از مهارگرهای سنتی AFL برای کد بومی کامپایلشده نیز که از فراخوانی سیستمی (System Call) کُند fork استفاده میکنند، عملکرد بهتری دارد.
۲ پیشزمینه (BACKGROUND)
فازینگ (Fuzzing) یا آزمون فاز (Fuzz Testing)، یک تکنیک تحلیل پویا (Dynamic Analysis) است که ورودیهای تصادفی (random input) را به برنامهها میدهد و رفتار آنها را مشاهده میکند. AFL (American Fuzzy Lop) یک موتور فازینگ هدایتشده با پوشش (Coverage-Guided Fuzzing) و متنباز (Open-Source) است. این ابزار که در سال ۲۰۱۴ معرفی گردید، اکنون به یک ابزار استاندارد تبدیل شده است. پس از متوقف شدن روند توسعه آن در سال ۲۰۱۷، فورک (Fork) مبتنی بر جامعه آن، یعنی AFL++ [2]، به ادغام بهبودهای حاصل از پژوهشهای علمی بر پایه AFL ادامه داد.
۲.۱ اندازهگیری پوشش (Coverage Measurement)
هدف AFL افزایش سطح آزمون (Test Surface) تحت پوشش ورودیهای فازینگ است. پوشش بر اساس تعداد یالهای پیمایششده (visited edges) در گراف جریان کنترل (Control-Flow Graph یا CFG) یک برنامه اندازهگیری میشود. یک ورودی زمانی جالب (Interesting) در نظر گرفته میشود که در هنگام اجرا، پوشش جدیدی را در هدف ایجاد کند.
به منظور جمعآوری بازخورد، ابزارگذاری (AFL Instrumentation) AFL یک نقشه حافظه اشتراکی (Shared Memory Map) را در برنامه تحت آزمون تزریق میکند. در هنگام اجرا، ابزارگذاری به هر بلوک پایه (Basic Block) یک هش (Hash) یا شناسه یکتا (Unique ID) اختصاص میدهد و یک شمارنده را در محل متناظر در نقشه AFL افزایش میدهد. پس از پایان یک اجرا، فازر وجود ورودیهای جدید در نقشه را بررسی میکند. اگر در طول اجرا ورودیهای جدیدی یافت شده باشد، AFL آن ورودی را برای جهشهای بعدی (Subsequent Mutations) نگه میدارد [29].
برنامههایی که کد منبع آنها در دسترس است، با استفاده از یک لایه واسط یا پوشاننده (Wrapper) پیرامون کامپایلرهای موجود، یعنی afl-cc، ابزارگذاری (Instrument) میشوند. این ابزار، گذرگاههای کامپایلر موردنیاز را به gcc یا clang تزریق میکند. گذرگاه InsTrim [6] که در ++AFL (تا نسخه 3.12c++) گنجانده شده است، با تحلیل گراف جریان کنترل (Control-Flow Graph)، سرعت ابزارگذاری را بهبود میدهد. این گذرگاه تنها زیرمجموعهای از بلوکها را که برای متمایز ساختن مسیرها (Paths) ضروری هستند علامتگذاری میکند؛ بر اساس گفته نویسندگان، این مقدار حدود ۲۰٪ است.
یکی از معایب تکنیک هش کردن شناسه بلوک (Block ID Hashing) که در InsTrim و گذرگاه سنتی afl-clang استفاده میشود، این است که با افزایش تعداد بلوکهای ابزارگذاریشده، الگوریتمها احتمالاً با تصادم شاخص (Index Collision) مواجه میشوند. حتی رویکرد پیشرفتهتری توسط گذرگاه LLVM SanitizerCoverage [21] اتخاذ شده است. این گذرگاه به هر یال یک متغیر Guard اختصاص میدهد و یک تابع Callback (فراخوانی) را درج میکند که متغیر مذکور را بهعنوان پارامتر دریافت میکند. مقداردهی اولیه Guardها در یک تابع Callback دوم انجام میشود؛ بنابراین میتوان هر یک از آنها را به یک عدد یکتا اختصاص داد و در نتیجه، نیاز به هش کردن (Hash) برطرف میشود. ++AFL بهصورت پیشفرض از این گذرگاه استفاده میکند.
۲.۲ WebAssembly
برنامههای کاربردی مبتنی بر وب، که زبان برنامهنویسی اصلی آنها جاوااسکریپت (JavaScript) است، روزبهروز محبوبتر و پیچیدهتر میشوند. با انتشار Emscripten در سال ۲۰۱۱، راهکاری برای کامپایل زبانهای سطح بالایی مانند C به JavaScript و استفاده از آنها در وب فراهم شد [28]. امکان انتقال کد بومی (Native Code) به وب نیز به کمک آن ایجاد گردید.
بااینحال، جاوااسکریپت هدف مناسبی برای کامپایل نیست. این زبان فاقد یک نمایش فشرده (Compact Representation) است. همچنین، از آنجا که جاوااسکریپت یک زبان اسکریپتی سطح بالا (High-Level Scripting Language) محسوب میشود، تمام کدها باید در زمان اجرا یا تفسیر شوند یا با کامپایل در زمان اجرا (Just-in-Time یا JIT) کامپایل گردند؛ در نتیجه، عملکرد ضعیفتر و زمان راهاندازی بیشتری به همراه دارد.
WebAssembly با هدف فراهم کردن یک هدف کامپایل (Compilation Target) مناسب برای برنامههای بومی (Native Applications) در وب معرفی شد [3]. این فناوری یک بایتکد سطح پایین (Low-Level Bytecode) برای یک ماشین پشتهای (Stack Machine) است که معمولاً در مرورگر وب با استفاده از کامپایل در زمان اجرا (Just-in-Time یا JIT) یا کامپایل پیش از اجرا (Ahead-of-Time یا AOT) به کد ماشین کامپایل میشود و به هدف کامپایل پیشفرض جدید Emscripten تبدیل شده است.
علاوه بر برنامههای وب که برای برونسپاری وظایف حساس از نظر عملکرد (Performance-Critical Tasks)، از طریق جاوااسکریپت کد WebAssembly را فراخوانی میکنند، Emscripten میتواند باینریهای مستقل (Standalone Binaries) نیز ایجاد کند. برای این منظور، یک رابط باینری برنامه (Application Binary Interface یا ABI) تعریف میکند که محیطهای اجرای WebAssembly میتوانند آن را پیادهسازی کنند و همچنین یک کتابخانه استاندارد C را که بر اساس آن کامپایل شده است، دربر میگیرد. WASI در تلاش برای استانداردسازی این ABI، ایجاد شد [4]. اگرچه این ABIها از نظر رویکرد مشابه یکدیگر هستند، اما کاملاً با هم سازگار نیستند. یک Backend مربوط به WebAssembly در ماشین مجازی سطح پایین (Low-Level Virtual Machine یا LLVM) گنجانده شده است و میتوان از آن در کنار wasi-libc [26] برای کامپایل C (و تا حدی ++C) به WASI استفاده کرد. Rust نیز از WASI بهعنوان یک هدف سطح ۲ پشتیبانی میکند.
۲.۳ محیطهای اجرای WebAssembly(WebAssembly Runtimes)
در اواسط سال ۲۰۲۱، بیشتر محیطهای اجرای WebAssembly هنوز در مراحل اولیه توسعه قرار داشتند یا از مشخصات کامل WASI پشتیبانی نمیکردند [1]. این پژوهش بر دو مورد از محیطهای اجرای WebAssembly با قابلیتهای کاملتر تمرکز دارد: WAVM و wasm3. مورد دوم، یک محیط اجرای سبک است که با C نوشته شده و زمان راهاندازی سریع و ردپای حافظه (Memory Footprint) کوچکی دارد. این محیط اجرا بر پایه یک طراحی بهینهشده از مفسر فراخوانی انتهایی (Tail-Call Interpreter) با نام M3 [10] ساخته شده است.
WAVM با ++C نوشته شده و از موتور JIT مربوط به LLVM برای کامپایل باینری WebAssembly به کد بومی (Native Code) بهصورت AOT استفاده میکند. هنگام ترجمه کد WebAssembly به یک باینری بومی پیش از اجرا، کد را به کتابخانه محیط اجرای WAVM پیوند میدهد؛ این کتابخانه، توابع داخلی WebAssembly و WASI را فراهم میکند. در نتیجه، WAVM در ازای عملکرد بالاتر پس از مقداردهی اولیه (Initialization)، زمان تأخیر راهاندازی (Startup Latency) به مراتب بیشتری را متحمل میشود.
یک ارزیابی عملکرد (Benchmark) که توسط Denis [1] انجام شده است، سرعت اجرای هشت محیط اجرای مستقل محبوب WebAssembly را با استفاده از کتابخانه رمزنگاری libsodium بررسی میکند. محیطهای اجرا مبتنی بر LLVM (AOT) بهترین نتایج را به دست میآورند؛ در این میان، WAVM با سرعتی ۱۵٪ بیشتر از گزینه بعدی، یعنی wasmer [24]، اجرا میشود و سرعت آن تنها ۲ برابر کمتر از کد بومی است. اگر حوزه آزمایش را به مفسرها محدود کنیم، wasm3 بهترین نتیجه را به دست میآورد، هرچند در مقایسه با کد بومی، سرعت آن ۳۰ برابر کمتر است.
۳. کارهای مرتبط (RELATED WORK)
اگرچه WebAssembly هنوز یک فناوری نسبتاً خاص و محدود (Niche) محسوب میشود، در این بخش برخی از پژوهشهای انجام شده در این حوزه را مرور میکنیم. در بخش دوم این قسمت، بحث درباره اسنپشاتهای ماشین مجازی (VM Snapshots) را با جزئیات بیشتری ادامه خواهیم داد.
۳.۱ تحلیل WebAssembly (WebAssembly Analysis)
Metzman [11] فازینگ مبتنی بر کد منبع را در مرورگر، بر پایه libFuzzer و با کامپایل توسط Emscripten، ارائه کرد. پس از ساخت یک باینری WebAssembly ابزارگذاری شده (Instrumented WebAssembly Binary) از روی کد منبع، فازر حاصل میتواند روی هر پلتفرم WebAssembly اجرا شده و فازینگ را انجام دهد. مهارگرهای هدف (Target Harnesses) مربوط به brotli، lzma و sqlite از OSS-Fuzz اقتباس شده بودند. نکته قابل توجه اینکه Metzman و همکاران همچنین چارچوبی برای ارزیابی فازرها ایجاد کردند که نمونههایی مشابه نمونههای OSS-Fuzz دارد و Fuzzbench [12] نامیده میشود.
Fuzzcoin [7]، یک شبکه محاسبات جمعی (Crowd Computing Network) برای فازینگ، همین ایده فازر مبتنی بر مرورگر برای WebAssembly را به ۳۰ پروژه OSS-Fuzz گسترش میدهد. یک رابط وب کاربرپسند، بازدیدکنندگان را دعوت میکند تا در ازای دریافت سکههای مجازی (Virtual Coins)، زمان پردازنده (CPU Time) خود را اهدا کنند. برخلاف فازینگِ فایلهای باینری WebAssembly، وات (Watt) و همچنین پرنی (Perényi) و میدتگارد (Midtgaard) روشهایی را برای اعتبارسنجی ماشینهای مجازی (VM) WebAssembly با استفاده از فازینگ پیشنهاد میکنند. در این روش، باینریها به صورت تصادفی بهعنوان ورودی به محیط اجرای تحت آزمون داده میشوند؛ این باینریها یا با ترجمه از C [25] تولید میشوند یا مستقیماً بهصورت WebAssembly ایجاد میشوند [15]. محیطهای اجرا نیز بهطور منظم فاز میشوند: wasm3 خود یک پروژه OSS-Fuzz است و WAVM چندین هدف (Target) برای libFuzzer فراهم میکند.
Lehmann و همکاران [9] نشان میدهند که باینریهای WebAssembly فاقد بسیاری از سازوکارهای مقابلهای (Mitigations) رایج در باینریهای سنتی هستند. آنها نشان میدهند که آسیبپذیریهای موجود در کدهای ناامن از نظر ایمنی حافظه (Memory-Unsafe Code) میتوانند به WebAssembly منتقل شوند و در صورت ترکیب در قالب زنجیرههای بهرهبرداری (Exploit Chains)، امکان به خطر انداختن سیستم میزبان را فراهم کنند.
در ادامه کار آنها، Hilbig و همکاران [5] یک مجموعهداده واقعی (Real-World Dataset) از باینریهای WebAssembly گردآوری کرده و آن را از نظر وجود آسیبپذیریها بررسی میکنند. آنها دریافتند که بسیاری از باینریها از پشته مدیریت نشده (Unmanaged Stack)، تخصیص دهندههای حافظه سفارشی (Custom Allocators) یا APIهای خطرناک (Dangerous APIs) استفاده میکنند؛ بهترتیب در ۶۵٪، ۳۸٫۶٪ و ۲۱٫۲٪ از باینریها. این موارد همان نقاط ضعف بالقوهای هستند که Lehmann و همکاران شناسایی کردهاند.
۳.۲ فازینگ مبتنی بر اسنپشات (Snapshot Fuzzing)
ایده اسنپشاتگیری (Snapshotting) از فضای حافظه یک برنامه کاربردی یا ماشین مجازی (VM) در فازینگ، ایده جدیدی نیست. حتی یک مهارگر (Harness) ساده در AFL که از فراخوانی سیستمی fork استفاده میکند، از نظر فنی برای فازینگ از اسنپشاتها استفاده میکند. البته، در طول سالها، سازوکارهای پیشرفتهتری برای اسنپشاتگیری پیشنهاد شدهاند. Newsham و Jesse [14] با ایجاد یک fork از یک نمونه QEMU در حالت سیستمی (System Mode)، فورکهای سنتی AFL را برای فازینگ کل سیستم (Full-System Fuzzing) تطبیق دادهاند. پس از آشکار شدن مقیاسپذیری و سرعت نامطلوب فراخوانی سیستمی fork، Xu و همکاران [27] یک فراخوانی سیستمی پیشنهاد میکنند که برای فازینگ مناسبتر است.
بهجای اسنپشاتگیری از یک فرایند فضای کاربر (Userspace Process)، ماشینهای مجازی (VMs) میتوانند از وضعیت کامل مهمان (Complete Guest State) اسنپشات بگیرند تا امکان بازنشانی سریع (Fast Reset) یک سیستم کامل فراهم شود. بهطور مشخص، Agamotto که توسط Dokyung و همکاران [20] ارائه شده است، یک سازوکار سریع اسنپشاتگیری ماشین مجازی را به QEMU KVM اضافه میکند تا سپس برای فاز کردن درایورهای هسته (Kernel Drivers) مورد استفاده قرار گیرد. بهطور مشابه، Nyx که توسط Schumilo و همکاران [18] ارائه شده است، از اسنپشاتهای سریع ماشین مجازی برای فاز کردن اهداف هسته و حتی اهداف فضای کاربر (Userspace Targets) با نرخ پردازش بالا (High Throughput) استفاده میکند.
۴. طراحی (DESIGN)
هدف کلی WAFL این است که امکان فازینگ هدایت شده با پوشش (Coverage-Guided Fuzzing) را برای برنامههای WebAssembly صرفاً باینری (Binary-Only) با استفاده از ++AFL فراهم کند و با بهرهگیری از اسنپشاتهای سبک (Lightweight Snapshots)، امکان بازنشانی سریع ماشین مجازی (VM Reset) پس از هر اجرا را فراهم سازد. همانطور که در بخش ۲.۲ اشاره شد، باینریهای WebAssembly میتوانند ABIهای مختلفی را هدف قرار دهند. در این پژوهش، تمرکز ما بر باینریهای مستقل (Standalone Binaries) است که WASI را هدف قرار میدهند و تا حدی نیز بر Emscripten تمرکز داریم.
۴.۱ WAFL-wasm3
در طول این مقاله، روشهای مختلفی برای پیادهسازی فازینگ WebAssembly را بررسی خواهیم کرد. ما یک پیادهسازی پایه و ساده از WAFL را بر مبنای wasm3، که یک ماشین مجازی (VM) کاملاً تفسیری است، ایجاد کردیم. در این ماشین مجازی WebAssembly مبتنی بر مفسر، سازوکار بازخورد (Feedback Mechanism) مربوط به AFL را مستقیماً در محیط اجرای wasm3 قرار دادیم. مفسر وصله شده WAFL-wasm3 در هنگام اجرا، کد ابزارگذاری (Instrumentation Code) را در هر دستور کنترلی WebAssembly (WebAssembly Control Instruction) اجرا میکند [16].
این فرایند در مؤلفه m3_exec انجام میشود. هنگام اجرای یک دستور کنترلی، آدرس هدف (Target Address) ــ یعنی آدرس بلوک بعدی که قرار است اجرا شود ــ را ثبت کرده و آن را به یک تابع بازخورد (Feedback Function) ارسال میکنیم. این تابع، یک هش (Hash) را از آدرس دریافت شده و آدرسی که پیشتر مشاهده کرده است محاسبه میکند. این هشها (Hash) متناظر با یالهای (Edges) موجود در گراف جریان کنترل (Control-Flow Graph) برنامه هستند. سپس بیتهای کمارزش (Least Significant Bits) آنها به عنوان اندیس (Index) در نقشه اشتراکی AFL (AFL Shared Map) استفاده میشوند و فیلد متناظر افزایش مییابد. هنگام راهاندازی محیط اجرا (Runtime Startup)، لازم است یک روال راهاندازی (Setup Routine) اجرا شود که این ناحیه حافظه اشتراکی ارائهشده توسط ++AFL را در فضای آدرس محیط اجرا (Runtime Address Space) نگاشت (Map) کند.
دومین مؤلفه اصلی، Forkserver است [2]؛ مؤلفهای که ++AFL معمولاً آن را در باینریهای تحت آزمون قرار میدهد. پیش از آنکه برنامه اصلی واقعاً شروع شود، این مؤلفه با فرایند والد (Parent Process) یک دستدهی (Handshake) انجام میدهد و وارد یک حلقه فورک (Forking Loop) میشود. فرایندهای فرزند (Child Processes) از این حلقه خارج میشوند، اجرای عادی خود را در برنامه تحت آزمون ادامه میدهند و سپس خاتمه مییابند.
قرار دادن این مؤلفه تا حد امکان در مراحل پایانی راهاندازی محیط اجرا (Runtime Startup)، فرصتی برای بهبود سرعت فراهم میکند؛ زیرا در این حالت، فرایندهای فرزند میتوانند مرحله مقداردهی اولیه (Initialization Phase) را رد کنند. اگر خطایی در Runtime رخ دهد، فرایند فرزند متوقف میشود و Forkserver وقوع کرش (Crash) را به ++AFL اطلاع میدهد. با اعمال این تغییرات (و غیرفعال کردن بررسی باینری)، ++AFL میتواند باینریهای WebAssembly را از طریق wasm3 فاز کند. بااینحال، از نظر عملکرد، این روش هنوز فاصله زیادی با وضعیت مطلوب دارد.
۴.۲ WAVM-WAFL
با توجه به تفاوت چشمگیر عملکرد میان محیطهای اجرای WebAssembly (بخش ۲.۳)، نمیتوان انتظار داشت راهکارهای فازینگ مبتنی بر یک مفسر، مانند wasm3، بالاترین سرعت اجرای ممکن را ارائه دهند. بنابراین، نسخه اصلی WAFL را بر پایه یک محیط اجرای سریع با قابلیت کامپایل پیش از اجرا (AOT) پیادهسازی کردیم. ما WAVM را انتخاب کردیم که بر اساس نتایج ارزیابیهای عملکرد (Benchmarks) [1]، عملکرد مناسبی دارد. بااینحال، این محیط اجرا به رویکرد متفاوتی برای ابزارگذاری (Instrumentation) نیاز دارد.
WAVM هنگام بارگذاری یک فایل WebAssembly، با استفاده از LLVM-JIT، باینری را به کد بومیِ پلتفرم (Platform-Native Code) کامپایل میکند و گذرگاههای بهینهسازی (Optimization Passes) را روی کد تولید شده اجرا میکند. این موضوع از آن جهت مناسب است که ++AFL از گذرگاههای بهینهساز LLVM برای درج کد ابزارگذاری خود استفاده میکند. در نتیجه، نخستین گام، پیوند دادن گذرگاه کلاسیک LLVM مربوط به AFL به WAVM بود؛ کاری که به یک تغییر یکخطی در گذرگاه نیاز داشت و سپس تنظیم آن برای اجرا بهعنوان آخرین گذرگاه انجام شد. کد راهاندازی نقشه اشتراکی (Shared Map) و Forkserver نیز مانند WAFL-wasm3 است.
۴.۳ اسنپشاتهای سبک ماشین مجازی و بازنشانی (Lightweight VM Snapshots and Resets)
در اصطلاحات ++AFL، فازینگ در حالت پایدار (Persistent Mode) [2] به معنای استفاده مجدد از یک فرایند فرزند برای چندین تکرار است. این حالت امکان جایگزین کردن فراخوانیهای سیستمی زمانبر ()fork را با اجرای یک حلقه روی نواحی کد مرتبط در فرایند فرزند فراهم میکند. حالت پایدار با کاربرد ما سازگاری خوبی دارد، زیرا کد موردنظر (Target) همان کد از پیش کامپایلشده است. بااینحال، یک هدف ممکن است در طول اجرا وضعیت داخلی (State) جمع کند یا حتی دچار نشت حافظه (Memory Leak) شود و در نتیجه، فازینگ در حالت پایدار ناپایدار شود. بنابراین، اگر بخواهیم بدون استفاده از fork فازینگ کنیم، باید وضعیت هدف را پس از هر اجرا بازنشانی کنیم.
در حالت ایدهآل، میخواهیم این کار را بدون وصلهکردن باینری WebAssembly یا ابزارگذاری بیشتر روی آن انجام دهیم، بهویژه در سناریویی که فقط باینری در دسترس است. WebAssembly سه نوع شیء دارای وضعیت (Stateful Objects) را تعریف میکند که ممکن است توسط برنامه هدف تغییر داده شوند: متغیرهای سراسری (Globals)، جدولها (Tables) و حافظهها (Memories) [3]. در حال حاضر، کامپایلرها تنها از یک حافظه استفاده میکنند که آرایهای از بایتهاست. آنها در این حافظه یک چیدمان آشنا شامل بخشهای پشته (Stack)، Heap و داده (Data) ایجاد میکنند [9].
بر اساس این مشاهده، میتوانیم اسنپشاتگیری و بازیابی (Restore) ماشین مجازی را پیادهسازی کنیم: محیط اجرا را اندکی پیش از نخستین فراخوانی به کد هدف متوقف میکنیم. در این نقطه، حافظه خطی (Linear Memory) پیشتر توسط محیط اجرا مقداردهی اولیه شده است. سپس یک اسنپشات از محتوا و اندازه آن ایجاد میکنیم. هنگامی که پس از هر تکرار حلقه، جریان کنترل به محیط اجرا بازمیگردد، حافظه را به اندازه اولیه آن کاهش میدهیم و اسنپشات را دوباره در آن مینویسیم. استفاده از این موتور اسنپشات مینیمال، سربار قابلتوجه ناشی از مقداردهی اولیه مجدد محیط اجرا را حذف میکند و در نتیجه، انتظار میرود افزایش متناظری در عملکرد حاصل شود.
۴.۴ ابزارگذاری بهبودیافته (Improved Instrumentation)
++AFL علاوه بر گذرگاه کلاسیک (Classic Pass)، گزینههای متعددی برای ابزارگذاری (Instrumentation) ارائه میدهد. یکی از بهبودهای اولیه با نام InsTrim [6]، گراف جریان کنترل (Control-Flow Graph) برنامه را تحلیل میکند تا تعداد نقاط ابزارگذاری (Instrumentation Points) را کاهش دهد و در نتیجه، عملکرد فازینگ را افزایش دهد.
یکپارچهسازی InsTrim با WAVM به یک تغییر کوچک در Makefile مربوط به ++AFL و همان وصله یکخطی موردنیاز برای گذرگاه کلاسیک نیاز داشت. ++AFL بهعنوان گزینههای دیگری برای تکنیکهای ابزارگذاری مورد بحث، پوشش شاخهای حساس به زمینه (Context-Sensitive Branch Coverage) و پوشش شاخهای N-Gram را که در [22] ارائه شدهاند، در اختیار قرار میدهد. هر دو روش، هنگام برخورد (Hit) با یک نقطه ابزارگذاری، نحوه محاسبه اندیسهای نقشه اشتراکی (Shared Map Indices) را تغییر میدهند. روش نخست، زمینه فراخوانی (Calling Context) را که بهصورت یک پشته فراخوانی سادهشده (Simplified Call Stack) نمایش داده میشود، با استفاده از عملگر XOR با شناسه شاخه (Branch ID) معمول ترکیب میکند. روش دوم، آخرین N شناسه شاخه را در یک Tuple ذخیره کرده و آنها را با یکدیگر هش (Hash) میکند؛ این کار با استفاده از XOR و جابهجایی بیتها (Bit-Shifting) انجام میشود تا یک اندیس محاسبه شود. برای جابهجایی میان تمام این گزینهها، میتوان یک متغیر محیطی (Environment Variable) با نام AFL_LLVM_INSTRUMENT را برای WAVM، مشابه afl-clang-fast، تنظیم کرد.
در حال حاضر، ابزارگذاری پیشفرض ++AFL بر پایه SanitizerCoverage [21]، یعنی ابزارگذاری پوشش کد (Code Coverage Instrumentation) مربوط به LLVM، است. در SanitizerCoverage، از یک آدرس برای محاسبه یک اندیس یکتا (Unique Index) در نقشه اشتراکی استفاده میشود:
idx = ((&offset - &guard) >> 2) & (MAP_SIZE - 1)
ابزارگذاری برای محاسبه اندیسهای قابل بازتولید (Reproducible Indices) در اجراهای مختلف، یک اشارهگر آفست (Offset) را از آدرس کم میکند؛ این کار با وجود نگاشت مجدد تصادفی متغیرهای Guard انجام میشود. از آنجا که تمام متغیرهای Guard اعداد صحیح بدون علامت ۳۲ بیتی (32-bit Unsigned Integers) هستند و بهصورت پیوسته در حافظه قرار گرفتهاند، با انتقال آدرس آنها بهاندازه دو بیت به سمت راست (Right Shift)، اندیسهای متوالی به دست میآیند. در نهایت، عملیات Masking تضمین میکند که تمام اندیسها در بازه [0, MAP_SIZE − 1] قرار داشته باشند؛ بنابراین، تا زمانی که MAP_SIZE > #Guards باشد، با یکدیگر برخورد (Collision) نخواهند داشت.
ما با مشکلی در پیادهسازی SanitizerCoverage در ++AFL مواجه شدیم که مانع از کارکرد آن در WAFL میشد. کاربر (User) باید پیادهسازی دو تابع Callback را ارائه کند. بااینحال، در WAFL، Callback نخست هرگز فراخوانی نمیشود و در نتیجه، Guardها (که اندیسهایی در نقشه اشتراکی (Shared Map) هستند) مقداردهی اولیه نمیشوند. در عوض، WAFL اکنون مستقیماً به پیادهسازی SanitizerCoverage خود LLVM متکی است.
۴.۵ فازینگ با حافظه اشتراکی (Shared Memory Fuzzing)
AFL معمولاً موارد آزمون (Testcases) را از طریق سیستم فایل به برنامههای هدف منتقل میکند که این کار سربار (Overhead) ایجاد میکند. در یک روش، AFL نام فایل ورودی را بهعنوان یک آرگومان به برنامه میدهد؛ در روش دیگر، خود فایل را باز میکند و محتوای آن را از طریق ورودی استاندارد (Standard Input) به برنامه هدف ارسال میکند. هنگام فازینگ در حالت پایدار (Persistent Mode)، ++AFL میتواند بهجای این روش، ورودی را از طریق یک بافر حافظه اشتراکی (Shared Memory Buffer) مشابه نقشه اشتراکی (Shared Map) مورد استفاده برای شمارش دفعات برخورد (Hit Counts) منتقل کند.
با فرض اینکه بیشتر اهداف بتوانند ورودی را از ورودی استاندارد بخوانند، هدف این بود که خواندن از ورودی استاندارد را از سیستم فایل مستقل کنیم. برای این منظور، یک بررسی به پیادهسازی محیط اجرای WAVM از فراخوانی سیستمی ()readv در WASI اضافه کردیم: اگر شماره فایل (File Number) متناظر با ورودی استاندارد باشد، یک روال سفارشی (Custom Routine) بهجای آن دادهها را از بافر اشتراکی میخواند. در هر تکرار، حلقه پایدار (Persistent Loop) با استفاده از ()fmemopen بافر را با طول صحیح دوباره باز میکند.
۴.۶ مسدودسازی ابزارگذاری libc برای افزایش سرعت (Blocking libc Instrumentation for Speed)
معمولاً در WebAssembly پیونددهی پویا (Dynamic Linking) وجود ندارد. همهچیز در یک فایل با پیونددهی ایستا (Statically Linked) و بهصورت یکپارچه قرار میگیرد. برنامههای WebAssembly معمولاً با زبانهای سطح بالایی مانند C یا Rust نوشته میشوند. این زبانها مجموعهای از توابع استاندارد را فراهم میکنند که باید در باینری گنجانده شوند؛ بنابراین، این توابع نیز توسط WAFL ابزارگذاری (Instrument) میشوند. با توجه به این موضوع، در یک سناریوی فازینگ جعبهسفید (White-Box Fuzzing)، احتمالاً کد کتابخانه ابزارگذاری نمیشود؛ بنابراین، توابع متناظر در باینریهای WebAssembly نیز باید از ابزارگذاری مستثنا شوند. برای این منظور، از قابلیت فهرست مسدودسازی (Blocklist) در ++AFL استفاده میکنیم. برای پشتیبانی WAFL از این قابلیت، تنها لازم بود تغییر کوچکی در WAVM ایجاد کنیم: نام توابع معمولاً از فایل ورودی به کد تولیدشده منتقل نمیشوند. پس از افزودن این قابلیت، توانستیم نام توابع را در گذرگاههای LLVM (LLVM Passes) بخوانیم. ما فهرستی از نمادهای (Symbols) موجود در کتابخانه C مربوط به WASI [26]² را با قالب LLVM تطبیق دادیم؛ به این صورت که پیشوند fun: را به ابتدای هر نام اضافه کردیم. علاوه بر این، دو نام تابع printf_core و pop_arg را نیز اضافه کردیم که در باینریهای WASI بهطور معمول ظاهر میشوند.
با استفاده از این فهرست مسدودسازی، تعداد بلوکهای کدی که ابزارگذاری میشوند بهطور قابلتوجهی کاهش مییابد؛ همانطور که در شکل ۲ نشان داده شده است. در اهداف کوچک، مانند brotli و lzma2dec، میتوان تعداد یالهای ابزارگذاریشده (Instrumented Edges) را تا ۶۳٪ کاهش داد (برای lzma2dec با گذرگاه کلاسیک این مقدار از ۱۶۹۷ به ۶۲۸ کاهش مییابد).
برای گذرگاه LLVM، یک بررسی کوچک به WAVM اضافه کردیم که متغیرهای محیطی AFL_LLVM_{ALLOW,DENY}LIST را میخواند و در صورتی که LLVM از این قابلیت پشتیبانی کند (نسخه ۱۱ و بالاتر)، نام فایلهای مشخصشده را به LLVM منتقل میکند.
۵. ارزیابی (EVALUATION)
در ادامه، مراحل توسعه WAFL که در بخش ۴ مورد بحث قرار گرفتند را ارزیابی میکنیم. علاوه بر این، WAFL را با ابزارگذاری بومی (Native Instrumentation) و همچنین با یک باینری کامپایلشده با Backend مربوط به LLVM در ++AFL، با استفاده از همان مهارگر (Harness) ساده، مقایسه میکنیم.
همانطور که در بخش ۳ بیان شد، کارهای پیشین چندانی برای مقایسه با WAFL وجود ندارد؛ بهجز راهکار درونمرورگری (In-Browser) ارائهشده توسط Metzman [11]. متأسفانه، باینریهای آنها در WAVM یا wasm3 اجرا نمیشوند. بنابراین، این باینریها از روی کد منبع و با استفاده از یک اجراکننده مستقل سفارشی (Custom Standalone Runner) کامپایل شدند که یک بافر را از ورودی استاندارد (Standard Input) میخواند و آن را به ()LLVMFuzzerTestOneInput ارسال میکند. این روش برای brotli و lzma، که دو کتابخانه فشردهسازی هستند، بهخوبی عمل کرد؛ اما برای sqlite با شکست مواجه شد. علاوه بر این، در این ارزیابی libsndfile، یک کتابخانه صوتی، را نیز در نظر میگیریم و یک آزمون موردی (Ad-Hoc Test) را برای باینریهای WebAssembly صرفاً باینری (Binary-Only WebAssembly Blobs) توصیف میکنیم.
۵.۱ تنظیمات آزمون (Test Setup)
بستر مورد استفاده، یک سرور مجهز به دو پردازنده ۱۶ هستهای AMD EPYC 7281 و ۱۱۲ گیگابایت حافظه RAM بود که Ubuntu 20.04 با Linux 5.4 روی آن اجرا میشد. تمام آزمونها بهصورت موازی و با استفاده از LLVM / Clang نسخه ۱۲ و AFL++ 3.15a اجرا شدند. اهداف فازینگ و مهارگرهای (Harnesses) مورد استفاده را از OSS-Fuzz [19] دریافت کردیم. پس از کامپایل آنها مطابق با اسکریپتهای ساخت (Build Scripts) ارائه شده، آنها را به یک اجراکننده مستقل سفارشی (Custom Standalone Runner) پیوند دادیم که دادهها را از ورودی استاندارد (Standard Input) میخواند.
برای باینری بومی (Native Binary)، از afl-clang-fast استفاده کردیم. باینریهای WASI را با استفاده از Clang، همراه با Backend مربوط به wasm32-wasi و یک Sysroot از wasi-libc 12 [26] کامپایل کردیم. همچنین، از آنجا که WASI از نخها (Thread) پشتیبانی نمیکند، لازم بود گزینه -mthread-model single نیز به کامپایلر داده شود. عملکرد WAFL را در پیکربندیهای مختلفی که در جدول ۱ فهرست شدهاند، ارزیابی کردیم. هر پیکربندی را به مدت ۲۴ ساعت اجرا کردیم و نتایج را از میان پنج اجرا میانگین گرفتیم تا مشخص شود هدف چند بار در ثانیه اجرا میشود و فازر چند شاخه (Branch) را کشف میکند.
برای مقایسه، فازرهای مربوط به brotli و lzma2dec که در [11] ایجاد شده بودند، طی یک اجرای فازینگ چندساعته در Firefox 91 اجرا شدند. بیشینه سرعت اجرای گزارششده در طول آن بازه زمانی را در اینجا استفاده میکنیم تا برآوردی خوشبینانه از عملکرد راهکار درونمرورگری (In-Browser) به دست آوریم. بااینحال، این مقدار ممکن است همچنان پتانسیل کامل راهکار را کمتر از واقعیت برآورد کند؛ زیرا Metzman یک گونه بهینهشده با نام sqlite-fast ارائه میکند که از Canvas مرورگر استفاده نمیکند و عملکرد آن بهطور قابلتوجهی، حدود ۱۰ برابر، از sqlite بهتر است. برای brotli و lzma2dec، باینری سریع مشابهی در دسترس نیست.
۵.۲ مهارگر Brotli(Brotli Harness)
کتابخانه brotli یک هدف فازینگ (Fuzz Target) برای Decoder خود فراهم میکند؛ باینری WebAssembly تولیدشده ۲۰۱ کیلوبایت حجم دارد. بهعنوان ورودی برای ++AFL، از Corpus اولیه فازر (Fuzzer Seed Corpus) موجود در پروژه استفاده شد. نتایج را میتوان در شکل ۳ مشاهده کرد. در حالی که پیکربندی WAVM بدون حالت پایدار (Non-Persistent) با سرعت ۱۷۴ اجرا در ثانیه (exec/s)، بهمراتب عملکرد ضعیفتری نسبت به باینری بومی با سرعت ۱۸۷۸ اجرا در ثانیه دارد، فعال کردن حالت پایدار (Persistent Mode) این وضعیت را تغییر میدهد.
در این حالت، هر سه گزینه ابزارگذاری (Instrumentation) یعنی Classic، InsTrim و SanitizerCoverage، سریعتر از باینری بومی اجرا میشوند؛ بهترتیب با سرعتهای ۲۵۳۲، ۵۰۰۸ و ۲۷۵۳ اجرا در ثانیه. علاوه بر این، فعال کردن فازینگ با حافظه اشتراکی (Shared Memory Fuzzing)، عملکرد Classic را به ۳۳۲۸ و SanitizerCoverage را به ۳۳۷۶ اجرا در ثانیه افزایش میدهد؛ اما برای InsTrim عملکرد اندکی کاهش مییابد و به ۴۷۱۵ اجرا در ثانیه میرسد. در مرحله نهایی، یعنی با فعال کردن فازینگ با حافظه اشتراکی و مسدودسازی توابع کتابخانه C (Blocklisting C Library Functions)، عملکرد ابزارگذاری مبتنی بر SanitizerCoverage به کمتر از نسخهای که تنها حالت پایدار در آن فعال بود کاهش مییابد و به ۲۶۴۴ اجرا در ثانیه میرسد. بااینحال، دو پیکربندی دیگر به بهترین مقادیر عملکرد خود دست پیدا میکنند: کلاسیک (Classic) با ۴۰۷۷ اجرا در ثانیه و InsTrim با ۷۴۴۳ اجرا در ثانیه. پیکربندی دوم حتی از باینری بومی بهینهشده (Optimized Native Binary) با سرعت ۶۹۱۷ اجرا در ثانیه نیز عملکرد بهتری دارد. فازر libFuzzer مبتنی بر مرورگر (Browser-Based libFuzzer) نیز حداکثر به سرعت ۱۰۸۱ اجرا در ثانیه دست یافت.
۵.۳ مهارگر LZMA (Lzma Harness)
پروژه lzma در OSS-Fuzz شامل اهداف فازینگ (Fuzz Targets) برای الگوریتمهای فشردهسازی پرکاربردی مانند 7z، lzma، lzma2 و xz است. اندازه باینریها مشابه یکدیگر است و برای ارزیابی، فازر Decode مربوط به lzma2 با اندازه ۹۶ کیلوبایت، بهصورت تصادفی انتخاب شد. فایلهای بذر (Seed) در خود پروژه ارائه شدهاند؛ بنابراین، از مجموعه ورودی بذر (Corpus) مربوط به lzma2dec استفاده شد. بیشترین سرعتها در هر سه پیکربندی با ابزارگذاری مبتنی بر InsTrim به دست میآیند: با اسنپشاتها و حافظه اشتراکی (Snapshots and Shared Memory)، سرعت ۹۶۵۵ اجرا در ثانیه (exec/s) به دست آمد؛ پس از آن، پیکربندی مشابه همراه با فهرستهای مسدودسازی (Blocklists) با ۹۱۷۱ اجرا در ثانیه و پیکربندی دارای تنها اسنپشاتها با ۸۸۵۹ اجرا در ثانیه قرار میگیرند.
ابزارگذاری کلاسیک AFL با فعال بودن فهرستهای مسدودسازی بهترین عملکرد را دارد و به ۷۰۵۳ اجرا در ثانیه میرسد؛ این مقدار از پیکربندیهای بدون فهرست مسدودسازی (۶۳۷۲ اجرا در ثانیه) و پیکربندی دارای تنها اسنپشاتها (۴۹۰۲ اجرا در ثانیه) بیشتر است. SanitizerCoverage نیز رفتار مشابهی دارد و با فعال بودن تمام گزینهها، بهترین عملکرد را نشان میدهد (۶۳۳۹ اجرا در ثانیه). بدون فهرستهای مسدودسازی، عملکرد اندکی ضعیفتر است (۵۹۸۸ اجرا در ثانیه) و بدون ورودی حافظه اشتراکی (Shared Memory Input)، سرعت بهطور محسوسی کاهش مییابد و به ۴۵۷۱ اجرا در ثانیه میرسد.
این اعداد بین عملکرد هدف بومیِ بهینهنشده ما (۱۷۸۰ اجرا در ثانیه) و هدف بهینهشدهای قرار میگیرند که با درایور libFuzzer مربوط به ++AFL کامپایل شده و حالت پایدار و فازینگ با حافظه اشتراکی را نیز شامل میشود (۹۱۸۶ اجرا در ثانیه). بار دیگر، بهترین پیکربندی حتی از نسخه بومی بهینهشده نیز عملکرد بهتری دارد. حداکثر سرعتی که libFuzzer در مرورگر به آن دست یافت، ۱۰۷۶ اجرا در ثانیه بود.
۵.۴ مهارگر Libsndfile (Libsndfile Harness)
Sndfile یک کتابخانه برای تبدیل فایلها و قالبهای صوتی است. این کتابخانه بهطور قابلتوجهی بزرگتر از brotli است (اندازه آن در قالب WebAssembly برابر با ۱٫۲ مگابایت است) و به دلیل سربار کامپایل پیش از اجرا (AOT)، لازم بود مهلت زمانی (Timeout) مقداردهی اولیه Forkserver در ++AFL افزایش یابد. از آنجا که یک مجموعه ورودی (corpus) مناسب از فایلهای بذر (Seed) در اختیار نداشتیم، فایل testfile.mp3 از مخزن liblame بهعنوان ورودی ++AFL استفاده شد. زمان اجرای پیکربندیهای مختلف در شکل ۳ نشان داده شده است.
تمام پیکربندیها نسبتاً کُند هستند که علت آن اندازه بزرگ باینری است. اجرای برنامه بدون اسنپشاتها در نگاه اول عملکرد مناسبی دارد (۴۱۴ اجرا در ثانیه در مقایسه با ۷۵۴ اجرا در ثانیه در حالت پایدار)، اما نمیتوان آن را توصیه کرد؛ زیرا عملاً هیچ مسیر (Path) جدیدی کشف نمیشود. فعال کردن فهرستهای مسدودسازی (Blocklists) برای این برنامه تقریباً عملکرد را به نصف کاهش میدهد و هر سه الگوریتم ابزارگذاری (Instrumentation Algorithms) نیز عملکرد تقریباً یکسانی دارند. بااینحال، اگر مسیرهای کشفشده بدون استفاده از فهرستهای مسدودسازی را بررسی کنیم، SanitizerCoverage با کشف ۳۷۷۸ مسیر بهترین نتیجه را به دست میآورد و پس از آن InsTrim با ۳۴۲۸ مسیر و کلاسیک (Classic) با ۲۹۷۸ مسیر قرار میگیرند.
جدول ۱: تعداد اجرای فازر در هر ثانیه و مسیرهای کشف شده برای WAFL در پیکربندیهای مختلف، باینریهای بومی ابزارگذاریشده با afl-clang، و راهکار WebAssembly مبتنی بر libFuzzer درونمرورگری (In-Browser) ارائه شده در [11]:
۵.۵ عملکرد اسنپشات WAFL (WAFL Snapshot Performance)
در ارزیابیهای عملکردی (Benchmarks) که در بخشهای پیشین شرح داده شدند، WAFL در حالت پایدار (Persistent Mode) عملکرد بهتری نسبت به یک باینری بومی (Native Binary) بدون حالت پایدار دارد و در برخی موارد نیز عملکردی قابل مقایسه با یک باینری بومی بهینه شده (Optimized Native Binary) ارائه میدهد. برای ارزیابی سرعت اسنپشاتهای سبک حافظه ماشین مجازی (Lightweight VM Memory Snapshots)، یک برنامه کوچک C را آزمایش کردیم که بخشی از حافظه را تخصیص میدهد و در آن مینویسد. نتایج در شکل ۴ نشان داده شدهاند.
برای اندازههای کوچک تخصیص حافظه (Allocation Sizes)، WAFL برای یک اجرا به ۵۵ میکروثانیه (μs) زمان نیاز دارد، درحالیکه این مقدار برای باینری بومی ۱۵۰ میکروثانیه است. تنها از اندازه تخصیص ۲۵۶ کیلوبایت (KiB) به بالا است که fork عملکرد بهتری نسبت به اسنپشاتهای WAFL دارد؛ اسنپشاتهای WAFL در این نقطه به ۱۶۰ میکروثانیه زمان نیاز دارند. نکته قابل توجه این است که این اندازهگیری، سربار حین اجرا (Execution Overhead) را نیز در نظر میگیرد؛ بر اساس ارزیابی ما، بخش عمده زمان صرفشده مربوط به همین سربار است. علاوه بر این، ارزیابی عملکرد را روی یک هسته (Single Core) انجام دادیم. ما فرض میکنیم که اسنپشاتهای WAFL نسبت به تعاملات هسته (Kernel Interactions) و پیمایشهای جدول صفحه (Page Table Walks) که فراخوانی سیستمی کند fork به آنها نیاز دارد، مقیاسپذیری (Scalability) بسیار بهتری خواهند داشت [27].
۵.۶ فازینگ صرفاً باینری برنامههای کاربردی واقعی WebAssembly (Binary-Only Fuzzing of Real-World)
در حال حاضر، تعدادی برنامه کاربردی واقعی وجود دارند که با استفاده از WebAssembly اجرا میشوند [5]. برای یافتن نمونههای مناسب جهت آزمون، wapm [23] را بررسی کردیم. تعداد بستههای موجود در wapm هنوز کم است. بااینحال، دو بسته را پیدا کردیم که اجرای آنها و انجام فازینگ روی آنها بهسادگی امکانپذیر بود؛ یعنی cowsay و qr2text.
پس از اجرای WAVM روی qr2text، این برنامه که برخلاف نامش، کدهای QR را از ورودی متنی ایجاد میکند، هیچ باگی پیدا نکرد. پس از یک مرحله اولیه، سرعت فازینگ در حدود ۴۰۰ اجرا در ثانیه (exec/s) تثبیت شد. در طول شش ساعت، هیچ کرشی (Crash) ایجاد نشد. در نسخه cowsay موجود در wapm، فازر ++AFL بلافاصله Crashهایی را شناسایی کرد. پس از تحلیل دستی، به این نتیجه رسیدیم که این نسخه Rust از cowsay که در مدیر بسته (Package Manager) موجود است، در صورت دریافت هرگونه ورودی غیر UTF-8، واقعاً دچار Panic میشود. اگرچه این موضوع کاربردپذیری WAFL را در یک سناریوی واقعی نشان میدهد، این باگ در عمل آسیب واقعی ایجاد نمیکند و در بدترین حالت، میتواند در برابر یک cowsay مبتنی بر شبکه (Network-Based Cowsay) به انکار سرویس (Denial of Service یا DoS) منجر شود.
۵.۷ بحث (Discussion)
همانطور که انتظار میرفت، WAFL-wasm3 مبتنی بر مفسر (Interpreter-Based) و WAFL-WAVM بدون حالت پایدار (Persistent Mode) کُند میباشند و عملکرد آنها بسیار پایینتر از خط مبنای فازر درونمرورگری (In-Browser Fuzzer Baseline) است. بااینحال، در این سناریو، wasm3 بهطور غیرمنتظرهای از WAVM سریعتر است. یک توضیح احتمالی این است که محیط اجرای کوچکتر wasm3 ممکن است هنگام ایجاد Fork سودمند باشد. بهمحض فعال شدن حالت اسنپشات (Snapshot Mode)، عملکرد، صرفنظر از نوع ابزارگذاری (Instrumentation) مورد استفاده، از باینری بومی (Native Binary) فراتر میرود. این موضوع نشان میدهد که حالت اسنپشات در عمل قابل استفاده است.
علاوه بر این، فعال کردن فازینگ با حافظه اشتراکی (Shared Memory Fuzzing) برای اهداف brotli و lzma2dec تأثیر مثبتی دارد. در مجموع، نتیجه مثبت است و نشان میدهد که در صورت امکان، باید از فازینگ با حافظه اشتراکی استفاده شود؛ یعنی زمانی که ورودی از طریق ورودی استاندارد (stdin) و نه فایلها منتقل میشود. انتظار داریم این نتیجه با مقیاسدهی به چندین هسته (Multiple Cores) نیز بهبود یابد، زیرا ورودی حافظه اشتراکی نیازی به عبور از هسته (Kernel) ندارد. در مورد فهرستهای مسدودسازی (Blocklists)، نتایج یکدست نیستند: در برخی پیکربندیها، مانند Brotli با SanitizerCoverage و Lzma با InsTrim، به نظر میرسد که استفاده از فهرستهای مسدودسازی باعث کاهش عملکرد میشود. این موضوع ممکن است به دلیل کشف مسیرهای دشوارتر (و مرتبطتر) توسط فازر باشد.
شکل ۲ نشان میدهد که در ابزارگذاری کلاسیک (Classic)، تعداد مطلق بلوکهای ابزارگذاریشدهای که با استفاده از فهرست مسدودسازی حذف میشوند، زیاد است؛ اما برای SanitizerCoverage تقریباً هیچ بلوکی حذف نمیشود. به همین دلیل، اثر مثبت فهرست مسدودسازی بر SanitizerCoverage ممکن است اندک باشد. در مجموع، معتقدیم فهرستهای مسدودسازی به فازینگ کمک میکنند، اما اثر اندازهگیریشده آنها کوچک است. با مقایسه گذرگاههای مختلف ابزارگذاری (Instrumentation Passes)، SanitizerCoverage نتایج ناامیدکنندهای ارائه میدهد و InsTrim عملکرد بهتری نسبت به هر دو روش دیگر دارد. نتایج ممکن است به هدف (Target) وابسته باشند، اما در مجموعه آزمون ما، میتوان InsTrim را بهعنوان بهترین گزینه برای ابزارگذاری توصیه کرد.
۶. کارهای آینده (FUTURE WORK)
SanitizerCoverage کمترین تعداد بلوکهای ابزارگذاریشده را دارد و در ++AFL اصلی نسبت به InsTrim ترجیح داده میشود. بااینحال، در اندازهگیریهای ما، یکپارچهسازی آن در WAFL-WAVM در مقایسه با InsTrim عملکرد ضعیفتری دارد. ما احتمال میدهیم که طراحی Callbackها، بهویژه مقداردهی اولیه Guardها در Runtime (محیط اجرا)، همچنان قابلیت بهبود داشته باشد.
در نهایت، باید بتوان به سرعتهایی بالاتر از InsTrim دست یافت. اگر مشکل مقداردهی اولیه، چه در WAVM و چه با اصلاح گذر (pass) مربوط به SanitizerCoverage برطرف شود، درونخطیسازی Callbackها با استفاده از گزینه Inline8bitCounters هیچ هزینهای در پی نخواهد داشت.
گزینههای دیگری برای ابزارگذاری (Instrumentation) در ++AFL وجود دارند که در این مقاله بررسی نشدهاند. ++AFL گذرگاههای LLVM مربوط به بهینهسازی در زمان پیوند (Link Time Optimization یا LTO) را در اختیار دارد و استفاده از آنها را توصیه میکند. از آنجا که در مورد ما، ورودی از قبل در یک واحد ترجمه (Translation Unit) بزرگ قرار دارد، بعید است که این گذرگاهها نتایج را بهبود دهند؛ بااینحال، این موضوع همچنان باید بررسی و تأیید شود.
گزینههای امیدوارکنندهتر، بهبودهای مربوط به بازخورد (Feedback Improvements) هستند. CompCov [2] دستورهای مقایسه چندبایتی (Multi-Byte Compare Instructions) را به مقایسههای تکبایتی (Single-Byte Compares) تقسیم میکند و در نتیجه، پیمایش آنها برای فازر آسانتر میشود. همچنین، با هوک کردن (Hook) مقایسهها (Compares) و استفاده از روشهای اکتشافی (Heuristics) مشابه فهرست مسدودسازی ابزارگذاری libc، میتوان پشتیبانی از CmpLog را اضافه کرد تا بازخورد را به جهشگر (Mutator) موسوم به RedQueen در ++AFL منتقل کند. هر دو گزینه، در ازای پوشش (Coverage) بهتر، بخشی از سرعت اجرا را کاهش میدهند؛ بااینحال، برای پیادهسازی آنها همچنان به تغییرات بیشتری در محیط اجرای WAVM نیاز خواهد بود.
۷. نتیجهگیری (CONCLUSION)
WAFL باینریهای WebAssembly را بدون ایجاد تغییر در آنها، در مرحله کامپایل پیش از اجرا (Ahead-of-Time یا AOT) در WAVM و با اعمال گذرگاههای موجود LLVM در ++AFL ابزارگذاری (Instrument) میکند. ما بهینهسازیهای ++AFL، مانند فازینگ با حافظه اشتراکی (Shared-Memory Fuzzing)، را در محیط اجرا یکپارچه میکنیم تا بتوان از مزایای آنها بهره برد؛ حتی با وجود اینکه برنامه تحت آزمون در یک محیط Sandboxشده اجرا میشود. ارزیابی ما نشان میدهد که عملکرد WAFL بسیار خوب است. در مقایسه با اهداف بومی (Native Targets) که از کد منبع ساخته شدهاند، به نتایج جالبی دست یافتیم: برای اهداف کوچک، اسنپشاتهای سبکوزن WAVM عملکرد بهتری از مهارگرهای بومی AFL برای x86-64 دارند که از روی کد منبع کامپایل شدهاند، مشروط بر اینکه این مهارگرها به فراخوانی سیستمی کند fork متکی باشند. این موضوع برای هر هدفی که در حالت غیرپایدار (Non-Persistent Mode) اجرا شود، صادق است. WAFL مسئلهای را حل میکند که تاکنون راهکاری برای آن وجود نداشته است: چگونه باگها را در باینریهای کامپایلشده WebAssembly کشف کنیم. نیاز به یک فازر صرفاً باینری (Binary-Only Fuzzer) آشکار است، زیرا پژوهشهای پیشین نشان دادهاند که بخش قابلتوجهی از باینریهای موجود در دنیای واقعی بهطور بالقوه آسیبپذیر هستند [5]. WAFL نخستین ابزاری است که اهداف WebAssembly صرفاً باینری (Binary-Only WebAssembly Targets) را فاز میکند.
دسترسی
WAFL به صورت متنباز (Open-Source) در پروژه WAFL مخزن GitHub در دسترس است.
منابع
[1] Frank Denis. 2021. Benchmark of WebAssembly runtimes - 2021 Q1. https://00f.net/2021/02/22/webassembly-runtimes-benchmarks/
[2] Andrea Fioraldi, Dominik Maier, Heiko Eißfeldt, and Marc Heuse. 2020. AFL++:Combining Incremental Steps of Fuzzing Research. In 14th USENIX Workshop on Offensive Technologies (WOOT 20). USENIX Association. https://www.usenix.org/conference/woot20/presentation/fioraldi
[3] Andreas Haas, Andreas Rossberg, Derek L. Schuff, Ben L. Titzer, Michael Holman, Dan Gohman, Luke Wagner, Alon Zakai, and Jf Bastien. 2017. Bringing the web up to speed with WebAssembly. In Proceedings of the 38th ACM SIGPLAN Conference on Programming Language Design and Implementation. ACM, Barcelona Spain, 185–200. https://doi.org/10.1145/3062341.3062363
[4] Pat Hickey, Jakub Konka, Dan Gohman, Sam Clegg, Andrew Brown, Alex Crichton, Lin Clark, Colin Ihrig, Peter Huene, YAMAMOTO Yuji, Denis Vasilik, Josh Triplett, Sergey Rubanov, Syrus Akbary, Mike Frysinger, Aaron Turner, Alon Zakai, Andrew Mackenzie, Benjamin Brittain, Casper Beyer, David McKay, Leon Wang, Marcin Mielniczuk, Mendy Berger, PTrottier, Piotr Sikora, Till Schnei dereit, Katelyn Martin, and Nasso. 2020. WebAssembly/WASI: snapshot-01. https://doi.org/10.5281/ZENODO.4323447
[5] Aaron Hilbig, Daniel Lehmann, and Michael Pradel. 2021. An Empirical Study of Real-World WebAssembly Binaries: Security, Languages, Use Cases. In Proceedings of the Web Conference 2021. ACM, Ljubljana Slovenia, 2696–2708. https://doi.org/10.1145/3442381.3450138
[6] Chin-Chia Hsu, Che-Yu Wu, Hsu-Chun Hsiao, and Shih-Kun Huang. 2018. INSTRIM: Lightweight Instrumentation for Coverage-guided Fuzzing. In Proceedings 2018 Workshop on Binary Analysis Research. Internet Society, San Diego, CA. https://doi.org/10.14722/bar.2018.23014
[7] Daehee Jang and Ammar Askar. 2020. FuzzCoin: A Digital Currency with Fuzzing as a Proof-of- Work. https://fuzzcoin.kr
[8] Denis Kolodin, Henry Zimmerman, Justin Starry, and Simon. 2021. Yew: Rust /Wasm framework for building client web apps. https://github.com/yewstack/yew
[9] Daniel Lehmann, Johannes Kinder, and Michael Pradel. 2020. Everything Old is New Again: Binary Security of WebAssembly. In 29th USENIX Security Symposium(USENIX Security 20). USENIX Association, 217–234. https://www.usenix.org/conference/usenixsecurity20/presentation/lehmann
[10] Steven Massey and Volodymyr Shymanskyy. 2021. wasm3: The fastest WebAssembly interpreter. https://github.com/wasm3/wasm3
[11] Jonathan Metzman. 2019. Your Browser is my Fuzzer: Fuzzing Native Applications in Web Browsers. https://raw.githubusercontent.com/jonathanmetzman/wasm-fuzzing-demo/master/meetup-Fuzzing-Native-Applications-in-Browsers-With-WASM.pdf
[12] Jonathan Metzman, László Szekeres, Laurent Simon, Read Sprabery, and Abhishek Arya. 2021. FuzzBench: an open fuzzer benchmarking platform and service. In Proceedings of the 29th ACM Joint Meeting on European Software Engineering Conference and Symposium on the Foundations of Software Engineering. 1393–1403.
[13] Microsoft. 2021. Blazor: Build client web apps with C#. https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor
[14] Tim Newsham and Jesse Hertz. 2016. Project Triforce: Run AFL on Everything! https://web.archive.org/web/20160627201122/https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2016/june/project-triforce-run-afl-on-everything/ archived from the original.
[15] Árpád Perényi and Jan Midtgaard. 2020. Stack-Driven Program Generation of WebAssembly. In Programming Languages and Systems, Bruno C. d. S. Oliveira (Ed.). Vol. 12470. Springer International Publishing, Cham, 209–230. https://doi.org/10.1007/978-3-030-64437-6_11 Series Title: Lecture Notes in Computer Science.
[16] Andreas Rossberg. 2019. WebAssembly Core Specification. Technical Report. W3C. https://www.w3.org/TR/wasm-core-1/
[17] Andrew Scheidecker. 2021. WebAssembly Virtual Machine. https://github.com/WAVM/WAVM
[18] Sergej Schumilo, Cornelius Aschermann, Ali Abbasi, Simon Wör-ner, and Thorsten Holz. 2021. Nyx: Greybox Hypervisor Fuzzing using Fast Snapshots and Affine Types. In 30th USENIX Security Symposium (USENIX Security 21). USENIX Association, 2597–2614. https://www.usenix.org/conference/usenixsecurity21/ presentation/schumilo
[19] Kostya Serebryany. 2017. OSS-Fuzz - Google’s continuous fuzzing service for open source software. USENIX Association, Vancouver, BC.
[20] Dokyung Song, Felicitas Hetzelt, Jonghwan Kim, Brent ByungHoon Kang, Jean-Pierre Seifert, and Michael Franz. 2020. Agamotto: Accelerating Kernel Driver Fuzzing with Lightweight Virtual Machine Checkpoints. In 29th USENIX Security Symposium (USENIX Security 20). USENIX Association, 2541–2557. https://www. usenix.org/conference/usenixsecurity20/presentation/song
[21] The Clang Team. 2021. LLVM SanitizerCoverage. https://clang.llvm.org/docs/SanitizerCoverage.html
[22] Jinghan Wang, Yue Duan, Wei Song, Heng Yin, and Chengyu Song. 2019. Be Sensitive and Collaborative: Analyzing Impact of Coverage Metrics in Greybox Fuzzing. In 22nd International Symposium on Research in Attacks, Intrusions and Defenses (RAID 2019). USENIX Association, Chaoyang District, Beijing, 1–15. https://www.usenix.org/conference/raid2019/presentation/wang
[23] Wasmer, Inc. 2019. wapm is the WebAssembly Package Manager. https://wapm.io/
[24] Wasmer, Inc. 2021. wasmer: The leading WebAssembly Runtime. https://github.com/wasmerio/wasmer
[25] Conrad Watt. 2018. Mechanising and verifying the WebAssembly specification. In Proceedings of the 7th ACM SIGPLAN International Conference on CertifiedPrograms and Proofs. ACM, Los Angeles CA USA, 53–65. https://doi.org/10.1145/3167082
[26] WebAssembly Community Group. 2020. wasi-libc: WASI libc implemenation for WebAssembly. https://github.com/WebAssembly/wasi-libc
[27] Wen Xu, Sanidhya Kashyap, Changwoo Min, and Taesoo Kim. 2017. Designing New Operating Primitives to Improve Fuzzing Performance. In Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security. ACM, Dallas Texas USA, 2313–2328. https://doi.org/10.1145/3133956.3134046
[28] Alon Zakai. 2011. Emscripten: an LLVM-to-JavaScript compiler. In Proceedings of the ACM international conference companion on Object oriented programming systems languages and applications companion - SPLASH ’11. ACM Press, Portland, Oregon, USA, 301. https://doi.org/10.1145/2048147.2048224
[29] Michał Zalewski. 2017. American Fuzzy Lop. Technical Whitepaper. https://lcamtuf.coredump.cx/afl/technical_details.txt