خانه » نوآوری یافت نشد: بررسی سایه‌سازی ورودی در فازینگ از طریق راه‌اندازی مجدد فازر تطبیقی

نوآوری یافت نشد: بررسی سایه‌سازی ورودی در فازینگ از طریق راه‌اندازی مجدد فازر تطبیقی

Novelty Not Found: Exploring Input Shadowing in Fuzzing through Adaptive Fuzzer Restarts

توسط Vulnerlab
115 بازدید
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer

فازینگ جعبه‌ خاکستری (greybox fuzzing) از طریق اثربخشی بی‌سابقه در شناسایی خودکار خطاها، به بهبود امنیت نرم‌افزار کمک می‌کند. موفقیت این رویکرد ناشی از بازخورد پوشش (coverage feedback) استخراج‌ شده از سیستم تحت آزمون (Program Under Test – PUT) است که فازر را برای کاوش بخش‌های مختلف برنامه هدایت می‌کند. رایج‌ترین شیوه استفاده از این بازخورد، جستجوی نوآوری‌محور (novelty search)  است؛ در این روش، فازر تنها ورودی‌هایی را نگه می‌دارد که یک یال (edge) جدید در برنامه را فعال می‌کنند. با این حال، این رویکرد به‌ صورت ذاتی پدیده سایه‌سازی ورودی (input shadowing) را نادیده می‌گیرد؛ به این معنا که ورودی‌های بالقوه جالب، اگر منجر به تولید پوشش جدید نشوند، کنار گذاشته می‌شوند. این محدودیت موجب کاهش فضای ورودی‌های پذیرفته ‌شده می‌شود و ممکن است منجر به از دست رفتن باگ‌هایی گردد که تنها در اثر اعمال جهش‌ها (mutations) بر روی ورودی‌های سایه‌ گذاری‌شده قابل تحریک هستند.

ما در این پژوهش، یک تحلیل جامع از پدیده سایه‌سازی ورودی (input shadowing) ارائه می‌کنیم و نشان می‌دهیم که اجرای چندباره فازینگ بر روی یک هدف یکسان، با وجود همپوشانی در پوشش کد (code coverage)، منجر به توزیع متفاوتی از فراوانی بازدید بلوک‌های پایه (basic block hit frequency distribution) می‌شود. ما استفاده از راه‌اندازی مجدد فازر (fuzzer restarts) را برای بازتوزیع مؤثر فراوانی بازدید بلوک‌های پایه پیشنهاد می‌کنیم و نشان می‌دهیم که این رویکرد به‌ طور میانگین پوشش کلی به‌دست‌ آمده را در 15 هدف ارزیابی ‌شده به میزان 9.5٪ و در برخی موارد تا 25.0٪ افزایش می‌دهد.

علاوه بر این، شروع مجدد به یافتن اشکالات بیشتر و فعال‌سازی قابل اعتمادتر آنها کمک می‌کند. در مجموع، نتایج ما اهمیت درنظرگرفتن سایه‌سازی ورودی (input shadowing) در طراحی فازرها و همچنین مزایای بالقوه یک راهبرد مبتنی بر شروع مجدد را برای بهبود عملکرد روش‌های پیچیده فازینگ برجسته می‌سازد.

۱. مقدمه (Introduction)

فازینگ (Fuzzing) یک تکنیک بسیار مؤثر برای شناسایی خطاهای نرم‌افزاری در برنامه‌ها است و در سال‌های اخیر در جامعه آزمون امنیت نرم‌افزار با سرعت زیادی مورد توجه قرار گرفته است. این حوزه از اجرای ورودی‌های تصادفی بر روی ابزارهای خط فرمان لینوکس و مشاهده رفتار آن‌ها [1] آغاز شد، اما به‌تدریج به یک روش بسیار تخصصی و پیشرفته تبدیل گردید که طی چند سال گذشته بهبودهای قابل‌توجهی را تجربه کرده است.

فازینگ جعبه‌ خاکستری مبتنی بر بازخورد (feedback-driven greybox fuzzing) که توسط AFL معرفی گردید و محبوب شد [2]، یکی از مهم‌ترین پیشرفت‌های این حوزه به شمار می‌آید. این تکنیک رویکرد را از تولید تصادفی ورودی‌ها و مشاهده رفتار برنامه به سمت پایش این موضوع که هر ورودی کدام بخش‌های برنامه را پوشش داده است تغییر می‌دهد. این هدایت مبتنی بر پوشش کد (coverage guidance) به فازر اجازه می‌دهد تا ورودی‌هایی را که رفتار جدیدی در برنامه ایجاد می‌کنند، به‌ صورت مؤثر و کارآمد انتخاب، جهش (mutate) و اجرا کند و در نتیجه بخش‌های بیشتری از برنامه را تحت آزمون قرار دهد.

یک فازر به منظور کشف هرچه بیشتر باگ‌ها (Bug) و در نتیجه آسیب‌پذیری‌های امنیتی، تلاش می‌کند تا حد ممکن فضای ورودی برنامه (input space) را مورد آزمون قرار دهد. برای این منظور، فازر مجموعه‌ای از ورودی‌ها را با استفاده از جهش‌دهنده‌ها (mutators) تغییر می‌دهد. در عمل، جهش‌دهنده‌های فازر و مجموعه ورودی‌ها(corpus)، فضای ورودی را شکل می‌دهند که فازر قادر به پوشش آن در هدف مورد نظر است.

از آنجا که فازرها ذاتاً بر جستجوی نوآوری‌محور (novelty search) متکی هستند، تنها ورودی‌هایی را به مجموعه ورودی‌ (corpus) می‌پذیرند که موجب فعال‌سازی پوشش کد جدید در برنامه هدف شوند. تمامی ورودی‌هایی که همان پوشش قبلی (پوشش یکسان) را تکرار می‌کنند، کنار گذاشته می‌شوند؛ به این ترتیب، فازر می‌تواند جستجوی خود را برای کشف نواحی جدید و تاکنون اجرا نشده در برنامه بهینه‌ کند. متأسفانه این امر فضای ورودی قابل اکتشاف را محدود می‌کند. فرض کنید فازر، ورودی‌ را پیدا می‌یابد که در برنامه پوشش جدیدی ایجاد می‌کند. فازر در این حالت، این ورودی را به ‌عنوان ورودی جدید (novel) در نظر گرفته و آن را برای جهش‌های بیشتر به مجموعه ورودی (corpus) اضافه می‌کند.

اکنون فرض کنید فازر، ورودی دیگری (متفاوت از اولی) را می‌یابد که همان پوشش را فعال می‌کند؛ در این صورت این ورودی دوم به دلیل عدم ایجاد نوآوری کنار گذاشته می‌شود. ما در این وضعیت می‌گوییم ورودی اول، ورودی دوم را در سایه (shadow) قرار می‌دهد. با این حال، ورودی دوم می‌تواند پیش‌نیازی مهم برای رسیدن به یک موقعیت جالب در برنامه در مراحل بعدی کمپین فازینگ باشد، حتی اگر خودش هیچ پوشش جدیدی ایجاد نکند. این مسئله به سازوکار داخلی فازر بازمی‌گردد: فازر، ورودی بعدی را با انتخاب یک ورودی از مجموعه و اعمال جهش (mutation) بر روی آن تولید می‌کند.

در نتیجه، فضای ورودی قابل دسترس فازر توسط دو عامل محدود می‌شود: (۱) جهش‌هایی که فازر قادر به اعمال آن‌ها است و (۲) ورودی‌های موجود در مجموعه ورودی (corpus). این موضوع نشان می‌دهد که بخش‌هایی از فضای ورودی (با احتمال قابل‌توجه) برای فازر غیرقابل دسترس باقی می‌مانند، زیرا جهش‌های اعمال‌ شده بر مجموعه ورودی تنها زیرمجموعه‌ای از همه ورودی‌های ممکن را تولید می‌کنند. وجود سایر ورودی‌های سایه‌دار یا سایه‌سازی شده (shadowed inputs) در مجموعه ورودی می‌تواند به‌صورت نظری فضای ورودی قابل دسترس را افزایش دهد. از این منظر، ورودی‌های سایه‌سازی شده (shadowed inputs) باید در مجموعه ورودی‌ها (corpus) نگهداری شوند تا فازر بتواند بخش بزرگ‌تری از فضای ورودی را کاوش کند. با این حال، این کار از نظر کارایی عملی غیرقابل اجرا است و همچنین با اصول جستجوی نوآوری‌محور (novelty search) در تعارض قرار دارد؛ موضوعی که نیازمند طراحی تکنیک‌های جدید است.

کارهای پیشین دو مسیر را برای مواجهه با این مسئله بررسی کرده‌اند. نخست، رویکردهایی مانندCollAFL [3] ،datAFLow [4] یا InvsCov [5] که به ‌صورت ضمنی تلاش می‌کنند با استفاده از سازوکارهای بازخوردی متفاوت یا ریزدانه (fine-grained)، این مشکل را دور بزنند. این روش‌ها احتمال در نظر گرفتن ورودی‌های سایه‌سازی شده (shadowed inputs) به‌ عنوان ورودی‌های جدید  نوآورانه (novel) را بر اساس معیارهای خود افزایش می‌دهند. از سوی دیگر، افزایش تعداد ورودی‌هایی که به‌ عنوان «جدید یا novel » پذیرفته می‌شوند به‌طور خودکار موجب رشد صف (queue) می‌شود؛ امری که می‌تواند به اتلاف چرخه‌های فازینگ بر روی ورودی‌های کم‌اهمیت منجر شود. بنابراین، اگرچه این تکنیک‌ها تعداد ورودی‌های سایه‌سازی شده را کاهش می‌دهند، اما مشکل بنیادین را حل نمی‌کنند.

رویکرد دوم همان‌طور که در شکل 1 نشان داده شده است، شامل تکنیک‌هایی است که شکاف‌ها و فاصله‌های بزرگ (bridge distant gaps) در فضای ورودی را پر می‌کنند. این کار می‌تواند یا با تقسیم این شکاف به چند شکاف کوچک‌تر (برای مثال توسط cmplog [6, 7]) یا از طریق بهبود در مکانیزم‌های جهش (mutations) انجام شود. به‌صورت شهودی، می‌توان از جهش‌های وابسته به هدف (target-specific mutations) برای پر کردن شکاف‌ها و فاصله‌های بزرگ استفاده کرد و در نتیجه فضای ورودی قابل دسترس را افزایش داد [8، 9، 10]. با وجود اینکه این روش‌ها می‌توانند اثر ورودی‌های سایه‎سازی شده را کاهش دهند، اما آن‌ها نیز همچنان مشکل بنیادین را به‌طور کامل برطرف نمی‌کنند.

ما در این پژوهش، تأثیر ورودی‌های سایه‌سازی شده (shadowed inputs) را بر فضای ورودی اکتشاف ‌شده تحلیل کرده و یک راهبرد که راه‌اندازی مجدد فازر (fuzzer restarts) می‌باشد را برای کاهش اثرات آن بررسی می‌کنیم. با راه‌اندازی مجدد تطبیقی فازر، ما وضعیت داخلی آن را بازنشانی می‌کنیم و در نتیجه این امکان فراهم می‌شود که ورودی‌هایی که پیش‌تر سایه‌سازی شده محسوب می‌شدند، دوباره به ‌عنوان ورودی‌های جدید (novel) در نظر گرفته شوند. ما فرض می‌کنیم که فازرها می‌توانند از چنین راه‌اندازی‌های مجددی از نظر پوشش کد (coverage) و تعداد باگ‌های کشف ‌شده سود ببرند، زیرا این کار تصمیمات قبلی را که ممکن است مانع پیشرفت بیشتر فازر شده باشند، خنثی می‌کند.

اگرچه این ایده در نگاه اول غیرشهودی به نظر می‌رسد، زیرا اطلاعاتی که با اجرای پرهزینه به‌دست آمده‌اند کنار گذاشته می‌شوند؛ اما نتایج حاکی از آن است که این تکنیک واقعاً مفید می‌باشد. این روش، بلوک‌های پایه (basic blocks) آزمایش‌ شده را متنوع ساخته و به فازر کمک می‌کند تا پوشش جدیدی را یافته و باگ‌های (Bug) بیشتری را تحریک کند.

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

  • ما نشان می‌دهیم که اجرای فازینگ با یک پیکربندی اولیه یکسان، حتی در شرایطی که پوشش کد (code coverage) مشابهی حاصل می‌شود، اغلب توجه خود را بر بخش‌های متفاوتی از برنامه متمرکز می‌کند و به‌طور مکرر پدیده سایه‌سازی ورودی (input shadowing) را نشان می‌دهد.
  • به منظور کمّی‌سازی این پدیده، معیار جدیدی تحت عنوان فراوانی بلوک‌های پایه (basic block frequency) پیشنهاد می‌دهیم تا توزیع توجه فازر در بخش‌های مختلف برنامه را اندازه‌گیری کند.
  • بر اساس این یافته‌ها، چندین راهبرد زمان‌بندی (scheduling strategies) ارائه می‌دهیم که از طریق راه‌اندازی مجدد فازر (fuzzer restarts) وضعیت فازینگ را بازنشانی می‌کنند؛ این کار موجب کاهش اثر سایه‌گذاری ورودی و دستیابی هم‌زمان به فراوانی بالای بلوک‌ها و پوشش کد می‌شود.

دسترس‌ به آثار و خروجی‌های تولید شده پژوهشی (Research Artifact Availability). ما کد و آثار و خروجی‌های تولید شده پژوهشی خود را در نشانی https://github.com/CISPA-SysSec/fuzzing-restarts منتشر کرده‌ایم تا پژوهش در این حوزه را تسهیل کرده و ترویج دهیم.

ساختار رساله. این رساله شامل پژوهش‌هایی است که زیربنای آن را تشکیل می‌دهند:

۲. انگیزه (Motivation)

ما در این بخش، مفاهیم جستجوی نوآوری‌محور (novelty search) و سایه‌سازی ورودی (input shadowing) و تأثیر آن‌ها بر پیشرفت فازینگ را مورد بررسی قرار می‌دهیم.

   ۲.۱ جستجوی نوآوری‌محور در فازینگ (Novelty Search in Fuzzing)

الگوریتم جستجوی نوآوری‌محور و جدید (novelty search) یک الگوریتم اکتشافی است که بر اساس مشاهده جدید بودن یک رفتار مشخص هدایت می‌شود [11]. این روش در حوزه فازینگ، عمدتاً برای تعیین این موضوع به کار می‌رود که آیا یک ورودی برای جهش‌های بیشتر ارزشمند است یا خیر. برای مثال، AFL از یک الگوریتم جستجوی نوآوری‌محور استفاده می‌کند تا بر اساس پوششی که هر ورودی در زمان اجرا ایجاد می‌کند، تصمیم بگیرد آیا آن ورودی باید در مجموعه ورودی (corpus) نگه‌داری شود یا خیر. این الگوریتم سریعِ جستجوی نوآوری‌محور که در AFL به کار رفته است، یکی از ویژگی‌های کلیدی و مؤثر در موفقیت عملکرد آن محسوب می‌شود. با این حال، جستجوی نوآوری‌محور (Novelty Search) دارای معایبی نیز هست؛ از جمله پدیده سایه‌سازی ورودی (input shadowing).

   ۲.۲ سایه‌سازی ورودی (Input Shadowing)

پدیده سایه‌سازی ورودی (input shadowing) به حالتی اشاره دارد که در آن یک فازر مبتنی بر پوشش کد (coverage-guided fuzzer) یک ورودی را به‌ عنوان جدید (novel) در نظر نمی‌گیرد، زیرا آن ورودی هیچ پوشش یکتایی (unique coverage) به سیستم اضافه نمی‌کند. در عمل، این بدان معناست که فازر پیش‌تر ورودی دیگری را یافته است که همان پوشش را ایجاد کرده است. در سطح فنی، اکثر فازرها پیشرفت پوشش را با استفاده از یک بیت‌مپ (bitmap) ردیابی می‌کنند. از دیدگاه فازر، ورودی دوم شایسته توجه نیست، زیرا ورودی اول قبلاً خانه‌های مربوطه در بیت‌مپ (bitmap) را مقداردهی کرده است؛ بنابراین ورودی دوم رفتار جدیدی از برنامه را کاوش نکرده است. در نتیجه، گفته می‌شود این دو ورودی یک کلاس هم‌ارزی نوآوری‌محور (novelty equivalence class) را تشکیل می‌دهند، که تعریف دقیق‌تر آن به صورت زیر ارائه می‌شود:

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
شکل 1: فضای ورودی یک برنامه هدف. نواحی خاکستری منجر به ایجاد پوشش یکسان (same coverage) می‌شوند و دایره‌های قرمز، فضای ورودی‌ را نشان می‌دهند که با توجه به ورودی متناظر و با استفاده از جهش‌های (mutations) فازر قابل دسترس است. همان‌طور که نشان داده شده، ورودی‌های i1 و i2 پوشش یکسانی را فعال می‌کنند، با این حال جهش‌های ما تنها می‌توانند ورودی t را از طریق i2 تولید کنند.

تعریف ۱: کلاس هم‌ارزی نوآوری‌محور و جدید (Novelty Equivalence Class)

برای یک برنامه مفروض (p) با فضای ورودی (I) و یک فازر (f)، یک کلاس هم‌ارزی نوآوری‌محور و جدید ν شامل تمام ورودی‌های ممکن i I است که توسط برنامه (p) پذیرفته می‌شوند و در زمان اجرا، یک اندازه‌گیری (معیار) پوشش یکسان (same coverage measurement) را برای فازر (f) ارائه می‌دهند.

تفاوت میان ورودی‌های موجود در این کلاس هم‌ارزی نوآوری‌محور و جدید صرفاً در سطح بایت‌ها قابل تشخیص است (و نکته مهم این است که این تفاوت هیچ تأثیری بر مسیر اجرایی (execution path) ندارد). برای مثال، فرض کنید دو ورودی i1 و i2 در یک کلاس هم‌ارزی نوآوری‌محور و جدید ν قرار دارند. ورودی i2 لزوماً بهتر یا بدتر از i1 نیست؛ اما نکته کلیدی این است که تفاوت در سطح بایت‌ها موجب می‌شود جهش‌های فازر بتوانند از i2 ورودی‌های متفاوتی نسبت به i1 تولید کنند.

به یک برنامه نمونه با فضای ورودی نشان داده شده مطابق شکل ۱ توجه کنید: در این حالت، جهش‌های ما قادرند یک ورودی جالب دیگر t را زمانی که از i2 شروع می‌کنیم تولید کنند، اما این کار از طریق i1 ممکن نیست. از آنجا که هر دو ورودی i1 و i2 پوشش یکسانی دارند و i1 زودتر کشف شده است، فازر ورودی i2 را در نظر نمی‌گیرد و در نتیجه ورودی t نیز تولید نمی‌شود. در نتیجه ممکن است این بخش از برنامه هرگز مورد آزمون قرار نگیرد، زیرا فازر به ‌نوعی خود را محدود می‌کند (self-blocking). در این حالت گفته می‌شود که i2 توسط i1 سایه‌سازی شده است (is shadowed by i1)، یا به‌صورت رسمی‌تر:

تعریف ۲: سایه‌سازی ورودی (Input Shadowing)

یک ورودی i2 زمانی توسط ورودی دیگر i1 سایه‌سازی می‌شود (shadowed) که هر دو ورودی در یک کلاس هم‌ارزی نوآوری‌محور و جدید ν قرار داشته باشند و i1 پیش از i2 اجرا شده باشد، به‌گونه‌ای که اجرای i2 هیچ پوشش جدیدی (new coverage) ایجاد نکند.

تأکید می‌کنیم که سایه‌سازی ورودی (input shadowing) لزوماً پدیده‌ای منفی نیست. در واقع، این پدیده پیامد طبیعی جستجوی نوآوری‌محور و جدید (novelty search) در فازرها است؛ رویکردی که بر کشف رفتارهای جدید برنامه تمرکز دارد و نه آزمون مجدد رفتارهای قبلی.

با این حال، ما معتقدیم که پیامدهای منفی سایه‌سازی ورودی در ادبیات پژوهشی تا حد زیادی نادیده گرفته شده‌اند: یک ورودی سایه‌سازی شده i2 ممکن است پیش‌نیاز ضروری برای رسیدن به یک رفتار منجر به باگ باشد؛ به این معنا که فازر با حذف i2، عملاً خود را از رسیدن به آن مسیرهای اجرایی محروم ساخته و به‌ طور ناخواسته خود-محدودسازی (self-locking) ایجاد می‌کند. ما این رفتار را با استفاده از تابع نمونه ارائه‌ شده در الگوریتم ۱ به ‌عنوان هدف فازینگ نشان می‌دهیم. تابع هدف دارای دو پارامتر است: یک عدد صحیح بدون علامت ۳۲ بیتی a و یک عدد صحیح بدون علامت ۱۶ بیتی b. هدف تابع بررسی این است که آیا مجموع a و b کمتر از ۱۲۸ است یا خیر، مشروط بر اینکه a کوچکتر از ۲۵۶ باشد. اگر هر دو شرط برقرار باشد، اجرای برنامه متوقف می‌شود.

