خانه » Fuzzm: شناسایی آسیب‌پذیری‌های حافظه با استفاده از ابزاربندی در سطح باینری و فازینگ WebAssembly

Fuzzm: شناسایی آسیب‌پذیری‌های حافظه با استفاده از ابزاربندی در سطح باینری و فازینگ WebAssembly

Fuzzm: Finding Memory Bugs through Binary-Only Instrumentation and Fuzzing of WebAssembly

توسط Vulnerlab
66 بازدید
WebAssembly - فازینگ - fuzzm

باینری‌های WebAssembly اغلب از زبان‌های ناایمن از نظر مدیریت حافظه مانند C و ++C کامپایل می‌شوند. به دلیل وجود مدل حافظهٔ خطی (Linear Memory) در WebAssembly و فقدان برخی مکانیزم‌های حفاظتی (مانند stack canary) آسیب‌پذیری‌های حافظه که در سطح کد منبع وجود دارند، در باینری‌های کامپایل ‌شدهٔ WebAssembly قابل بهره‌برداری (exploitable) هستند؛ در برخی موارد حتی آسان‌تر از کد بومی (native code).

در این مقاله، مسئلهٔ کشف چنین آسیب‌پذیری‌هایی از طریق نخستین فازر مبتنی بر باینری (binary-only fuzzer) برای WebAssembly مورد بررسی قرار می‌گیرد. رویکرد پیشنهادی ما که Fuzzm نام دارد، چندین مؤلفه از جمله ابزار قناری (Canary) را برای تشخیص سرریزهای مثبت و منفی (overflow و underflow) در پشته (stack) و هیپ (heap)، یک ابزاربندی پوشش کد کارآمد (coverage instrumentation)، یک ماشین مجازی WebAssembly و الگوریتم تولید ورودی فازر محبوب AFL را با یکدیگر ترکیب می‌کند.

علاوه بر ایفای نقش قناری‌ها (canary) به‌ عنوان اوراکل (oracle) برای فازینگ، همچنین می‌توان از آن‌ها به‌ عنوان یک تکنیک مستقل سخت‌سازی باینری (binary hardening) نیز بهره برد تا از بهره‌برداری از باینری‌های آسیب‌پذیر در محیط‌های عملیاتی (production) جلوگیری شود.

ما Fuzzm را با استفاده از ۲۸ باینری واقعی WebAssembly ارزیابی کردیم؛ برخی از این باینری‌ها از کد منبع کامپایل شده‌اند و برخی دیگر بدون دسترسی به سورس‌کد و به‌صورت موجود در دنیای واقعی (in the wild) به دست آمده‌اند. نتایج نشان می‌دهد که این فازر قادر است هزاران مسیر اجرایی (execution paths) را کاوش کند، ده‌ها کرش (crash) را تحریک نماید و صدها اجرای برنامه در هر ثانیه انجام دهد.

زمانی که این رویکرد برای سخت‌سازی باینری مورد استفاده قرار می‌گیرد، می‌تواند اکسپلویت‌های منتشرشدهٔ قبلی علیه باینری‌های آسیب‌پذیر WebAssembly را خنثی کند، در حالی که افزایش سربار زمان اجرا (runtime overhead) بسیار اندکی ایجاد می‌کند.

1. مقدمه

WebAssembly یک زبان بایت‌کد با اهمیت روزافزون است ‎[20, 55, 56]‎ که دارای معنا‌شناسی سطح پایین (low-level semantics)، اجرای سریع و مجموعه‌ای متنوع از زبان‌های مبدأ است که به آن کامپایل می‌شوند. این فناوری به‌طور گسترده در مرورگرها پشتیبانی می‌شود و در طیف وسیعی از کاربردها مورد استفاده قرار می‌گیرد؛ از جمله در برنامه‌های وب متنوع ‎[24]‎، رایانش ابری بدون سرور (serverless cloud computing) ‎[21, 52]‎، پلتفرم‌های قرارداد هوشمند ‎[23, 36, 46]‎، برای سندباکس (sandboxing) کتابخانه‌ها در برنامه‌های بومی ‎[40, 58]‎، و حتی به‌عنوان یک بایت‌کد عمومی در زمان‌اجراهای مستقل WebAssembly (standalone runtimes) ‎[1, 2, 13]‎.

با توجه به این اهمیت، امنیت WebAssembly نیز به‌طور فزاینده‌ای مورد توجه قرار گرفته است. اگرچه WebAssembly به‌ طور ذاتی برخی مسائل امنیتی را برطرف می‌کند، اما آسیب‌پذیری‌های موجود در سطح کد منبع همچنان ممکن است به باینری‌های WebAssembly منتقل شوند ‎[8, 30, 37]. پژوهش‌های اخیر ‎[30]‎ نشان داده‌اند که به‌طور شگفت‌آوری، آسیب‌پذیری‌های حافظه در باینری‌های WebAssembly گاهی حتی ساده‌تر از زمانی که همان کد منبع برای معماری‌های بومی کامپایل می‌شود قابل بهره‌برداری هستند.

یکی از دلایل این مسئله فقدان برخی مکانیزم‌های کاهندهٔ ریسک مانند stack canaryها، فلگ‌های حفاظت صفحهٔ حافظه (page protection flags) یا مدیرهای تخصیص حافظهٔ سخت‌سازی‌شده (hardened memory allocators) است ‎[30].

برای کشف آسیب‌پذیری‌ها، فازینگ جعبه‌ خاکستری (greybox fuzzing) به‌ عنوان یک تکنیک مؤثر شناخته شده است  ‎[9, 22, 32, 47, 59]. برای نمونه، پروژهٔ OSSFuzz متعلق به گوگل موفق شده است هزاران آسیب‌پذیری را در نرم‌افزارهای پرکاربرد کشف کند ‎[3, 51]‎. یک فازر جعبه‌خاکستری به‌ صورت خودکار ورودی‌هایی تولید می‌کند که برنامهٔ هدف را کاوش کرده و در نهایت یک آسیب‌پذیری را فعال می‌کنند. برای این منظور، فازر به دو مؤلفهٔ اصلی نیاز دارد:
(۱) بازخورد سبک از اجرای برنامه ــ برای مثال اطلاعات پوشش کد (coverage information) ــ به‌منظور هدایت فرآیند تولید ورودی، و
(۲) اوراکل‌های زمان اجرا (runtime oracles) که وجود یک آسیب‌پذیری را آشکار می‌کنند، برای مثال از طریق ایجاد توقف ناگهانی در برنامه (Crash).

وجود یک فازر جعبه‌ خاکستری (greybox fuzzer) برای WebAssembly بسیار مطلوب است، اما لازم است چندین ویژگی خاص WebAssembly در نظر گرفته شود. نخست آن‌که WebAssembly یک هدف کامپایل (compilation target) برای زبان‌های مبدأ متعددی از جمله C، C++، Rust، Go و بسیاری زبان‌های دیگر است ‎[24]‎. در نتیجه، فازری که تنها برای یک زبان مبدأ خاص طراحی شده باشد، قادر خواهد بود تنها بخش کوچکی از باینری‌های واقعی موجود را تحلیل کند. دوم آن‌که ممکن است کد منبع یک باینری WebAssembly در دسترس نباشد؛ برای مثال هنگام تحلیل وب‌سایت‌های ثالث، کتابخانه‌های شخص ثالث، یا برنامه‌های قدیمی داخلی (legacy applications).

حتی در صورتی که کد منبع در دسترس باشد، ادغام یک فازر در جریان توسعهٔ نرم‌افزار (development workflow) دشوارتر می‌شود اگر این کار مستلزم تغییر در سیستم ساخت (build system) یا استفاده از کامپایلرهای خاص (یا نسخه‌های مشخصی از آن‌ها) باشد. سوم آن‌که حتی زمانی که یک برنامه از یک کد منبع یکسان کامپایل می‌شود، رفتار امنیتی آن در نسخهٔ کامپایل‌شده برای WebAssembly ممکن است با همان برنامه در حالت کامپایل‌شده برای معماری و کد بومی (native code) متفاوت باشد ‎[30]‎. همان‌گونه که در بخش ۳ نشان می‌دهیم، این‌که آیا یک آسیب‌پذیری قابل بهره‌برداری است یا خیر، به نحوهٔ ترجمهٔ معناشناسی زبان مبدأ در فرآیند کامپایل و همچنین به مکانیزم‌های حفاظتی ارائه‌ شده توسط پلتفرم مقصد بستگی دارد.

در نتیجه، فازینگ یک برنامه در حالتی که برای پلتفرم دیگری کامپایل شده باشد ــ برای مثال x86 ‎[16]‎ ــ برای آشکارسازی باگ‌های حافظه در WebAssembly کافی نیست. در مجموع، این ویژگی‌ها ضرورت طراحی یک فازر اختصاصی برای باینری‌های WebAssembly را نشان می‌دهند. با این حال، با وجود موفقیت گستردهٔ فازینگ جعبه‌ خاکستری و افزایش اهمیت WebAssembly، در حال حاضر چنین فازری وجود ندارد.

در این مقاله Fuzzm[1] معرفی می‌شود؛ نخستین فازر جعبه‌ خاکستری مبتنی بر باینری (binary-only greybox fuzzer) برای WebAssembly. مؤلفه‌های اصلی این سامانه ــ که در شکل ۱ نشان داده شده‌اند ــ چندین چالش فنی مهم را مورد توجه قرار می‌دهند.

نخست آن‌که برخلاف برنامه‌های بومی، WebAssembly فاقد برخی اوراکل‌های داخلی (builtin oracles) است که فازرهای بومی برای شناسایی رفتارهای مشکوک برنامه به آن‌ها متکی هستند. برای مثال، هیچ‌یک از کامپایلرهای فعلی که WebAssembly را هدف قرار می‌دهند stack canary اضافه نمی‌کنند ‎[14, 45]‎، و به دلیل وجود حافظهٔ خطی WebAssembly، سرریز حافظه ــ برای نمونه از پشته به داده‌های هیپ ــ بدون آن‌که شناسایی شود باقی می‌ماند ‎[30]‎. اگرچه ابزارهایی مانند AddressSanitizer ‎[50]‎ می‌توانند با ابزاربندی در سطح کد منبع رفتارهای نادرست مرتبط با حافظه را شناسایی کنند، این روش‌ها برای باینری‌ها قابل استفاده نیستند. در مقابل، ابزاربندی مبتنی بر قناری (canary) برای پشته (Stack) و هیپ (Heap) در رویکرد ما با بازنویسی باینری‌ها، سرریز مثبت و منفی (overflows / underflows) در پشته و هیپ را تشخیص می‌دهد. افزون بر کاربرد در فازینگ، این قناری‌ها می‌توانند برای سخت‌سازی پسینی (retroactive hardening) باینری‌های موجود WebAssembly در محیط‌های عملیاتی نیز مورد استفاده قرار گیرند.

دوم آن‌که در یک فازر مبتنی بر باینری نمی‌توان به کدی که توسط کامپایلر برای ردیابی پوشش برنامه درج شده است اتکا کرد؛ روشی که فازرهایی مانند AFL و بسیاری دیگر از آن استفاده می‌کنند ‎[9, 32, 47]‎. هرچند روش‌هایی برای ابزاربندی پویا در سطح باینری وجود دارد ــ برای مثال حالت QEMU در AFL ــ این روش‌ها اغلب با سربار اجرایی بالا همراه هستند، به معماری سخت‌افزار وابسته‌اند و برای WebAssembly قابل استفاده نیستند. در مقابل، ابزاربندی پوشش کد در رویکرد ما روی باینری‌های WebAssembly بدون تغییر و در سطح تولید (production) اعمال شده و پوشش برنامه را به‌ صورت کارآمد ردیابی می‌کند.

چالش نهایی، به‌ویژه هنگام فازینگ برنامه‌های مبتنی بر بایت‌کد، مسئلهٔ کارایی است. باینری‌های WebAssembly درون یک ماشین مجازی (Virtual Machine یا VM) اجرا می‌شوند و یک رویکرد ساده ممکن است با زمان راه‌اندازی بالا مواجه شود که در نتیجه فازینگ را غیرعملی می‌سازد.

برای رفع این مشکل، ما یک ماشین مجازی WebAssembly را با الگوریتم تولید ورودی آزموده و کارآمد AFL یکپارچه می‌کنیم تا برنامهٔ هدف را اجرا کند. در اینجا، خاصیت سندباکس‌سازی WebAssembly در واقع می‌تواند به‌جای یک نقطه‌ضعف، یک فرصت محسوب شود: حافظهٔ برنامهٔ هدف و AFL می‌توانند در یک فضای آدرس مشترک قرار گیرند، بدون آن‌که نیاز به فرآیندهای جداگانه برای جداسازی آن‌ها وجود داشته باشد.

نتیجهٔ پرداختن به چالش‌های فوق، ارائهٔ یک فازر end-to-end عملی، مؤثر و کارآمد برای باینری‌های WebAssembly است. در ارزیابی خود، Fuzzm را بر روی ۲۸ برنامه به کار گرفتیم که از میان آن‌ها ۱۰ برنامه شناخته‌ شده از کد منبع به WebAssembly کامپایل شده‌اند و ۱۸ مورد دیگر باینری‌های WebAssembly بدون کد منبع هستند که در دنیای واقعی (in the wild) یافت شده‌اند. نتایج نشان می‌دهد که رویکرد ما کارآمد است؛ به‌ طور میانگین در طی ۲۴ ساعت فازینگ، ۱٬۲۳۲ مسیر اجرایی منحصربه‌فرد را پوشش داده و ۴۰ کرش منحصربه‌فرد را فعال کرده است. بخش عمده‌ای از این توقف‌های ناگهانی (Crash) ناشی از اوراکل‌های مبتنی بر قناری (canary) در سیستم ما هستند.

از نظر کارایی، Fuzzm قادر است صدها اجرای برنامه در هر ثانیه انجام دهد که با عملکرد AFL قابل مقایسه است، با وجود آن‌که تنها به یک باینری به‌ عنوان ورودی نیاز دارد و برنامه را درون یک ماشین مجازی (VM) اجرا می‌کند. در نهایت نشان می‌دهیم که قناری‌هایی که توسط ابزاربندی ما درج می‌شوند می‌توانند سه اکسپلویت منتشر شدهٔ قبلی علیه باینری‌های آسیب‌پذیر WebAssembly ‎[30]‎ را به‌ طور مؤثر خنثی کنند. به دلیل سربار زمان اجرای پایین آن‌ها (به‌ترتیب 1.05x و 1.06x برای قناری‌های پشته و هیپ)، این ابزاربندی مبتنی بر قناری علاوه بر کاربرد در فازینگ، می‌تواند به‌ عنوان یک ابزار مستقل سخت‌سازی (hardening) برای باینری‌های موجود و آسیب‌پذیر WebAssembly نیز مورد استفاده قرار گیرد.

WebAssembly - فازینگ - fuzzm
شکل ۱: نمای کلی از اجزای اصلی Fuzzm