حال فازری را در نظر بگیرید که ورودی‌های این تابع را با انتخاب تصادفی مقادیر از یک توزیع یکنواخت و مستقل از اعداد صحیح ۳۲ بیتی تولید می‌کند. فرض کنید فازر در ابتدا مقدار a = 200 را انتخاب می‌کند. فازر به‌طور موفقیت‌آمیز شاخه اول را حل کرده و بازخورد آن را در بیت‌مپ (bitmap) خود ثبت می‌کند. در گام بعدی، فازر تلاش می‌کند شرط شاخه بعدی (a + b < 128) را با اعمال جهش بر روی متغیر b، در حالی که مقدار کشف ‌شده قبلی برای a را ثابت نگه می‌دارد، محقق کند. با این حال، با مقدار a = 200، تحقق شرط دوم عملاً غیرممکن می‌شود. چنانچه فازر مقدار a را مجدد جهش دهد و مثلاً به a = 50 و b = 100 برسد، شرط اول دوباره برقرار می‌شود اما شرط دوم همچنان برآورده نمی‌گردد. با این وجود، به دلیل جستجوی نوآوری‌محور (novelty search)، فازر این مقدار جدید برای a را ذخیره نمی‌کند، زیرا پیش‌تر ورودی را شناسایی کرده است که دقیقاً همان پوشش را ایجاد می‌کند (a = 200).

به بیان دیگر، ورودی اول (a = 200) ورودی دوم (a = 50) را سایه سازی می‌کند (shadows). با این حال، به‌ سادگی می‌توان مشاهده کرد که مقدار a = 50 انتخاب مناسب‌تری برای برآورده‌سازی شرط دوم بوده است. البته این بدان معنا نیست که فازر دیگر هرگز قادر به حل این شاخه نخواهد بود، بلکه صرفاً احتمال موفقیت را کاهش می‌دهد. فازر همچنان می‌تواند در یک تکرار (iteration)، به ‌طور تصادفی دو مقدار جدید و صحیح برای a و b انتخاب کند به‌گونه‌ای که شرط (a + b < 128) برقرار شود. اما بدیهی است که احتمال وقوع این حالت کمتر از زمانی است که فازر فقط نیاز داشته باشد مقدار b را حدس بزند. اگرچه این مثال ساده‌سازی‌ شده و تا حدی مصنوعی است، اما به‌ خوبی نشان می‌دهد که در برخی شرایط، فازر می‌تواند از وجود سایر ورودی‌ها در همان کلاس هم‌ارزی نوآوری‌محور (novelty equivalence class) سود ببرد.

الگوریتم 1: تابع نمونه‌ای که تأثیر سایه‌سازی ورودی (input shadowing) را نشان می‌دهد:

				
					Function foo(a : u32, b : u16):
	if a < 256 then
		if a + b < 128 then
			Abort();
				
			

در نتیجه، این پرسش مطرح می‌شود که آیا می‌توان اثرات منفی سایه‌سازی ورودی (input shadowing) را بدون از دست دادن مزایای جستجوی نوآوری‌محور (novelty search) که یکی از عوامل اصلی موفقیت فازرهای مدرن محسوب می‌شود، کاهش داد. یک راه ساده برای کاهش اثرات این پدیده، تنظیم مجدد وضعیت فازر (از نظر ورودی‌های ذخیره شده و بیت‌مپ (bitmap)) و بررسی نحوه عملکرد یک اجرای جدید و “تمیز” (clean run) است که می‌توانیم با راه‌اندازی مجدد فازر به آن دست یابیم. از نظر شهودی، این ایده در نگاه اول چندان منطقی به نظر نمی‌رسد؛ چراکه ما پیش‌تر منابع قابل‌توجهی به ‌ویژه از نظر تعداد اجراها (executions) صرف کرده‌ایم تا اکتشاف برنامه را به سمت رفتارهای جدید و تاکنون مشاهده ‌نشده هدایت کنیم.

   2.3 فراوانی بلوک‌های پایه (Basic Block Frequencies)

برای تحلیل تأثیر چنین راه‌اندازی‌های مجدد مبتنی بر بازنشانی وضعیت (state-resetting restarts)، یک کمپین فازینگ بر روی libpng اجرا کردیم و تعداد برخوردها (hits) با بلوک‌های پایه (basic blocks) را در طول اجرای فازینگ ثبت نمودیم. ما به‌طور دقیق‌تر، ++AFL را چندین بار و به مدت 24 ساعت اجرا کردیم و در هر اجرا از تنظیمات متفاوتی استفاده شد. ما از پیکربندی استاندارد ++AFL بدون هیچ‌گونه راه‌اندازی مجدد به‌ عنوان مبنای مقایسه (baseline) استفاده کردیم. علاوه بر آن، پنج پیکربندی دیگر با فواصل راه‌اندازی مجدد 12، 6، 2 و 1 ساعت، و همچنین 15 دقیقه در نظر گرفته شد که به‌ترتیب منجر به 1، 3، 11، 23 و 96 بار راه‌اندازی مجدد شدند. برای کنترل تصادفی ‌بودن ذاتی (inherent randomness) در فرایند فازینگ، این آزمایش برای هر پیکربندی ده بار تکرار شد و اجرای میانگین (median trial)  برای تحلیل‌های بعدی انتخاب گردید. نتایج این آزمایش در جدول 1 و شکل 2 ارائه شده‌اند. در ادامه، این آزمایش را با جزئیات بیشتری مورد بحث قرار می‌دهیم.

برخلاف این فرض شهودی که حذف وضعیت (discarding state) منجر به کاهش پوشش (coverage) می‌شود، مشاهده کردیم که تمامی اجراهایی که از راه‌اندازی مجدد (restarts) استفاده کرده‌اند، پوشش شاخه‌ای (branch coverage) اندکی بالاتر از اجرای پایه (baseline) دارند (رجوع شود به جدول ۱). نکته جالب‌تر این است که اجراهای مبتنی بر راه‌اندازی مجدد، رفتار اکتشافی متنوع‌تری از خود نشان می‌دهند: در تمامی موارد، بلوک‌های پایه (basic blocks) در مقایسه با حالت پایه، دفعات بیشتری مورد اصابت (hit) قرار گرفته‌اند.

به منظور بررسی دقیق‌تر این پدیده، در طول اجراها از هر ده ‌هزارمین ورودی (every 10,000-th input) نمونه‌برداری کردیم و بلوک‌های پایه‌ای را که توسط آن ورودی‌ها پیمایش می‌شدند ثبت نمودیم. در این فرایند، هر بلوک پایه برای هر نمونه فقط یک بار شمارش گردید. بنابراین، ما تعداد اصابت‌های هر بلوک پایه را اندازه‌گیری نکردیم، بلکه تعداد نمونه‌هایی را محاسبه کردیم که به هر بلوک پایه رسیده‌اند. نکته مهم این است که تحلیل خود را بر اساس صف ورودی‌ها (queue)  انجام ندادیم تا از بروز سوگیری بقا (survivorship bias) جلوگیری شود:

جدول 1: پوشش شاخه‌ای (branch coverage) به ‌دست‌آمده از libpng  بر اساس اجرای میانگین (median run)  از میان ده اجرای 24 ساعته، و همچنین بلوک‌های پایه‌ای که بیش از دو برابر دفعات، توسط اجرای پایه (رنگ آبی) یا فازر مبتنی بر راه‌اندازی مجدد (رنگ قرمز) تحریک شده‌اند. مقادیر پررنگ نشان‌دهنده بهترین عملکرد از منظر بلوک‌های پایه ترجیحی (favored basic blocks) می‌باشند:

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer

از آنجا که هر ورودی پذیرفته‌ شده در مجموعه وروردی‌ها (corpus) ذاتاً یک ورودی جدید و نوآوری‌محور (novel) محسوب می‌شود، چنانچه نمونه‌برداری را از صف ورودی‌ها (queue) انجام می‌دادیم، نتایج آزمایش دچار ابهام می‌گردید و تحلیل ما به‌سمت چنین ورودی‌هایی متمایل می‌گشت؛ در نتیجه، ورودی‌های سایه‌سازی شده (shadowed inputs) نادیده گرفته می‌شدند. با توجه به تعداد بسیار زیاد ورودی‌هایی که توسط فازرها اجرا می‌شوند، تنها زیرمجموعه‌ای از آن‌ها را انتخاب کرده و بلوک‌های پایه پوشش‌داده‌ شده را برای آن‌ها نمونه‌برداری کردیم.

نتایج این آزمایش به ‌صورت نقشه‌های حرارتی (heatmaps) در شکل ۲ نمایش داده شده است. برای این منظور، بیت‌مپ (bitmap)، پوشش یک‌ بُعدی (1-D coverage bitmap) را با استفاده از یک منحنی هیلبرت پرکننده فضا (space-filling Hilbert curve) به یک نمایش دوبعدی (2-D representation) تبدیل کردیم و سپس فراوانی برخوردهای بلوک پایه را با خط پایه بدون هیچ‌گونه راه‌اندازی مجدد، مقایسه نمودیم.

در این نمایش، هر سلول (cell) نمایانگر یک بلوک پایه مجزا (distinct basic block) است. رنگ آبی  نشان می‌دهد که آن بلوک پایه در اجرای پایه (baseline) با دفعات بیشتری مورد بازدید قرار گرفته است، در حالی که رنگ قرمز بیانگر آن است که همان بلوک در پیکربندی مورد آزمایش، بیشتر پیمایش شده است. رنگ سفید نشان‌دهنده فراوانی یکسان است؛ یعنی هر دو فازر، بلوک پایه مورد نظر را به تعداد یکسانی بازدید کرده‌اند. شدت اختلاف نیز از طریق درجه تیرگی رنگ نمایش داده می‌شود؛ به‌گونه‌ای که رنگ‌های تیره‌تر نشان‌دهنده اختلاف بیشتر در تعداد بازدیدها هستند.

نتایج نشان می‌دهد که در اجراهای بدون راه‌اندازی مجدد (without restarts)، یال‌های نادر (rare edges) با دفعات کمتری مورد بازدید قرار می‌گیرند، یا حتی در برخی موارد اصلاً پیمایش نمی‌شوند. در مقابل، این اجراهای بدون راه‌اندازی مجدد، بلوک‌های پرتکرار (frequent blocks) را حتی بیشتر از اجراهای دارای راه‌اندازی مجدد پوشش می‌دهند. از آنجا که در نمایش نتایج از مقیاس لگاریتمی (logarithmic scale) استفاده کرده‌ایم، این تفاوت در نمودارها کمتر از میزان واقعی آن به چشم می‌آید. در اجرای این آزمایش، ممکن است به ‌صورت شهودی انتظار داشته باشیم که اجراهای فازینگ با پوشش تقریباً یکسان (roughly equal coverage)، یال‌های (edges) برنامه را نیز با فراوانی تقریباً مشابهی پیمایش کنند. با این حال، زمانی که دو اجرای معمولی فازینگ، بدون هیچ‌گونه راه‌اندازی مجدد (restart) را با یکدیگر مقایسه کردیم، به نتیجه‌ای کاملاً متفاوت رسیدیم.

همان‌طور که در شکل 2a نشان داده شده است، توزیع فراوانی‌ها اختلاف‌هایی بیش از یک مرتبه بزرگی (an order of magnitude) را نشان می‌دهد. به بیان دیگر، هر اجرای فازینگ، با وجود دستیابی به پوشش کد قابل مقایسه (comparable code coverage)، بخش‌های متفاوتی از برنامه را با شدت بیشتری مورد آزمون قرار می‌دهد. این یافته بیانگر آن است که نتایج نهایی پوشش در قالب مجموعه ورودی‌ها (corpus)، نمایش دقیقی از توزیع ورودی‌هایی که در طول اجرای فازینگ واقعاً آزمون شده‌اند نیست. برای مثال، کافی است یک ورودی، تنها یک‌بار پوشش جدیدی ایجاد کند تا در مجموعه ذخیره شود، حتی اگر هرگز دوباره مورد آزمون قرار نگیرد. به همین ترتیب، ممکن است یک بلوک پایه (basic block) هزاران بار اجرا شده باشد، اما در نهایت تنها توسط یک ورودی در مجموعه نمایش داده شود. تأکید می‌کنیم که این مشاهده به هیچ وجه به معنای ناکارآمد بودن پوشش کد (code coverage) به‌ عنوان معیار ارزیابی نیست، زیرا این معیار همچنان یک شاخص جانشین بسیار مناسب (excellent proxy) برای نمایش وضعیت‌های مشاهده ‌شده برنامه (observed program states) محسوب می‌شود.

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
شکل ۲: نقشه‌های حرارتی (heatmaps) مبتنی بر منحنی‌های هیلبرت (Hilbert curves) که فراوانی پیمایش و مشاهده بلوک‌های پایه (basic blocks) را در طول یک اجرای فازینگ، در مقیاس لگاریتمی (logarithmic scale) و نسبت به یک اجرای پایه (baseline run) برای هدف libpng نشان می‌دهند (برای هر حالت، از اجرای میانگین از میان ده اجرای 24 ساعته استفاده شده است). هر سلول یا خانه (cell) نمایانگر یک بلوک پایه است؛ بلوک‌های آبی نشان می‌دهند که اجرای میانه در حالت پایه، آن بلوک را با فراوانی بیشتری پیمایش کرده است، در حالی که بلوک‌های قرمز عکس این وضعیت را نشان می‌دهند. این نتایج حاکی از آن است که راه‌اندازی مجدد فازر (fuzzer restarts) موجب تنوع‌بخشی به بلوک‌های پایه بازدید شده می‌شود؛ زیرا تصادفی‌بودن ذاتی (inherent randomness) در فرآیند فازینگ موجب می‌شود فازر در هر اجرا، بلوک‌های پایه متفاوتی را با شدت بیشتری مورد بازدید قرار دهد.

نتایج اولیه نشان می‌دهد که صِرف اجرای فازر به‌تنهایی می‌تواند موجب افزایش فراوانی پیمایش برخی بلوک‌های پایه شود، بدون آنکه لزوماً از میزان اکتشاف کلی کاسته شود. به‌صورت فرضی، تنوع در ورودی‌هایی که یک بلوک پایه را فعال می‌کنند می‌تواند در کشف باگ‌ها مؤثر باشد [5]. با این حال، اجرای مکرر فازر با هزینه محاسباتی قابل‌توجهی همراه است؛ به‌گونه‌ای که اجرای مجدد آن عملاً هزینه را دو برابر می‌کند و بنابراین، نسبت هزینه به فایده در تولید ورودی‌های متنوع چندان شفاف نیست. به منظور کاهش این هزینه، می‌توان فازر را در میانگین زمان اجرا (به عنوان مثال پس از 12 ساعت) بازنشانی و مجدداً اجرا کرد. این کار احتمالاً موجب توزیع یکنواخت‌تر فراوانی پیمایش بلوک‌های پایه می‌شود، اما ممکن است بخشی از پوشش حاصل‌شده نیز از دست برود. برای بررسی عملی بودن این ایده، فازر با فواصل مختلف راه‌اندازی مجدد اجرا شد؛ از یک بار (پس از 12 ساعت) تا 96 بار (هر 15 دقیقه)، که نتایج آن به‌ترتیب در شکل‌های 2b ،2c ،2d ،2e و 2f نشان داده شده‌اند.

   ۲.۴ چالش‌ها

یک راهبرد بهتر برای غلبه بر محدودیت‌های راه‌اندازی مجدد با فواصل ثابت (fixed-interval restarts)، می‌بایست عمدتاً دو چالش اصلی را حل کند. اول، باید مشخص شود چه زمانی برای بازنشانی فازر «مناسب» است. چنانچه فازر خیلی زود راه‌اندازی مجدد شود، فرصت کافی برای کاوش عمیق بخش‌های پیچیده‌تر برنامه فراهم نخواهد شد. در مقابل، اگر راه‌اندازی مجدد بیش از حد دیر انجام شود، زمان قابل‌توجهی از فازینگ صرف حالتی می‌شود که اثر سایه‌گذاری ورودی (input shadowing)  در آن لحاظ نشده است. دوم، بازنشانی کامل وضعیت فازر منجر به حذف اطلاعات ارزشمند می‌شود. ما این فرض را مطرح می‌کنیم که ایجاد تعادل بین حفظ اطلاعات مفید مانند بذرهایی (seeds) که نواحی جدیدی از برنامه تحت آزمون را باز می‌کنند و حذف اطلاعاتی که ممکن است موجب سایه‌گذاری سایر ورودی‌ها شود، می‌تواند سودمند باشد.

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

۳. راه‌اندازی مجدد فازر تطبیقی (Adaptive Fuzzer Restarts)

به منظور غلبه بر این چالش‌ها و کاهش اثر سایه‌سازی ورودی (input shadowing) بدون از بین بردن پوشش کد (code coverage)، ما یک زمان‌بند (scheduler) پیشنهاد می‌کنیم که از چندین تکنیک برای بازنشانی وضعیت فازر در زمان مناسب استفاده می‌کند.

   ۳.۱ نمای کلی زمان‌بند راه‌اندازی مجدد (Restart Scheduler Overview)

زمان‌بند راه‌اندازی مجدد (restart scheduler) مسئول هماهنگ‌سازی و مدیریت یک کمپین فازینگ است. این مؤلفه نباید با زمان‌بندی بذرهای‌های داخلی فازر(fuzzer-internal seed scheduling) اشتباه گرفته شود. در این مورد، وظیفه زمان‌بند آن است که یک اجرای فازینگ را پایش کند و فازر را بر اساس معیاری که در ادامه مورد بحث قرار می‌گیرد، راه‌اندازی مجدد نماید. این راه‌اندازی مجدد ممکن است شامل حفظ بخشی از وضعیت فازینگ (preservation of the fuzzing state) نیز باشد.

به منظور توضیح رفتار زمان‌بند در طول یک کمپین فازینگ، نمای کلی آن را در شکل 3 نشان داده شده است. در ابتدا، یک اجرای اولیه (run_0) با ایجاد یک نمونه فازر آغاز می‌شود. همانند فازینگ سنتی، لازم است مجموعه‌ای از بذرهای اولیه (initial seeds) به آن ارائه شود. سپس، زمان‌بند در طول فرآیند فازینگ این نمونه را پایش می‌کند (۱) و به‌ طور خاص، تغییراتی مانند مسیرهای کشف‌ شده (paths found) را دنبال می‌کند؛ این اطلاعات برای تصمیم‌گیری در مورد راه‌اندازی مجدد (۲) فازر بر اساس برخی از سیاست‌های راه‌اندازی مجدد استفاده می‌شوند. در صورت راه‌اندازی مجدد، نمونه فازر runn−1 که در حال اجرا  است، متوقف می‌شود و در صورتی که بخواهیم بخشی از وضعیت را در بین اجراها حفظ کنیم، صف خروجی (queue) آن مطابق با راهبرد مجموعه ورودی (corpus strategy) پردازش می‌شود (۳). وضعیت حفظ‌ شده به‌ عنوان زیرمجموعه‌ای از صف، به ‌عنوان بذرهای اولیه به runn اجرای بعدی فازر(در کنار بذرهای اولیه اصلی) منتقل می‌شود. سپس این فرایند با پایش نمونه جدید (۴) ادامه پیدا می‌کند تا زمان راه‌اندازی مجدد بعدی فرا برسد.

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
شکل 3: نمای کلی از زمان‌بند راه‌اندازی مجدد (restart scheduler) ما.

۳.۲ چه زمانی باید راه‌اندازی مجدد انجام شود: مرحله سکون پوشش (When to Restart: Coverage Plateaus)

اولین چالش، شناسایی زمان مناسب برای راه‌اندازی مجدد فازر است.