مشارکت‌ها (Contributions). به‌طور خلاصه، این مقاله مشارکت‌های زیر را ارائه می‌کند:

  • معرفی نخستین فازر مبتنی بر باینری برای برنامه‌های WebAssembly.
  • ارائهٔ یک ابزار باینری که قناری پشته و هیپ (stack canary و heap canary) را وارد می‌کند؛ این سازوکار می‌تواند هم برای سخت‌سازی برنامه‌های WebAssembly موجود و هم به‌ عنوان اوراکل در فازر پیشنهادی استفاده شود (بخش ۴).
  • یکپارچه‌سازی فازر AFL و الگوریتم تولید ورودی آزمودهٔ آن با یک ابزاربندی باینری که اطلاعات پوشش سازگار فراهم می‌کند و یک ماشین مجازی WebAssembly برای دستیابی به فازینگ سرتاسری کارآمد (بخش).
  • ارائهٔ شواهد تجربی مبنی بر این‌ که Fuzzm به‌ طور مؤثر مسیرهای اجرایی را کاوش کرده و در برنامه‌های شناخته‌ شدهٔ کامپایل‌ شده به WebAssembly و همچنین در باینری‌های بزرگ واقعی بدون کد منبع کرش‌ها را شناسایی می‌کند (بخش ۶).
  • ارائهٔ شواهد تجربی مبنی بر این‌ که باینری‌هایی که با ابزاربندی فناری (canary) پیشنهادی، سخت‌سازی شده‌اند، با سربار زمان اجرای اندک اجرا می‌شوند و به‌طور مؤثر اکسپلویت‌های منتشر شدهٔ پیشین را خنثی می‌کنند (بخش ۶).

۲. پیش‌زمینهٔ WebAssembly

در این بخش، ویژگی‌های WebAssembly که بیشترین ارتباط را با این مقاله دارند و همچنین برخی جنبه‌های امنیتی این زبان را به‌طور خلاصه معرفی می‌کنیم. WebAssembly یک زبان شبه‌اسمبلی است که به‌عنوان هدف کامپایل قابل‌حمل برای زبان‌های مبدأ مختلف طراحی شده است؛ برای مثال از C++/C با استفاده از Emscripten یا Clang و یا از Rust. علاوه بر پلتفرم مرورگر ــ که نخستین محیطی بود که به‌طور گسترده از WebAssembly پشتیبانی کرد ــ امروزه پلتفرم‌های دیگری نیز از آن پشتیبانی می‌کنند؛ از جمله Node.js و ماشین مجازی مستقل Wasmtime.

انواع داده (Types). WebAssembly  یک زبان پشته‌محور (stack-based) با نوع‌دهی ایستا (statically typed) است. در WebAssembly چهار نوع داده وجود دارد:

  • i32 و i64 برای اعداد صحیح ۳۲ و ۶۴ بیتی
  • f32 و f64 برای اعداد اعشاری (floating point) ۳۲ و ۶۴ بیتی

دستورالعمل‌های دارای نوع‌، مقادیر را از یک پشتهٔ عملوند ضمنی (operand stack) برداشته یا در آن قرار می‌دهند. برای مثال، دستور i32.const N مقدار ثابت ۳۲ بیتی N را روی پشته قرار می‌دهد، و دستور f64.add دو مقدار اعشاری ۶۴ بیتی را از پشته برداشته و مجموع آن‌ها را دوباره روی پشته قرار می‌دهد.

جریان کنترل (Control Flow). برخلاف بسیاری از زبان‌های شبه‌اسمبلی دیگر، WebAssembly دارای جریان کنترل ساخت‌یافته (structured control flow) است که با استفاده از بلوک‌های تودرتو (nested blocks) کدگذاری می‌شود. یک بلوک (block) دنباله‌ای از دستورالعمل‌ها است که با دستور block یا loop آغاز شده و با دستور end خاتمه می‌یابد. بلوک‌ها می‌توانند به‌صورت دلخواه در عمق‌های مختلف تودرتو شوند.

در داخل یک بلوک، دستور br (br_if) L به‌طور (شرطی) به انتهای بلوک L-ام پرش می‌کند (و در مورد حلقه‌ها به ابتدای آن بازمی‌گردد). در اینجا:

  • 0 به بلوکی اشاره دارد که دستور br_if در آن قرار دارد
  • 1 به بلوک والد آن
  • و به همین ترتیب برای سطوح بالاتر

بنابراین L را می‌توان به‌عنوان یک برچسب عددی نسبی برای بلوک‌ها در نظر گرفت. برای مثال، دستور br در خط ۳ شکل ۲ به ابتدای بلوک loop در خط ۱ پرش می‌کند، و دستور br در خط ۸ از دو بلوک خارج شده و به انتهای برنامه در خط ۱۰ می‌پرد. دستور br_table L0 . . . Lt , D نیز برای پیاده‌سازی jump table  (جدول پرش) استفاده می‌شود؛ این دستور مقدار صحیح بالای پشتهٔ عملوند را مصرف کرده و به Lᵢ پرش می‌کند، یا اگر i > t باشد به مسیر پیش‌فرض D منتقل می‌شود.

فازینگ - فازم - fuzzm
شکل ۲: شاخه‌بندی در WebAssembly به صورت تصویری

توابع و متغیرها (Functions and Variables). در WebAssembly هر تابع دارای پپارامترهای دارای نوع (typed parameters)، متغیرهای محلی دارای نوع (typed local variables) و یک دنباله از دستورالعمل‌ها است. پارامترها و متغیرهای محلی با استفاده از local.get N خوانده شده و با local.set N مقداردهی می‌شوند، که در آن N به متغیر محلی یا پارامتر N-ام اشاره دارد. بیشتر دستورالعمل‌ها، از جمله فراخوانی‌های مستقیم (direct calls)، به‌صورت ایستا (static) از نظر نوع بررسی می‌شوند؛ اما فراخوانی‌های غیرمستقیم (indirect calls) در زمان اجرا (runtime) بررسی نوع می‌شوند. همچنین متغیرهای سراسری (global variables) وجود دارند که با global.get خوانده شده و با global.set نوشته می‌شوند.

حافظه (Memory). برخلاف برنامه‌های بومی، WebAssembly از یک حافظه خطی با قابلیت آدرس‌دهی بایتی (byte-addressable linear memory) برای ذخیره آبجکت‌ها (Object) با طول عمر بالا استفاده می‌کند. این حافظه هنگام ایجاد نمونهٔ ماژول (module instantiation) با اندازهٔ مشخصی مقداردهی اولیه می‌شود و می‌تواند در زمان اجرا با دستور memory.grow افزایش یابد.

فضای آدرس‌دهی ۳۲ بیتی فاقد شکاف (holes) است؛ بنابراین هر اشاره‌گر در بازهٔ [0, size] معتبر محسوب می‌شود. مدیریت این حافظه کاملاً بر عهدهٔ برنامه است و گردآوری زباله (garbage collection) در آن وجود ندارد. دستورالعمل‌های load و store مقادیر i32 را به‌ عنوان آدرس حافظه دریافت می‌کنند.

برای مثال، دستور i64.store دو مقدار را از پشتهٔ عملوند مصرف می‌کند: یک مقدار i64 و یک آدرس i32، و مقدار هشت‌بایتی را در آن آدرس از حافظهٔ خطی ذخیره می‌کند.

WASI. به‌ طور پیش‌فرض WebAssembly دارای کتابخانهٔ استاندارد یا توابع ورودی/خروجی (I/O) نیست. در عوض، تمام تعامل با سیستم میزبان باید از طریق imports انجام شود. WASI (WebAssembly System Interface) یک رابط فراخوانی سیستمی (syscall interface) شامل مجموعه‌ای از توابع برای انجام عملیات‌هایی مانند I/O، دسترسی به سیستم فایل و سایر تعاملات با سیستم میزبان تعریف می‌کند.

یک برنامه که در یک زبان سطح بالا (معمولاً C++، C یا Rust) نوشته شده است می‌تواند به یک باینری WASI کامپایل شده و سپس توسط یک runtime سازگار با WASI مانند Wasmtime اجرا شود. روش‌های دیگری نیز برای اجرای WebAssembly وجود دارد؛ برای مثال در مرورگر یا در Node.js، اما در این پژوهش تمرکز ما بر WASI است.

امنیت (Security). امنیت WebAssembly دارای دو جنبه است. از یک سو، برنامه‌های WebAssembly در یک محیط سندباکس شده اجرا می‌شوند که آن‌ها را به‌ خوبی از حافظه و کد سیستم میزبان جدا می‌کند. این ویژگی بسیاری از حملات را در محیط‌های مرورگر یا رایانش ابری خنثی می‌کند.

از سوی دیگر، حفاظت از حافظهٔ داخلی خود برنامه‌های WebAssembly بسیار محدود است ‎[8, 18, 30]‎، حتی در مقایسه با باینری‌های بومی. از آنجا که پشتهٔ عملوند (operand stack) تنها مقادیر اولیه (primitive) را ذخیره می‌کند، حافظهٔ خطی (linear memory) باید برای نگهداری موارد زیر استفاده شود:

  • تمام مقادیر غیرابتدایی روی پشته
  • داده‌های ایستا (static data)
  • Heap

کامپایلرهای فعلی هیچ‌گونه قناری (canary) در پشتهٔ قرار گرفته در حافظهٔ خطی درج نمی‌کنند و همچنین هیچ مکانیزم حفاظتی دیگری برای تشخیص سرریز بافر در زمان اجرا ارائه نمی‌دهند. این مسئله با این واقعیت تشدید می‌شود که هیچ صفحهٔ محافظ (guard page) میان نواحی مختلف حافظه وجود ندارد؛ در نتیجه برای مثال یک سرریز بافر روی پشته می‌تواند از داده‌های ایستا عبور کرده و به داده‌های موجود در هیپ برسد.

همچنین هیچ راهی برای علامت‌گذاری بخش‌هایی از حافظهٔ خطی به‌ عنوان فقط‌خواندنی (read-only) وجود ندارد؛ این حافظه در همه‌ جا قابل نوشتن است. به دلیل این محدودیت‌ها، برنامه‌های WebAssembly در عمل قابل بهره‌برداری (exploitable) هستند و ممکن است منجر به Cross-Site Scripting، اجرای کد از راه دور (Remote Code Execution) و سایر رفتارهای مخرب شوند ‎[30]‎. این مسئله ضرورت توسعهٔ ابزارهایی را نشان می‌دهد که بتوانند آسیب‌پذیری‌های مرتبط با حافظه در باینری‌های WebAssembly را کشف کنند و همچنین اکسپلویت‌ها را در زمان اجرا کاهش دهند.

۳. نمای کلی و مثال انگیزشی (Overview and Motivating Example)

رویکرد ما از دو مؤلفهٔ اصلی تشکیل شده است، همان‌گونه که در شکل ۱ نشان داده شده است.

نخست، ما یک تکنیک جدید ابزاربندی مبتنی بر باینری برای درج قناری (canary) ارائه می‌کنیم (بخش ۴) که با افزودن stack canary و heap canary برنامه‌های WebAssembly را سخت‌سازی (hardening) می‌کند.

دوم، یک فازر مبتنی بر باینری برای WebAssembly ارائه می‌دهیم (بخش ۵). این فازر چندین مؤلفه را با یکدیگر یکپارچه می‌کند تا یک فازر سرتاسری کارآمد و مؤثر ایجاد شود، از جمله:

  • یک ابزاربندی جدید برای جمع‌آوری اطلاعات پوشش کد مستقیماً از باینری‌های WebAssembly
  • یک ماشین مجازی WebAssembly
  • و قابلیت‌های تولید ورودی ابزار AFL

در ادامهٔ این بخش، رویکرد ما با یک مثال انگیزشی توضیح داده می‌شود و بخش‌های بعدی جزئیات بیشتری را ارائه می‌کنند.

مثال. برنامهٔ نشان ‌داده ‌شده در شکل ۳ دارای یک سرریز بافر پشته (stack buffer overflow) است (خط ۳) که می‌تواند با ورودی‌های مناسب فعال شود (خطوط ۱۰ و ۳). به دلیل تفاوت در کامپایلرها، کتابخانه‌های سیستمی و مکانیزم‌های حفاظتی، این آسیب‌پذیری زمانی که برنامه برای یک معماری بومی مدرن مانند x86-64 کامپایل شود قابل بهره‌برداری نیست، اما هنگامی که به WebAssembly کامپایل شود قابل بهره‌برداری خواهد بود ‎[30]‎.

شکل‌های 4a و 4b  چیدمان پشتهٔ برنامه را هنگام اجرا به‌ صورت یک باینری بومی (کامپایل‌شده با GCC) و یک باینری WebAssembly (کامپایل‌شده با Emscripten) نشان می‌دهند. از آنجا که متغیرهای input1 و input2 در این دو حالت با ترتیب متفاوتی روی پشته ذخیره می‌شوند، یک مهاجم با ایجاد سرریز در input1 نمی‌تواند رفتار برنامه را در باینری بومی تغییر دهد، اما در نسخهٔ WebAssembly قادر به انجام این کار خواهد بود.

بنابراین فازینگ باینری WebAssembly ــ و نه صرفاً باینری بومی کامپایل‌شده از همان کد منبع ــ اهمیت زیادی دارد.

ابزاربندی مبتنی بر قناری (Canary instrumentation). برای شناسایی اجراهایی که از آسیب‌پذیری‌هایی مانند مثال بالا سوءاستفاده می‌کنند، ما یک تکنیک ابزاربندی مبتنی بر باینری ارائه می‌کنیم که با افزودن stack canary و heap canary مکانیزم‌های حفاظتی لازم را فراهم می‌کند.

این رویکرد، هر تابع در فایل باینری را با کدی تجهیز (ابزاربندی) می‌کند که یک قناری را هنگام ورود به فریم پشته (stack frame) وارد می‌کند و هنگام خروج از تابع، آن را بررسی می‌کند. علاوه بر سرریزهای پشته، این ابزاربندی نقض‌های حافظه در هیپ را نیز شناسایی می‌کند؛ به این صورت که بلوک‌های حافظهٔ هیپ با قناری‌ها احاطه می‌شوند. شکل 4c نمونه‌ای از قناری درج‌ شده در پشتهٔ برنامهٔ را نشان می‌دهد.

در صورتی که یک حمله با نوشتن داده فراتر از بافر انجام شود، مقدار قناری (canary) بازنویسی خواهد شد و باینری ابزاربندی‌ شده این وضعیت را تشخیص داده و اجرای برنامه را متوقف می‌کند.

				
					void vulnerable() {
	char input1[8];
	scanf("%16s", input1); // Buffer overflow!
	char input2[8];
	scanf("%8s", input2);
	/* more code... */ }
int read_int() { int i; scanf("%d", &i); return i; }
int main(int argc, char** argv) {
	char data[10] = "some data";
	if (read_int() == 42) // Input, figured out by fuzzer.
		vulnerable();
	if (strcmp(data, "some data") == 0)
		puts("equal");
	else puts("not equal"); }
				
			

شکل ۳: نمونه برنامه دارای آسیب‌پذیری (ساده ‌شده)

ابزاربندی مبتنی بر قناری دو هدف اصلی را دنبال می‌کند که در شکل ۱ در بخش‌های A و B مشخص شده‌اند:

A) هدف اصلی که در این مقاله بررسی می‌شود، استفاده از آن به‌عنوان اوراکل در فرآیند فازینگ است. اگر فازر موفق شود ورودی‌ای تولید کند که باعث سرریز حافظه شود (برای مثال در متغیر input1 در مثال فوق)، این مسئله ممکن است بدون ایجاد کرش تشخیص داده نشود. مشابه بررسی‌های پویای خرابی حافظه در برنامه‌های بومی ‎[16, 45, 48]‎، ابزاربندی مبتنی بر stack و heap canary در رویکرد ما یک اوراکل دقیق فراهم می‌کند که خرابی‌های حافظه مشاهده‌شده در طول اجرا را گزارش می‌دهد.

B) علاوه بر فازینگ، این ابزاربندی می‌تواند به‌عنوان یک تکنیک سخت‌سازی برای باینری‌هایی که در محیط عملیاتی اجرا می‌شوند نیز مورد استفاده قرار گیرد. این مکانیزم با تشخیص سرریزها در زمان اجرا، برنامه را متوقف کرده و در نتیجه از بهره‌برداری موفق از آسیب‌پذیری جلوگیری می‌کند. همان‌گونه که در ارزیابی خود نشان می‌دهیم، این مکانیزم حفاظتی با سربار اجرایی اندک همراه است و می‌تواند بر روی باینری‌های بزرگ و واقعی که از زبان‌های C++، C و Rust کامپایل شده‌اند نیز اعمال شود.

WebAssembly فازینگ. دومین مؤلفهٔ اصلی رویکرد ما خود فازر است. ما از یک رویکرد فازینگ جعبه‌ خاکستری مبتنی بر فازر شناخته ‌شدهٔ AFL و سازوکار تولید ورودی آزمودهٔ آن استفاده می‌کنیم. روند کلی همانند فازینگ جعبه‌ خاکستری ( greybox fuzzing) استاندارد است: با شروع از یک یا چند بذر اولیه (seed input)، برنامه به‌طور مکرر اجرا می‌شود و بازخورد پوشش کد جمع‌آوری می‌گردد؛ این بازخورد برای جهت‌دهی به جهش (mutation) ورودی‌ها تا زمانی که یک توقف ناگهانی (Crash) رخ دهد استفاده می‌شود.

در مثال برنامه، الگوریتم تولید ورودی AFL در نهایت تشخیص می‌دهد که ورودی باید با رشتهٔ “42” آغاز شود (خط ۹) تا رفتار بیشتری در تابع آسیب پذیر کاوش شود. با این حال، در این تابع AFL در حالت بومی قادر به شناسایی آسیب‌پذیری نیست، زیرا چیدمان پشته در باینری بومی و باینری WebAssembly متفاوت است؛ در حالی که Fuzzm پس از چند دقیقه فازینگ یک ورودی کرش‌کننده پیدا می‌کند.

به‌کارگیری فازینگ جعبه‌ خاکستری به سبک AFL برای WebAssembly به دو دلیل غیرساده است. نخست آن‌ که فازر به اطلاعات پوشش کد نیاز دارد، در حالی که AFL بومی این اطلاعات را با درج کد در هنگام کامپایل از سورس به دست می‌آورد. اما هدف ما فازینگ باینری‌های WebAssembly بدون نیاز به دسترسی به کد منبع است. از این رو، ما یک تکنیک جدید ابزاربندی صرفاً باینری ارائه می‌دهیم که اطلاعات پوشش سازگار با AFL را از باینری‌های WebAssembly استخراج می‌کند.

دوم آن‌ که برای عملی بودن، فازرها معمولاً باید صدها بار در ثانیه برنامه را اجرا کنند. ما مجموعه‌ای از بهینه‌سازی‌ها و تطبیق‌های خاص WebAssembly برای AFL ارائه می‌کنیم که دستیابی به چنین سطحی از کارایی را ممکن می‌سازد

فازینگ - فازم - fuzzm
شکل ۴: لایه‌های پشته برنامه‌ی نمونه.

۴.  سخت‌سازی برنامه‌های WebAssembly با ابزاربندی قناری (Canary) در سطح باینری

فقدان حافظهٔ مجازی، مکانیزم‌های حفاظت صفحه (page protections) یا مکانیزم‌های حفاظتی درج‌شده توسط کامپایلر ‎[14]‎ باعث می‌شود برنامه‌های WebAssembly در مقایسه با برنامه‌های بومی در برابر سرریز بافر آسیب‌پذیرتر باشند. این مسئله می‌تواند پیامدهای غیرمنتظره‌ای داشته باشد؛ از جمله بازنویسی داده‌هایی که ظاهراً ثابت هستند یا سرریز از پشته به هیپ ‎[30]‎.

اگرچه حافظهٔ خطی بخشی اساسی از زبان WebAssembly است و قابل تغییر نیست، اما سرریز مثبت و منفی (overflows and underflows) در پشته (stack) و هیپ (heap) باید در زمان اجرا شناسایی شوند. برای این منظور، ما یک ابزاربندی ایستا ارائه می‌دهیم که قناری پشته و هیپ (stack canary و heap canary) را در باینری‌های WebAssembly درج می‌کند.

مشابه کارهای پیشین در مورد قناری‌های پشته ‎[19, 45]‎ یا هیپ ‎[42, 48]‎ در برنامه‌های بومی، ایدهٔ اصلی این است که بلوک‌های حافظه با مقدار ویژه‌ای به نام قناری احاطه شوند و سپس بررسی شود که آیا این مقدار بازنویسی شده است یا خیر؛ برای مثال پیش از آزادسازی حافظه یا هنگام بازگشت از یک تابع.

رویکرد ما از سه جهت با کارهای پیشین متفاوت است: اول، تا آنجا که ما می‌دانیم، ما اولین کسی هستیم که یک محافظت مبتنی بر قناری برای WebAssembly ارائه می‌دهیم. کامپایلرهای فعلی قناری‌ها را پیاده‌سازی نمی‌کنند، بنابراین بسیاری از باینری‌های WebAssembly موجود به طور بالقوه آسیب‌پذیر هستند. دوم، برخلاف مثلاً قناری‌های درج شده توسط کامپایلر یا AddressSanitizer محبوب [50]، رویکرد ما به کد منبع نیاز ندارد، بلکه باینری‌های WebAssembly را مستقیماً ابزارسازی می‌کند. چنین قابلیتی به ما این امکان را می‌دهد تا باینری‌های موجود را به صورت گذشته‌نگر مقاوم کنیم.

در نهایت، برخلاف تکنیک‌های مبتنی بر باینری برای برنامه‌های بومی، مانند Valgrind [41] یا Intel Pin [33]، رویکرد ما از ابزاربندی ایستای قابل اعتماد استفاده می‌کند. در نتیجه، سربار زمان اجرای فایل‌های دودویی ابزاربندی شده ما ناچیز است، در حالی که سربارهای بسیار بالاتری که توسط ابزار دقیق پویا ایجاد می‌شوند، وجود دارد.

   ۴.۱ قناری‌های پشته (Stack Canaries)

برای شناسایی سرریز بافرهایی که از محدودهٔ فریم پشته (stack frame) فعلی عبور می‌کنند، Fuzzm سه تبدیل را روی هر تابع در باینری اعمال می‌کند؛ همان‌گونه که در شکل ۵ نشان داده شده است.

شکل ۵: نمودارهای CFG یک تابع WebAssembly قبل و بعد از ابزاربندی دقیق. مستطیل‌ها نشان دهنده زیرگراف‌ها هستند.

ابتدا، یک بخش (قطعه) آغازین (preamble) وارد می‌کنیم که مقدار قناری را به پشتهٔ حافظهٔ خطی تزریق می‌کند. سپس، بدنه اصلی تابع را در یک بلوک جدید قرار می‌دهیم و تمام دستورهای بازگشتی (return) را طوری بازنویسی می‌کنیم که به انتهای بلوک مذکور پرش کنند، در نتیجه تابع دارای یک نقطهٔ خروج یکتا می‌شود. در نهایت یک بخش پایانی (postamble) برای اعتبارسنجی قناری به انتهای تابع افزوده می‌شود.

الگوریتم ۱- ابزاربندی دقیق قناری پشته‌:

				
					1: procedure INSTRUMENTFUNCTION(body)
2: canary ← eight randomly generated bytes
3: body ← INJECTCANARY(canary) ++ body
4: body ← block ++ body ++ end . Wrap original body
5: depth ← 0
6: for instr in body do
7: if OPENSBLOCK(instr) then . Track block depth
8: depth ← depth + 1
9: else if CLOSESBLOCK(instr) then
10: depth ← depth − 1
11: if instr = return then . Redirect returns
12: instr ← br (depth − 1)
13: body ← body ++ VALIDATECANARY(canary)
				
			

الگوریتم ۱ ابزاربندی یک تابع را با جزئیات بیشتری نشان می‌دهد. در خط ۲ یک مقدار تصادفی قناری (canary)‌ هشت‌ بایتی تولید می‌شود. از مقدار هشت‌ بایتی استفاده می‌کنیم زیرا بزرگ‌ترین مقدار اولیه‌ای است که WebAssembly پشتیبانی می‌کند، و استفاده از مقدار تصادفی احتمال عدم شناسایی سرریزهایی که به‌طور تصادفی با مقدار قناری یکسان شوند را کاهش می‌دهد.

خط ۳ کد تزریق قناری (canary)‌ را در ابتدای بدنهٔ تابع قرار می‌دهد. قالبی از این کد تزریق‌شده در شکل ۶ نشان داده شده است. این کد با مقدار قناری <CANARY> و همچنین <SP> که اندیس متغیر سراسری WebAssembly نگهدارندهٔ اشاره‌گر پشته (stack pointer) است پارامتردهی می‌شود. در تمام برنامه‌های WASI، اشاره‌گر پشته اولین متغیر سراسری است؛ بنابراین مقدار <SP> برابر ۰ است. برای برنامه‌های غیر-WASI می‌توان از روش‌های ابتکاری برای شناسایی اشاره‌گر پشته استفاده کرد [24].

این کد در حافظهٔ خطی پشته فضایی را برای قناری‌ها رزرو می‌کند (خطوط ۱ تا ۴، به اندازهٔ ۱۶ بایت به دلیل هم‌ترازی پشته) و سپس مقدار پشته را در آن محل ذخیره می‌کند؛ یعنی در ابتدای فریم پشته (stack frame) (خطوط ۵ تا ۷). از آنجا که یک تابع WebAssembly ممکن است از چندین نقطه بازگردد، این پرسش مطرح می‌شود که کد اعتبارسنجی قناری در کجا قرار گیرد. برخلاف کد بومی، در WebAssembly هیچ پایان تابع واحدی وجود ندارد که پشته را پاک کرده و به فراخوانی کننده بازگردد.

یک راهکار این است که هر دستور return به‌طور جداگانه ابزاربندی شود، اما این کار اندازهٔ کد را به‌طور قابل‌توجهی افزایش می‌دهد. در مقابل، ابتدا تابع را بازنویسی می‌کنیم تا تنها یک نقطهٔ بازگشت داشته باشد و سپس کد اعتبارسنجی را در همان نقطه درج می‌کنیم. برای این کار:

  • کل تابع در یک بلوک WebAssembly جدید قرار داده می‌شود (خط ۴ در الگوریتم ۱).
  • سپس هر دستور return در بدنهٔ تابع با پرش به انتهای بلوک جدید جایگزین می‌شود (خطوط ۶ تا ۱۲) بدون آن‌که معناشناسی برنامه تغییر کند.

برای انجام این کار، متغیر depth (خط ۵) باید تعداد بلوک‌های تو در تو پیرامون دستور فعلی را ردیابی کند. در نهایت، در خط ۱۳ یک بخش پایانی (postamble) برای اعتبارسنجی قناری پیش از بازگشت تابع افزوده می‌شود. قالب این کد در شکل ۷ نشان داده شده است. از آنجا که همهٔ دستورهای return به انتهای بلوک هدایت شده‌اند، مقدار بازگشتی تابع در لحظهٔ اجرای بخش پایانی (postamble) در بالای پشتهٔ عملوند WebAssembly قرار دارد.

کد اعتبارسنجی canary در خطوط ۱ تا ۶ شکل ۷ این مقدار بازگشتی را تغییر نمی‌دهد و در نتیجه نیازی به ایجاد یک متغیر محلی جدید نیست. در این مرحله stack pointer به پایهٔ پشتهٔ تابع اصلی اشاره می‌کند که پس از درج کد ما همان محل ذخیرهٔ قناری است.

این مقدار از حافظه خوانده شده و با مقدار صحیح قناری مقایسه می‌شود. اگر این دو مقدار متفاوت باشند، یک unreachable trap فعال می‌شود که اجرای برنامه را متوقف کرده و از بهره‌برداری احتمالی جلوگیری می‌کند. محل وقوع این trap همچنین نشان می‌دهد که یک سرریز بافر از مرز stack frame تابع عبور کرده است.

اگر مقدار قناری دست‌ نخورده باشد، خطوط ۹ تا ۱۲ با تنظیم اشاره‌گر پشته (stack pointer) فضای اختصاص‌ داده‌ شده به قناری را آزاد می‌کنند. در نهایت، در خط ۱۳ تنها مقدار باقی‌مانده روی پشتهٔ عملوند ــ یعنی مقدار بازگشتی تابع ــ بازگردانده می‌شود.

				
					global.get <SP> ;; Reserve stack space for canary value.
i32.const 16 ;; WASI has 16-byte stack alignment.
i32.sub
global.set <SP>
global.get <SP> ;; Store canary at beginning of stack frame.
i64.const <CANARY>
i64.store
				
			

شکل ۶: الگوی INJECTCANARY برای الگوریتم ۱

				
					block ;; Original return value is at top of operand stack.
	global.get <SP> ;; Compare canary value against reference.
	i64.load
	i64.const <CANARY>
	i64.eq
	br_if 0 ;; Jump out of block if correct.
	unreachable ;; Otherwise: Overflow detected!
end
global.get <SP> ;; Adjust stack pointer.
i32.const 16
i32.add
global.set <SP>
return
				
			

شکل ۷: الگوی VALIDATECANARY برای الگوریتم ۱

۴.۲ قناری‌های هیپ (Heap Canaries)

شناسایی و جلوگیری از نقض‌های حافظه در هیپ (heap) نیز اهمیت زیادی دارد. در WebAssembly این موضوع به‌طور ویژه‌ای حیاتی است، زیرا باینری‌ها اغلب همراه با تخصیص‌دهنده‌های حافظه (allocators) ارائه می‌شوند که برای کاهش اندازهٔ کد بهینه‌سازی شده‌اند و در نتیجه فاقد ویژگی‌های امنیتی مانند unlinking ایمن (safe unlinking) هستند [30].

 
 

 

 

fuzzm
شکل ۸: بخش‌ها و تکه‌های هیپ، قبل و بعد از ابزاربندی.

برای توضیح این مشکل، شکل ۸a چیدمان معمول یک قطعه حافظهٔ هیپ (heap chunk) را نشان می‌دهد؛ یعنی بخشی از حافظه که به‌ صورت پویا توسط توابعی مانند ‎malloc‎ تخصیص داده می‌شود. بخش payload (پیلود) جایی است که کاربر داده‌ها را در آن می‌خواند یا می‌نویسد. در مقابل، metadata (فراداده) قبل یا بعد از payload قرار دارد و توسط تخصیص‌ دهندهٔ حافظه برای مدیریت و ثبت اطلاعات استفاده می‌شود.

اگر مهاجم بتواند با سرریز مثبت و منفی (overflow یا underflow) بافر موجود در payload، به داده‌های مجاور در metadata بنویسد، این می‌تواند منجر به یک قابلیت خطرناک «نوشتن دلخواه» (arbitrary write) شود که بسیار قدرتمندتر از یک سرریز خطی (linear overflow) ساده است [5, 27].

برای شناسایی چنین نقض‌هایی در هیپ، ابزار Fuzzm توابع تخصیص و آزادسازی حافظه را در باینری ابزاربندی می‌کند. این ابزار با تزریق مقادیر قناری (canary values) قبل و بعد از payload، همان‌طور که در شکل ۸b نشان داده شده، امکان تشخیص هر دو نوع سرریز مثبت و منفی (overflow و underflow) را فراهم می‌کند. این قناری‌ها توسط نسخه‌های ابزاربندی شدهٔ توابع تخصیص (بخش ۴.۲.۱) در هیپ قرار داده می‌شوند و توسط نسخه‌های ابزاربندی شدهٔ توابع آزادسازی (بخش ۴.۲.۲) بررسی می‌گردند.

      ۴.۲.۱ درج قناری در هنگام تخصیص حافظه در هیپ (Insert Canaries on Heap Allocation)

قناری‌های هیپ (Heap canaries) با ابزاربندی تمام توابعی که مستقیماً بلوک‌های حافظهٔ هیپ را تخصیص می‌دهند درج می‌شوند. در پیاده‌سازی فعلی، ما توابع تخصیص حافظه در کتابخانهٔ استاندارد C را ابزاربندی می‌کنیم؛ یعنی:

  • malloc
  • calloc
  • realloc

سایر توابعی که به‌طور غیرمستقیم این توابع سطح پایین libc را فراخوانی می‌کنند نیز از این مکانیزم حفاظتی بهره‌مند می‌شوند؛ همان‌گونه که برای مثال در operator new در کتابخانهٔ استاندارد ++C رایج است.

ابزاربندی ما در دو نقطه از توابع تخصیص حافظه کد درج می‌کند:

  1. یک preamble در ابتدای تابع
  2. یک postamble در انتهای تابع

همان‌گونه که در الگوریتم ۲ نشان داده شده است.

کد افزوده‌ شده سه هدف اصلی دارد:

  1. افزایش اندازهٔ تخصیص حافظه برای جا دادن قناری‌ها (خط ۵)
  2. نوشتن مقادیر قناری در حافظه (خط ۹)
  3. تنظیم اشاره‌گر دادهٔ بازگشتی پیش از تحویل آن به برنامهٔ کاربر، به‌طوری که به پیلود (payload) جابه‌جا شده اشاره کند (خط ۱۰)

علاوه بر این، دو متغیر محلی جدید به تابع اضافه می‌کنیم (خطوط ۲ و ۶) تا داده‌هایی را ذخیره کنند (خطوط ۴ و ۸) که بعداً توسط کد درج‌شده استفاده خواهند شد.

به‌طور مؤثر، کد قناری ما میان allocator اصلی و کد کاربر که درخواست تخصیص حافظه می‌دهد قرار می‌گیرد و باید برای هر دو شفاف باشد.

  • از دید allocator، پیلود کل ناحیه پس از metadata در شکل 8b است که شامل قناری های درج‌شده نیز می‌شود.
  • از دید کد کاربر، پیلود تنها ناحیهٔ بین قناری‌ها است که اندازهٔ آن دقیقاً برابر با اندازهٔ درخواست‌شدهٔ اولیه است.

در نتیجه، هیچ‌یک از این دو بخش از داده‌های اضافی درج‌شده توسط ابزاربندی ما آگاه نیستند.

جزئیات. در بخش آغازین (preamble) ابتدا اندازهٔ درخواستی اولیهٔ تخصیص حافظه بازیابی شده و برای استفادهٔ بعدی در یک متغیر محلی ذخیره می‌شود (خط ۴)، سپس ۲۰ بایت به آن افزوده می‌شود (خط ۵).

الگوریتم ۲ – ابزار دقیق توابع تخصیص Heap:

				
					1: procedure INSTRUMENTALLOCFUNCTION(f)
2: localreq_size ← ADDFRESHLOCAL(f)
3: f.body ← . Insert preamble
4: SAVEALLOCREQUESTSIZE(f, localreq_size) ++
5: INCREASEALLOCSIZE(f, localreq_size) ++ f.body
6: localdata_ptr ← ADDFRESHLOCAL(f)
7: f.body ← f.body ++ . Insert postamble
8: SAVEDATAPOINTER(localdata_ptr) ++
9: WRITESIZEANDCANARIES(localreq_size, localdata_ptr) ++
10: ADJUSTDATAPOINTER(localdata_ptr)
				
			

این ۲۰ بایت اضافی شامل دو قناری هشت‌بایتی و یک فیلد اندازهٔ چهار بایتی است. فیلد اندازه برای کد بررسی (بخش 4.2.2) مورد نیاز است. دستورالعمل‌های دقیق بخش آغازین (preamble) بسته به تابع تخصیص حافظه متفاوت هستند. برای مثال در malloc(size_t) و *realloc(void, size_t)**، دستور SAVEALLOCREQUESTSIZE آرگومان اول (یا دوم) تابع را با local.get بازیابی کرده و در یک متغیر محلی ذخیره می‌کند.

دستور INCREASEALLOCSIZE نیز با انجام یک جمع ساده مقدار آرگومان را به اندازهٔ جدید تغییر می‌دهد. ابزاربندی صحیح تابع calloc(size_t nitems, size_t item_size) کمی پیچیده‌تر است، زیرا اندازهٔ تخصیص برابر حاصل‌ضرب دو آرگومان است و هیچ‌یک الزاماً مضربی از ۲۰ بایت نیستند. بنابراین بخش آغازین (preamble) درج‌شده آرگومان‌ها را به‌گونه‌ای تغییر می‌دهد که:

 
 

 

 

WebAssembly - فازینگ - fuzzm

علاوه بر این، دستور INCREASEALLOCSIZE بررسی می‌کند که عبارت دوم منجر به سرریز عدد صحیح (integer overflow) نشود تا اطمینان حاصل شود که ابزاربندی هرگز خطایی در برنامه ایجاد نمی‌کند.

پس از اجرای بخش آغازین (preamble)، کد اصلی تابع تخصیص حافظه عملیات تخصیص حافظه عادی را انجام می‌دهد. سپس بخش پایانی (postamble) ما اجرا می‌شود، که در شکل ۹ نشان داده شده است.

از آنجا که قطعه پایانی پس از بدنهٔ اصلی تابع تخصیص اجرا می‌شود، عنصر بالای پشتهٔ عملوند همان مقدار بازگشتی اصلی است، یعنی اشاره‌گر به حافظهٔ تازه تخصیص داده‌ شده. این مقدار در یک متغیر محلی ذخیره می‌شود (خط ۱).

سپس اندازهٔ بلوک و قناری سرریز منفی (underflow canary) پیش از پیلود (payload) ذخیره می‌شوند (خطوط ۲ تا ۷) قناری سرریز (overflow canary) نیز پس از پیلود (payload) قرار داده می‌شود (خطوط ۸ تا ۱۲).

در نهایت، اشاره‌گر داده (data pointer) گرفته شده و به گونه‌ای تنظیم می‌شود که به‌ بعد از قناری سرریز منفی اشاره کند (خطوط ۱۳ تا ۱۵). این مقدار نهایتاً به کد فراخوان بازگردانده می‌شود.

نتیجهٔ این ابزاربندی این است که توابع تخصیص حافظه، بلوک‌هایی را ایجاد می‌کنند که مطابق شکل ۸b هستند.

      ۴.۲.۲ اعتبارسنجی قناری‌ها هنگام آزادسازی حافظهٔ هیپ (Check Canaries on Heap Deallocation)

 Fuzzm هر زمان که یک بخش از هیپ آزاد (deallocate) می‌شود، بررسی می‌کند که آیا قناری‌های هیپ (heap canary) معتبر هستند یا خیر. مشابه توابع تخصیص حافظه، این روش نیز به فهرستی از توابع آزادسازی حافظه نیاز دارد و پیاده‌سازی فعلی ما از توابع موجود در کتابخانه استاندارد C یعنی free و realloc پشتیبانی می‌کند.

اعتبارسنجی قناری‌های هیپ (heap canary) توسط کدی انجام می‌شود که در شکل ۱۰ نشان داده شده است و این کد در ابتدای هر تابع آزادسازی حافظه درج می‌شود. آرگومان این تابع یک اشاره‌گر به پیلود بلوک هیپ است. برای اینکه وجود کاناری‌ها برای تابع آزادسازی شفاف باشد، در ابتدا باید مقدار این اشاره‌گر کمی کاهش داده شود (خطوط ۱ تا ۴).

در خطوط ۵ تا ۱۲ صحت قناری سرریز منفی (underflow canary) بررسی می‌شود و در خطوط ۱۳ تا ۲۳ صحت قناری‌های سرریز (overflow canary) اعتبارسنجی می‌شود. این روش از اندازه‌ای که هنگام تزریق قناری ذخیره شده است استفاده می‌کند تا موقعیت قناری‌های سرریز (overflow canary) را محاسبه کند.

در نتیجه، این مکانیزم تمامی سرریزهای مثبت و منفی (overflow و underflow) در هیپ را تشخیص می‌دهد و به‌طور شفاف در زمان اجرا جلوی بهره‌برداری‌های احتمالی را می‌گیرد، بدون آن‌که کد اصلی تابع آزادسازی یا تخصیص حافظه نیاز به تغییر داشته باشد.

زمان بررسی قناری‌های هیپ یک موازنه میان کارایی، پیچیدگی ابزاربندی و احتمال شناسایی سرریز بافرها است. Fuzzm این بررسی را هنگام آزادسازی حافظه (deallocation) انجام می‌دهد، که هزینهٔ اجرایی پایینی دارد، زیرا هر قناری حداکثر یک بار بررسی می‌شود. اما این روش یک نقطه ضعف دارد: سرریز در بلوک‌هایی که هرگز آزاد نمی‌شوند، شناسایی نمی‌شود.

روش‌های دیگری نیز پیشنهاد شده‌اند که تهاجمی تر هستند؛ برای مثال بررسی قناری ها در هر خواندن یا نوشتن حافظه ‎[50]‎ و اعتبارسنجی قناری ها در هر syscall ‎[42]‎. در حالی که این رویکردها ممکن است حملات بیشتری در محیط عملیاتی شناسایی کنند، سربار زمان اجرای بالاتری ایجاد می‌کنند، که آن‌ها را کمتر مناسب برای سخت‌سازی باینری و فازینگ می‌کند.

				
					local.set <DATA_PTR> ;; Save pointer returned by allocator.
local.get <DATA_PTR> ;; \
local.get <REQ_SIZE> ;; | Write requested allocation size
i32.store ;; / at the beginning (data_ptr).
local.get <DATA_PTR> ;; \
i64.const <CANARY> ;; | Write underflow canary
i64.store offset=4 ;; / at data_ptr + 4.
local.get <DATA_PTR> ;; \
local.get <REQ_SIZE> ;; | Write overflow canary
i32.add ;; | at data_ptr + size + 12.
i64.const <CANARY> ;; |
i64.store offset=12 ;; /
local.get <DATA_PTR> ;; \
i32.const 12 ;; | Adjust returned pointer to payload.
i32.add ;; /
				
			

شکل ۹: قالبِ بخش پایانیِ افزوده‌ شده در الگوریتم ۲

				
					local.get <PARAM> ;; The argument passed to, e.g., free().
i32.const 12 ;; Adjust the pointer before passing
i32.sub ;; it to the allocator.
local.set <PARAM>
block ;; Check underflow canary.
	local.get <PARAM>
	i64.load offset=4
	i64.const <CANARY>
	i64.eq
	br_if 0
	unreachable ;; Underflow detected!
end
block ;; Check overflow canary.
	local.get <PARAM> ;; \ Load payload size from our own
	i32.load ;; / metadata at beginning of chunk.
	local.get <PARAM> ;; \
	i32.add ;; | Load overflow canary from
	i64.load offset=12 ;; | data_ptr + size + 12.
	i64.const <CANARY> ;; /
	i64.eq
	br_if 0
	unreachable ;; Overflow detected!
end
				
			

شکل ۱۰: بخش آغازین که به توابع تخصیص مجدد هیپ تزریق شده است تا اعتبارسنجی قناری‌های هیپ (heap canaries) را تأیید کند

۵. فازینگ صرفاً باینری برای WebAssembly

این بخش نخستین فازر صرفاً باینری برای WebAssembly را معرفی می‌کند. ما از رویکرد فازینگ جعبه‌خاکستری استفاده کرده و بر اساس چارچوب شناخته‌شدهٔ AFL عمل می‌کنیم، که به ما امکان می‌دهد قابلیت‌های اثبات‌شدهٔ تولید ورودی آن را دوباره به کار بگیریم.

چون AFL معمولاً برای برنامه‌هایی با کد منبع در دسترس طراحی شده و از WebAssembly پشتیبانی نمی‌کند، دو چالش اصلی وجود دارد:

  1. جمع‌آوری اطلاعات پوشش سازگار با AFL هنگام اجرای یک برنامهٔ
  2. ادغام اجرای WebAssembly در یک VM با چارچوب موجود AFL به‌گونه‌ای که بتوان صدها اجرای برنامه در ثانیه داشت، سطحی از کارایی که AFL برای کدهای کامپایل‌شدهٔ بومی ارائه می‌دهد.

بخش ۵.۱ توضیح می‌دهد که Fuzzm چگونه چالش اول را با استفاده از ابزاربندی ایستای باینری‌های WebAssembly حل می‌کند و بخش ۵.۲ به چالش دوم می‌پردازد.

   ۵.۱ ابزاربندی پوشش کد (Coverage Instrumentation)

فازینگ جعبه‌خاکستری (Greybox fuzzing) مؤثر است زیرا بر بازخورد سبک (lightweight) در زمان اجرا برای جهت‌دهی فازر تکیه دارد. برای جمع‌آوری این بازخورد، AFL بومی برنامه‌ها را از سورس کامپایل می‌کند و کدی برای ردیابی پوشش تقریبی مسیرها درج می‌کند ‎[9]‎ که در یک آرایهٔ trace bits ذخیره می‌شود.

برخلاف AFL بومی، ما باینری‌های WebAssembly را بدون دسترسی به کد منبع فاز می‌کنیم و بنابراین ابزاربندی در زمان کامپایل ممکن نیست. AFL حالت QEMU برای ابزاربندی پویا ارائه می‌دهد، اما این روش سربار بالایی دارد و طبیعتاً وابسته به معماری است و هیچ پیاده‌سازی برای WebAssembly ندارد.