مرحله سکون پوشش (coverage plateau). در این زمینه، مفهوم سکون پوشش (coverage plateau) مطرح می‌شود. ما بر این بینش تکیه می‌کنیم که تمامی اجرای فازینگ‌ها در نقطه‌ای از زمان به حالت‌هایی موسوم به مرحله سکون پوشش می‌رسند. این وضعیت زمانی رخ می‌دهد که فازر، علی‌رغم جهش مداوم ورودی‌ها، دیگر قادر به کشف پوشش جدید نیست. این پدیده در نمودارهای پوشش به ‌صورت یک ناحیه تخت و صاف (plateau) ظاهر می‌شود و از همین رو این نام‌گذاری برای آن به‌کار رفته است. از دید پسینی یا گذشته‌نگر (retrospective)، تشخیص این سکون‌ها نسبتاً ساده است؛ اما در حین اجرای واقعی، تشخیص اینکه آیا فازر واقعاً در یک سکون به دام افتاده است یا در آستانه حل یک مجموعه محدودیت پیچیده می‌باشد که منجر به پوشش جدید می‌شوند، بسیار دشوار خواهد بود. برای دستیابی به تشخیص این سکون‌ها (plateaus) به‌صورت محاسباتی قابل‌اجرا و در عین حال مؤثر، ما یک سنجش اکتشافی (heuristic) ارائه می‌دهیم: ما فرض می‌کنیم فازر زمانی در مرحله سکون پوشش (coverage plateau) قرار دارد که برای مدت n دقیقه (که n یک مقدار کوچک مانند 15است) هیچ پوشش جدیدی یعنی هیچ یال جدیدی از برنامه (new edges) را کشف نکند. همان‌طور که آزمایش‌های بخش انگیزشی ما نشان می‌دهد، زمان‌های کوتاه‌تر برای راه‌اندازی مجدد، بیشترین تأثیر را در بهبود توزیع فراوانی برخورد با بلوک‌های پایه دارند.

اکتشاف‌های تشخیصی (Detection Heuristics). زمان‌بند ما شامل چندین راهبرد برای تعیین نقطه زمانی مناسب جهت راه‌اندازی مجدد فازر است. اولین و ساده‌ترین روش، به‌جای تکیه بر تشخیص مرحله سکون پوشش (coverage plateau)، از یک شمارش‌گر معکوس ثابت یا تصادفی (fixed or random countdown) استفاده می‌کند و به محض اتمام این شمارش، فازر را مجدداً راه‌اندازی می‌کند (مشابه همان راهبردی که در آزمایش انگیزشی بخش ۲ به کار گرفته شد). اگرچه این ابتکار ساده است، اما یک نقص اساسی دارد: این روش وضعیت فعلی عملکرد فازینگ را در نظر نمی‌گیرد. در نتیجه، ممکن است فازر دقیقاً در زمانی راه‌اندازی مجدد شود که در حال کشف یک ناحیه جدید و تاکنون‌ پوشش‌ داده ‌نشده از برنامه است. به منظور جلوگیری از افت عملکرد فازر به دلیل زمان‌بندی نامناسب راه‌اندازی مجدد، زمان‌بند ما پیشرفت پوشش (coverage progress) را پایش می‌کند تا تشخیص دهد آیا فازر در حال پیشرفت است یا خیر. هر بار که فازر پوشش جدیدی کشف می‌کند، شمارش‌گر معکوس مجدداً تنظیم (reset) می‌شود؛ به این معنا که راه‌اندازی مجدد تنها زمانی انجام می‌شود که فازر وارد حالت رکود پوشش شده و برای مدت مشخصی قادر به یافتن پوشش جدیدی نباشد. این مکانیزم موجب می‌شود راه‌اندازی مجددها با وضعیت واقعی برنامه تحت آزمون سازگار شوند. ترکیب این رویکرد با زمان‌های کوتاه راه‌اندازی مجدد و تشخیص رکود پوشش، به زمان‌بند اجازه می‌دهد در برنامه‌هایی که زودتر به سکون می‌رسند، دفعات بیشتری فازر را بازنشانی کند و در مقابل، در برنامه‌هایی که فازر همچنان قادر به کشف پوشش جدید است، فاصله بین راه‌اندازی‌های مجدد را افزایش دهد.

متأسفانه این رویکرد همچنان دارای یک نقطه‌ ضعف است؛ به‌ ویژه برای اهدافی که در آن‌ها پوشش جدید (new coverage) به ‌صورت آهسته اما پیوسته کشف می‌شود. تصور کنید فازری که هر پنج دقیقه تنها یک یال جدید شناسایی می‌کند: ما حدس می‌زنیم که حتی در چنین حالتی، راه‌اندازی مجدد سریع می‌تواند همچنان مفید باشد. بنابراین، زمان‌بند ما دارای یک روش اکتشافی سوم است که از یک آستانه (threshold) استفاده می‌کند. این آستانه امکان تنظیم میزان رشد پوشش را فراهم می‌سازد؛ به‌گونه‌ای که مشخص می‌کند چه سطحی از پیشرفت پوشش موجب بازنشانی مجدد شمارش‌گر راه‌اندازی (restart timer) می‌شود. این آستانه بر اساس روند پیشرفت فازینگ در بازه‌های زمانی گذشته محاسبه می‌گردد و از گیر افتادن زمان‌بند در برنامه‌هایی با رشد پوشش بسیار اندک اما مداوم جلوگیری می‌کند. به‌طور خلاصه، زمان‌بند راه‌اندازی مجدد ما شامل سه ابتکار اصلی است:

  1. راه‌اندازی مجدد کورکورانه با شمارش‌گر معکوس ثابت یا تصادفی (Blind restarts with fixed or random reset countdown)
  2. راه‌اندازی مجدد مبتنی بر مرحله سکون پوشش (Coverage plateau-based restarts)
  3. راه‌اندازی مجدد مبتنی بر نرخ رشد پوشش با استفاده از یک آستانه (Coverage growth rate-based restarts using a threshold)

   ۳.۳ نحوه راه‌اندازی مجدد: نگهداشت مجموعه ورودی‌ها (How to Restart: Corpus Retention)

دومین تصمیم طراحی مهمی که باید در زمان‌بند خود در نظر بگیریم، این است که چه میزان از وضعیت فازر (fuzzer state) باید بازنشانی شود. این موضوع مستقیماً به این مسئله تبدیل می‌شود که چه مقدار از مجموعه ورودی‌های تولید شده (generated corpus) باید در طول اجراهای متوالی حفظ شود؛ به این صورت که به ‌عنوان بذر‌های اولیه (initial seeds) به اجرای بعدی فازر منتقل گردد.

انگیزه (Motivation). ساده‌ترین رویکرد این است که کل وضعیت فازر (fuzzer state) به ‌طور کامل بازنشانی شود و اجرای جدید در یک محیط تازه آغاز گردد؛ به‌گونه‌ای که نمونه جدید هیچ اطلاعی از پیشرفت‌های قبلی نداشته باشد. ایراد این روش این است که اجرای جدید فازینگ باید مجدداً کل برنامه را از ابتدا کاوش کند. به همین دلیل، راهبردهای پیشرفته‌تری مطلوب هستند که در آن‌ها بخشی از مجموعه ورودی تولید شده (generated corpus) از اجرای قبلی حفظ می‌شود تا به ‌صورت پیوسته بخشی از اطلاعات پوشش (coverage information) به اجراهای بعدی منتقل گردد. این مجموعه ورودی نسبتا حفظ‌ شده (partially preserved corpus)، نقش نوعی راه‌اندازی اولیه (bootstrap) برای فازر ایفا می‌کند، زیرا در همان ابتدای اجرا، چندین ورودی در اختیار دارد که رفتارهای متفاوتی از برنامه را کاوش می‌کنند.

راهبردهای حفظ مجموعه ورودی (Corpus Retention Strategies). زمان‌بند ما به شش راهبرد برای تعیین بخش‌هایی از مجموعه ورودی (corpus) که می‌بایست حفظ شوند مجهز است:

(۱) بازنشانی (Reset). این راهبرد نمایانگر یک بازنشانی کامل است؛ در این حالت هیچ‌یک از فایل‌های موجود در پیکره حفظ نمی‌شود و در نتیجه هیچ اطلاعاتی از اجرای قبلی باقی نمی‌ماند.

(۲) درهم‌سازی ورودی‌ها (Input Shuffle). راهبرد درهم‌سازی ورودی‌ها در نقطه مقابل راهبرد بازنشانی (reset) قرار دارد. در زمان وقوع راه‌اندازی مجدد، تمامی فایل‌های موجود در مجموعه ورودی با اختصاص یک پیشوند عددی تصادفی به نام هر مورد آزمون (test case) درهم‌سازی می‌شوند. این کار موجب تغییر ترتیب خواندن فایل‌های پیکره توسط ++AFL می‌گردد. سپس این فایل‌های تغییرنام‌یافته به‌عنوان مجموعه ورودی بذر جدید (new seed corpus) برای اجرای بعدی مورد استفاده قرار می‌گیرند؛ بنابراین در این راهبرد هیچ داده‌ای حذف نمی‌شود و تمام اطلاعات حفظ می‌گردد.

(۳) هرس کردن مجموعه ورودی‌ها (Corpus Pruning). در زمان وقوع راه‌اندازی مجدد، راهبرد هرس کردن مجموعه ورودی‌ها به‌ صورت تصادفی بین ۵ تا ۹۵ درصد از ورودی‌های تولید شده در مجموعه را برای حذف انتخاب می‌کند. حذف تعداد بیشتری از ورودی‌ها می‌تواند اثر سایه‌سازی ورودی (input shadowing) را کاهش دهد، اما در عین حال فازر را وادار می‌کند چرخه‌های بیشتری را صرف کشف مجدد ورودی‌ها کند. ما حدس می‌زنیم که هیچ فرمول کلی و جهان‌شمولی برای تعیین اینکه کدام ورودی‌ها باید حذف شوند وجود ندارد، زیرا این مسئله به‌ شدت وابسته به هدف (target-dependent) است. بنابراین، زمان‌بند ما در راستای روح کلی فازینگ، از انتخاب تصادفی استفاده می‌کند تا هر دو جهت (حذف بیشتر یا کمتر) به‌صورت متعادل مورد کاوش قرار گیرند.

(۴) سفر در زمان (Time Travel). راهبرد سفر در زمان (time travel) یک برچسب زمانی (timestamp) تصادفی را انتخاب می‌کند که در آن، مجموعه ورودی‌های (corpus) اجرای فعلی به ‌صورت یک اسنپ‌شات (snapshot) ذخیره شده است. زمان‌بند به هنگام راه‌اندازی مجدد، این اسنپ‌شات از مجموعه ورودی را به اجرای جدید فازر منتقل می‌کند. در نتیجه، تمام فایل‌هایی که پس از آن نقطه زمانی مشخص ایجاد شده‌اند حذف می‌گردند. انگیزه این روش آن است که توجه فازر بازنشانی شود و امکانی فراهم گردد تا برنامه دوباره از همان نقطه زمانی مشخص، به‌صورت متفاوت مورد کاوش قرار گیرد.

(۵) هرس‌کننده درخت جستجو (Tree Chopper). ایده‌ی راهبرد هرس‌کننده درخت جستجو (tree chopper)، حذف تمام فرزندان (offspring) یک یا چند فایل انتخاب ‌شده درون مجموعه ورودی‌های تولید شده (generated corpus) است. این کار امکان شناسایی درخت‌های ورودی (trees of inputs) یعنی یک ورودی و تمام ورودی‌های بعدی که از طریق جهش‌ها (mutations) تولید شده‌اند را فراهم می‌کند. زمان‌بند یک زیر‌درخت تصادفی در گراف جهش ورودی (input-mutation graph) انتخاب می‌کند و سپس تمام فرزندانی را که از آن ورودی ایجاد شده‌اند از مجموعه ورودی‌ حذف می‌نماید. این رویکرد به فازر اجازه می‌دهد تا بر اساس بخش باقی‌مانده مجموعه ورودی‌ها، حالت‌های متفاوتی از برنامه را بررسی کند و به‌ طور بالقوه تصمیم‌های جایگزین و متفاوتی نسبت به آنچه در اجرای قبلی اتخاذ شده بود، تجربه نماید.

(۶) گسترش‌دهنده درخت جستجو (tree planter). راهبرد گسترش‌دهنده درخت جستجو (tree planter) مشابه هرس‌کننده درخت جستجو (tree chopper) است، با این تفاوت اصلی که در این حالت، به‌ جای حذف یک زیر‌درخت خاص، تمام درخت‌های تولید شده به‌ جز یک درخت انتخاب‌ شده از مجموعه ورودی حذف می‌شوند. این کار می‌تواند به فازر اجازه دهد تا یک بخش مشخص از هدف (target) را با عمق بیشتری مورد کاوش قرار دهد.

(۷) گروهی (Ensemble). مشابه رویکرد فازینگ گروهی (ensemble fuzzing) که در آن چند فازر با یکدیگر ترکیب شده و به‌صورت تطبیقی زمان‌بندی می‌شوند [12, 13, 14]، ما این فرض را مطرح می‌کنیم که یک راهبرد گروهی که بتواند به ‌صورت پویا مناسب‌ترین راهبرد حفظ و نگهداری (retention strategy) را متناسب با هدف در حال آزمون انتخاب کند، می‌تواند کارایی کلی را بهبود دهد. بر این اساس، ما یک راهبرد گروهی پیشنهاد می‌کنیم که در طول فرآیند فازینگ، به ‌صورت متناوب از مجموعه‌ای متنوع از راهبردهای نگهداری استفاده می‌کند تا خود را با ویژگی‌های در حال تحول برنامه هدف (target application) تطبیق دهد.

راهبرد گروهی (Ensemble) ما به ‌صورت نظام‌مند بین راهبردهای مختلف نگهداری مجموعه ورودی (retention strategies)، چرخه‌ای را طی می‌کند. این فرآیند با راهبرد بازنشانی (reset) آغاز می‌شود تا یک نقطه شروع برای اجراهای بعدی ایجاد کند. در اجراهای بعدی، یک راهبرد حفظ یا نگهداری به‌صورت تصادفی از میان گزینه‌های موجود انتخاب می‌شود (با اولویت دادن به آن‌هایی که هنوز آزمایش نشده‌اند). زمان‌بند به‌طور دقیق‌تر، عملکرد راهبرد انتخاب ‌شده را در طول سه راه‌اندازی مجدد بعدی ارزیابی می‌کند. پس از وقوع چهارمین راه‌اندازی مجدد، زمان‌بند تصمیم می‌گیرد که آیا راهبرد فعلی حفظ شود یا با یک راهبرد دیگر برای اجراهای بعدی جایگزین گردد. به طور دقیق‌تر، میانگین تعداد مجموعه ورودی‌ها برای راهبرد فعلی را با میانگین کلی اجراهای قبلی مقایسه می‌کند. علاوه بر عملکرد راهبرد فعال و میانگین عملکرد مشاهده‌ شده در میان تمام راهبردهای آزموده‌ شده، زمان‌بند همچنین بهترین راهبرد مشاهده ‌شده در تمام راه‌اندازی‌های مجدد را نگهداری می‌کند. این امر تضمین می‌کند که در صورت ناکارآمدی سایر گزینه‌ها، همواره امکان بازگشت به بهترین راهبرد تجربی وجود داشته باشد. در صورتی که حتی این بهترین راهبرد نیز در چندین راه‌اندازی مجدد متوالی هیچ پوشش جدیدی (new coverage) ایجاد نکند، زمان‌بند آن را کنار گذاشته و با یک راهبرد تصادفی جایگزین می‌کند.

با توجه به اینکه راهبرد گروهی (Ensemble)، مستلزم اجرای چندین دور فازینگ است، این روش به‌ طور ویژه برای کمپین‌های فازینگ بلندمدت و همچنین اهدافی مناسب است که پس از یک دوره اولیه از کاوش، دچار مرحله سکون پوشش (coverage plateau) می‌شوند. این راهبرد با تطبیق پویا با پویایی‌های در حال تغییر برنامه هدف (target application)، تلاش می‌کند تا در طول کمپین‌های فازینگ طولانی مدت، حداکثر پوشش کد (code coverage) و پتانسیل کشف آسیب‌پذیری‌ها (vulnerability discovery) را به حداکثر برساند.

۴. پیاده‌سازی (Implementation)

برای بررسی تأثیر راه‌اندازی‌های مجدد در طول یک کمپین فازینگ، ما زمان‌بند خود را در قالب یک نمونه اولیه به نام Sileo پیاده‌سازی کردیم. این ابزار نمونه، مسئول ایجاد (spawn) و پایش (monitoring) نمونه‌های فازر زیرین (underlying fuzzer) است. پیاده‌سازی ما شامل ۱٬۳۵۸ خط کد پایتون می‌باشد و برای فازینگ هدف تحت آزمون از نسخه 4.04c ابزار ++AFL استفاده می‌کند. Sileo به ‌راحتی قابل ترکیب با سایر فازرها است، زیرا تنها به سه قابلیت پایه نیاز دارد:

  1. باید بتواند اطلاعات مربوط به پوشش کد (coverage) را تا این لحظه دریافت کند (برای مثال، تعداد یال‌های کشف ‌شده).
  2. می‌بایست به ورودی‌های «مهم» یا جالب (interesting inputs) دسترسی داشته باشد (یعنی همان صف یا queue) تا بتواند یکی از راهبردهای حفظ مجموعه ورودی را اعمال کند.
  3. بایستی قادر باشد یک نمونه جدید فازر را راه‌اندازی و متوقف کند.

از آنجا که این پیش‌نیازها تقریباً در تمام فازرهای اهداف عمومی و همه منظوره (general-purpose fuzzers) وجود دارد، جایگزینی موتور فازینگ زیرین در Sileo نسبتاً ساده است. در مورد ++AFL، می‌توان پوشش کد (coverage) را با مشاهده فایل fuzzer_stats پایش کرد؛ این فایل به‌صورت دوره‌ای چندین معیار مرتبط با فازر را از طریق سیستم فایل (filesystem) صادر می‌کند. به‌طور مشابه، دسترسی به ورودی‌های تولید شده از طریق خواندن محتوای دایرکتوری صف در ++AFL امکان‌پذیر است. از آنجا که ++AFL یک برنامه در فضای کاربر (user-space application) است، ایجاد (spawning) و خاتمه (termination) یک نمونه فازر را می‌توان با استفاده از اصول اولیه استاندارد سیستم‌عامل برای ایجاد و مدیریت فرایندها تسهیل نمود.

روش‌های اکتشافی تشخیص مرحله سکون پوشش (Coverage Plateau Detection Heuristics). ما به منظور برای تشخیص معیارهای پوشش، به ‌صورت دوره‌ای فایل fuzzer_stats را بررسی می‌کنیم و زمان سپری ‌شده از last_find (آخرین یافته) و corpus_count (شمار مجموعه ورودی) را پایش می‌کنیم. در صورتی که زمان last_find از مقدار شمارش معکوس راه‌اندازی مجدد (restart countdown) فراتر رود، یک راه‌اندازی مجدد فعال می‌شود. هنگامی که از از روش اکتشافی راه‌اندازی مجدد مبتنی بر آستانه (threshold-based heuristic) استفاده ‌شود، مقدار corpus_count فعلی را ذخیره می‌کنیم و آستانه را بر اساس n مقدار آخر از این شمار مجموعه ورودی محاسبه می‌کنیم. در صورتی که شمارش معکوس راه‌اندازی مجدد به پایان برسد و مقدار آستانه نیز برآورده شده باشد، فرآیند راه‌اندازی مجدد اجرا می‌شود.

راهبردهای حفظ مجموعه ورودی (Corpus Retention Strategies). ما راهبردهای مختلف حفظ مجموعه ورودی (corpus retention strategies) معرفی ‌شده در بخش 3.3 را در Sileo پیاده‌سازی کرده‌ایم. پس از آنکه یک راه‌اندازی مجدد (restart) زمان‌بندی می‌شود، ابتدا یک کپی از صف فعلی (queue) ایجاد می‌گردد. سپس، بسته به راهبرد نگهداری انتخاب ‌شده، برخی فایل‌ها ممکن است برای مقابله با اثر سایه‌سازی ورودی (input shadowing) حذف یا درهم‌سازی شوند؛ در حالی که در همین فرآیند، مجموعه ورودی بذر جدید (new seed corpus) نیز وارد سیستم می‌شود.