در عوض، Fuzzm با ابزاربندی ایستای باینری پوشش را جمع‌آوری می‌کند، به این صورت که کد در تمامی شاخه‌ها درج می‌شود تا اطلاعات پوشش سازگار با AFL استخراج شود. بنابراین، هزینهٔ ابزاربندی یک بار و همیشگی است.

به عنوان پیش‌نیازی برای ابزار دقیق، این رویکرد تمام شاخه‌ها را تعیین می‌کند. جریان کنترل ساختاریافته WebAssembly، می‌تواند Fuzzm را قادر سازد تا تمام نقاط شاخه‌بندی در یک برنامه را به طور دقیق شناسایی کند. الگوریتم ۳ این مرحله را خلاصه می‌کند و هر تابع از یک فایل باینری را پیمایش کرده و دستورالعمل‌هایی را که مربوط به شاخه‌ها هستند، علامت‌گذاری می‌کند.

الگوریتم ۳ این مرحله را خلاصه می‌کند: هر تابع در باینری مرور شده و دستورات متناظر با شاخه‌ها علامت‌گذاری می‌شوند. این شامل br_if (خط ۱۲) و همچنین if، else و loop (خط ۹) می‌شود، زیرا آن‌ها نیز شاخه محسوب می‌شوند.

علاوه بر این، الگوریتم عمق (depth) هر دستور را دنبال می‌کند:

  • هنگام دستورهای block، if و loop مقدار depth افزایش می‌یابد (خط ۷)
  • هنگام دستور end مقدار depth کاهش می‌یابد (خط ۱۹)

پیگیری عمق برای محاسبهٔ هدف شاخه‌ها ضروری است. هرگاه الگوریتم با یک break شرطی مواجه شود (مثلاً br_if در خط ۱۰ یا br_table در خط ۱۳)، عمق هدف شاخه به مجموعهٔ targets اضافه می‌شود. در هر دستور end، الگوریتم بررسی می‌کند که آیا depth آن دستور در targets است (خط ۱۵). اگر چنین باشد، آن end هدف یک شاخه است و بنابراین برای ابزاربندی علامت‌گذاری می‌شود (خط ۱۷).

علاوه بر دستورات علامت‌گذاری‌شده توسط الگوریتم، Fuzzm ابتدای هر تابع را نیز علامت‌گذاری می‌کند (خط ۲۰) زیرا فراخوانی غیرمستقیم تابع نیز یک شاخه محسوب می‌شود.

الگوریتم ۳ ـ فرایندِ درجِ ابزاربندی پوشش در AFL:

				
					procedure AFL_INSTRUMENT_FUNCTION(f)
	depth ← 0
	targets ← {}
	for instr in f.body do
		if instr ∈ {block, if, else, loop} then
			if instr ∈ {block, if, loop} then
				depth ← depth + 1
			if instr ∈ {if, else, loop} then
				mark(instr)
		else if instr = br_if n then
			targets ← targets ∪ {depth − n}
			mark(instr)
		else if instr = br_table(jmp_targets) then
			targets ← targets ∪ ⋃t∈jmp_targets{t − n}
		else if instr = end then
			if depth ∈ targets then
				mark(instr)
				targets ← targets \ {depth}
			depth ← depth − 1
	mark(f.body[0])
				
			
				
					i32.const <CUR_LOCATION> ;; Id of current branch.
global.get <PREV_LOCATION> ;; Id of previous branch.
i32.xor
global.get <TRACE_BITS>
i32.add
local.tee $l ;; Set local without pop.
local.get l
i32.load8_u ;; Unsigned load of 1 byte.
i32.const 1
i32.add
i32.store8 ;; Store counter in trace_bits.
i32.const <CUR_LOCATION  1> ;; Shift right once.
global.set <PREV_LOCATION>
				
			

شکل 11: ابزار پوشش به سبک AFL در Wasm

با استفاده از نقاط شاخه شناسایی‌شده، Fuzzm کد ابزاربندی را در هر نقطه درج می‌کند. قالب این کد در شکل ۱۱ نشان داده شده است. این ابزاربندی مکانیزم پوشش توصیف‌شده در مستندات AFL را به WebAssembly تطبیق می‌دهد. ایدهٔ اصلی این است که:

  • یک آرایهٔ سراسری شمارنده‌ها (trace bits array) نگهداری شود
  • این آرایه نشان می‌دهد که هر جفت شاخهٔ متوالی چند بار گرفته شده است

هر هدف شاخه یک شناسهٔ تصادفی اختصاص داده می‌شود. هر بار که یک شاخه اجرا شود، کد ابزاربندی اندیسی محاسبه می‌کند که شناسهٔ شاخهٔ فعلی (<CUR_LOCATION>) و شناسهٔ شاخهٔ قبلی (<PREV_LOCATION>) را ترکیب می‌کند (خطوط ۱–۳).

سپس مقدار متناظر در آرایهٔ trace bits افزایش می‌یابد (خطوط ۴–۱۱). برای مقداردهی اولیهٔ این آرایه، Fuzzm کد لازم را در تابع _start باینری که نقطهٔ ورود WASI است، نیز درج می‌کند.

   ۵.۲ ادغام WebAssembly VM و AFL

نسخهٔ بومی AFL به شدت بهینه‌سازی شده است تا در یک بازهٔ زمانی مشخص بیشترین تعداد اجرای ممکن برنامه هدف را انجام دهد. در این بخش، چندین تکنیک نوآورانه معرفی می‌شوند که به Fuzzm اجازه می‌دهند سطح مشابهی از کارایی را برای باینری‌های WebAssembly به دست آورد. پیاده‌سازی ما روی باینری‌های WebAssembly با استفاده از رابط syscall WASI تمرکز دارد، یعنی برنامه‌هایی که روی VMهای سازگار مانند Wasmer یا Wasmtime اجرا می‌شوند.

جلوگیری از راه‌اندازی مجدد  VM. در صورتی که برنامهٔ فاز شده روی یک VM اجرا شود، یکی از روش‌ها، شروع یک نمونهٔ جدید VM برای هر اجرا است. اما این کار زمان زیادی صرف راه‌اندازی VM و کامپایل ماژول به کد بومی می‌کند و از زمان اجرای برنامهٔ هدف بیشتر می‌شود. Fuzzm  اما VM را یک بار راه‌اندازی می‌کند، اجازه می‌دهد VM ماژول WebAssembly هدف را پیش‌کامپایل کند و سپس در طول فرایند فازینگ از آن مجدداً استفاده می‌کند. Fuzzm از API C مربوط به Wasmtime برای کامپایل جداگانه، سپس نمونه‌سازی (instantiate) و نهایتاً اجرای ماژول‌های WebAssembly استفاده می‌کند. برای هر ورودی جدید تولید شده، فازر ماژول WebAssembly که قبلاً به کد بومی کامپایل شده است را نمونه‌سازی می‌کند و سپس تابع _start، یعنی نقطهٔ ورود باینری هدف، را فراخوانی می‌کند.

دسترسی به آرایهٔ trace bits. برای تولید ورودی‌های جدید، AFL نیاز دارد به اطلاعات پوشش (coverage) که در آرایهٔ trace bits  ذخیره شده‌اند دسترسی پیدا کند. در نسخهٔ بومی AFL، برنامهٔ هدف به‌صورت یک زیرفرایند (subprocess) اجرا می‌شود و دسترسی به آرایهٔ trace bits از طریق حافظهٔ اشتراکی (shared memory) انجام می‌گیرد. در مقابل، Fuzzm از این واقعیت بهره می‌برد که محیط سندباکس ماشین مجازی (VM) اجازه می‌دهد برنامهٔ هدف و کد خود AFL یک فضای آدرس واحد (single address space) را به اشتراک بگذارند. برای این منظور، رویکرد ما در زمان ابزاربندی پوشش (coverage instrumentation) یک تابع دسترسی (accessor function) را درون باینری درج می‌کند. این تابع یک اشاره‌گر (pointer) به آرایهٔ trace bits در حافظهٔ خطی (linear memory) برنامهٔ هدف باز می‌گرداند. پس از هر بار اجرای برنامهٔ هدف، Fuzzm این تابع ویژه را فراخوانی می‌کند و با استفاده از Wasmtime C API، مقدار ۶۴ کیلوبایت از حافظهٔ خطی که متناظر با trace bits است را استخراج می‌کند.

شناسایی وقفه‌های ناگهانی. نسخهٔ بومی AFL کرش‌ها را با بررسی سیگنال‌های کشنده (SIGSEGV، SIGKILL، SIGABRT) شناسایی می‌کند. با این حال، WASI سیگنال‌ها را پشتیبانی نمی‌کند، بنابراین Fuzzm از سیستم trap WebAssembly برای تشخیص کرش استفاده می‌کند. برای این منظور، oracleهایی که Fuzzm درج کرده است (بخش ۴)، وقتی overflow یا underflow شناسایی شود، یک unreachable trap ایجاد می‌کنند. علاوه بر این، WebAssembly دارای trapهای داخلی دیگری است که نشان‌دهندهٔ رفتار معیوب برنامه هستند (بخش ۲). هنگام پایان اجرای تابع _start، Fuzzm بررسی می‌کند که آیا پایان توسط trap ایجاد شده است یا خیر و در صورت مثبت بودن، آن را به‌عنوان کرش علامت‌گذاری می‌کند.

متوقف کردن اجرای طولانی‌مدت. ورودی‌های تولیدشدهٔ تصادفی ممکن است اجرای طولانی یا حتی غیرقابل خاتمه ایجاد کنند. در AFL بومی، برنامهٔ فازشده در یک پردازش جداگانه اجرا شده و پس از timeout مشخص، پردازش متوقف می‌شود. اما در Fuzzm، VM WebAssembly و کد تولیدشده در همان پردازش AFL اجرا می‌شوند، بنابراین دو مکانیزم برای متوقف کردن اجرای طولانی طراحی شده است. Soft killing یک مکانیزم نرم که با استفاده از یک thread جداگانه در VM WASI، thread برنامهٔ هدف را پس از یک timeout پویا (توسط AFL تعیین شده) قطع می‌کند. Hard killing اگر برنامهٔ هدف به interrupt پاسخ ندهد، یک مکانیزم دوم VM را پس از timeout طولانی‌تر کاملاً راه‌اندازی مجدد می‌کند. این روش‌ها تضمین می‌کنند که فازینگ WebAssembly به‌صورت سریع و پایدار انجام شود، حتی در مواجهه با ورودی‌های تصادفی طولانی یا بی‌پایان.

۶. ارزیابی (Evaluation)

ما Fuzzm را بر اساس دو مورد کاربردی که در بخش ۳ معرفی کرده‌ایم ارزیابی می‌کنیم. نخست، فازینگ انتهابه‌انتها (end-to-end fuzzing) برای باینری‌های WebAssembly:

پرسش ۱ – اثربخشی : Fuzzm در پوشش مسیرها و یافتن کرش‌ها چقدر مؤثر است؟

پرسش ۲ – استحکام: ابزار دقیق هنگام اعمال بر روی فایل‌های باینری دنیای واقعی چقدر مقاوم است؟

پرسش ۳ – کارایی: فازینگ با Fuzzm چقدر کارآمد است؟

دوم، مقاوم‌سازی فایل‌های باینری برای تولید از طریق قناری‌ها (canary):

پرسش ۴ – اثربخشی: قناری‌های درج شده در جلوگیری از سوءاستفاده‌های نشان داده شده قبلی چقدر مؤثر هستند؟

پرسش ۵ – کارایی: قناری‌ها چقدر سربار تحمیل می‌کنند؟

برای قابلیت تکرارپذیری و تحقیقات آینده، سورس کد، داده‌ها و تمامی نتایج تجربی در آدرس Fuzzm GitHub در دسترس هستند.

   ۶.۱ تنظیمات آزمایشگاه (Experimental Setup)

بنچمارک‌ها. ما از سه مجموعه بنچمارک استفاده می‌کنیم (جدول ۱):

  1. بنچمارک‌های ۱ تا ۷: برنامه‌ها و کتابخانه‌های واقعی که می‌توانند با WASI به WebAssembly کامپایل شوند. نسخه‌های انتخاب‌شده دارای آسیب‌پذیری‌های حافظه شناخته‌شده هستند که یک فازر می‌تواند در شناسایی و رفع آن‌ها کمک کند.
  2. بنچمارک‌های ۸ تا ۱۰: از LAVA-M benchmark [17] انتخاب شده‌اند. برنامهٔ who حذف شد زیرا لیست سیستم فایل‌های نصب‌شده را می‌خواند، عملی که هنوز توسط WASI پشتیبانی نمی‌شود. AFL و به تبع آن Fuzzm معمولاً روی LAVA-M عملکرد ضعیفی دارند زیرا محدودیت‌های چندبایتی باگ‌های LAVA-M را مدیریت نمی‌کنند [47]. همچنین، LAVA-M به‌خاطر عدم نمایندگی باگ‌های واقعی مورد انتقاد است [28]؛ ما ترجیح می‌دادیم از Magma benchmark suite استفاده کنیم، اما تمام بنچمارک‌های Magma ویژگی‌هایی مانند thread، شبکه و long jump دارند که هنوز در WASI پشتیبانی نمی‌شوند.
  3. بنچمارک‌های ۱۱ تا ۲۸: باینری‌های واقعی WebAssembly جمع‌آوری‌شده از وب‌سایت‌ها، GitHub و بسته‌های NPM با استفاده از WasmBench dataset [24]. از بین آن‌ها، ۱۸ باینری بدون خطا در VM Wasmtime انتخاب شدند. این مجموعه شامل برنامه‌های بزرگ مانند SQLite و Clang کامپایل‌شده به WebAssembly و همچنین برنامه‌های کوچک‌تر مانند canonicaljson (فرمت‌کننده JSON)، handlebars-cli  (موتور قالب) و bfi (مفسر) است.

کامپایل. مجموعه‌های اول و دوم با نسخه‌ای از Clang که هدف آن WebAssembly است کامپایل شدند و سپس ابزاربندی‌ها طبق بخش‌های ۴ و ۵ اعمال شدند. برای مقایسه با AFL بومی، بنچمارک‌ها با نسخه AFL از GCC کامپایل شدند. هیچ ابزار مانند AddressSanitizer یا oracle دیگری که نیاز به سورس کد دارد، استفاده نشد تا شرایط واقعی باینری بدون سورس حفظ شود. برای مجموعه سوم بنچمارک‌ها، سورس کد موجود نبود که ضرورت وجود یک فازر صرفاً باینری را نشان می‌دهد.

تکرارها و پیکربندی سیستم. ما هر بنچمارک را پنج بار و هر بار به مدت ۲۴ ساعت، هم با Fuzzm و هم با AFL فاز می‌کنیم. این تکرارها برای در نظر گرفتن واریانس نتایج ناشی از غیرقطعی بودن ذاتی فرایند فازینگ [28] انجام می‌شوند. علاوه بر میانگین نتایج به‌دست‌آمده از تکرارها، بازه‌های اطمینان ۹۵٪ نیز گزارش می‌کنیم. تمامی آزمایش‌ها روی دو ماشین انجام شدند که هر کدام دارای دو پردازندهٔ Intel Xeon با ۱۲ هسته و ۲۴ رشته با فرکانس ۲٫۲ گیگاهرتز بودند، از ۲۵۶ گیگابایت حافظهٔ سیستم استفاده می‌کردند و سیستم‌عامل Ubuntu 18.04 LTS روی آن‌ها اجرا می‌شد. برای AFL از نسخهٔ 2.57b استفاده کرده‌ایم.

   ۶.۲ پرسش پژوهشی اول (RQ1): اثربخشی Fuzzm

ما اثربخشی فازینگ انتهابه‌انتها (end-to-end) برای باینری‌های WebAssembly با استفاده از Fuzzm را ارزیابی می‌کنیم. این ارزیابی با اندازه‌گیری چند معیار انجام می‌شود: تعداد مسیرهای یکتای کشف ‌شده (unique paths)، تعداد وقفه‌های ناگهانی یکتای ایجاد شده (unique crashes)، و این‌که آیا قناری (Canary) در یافتن این کرش‌ها کمک می‌کند یا خیر.

جدول ۱ نتایج را نشان می‌دهد که در آن اعداد مربوط به Fuzzm در بخش سمت چپ ارائه شده‌اند. شمارش کرش‌ها و مسیرها بر اساس تعریف AFL از کرش یکتا و مسیر یکتا انجام می‌شود؛ به این صورت که اگر دو کرش در یک مسیر اجرایی یکسان پیدا شوند، با هم ادغام در نظر گرفته می‌شوند. بر اساس این معیار، کرش‌های متفاوت ممکن است گاهی علت ریشه‌ای یکسانی داشته باشند [28].

نتایج نشان می‌دهد که Fuzzm با موفقیت صدها مسیر اجرایی را در برنامه‌ها کاوش می‌کند؛ به‌طور میانگین پس از ۲۴ ساعت فازینگ، ۱٬۲۳۲ مسیر یکتا برای هر بنچمارک کشف می‌شود. مشاهده می‌کنیم که این امر حتی برای برنامه‌های پیچیده‌ای مانند flac (در مجموعهٔ بنچمارک ۱) یا sqlite (در مجموعهٔ ۳) نیز صادق است.

برای مجموعه‌های بنچمارک اول و دوم که برنامه‌های مورد فازینگ شناخته‌شده هستند، ما ورودی‌های بذر (seed inputs) را در اختیار فازر قرار دادیم. برای مجموعهٔ سوم تنها یک فایل خالی به‌عنوان ورودی بذر ارائه شد که می‌تواند دلیل میانگین کمتر مسیرهای کشف‌شده در این مجموعه باشد.

از نظر کرش‌ها، Fuzzm به‌طور میانگین ۴۰٫۳ کرش برای هر بنچمارک پیدا می‌کند. برای مثال، در مورد libpng و pdfresurrect، Fuzzm ورودی‌هایی تولید می‌کند که باعث کرش شده و دقیقاً همان stack trace مربوط به اکسپلویت‌های اثبات مفهوم (PoC) برای آسیب‌پذیری‌ها را ایجاد می‌کنند؛ این موضوع تأیید می‌کند که Fuzzm قادر به کشف باگ‌های واقعی است.

برای درک بهتر این‌که Fuzzm در طول یک دورهٔ ۲۴ ساعتهٔ فازینگ چگونه یک برنامه را مورد کاوش قرار می‌دهد، شکل ۱۲ تعداد مسیرهای یکتای کشف‌شده را در طول زمان برای چهار برنامه نشان می‌دهد. از آن‌جا که نمودارهای مربوط به کرش‌های کشف‌شده همبستگی زیادی با مسیرهای کاوش‌شده دارند، به دلیل محدودیت فضا از ارائهٔ آن‌ها صرف‌نظر کرده‌ایم. (هر دو نوع نمودار برای تمام برنامه‌ها به‌صورت آنلاین در دسترس هستند.)

همان‌طور که معمولاً در فازرها مشاهده می‌شود، بیشتر رفتارهای برنامه در مراحل اولیه کشف می‌شوند؛ معمولاً در چند ساعت اول اجرا (۱۲a/b/c). پس از آن، تعداد مسیرهای جدید و کرش‌های جدید اغلب به حالت اشباع می‌رسد، به‌ویژه در مورد بنچمارک‌های LAVA-M (بخش b).

با این حال، در برخی بنچمارک‌ها — برای مثال qjs — در صورت افزایش زمان اجرا، Fuzzm همچنان قادر به کشف مسیرهای جدید است (بخش d). بازه‌های اطمینان معمولاً کوچک هستند، به‌جز در مورد openjpeg (الف) و pdfresurrect که در آن‌ها نتایج در اجراهای مختلف تغییرپذیری قابل‌توجهی دارند. در مجموع، این یافته‌ها با کارهای پژوهشی پیشین همخوانی دارند و نشان می‌دهند که اجرای چندبارهٔ یک فازر برای دستیابی به نتایج آماری معنادار اهمیت دارد [28].

جدول 1: مرور کلی معیارها و نتایج فازینگ (5 × 24 ساعت). اعداد گزارش شده میانگین و فواصل اطمینان 95٪ هستند.

فازینگ- fuzzm
WebAssembly - فازینگ - fuzzm
شکل ۱۲: میانگین مسیرهای منحصر به فرد کشف شده (محور y) طی ۲۴ ساعت فازینگ (محور x)، با فواصل اطمینان ۹۵٪

مقایسه. چون Fuzzm اولین رویکرد فازینگ صرفاً باینری برای WebAssembly است، نمی‌توانیم به صورت مستقیم آن را با هیچ baseline استانداردی مقایسه کنیم. با این حال، برای قرار دادن تعداد مسیرها و کرش‌ها در یک چارچوب مرجع تقریبی، نتایج AFL بومی نیز در سمت راست جدول ۱ ارائه شده است. این مقایسه صرفاً به عنوان یک راهنمای تقریبی است و مقایسهٔ عادلانه به چند دلیل امکان‌پذیر نیست. Fuzzm تنها به باینری نیاز دارد، در حالی که AFL ابزاربندی خود را هنگام کامپایل از سورس اعمال می‌کند. تعریف ما از شاخه‌ها ممکن است با تعریف AFL متفاوت باشد، صرفاً به دلیل تفاوت کامپایلرها و بهینه‌سازی‌ها و کد تولید شده وابسته به هدف. برخلاف باینری‌های بومی، تمام کتابخانه‌ها (از جمله libc) در WebAssembly به صورت static linked هستند، که مقدار کد مورد ابزاربندی Fuzzm را افزایش می‌دهد و بنابراین فضای فازینگ بزرگ‌تر می‌شود. تعداد مسیرهای کاوش‌شده به طور طبیعی به سرعت اجرای برنامه بستگی دارد، که اصولاً در WebAssembly نسبت به بومی کمتر است (به بخش ۶.۴ نیز مراجعه شود). مجموعه بنچمارک سوم تنها به صورت باینری WebAssembly موجود است، بنابراین نمی‌توانیم آن‌ها را با AFL بومی مقایسه کنیم.

برای مجموعه‌های بنچمارک ۱ و ۲، Fuzzm به طور متوسط تعداد مسیرهای مشابه AFL را کشف می‌کند: ۱۳۰۴ مسیر در مقابل ۱۵۱۹ مسیر. در زمینه کرش‌ها، AFL به طور متوسط ۱۲۲ کرش تولید می‌کند، که تقریباً دو برابر Fuzzm با ۵۴ کرش است. یک مورد استثنا base64 است: Fuzzm ۳۴ کرش یکتا ایجاد کرد، در حالی که AFL تنها یک کرش در یکی از اجراها داشت. این ۳۴ کرش WebAssembly توسط sanity check داخلی VM اجراکننده ایجاد شدند، که در باینری‌های بومی وجود ندارد و توضیح می‌دهد چرا AFL این کرش‌ها را شناسایی نکرده است. برای برنامه‌هایی که Fuzzm هیچ کرشی پیدا نکرد (مثلاً flac)، AFL نیز هیچ کرشی پیدا نکرد.

برای بنچمارک‌های LAVA-M، هم Fuzzm و هم AFL نتوانستند هیچ‌یک از باگ‌های تزریق‌شده توسط ابزار LAVA را فعال کنند. این موضوع کمی غیرمنتظره است، چرا که مقالات دیگر گزارش داده‌اند که حداقل برخی باگ‌ها برای uniq و گاهی برای base64 شناسایی شده‌اند [10, 60]. بررسی دستی برخی باگ‌های LAVA-M نشان می‌دهد که آن‌ها شبیه use-after-free هستند و وقوع کرش بستگی به این دارد که memory allocator دقیقاً یک chunk جدید را در همان موقعیت chunk آزادشده قبلی تخصیص دهد. نتیجه می‌گیریم که دلیل عدم شناسایی این باگ‌ها توسط Fuzzm یا AFL، تفاوت در نسخه‌های allocator مورد استفاده و همچنین تفاوت بین نسخه‌های AFL است.

اثربخشی قناری‌ها (Canary). علاوه بر اینکه Fuzzm اولین رویکرد فازینگ برای باینری‌های WebAssembly است، این ابزار oracleهای مبتنی بر canary را نیز ارائه می‌دهد تا سرریز (overflow) و سرریز منفی (underflow) در stack و heap را تشخیص دهد.
برای سنجش میزان سهم این oracleها در کرش‌های شناسایی‌شده توسط Fuzzm، کرش‌ها را بر اساس اینکه توسط canaryها ایجاد شده‌اند یا توسط عوامل دیگر، تفکیک می‌کنیم. سه ستون “Crashes” در جدول ۱ نتایج را نشان می‌دهند. Stack و Heap canaryها به ترتیب مسئول ۲۲.۲٪ و ۲۲.۰٪ از کل کرش‌های شناسایی‌شده در تمامی بنچمارک‌ها هستند. برای مجموعه‌های بنچمارک ۱ و ۲ این مقادیر به ترتیب ۴۵.۵٪ و ۱۹.۷٪ هستند. این ارقام نشان می‌دهد که هر دو نوع canary به طور قابل توجهی در اثربخشی Fuzzm نقش دارند.

خلاصه: وقتی بر روی اپلیکیشن‌ها و کتابخانه‌های شناخته‌شده و باینری‌های واقعی WebAssembly اعمال شود، Fuzzm به طور متوسط ۴۰ کرش یکتا و ۱,۲۳۲ مسیر یکتا را در طول ۲۴ ساعت فازینگ ایجاد می‌کند، که با نتایج AFL برای برنامه‌های بومی مشابه است. Oracleهای مبتنی بر canary تقریباً نیمی از تمام کرش‌های شناسایی‌شده را تشخیص می‌دهند و بنابراین به طور قابل توجهی به اثربخشی Fuzzm کمک می‌کنند.

   ۶.۳ پرسش پژوهشی دوم (RQ2): مقاومت ابزارسازی  (Robustness of Instrumentation)

برای فازینگ مؤثر یک برنامه، ابزارسازی (Instrumentation) ما نباید معنای اصلی برنامه را تغییر دهد، به جز در شرایط  سرریز (overflow)، که در آن قناری‌ها باید برنامه را متوقف کنند. برای اعتبارسنجی مقاومت ابزارسازی Fuzzm، خروجی تولیدشده توسط نسخه‌های instrumented و غیر-instrumented هر بنچمارک را مقایسه کردیم. برای هر بنچمارک در دو مجموعهٔ اول، حداقل ده ورودی مختلف جمع‌آوری شد، که مجموعاً ۱۳۸ تست‌کیس را شامل می‌شود (جدول ۲). این ورودی‌ها از وب‌سایت‌های مختلف نمونه‌برداری شدند و برای برنامه‌هایی که تعداد کافی نمونه آنلاین یافت نشد (مثلاً pal2rgb) ورودی‌ها با تبدیل تصاویر به فرمت pal تولید شدند.

مجموعهٔ سوم بنچمارک تنها به صورت باینری و بدون سورس یا مستندات موجود است، بنابراین برای آن ورودی تولید نکردیم. برای تمام باینری‌ها و ورودی‌های آزمایشی نشان‌داده‌شده در جدول ۲، بررسی شد که خروجی‌های تولیدشده توسط باینری‌های instrumented با خروجی‌های باینری‌های غیر-instrumented برابر باشند.

به عنوان شواهد تکمیلی برای مقاومت ابزارسازی، مشاهده شد که تمامی باینری‌های instrumented، اعتبارسنجی داخلی WebAssembly را گذراندند، که شامل بررسی نوع دستورات و توابع می‌شود.

خلاصه: اجرای تست‌های بنچمارک و اعتبارسنجی استاتیک هر ماژول WebAssembly قبل از اجرا نشان می‌دهد که ابزارسازی باینری Fuzzm، معنای برنامه اصلی را حفظ می‌کند.

   ۶.۴ پرسش پژوهشی سوم (RQ3): کارایی فازینگ  end-to-end

برای فازینگ مؤثر، لازم است برنامهٔ هدف بارها و بارها در زمان محدود اجرا شود. این موضوع برای Fuzzm به دلیل اجرای WebAssembly به عنوان یک زبان بایت‌کد و اعمال ابزارسازی در سطح باینری حتی چالش‌برانگیزتر است. ستون Execs/sec در جدول ۱ نشان‌دهندهٔ میانگین تعداد اجرای برنامه در ثانیه است. با میانگین ۳۲۱ اجرای برنامه در ثانیه، Fuzzm قادر است مسیرهای زیادی را سریع بررسی کند. همان‌طور که در بخش ۶.۲ توضیح داده شد، مقایسهٔ Fuzzm و AFL تنها به صورت حدودی ممکن است. حتی باینری‌های WebAssembly غیر-instrumented می‌توانند تا ۵۰٪ کندتر از کد بومی (native code) اجرا شوند. با این حال، برای مجموعه‌های بنچمارک ۱ و ۲، Fuzzm به میانگین ۴۱۴ اجرای برنامه در ثانیه دست یافت، که تنها ۳۷٪ کندتر از ۶۶۳ اجرای بومی در AFL است. این سرعت برای فازینگ عملی WebAssembly قابل قبول است، به خصوص که برنامه در یک VM اجرا می‌شود.

علاوه بر اجرای برنامه در VM، دیگر منابع کندی می‌تواند از ابزارسازی باینری اعمال‌شده ناشی شود. برای ارزیابی overhead زمان اجرا، برنامه‌های بنچمارک با همان ورودی‌های RQ3 اجرا شدند و زمان اجرا بین باینری اصلی و باینری instrumented مقایسه شد. نتایج در بخش راست جدول ۲ آمده است. به طور میانگین بر اساس ۲۵ اجرای برنامه، ابزارسازی پوشش (coverage instrumentation) ۱.۴۶ برابر overhead نسبت به باینری اصلی دارد. این overhead معمولاً بیشتر از overhead مربوط به canaryها است، چرا که برای هر شاخه در برنامه، ۱۳ دستورالعمل اضافه می‌کند. پیاده‌سازی‌های بهینه‌تر، مثل پیش‌بینی برخی شاخه‌ها بر اساس تحلیل استاتیک، می‌توانند این overhead را کاهش دهند که به عنوان کار آینده پیشنهاد شده است.