ما برای آزمایش‌های خود از FuzzBench استفاده کردیم. با این حال، مشاهده گردید که استفاده از راهبردهای مبتنی بر مجموعه ورودی‌ها موجب افزایش قابل‌توجه مصرف فضای دیسک می‌شود، زیرا FuzzBench به‌ صورت دوره‌ای از مجموعه ورودی‌ها، اسنپ‌شات‌ (snapshot) تهیه می‌کند. به منظور کاهش این مشکل، گزینه‌ای به FuzzBench اضافه کردیم تا تنها دو آرشیو آخر از مجموعه ورودی‌ را نگه‌داری کند. علاوه بر این، ما برای تسهیل نمونه‌برداری از تمام ورودی‌ها (به‌جای تنها ورودی‌های موجود در مجموعه)، ++AFL را وصله (patch) کردیم و قابلیتی اضافه کردیم که هر nاُمین اجرای فازر را ذخیره کند. این قابلیت در مرحله ارزیابی برای محاسبه معیارهایی مانند نقشه‌های حرارتی (heatmaps) در شکل 2 و نمودارهای فراوانی بلوک‌های پایه (basic block frequency plots) بر اساس موارد آزمون تولید شده توسط فازر استفاده می‌شود.

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

ما برای ارزیابی، از نمونه اولیه Sileo خود استفاده می‌کنیم و آن را با ابتکارهای مختلف برای تعیین زمان راه‌اندازی مجدد فازر و همچنین با راهبردهای گوناگون حفظ مجموعه ورودی‌ها (corpus retention strategies) آزمایش می‌کنیم تا اثربخشی آن‌ها را مورد ارزیابی قرار دهیم. در طول این ارزیابی، به سه پرسش پژوهشی زیر پاسخ می‌دهیم.

  • پرسش پژوهشی ۱: آیا راه‌اندازی مجدد فازر (fuzzer restarts) یک راهکار مؤثر و کارا برای کاهش اثرات منفی سایه‌سازی ورودی (input shadowing) است؟
  • پرسش پژوهشی ۲: تأثیر راه‌اندازی مجدد فازر بر پوشش کد (code coverage) چیست؟
  • پرسش پژوهشی ۳: آیا راه‌اندازی مجدد فازر می‌تواند به کشف باگ‌های نرم‌افزار کمک کند؟

تنظیمات آزمایش (Experiment Setup). ما ارزیابی خود را روی یک سرور مجهز به پردازنده Intel Xeon Gold 6230R  با ۵۲ هسته و فرکانس ۲.۱۰ گیگاهرتز و ۱۹۶ گیگابایت حافظه و با سیستم‌عامل Ubuntu 22.04 به انجام رساندیم. ما با استفاده از FuzzBench، کمپین فازینگ را برای آزمایش‌های پوشش (coverage experiments) هماهنگ و اجرا کردیم؛ به ‌طوری که هر راهبرد به ‌عنوان یک فازر مستقل در نظر گرفته شد. تمام فازرها به مدت ۲۴ ساعت اجرا شدند و مطابق توصیه‌های Klees و همکارانش [16] و Schloegel و همکارانش [17]، هر آزمایش برای در نظر گرفتن تصادفی بودن ذاتی فرآیند فازینگ، ۱۰ مرتبه تکرار گردید. علاوه بر آزمایش‌های پوشش، یک اجرای بلندمدت روی یک هدف و همچنین آزمایش‌های مربوط به کشف باگ‌ها (bug-finding experiments) نیز انجام دادیم. به منظور جلوگیری از تأثیر عوامل خارجی بر نتایج، به هر اجرای فازر یک هسته (core) فیزیکی اختصاص دادیم و قابلیت توربو بوست پردازنده (CPU turbo-boost) را غیرفعال کردیم تا فرکانس هر هسته حتی در شرایط استفاده جزئی از CPU ثابت باقی بماند.

FuzzBench و برنامه‌های هدف (Fuzzbench and Target Applications). بخش عمده‌ای از آزمایش‌های ما با استفاده از چارچوب معیار FuzzBench [15] انجام شده است. برای این منظور، از یک نسخه سفارشی‌سازی ‌شده از FuzzBench استفاده کردیم که شامل تغییراتی در کد منبع بود؛ این تغییرات با هدف کاهش سربار مصرف فضای دیسک (disk space overhead) و همچنین رفع برخی باگ‌های جزئی (minor bugs) اعمال شدند. نسخه اصلاح ‌شده FuzzBench که در این پژوهش استفاده شده است، در خروجی‌های تولید شده (artifact) پژوهش ما نیز منتشر خواهد شد. FuzzBench مجموعه متنوعی از برنامه‌های هدف (target applications) را برای ارزیابی عملکرد فازرها از نظر پوشش کد (coverage) و کشف باگ (bug discovery) فراهم می‌کند. ما در مجموع ۱۵ هدف را انتخاب کردیم که شامل ۱۲ هدف از FuzzBench و یک هدف اضافی خارج از FuzzBench (objdump 2.41.50.20231023) بود. علاوه بر این، ۸ هدف دیگر از FuzzBench نیز به‌ طور اختصاصی برای آزمایش‌های کشف باگ در نظر گرفته شدند. انتخاب اهداف FuzzBench بر اساس گزارش‌های عمومی رسمی انجام شد؛ گزارش‌هایی که نشان می‌دادند این اهداف از نظر میزان پوشش حاصل ‌شده توسط فازرهای مختلف، تنوع قابل‌توجهی دارند.

راهبردها و ابتکارهای انتخاب ‌شده برای راه‌اندازی مجدد (Selected Restart Strategies and Heuristics). نمونه اولیه ما، Sileo، از سه ابتکار متفاوت برای راه‌اندازی مجدد و شش راهبرد حفظ مجموعه ورودی‌ها پشتیبانی می‌کند. از آنجا که اکتشاف مبتنی بر رکود (plateau-based heuristic)  در برخی اهداف منجر به تعداد کمی راه‌اندازی مجدد و در برخی موارد حتی بدون هیچ راه‌اندازی مجدد می‌شود، در تمامی آزمایش‌های خود از اکتشاف رشد پوشش (coverage growth heuristic) برای تعیین زمان راه‌اندازی مجدد فازر استفاده کردیم. از میان شش راهبرد حفظ مجموعه ورودی‌ها (که تعیین می‌کنند پس از راه‌اندازی مجدد، کدام بخش از مجموعه ورودی‌های تولید شده (generated corpus) به‌ عنوان ورودی اولیه در اجرای بعدی استفاده شوند)، زیرمجموعه‌ای شامل سه راهبرد بازنشانی (reset)، هرس کردن مجموعه ورودی (corpus pruning) و درهم‌سازی ورودی‌ها (input shuffle) را برای ارزیابی‌های ارائه ‌شده در ادامه، به‌ منظور حفظ خوانایی انتخاب کردیم. خوانندگان علاقه‌مند می‌توانند برای مشاهده تمامی راهبردهای پیشنهادی راه‌اندازی مجدد (از جمله گسترش‌دهنده درخت جستجو (tree planter)، هرس‌کننده درخت جستجو (tree chopper) و سفر در زمان (time travel)) برای تمامی اهداف آزمایش‌ شده در FuzzBench، به آثار و خروجی‌های پژوهش ما مراجعه کنند. ما این سه راهبرد نام‌برده را برای ارزیابی اصلی انتخاب کردیم، زیرا راهبردهای بازنشانی (reset) و درهم‌سازی ورودی‌ها (input shuffle) به‌ عنوان مبناهای مناسب برای خود Sileo عمل می‌کنند و راهبرد هرس کردن مجموعه ورودی (corpus pruning) نیز بهترین عملکرد را در آزمایش‌ها نشان داده است. راهبرد راهبردهای بازنشانی (reset)، کل وضعیت فازر را بازنشانی می‌کند و هیچ بخشی از مجموعه ورودی‌های تولید شده را برای اجرای بعدی حفظ نمی‌کند. در مقابل، راهبرد درهم‌سازی ورودی‌ها (input shuffle)، کل مجموعه ورودی‌ را نگه می‌دارد و تمامی فایل‌ها را به‌ صورت تصادفی جابجا و بازآرایی می‌کند؛ در نتیجه، فازر در اجرای بعدی با یک مجموعه بزرگ از بذرها (seed) آغاز به کار می‌کند. راهبرد هرس کردن مجموعه ورودی (corpus pruning) نیز به ‌عنوان یک راه حل میانی، بین این دو رویکرد به کار گرفته می‌شود.

فازرها و مبناهای مقایسه (Fuzzers and Baselines). ما از ++AFL نسخه c 4.04 [7] به ‌عنوان پایه اصلی Sileo استفاده می‌کنیم و در نتیجه، همین فازر را در حالت بدون راه‌اندازی مجدد (without restarts) به ‌عنوان مبنای مقایسه (baseline) در نظر می‌گیریم. با توجه به راهبردهای مختلف راه‌اندازی مجددی که ارائه کرده‌ایم، هر راهبرد را بر پایه ++AFL پیاده‌سازی کرده و آن را به‌ عنوان یک فازر مستقل به FuzzBench اضافه می‌کنیم. این کار امکان مقایسه مستقیم با مبنای اصلی و همچنین با سایر راهبردهای پیشنهادی را فراهم می‌سازد.

محاسبه پوشش (Coverage Computation). ما برای محاسبه پوشش کد (coverage) از FuzzBench که به صورت داخلی از llvm-lcov استفاده می‌کند، بهره گرفتیم. FuzzBench یک گزارش تفصیلی تولید می‌کند که نه‌تنها میزان پوشش به‌دست‌آمده در آزمایش انجام‌ شده را نمایش می‌دهد، بلکه اطلاعات تکمیلی دیگری نیز مانند ارزیابی آماری را فراهم می‌کند تا معناداری آماری نتایج تأیید شود. ما برای هدف GNU objdump که خارج از FuzzBench اجرا کردیم نیز از llvm-lcov به منظور محاسبه معیارهای پوشش استفاده کردیم. برای این منظور، تمامی ورودی‌های تولید شده توسط فازرها برای آن هدف را روی یک باینری پوشش یکسان (coverage binary) اجرا نمودیم تا سازگاری و قابلیت مقایسه نتایج حفظ شود. این رویکرد امکان یک ارزیابی استاندارد شده از پوشش کد را میان راهبردهای مختلف فازینگ فراهم می‌کند و در نتیجه به استحکام و قابلیت اعتماد ارزیابی‌های تجربی ما کمک می‌کند.

   ۵.۱ آزمایش‌ها (Experiments)

ما به منظور ارزیابی اثربخشی راه‌اندازی مجدد فازر، راهبردهای مختلف راه‌اندازی مجدد خود را در برابر ++AFL و بر روی مجموعه برنامه‌های هدف منتخب (در مجموع ۱۵ هدف) مقایسه کردیم. علاوه بر آزمایش‌های مربوط به راهبردهای مختلف راه‌اندازی مجدد، یک آزمایش نیز با استفاده از afl-cmin و همچنین یک آزمایش بلندمدت ۷ روزه با استفاده از راهبرد گروهی (Ensemble) صورت پذیرفت. هر هدف به مدت ۲۴ ساعت (به‌جز آزمایش بلندمدت) و با ۱۰ تکرار اجرا گردید. به منظور نمایش نتایج، از میانگین (median) تمام اجراها و فاصله زمانی کوتاه‌ شده ۶۰٪ (60% trimmed interval) استفاده کردیم تا اثر داده‌های پرت (outliers) حذف گردد. بطور خلاصه، این مقاله نتایج تنها زیرمجموعه‌ای از اهداف را ارائه می‌دهد، در حالی که مجموعه کامل نتایج برای تمام اهداف در خروجی‌های تولید شده (artifact) پژوهشی به ‌صورت آنلاین در دسترس می‌باشد. ما در مجموع، شش آزمایش زیر را برای پاسخ به پرسش‌های پژوهشی خود به انجام رساندیم:

  1. آزمایش پوشش (coverage) اصلی که در مجموع نه هدف را پوشش می‌دهد.
  2. آزمایش پوشش برای بررسی اثر کمینه‌سازی مجموعه (corpus minimization) پس از راه‌اندازی مجدد.
  3. آزمایش پوشش بلندمدت (با زمان اجرای ۷ روز برای هر فازر) که از راهبرد راهبرد گروهی (Ensemble) ما استفاده می‌کند.
  4. آزمایش پوشش برای بررسی زمان‌های ثابت راه‌اندازی مجدد.
  5. آزمایش نمونه‌برداری (sampling experiment) روی اهداف SQLite3، Libpcap و libpng.
  6. آزمایش کشف باگ (bug experiment) روی ۸ هدف از FuzzBench.

آزمایش ۱: آزمایش پوشش (Coverage Experiment). نتایج آزمایش اصلی پوشش در شکل 4 و آمارهای مربوطه در جدول 2 ارائه شده است. به‌طور کلی، نتایج ما نشان می‌دهد که راه‌اندازی مجدد فازر تأثیر مثبتی بر پوشش کد در تمامی اهداف انتخاب ‌شده دارد؛ به‌طوری که حداقل یکی از راهبردهای راه‌اندازی مجدد از نظر تعداد شاخه‌های پوشش ‌داده ‌شده، عملکرد بهتری نسبت به مبنای ++AFL نشان می‌دهد. این مشاهده با نتایج گزارش کامل پوشش بر روی هر ۹ هدف موجود در خروجی‌های تولید شده (artifact) پژوهشی نیز هم‌راستا است.

در مقایسه عملکرد راهبردهای مختلف راه‌اندازی مجدد، مشخص می‌شود که اثربخشی آن‌ها به‌ شدت به خود هدف (target) وابسته است. در سه مورد از شش هدف، راهبرد هرس کردن مجموعه ورودی (corpus pruning) بهترین عملکرد را دارد؛ به ‌ویژه در مورد GNU objdump، که در آن این روش به‌ طور قابل‌توجهی پوشش بیشتری نسبت به هر دو راهبرد بازنشانی (reset) و درهم‌سازی ورودی‌ها (input shuffle) به دست می‌آورد و همچنین اندکی از ++AFL نیز بهتر عمل می‌کند. همچنین عملکرد مشابهی در مورد SQLite مشاهده می‌شود، جایی که هرس کردن مجموعه ورودی از هر دو روش ++AFL و درهم‌سازی ورودی‌ها پیشی می‌گیرد.

در مقایسه بین بازنشانی (reset) و درهم‌سازی ورودی‌ها (input shuffle)، میانگین تعداد شاخه‌های پوشش ‌داده ‌شده در روش بازنشانی در چهار مورد از شش هدف از درهم‌سازی ورودی‌ها بهتر عمل می‌کند (رجوع شود به شکل 4 و جدول 2)، در حالی که در دو مورد از این چهار هدف، مقدار میانگین در فواصل 60٪ کوتاه شده (trimmed intervals) قرار می‌گیرد.

برای تأیید آماری نتایج خود، مطابق با توصیه‌های آرکوری (Arcuri) و بریاند (Briand) [20] از آزمون ناپارامتری دوسویه Mann–Whitney U و همچنین اندازه اثر (effect size) بر اساس آزمون Â12 وارهگا و دلانی [19] استفاده می‌کنیم. نتایج این آزمون‌ها در جدول 2 ارائه شده‌اند.

برای راهبرد هرس کردن مجموعه ورودی (corpus pruning)، تمام نتایج (به‌ جز مورد مربوط به MuPDF در مقایسه با مبنا) از نظر آماری معنادار و قابل توجه هستند و اندازه‌های اثر (effect sizes) نیز بزرگ گزارش شده‌اند. در مورد MuPDF، اندازه اثر در حد متوسط است، اما تفاوت از نظر آماری ناچیز نیست. در خصوص راهبرد درهم‌سازی ورودی‌ها (input shuffle)، تنها دو مورد (Bloaty و GNU objdump) از نظر آماری معنادار و قابل توجه هستند که در هر دو، اندازه اثر بزرگ است؛ با این حال، در هدف objdump اندازه اثر مشاهده ‌شده منفی است. در نهایت، راهبرد بازنشانی (reset)‌ در هر شش هدف ذکر شده دارای معناداری آماری بوده و اندازه اثر آن نیز بزرگ است. با این حال، اندازه اثر برای دو مورد از شش هدف منفی است (objdump و SQLite3)، که نشان می‌دهد تنظیم مجدد توسط ++AFL بهتر عمل می‌کند.

جدول 2: تحلیل آماری آزمایش پوشش کد. در این جدول، تحلیل آماری آزمایش پوشش کد ارائه شده است؛ با این فرضیه که هر tweak  عملکرد بهتری نسبت به baseline دارد. برای سنجش اندازه اثر (effect size) از مقدار میانگین شاخه‌های پوشش‌داده‌ شده و آزمون Â12 وارهگا و دلانی (Vargha and Delaney’s Â12 test)  استفاده شده است. نتایج بر اساس دسته‌بندی این آزمون گزارش می‌شوند:
L
 = بزرگ (large)، M = متوسط (medium)، S = کوچک (small)؛ همچنین علامت منفی نشان‌ دهنده اندازه اثر منفی است، به این معنا که عملکرد tweak از baseline ضعیف‌تر است [19]. برای بررسی معناداری آماری نیز از آزمون ناپارامتری دوسویه Mann–Whitney U  استفاده شده و مقادیر p < 0.05 به ‌صورت بولد مشخص شده‌اند:

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
شکل ۴: پوشش کد (تعداد شاخه‌ها) برای ابزارهای مختلف در اجرای ۲۴ ساعته. در این شکل، میزان پوشش کد بر حسب تعداد شاخه‌ها (branches) برای ابزارهای مختلف در اجرای ۲۴ ساعته نمایش داده شده است. مقادیر ارائه‌ شده شامل میانگین پوشش در بین ۱۰ تکرار آزمایش و همچنین در فواصل 60٪ کوتاه شده (trimmed intervals) هستند.

برای تحلیل تکنیک‌های خود، تعداد راه‌اندازی‌های مجدد (restarts) انجام‌ شده توسط راهبردهای هرس کردن مجموعه ورودی‌ها (corpus pruning)، بازنشانی (reset) و درهم‌سازی ورودی (input shuffle) را در طول ارزیابی پایش می‌کنیم؛ که خلاصه آن در جدول 2 ارائه شده است.

به منظور نشان دادن تأثیر واقعی راه‌اندازی مجدد (restart) بر پوشش، میانگین اجرای چهار هدف را در شکل 5 ترسیم کرده‌ایم و محل وقوع هر راه‌اندازی مجدد را با نماد مربوطه مشخص کرده‌ایم. همان‌طور که مشاهده می‌شود، راه‌اندازی مجدد لزوماً منجر به یک جهش فوری در پوشش جدید نمی‌شود؛ بلکه می‌تواند به رشد تدریجی و پایدار پوشش منجر شود. داده‌ها نشان می‌دهند که راهبرد هرس کردن مجموعه ورودی‌ها (corpus pruning) در مقایسه با درهم‌سازی ورودی (input shuffle) به‌ طور قابل‌توجهی به تعداد کمتری راه‌اندازی مجدد نیاز دارد (تا ۱۳ برابر کمتر، با میانگین ۱۲ راه‌اندازی مجدد در تمامی اهداف)، در حالی که همچنان در پنج مورد از شش هدف، پوشش بالاتری را به دست می‌آورد.

در مقایسه تعداد راه‌اندازی‌های مجدد بین هرس کردن مجموعه ورودی‌ها و بازنشانی، مشاهده می‌شود که راهبرد بازنشانی نیز تعداد راه‌اندازی مجدد بیشتری انجام می‌دهد (با میانگین ۲۵ راه‌اندازی مجدد در تمامی اهداف)، هرچند تنها در دو مورد از شش هدف، آن هم به‌ صورت جزئی، عملکردی بهتر نسبت به هرس کردن مجموعه ورودی‌ها دارد. با مقایسه تعداد راه‌اندازی‌های مجدد با پوشش شاخه‌ای به ‌دست‌آمده و راهبرد حفظ مجموعه ورودی (corpus) مربوطه، مشخص می‌شود که موفقیت روش ما صرفاً به تعداد دفعات راه‌اندازی مجدد وابسته نیست. بلکه این موفقیت به‌شدت به تعامل میان راهبرد حفظ مجموعه ورودی مورد استفاده و هدف تحت آزمایش وابسته است.

بررسی دقیق‌تر رابطه میان فراوانی راه‌اندازی مجدد و نحوه حفظ مجموعه ورودی نشان می‌دهد که تعداد بالای راه‌اندازی‌های مجدد یا حتی حفظ کامل مجموعه ورودی لزوماً مزیت محسوب نمی‌شود. نتایج نشان حاکی از آن است که حفظ به‌ صورت پویا و انتخابی بخش‌هایی از مجموعه ورودی می‌تواند منجر به تعداد کمتری راه‌اندازی مجدد شود. از آنجا که هر سه راهبرد از یک ابتکار مشترک برای راه‌اندازی مجدد یعنی رشد پوشش (coverage growth) استفاده می‌کنند، نتایج ما نشان می‌دهند که:

(۱) حفظ کامل مجموعه ورودی‌ها (preserving the entire corpus) اغلب موجب می‌شود که مجموعه ورودی‌ پس از مدت کوتاهی دچار اشباع (saturation) شود. استفاده از چنین مجموعه ورودی‌ اشباع‌ شده‌ای به‌ عنوان مجموعه بذر ورودی اولیه (initial seed corpus) برای فازر، یافتن ورودی‌های جدیدی که بتوانند پوشش تازه‌ای ایجاد کنند را دشوار می‌سازد. در نتیجه، رشد پوشش (coverage growth) به‌سرعت متوقف یا کُند شده و این وضعیت موجب فعال شدن راه‌اندازی مجدد (restart) دیگری می‌شود. در حالی که راه‌اندازی مجدد مزیت بازنشانی کامل بیت‌مپ (bitmap) فازر را فراهم می‌کند، همچنان از مشکل اشباع مجموعه ورودی‌ رنج می‌برد.

(۲)‌ در مقابل، عدم حفظ هیچ بخشی از مجموعه ورودی‌ (preserving nothing from the corpus) و کنار گذاشتن کامل پیشرفت قبلی فازر، معمولاً به تعداد بیشتری راه‌اندازی مجدد (restart) منجر می‌شود. مشکل اصلی در این حالت آن است که نخستین راه‌اندازی مجدد (پس از شروع از یک وضعیت کاملاً تازه) با دفعات بیشتری رخ می‌دهد، زیرا فازر همواره با همان مجموعه ثابت از فایل‌های بذر اولیه (initial seed files) آغاز می‌کند. این موضوع موجب می‌شود رسیدن به مسیرهای عمیق‌تر کد پیش از وقوع راه‌اندازی مجدد بعدی دشوارتر شود. مشابه حالت (۱)، راه‌اندازی مجدد در اینجا نیز مزیت بازنشانی کامل بیت‌مپ (bitmap) فازر را فراهم می‌آورد، اما از این ضعف رنج می‌برد که در اجرای بعدی، فازر دوباره با همان مجموعه بذرها (seed) آغاز می‌کند؛ در نتیجه، همواره از یک نقطه آغاز یکسان به جست‌وجو می‌پردازد

به ‌عنوان یک راهکار بینابینی میان بازنشانی (reset) و درهم‌سازی ورودی (input shuffle)، طبیعی است که راهبرد هرس کردن مجموعه ورودی (corpus pruning) در تقریباً تمامی اهداف بهترین عملکرد را نشان دهد. با این حال، با وجود اثرات منفی مطرح ‌شده، مشاهده می‌کنیم که در برخی اهداف، هر دو راهبرد بازنشانی و درهم‌سازی ورودی عملکردی تقریباً هم‌سطح یا حتی اندکی بهتر از هرس کردن مجموعه ورودی دارند (برای مثال، استفاده از reset در MuPDF). پس از بررسی دقیق‌تر این اهداف، به این نتیجه رسیدیم که این موارد احتمالاً شرایطی هستند که در آن‌ها مزایای راه‌اندازی مجدد با دفعات بیشتر بر معایب مطرح ‌شده در بندهای (۱) و (۲) غلبه می‌کند. در مجموع، این نتایج نشان می‌دهد که راه‌اندازی مجدد فازر در طول یک کمپین فازینگ واقعاً تأثیر مثبتی بر پوشش کد دارد و بر همین اساس، می‌توانیم به پرسش پژوهشی دوم پاسخ مثبت دهیم.

آزمایش ۲: کمینه‌سازی مجموعه ورودی‌ها پس از حفظ مجموعه ورودی‌ (Corpus Minimization after Corpus Retention). بسته به هدف مورد آزمایش، راهبردهای حفظ مجموعه ورودی‌ (corpus retention) ما اغلب حجم زیادی داده تولید می‌کنند؛ زیرا با هر راه‌اندازی مجدد (restart)، ورودی‌هایی که پیش‌تر توسط فازر کشف شده‌اند ممکن است دوباره کشف شوند (البته همچنان لازم است صف قبلی را برای محاسبه پوشش کد نگه‌داری کنیم). این مسئله موجب می‌شود میزان فضای دیسک موردنیاز برای ذخیره مجموعه ورودی‌ در هر اجرا به‌ طور پیوسته افزایش یابد. علاوه بر افزایش مصرف فضای ذخیره‌سازی، بزرگ بودن مجموعه بذرهای ورودی (seed corpus) یک نقطه‌ ضعف دیگر نیز دارد: در این حالت، ++AFL می‌بایست در مرحله کالیبراسیون اولیه (startup calibration phase) تمامی این فایل‌ها را پردازش کند.

از این‌رو، به نظر می‌رسد استفاده از یک مجموعه داده فشرده‌تر برای اجرای بعدی پس از راه‌اندازی مجدد، ترجیح داده می‌شود. ++AFL ابزاری با نام afl-cmin ارائه می‌دهد که می‌توان از آن برای کمینه‌سازی مجموعه ورودی استفاده کرد. این اسکریپت با استفاده از AWK و ابزار afl-showmap، مجموعه‌ای از فایل‌های ورودی را کاهش می‌دهد. وظیفه اصلی آن، شناسایی و نگه‌داری کوچک‌ترین مجموعه ممکن از نمونه‌های آزمون منحصربه‌فرد است؛ به‌گونه‌ای که این مجموعه، در کنار هم، مسیرهای متنوع کد که توسط مجموعه اولیه پوشش داده شده‌اند را همچنان حفظ کند. این ابزار با تحلیل خروجی ابزارگذاری (instrumentation output) تولید شده توسط ++AFL از طریق afl-showmap، فایل‌های ورودی را در اولویت حفظ و نگهداری قرار می‌دهد که سهم متمایزی در پوشش کد دارند و هدف آن کاهش افزونگی (redundancy) در مجموعه ورودی‌ها است.

ما این قابلیت را در زمانبند (scheduler) خود گنجاندیم تا تأثیر کمینه‌سازی مجموعه ورودی‌ها (corpus minimization) پس از راه‌اندازی مجدد (restart) را مورد بررسی قرار دهیم و آزمایشی را با استفاده از راهبردهای هرس کردن مجموعه ورودی (corpus pruning) و درهم‌سازی ورودی (input shuffle) با afl-cmin به انجام رساندیم. لازم به ذکر است که به دلیل وجود یک باگ در AFL++ 4.04c، نسخه تمامی فازرها را برای این آزمایش به نسخه جدیدتر 4.06c تغییر دادیم. هنگامی که راه‌اندازی مجدد انجام می‌شود، afl-cmin فرایند کمینه‌سازی مجموعه ورودی‌ها را انجام می‌دهد و فایل‌های باقی‌مانده سپس به عنوان بذر (seed) برای اجرای بعدی مورد استفاده قرار می‌گیرند.

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
شکل ۵: پوشش (در شاخه‌ها) برای چهار ابزار مختلف با اجرای ۲۴ ساعته. در این شکل میانگین اجرای ده تکرار نمایش داده شده است و برای Sileo، نقاط مربوط به راه‌اندازی مجدد (restarts) مشخص شده‌اند. . برای مشاهده نمودارهای مربوط به تمام ابزارهای آزمایش شده، به خروجی‌های تولید شده (artifact) پژوهشی ما در GitHub مراجعه کنید.

در شکل 6، عملکرد ++AFL با راهبردهای هرس کردن مجموعه ورودی (corpus pruning) و درهم‌سازی ورودی (input shuffle) در نسخه‌هایی که از cmin استفاده می‌کنند مقایسه شده است. نمودار نشان می‌دهد که استفاده از afl-cmin اثر منفی بر عملکرد هرس کردن مجموعه ورودی دارد و موجب می‌شود رشد پوشش زودتر به حالت اشباع (flattening) برسد. در مقابل، تأثیر cmin بر درهم‌سازی ورودی ناچیز است. نکته جالب این است که راهبردهای مبتنی بر cmin در ابتدا تقریباً عملکردی مشابه نسخه‌های بدون cmin دارند، اما رشد پوشش آن‌ها زودتر متوقف می‌شود. این رفتار در آزمایش با توجه به پدیده سایه‌سازی ورودی (input shadowing) قابل انتظار است؛ جایی که فرآیند کمینه‌سازی (که صرفاً بر اساس پوشش کد عمل می‌کند) ممکن است ورودی‌هایی را حذف کند که در عمق بیشتر اکتشاف کد نقش دارند. در نتیجه، توانایی فازر برای کشف مسیرهای جدید تا حدی کاهش می‌یابد و در نهایت پوشش کمتری در طول زمان به دست می‌آید.

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
شکل ۶: تأثیر استفاده از afl-cmin پس از هر راه‌اندازی مجدد برای کمینه‌سازی مجموعه بذرهای ‌ورودی به منظور اجرای بعدی. مقادیر ارائه ‌شده شامل میانگین پوشش در بین ۱۰ تکرار و همچنین در فواصل 60٪ کوتاه شده (trimmed intervals) می‌باشند.

آزمایش ۳: اجرای بلندمدت و گروهی (Ensemble). ما یک اجرای ۷ روزه روی SQLite  انجام دادیم تا بررسی کنیم آیا ++AFL می‌تواند در بازه زمانی طولانی‌تر به عملکرد Sileo نزدیک شود یا خیر؛ زیرا نتایج اجرای ۲۴ ساعته حاکی از آن بودند که ++AFL به‌تدریج در طول زمان به Sileo نزدیک می‌شود. علاوه بر این، چنین اجرای طولانی‌مدتی امکان ارزیابی راهبرد گروهی ما را نیز فراهم می‌کند؛ راهبردی که برای شناسایی و بهره‌برداری از بهترین استراتژی عملکردی، به چندین راه‌اندازی مجدد (restart) نیاز دارد. همان‌طور که در شکل 7 نشان داده شده است، ++AFL در طول زمان به هرس کردن مجموعه ورودی (corpus pruning) و گروهی (ensemble) نزدیک می‌شود، اما مقدار میانگین پوشش هرگز به سطح مشابه این دو راهبرد در Sileo نمی‌رسد.

با مقایسه میان هرس کردن مجموعه ورودی (corpus pruning) و گروه (ensemble)، مشاهده می‌شود که هر دو در تمام بازه زمانی فازینگ، تقریباً عملکرد یکسانی دارند. این نتیجه تا حدی قابل انتظار است، چرا که گروه (ensemble)، حالت‌های خود را از میان راهبردهای حفظ مجموعه ورودی (corpus retention strategies) انتخاب می‌کند، و هیچ‌یک از این حالت‌ها در این هدف خاص عملکردی بهتر از هرس کردن مجموعه ورودی (corpus pruning)، سفر در زمان (time travel) یا گسترش‌دهنده درخت جستجو (tree planter) نداشته‌اند. بنابراین، گروه (ensemble) تنها می‌تواند حداکثر به اندازه بهترین حالت برای این هدف عمل کند. این رفتار در فایل‌های گزارش (Log) ما نیز مشهود است؛ به‌طوری که گروه (ensemble) عمدتاً میان همین سه حالت جابه‌جا می‌شود. تعداد راه‌اندازی‌های مجدد (restarts) برای گروه (ensemble) در این هدف بین ۱۶ (در بهترین حالت: tree planter) تا ۸۹ مرتبه (در بهترین حالت: time travel) متغیر است، و مقدار میانگین آن ۳۱ راه‌اندازی مجدد (restart) در حالت هرس کردن مجموعه ورودی (corpus pruning) است.

ما یک هدف دوم، Bloaty را انتخاب کردیم تا عملکرد گروه (ensemble) را با یک زمان اجرای کوتاه‌تر (۲۴ ساعت) ارزیابی کنیم. این هدف در ۲۴ ساعت نخست، در تمام راهبردهای ما تعداد بالایی از راه‌اندازی مجدد (restarts) را تجربه می‌کند؛ از این رو، برای آزمایش با گروه مناسب است.

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
شکل 7: در این شکل، یک اجرای ۷ روزه روی هدف SQLite و همچنین یک اجرای دیگر روی Bloaty نمایش داده شده است که در آن عملکرد ++AFL، هرس کردن مجموعه ورودی و گروه مقایسه شده‌اند.

در این آزمایش، ما بهترین عملکرد راهبرد واحد (گسترش‌دهنده درخت جستجو (tree planter)) را در کنار حالت هرس کردن مجموعه ورودی (corpus pruning) و گروه (ensemble) ترسیم کردیم. بررسی نمودار پوشش در شکل 7 نشان می‌دهد که در شش ساعت نخست، گروه (ensemble) در مقایسه با حالت هرس کردن مجموعه ورودی و گسترش‌دهنده درخت جستجو (tree planter) عملکرد بهتری از خود نشان می‌دهد و همچنین دارای واریانس پوشش بیشتری است. گروه (ensemble) با پیشرفت آزمایش، همچنان در مجموع اندکی واریانس بالاتر نسبت به گسترش‌دهنده درخت جستجو (tree planter) و حالت هرس کردن مجموعه ورودی حفظ می‌کند، اما در نهایت تقریباً به همان مقدار میانگین پوشش گسترش‌دهنده درخت جستجو (tree planter) دست می‌یابد.

با نگاهی دقیق‌تر، مشاهده می‌کنیم که تعداد راه‌اندازی‌های مجدد در این هدف بین ۱۲ تا ۶۰ مرتبه، با میانگین ۱۸ راه‌اندازی مجدد، متغیر است. با بررسی بهترین راهبرد انتخاب‌ شده توسط گروه (ensemble) پس از ۲۴ ساعت (در ۱۰ تکرار آزمایش)، نتایج زیر حاصل می‌شود:

  • ۱۰/۴ درهم‌سازی ورودی (input shuffle)، تعداد شروع یا راه‌اندازی مجدد: ۱۳، ۱۴، ۵۸، ۶۰
  • ۱۰/۲ گسترش‌دهنده درخت جستجو (tree planter)، تعداد راه‌اندازی مجدد: ۲۷، ۳۰
  • ۱۰/۲ سفر در زمان (time travel)، تعداد راه‌اندازی مجدد: ۱۲، ۱۹
  • ۱۰/۲ هرس کردن مجموعه ورودی (corpus pruning)، تعداد راه‌اندازی مجدد: ۱۳، ۱۸

نکته جالب این است که بهترین راهبرد واحد از نظر عملکرد، یعنی گسترش‌دهنده درخت جستجو (tree planter)، در این هدف بیشترین استفاده را در گروه (ensemble) نداشته است؛ بلکه درهم‌سازی ورودی (input shuffle) بیشترین کاربرد را به خود اختصاص داده است. با این حال، گروه همچنان به میانگین پوشش بالاتری نسبت به درهم‌سازی ورودی دست می‌یابد که این موضوع نشان‌ دهنده مزایای استفاده از راهبردهای مختلف برای یک هدف واحد است؛ زیرا این ترکیب می‌تواند اثرات هم‌افزایی (synergy) ایجاد کند و به ‌طور بالقوه عملکرد کلی را بهبود بخشد.

آزمایش ۴: زمان‌های ثابت راه‌اندازی مجدد و بررسی آن (Fixed Restart Times and Restart Examination). به منظور ارزیابی اثرات زمان‌های مختلف راه‌اندازی مجدد (restart) و مقایسه کارایی زمان انتخاب ‌شده به‌صورت تطبیقی با زمان‌های ثابت، چند فازر مختلف در FuzzBench بر پایه هرس کردن مجموعه ورودی (corpus pruning) ایجاد کردیم. این فازرها از زمان‌های ثابت شروع یا راه‌اندازی مجدد (restart) شامل ۳۰ دقیقه، ۶۰ دقیقه و ۲۴۰ دقیقه استفاده می‌کنند. این راهبردها را روی تمامی اهداف خود آزمایش و زیرمجموعه‌ای از نتایج را در شکل ۸ ارائه کرده‌ایم.

به منظور دستیابی به داده‌های کامل تمامی اهداف، لطفاً به خروجی‌های تولید شده (artifact) پژوهشی ما مراجعه کنید. علاوه بر پوشش به‌دست‌آمده، در این آزمایش رشد اندازه مجموعه (corpus) برای هر راهبرد را نیز بررسی می‌کنیم (نمودارهای سمت راست شکل ۸). با بررسی دقیق‌تر نمودارهای پوشش مشخص می‌شود که زمان ثابت ۳۰ دقیقه برای راه‌اندازی مجدد، تأثیر منفی بر تعداد شاخه‌های پوشش ‌داده‌ شده در هر سه هدف ترسیم‌ شده دارد. زمان‌های طولانی‌تر ۶۰ و ۲۴۰ دقیقه اندکی بهتر از هرس کردن مجموعه ورودی (corpus pruning)‌ در دو مورد از سه هدف عمل می‌کنند. در هدف SQLite، هم راهبرد هرس کردن مجموعه ورودی (corpus pruning) با زمان تطبیقی راه اندازی مجدد (restart) و هم هرس کردن مجموعه ورودی با زمان ثابت ۲۴۰ دقیقه از سایر راهبردها و همچنین از خط پایه ++AFL عملکرد بهتری نشان می‌دهند.

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

فازرهایی که زمان‌های شروع یا  راه‌اندازی مجدد (restart) کوتاه‌تری دارند، نمی‌توانند برای هر اجرا، مجموعه بزرگی تشکیل دهند، زیرا راه‌اندازی‌های مکرر مانع رشد آن می‌شود. با این حال، علی‌رغم کوچک بودن اندازه مجموعه در مقایسه با دو راهبرد دیگر، راه‌اندازی هر ۶۰ دقیقه در دو مورد از سه هدف هیچ کاهش عملکردی از نظر پوشش ایجاد نمی‌کند. همچنین باید تأکید کرد که صرفاً پایش اندازه مجموعه تنها میزان اطلاعات حذف ‌شده بین اجراها را نشان می‌دهد، اما رابطه مستقیمی با پوشش ندارد؛ زیرا پوشش را بر اساس صف تمام اجراهای شروع یا  راه‌اندازی مجدد شده (restart) اندازه‌گیری می‌کنیم (نه فقط آخرین اجرا).

با وجود عملکرد خوب هرس مجموعه ورودی (corpus pruning)‌ با راه‌اندازی‌های مجدد (restart) ثابت روی این اهداف، همچنان هرس کردن مجموعه ورودی با راه‌اندازی‌های مجدد تطبیقی، همچنان اندکی عملکرد بهتری نسبت به سایر راهبردها در تمامی اهداف دارد. بطور کلی، این روش انعطاف‌پذیری بهتری نشان می‌دهد و می‌تواند واکنش مناسب‌تری نسبت به هدف تحت آزمون داشته باشد، به‌طوری که تعداد متعادلی از راه‌اندازی‌های مجدد (restart) را فعال می‌کند.

یکی دیگر از معایب زمان‌های ثابت برای شروع مجدد، افزایش سربار مربوط به مصرف دیسک با هربار راه‌اندازی‌ مجدد است، به‌ویژه در زمان‌های کوتاه‌تر. به عنوان مثال، اگر زمان شروع مجدد ثابت ۶۰ دقیقه باشد، در طول ۲۴ ساعت حدود ۲۳ بار راه‌اندازی‌ مجدد رخ می‌دهد که تنها بهبود اندکی در پوشش نسبت به حالتی دارد که فقط حدود ۷ بار یا کمتر راه‌اندازی‌ مجدد انجام می‌شود، اما در مقابل بیش از دو برابر فضای دیسک مصرف می‌گردد.

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
شکل ۸: (سمت چپ): پوشش (بر حسب شاخه‌ها) برای سه هدف SQLite3، libxml2 و readelf با زمان‌های ثابت برای راه‌اندازی مجدد. (سمت راست): اندازه مجموعه برای AFL++، corpus pruning و corpus pruning همراه با زمان‌های ثابت مذکور برای راه‌اندازی مجدد، با هدف نمایش تعداد فایل‌های مجموعه که توسط هر راه‌اندازی مجدد حفظ می‌شوند. در هر نمودار، میانگین اجرای ۱۰ تکرار نمایش داده شده است و برای Sileo نقاط مربوط به راه‌اندازی مجدد مشخص شده‌اند.

جدول ۳: پوشش شاخه‌ای از libpng، Libpcap و SQLite3 در میانگین اجرای ۱۰ تکرار ۲۴ ساعته، و همچنین تعداد برخورد با بلوک‌های پایه‌ای که بیش از دو برابر بیشتر توسط خط مبنا (رنگ آبی) یا فازر بازراه‌اندازی‌ شده ما (رنگ قرمز) تحریک شده‌اند. برای حالت بدون بازراه‌اندازی‌، از میانگین منهای یک اجرا (median − 1 run) استفاده شده است. پس‌زمینه خاکستری نشان‌دهنده بهترین مقدار از نظر میانگین پوشش شاخه‌ها است. مقادیر بولد نشان‌دهنده بهترین عملکرد از نظر تعداد بلوک‌های پایه ترجیحی برای هر راهبرد می‌باشند:

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer

آزمایش ۵: نمونه‌برداری و فراوانی بلوک‌های پایه (Sampling and Basic Block Frequency). فراتر از پوشش کد، ما مزایای راه‌اندازی مجدد فازر را از نظر توزیع بهتر فراوانی برخورد با بلوک‌های پایه (basic block hits) بررسی می‌کنیم؛ یعنی اینکه آیا بازراه‌اندازی‌ها (‌restart) می‌توانند موجب شوند تا توجه فازر به مجموعه متنوع‌تری از بلوک‌های پایه گسترش یابد یا خیر. برای این منظور، از ورودی‌های نمونه‌برداری‌شده در طول اجرای فازینگ (مشابه آزمایش بخش ۲) استفاده کردیم. نکته مهم این است که برای جلوگیری از سوگیری در نتایج، از مجموعه (corpus) استفاده نکردیم. به منظور بررسی این رفتار، زیرمجموعه‌ای از تمام ورودی‌های اجراشده توسط ++AFL، هرس کردن مجموعه ورودی (corpus pruning) و بازنشانی (reset) را روی سه هدف libpng، Libpcap و SQLite3 نمونه‌برداری کردیم. شایان ذکر است که برخلاف آزمایش بخش ۲، در اینجا راهبردهای Sileo از اکتشافات رشد پوشش (coverage growth heuristic) برای تعیین زمان راه اندازی مجدد استفاده می‌کنند، نه زمان‌های ثابت راه‌اندازی مجدد.

ما مقادیر میانگین پوشش را برای این آزمایش در جدول ۳ گزارش کرده و نقشه‌های حرارتی (heatmaps) متناظر را در شکل ۹ نمایش می‌دهیم. علاوه بر این، یک نمایش جایگزین در شکل ۱۰ ارائه می‌کنیم که توزیع برخوردهای بلوک‌های پایه (basic block hits) را به‌صورت تابع توزیع تجمعی (CDF) برای هر سه هدف نشان می‌دهد.

در این نمایش، بررسی می‌کنیم هر یال (edge) چند بار اجرا شده و آن‌ها را بر اساس تعداد دفعات اجرا مرتب می‌کنیم (به گونه‌ای که یالی که بیشترین تعداد برخورد را دارد در x = 0 قرار می‌گیرد و یالی که کمترین تعداد برخورد را دارد در سمت راست نمودار قرار می‌گیرد). برای مثال، نقطه ورود برنامه (entry point) که معمولاً بیشترین میزان برخورد را دارد، در سمت چپ نمودار قرار می‌گیرد. محور y نشان‌ دهنده فراوانی برخورد برای تمام بلوک‌های پایه است. توجه داشته باشید که این مرتب‌سازی برای هر فازر به‌ صورت جداگانه انجام می‌شود: بنابراین یال در x = 100 ممکن است بین ++AFL و هرس مجموعه ورودی (corpus pruning) متفاوت باشد، اما در هر دو حالت نشان می‌دهد که ۹۹ یال دیگر وجود دارند که حداقل به همان اندازه یا بیشتر از آن مورد برخورد قرار گرفته‌اند.

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer
Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing
شکل ۹: نقشه‌های حرارتی (heatmaps) که فراوانی برخورد با بلوک‌های پایه را در طول اجرای فازینگ میانگین ++AFL، هرس کردن مجموعه (corpus pruning) و تنظیم مجدد (reset) روی اهداف libpng، Libpcap و SQLite3 نشان می‌دهند. هر سلول نشان ‌دهنده یک بلوک پایه است؛ بلوک‌های آبی رنگ نشان می‌دهند که اجرای خط مبنای میانگین بیشتر به این بلوک پایه برخورد کرده است و رنگ قرمز خلاف آن را نشان می‌دهد.
فازینگ
فازینگ
شکل 10: نمودار فرکانس بلوک پایه libpng، Libpcap و SQLite3 که استراتژی‌های هرس کردن مجموعه، تنظیم مجدد و ++AFL پایه را نشان می‌دهد. توجه داشته باشید که ما یک شناسه منحصر به فرد به هر یال اختصاص می‌دهیم و تعداد دفعات بازدید از هر یال را پیگیری می‌کنیم: x = 0 نشان دهنده بیشترین تکرار یال است (یالی که معمولاً در هر اجرا، شرکت دارد، مانند نقطه ورود برنامه). در مقابل، مقدار سمت راست x نشان دهنده کمترین تکرار مشاهده شده است (اغلب یالی که هرگز بازدید نمی‌شود). سپس روی محور y، نشان می‌دهیم که هر یال خاص چند مرتبه مشاهده شده است. یک فازر زمانی عملکرد بهتری دارد که بتواند یال‌های نادر بیشتری را پوشش دهد (یعنی x بزرگتر باشد در حالی که y > 0) و همچنین این یال‌های نادر را با دفعات بیشتری مشاهده کند.

با تحلیل نمودارهای CDF (شکل ۱۰) مشخص می‌شود که در تمامی اهداف به ‌جز libpng، راهبرد هرس کردن مجموعه ورودی‌ها (corpus pruning)، یال‌ها را با دفعات بیشتری نسبت به خط مبنا فعال می‌کند، به‌ویژه برای یال‌هایی که کمتر مورد بازدید قرار می‌گیرند. نمودارهای CDF نشان می‌دهند که در هدف libpng، هر دو راهبرد هرس کردن مجموعه ورودی‌ و بازنشانی (reset) اکثر یال‌ها را با دفعات بیشتری نسبت به ++AFL فعال می‌کنند. این مشاهده همچنین توسط داده‌های جدول ۳ و نمایش بصری شکل ۹ تأیید می‌شود. هر دو نشان می‌دهند که راهبردهای Sileo در این زمینه نسبت به ++AFL مزیت دارند. در هدف Libpcap، تنها هرس کردن مجموعه ورودی عملکردی اندکی بهتر از ++AFL دارد، در حالی که بازنشانی فقط ۱۵۰۰ یال پرتکرار را با دفعات بیشتری نسبت به ++AFL فعال می‌کند.

در SQLite3، راهبرد هرس کردن مجموعه ورودی‌ها (corpus pruning)، تعداد یال‌های بیشتری را نسبت به ++AFL وبازنشانی (reset) پوشش می‌دهد. در نتیجه، این تنها فازری است که یال‌هایی را در بخش انتهایی سمت راست CDF فعال می‌کند. نکته جالب آن است که نمودار CDF نشان می‌دهد ++AFL در ۱۰٬۰۰۰ یال پرتکرارتر عملکرد بهتری نسبت به راهبردهای Sileo دارد، در حالی که مجموعه ورودی‌ها، یال‌های نادرتر (۱۰٬۰۰۰ یال کم‌تکرار) را با دفعات بیشتری پوشش می‌دهند. این رفتار همچنین در جدول ۳ و خود نقشه‌های حرارتی (شکل ۹) نیز قابل مشاهده است؛ جایی که نشان داده می‌شود ++AFL در SQLite3، با اختلاف قابل توجهی نسبت به هرس کردن مجموعه ورودی‌ و بازنشانی، بلوک‌های پایه را با دفعات بیشتری فعال می‌کند، اما در نهایت پوشش کمتری نسبت به هرس کردن مجموعه ورودی‌ به دست می‌آورد.

جالب اینجاست که این رفتار با مشاهدات ما در اهداف libpng و Libpcap در تضاد است. با این حال، این موضوع با بررسی دقیق‌تر نقشه‌های حرارتی قابل توضیح است: در این دو هدف، هر سه فازر تقریباً پوشش یکسانی به دست می‌آورند، در حالی که فراوانی برخورد با بلوک‌های پایه برای Sileo بالاتر است.

دلیل این اتفاق آن است که این اهداف در مقایسه با SQLite3 کوچک‌تر می‌باشند و رشد پوشش در همه فازرها در مراحل اولیه فازینگ به حالت اشباع (flattening) می‌رسد. با هر شروع مجدد (restart)، عمدتاً همان بخش‌های یکسانی از کد دوباره پوشش داده می‌شوند که این امر موجب افزایش فراوانی برخورد در این اهداف می‌شود.

در مقابل، SQLite3 که به ‌مراتب پیچیده‌تر از دو هدف دیگر است، نشان می‌دهد که فازرها در طول اجرای ۲۴ ساعته به حالت اشباع کامل نمی‌رسند و به‌طور مداوم پوشش جدید پیدا می‌کنند. ماهیت پویای SQLite3 و رشد پیوسته پوشش موجب می‌شود که هرس کردن مجموعه ورودی (corpus pruning) حتی پس از ۲۴ ساعت نیز بتواند بلوک‌های پایه جدید و کم‌تکرار را فعال کند. در این هدف، زمانی که هرس کردن مجموعه ورودی مجدداً راه‌اندازی می‌شود، از یک رکود (plateau) پوشش خارج شده و این امکان را پیدا می‌کند که به‌ صورت تصادفی اکتشاف را به بخش دیگری از برنامه هدایت کند. این رفتار در نقشه‌های حرارتی شکل ۹ قابل مشاهده است؛ جایی که هرس کردن مجموعه ورودی بیشتر بر «بخش‌های (islands)» متفاوت تمرکز دارد، در حالی که ++AFL اجرای خود را در بخش‌های باقی‌مانده برنامه توزیع می‌کند. مقایسه مستقیم نشان می‌دهد که این بخش‌های بلوکی قرمز، همان بخش‌هایی از کد هستند که توسط ++AFL اصلاً پوشش داده نشده‌اند (که به ‌صورت نواحی کاملاً سفید در مستطیل‌ها مشخص شده‌اند).

نتایج آزمایش درک ارزشمندی از تأثیر سایه‌سازی ورودی (input shadowing) بر انواع مختلف اهداف ارائه می‌دهد و در نتیجه به‌طور مؤثری به پرسش پژوهشی اول (RQ1) پاسخ می‌دهد. این نتایج نشان می‌دهند که یک فازر بسته به پیچیدگی هدف می‌تواند از راه‌اندازی مجدد (restart) مزایای متفاوتی به دست آورد و بدین ترتیب اثرات سایه‌سازی ورودی را تا حدی کاهش دهد. این یافته‌ها را می‌توان به مثال ارائه‌ شده در الگوریتم ۱ (بخش ۲.۲) مرتبط دانست: در حالتی که هدف کوچک است و فازر به ‌سرعت به حالت رکود یا خط صاف (flatline) می‌رسد، زمان‌های محاسباتی زیادی صرف جهش (mutation) روی ورودی‌هایی می‌شود که پیش‌تر شناخته شده‌اند تا مسیرهای عمیق‌تر کد پوشش داده شوند. با این حال، برخی از این مسیرهای کاوش‌ نشده ممکن است توسط ورودی‌های قبلاً کشف ‌شده سایه‌گذاری (shadowed) شده باشند که این موضوع روند پیشرفت را به ‌طور غیرضروری دشوار می‌کند.

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

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

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

آزمایش ۶: توانایی کشف باگ (Bug-finding ability). هدف اصلی یک فازر، شناسایی آسیب‌پذیری‌ها در نرم‌افزار است. بنابراین، ارزیابی این موضوع ضروری است که آیا تغییرات پیشنهادی در فرایند فازینگ تأثیری (مثبت یا منفی) بر توانایی فازر در کشف باگ‌ها دارند یا خیر. همان‌طور که در آزمایش‌های مربوط به پوشش نشان داده شد، Sileo توانایی خود را در دستیابی به پوشش بیشتر اثبات کرده است. با این حال، مشخص نیست که آیا این موضوع به توانایی بهتر در کشف باگ نیز منجر می‌شود یا خیر. در سناریوهایی که پوشش Sileo با خط پایه برابر است، Sileo همچنان این توانایی را دارد که بلوک‌ها را با دفعات بیشتری فعال کند؛ موضوعی که از نظر تئوری می‌تواند احتمال کشف باگ‌های بیشتر را افزایش دهد.

ما هشت آزمایش کشف باگ را با استفاده از FuzzBench انجام دادیم که تعداد کمی باگ در اهداف مشخص تزریق شده بود و در نتیجه یک حقیقت اساسی (ground truth) را در اختیار ما قرار داشت. نتایج بصورت خلاصه در جدول ۴ ارائه شده است. از آنجا که هیچ‌یک از فازرها نتوانستند در سه مورد از هشت هدف (arrow_parquet، file_magic و mbedtls) باگی را فعال کنند، این اهداف را در ادامه حذف می‌کنیم. گزارش کامل شامل تمامی اهداف در خروجی های تولید شده پژوهشی (artifact) ما قابل دسترسی می‌باشد. ما مجموعه ورودی‌های نهایی (final corpora) و دایرکتوری‌های کرش‌های (crashes directories) مربوط به ++AFL، هرس کردن مجموعه ورودی (corpus pruning) و گروه (ensemble) را تحلیل کردیم تا مشخص شود فازرها در طول ۱۰ اجرا چند مرتبه موفق به فعال‌سازی کرش شده‌اند. نتایج این بررسی در ستون Runs w/ Bugs جدول نمایش داده شده است.

نتایج حاکی از آن است که راهبردهای Sileo در تعداد بیشتری از اجراها موفق به فعال‌سازی باگ شده‌اند و همچنین، بر اساس گزارش FuzzBench، در مقایسه با ++AFL پایه ما، تعداد باگ‌های بیشتری نیز در تمام اهداف کشف کرده‌اند.

نکته قابل توجه این است که هرس کردن مجموعه ورودی (corpus pruning) در هدف Bloaty در ۷ مورد از ۱۰ اجرا و در هدف GNU Aspell در ۶ مورد از ۱۰ اجرا موفق به فعال‌سازی باگ شده است؛ در حالی که ++AFL در هر دو مورد تنها در یک اجرا موفق بوده است. علاوه بر این، هرس کردن مجموعه ورودی در ۲ مورد از ۱۰ اجرا برای libxml2 موفق به فعال‌سازی باگ شد، در حالی که ++AFL و گروه (ensemble) در این هدف موفقیتی نداشتند. در مجموع، هر دو راهبرد Sileo توانستند در تعداد بیشتری از اجراها باگ‌ها را فعال کنند؛ موضوعی که با نتایج و مشاهدات ما درباره سایه‌سازی ورودی (input shadowing) همخوانی دارد. این نتایج به ما اجازه می‌دهند که به پرسش پژوهشی سوم (RQ3) پاسخ مثبت دهیم. از این رو، می‌توان اذعان داشت که راه‌اندازی مجدد (restart) در فازینگ می‌تواند به کشف باگ‌های بیشتر در نرم‌افزار کمک کند.

   ۵.۲ سربار (Overhead)

راه‌اندازی مجدد فرایند فازینگ، به دلیل نحوه بازراه‌اندازی (restart) فازر پایه، انواع مختلفی از سربار (overhead) را ایجاد می‌کند. ما میزان مصرف فضای دیسک، کل زمان فازینگ و تعداد کل اجراها را پس از ۲۴ ساعت اندازه‌گیری کرده و این داده‌ها را در شکل ۱۱ ترسیم کرده‌ایم. علاوه بر ارزیابی سه هدف از آزمایش پوشش، سربار ناشی از آزمایش کمینه‌سازی مجموعه ورودی (corpus minimization) را نیز ارزیابی کردیم. فایل fuzzer_stats در ++AFL مقادیر مربوط به تعداد اجراها (execs_done) و زمان اجرای فازینگ (run_time) را فراهم می‌کند. ما همچنین، میزان مصرف فضای دیسک را برای ++AFL بر اساس تمامی فایل‌های موجود در صف (queue) و برای Sileo نیز بر اساس تمامی فایل‌های صف به ‌همراه فایل‌های بذر (seed) جدید تولید شده در هر اجرا محاسبه کردیم.

جدول ۴: باگ‌های کشف ‌شده در اجراهای ۲۴ ساعته روی پنج هدف از FuzzBench. در این جدول، تعداد کل باگ‌های یکتای کشف ‌شده، تعداد اجراهایی که در آن‌ها یک یا چند باگ شناسایی شده‌اند، و همچنین آمار مربوط به حداقل، متوسط و حداکثر تعداد باگ‌ها در هر اجرا ارائه شده است. برای خوانایی بهتر، مقادیر صفر با پس‌زمینه خاکستری نمایش داده شده‌اند:

 
 

 

Vulnerlab - والنرلب - فازینگ - فازر - Fuzzing - Fuzzer - سایه٬سازی ورودی - Input Shadowing

مصرف دیسک (Disk Usage). Sileo از نظر مصرف فضای دیسک، بیشترین سربار را نسبت به ++AFL نشان می‌دهد. به ‌ویژه، مصرف دیسک در حالت درهم‌سازی ورودی (input shuffle) به مقادیر بسیار بالایی می‌رسد؛ به‌طور مشخص در هدف bloaty، در حالی که ++AFL تنها ۱۱ مگابایت (MB) فضای دیسک مصرف می‌کند، Sileo  به ۳۶۸۷۰ مگابایت (MB) نیز می‌رسد (حدود 3352 برابر بیشتر از ++AFL). اگرچه مقدار مصرف در حالت‌های هرس کردن مجموعه ورودی (corpus pruning) و بازنشانی (reset) کمتر است، اما همچنان چندین برابر بیشتر از ++AFL باقی می‌ماند. مشکل اصلی به نگهداری مجموعه ورودی از اجراهای قبلی مربوط می‌شود، جایی که صف‌هایِ (queue) هر اجرایِ بازراه‌اندازی شده (restart) ذخیره می‌گردد.

فازر با هر راه‌اندازی مجدد، ورودی‌های مشابهی را کشف می‌کند که همان پوشش اجراهای قبلی را تولید می‌کنند و در نتیجه مزیت جدیدی ایجاد نمی‌شود، اما در عوض هزینه مصرف فضای دیسک افزایش می‌یابد (زیرا همان فایل‌ها چندین بار ذخیره می‌شوند). این رفتار در حالت درهم‌سازی ورودی (input shuffle) دارای بیشترین شدت است، جایی که کل مجموعه ورودی تولید شده استفاده و برای اجرای بعدی ذخیره می‌شود. در نتیجه، در هر اجرا دو دایرکتوری یکسان که شامل فایل‌های مشابه هستند ذخیره می‌شوند (صف فازر و دایرکتوری بذر‌های جدید). حفظ و نگهداری هر دو دایرکتوری ضروری است، زیرا در مرحله درهم‌سازی ورودی (input shuffle)، تمام فایل‌های ورودی تغییر نام داده می‌شوند و یک پیشوند تصادفی به هر فایل اضافه می‌شود؛ این موضوع یک لایه اضافی از تصادفی‌سازی را به فرایند پردازش بذرها در ++AFL اضافه می‌کند.

یک رویکرد جایگزین برای بهبود راهبرد درهم‌سازی ورودی (input shuffle) در آینده می‌تواند استفاده از آخرین دایرکتوری صف (queue directory) به عنوان دایرکتوری جدید بذرها (seed) بدون کپی کردن و تغییر نام تمام ورودی‌ها باشد. این کار موجب کاهش مصرف فضای دیسک می‌شود، بدون آنکه بر زمان اجرا، تعداد اجراها یا عملکرد کلی تأثیر بگذارد. با مقایسه هرس کردن مجموعه ورودی (corpus pruning) و درهم‌سازی ورودی (input shuffle) در نسخه‌های مربوط به afl-cmin، داده‌ها نشان می‌دهند که برای  درهم‌سازی ورودی، استفاده از afl-cmin مصرف دیسک را به حدود یک‌سوم حالتی کاهش می‌دهد که در آن از afl-cmin استفاده نشده است. نکته جالب این است که برای هرس کردن مجموعه ورودی، روند برعکس مشاهده می‌شود: مصرف دیسک اندکی افزایش می‌یابد. ما این موضوع را به این نسبت می‌دهیم که فازر پس از کمینه‌سازی، می‌بایست مجموعه ورودی را مجددا از روی بذر‌های باقی‌مانده بازسازی کند، بدون آن که امکان اکتشاف رفتار جدید برنامه را داشته باشد.