خلاصه: Fuzzm  صدها اجرای برنامه در ثانیه انجام می‌دهد، که تنها ۳۷٪ کندتر از AFL بومی است، با وجود اجرای برنامه در VM. ابزارسازی پوشش (coverage) میانگین overhead 1.46x دارد و این بخش بیشترین سهم را در overhead کلی Fuzzm دارد.

جدول ۲: سربار پایداری و زمان اجرا برای داده‌های باینری ابزار دقیق (میانگین بیش از ۲۵ تکرار، فاصله اطمینان ۹۵٪)

فازم - فازینگ

   ۶.۵ پرسش پژوهشی چهارم (RQ4): اثربخشی قناری‌ها در جلوگیری از بهره‌برداری

در پرسش‌های قبلی، Fuzzm را به عنوان یک فازر end-to-end برای WebAssembly بررسی کردیم. با این حال، ابزارسازی با Canaryها (Section 4) همچنین در حالت مستقل نیز مفید است، یعنی برای تشخیص خطاهای حافظه در باینری‌های تولیدی و جلوگیری از بهره‌برداری. برای ارزیابی این سناریو، قناری را روی سه برنامهٔ WebAssembly آسیب‌پذیر منتشرشده قبلی با exploitهای proof-of-concept اعمال کردیم [30]. این برنامه‌ها WebAssembly را در سه محیط مختلف استفاده می‌کنند:

  1. وب‌سایت در مرورگر
  2. js
  3. برنامهٔ خط فرمان برای VMهای مستقل WASI

از آنجا که ابزاربندی قناری (Canary) وابسته به پلتفرم نیست، می‌توان باینری‌ها را در هر سه محیط سخت‌سازی کرد. ورودی‌های proof-of-concept دو آسیب‌پذیری stack overflow و یک heap overflow (سرریز پشته و هیپ) که روی متادیتای allocator می‌نویسد را هدف قرار می‌دهند.

ابتدا تأیید شد که باینری‌های اصلی و غیر-instrumented قابل بهره‌برداری هستند و باعث cross-site scripting، اجرای کد و نوشتن روی فایل‌های ناخواسته می‌شوند. سپس، تمامی سه باینری را موفقیت‌آمیز instrumented کردیم، بدون نیاز به سورس یا تغییر در فرآیند  build. وقتی ورودی‌ها صحیح و بی‌خطر هستند، باینری‌های instrumented همانند قبل عمل می‌کنند. وقتی ورودی‌های اکسپلویت (exploit) اعمال می‌شوند، تمامی سه نمونه توسط قناری‌های (Canary) درج‌ شده متوقف می‌شوند.

خلاصه: قناری‌های stack و heap که با ابزارسازی فقط باینری درج شده‌اند، باینری‌های موجود را به‌طور مؤثر سخت می‌کنند و در برابر اکسپلویت‌های (exploit) قبلی محافظت می‌کنند.

   ۶.۶ پرسش پژوهشی پنجم (RQ5): کارایی قناری‌های وارد ‌شده

همان‌طور که در بخش قبل نشان داده شد، قناری‌های (Canary) وارد شده می‌توانند حمله‌های سرریز بافر (buffer overflow) را در باینری‌های موجود کاهش دهند. برای این سناریو، مهم است که قناری‌ها تنها تأثیر جزئی بر عملکرد برنامه داشته باشند.

برای ارزیابی کارایی، برنامه‌های بنچمارک با و بدون ابزارسازی روی همان ورودی‌های RQ3 اجرا شدند. نتایج در جدول ۲ آمده است و نشان می‌دهد زمان اجرای برنامه با ترکیب‌های مختلف قناری نسبت به باینری‌های غیرابزاربندی شده چگونه تغییر می‌کند.

ابزاربندی قناری پشته (stack canary) و قناری هیپ (heap canary) تنها تأثیر اندکی بر کارایی دارند؛ به‌طوری‌که میانگین زمان اجرا نسبت به باینری بدون ابزاربندی به‌ترتیب ۱٫۰۶ برابر و ۱٫۰۵ برابر است.

سربار ناشی از قناری پشته  مشابه پیاده‌سازی‌های کارآمد قناری برای باینری‌های بومی است [15] که به‌طور پیش‌فرض در کامپایلرهای رایج مانند Clang، GCC و MSVC استفاده می‌شوند.

با این حال، میزان تأثیر در برنامه‌های مختلف متفاوت است. برخی برنامه‌ها — برای مثال jbig2dec با زمان اجرای ۱٫۲۲ برابر — نسبتاً بیشتر تحت تأثیر قرار می‌گیرند، در حالی که در برنامه‌هایی مانند flac این سربار تقریباً ناچیز است.

هزینهٔ نسبی heap canary به تعداد تخصیص‌های حافظه (memory allocations)، به‌ویژه تخصیص‌های کوچک، بستگی دارد. در این میان، pdfresurrect — که یک ابزار تحلیل فایل‌های PDF است — به دلیل تخصیص‌های مکرر حافظه بیشتر از سایر برنامه‌ها متمایز می‌شود. با این حال، سربار ایجادشده برای سایر برنامه‌ها کم بوده یا حتی آن‌قدر ناچیز است که قابل اندازه‌گیری نیست.

زمانی که هر دو نوع ابزاربندی قناری‌ به‌طور هم‌زمان اعمال شوند (ستون “All Canaries”)، سربار ایجادشده تقریباً مجموع سربار هر یک به‌صورت جداگانه است و در مجموع سربار متوسطی معادل ۱٫۱۱ برابر ایجاد می‌کند.

جمع‌بندی: سربار تحمیل‌شده توسط اوراکل‌های مبتنی بر قناری‌ نسبتاً کم است (به‌ترتیب ۱٫۰۶ برابر و ۱٫۰۵ برابر) و با پیاده‌سازی‌های قناری‌ در سایر زبان‌ها قابل مقایسه است. این موضوع نشان می‌دهد که استفاده از آن‌ها برای سخت‌سازی (hardening) باینری‌های محیط تولید (production) گزینه‌ای امیدوارکننده محسوب می‌شود.

۷.  کارهای مرتبط (Related Work)

فازینگ. از میان روش‌های مختلف فازینگ خاکستری (greybox fuzzing) [10, 12, 22, 28, 35]، ما بر روی فازر محبوب AFL تمرکز کرده‌ایم. برخلاف حالت‌های معمول GCC و LLVM آن و بیشتر فازرهای دیگر [6, 32, 44, 47, 57]، ما به دسترسی به کد منبع نیاز نداریم و بنابراین نمی‌توانیم به اوراکل‌های اضافه‌شده توسط کامپایلر [43, 50] تکیه کنیم. بهبودهای الگوریتم تولید ورودی در AFL [34] نیز به نفع Fuzzm خواهد بود.

چند روش برای فازینگ باینری‌های بومی وجود دارد. حالت‌های QEMU و DynInst در AFL به ابزارهای دینامیک وابسته‌اند که باعث افزایش بار اجرایی می‌شود. دینش و همکاران [16] ابزارهای باینری x86-64 را به صورت ایستا (static) برای فازینگ ابزار کرده‌اند، اما آن‌ها نمی‌توانند باینری‌های WebAssembly را پوشش دهند، زیرا معماری متفاوت است و همچنین چند فرض غیرقابل اعمال در مورد WebAssembly دارند، مانند کد مستقل از موقعیت (position-independent code) یا نیاز به اطلاعات جابجایی (relocation information).

کارهای اخیر دیگری به ابزارسازی باینری برای جمع‌آوری اطلاعات پوشش مسیر (coverage) پرداخته‌اند [39]، اما اوراکل مشابه Fuzzm ارائه نکرده‌اند. ما تنها یک رویکرد برای فازینگ WebAssembly می‌شناسیم که پورت LibFuzzer است؛ آن روش به کد منبع نیاز دارد و فقط از کد C و ++C کامپایل‌شده با Emscripten پشتیبانی می‌کند که در مرورگر اجرا می‌شود. در مقابل، Fuzzm  اولین فازر باینری-تنها برای WebAssembly است و از برنامه‌های WASI پشتیبانی می‌کند. فازینگ همچنین برای تست ماشین‌های مجازی WebAssembly (VMs) استفاده شده است که با فازینگ برنامه‌ها تفاوت دارد.

بازنویسی باینری و محافظت در برابر  سرریز. ابزارسازی ایستا (static instrumentation) باینری‌های بومی به دلیل غیرقابل حل بودن تحلیل disassembly [54] چالش‌برانگیز است، با مشکلاتی مانند داده‌های درون‌کدی، طول متغیر دستورات، حل jumps غیرمستقیم و شناسایی توابع [4]. بسیاری از ابزارهای موجود برای بازیابی محدوده توابع به جدول سمبل‌ها وابسته‌اند [11, 16, 53]. WebAssembly از این مشکلات رنج نمی‌برد، و این مزیت در Fuzzm مورد استفاده قرار گرفته است.

چند مقاله تکنیک‌های بازنویسی باینری برای محافظت از stack در برنامه‌های x86 ارائه کرده‌اند [11, 16, 42, 53]. برای مثال، BodyArmor ابزارسازی‌ای اضافه می‌کند که خواندن و نوشتن نسبت به اشاره‌گرها را پایش می‌کند [53]. تکنیک دیگری ترکیبی از آرایش تصادفی، ایزولاسیون و تخصیص امن ارائه می‌دهد تا باینری‌ها در برابر آسیب‌پذیری‌های مبتنی بر stack سخت‌سازی شوند [11].

RetroWrite [16] از مکانیزم تشخیص overflow مشابه AddressSanitizer [50] استفاده می‌کند، یعنی با بهره‌گیری از حافظه سایه برای علامت‌گذاری بایت‌هایی که دسترسی به آن‌ها غیرقانونی است. این سه رویکرد قبلی همه به وجود جدول سمبل وابسته‌اند، در حالی که WebAssembly نیازی به این جدول ندارد.

Shadow Stack. پرساد و همکاران [45] از یک shadow stack برای شناسایی buffer overflowهایی که از مرزهای frameهای stack عبور می‌کنند استفاده کرده‌اند، بدون نیاز به دسترسی به جدول سمبل‌ها. تکنیک آن‌ها از نظر دقت در کشف باگ‌ها مشابه canaryهای Fuzzm است. استفاده از shadow stack تاثیری بر مراجع نسبی روی stack ندارد و بنابراین در معماری x86 که مراجع نسبی رایج است، ترجیح داده می‌شود. با این حال، مانند سایر تکنیک‌های x86، احتمال false positives و false negatives وجود دارد.

حفاظت از Heap. چندین کار به محافظت از باینری‌ها در برابر overflowهای heap پرداخته‌اند [16, 42, 48]. برای مثال، رابرتسون و همکاران [48] و نیکیفوراکیس و همکاران [42] تکنیک‌هایی ارائه می‌کنند که مشابه heap canaryهای Fuzzm هستند و توابع تخصیص و آزادسازی حافظه را ابزارسازی می‌کنند تا canaryها درج شوند. نیکیفوراکیس و همکاران canaryها را در زمان system call بررسی می‌کنند که باعث می‌شود overflowها سریع‌تر تشخیص داده شوند، اما با هزینه بررسی مکرر canaryها. Fuzzm برای کارایی بالاتر، canaryها را تنها در هنگام deallocation بررسی می‌کند تا فازینگ سریع و سخت‌سازی با overhead کم ممکن شود.

WebAssembly. چندین مقاله امنیت برنامه‌های WebAssembly را بررسی کرده‌اند و نشان داده‌اند که exploitهایی که دیگر در باینری‌های native امکان‌پذیر نیستند، هنوز ممکن است WebAssembly را تحت تاثیر قرار دهند [18, 30]. WebAssembly در ابتدا به‌طور گسترده برای اسخراج ارز دیجیتال (cryptomining) مخرب استفاده می‌شد [29, 38, 49]، اما اخیراً نشان داده شده که بیشتر باینری‌های WebAssembly امروزی به ندرت مخرب هستند [24]. با وجود طراحی WebAssembly برای دستیابی به سرعت نزدیک به native، اجرای آن هنوز حدود ۵۰٪ کندتر است [26] که تفاوت عملکرد بین Fuzzm و AFL را توضیح می‌دهد. فریمورک Wasabi امکان ایجاد آنالیزهای دینامیک برای WebAssembly را فراهم می‌کند [31]، اما Fuzzm برای کارایی از ابزارسازی (instrumentation) اختصاصی خود استفاده می‌کند.

8. نتیجه‌گیری (Conclusion)

برنامه‌های WebAssembly روزبه‌روز فراگیرتر می‌شوند و این موضوع نیاز به روش‌هایی برای کشف مشکلات امنیتی را افزایش می‌دهد. در این مقاله، Fuzzm معرفی می‌شود که نخستین فازر خاکستری (greybox fuzzer) در سطح باینری برای WebAssembly است. این رویکرد ترکیبی از چند مؤلفه است: ابزاربندی باینری مبتنی بر canary برای شناسایی سرریز مثبت و منفی (overflow و underflow)  در پشته (stack) و هیپ (heap)، یک ابزاربندی کارآمد برای اندازه‌گیری پوشش (coverage instrumentation)، یک ماشین مجازی WebAssembly برای اجرای برنامه، و الگوریتم تولید ورودی مورد استفاده در نسخهٔ بومی AFL.

برخلاف بسیاری از فازرهای کارآمد دیگر، Fuzzm مستقیماً روی باینری‌های محیط تولید (production binaries) کار می‌کند و نیازی به دسترسی به کد منبع ندارد. ما نشان می‌دهیم که Fuzzm قادر است تعداد قابل‌توجهی کرش را در باینری‌های واقعی WebAssembly کشف کند، در حالی که از نظر کارایی نیز به اندازه‌ای کارآمد است که حتی با وجود غیر بومی بودن و سرعت کمتر WebAssembly نسبت به زبان‌های بومی، صدها اجرا در ثانیه انجام دهد.

علاوه بر استفاده به‌عنوان اوراکل در فرایند فازینگ، قناری (canary) می‌توانند به‌عنوان یک روش مستقل برای سخت‌سازی باینری‌ها (binary hardening) نیز به کار روند تا از بهره‌برداری از باینری‌های آسیب‌پذیر در محیط تولید جلوگیری کنند. در این سناریو، این رویکرد قادر است اکسپلویت‌های منتشرشدهٔ قبلی را متوقف کند، در حالی که سربار اجرایی کمی ایجاد می‌کند. در مجموع، این کار گامی مهم در جهت ایمن‌سازی پلتفرم رو به رشد WebAssembly در برابر بهره‌برداری از آسیب‌پذیری‌های مرتبط با حافظه محسوب می‌شود.

[1] Fuzzm ترکیبی از دو کلمه «fuzzing» و «Wasm» است.

منابع

				
					[1] Wasmer – The Universal WebAssembly Runtime. https://wasmer.io/, 2019.