کل زمان فازینگ (Total Fuzzing Time). زمانی که یک راه‌اندازی مجدد (restart) رخ می‌دهد، فازر پایه (در مورد ما ++AFL) متوقف می‌شود و راهبردهای حفظ مجموعه ورودی (corpus retention) ما روی مجموعه تولید شده اعمال می‌گردد. فایل‌های باقی‌مانده که به‌ عنوان بذرهای (seed) جدید برای اجرای بعدی فازینگ استفاده می‌شوند، در دایرکتوری جدید بذرها کپی می‌شوند. این فرایند، خود نیازمند زمان است و همچنین پردازش این بذرها در مرحله موسوم به کالیبراسیون آغازین (startup calibration phase) در ++AFL موجب کاهش زمان مفید کلی فازینگ می‌شود. میزان این کاهش به تعداد فایل‌های باقی‌مانده بستگی دارد و از چند ثانیه تا نزدیک به یک ساعت متغیر است. در یک اجرای ۲۴ ساعته با چندین  راه‌اندازی مجدد (restart)، این موضوع موجب کاهش زمان کلی فازینگ می‌شود. همان‌طور که در شکل ۱۱ نشان داده شده است، در حالت درهم‌سازی ورودی (input shuffle)، مجموع زمان فازینگ حدود ۱.۵ ساعت کاهش می‌یابد، در حالی که این کاهش برای سایر راهبردها ناچیز است.

نکته جالب این است که هرس کردن مجموعه ورودی (corpus pruning) در هدف SQLite از نظر زمان اجرا نسبت به بازنشانی (reset) دارای حدود ۰.۱ درصد سربار است. این موضوع ناشی از تعداد بالاتر بازراه‌اندازی‌ها (restart) در بازنشانی و همچنین تعداد زیاد بذرهای اولیه (initial seeds) در این هدف خاص است. فراوانی بذرهای اولیه، در کنار تعداد زیاد بازراه‌اندازی‌ها، موجب می‌شود زمان مفید فازینگ در حالت بازنشانی کمی کاهش پیدا کند. با وجود آن که هرس کردن مجموعه ورودی هم مجموعه تولید شده و هم بذرهای اولیه را نگهداری می‌کند، تعداد بازراه‌اندازی‌های آن کمتر است. مقایسه هرس کردن مجموعه ورودی و درهم‌سازی ورودی در نسخه‌های مربوط به afl-cmin نشان می‌دهد که کاهش تعداد ورودی‌ها اثر مثبتی بر عملکرد درهم‌سازی ورودی دارد.

کاهش تعداد ورودی‌ها موجب سرعت گرفتن مرحله کالیبراسیون آغازین (startup calibration phase) می‌شود و این موضوع تا حدی زمان صرف‌ شده توسط afl-cmin برای کمینه‌سازی مجموعه را جبران می‌کند. این وضعیت در تضاد با هرس کردن مجموعه ورودی است؛ جایی که مرحله کالیبراسیون آغازین تنها حدود ۰.۱ درصد از کل زمان فازینگ را کاهش می‌دهد که در اینجا قابل چشم‌پوشی است، اما خود فرآیند کمینه‌سازی مجموعه، حدود ۰.۲ درصد سربار اضافی ایجاد می‌کند. از آنجا که بخش عمده زمان در مرحله کالیبراسیون آغازین تلف می‌شود، ما آزمایشی انجام دادیم که در آن این قابلیت را با استفاده از متغیر محیطی مربوطه در ++AFL غیرفعال کردیم. نتایج حاکی از آن است که غیرفعال‌سازی این ویژگی تأثیر منفی بر پوشش فازر دارد؛ بنابراین در ارزیابی نهایی از استفاده از این تنظیم صرف‌نظر کردیم.

تعداد کل اجراها (Total Executions). بررسی تعداد کل اجراها نشان می‌دهد که Sileo در مقایسه با ++AFL تعداد اجرای بسیار بیشتری به دست می‌آورد. ما فرض می‌کنیم این رفتار زمانی رخ می‌دهد که فازر (در اینجا ++AFL) با تعداد زیادی فایل بذر اولیه (initial seed files) مواجه می‌شود. این حجم بالای فایل‌ها پس از راه‌اندازی مجدد موجب افزایش ناگهانی تعداد اجرا در هر ثانیه (execs per second) می‌گردد، زیرا مرحله کالیبراسیون آغازین (startup calibration phase) در ابتدا سریع‌ترین بذرها را برای تست در اولویت قرار می‌دهد.

این اثر به ‌خصوص در هدف SQLite و در حالت درهم‌سازی ورودی (input shuffle)، شدیدتر است، جایی که به دلیل تولید یک مجموعه ورودی بزرگ، تعداد اجراها در ثانیه به‌ طور قابل توجهی افزایش می‌یابد. با این حال، با وجود این تعداد بالای اجرا، درهم‌سازی ورودی هیچ مزیتی نسبت به هرس کردن مجموعه ورودی (corpus pruning) از نظر پوشش (coverage) به دست نمی‌آورد. مقایسه تعداد اجراها در هرس کردن مجموعه ورودی و درهم‌سازی ورودی در نسخه‌های دارای afl-cmin  نشان می‌دهد که در هر دو حالت، تعداد کل اجراها به‌ طور قابل توجهی کاهش می‌یابد. بررسی آمار کمینه‌سازی مجموعه ورودی نشان می‌دهد که afl-cmin اندازه مجموعه را در بازه حدود ۲۰۰۰ تا ۶۰۰۰ فایل حفظ می‌کند. در حالی که بدون استفاده از afl-cmin، تعداد فایل‌های مجموعه که به ‌عنوان بذر مجدداً استفاده می‌شوند می‌تواند بسیار بیشتر باشد.

فازینگ
فازینگ
فازر
شکل ۱۱: مصرف دیسک، تعداد کل اجراها و کل زمان اجرای فازر برای اهداف مختلف برای میانگین اجرا.

 ۶. بحث (Discussion)

در ادامه، به بررسی تهدیدات بالقوه برای اعتبار یا صحت نتایج (threats to validity) می‌پردازیم، تفاوت‌های میان پوشش کد (code coverage) و فراوانی بلوک‌ها (block frequency) را مطرح می‌کنیم و در نهایت یک نسخه بهینه ‌شده از راهبرد درهم‌سازی ورودی (input shuffle) را ارائه می‌دهیم.

تهدیدات صحت یا اعتبار (Threats to Validity). اطمینان از صحت نتایج حاصل از آزمایش‌های تجربی امری ضروری است. پژوهش ما بر سه بُعد کلیدی تمرکز دارد که به‌ طور خاص در این زمینه حائز اهمیت می‌باشند. در ادامه، فرضیات خود را بیان کرده و اقداماتی را که برای حفظ صحت و اعتبار آزمایش‌ها انجام داده‌ایم تشریح خواهیم کرد.

صحت بیرونی (External Validity). ما به منظور ارزیابی رویکرد خود، مجموعه‌ای از اهداف با انواع مختلف را از مجموعه آزمون گوگل، یعنی FuzzBench انتخاب کردیم. علت استفاده از FuzzBench آن بود که یک چارچوب معیار پرکاربرد و استاندارد در حوزه فازینگ است. علاوه بر این، یکی از آزمایش‌های پوشش خود (objdump) را خارج از محیط FuzzBench نیز به انجام رساندیم تا نشان دهیم نتایج ما قابل اعتماد هستند و صرفاً تحت تأثیر یا سوگیری ناشی از استفاده از FuzzBench قرار ندارند.

صحت درونی (Internal Validity). به دلیل ماهیت غیرقطعی (non-deterministic) فرایند فازینگ، تمامی آزمایش‌ها را ۱۰ بار تکرار کردیم تا اعتبار آماری نتایج تضمین شود و بتوانیم معیارهای آماری مختلفی را برای تحلیل نتایج تجربی محاسبه کنیم. علاوه بر این، از مجموعه آزمون معتبر و شناخته ‌شده FuzzBench برای هماهنگ‌سازی (orchestrate) و ارزیابی آزمایش‌ها استفاده نمودیم.

صحت سازه‌ای (Construct Validity). در نهایت، به ‌عنوان سومین تهدید برای صحت (اعتبار)، اطمینان حاصل می‌کنیم که ارزیابی ما واقعاً همان چیزی را می‌سنجد که باید اندازه‌گیری کند. برای جلوگیری از هرگونه اختلاف بین خط مبنا و راهبردهای مورد آزمایش، اطمینان حاصل کردیم که تمام آن‌ها از یک پیکربندی یکسان فازر و نسخه یکسان از ++AFL استفاده می‌کنند.

پوشش کد در برابر فراوانی بلوک‌ها (Code Coverage vs. Block Frequency). پوشش کد (code coverage) در پژوهش‌های علمی و کاربردهای عملی، یکی از معیارهای استاندارد برای مقایسه عملکرد فازرها محسوب می‌شود. چنانچه یک فازر پوشش کد بالایی به دست آورد، بدان معناست که به‌ طور بالقوه می‌تواند باگ‌های بیشتری را فعال کند، زیرا در گام اول باید به کد دارای باگ دسترسی پیدا کند. این مقاله مفهوم فراوانی بلوک‌ها (block frequency) را به‌ عنوان یک معیار جدید معرفی می‌کند که هدف آن تکمیل (و نه جایگزینی) پوشش کد است. اندازه‌گیری فراوانی بلوک‌ها می‌تواند کمک کند تا مشخص شود فازر بر کدام بلوک‌های پایه (basic blocks) بیشتر تمرکز کرده است. بازدید مکرر از یک بلوک پایه می‌تواند در برخی موارد مفید باشد، به‌ویژه زمانی که وضعیت (state) بین این بازدیدها تغییر کند؛ زیرا ممکن است یک باگ در یک وضعیت خاص رخ ندهد، اما در وضعیت دیگری قابل بروز باشد.

درهم‌سازی ورودی با سربار کاهش‌یافته (Input Shuffle with Reduced Overhead). همان‌طور که در بخش ۵.۲ بحث شد، استفاده از راهبردهای حفظ مجموعه ورودی در Sileo موجب ایجاد سربار از نظر افزایش مصرف فضای دیسک می‌شود. این موضوع به ‌ویژه در راهبرد درهم‌سازی ورودی (input shuffle) پررنگ‌تر است، به‌طوری‌که مصرف دیسک در این حالت نسبت به سایر راهبردها و ++AFL به‌ طور قابل توجهی بیشتر می‌باشد. طراحی این راهبردها شامل کپی کردن دایرکتوری صف (queue) به یک دایرکتوری بذرهای جدید، درهم‌سازی تمام فایل‌ها و استفاده از این دایرکتوری به ‌عنوان دایرکتوری بذرهای جدید برای فازر پایه است. این کپی‌سازی ضروری است تا از تغییر مستقیم دایرکتوری صف جلوگیری شود (به عنوان مثال در حالتی مانند حذف فایل‌ها در هرس کردن مجموعه ورودی (corpus pruning)). با این حال، درهم‌سازی ورودی (input shuffle) در مقایسه با سایر راهبردهای حفظ مجموعه ورودی متفاوت است، زیرا در آن هیچ بخشی از مجموعه ورودی واقعی حذف نمی‌شود؛ کل مجموعه ورودی برای اجرای بعدی مورد استفاده قرار می‌گیرد. از این رو، به‌جز درهم‌سازی فایل‌ها، هیچ تغییری در دایرکتوری صف (queue) رخ نمی‌دهد. ما با در نظر گرفتن این ملاحظات، یک نسخه بهینه‌ شده از راهبرد درهم‌سازی ورودی را پیشنهاد می‌دهیم: در این نسخه، پس از هر بازراه‌اندازی مجدد (restart)، دایرکتوری بذرهای جدید مستقیماً برابر با دایرکتوری صف قبلی قرار داده می‌شود و به این ترتیب، فرآیند کپی و درهم‌سازی فایل‌ها حذف شده و سربار مربوطه به حداقل می‌رسد. این راهبرد نیازمند تنظیم متغیر محیطی ++AFL با نام AFL_SHUFFLE_QUEUE است. ما این نسخه بهینه ‌شده را آزمایش کرده‌ایم و نتایج اولیه مطابق انتظار ما بوده است: عملکرد آن از نظر پوشش اندکی بهتر از درهم‌سازی ورودی می‌باشد و در عین حال سربار آن کاهش یافته است. در نتیجه، پیشنهاد می‌کنیم از این نسخه به‌جای درهم‌سازی ورودی استفاده شود و پیاده‌سازی آن به همراه خروجی‌های پژوهشی (artifact) ارائه گردد.

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

کار ما ارتباط نزدیکی با چندین پژوهش پیشین دارد و در این بخش، کار خود را در بستر ادبیات علمی موجود قرار می‌دهیم.

بازنشانی وضعیت (Resetting State). بازنشانی وضعیت یا شروع مجدد با یک محیط کاملاً تمیز (clean environment) در حوزه‌های مختلف اثرات مثبتی نشان داده است. برای مثال، Zaidi  و همکارانش [21] اثبات کرده‌اند که مقداردهی اولیه مجدد (reinitialization) یک شبکه عصبی می‌تواند نتایج آموزش را به‌ طور قابل توجهی بهبود بخشد. آن‌ها اذعان داشتند که این پدیده به‌طور شگفت‌آوری کمتر مورد مطالعه و استفاده قرار گرفته است. Koenig و همکارانش [22] اخیراً در حوزه سنتز برنامه (program synthesis)، نشان داده‌اند که راه‌اندازی مجدد وظایف سنتز تصادفی می‌تواند سرعت سنتز را تا یک مرتبه بزرگی افزایش دهد. مشابه فازینگ، مشاهده اصلی آن‌ها این است که وظایف سنتز از مجموعه‌ای از رکودها (plateaus) عبور می‌کنند که اغلب دارای توزیع سنگین (heavy-tailed) می‌باشند، بنابراین ممکن است بدون پیشرفت بیشتر در آن‌ها به دام افتند. حتی در زیست‌شناسی نیز، «شروع مجدد تمیز» (clean restarts) می‌توانند مفید باشند [23].

همچنین در حوزه پژوهشی الگوریتم‌های ژنتیک (genetic algorithms) و استراتژی‌های تکاملی (evolution strategies)، بازنشانی تصادفی وضعیت برای غلبه بر کمینه‌های محلی سابقه‌ای طولانی دارد [24, 25, 26]. قنادیان (Ghannadian) [25] یک الگوریتم ژنتیک ارائه می‌دهد که در آن عملگر جهش (mutation) سنتی با یک راهبرد راه‌اندازی مجدد تصادفی (random restart) جایگزین می‌شود. این بدان معناست که به ‌جای اعمال جهش بر اعضای جمعیت، الگوریتم جست‌وجو را به ‌صورت دوره‌ای از یک نقطه تصادفی دوباره آغاز می‌کند. هدف از این تغییر، بهبود اکتشاف در فضای جست‌وجو و جلوگیری از همگرایی زودهنگام به بهینه‌های محلی (local optima) است. با این حال، Fukunaga [24]  نشان می‌دهد که رویکرد راه‌اندازی مجدد زمان‌بندی ‌شده (scheduled restarts) می‌تواند از نظر عملکرد با راهبردهای بازراه‌اندازی پویا (dynamic restart strategies) رقابتی باشد، در حالی که اشاره می‌کند ترکیب این دو نوع راه‌اندازی مجدد (پویا و زمان‌بندی ‌شده) می‌تواند مسیر پژوهشی جالبی باشد. این رفتار در نتایج ما نیز مشاهده می‌شود، جایی که راه‌اندازی‌های زمان‌بندی ‌شده ثابت ( هرس کردن مجموعه ورودی با بازه‌های ۳۰، ۶۰ و ۲۴۰ دقیقه) را با رویکرد بازراه‌اندازی پویا برای همان راهبرد مقایسه کردیم. با این حال، نتایج ما نشان می‌دهد که تعداد بهینه‌ی باز‌راه‌اندازی‌های زمان‌بندی‌ شده برای دستیابی به عملکرد بالا به هدف مورد آزمایش وابسته است؛ بنابراین رویکرد پویا در عمل و در مجموع روی تمام اهداف، قابل‌اعتمادتر است.

مرحله سکون پوشش (Coverage Plateaus). مشابه رویکرد ما، کارهای پیشین نیز مرحله‌های سکون پوشش (coverage plateaus) را به‌عنوان نقطه‌ای مناسب برای کمک به فازر شناسایی کرده‌اند. بر اساس مطالعه Lemieux و همکارانش [27]، پدیده‌ای که با عنوان توقف رشد پوشش (coverage stall) یا مرحله سکون پوشش (coverage plateau) شناخته می‌شود زمانی رخ می‌دهد که یک الگوریتم جست‌وجو پس از انجام چندین تکرار جهش (mutation)، دیگر هیچ بهبود جدیدی در پوشش کد نشان نمی‌دهد. در این حالت، با وجود تولید چندین مورد آزمون جهش‌یافته، هیچ‌یک قادر به پوشش بخش‌های جدید و کاوش ‌نشده برنامه نیستند؛ از این رو نویسندگان از اصطلاح «coverage stall» یا «توقف رشد پوشش» برای توصیف این وضعیت استفاده می‌کنند.

در حالی که Sileo در چنین شرایطی اقدام به راه‌اندازی مجدد فازر (restart) می‌کند، Lemieux و همکاران [27] پیشنهاد می‌کنند از مدل‌های زبانی بزرگ (Large Language Models, LLMs) که برای تولید کد تنظیم شده‌اند، مانند Codex، برای «خارج کردن فازر از وضعیت بن‌بست و سکون» استفاده شود. فراتر از حوزه فازینگ، مطالعات دیگر نیز نشان داده‌اند که راه‌اندازی‌های مجدد می‌توانند برای عبور از رکودها در مسائل صدق‌پذیری بولی (Boolean Satisfiability Problems)  مورد استفاده قرار گیرند [28].

در حالی که رکودها یا مرحله سکون پوشش (coverage plateaus) برای نمونه اولیه ما عملکرد مناسبی داشتند، ممکن است شاخص‌های دیگری نیز وجود داشته باشند. به‌طور خاص، Liyanage و همکارانش [29] اخیراً موضوع برون‌یابی نرخ پوشش (extrapolation of coverage rates) را مطرح کرده‌اند. در حالی که هدف آن‌ها یافتن یک معیار توقف برای فازینگ بوده است (که در غیر این صورت می‌تواند به‌صورت نامحدود اجرا شود) می‌توان هر رویکردی از این نوع را مجددا استفاده کرد و در صورتی که نرخ پوشش برآورد شده به زیر یک آستانه مشخص برسد، فازر را بازراه‌اندازی کرد.

فازینگ (Fuzzing). فازینگ سابقه‌ای طولانی و موفق دارد که با کار اولیه Miller آغاز شد [1]. یکی از مهم‌ترین نقاط عطف این حوزه، معرفی بازخورد مبتنی بر پوشش (coverage feedback) بود که توسط AFL++  محبوب شد [2] و موجب شکل‌گیری حجم گسترده‌ای از پژوهش‌های بعدی گردید [30, 31, 32]. این پژوهش‌ها سپس به سمت تکنیک‌های بازخوردی سنگین‌تر مانند ردگیری آلودگی (taint tracking) [33, 34]  یا اجرای نمادین (symbolic execution) [35, 36, 37] حرکت کردند، همچنین به بهبود زمان‌بندی انتخاب بذرها (seed scheduling) [30, 38] پرداختند یا رویکردهای کاملاً جدیدی را پیشنهاد کردند [39, 40, 41, 42, 43, 44].

موفقیت فازینگ تنها به پذیرش در صنعت محدود نشده است [18]، بلکه این تکنیک به حوزه‌ها و انواع مختلفی از برنامه‌ها نیز گسترش یافته است و دیگر صرفاً محدود به باینری‌های فضای کاربری لینوکس نیست. به‌طور خاص، پژوهش‌های متعددی برای آزمون هسته‌های سیستم‌عامل (kernel) [45, 46, 47, 48, 49]، برنامه‌های شبکه‌ای [50, 51, 52, 53, 54]، موتورهای جاوااسکریپت (JavaScript) و سایر اجزای مرورگر [55, 56, 57, 58, 59, 60, 61] و میان‌افزار (firmware) [62, 63, 64, 65, 66, 67]  انجام شده است. با ظهور شبکه‌های عصبی (neural networks) و مدل‌های زبانی بزرگ (large language models)، رویکردهای جدیدی در فازینگ توسعه یافته‌اند که از این تکنیک‌ها بهره‌مند می‌شوند [27, 42, 68, 69, 70].