[2] Wasmtime – A small and efficient runtime for WebAssembly & WASI. https://wasmtime.dev/, 2020.
[3] OSS-Fuzz. https://google.github.io/oss-fuzz/, 2021.
[4] Dennis Andriesse, Xi Chen, Victor van der Veen, Asia Slowinska, and Herbert Bos. An in-depth analysis of disassembly on full-scale x86/x64 binaries. In Thorsten Holz and Stefan Savage, editors, 25th USENIX Security Symposium, USENIX Security 16, Austin, TX, USA, August 10-12, 2016, pages 583–600. USENIX Association, 2016.
[5] Anonymous. Once upon a free. Phrack, 11(9), November 2001.
[6] Cornelius Aschermann, Sergej Schumilo, Tim Blazytko Robert Gawlik, and Thorsten Holz. Redqueen: Fuzzing with input-to-state correspondence. In NDSS, volume 19, pages 1–15, 2019.
[7] M. Ammar Ben Khadra, Dominik Stoffel, and Wolfgang Kunz. Efficient binary-level coverage analysis. In FSE, 2020.
[8] John Bergbom. Memory safety: old vulnerabilities become new with WebAssembly. https://www.forcepoint.com/sites/default/files/resources/files/report-web-assembly-memorysafety-en.pdf, 2018.
[9] Marcel Böhme, Van-Thuan Pham, and Abhik Roychoudhury. Coverage-based greybox fuzzing as markov chain. IEEE Trans. Software Eng., 45(5):489–506, 2019.
[10] Peng Chen and Hao Chen. Angora: Efficient fuzzing by principled search. In 2018 IEEE Symposium on Security and Privacy, SP 2018, Proceedings, 21-23 May 2018, San Francisco, California, USA, pages 711–725. IEEE Computer Society, 2018.
[11] Xi Chen, Asia Slowinska, Dennis Andriesse, Herbert Bos, and Cristiano Giuffrida. Stackarmor: Comprehensive protection from stack-based memory error vulnerabilities for binaries. In 22nd Annual Network and Distributed System Security Symposium, NDSS 2015, San Diego, California, USA, February 8-11, 2015. The Internet Society, 2015.
[12] Yaohui Chen, Dongliang Mu, Jun Xu, Zhichuang Sun, Wenbo Shen, Xinyu Xing, Long Lu, and Bing Mao. Ptrix: Efficient hardware-assisted fuzzing for COTS binary. CoRR, abs/1905.10499, 2019.
[13] Lin Clark. Standardizing WASI: A system interface to run WebAssembly outside the web. https://hacks.mozilla.org/2019/03/standardizing-wasia-webassembly-system-interface/, 2019.
[14] Crispan Cowan. Stackguard: Automatic adaptive detection and prevention of buffer-overflow attacks. In Aviel D. Rubin, editor, Proceedings of the 7th USENIX Security Symposium, San Antonio, TX, USA, January 26-29, 1998. USENIX Association, 1998.
[15] Thurston H. Y. Dang, Petros Maniatis, and David A. Wagner. The performance cost of shadow stacks and stack canaries. In Proceedings of the 10th ACM Symposium on Information, Computer and Communications Security, ASIA CCS ’15, Singapore, April 14-17, 2015, pages 555–566. ACM, 2015.
[16] Sushant Dinesh, Nathan Burow, Dongyan Xu, and Mathias Payer. Retrowrite: Statically instrumenting COTS binaries for fuzzing and sanitization. In 2020 IEEE Symposium on Security and Privacy, SP 2020, San Francisco, CA, USA, May 18-21, 2020, pages 1497–1511. IEEE, 2020.
[17] Brendan Dolan-Gavitt, Patrick Hulin, Engin Kirda, Tim Leek, Andrea Mambretti, William K. Robertson, Frederick Ulrich, and Ryan Whelan. LAVA: large-scale automated vulnerability addition. In IEEE Symposium on Security and Privacy, SP 2016, San Jose, CA, USA, May 22-26, 2016, pages 110–121. IEEE Computer Society, 2016.
[18] Brian McFadden Tyler Lukasiewicz Jeff Dileo Justin Engler. Security chasms of WASM. In NCC Group Whitepaper. NCC Group, 2018.
[19] Dongsoo Ha, Wenhui Jin, and Heekuck Oh. REPICA: rewriting position independent code of ARM. IEEE Access, 6:50488–50509, 2018.
[20] Andreas Haas, Andreas Rossberg, Derek L Schuff, Ben L Titzer, Michael Holman, Dan Gohman, Luke Wagner, Alon Zakai, and JF Bastien. Bringing the web up to speed with webassembly. In Proceedings of the 38th ACM SIGPLAN Conference on Programming Language Design and Implementation, pages 185–200, 2017.
[21] Adam Hall and Umakishore Ramachandran. An execution model for serverless functions at the edge. In Proceedings of the International Conference on Internet of Things Design and Implementation, IoTDI ’19, page 225–236, New York, NY, USA, 2019. Association for Computing Machinery.
[22] Ahmad Hazimeh, Adrian Herrera, and Mathias Payer. Magma: A ground-truth fuzzing benchmark. CoRR, abs/2009.01120, 2020.
[23] Ningyu He, Ruiyi Zhang, Lei Wu, Haoyu Wang, Xiapu Luo, Yao Guo, Ting Yu, and Xuxian Jiang. Security analysis of eosio smart contracts. arXiv preprint arXiv:2003.06568, 2020.
[24] Aaron Hilbig, Daniel Lehman, and Michael Pradel. An empirical study of real-world webassembly binaries: Security, languages, use cases. In The Web Conference 2021 (WWW ’21), 2021.
[25] Abhinav Jangda, Bobby Powers, Emery Berger, and Arjun Guha. Not so fast: Analyzing the performance of webassembly vs. native code. login Usenix Mag., 44(3), 2019.
[26] Abhinav Jangda, Bobby Powers, Emery D Berger, and Arjun Guha. Not so fast: Analyzing the performance of webassembly vs. native code. In 2019 {USENIX} Annual Technical Conference ({USENIX}{ATC} 19), pages 107–120, 2019.
[27] Michel Kaempf. Vudo – An object superstitiously believed to embody magical powers. Phrack, 11(8), November 2001.
[28] George Klees, Andrew Ruef, Benji Cooper, Shiyi Wei, and Michael Hicks. Evaluating fuzz testing. In Proceedings of the 2018 ACM SIGSAC Conference on Computer and Communications Security, CCS 2018, Toronto, ON, Canada, October 15-19, 2018, pages 2123–2138. ACM, 2018.
[29] Radhesh Krishnan Konoth, Emanuele Vineti, Veelasha Moonsamy, Martina Lindorfer, Christopher Kruegel, Herbert Bos, and Giovanni Vigna. Minesweeper: An in-depth look into drive-by cryptocurrency mining and its defense. In Proceedings of the 2018 ACM SIGSAC Conference on Computer and Communications Security, CCS 2018, Toronto, ON, Canada, October 15-19, 2018, pages 1714–1730. ACM, 2018.
[30] Daniel Lehmann, Johannes Kinder, and Michael Pradel. Everything old is new again: Binary security of webassembly. In 29th USENIX Security Symposium, USENIX Security 2020, August 12-14, 2020, pages 217–234. USENIX Association, 2020.
[31] Daniel Lehmann and Michael Pradel. Wasabi: A framework for dynamically analyzing WebAssembly. In ASPLOS, 2019.
[32] Yuekang Li, Bihuan Chen, Mahinthan Chandramohan, Shang-Wei Lin, Yang Liu, and Alwen Tiu. Steelix: program-state based binary fuzzing. In Proceedings of the 2017 11th Joint Meeting on Foundations of Software Engineering, pages 627–637, 2017.
[33] Chi-Keung Luk, Robert S. Cohn, Robert Muth, Harish Patil, Artur Klauser, P. Geoffrey Lowney, Steven Wallace, Vijay Janapa Reddi, and Kim M. Hazelwood. Pin:building customized program analysis tools with dynamic instrumentation. In Proceedings of the ACM SIGPLAN 2005 Conference on Programming Language Design and Implementation, Chicago, IL, USA, June 12-15, 2005, pages 190–200. ACM, 2005.
[34] Dominik Maier, Heiko Eißfeldt, Andrea Fioraldi, and Marc Heuse. AFL++ : Combining incremental steps of fuzzing research. In 14th USENIX Workshop on Offensive Technologies, WOOT 2020, August 11, 2020. USENIX Association, 2020.
[35] Valentin Jean Marie Manès, HyungSeok Han, Choong-woo Han, Sang Kil Cha, Manuel Egele, Edward J Schwartz, and Maverick Woo. The art, science, and engineering of fuzzing: A survey. IEEE Transactions on Software Engineering, 2019.
[36] Timothy McCallum. Diving into Ethereum’s Virtual Machine (EVM): the future of Ewasm. https://hackernoon.com/diving-into-ethereumsvirtual-machine-the-future-of-ewasm-wrk32iy, 2019.
[37] Brian McFadden, Tyler Lukasiewicz, Jeff Dileo, and Justin Engler. Security chasms of wasm. NCC Group Whitepaper, 2018.
[38] Marius Musch, Christian Wressnegger, Martin Johns, and Konrad Rieck. Thieves in the browser: Web-based cryptojacking in the wild. In Proceedings of the 14th International Conference on Availability, Reliability and Security, ARES 2019, Canterbury, UK, August 26-29, 2019, pages 4:1–4:10. ACM, 2019.
[39] Stefan Nagy, Anh Nguyen-Tuong, Jason D. Hiser, Jack W. Davidson, and Matthew Hicks. Breaking through binaries: Compiler-quality instrumentation for better binary-only fuzzing. In 30th USENIX Security Symposium (USENIX Security 21), pages 1683–1700. USENIX Association, August 2021.
[40] Shravan Narayan, Craig Disselkoen, Tal Garfinkel, Nathan Froyd, Eric Rahm, Sorin Lerner, Hovav Shacham, and Deian Stefan. Retrofitting fine grain isolation in the firefox renderer. In 29th {USENIX} Security Symposium ({USENIX} Security 20), pages 699–716, 2020.
[41] Nicholas Nethercote and Julian Seward. Valgrind: a framework for heavyweight dynamic binary instrumentation. In Proceedings of the ACM SIGPLAN 2007 Conference on Programming Language Design and Implementation, San Diego, California, USA, June 10-13, 2007, pages 89–100. ACM, 2007.
[42] Nick Nikiforakis, Frank Piessens, and Wouter Joosen. Heapsentry: Kernel-assisted protection against heap overflows. In Detection of Intrusions and Malware, and Vulnerability Assessment - 10th International Conference, DIMVA 2013, Berlin, Germany, July 18-19, 2013. Proceedings, volume 7967 of Lecture Notes in Computer Science, pages 177–196. Springer, 2013.
[43] Sebastian Österlund, Kaveh Razavi, Herbert Bos, and Cristiano Giuffrida. Parmesan: Sanitizer-guided grey-box fuzzing. In 29th USENIX Security Symposium (USENIX Security 20), pages 2289–2306. USENIX Association, August 2020.
[44] Hui Peng, Yan Shoshitaishvili, and Mathias Payer. T-fuzz: fuzzing by program transformation. In 2018 IEEE Symposium on Security and Privacy (SP), pages 697–710. IEEE, 2018.
[45] Manish Prasad and Tzi-cker Chiueh. A binary rewriting defense against stack based buffer overflow attacks. In Proceedings of the General Track: 2003 USENIX Annual Technical Conference, June 9-14, 2003, San Antonio, Texas, USA, pages 211–224. USENIX, 2003.
[46] Lijin Quan, Lei Wu, and Haoyu Wang. EVulHunter: detecting fake transfer vulnerabilities for EOSIO’s smart contracts at Webassembly-level. arXiv preprint arXiv:1906.10362, 2019.
[47] Sanjay Rawat, Vivek Jain, Ashish Kumar, Lucian Cojocar, Cristiano Giuffrida, and Herbert Bos. Vuzzer: Application-aware evolutionary fuzzing. In NDSS, volume 17, pages 1–14, 2017.
[48] William K. Robertson, Christopher Krügel, Darren Mutz, and Fredrik Valeur. Run-time detection of heap based overflows. In Proceedings of the 17th Conference on Systems Administration (LISA 2003), San Diego, California, USA, October 26-31, 2003, pages 51–60. USENIX, 2003.
[49] Jan Rüth, Torsten Zimmermann, Konrad Wolsing, and Oliver Hohlfeld. Digging into browser-based crypto mining. In Proceedings of the Internet Measurement Conference 2018, IMC 2018, Boston, MA, USA, October 31 - November 02, 2018, pages 70–76. ACM, 2018.
[50] Konstantin Serebryany, Derek Bruening, Alexander Potapenko, and Dmitriy Vyukov. Addresssanitizer: A fast address sanity checker. In 2012 USENIX AnnualTechnical Conference, Boston, MA, USA, June 13-15, 2012, pages 309–318. USENIX Association, 2012.
[51] Kostya Serebryany. Oss-fuzz-google’s continuous fuzzing service for open source software. USENIX Security, 2017.
[52] Simon Shillaker and Peter Pietzuch. Faasm: Lightweight isolation for efficient stateful serverless computing. In 2020 USENIX Annual Technical Conference (USENIX ATC 20), pages 419–433. USENIX Association, July 2020.
[53] Asia Slowinska, Traian Stancescu, and Herbert Bos. Body armor for binaries: Preventing buffer overflows without recompilation. In 2012 USENIX Annual Technical Conference, Boston, MA, USA, June 13-15, 2012, pages 125–137. USENIX Association, 2012.
[54] Richard Wartell, Yan Zhou, Kevin W. Hamlen, Murat Kantarcioglu, and Bhavani M. Thuraisingham. Differentiating code from data in x86 binaries. In Machine Learning and Knowledge Discovery in Databases - European Conference, ECML PKDD 2011, Athens, Greece, September 5-9, 2011, Proceedings, Part III, volume 6913 of Lecture Notes in Computer Science, pages 522–536. Springer, 2011.
[55] Conrad Watt. Mechanising and verifying the webassembly specification. In Proceedings of the 7th ACM SIGPLAN International Conference on certified programs and proofs, pages 53–65, 2018.
[56] WebAssembly Community Group. WebAssembly Specification. https://webassembly.github.io/spec/core/, 2021.
[57] Insu Yun, Sangho Lee, Meng Xu, Yeongjin Jang, and Taesoo Kim. QSYM : A practical concolic execution engine tailored for hybrid fuzzing. In 27th USENIX Security Symposium, USENIX Security 2018, Baltimore, MD, USA, August 15-17, 2018, pages 745–761. USENIX Association, 2018.
[58] Alon Zakai. WasmBoxC: Simple, Easy, and Fast VM-less Sandboxing. https://kripken.github.io/blog/wasm/2020/07/27/wasmboxc.html, 2020.
[59] Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler. The fuzzing book. https://www.fuzzingbook.org/, 2019.
[60] Bin Zhang, Jiaxi Ye, Chao Feng, and Chaojing Tang. S2F: discover hard-to-reach vulnerabilities by semisymbolic fuzz testing. In 13th International Conference on Computational Intelligence and Security, CIS 2017, Hong Kong, China, December 15-18, 2017, pages 548–552. IEEE Computer Society, 2017.

				
			

همچنین ممکن است دوست داشته باشید

پیام بگذارید

wpChatIcon
wpChatIcon