به‌تازگی، Wu و همکارانش [71] برای توجیه طراحی فازر خود از یک تابع نمونه مشابه استفاده کرده‌اند، مشابه رویکردی که ما در الگوریتم ۱ به کار گرفته‌ایم. در حالی که ما برای کاهش اثر سایه‌سازی ورودی (input shadowing) از راه‌اندازی مجدد فازر استفاده می‌کنیم، رویکرد آن‌ها تولید برنامه‌های موسوم به برنامه‌های فانتوم (phantom programs) را پیشنهاد می‌کند که هدف آن‌ها ساده‌سازی شروط تو در تو (nested conditional) است، به‌گونه‌ای که این شروط بتوانند به ‌صورت مستقل توسط فازر حل شوند و در نتیجه سرعت پیشرفت آن افزایش یابد. از این رو، روش آن‌ها به رویکردهایی مانند  laf-intel [72]نزدیک‌تر است.

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

ما در این کار، یک نقطه شروع برای کاهش اثرات منفی سایه‌سازی ورودی (input shadowing) در کمپین‌های فازینگ ارائه کردیم. مشاهده گردید که جست‌وجوی مبتنی بر نوآوری‌محور (novelty search) مورد استفاده در الگوریتم‌های فازینگ، فضای ورودی قابل دسترس در عمل را محدود می‌کند. به منظور کاهش این ضعف (در حالی که این نوع جست‌وجو همچنان مزایای قابل توجهی دارد) یک زمان‌بند کمپین فازینگ با نام Sileo پیشنهاد گردید که با راه‌اندازی مجدد تطبیقی (adaptive restart)، توجه فازر را متنوع‌تر می‌کند. نتایج ما حاکی از آن است که Sileo به‌طور مؤثری فراوانی برخورد با بلوک‌ها (hit block frequencies) را توزیع کرده و توجه فازر را بهتر در میان بلوک‌های پایه مختلف پخش می‌کند. راه‌اندازی مجدد فرایند فازینگ در غلبه بر سایه‌سازی ورودی مفید واقع می‌شود، زیرا ورودی‌هایی که پیش‌تر به دلیل عدم نوآوری در پوشش جدید غیرجذاب تلقی شده بودند، دوباره فرصت بررسی پیدا می‌کنند. تعامل پویا میان ردگیری پوشش توسط بیت‌مپ (bitmap coverage tracking)، مجموعه ورودی حفظ‌ شده (preserved corpus) و اکتشافی راه‌اندازی مجدد، نشان‌ دهنده رویکرد تطبیقی Sileo است که در نهایت منجر به کاوش جامع‌تر و دقیق‌تر فضای ورودی برنامه می‌شود.

قدردانی‌ها (Acknowledgments)

این پژوهش با حمایت مالی شورای تحقیقات اروپا (ERC) تحت طرح پژوهشی تثبیت‌کننده (Consolidator) با عنوان RS3 (101045669) و همچنین وزارت فدرال آموزش و پژوهش آلمان (BMBF) تحت طرح KMU-Fuzz (16KIS1898) انجام شده است.

منابع

				
					[1] Barton P Miller, David Koski, Cjin Pheow Lee, Vivekandanda Maganty, Ravi Murthy, Ajitkumar Natarajan, and Jeff Steidl. Fuzz Revisited: A Re-examination of the Reliability of UNIX Utilities and Services. Technical report, University of Wisconsin-Madison Department of Computer Sciences, 1995.
[2] Michał Zalewski. American Fuzzy Lop. http://lcamtuf.coredump.cx/afl/.Accessed: January 20, 2026.
[3] Shuitao Gan, Chao Zhang, Xiaojun Qin, Xuwen Tu, Kang Li, Zhongyu Pei, and Zuoning Chen. CollAFL: Path Sensitive Fuzzing. In IEEE Symposium on Security and Privacy (S&P), 2018.
[4] Adrian Herrera, Mathias Payer, and Antony L Hosking. DatAFLow: Toward a Data-Flow-Guided Fuzzer. ACM Transactions on Software Engineering and Methodology, 2022.
[5] Andrea Fioraldi, Daniele Cono D’Elia, and Davide Balzarotti. The Use of Likely Invariants as Feedback for Fuzzers. In USENIX Security Symposium, 2021.
[6] Cornelius Aschermann, Sergej Schumilo, Tim Blazytko, Robert Gawlik, and Thorsten Holz. REDQUEEN: Fuzzing with Input-to-State Correspondence. In Symposium on Network and Distributed System Security (NDSS), 2019.
[7] Andrea Fioraldi, Dominik Maier, Heiko Eißfeldt, and Marc Heuse. AFL++: Combining Incremental Steps of Fuzzing Research. In USENIX Workshop on Offensive Technologies (WOOT), 2020.
[8] Caroline Lemieux and Koushik Sen. FairFuzz: A Targeted Mutation Strategy for Increasing Greybox Fuzz Testing Coverage. In ACM/IEEE International Conference on Automated Software Engineering (ASE), 2018.
[9] Chenyang Lyu, Shouling Ji, Chao Zhang, Yuwei Li, Wei-Han Lee, Yu Song, and Raheem Beyah. MOPT: Optimized Mutation Scheduling for Fuzzers. In USENIX Security Symposium, 2019.
[10] Tim Blazytko, Cornelius Aschermann, Moritz Schloegel, Ali Abbasi, Sergej Schumilo, Simon Wörner, and Thorsten Holz. GRIMOIRE: Synthesizing Structure while Fuzzing. In USENIX Security Symposium, 2019.
[11] Stephane Doncieux, Alban Laflaquière, and Alexandre Coninx. Novelty Search: A Theoretical Perspective. In Genetic and Evolutionary Computation Conference (GECCO), 2019.
[12] Emre Güler, Philipp Görz, Elia Geretto, Andrea Jemmett, Sebastian Österlund, Herbert Bos, Cristiano Giuffrida, and Thorsten Holz. Cupid: Automatic Fuzzer Selection for Collaborative Fuzzing. In Annual Computer Security Applications Conference (ACSAC), 2020.
[13] Yuanliang Chen, Yu Jiang, Fuchen Ma, Jie Liang, Mingzhe Wang, Chijin Zhou, Xun Jiao, and Zhuo Su. EnFuzz: Ensemble Fuzzing with Seed Synchronization among Diverse Fuzzers. In USENIX Security Symposium, 2019.
[14] Yu-Fu Fu, Jaehyuk Lee, and Taesoo Kim. autofz: Automated Fuzzer Composition at Runtime. In USENIX Security Symposium, 2023.
[15] Jonathan Metzman, László Szekeres, Laurent Simon, Read Sprabery, and Abhishek Arya. FuzzBench: An Open Fuzzer Benchmarking Platform and Service.In ACM Joint Meeting on European Software Engineering Conference and Symposium on the Foundations of Software Engineering, 2021.
[16] George Klees, Andrew Ruef, Benji Cooper, Shiyi Wei, and Michael Hicks. Evaluating Fuzz Testing. In ACM Conference on Computer and Communications Security (CCS), 2018.
[17] M. Schloegel, N. Bars, N. Schiller, L. Bernhard, T. Scharnowski, A. Crump, A. AleEbrahim, N. Bissantz, M. Muench, and T. Holz. Sok: Prudent evaluation practices for fuzzing. In IEEE Symposium on Security and Privacy (S&P), 2024.
[18] Mike Aizatsky, Kostya Serebryany (Software Engineers, Dynamic Tools); Oliver Chang, Abhishek Arya (Security Engineers, Google Chrome); and Meredith Whit-taker (Open Research Lead). Announcing OSS-Fuzz: Continuous fuzzing for open source software. https://opensource.googleblog.com/2016/12/announcing-oss-fuzz-continuous-fuzzing.html, 2016.
[19] András Vargha and Harold D Delaney. A Critique and Improvement of the CL Common Language Effect Size Statistics of McGraw and Wong. Journal of Educational and Behavioral Statistics, 25(2):101–132, 2000.
[20] Andrea Arcuri and Lionel Briand. A Practical Guide for Using Statistical Tests to Assess Randomized Algorithms in Software Engineering. In International Conference on Software Engineering (ICSE), 2011.
[21] Sheheryar Zaidi, Tudor Berariu, Hyunjik Kim, Jörg Bornschein, Claudia Clopath, Yee Whye Teh, and Razvan Pascanu. When Does Re-initialization Work? In I Can’t Believe It’s Not Better Workshop at NeurIPS, 2022.
[22] Jason R. Koenig, Oded Padon, and Alex Aiken. Adaptive Restarts for Stochastic Synthesis. In ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 2021.
[23] Joel Lehman and Risto Miikkulainen. Extinction Events can Accelerate Evolution. PloS one, 10(8):e0132886, 2015.
[24] Alex S Fukunaga. Restart scheduling for genetic algorithms. In International Conference on Parallel Problem Solving from Nature, pages 357–366. Springer, 1998.
[25] Farzad Ghannadian, Cecil Alford, and Ron Shonkwiler. Application of random restart to genetic algorithms. Information Sciences, 95(1-2):81–102, 1996. 
[26] Giuseppe Cuccu, Faustino Gomez, and Tobias Glasmachers. Novelty-based restarts for evolution strategies. In 2011 IEEE Congress of Evolutionary Computation (CEC), pages 158–163. IEEE, 2011.
[27] Caroline Lemieux, Jeevana Priya Inala, Shuvendu K. Lahiri, and Siddhartha Sen. CodaMosa: Escaping Coverage Plateaus in Test Generation with Pre-trained Large Language Models. In International Conference on Software Engineering (ICSE), 2023.
[28] Steven Hampson and Dennis Kibler. Plateaus and plateau search in boolean satisfiability problems: When to give up searching and start again. In Workshop Notes: 2nd DIMACS Challenge. Citeseer, 1993.
[29] Danushka Liyanage, Seongmin Lee, Chakkrit Tantithamthavorn, and Marcel Böhme. Extrapolating Coverage Rate in Greybox Fuzzing. In International Conference on Software Engineering (ICSE), 2024.
[30] Marcel Böhme, Van-Thuan Pham, and Abhik Roychoudhury. Coverage-based Greybox Fuzzing as Markov Chain. IEEE Transactions on Software Engineering, 45(5):489–506, 2017.
[31] Marcel Böhme, Van-Thuan Pham, Manh-Dung Nguyen, and Abhik Roychoudhury. Directed greybox fuzzing. In Proceedings of the 2017 ACM SIGSAC conference on computer and communications security, pages 2329–2344, 2017.
[32] Van-Thuan Pham, Marcel Böhme, Andrew E Santosa, Alexandru Răzvan Căciulescu, and Abhik Roychoudhury. Smart greybox fuzzing. IEEE Transactions on Software Engineering, 47(9):1980–1997, 2019.
[33] Tielei Wang, Tao Wei, Guofei Gu, and Wei Zou. TaintScope: A Checksum-aware Directed Fuzzing Tool for Automatic Software Vulnerability Detection. In IEEE Symposium on Security and Privacy (S&P), 2010.
[34] Peng Chen and Hao Chen. Angora: Efficient Fuzzing by Principled Search. In IEEE Symposium on Security and Privacy (S&P), 2018.
[35] Cristian Cadar, Daniel Dunbar, Dawson R Engler, et al. Klee: Unassisted and Automatic Generation of High-coverage Tests for Complex Systems Programs. In Symposium on Operating Systems Design and Implementation (OSDI), 2008.
[36] Insu Yun, Sangho Lee, Meng Xu, Yeongjin Jang, and Taesoo Kim. QSYM: A Practical Concolic Execution Engine Tailored for Hybrid Fuzzing. In USENIX Security Symposium, 2018.
[37] Sebastian Poeplau and Aurélien Francillon. Symbolic execution with SymCC: Don’t interpret, compile! In USENIX Security Symposium, 2020.
[38] Dongdong She, Abhishek Shah, and Suman Jana. Effective Seed Scheduling for Fuzzing with Graph Centrality Analysis. In IEEE Symposium on Security and Privacy (S&P), 2022.
[39] Hui Peng, Yan Shoshitaishvili, and Mathias Payer. T-Fuzz: Fuzzing by Program Transformation. In IEEE Symposium on Security and Privacy (S&P), 2018.
[40] Nils Bars, Moritz Schloegel, Tobias Scharnowski, Nico Schiller, and Thorsten Holz. Fuzztruction: Using Fault Injection-based Fuzzing to Leverage Implicit Domain Knowledge. In USENIX Security Symposium, 2023.
[41] Ju Chen, Jinghan Wang, Chengyu Song, and Heng Yin. JIGSAW: Efficient and Scalable Path Constraints Fuzzing. In IEEE Symposium on Security and Privacy (S&P), 2022.
[42] Dongdong She, Kexin Pei, Dave Epstein, Junfeng Yang, Baishakhi Ray, and Suman Jana. NEUZZ: Efficient Fuzzing with Neural Program Smoothing. In IEEE Symposium on Security and Privacy (S&P), 2019.
[43] Jinghan Wang, Yue Duan, Wei Song, Heng Yin, and Chengyu Song. Be Sensitive and Collaborative: Analyzing Impact of Coverage Metrics in Greybox Fuzzing. In Symposium on Recent Advances in Intrusion Detection (RAID), 2019.
[44] Nico Schiller, Merlin Chlosta, Moritz Schloegel, Nils Bars, Thorsten Eisenhofer, Tobias Scharnowski, Felix Domke, Lea Schönherr, and Thorsten Holz. Drone security and the mysterious case of dji’s droneid. In NDSS, 2023.
[45] Sergej Schumilo, Cornelius Aschermann, Robert Gawlik, Sebastian Schinzel, and Thorsten Holz. kAFL: Hardware-Assisted Feedback Fuzzing for OS Kernels. In USENIX Security Symposium, 2017.
[46] Dae R. Jeong, Kyungtae Kim, Basavesh Shivakumar, Byoungyoung Lee, and Insik Shin. Razzer: Finding Kernel Race Bugs through Fuzzing. In IEEE Symposium on Security and Privacy (S&P), 2019.
[47] Kyungtae Kim, Dae R. Jeong, Chung Hwan Kim, Yeongjin Jang, Insik Shin, and Byoungyoung Lee. HFL: Hybrid Fuzzing on the Linux Kernel. In Symposium on Network and Distributed System Security (NDSS), 2020.
[48] Daimeng Wang, Zheng Zhang, Hang Zhang, Zhiyun Qian, Srikanth V. Krishnamurthy, and Nael B. Abu-Ghazaleh. SyzVegas: Beating Kernel Fuzzing Odds with Reinforcement Learning. In USENIX Security Symposium, 2021.
[49] google. syzkaller - kernel fuzzer. https://github.com/google/syzkaller. Accessed: January 20, 2026.
[50] Jinsheng Ba, Marcel Böhme, Zahra Mirzamomen, and Abhik Roychoudhury. Stateful Greybox Fuzzing. In USENIX Security Symposium, 2022.
[51] Sergej Schumilo, Cornelius Aschermann, Ali Abbasi, Simon Wörner, and Thorsten Holz. Nyx: Greybox Hypervisor Fuzzing using Fast Snapshots and Affine Types. In USENIX Security Symposium, 2021.
[52] Van-Thuan Pham, Marcel Böhme, and Abhik Roychoudhury. AFLNet: A Greybox Fuzzer for Network Protocols. In IEEE International Conference on Software Testing, Validation and Verification (ICST), 2020.
[53] Roberto Natella. StateAFL: Greybox Fuzzing for Stateful Network Servers. Empirical Software Engineering, 27(7):191, 2022.
[54] Zhengxiong Luo, Junze Yu, Feilong Zuo, Jianzhong Liu, Yu Jiang, Ting Chen, Abhik Roychoudhury, and Jiaguang Sun. Bleem: Packet Sequence Oriented Fuzzing for Protocol Implementations. In USENIX Security Symposium, 2023.
[55] Xiaoyu He, Xiaofei Xie, Yuekang Li, Jianwen Sun, Feng Li, Wei Zou, Yang Liu, Lei Yu, Jianhua Zhou, Wenchang Shi, and Wei Huo. SoFi: Reflection-Augmented Fuzzing for JavaScript Engines. In ACM Conference on Computer and Communications Security (CCS), 2021.
[56] Lukas Bernhard, Tobias Scharnowski, Moritz Schloegel, Tim Blazytko, and Thorsten Holz. JIT-Picking: Differential Fuzzing of JavaScript Engines. In ACM Conference on Computer and Communications Security (CCS), 2022.
[57] Junjie Wang, Bihuan Chen, Lei Wei, and Yang Liu. Superion: Grammar-aware Greybox Fuzzing. In ACM/IEEE International Conference on Automated Software Engineering (ASE), 2019.
[58] Suhwan Song, Jaewon Hur, Sunwoo Kim, Philip Rogers, and Byoungyoung Lee. R2Z2: Detecting Rendering Regressions in Web Browsers through Differential Fuzz Testing. In ACM/IEEE International Conference on Automated Software Engineering (ASE), 2022.
[59] Wen Xu, Soyeon Park, and Taesoo Kim. FREEDOM: Engineering a State-of-the-Art DOM Fuzzer. In ACM Conference on Computer and Communications Security (CCS), 2020.
[60] Samuel Groß, Simon Koch, Lukas Bernhard, Thorsten Holz, and Martin Johns. Fuzzilli: Fuzzing for JavaScript JIT Compiler Vulnerabilities. In Symposium on Network and Distributed System Security (NDSS), 2023.
[61] Chijin Zhou, Quan Zhang, Mingzhe Wang, Lihua Guo, Jie Liang, Zhe Liu, Mathias Payer, and Yu Jiang. Minerva: Browser API Fuzzing with Dynamic mod-ref Analysis. In ACM Joint Meeting on European Software Engineering Conference and Symposium on the Foundations of Software Engineering, 2022.
[62] Yaowen Zheng, Ali Davanian, Heng Yin, Chengyu Song, Hongsong Zhu, and Limin Sun. FIRM-AFL: High-Throughput Greybox Fuzzing of IoT Firmware via Augmented Process Emulation. In USENIX Security Symposium, 2019. 
[63] Xiaotao Feng, Ruoxi Sun, Xiaogang Zhu, Minhui Xue, Sheng Wen, Dongxi Liu, Surya Nepal, and Yang Xiang. Snipuzz: Black-box Fuzzing of IoT Firmware via Message Snippet Inference. In ACM Conference on Computer and Communications Security (CCS), 2021.
[64] Tobias Scharnowski, Nils Bars, Moritz Schloegel, Eric Gustafson, Marius Muench, Giovanni Vigna, Christopher Kruegel, Thorsten Holz, and Ali Abbasi. Fuzzware: Using Precise MMIO Modeling for Effective Firmware Fuzzing. In USENIX Security Symposium, 2022.
[65] Lukas Seidel, Dominik Maier, and Marius Muench. Forming Faster Firmware Fuzzers. In USENIX Security Symposium, 2023.
[66] Ioannis Angelakopoulos, Gianluca Stringhini, and Manuel Egele. FirmSolo: Enabling Dynamic Analysis of Binary Linux-based IoT Kernel Modules. In USENIX Security Symposium, 2023.
[67] Tobias Scharnowski, Simon Woerner, Felix Buchmann, Nils Bars, Moritz Schloegel, and Thorsten Holz. Hoedur: Embedded Firmware Fuzzing using Multi-Stream Inputs. In USENIX Security Symposium, 2023.
[68] Ruijie Meng, Martin Mirchev, Marcel Böhme, and Abhik Roychoudhury. Large language model guided protocol fuzzing. In Proceedings of the 31st Annual Network and Distributed System Security Symposium (NDSS), 2024.
[69] Yaroslav Oliinyk, Michael Scott, Ryan Tsang, Chongzhou Fang, Houman Homayoun, et al. Fuzzing busybox: Leveraging llm and crash reuse for embedded bug unearthing. arXiv preprint arXiv:2403.03897, 2024.
[70] Yinlin Deng, Chunqiu Steven Xia, Haoran Peng, Chenyuan Yang, and Lingming Zhang. Large language models are zero-shot fuzzers: Fuzzing deep-learning libraries via large language models. In Proceedings of the 32nd ACM SIGSOFT international symposium on software testing and analysis, pages 423–435, 2023.
[71] Mingyuan Wu, Kunqiu Chen, Qi Luo, Jiahong Xiang, Ji Qi, Junjie Chen, Heming Cui, and Yuqun Zhang. Enhancing coverage-guided fuzzing via phantom program. In Proceedings of the 31st ACM Joint European Software Engineering Conference and Symposium on the Foundations of Software Engineering, ESEC/FSE 2023, page 1037–1049, 2023.
[72] lafintel. laf-intel - Circumventing Fuzzing Roadblocks with Compiler Transformations. https://lafintel.wordpress.com.

				
			

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

پیام بگذارید

wpChatIcon
wpChatIcon