فازینگ جعبه خاکستری (Greybox Fuzzing) روشی مؤثر برای آزمون نرمافزار محسوب میشود. فازرهای جعبه خاکستری مانند AFL، از ابزارگذاری (Instrumentation) استفاده میکنند که اطلاعات مربوط به پوشش مسیر (Path Coverage) را جمعآوری کرده و از این اطلاعات برای هدایت فرایند فازینگ بهره میگیرند. این ابزارگذاری معمولاً توسط یک زنجیره ابزار کامپایلر اصلاح شده (Modified Compiler Toolchain) در برنامه قرار داده میشود؛ بنابراین، برنامه برای سازگاری با فازینگ جعبه خاکستری باید مجدداً کامپایل (Recompile) گردد. در شرایطی که کد منبع (Source Code) در دسترس نباشد، یا پروژه دارای سیستم ساخت (Build System) پیچیدهای باشد، انجام کامپایل مجدد همیشه امکانپذیر نیست. ما در این مقاله، E9AFL را معرفی میکنیم؛ ابزاری سریع و مقیاسپذیر که میتواند بهصورت خودکار ابزارگذاری (Instrumentation) مربوط به AFL را در باینریهای برنامه وارد کند. E9AFL بر پایه ابزار E9Patch که یک ابزار بازنویسی ایستای باینری (Static Binary Rewriting) است، ساخته شده است. E9AFL به منظور مقابله با سربار ایجاد شده در اثر ابزارگذاری باینری (Binary Instrumentation)، مجموعهای از راهبردهای بهینهسازی (Optimization Strategies) را توسعه میدهد. نتایج ارزیابی حاکی از آن است که E9AFL در مقایسه با ابزارهای موجود برای ابزارگذاری باینری عملکرد بهتری ارائه میدهد و از نظر کارایی، عملکردی قابل مقایسه با ابزارگذاری در زمان کامپایل (Compile-Time Instrumentation) دارد.
مخزن گیتهاب ابزار متنباز: https://github.com/GJDuck/e9afl
کلیدواژهها: فازینگ (Fuzzing)، بازنویسی باینری (Binary Rewriting)
۱. مقدمه (INTRODUCTION)
فازینگ جعبه خاکستری روشی اثبات شده و مؤثر برای آزمون نرمافزار است. ابزارهای متداول فازینگ جعبه خاکستری، مانند AFL [1]، با قرار دادن ابزارگذاری (Instrumentation) در برنامه عمل میکنند تا اطلاعات مربوط به پوشش مسیر (Path Coverage) را جمعآوری کرده و از آن برای هدایت فرایند فازینگ استفاده کنند. این ابزارگذاری معمولاً در زمان کامپایل (Compile-Time) و با استفاده از یک کامپایلر به طور خاص اصلاح شده (Specially Modified Compiler)، مانند afl-gcc، در برنامه قرار داده میشود. با این حال، در شرایطی که کد منبع (Source Code) در دسترس نباشد، انجام ابزارگذاری در زمان کامپایل امکانپذیر نیست. یک رویکرد جایگزین، ابزارگذاری مستقیم کد باینری (Binary Code) است که به آن ابزارگذاری باینری (Binary Instrumentation) گفته میشود. تکنیکهای موجود مانند AFL-qemu [2] یا باینری را همزمان با اجرای برنامه ابزارگذاری میکنند، یا از روش بازنویسی باینری (Binary Rewriting) مانند AFL-dyninst [3] استفاده میکنند.
بااینحال، هر یک از این رویکردها دارای محدودیتهایی میباشند. برای مثال، AFL-qemu مبتنی بر شبیهسازی (Emulation) است که میتواند سربار قابلتوجهی ایجاد کند؛ در آزمایشهای ما این سربار به حداکثر ۵ برابر رسیده است. از سوی دیگر، ابزارگذاری ایستای باینری (Static Binary Instrumentation) نیز ممکن است دقت کافی نداشته باشد و خطاهایی ایجاد کند که این خطاها میتوانند در فرایند فازینگ به شکل مثبت کاذب (False Positive) ظاهر شوند. در هر دو حالت، روشهای ابزارگذاری باینری معمولاً برای نرمافزارهای بزرگ یا پیچیده از نظر مقیاسپذیری مناسب نیستند. ما برای رفع محدودیتهای فوق، E9AFL را پیشنهاد میدهیم؛ ابزاری سریع و مقیاسپذیر برای ابزارگذاری باینری AFL (AFL Binary Instrumentation).
E9AFL بر پایه E9Patch [4] ساخته شده است؛ E9Patch یک سامانه مقیاسپذیر برای بازنویسی ایستای باینری میباشد که میتواند باینریهای بزرگ و پیچیده نماد زدایی شده (stripped binaries) را بدون اتکا به فرضیات یا ابتکارهای اکتشافی (heuristics) بازنویسی کند.
بهطور مشخص، E9Patch از یک روششناسی بازنویسی مبتنی بر ترامپولین (Trampoline یک قطعه کد واسط/پرشی است که ابزار بازنویسی باینری در یک محل از برنامه قرار میدهد تا اجرای برنامه را از کد اصلی به کد جدید یا ابزارگذاری منتقل کند و سپس دوباره به مسیر اصلی بازگرداند) استفاده میکند که با بازتعبیر دستورالعملها (Instruction Punning) [5] و افزونهها (Extension) [4] ترکیب شده است تا امکان درج ابزارگذاری در مکانهای دلخواه را فراهم کند.
بااینحال، این روششناسی ممکن است سربار قابلتوجهی ایجاد کند؛ زیرا بازنویسی باینری مبتنی بر ترامپولین موجب از بین رفتن پیوستگی کد (Code Contiguity) میشود. E9AFL به منظور کاهش این سربار، سه راهبرد اصلی بهینهسازی را معرفی میکند: (۱) مرتبسازی ترامپولینها (Trampoline Ordering)، انتخاب دستورالعمل (Instruction Selection) و حذف بلوکهای نامعتبر (Bad Block Elimination). ما E9AFL را در برابر تعدادی از برنامههای هدف منتخب از مجموعه معیارFuzzBench [6] ارزیابی میکنیم. نتایج آزمایشهای ما نشان میدهد که E9AFL با ۷۷٫۰٪ سرعت afl-gcc اجرا میشود و در عین حال به پوشش کد قابل مقایسه (Comparable Code Coverage) دست مییابد؛ عملکردی که از ابزارهای موجود AFL-qemu و AFL-dyninst بهتر است. در نهایت، مقیاسپذیری E9AFL را با فاز کردن باینری Google Chrome [7] نشان میدهیم.
۲. نحوه استفاده از ابزار (TOOL USAGE)
ابزار E9AFL با هدف سهولت استفاده طراحی شده است. بیشتر باینریها را میتوان تنها با استفاده از یک دستور ساده، ابزارگذاری (Instrumentation) کرد:
$ e9afl program
این دستور یک باینری ابزارگذاریشده با نام program.afl ایجاد میکند که میتوان آن را با استفاده از زنجیره ابزار استاندارد AFL (Standard AFL Toolchain) فاز کرد. برای مثال، میتوان باینری readelf را به شکل زیر ابزارگذاری و سپس فاز کرد:
$ e9afl readelf
$ afl-fuzz -i in/ -o out/ -- ./readelf.afl -a @@
هیچ مرحله اضافی یا پیکربندی ویژه دیگری موردنیاز نیست. در اینجا باید به سهولت استفاده از E9AFL در مقایسه با ابزارگذاری در سطح کد منبع (Source-Level Instrumentation) در AFL توجه کرد. برای ابزارگذاری یک برنامه در سطح کد منبع، لازم است ابتدا یک محیط کامپایل صحیح (Correct Compilation Environment) ایجاد گردد و برنامه با استفاده از یک کامپایلر بهطور خاص اصلاح شده، مانند afl-gcc، ساخته شود.
برای نرمافزارهایی که دارای سیستمهای Build پیچیده هستند، مانند Chrome، ایجاد محیط کامپایل مناسب و اعمال تغییرات لازم در سیستم Build میتواند کاری غیرساده و پیچیده باشد. در مقابل، E9AFL یک ابزار «Push-Button» است؛ یعنی برای استفاده از آن کافی است یک دستور را اجرا کنیم تا بتواند باینریهای از پیش ساخته شده (Pre-Built Binaries) را بدون نیاز به فرایند Build مجدد، ابزارگذاری کند.
۳. طراحی ابزار (TOOL DESIGN)
E9AFL بر پایه سیستم بازنویسی ایستای باینری (Static Binary Rewriting) یعنی E9Patch [4] ساخته شده است که برای باینریهایLinux x86_64 طراحی شده است. E9Patch از یک روششناسی بازنویسی مبتنی بر ترامپولین (trampoline) استفاده میکند؛ به این معنا که برخی دستورالعملهای انتخاب شده با دستورهای پرش (Jump) جایگزین میشوند. این پرشها جریان اجرای برنامه را به سمت ترامپولین هدایت میکنند که ابزارگذاری (Instrumentation) مربوط به AFL را پیادهسازی میکنند. نمونهای از این فرایند در شکل ۱ نشان داده شده است. در این شکل، برخی دستورالعملهای انتخاب شده که بخش (a) برجسته شدهاند، با دستورهای پرش (Jump) جایگزین میشوند. این پرشها، جریان کنترل (Control Flow) را به سمت ترامپولینهایی که در بخش (b) نمایش داده شدهاند، منحرف میکنند.
ترامپولینها وظیفه اجرای ابزارگذاری (Instrumentation) را بر عهده دارند و همچنین دستورالعمل جا به جا شده (Displaced Instruction) را اجرا میکنند؛ سپس جریان کنترل را مجدداً به برنامه اصلی بازمیگردانند. E9Patch برای دستورالعملهای کوتاهتر از ۵ بایت (که اندازه یک دستور jmpq است)، از تکنیک بازتفسیر دستورالعملها (Instruction Punning) [4] برای درج Jump استفاده میکند. روششناسی مبتنی بر ترامپولین در E9Patch یک روش عمومی و به شدت مقیاسپذیر (Highly Scalable) است و میتواند نرمافزارهای بزرگ و پیچیدهای مانند Chrome را نیز بازنویسی کند؛ حتی زمانی که اندازه باینری بیش از ۱۰۰ مگابایت باشد.
مهمترین نقطه ضعف بازنویسی مبتنی بر ترامپولین، کارایی (Performance) است؛ زیرا پرشهای اضافی به سمت ترامپولینها و بازگشت از آنها، در زمان اجرای برنامه سربار اجرایی (Runtime Overhead) ایجاد میکنند. یک روش جایگزین، بازنویسی باینری درون خطی (Inline Binary Rewriting) است که تلاش میکند ابزارگذاری (Instrumentation) را مستقیماً در رشته دستورالعملهای موجود (Existing Instruction Stream) وارد کند [8]، [9]. با این حال، بازنویسی درون خطی دارای محدودیتهای شناختهشدهای است؛ برای مثال، در باینری بازنویسی شده لازم است آفستهای (Offset) مربوط به دستورهای پرش/فراخوانی (Jump/Call) اصلاح شوند. انجام این کار در حالت کلی یک مسئله تصمیمناپذیر (Undecidable Problem) است.
E9AFL به صورت یک افزونه (Plugin) برای Frontend ابزار E9Patch پیادهسازی شده است. افزونه E9AFL، دیساسمبلی (Disassembly) باینری ورودی را که از Frontend دریافت میکند، به عنوان ورودی دریافت کرده و سه خروجی تولید میکند: (۱) یک قالب ترامپولین (trampoline) برای ابزارگذاری AFL ،(۲) زمان اجرای AFL و (۳) مجموعهای از مکانهای مناسب برای ابزارگذاری (Instrumentation Locations). سپس این اطلاعات به E9Patch منتقل میشود و E9Patch بر اساس آنها، باینری ابزارگذاری شده با AFL را تولید میکند.
قالب ترامپولین (Trampoline Template): قالب پایه ترامپولینکه توسط E9AFL استفاده میشود، به صورت زیر است:
... # Save state
mov prev_loc,%eax # Load prev_loc
xor $cur_loc,%eax
incb AREA(%eax) # AREA[cur_loc^prev_loc]++
movl $cur_loc>>1,prev_loc # Set prev_loc
... # Restore state
قالب ترامپولین، ابزارگذاری کلاسیک AFL را پیادهسازی میکند؛ یعنی AREA[cur_loc ^ prev_loc]++، در اینجا: AREA نقشه رهگیری مشترک AFL است، prev_loc موقعیت اجرای قبلی است که در فضای ذخیرهسازی محلی هر نخ (Thread Local Storage (TLS)) نگهداری میشود و cur_loc موقعیت اجرای فعلی است که بهصورت یک ثابت اختصاصی برای هر ترامپولین تعریف میگردد.
ترامپولین (trampoline) همچنین شامل مقداری کدهای آماده و تکراری (Boilerplate Code) برای ذخیره و بازیابی فلگهای CPU (CPU Flags) و ثبات %rax است؛ زیرا این مقادیر توسط عملیات ابزارگذاری مورد استفاده قرار میگیرند. این کدهای آماده و تکراری، مشابه کدی میباشند که afl-gcc نیز با همین هدف در برنامه وارد میکند. خود ابزارگذاری اساساً وظیفه شمارش انتقالهای بین یالها (Edge Transitions) را بر عهده دارد:
prev_loc → cur_loc
شمارندهها در نقشه رهگیری AREA ذخیره میشوند؛ این نقشه با afl-fuzz به صورت مشترک (Shared) استفاده میشود. این سازوکار به AFL اجازه میدهد اطلاعات مربوط به پوشش مسیر (Path Coverage) را جمعآوری کند؛ اطلاعاتی که برای انجام جعبه خاکستری ضروری است.
تزریق در زمان اجرا (Runtime Injection): زمان اجرا (Runtime) مربوط به AFL، شامل Fork Server و فرایند مقداردهی اولیه AREA، باید در باینری ابزارگذاری شده نیز تزریق شود. برای این منظور، از قابلیتی در E9Patch استفاده میکنیم که امکان تزریق کد کاربر (User Code) به باینری بازنویسی شده را در هنگام راهاندازی برنامه (Program Initialization) فراهم میکند؛ یعنی پیش از آنکه تابع ()main فراخوانی شود. زمان اجرای (Runtime) تزریق شده از زمان اجرای استاندارد AFL مشتق شده است و تنها تغییرات جزئی روی آن اعمال شده است.
تعیین مکانهای ابزارگذاری (Determining Instrumentation Locations): همانند AFL در سطح کد منبع (Source Code)، بهتر است ابزارگذاری یکبار برای هر بلوک پایه (Basic Block) قرار داده شود. E9AFL به منظور یافتن مجموعه بلوکهای پایه، یک تحلیل سبک برای بازیابی جریان کنترل (Control-Flow Recovery Analysis) پیادهسازی میکند. این تحلیل تمام اهداف احتمالی پرش (Likely Jump Targets) را از باینری ورودی شناسایی میکند؛ از جمله تمام اهداف مستقیم (Direct Targets)، تمام اهداف غیرمستقیم احتمالی از طریق تحلیل بخشهای داده (Data Segment) برای یافتن جدول پرش ها و اشارهگرهای کد (Code Pointer).
بازیابی دقیق اطلاعات جریان کنترل (Control Flow) در حالت کلی یک مسئله تصمیمناپذیر (Undecidable Problem) است. با این حال، برای کاربرد ابزارگذاری AFL، اطلاعات جریان کنترل بازیابی شده لازم نیست کاملاً دقیق و بدون خطا باشد. در حالت تقریب بیش از حد یا بیشبرآورد (Overapproximation)، یعنی شناسایی اهداف پرش اضافی (Superfluous Jump Targets)، ممکن است ابزارگذاری بیشتری نسبت به مقدار مورد نیاز واقعی قرار داده شود. این موضوع میتواند منجر به افزایش سربار زمان اجرا (Runtime Overhead) گردد، اما در حالت کلی اثر مخربی بر صحت اجرای برنامه ندارد.
در مقابل، در حالت تقریب ناقص یا کمبرآورد (Underapproximation)، یعنی از دست رفتن برخی اهداف پرش (Missed Jump Targets)، ممکن است اطلاعات پوشش (Coverage) ارائه شده به فازر از دقت کمتری برخوردار باشد. در نتیجه، afl-fuzz ممکن است برخی مسیرهای جدیدی را که در شرایط دیگر قابل شناسایی بودند، تشخیص ندهد. با این حال، حتی با وجود کاهش جزئی در دقت، فرایند فازینگ همچنان میتواند نتایج مفیدی تولید کند. E9AFL از یک روش بازیابی جریان کنترل (Control Flow) مبتنی بردادههای واقعی (Heuristic-Based Control-Flow Recovery) استفاده میکند که برای اغلب برنامههایی که با کامپایلرهای استاندارد، مانند gcc، کامپایل شدهاند، دقت قابلقبولی دارد.
در خصوص فایل اجرایی مستقل از موقعیت (Position Independent Executables – PIE)، دقت این تحلیل بیشتر نیز میشود؛ زیرا تحلیل میتواند از مکانیابی مجدد ELF (ELF Relocation – منظور همان اصلاح و بازیابی مجدد آدرس ELF است) برای شناسایی دقیق اشارهگرهای کد (Code Pointer) در بخشهای داده (Data Segment) استفاده کند. پس از شناسایی تمام اهداف پرش (Jump)، مجموعه بلوکهای پایه قابل استخراج است. سپس ابزارگذاری در نقطه ورود هر بلوک پایه قرار داده میشود. این کار با ارسال دستورهای وصلهبندی دستورالعملها (Instruction Patching) به E9Patch و استفاده از قالب ابزارگذاری AFL (AFL Instrumentation Template) انجام میشود. در نهایت، E9Patch فرایند بازنویسی باینری (Binary Rewriting) را تکمیل کرده و باینری نهایی ابزارگذاری شده را تولید میکند.
۴. بهینهسازی ابزار (TOOL OPTIMIZATION)
طراحی پایه E9AFL ازتوان عملیاتی (Throughput) پایین فازینگ، یعنی تعداد کم اجرای تست در ثانیه (execs/s)، رنج میبرد؛ بنابراین انجام برخی بهینهسازیها ضروری است. ما برای این کار، از یافتههای پژوهش اخیر FuZZan [10] استفاده میکنیم. نتایج این پژوهش حاکی از آن است که بخش قابلتوجهی از زمان فازینگ، صرف اجرای موارد آزمون (Test Case) نمیشود، بلکه صرف راهاندازی و خاتمه دادن به فرآیندها (Startup/Teardown) پس از ()fork میشود. بنابراین، ما با تمرکز بر همین نتایج، E9AFL را بهینه میکنیم.
در حالت عادی، فازرهایی مانند AFL برنامه هدف را چندین بار اجرا میکنند؛ بهطوری که برای هر مورد آزمون (Test Case) تولید شده، یک اجرای جداگانه انجام میشود. برای انجام این کار، AFL از یک Fork Server استفاده میکند که در اصل یک حلقه فراخوانی رویه از راه دور (Remote Procedure Call – RPC) است که داخل برنامه هدف تزریق شده است. هر زمان که یک مورد آزمون (Test Case) جدید تولید شود، afl-fuzz از طریق یک RPC به برنامه هدف دستور میدهد که با استفاده از فراخوانی سیستمی (System Call) مربوط به ()fork یک کپی از خود ایجاد کند. در اینجا، ()fork اساساً فرایند فراخواننده را به دو فرایند والد (Parent Process) و فرزند (Child Process) تکثیر میکند.
فرایند فرزند، مورد آزمون (Test Case) را اجرا میکند و اطلاعات پوشش (Coverage) را از طریق ابزارگذاری AFL جمعآوری میکند؛ سپس یا بهصورت عادی و یا به صورت غیرعادی خاتمه پیدا میکند؛ برای مثال، در صورت وقوع کرش (Crash). فرایند والد (Parent Process) منتظر میماند تا فرزند (Child) اجرای خود را به پایان برساند. سپس وضعیت خروج (Exit Status) حاصل را به فرایند اصلی afl-fuzz ارسال میکند و مجدداً منتظر RPC بعدی میماند.
فراخوانی سیستمی ()fork عملیاتی نسبتاً کند است و یکی از گلوگاههای اصلی (Bottleneck) در توان عملیاتی کلی فازر، یعنی تعداد اجرا در ثانیه (execs/sec) محسوب میشود. نسخههای مدرن لینوکس تلاش میکنند با جلوگیری از کپیکردن حافظه تا حد امکان، از جمله کپیکردن ورودیهای جدول صفحه (Page Table Entry)، عملیات fork() را بهینه کنند. برای مثال، هر ورودی جدول صفحه که مربوط به یک نگاشت فایل (File Mapping) باشد، هنگام اجرای ()fork به صورت صریح کپی نمیشود. در عوض، تنها زمانی که فرآیند فرزند (Child Process) واقعاً به آن نگاشت (Mapping) دسترسی پیدا کند، یک خطای صفحه (Page Fault) ایجاد میشود. این خطای صفحه به کرنل (Kernel) اجازه میدهد ورودیهای جدول صفحه (Page Table Entry) مربوطه را با تاخیر (Lazy) ایجاد و تنظیم کند. در حوزه فازینگ، این خطاهای صفحه (Page Fault)، یکی از عوامل اصلی ایجاد هزینه راه اندازی (Startup Cost) در فرآیند فرزند هستند و میتوانند به یک عامل غالب در عملکرد کلی فازر تبدیل شوند [10].
Trampolineها و خطاهای صفحه (Page Fault):
با توجه به این نکات، میتوان طراحی پایهی E9AFL را بهینه کرد. یکی از منابع اصلی ایجاد خطاهای صفحه (Page Fault) در باینری بازنویسی شده (Rewritten Binary)، ترامپولینهایی (trampoline) هستند که برای پیادهسازی ابزارگذاری AFL استفاده میشوند. بنابراین، برای به حداقل رساندن خطاهای صفحه، راهبرد ما دو بخش دارد: (۱) حافظهی مربوط به ترامپولینها را تا حد امکان پیوسته (Contiguous) قرار دهیم. (۲) در صورت امکان، ترامپولینها را حذف کنیم. برای انجام این کار، سه بهینهسازی اصلی را پیادهسازی میکنیم:
- 1. مرتبسازی ترامپولینها (Trampoline Ordering): ترامپولینها را بهصورت ترتیبی (In Order)در حافظه تخصیص میدهیم.
- 2. انتخاب دستورالعمل (Instruction Selection): دستورالعملهایی را برای ابزارگذاری انتخاب میکنیم که امکان مرتبسازی بهتر ترامپولینها را فراهم کنند.
- 3. حذف بلوکهای نامعتبر (Bad Block Elimination): تلاش میکنیم ابزارگذاریهای اضافی و زائد (Redundant Instrumentation) را حذف کنیم؛ زیرا این ابزارگذاریها احتمالاً باعث ایجاد خطای صفحه (Page Fault) خواهند شد.
مرتبسازی ترامپولینها (Trampoline Ordering): ایدهی مرتبسازی ترامپولینها بسیار ساده است. ترامپولینها را بهصورت پیوسته و با همان ترتیبی در حافظه تخصیص میدهیم که دستورالعملهای متناظرِ وصله شده (Patch) قرار گرفتهاند. بنابراین، نواحی یکسان کد به همان ناحیهی حافظهی ترامپولین نگاشت میشوند و در نتیجه خطاهای صفحه (Page Fault) به حداقل میرسند.
بااینحال،E9Patch برای دستورالعملهای کوتاهتر از ۵ بایت (<5 bytes)، به منظور درج ترامپولین از بازتعبیر دستورالعملها (Instruction Punning) استفاده میکند. این بدان معناست که E9Patch کنترل کاملی بر محل قرارگیری ترامپولینها ندارد و بنابراین همچنان احتمال بروز مقداری پراکندگی (Fragmentation) در ترامپولینها وجود دارد. به منظور کاهش این مشکل، میتوانیم انتخاب دستورالعملهایی را که برای ابزارگذاری در نظر گرفته میشوند، بهینه کنیم.
انتخاب دستورالعمل (Instruction Selection): بهطور سنتی، ابزارگذاری AFL، در ابتدای هر بلوک پایه (Basic Block) درج میشود. با این حال، میتوان ابزارگذاری را در محل دیگری از همان بلوک پایه نیز درج کرد و همچنان همان عملکرد را حفظ کرد. E9AFL یک الگوریتم ساده برای انتخاب دستورالعمل به کار میگیرد تا در صورت امکان، دستورالعملی با اندازهی ≥ ۵ بایت (size ≥5 bytes) را انتخاب کند و بدین ترتیب، امکان اعمال بهینهسازی مرتبسازی ترامپولینها (trampoline) برای تعداد بیشتری از بلوک های پایه فراهم شود.
با این حال، توجه داشته باشید که همهی بلوک های پایه دارای دستورالعملهایی با اندازهی ≥ ۵ بایت (size ≥5 bytes) نیستند. ما این موارد را بلوکهای نامعتبر (Bad Block) مینامیم، زیرا بهینهسازی مرتبسازی ترامپولینها نمیتواند روی آنها اعمال شود؛ در نتیجه، ترامپولین متناظر با آنها احتمال بیشتری دارد که موجب ایجاد خطای صفحه (Page Fault) شود و سرعت فازینگ را کاهش دهد. برای کاهش این مشکل، میتوان تلاش کرد ترامپولینهای مربوط به بلوکهای نامعتبر را بهطور کامل حذف کرد.
حذف بلوکهای نامعتبر (Bad Block Elimination): گاهی اوقات، ابزارگذاری AFL مربوط به یک هر بلوک پایه (Basic Block) مشخص، زائد (Redundant) است؛ به این معنا که میتوان این ابزارگذاری را بدون تأثیرگذاری بر پوشش مسیر (Path Coverage)، بهطور ایمن حذف کرد. برای مثال، فرض کنید تمام مسیرهایی که از بلوک A عبور میکنند، الزاماً از بلوک B نیز عبور میکنند و بالعکس. در این صورت، تنها یکی از بلوکهای A یا B نیاز به ابزارگذاری دارد، زیرا عبور یک مسیر از یکی از آنها، عبور همان مسیر از دیگری را نیز نتیجه میدهد.
این موضوع را میتوان به مسئلهی متمایزسازی مسیرها (Path Differentiation) تعمیم داد؛ یعنی حداقل تعداد رأسهای گراف جریان کنترل (Control Flow Graph (CFG)) (یعنی بلوکها) که میبایست علامتگذاری شوند (یعنی ابزارگذاری شوند) چقدر است، بهگونهای که همچنان بتوان تمام مسیرهای عبوری از CFG را از یکدیگر متمایز کرد؟
ابزارهایی مانند INSTRIM [11] از این ایده برای بهینهسازی فازینگ استفاده میکنند و تا حد امکان ابزارگذاری (Instrumentation) را حذف میکنند؛ بهطور متوسط، میتوان تا ۸۰٪ از ابزارگذاری را حذف کرد. با این حال، در زمینهی E9AFL، بینش اصلی ما این است که «کیفیت» مهمتر از «کمیت» است. بهطور مشخص، باید در درجهی اول ابزارگذاری مربوط به بلوکهای نامعتبر (Bad Block) را حذف کنیم، زیرا این بلوکها منبع اصلی خطای صفحههای اضافی هستند که باعث کند شدن فازینگ میشوند.
بهینهسازی حذف بلوکهای نامعتبر (Bad Block Elimination) از الگوریتمی با هدفی مشابه الگوریتم INSTRIM [11] استفاده میکند. ابتدا، الگوریتم CFG را بر اساس مجموعهی بازیابیشدهی بلوکهای پایهای (Basic Block) ایجاد میکند. سپس، هر بلوک را بهعنوان بهینه شده (Optimized) یا بهینه نشده (Unoptimized) علامتگذاری میکند؛ در اینجا، بهینه شده (Optimized) به این معناست که آن بلوک نباید ابزارگذاری (Instrument) شود. در ابتدا، همه ی بلوکها به شکل زیر علامتگذاری میشوند:
- بلوکهای معتبر (Good Block) در ابتدا به عنوان بهینه نشده (Unoptimized) علامتگذاری میشوند.
- بلوکهای نامعتبر (Bad Block) که اهداف بالقوهی پرش/فراخوانی غیرمستقیم (Indirect Jump/Call) هستند نیز در ابتدا به عنوان بهینهسازی نشده (Unoptimized) علامتگذاری میشوند.
- تمام بلوکهای نامعتبر (Bad Block) دیگر در ابتدا به عنوان بهینهسازی شده (Optimized) علامتگذاری میشوند.
در اینجا، مورد مورد دوم، یک سادهسازی است که نیاز به دنبالکردن مسیرها در طول پرش/فراخوانی غیرمستقیم (Indirect Jump/Call) را از بین میبرد. بخش باقیماندهی الگوریتم تلاش میکند راهحلی برای مسئلهی متمایزسازی مسیرها (Path Differentiation) پیدا کند و این کار را با ساختن تمام مسیر فرعی (Sub-path) σ = 〈A → … → B〉 در سراسر CFG انجام میدهد، بهگونهای که (۱) بلوکهای A و B، بهینه نشده (Unoptimized) باشند، و (۲) تمام بلوکهای میانی بین A و B، بهینه شده (Optimized) باشند.
توجه داشته باشید که Loop یا همان حلقهها (A = B) نیز مجاز هستند و در حالتی که آخرین یال (Edge) یک پرش/فراخوانی غیرمستقیم (Indirect Jump/Call) باشد، مطابق مورد ۲ فوق، میتوان B را یک شبه بلوک (Pseudo-block) بدون بهینهسازی در نظر گرفت. خاصیت متمایزسازی مسیرها زمانی نقض میشود که دو یا چند مسیر فرعی متمایز (σ₁ ≠ σ₂) برای همان جفت (A, B) وجود داشته باشند که:
σ1 = 〈A→ ... →B〉 and σ2 = 〈A→ ... →B〉
به منظور برقراری مجدد این ویژگی، الگوریتم بهصورت حریصانه (Greedy) یکی از بلوکهای میانیِ بهینه شده موجود در σ₁/σ₂ را بهعنوان بهینه نشده علامتگذاری میکند. این فرایند تکرار میشود تا دیگر هیچ جفت مسیر فرعی از نوع (σ₁, σ₂) وجود نداشته باشد.
مثال: نمونهای از کد بهینهسازیشده در شکل ۱ نشان داده شده است. در اینجا، BB_entry یک بلوک معتبر (Good Block) است (زیرا دستورالعمل مربوط به آزمون (test) دارای اندازهی ≥ ۵ بایت است) و بهعنوان بهینه نشده علامتگذاری میشود. بلوکهای باقیمانده، یعنی BB_prehdr، BB_loop و BB_exit، بلوکهای نامعتبر (Bad Block) هستند (زیرا تمام دستورالعملهای دیگر اندازهای کمتر از ۵ بایت دارند) و در ابتدا به عنوان بهینه شده علامتگذاری میشوند. ویژگی مسئلهی متمایزسازی مسیرها (Path Differentiation) توسط مسیرهای فرعی زیر نقض میشود:
σ1 =〈BB_entry → BB_prehdr → BB_exit〉
σ2 = 〈BB_entry → BB_prehdr → BB_loop → BB_exit〉
جدول ۱. نتایج آزمایشها. در این جدول، speed نشاندهندهی سرعت فازینگ، یعنی تعداد اجراها در ثانیه است و cov (%) نشاندهندهی Line Coverage یا پوشش خطوط کد بهصورت درصدی است:
ویژگی متمایزسازی مسیرها (Path Differentiation) توسط الگوریتم با علامتگذاری BB_loop به عنوان بهینه نشده بازیابی خواهد شد. کد بهینهشده در شکل ۱ تنها از دو ترامپولین (trampoline) استفاده میکند، در حالی که در طراحی پایه و بدون بهینهسازی به چهار ترامپولین نیاز است که این ترامپولینها ممکن است به صورت غیرپیوسته (Non-contiguous) در حافظه قرار گرفته باشند؛ یعنی برای هر بلوک پایه یک ترامپولین موردنیاز خواهد بود.
۵. آزمایشها (EXPERIMENTS)
- ما کارایی و اثربخشی E9AFL را بر روی پنج برنامه شامل FreeType، libjpeg، libpng، libxml و Vorbis که از مجموعه FuzzBench [6] انتخاب شدهاند، ارزیابی میکنیم. به منظور بررسی مقیاسپذیری (Scalability)، E9AFL را همچنین بر روی باینری Google Chrome [7] ارزیابی میکنیم. ارزیابی ما سه تکنیک قابلمقایسه را در نظر میگیرد:
- AFL-gcc [1]: روش اصلی ابزارگذاری AFL در زمان کامپایل
- AFL-qemu [2]: یک پیادهسازی باینری AFL مبتنی بر شبیهساز QEMU [12]
- AFL-dyninst [3]: یک روش ابزارگذاری AFL باینری مبتنی بر ابزار Dyninst [13] برای بازنویسی باینری (Binary Rewriting)
علاوه بر E9AFL، با فعال بودن کامل بهینهسازیها، نسخه E9AFL-O0 را نیز با غیرفعال بودن بهینهسازیها ارزیابی میکنیم. به جز AFL-gcc که نیازمند کد منبع (Source Code) است، سایر ابزارها مستقیماً باینریها را ابزارگذاری میکنند. تمام آزمایشها روی یک پردازنده Intel Xeon CPU E5-2660 با فرکانس 2.00GHz و 64 گیگابایت حافظه اجرا شدهاند. مدتزمان اجرای هر آزمایش 24 ساعت میباشد. نتایج ارزیابی ما در جدول ۱ ارائه شدهاند و نتیجه اصلی با حروف برجسته (Bold) مشخص شده است. در این جدول، «speed» نشاندهنده تعداد اجراها در ثانیه است، cov (%) نشاندهنده پوشش خطی (Line Coverage) یا پوشش خطوط کد بهصورت درصدی است، و n.r. (no result) به این معناست که ابزار مربوطه در شرایط آزمایش ما بهدرستی عمل نکرده است.
الف) کارایی فازینگ (Fuzzing Efficiency): به طور کلی مشاهده میکنیم که AFL-qemu بسیار کُند میباشد و تنها با 19.2٪ از سرعت (execs/s) روش مبنا یعنی AFL-gcc اجرا میشود. در مقابل، با فعال بودن تمامی بهینهسازیها، E9AFL با77.0٪ از سرعت AFL-gcc اجرا میگردد. این بدان معناست کهE9AFL تقریباً به همان سطح عملکرد AFL-gcc دست مییابد، بدون آنکه نیازی باشد برنامه از روی کد منبع مجدداً کامپایل شود. در نهایت، AFL-dyninst قابلیت اطمینان کمتری از خود نشان داد؛ بهطوریکه دو مورد از برنامههای مورد آزمایش با آن کار نکردند. AFL-dyninst قادر به ابزارگذاری libxml2 نبود و نسخه ابزارگذاری شده libjpeg نیز هنگام دریافت ورودیهای عادی و غیرمخرب (Benign Inputs) دچار کرش (Crash) میشد. با این وجود، E9AFL همچنان برای سایر برنامههای مورد آزمایش که با AFL-dyninst بهدرستی کار میکردند، عملکرد بهتری نسبت به AFL-dyninst ارائه داد.
ب) اثربخشی فازینگ (Fuzzing Effectiveness): از نظر اثربخشی، AFL-gcc، AFL-qemu و E9AFL بهترتیب به 17.3٪، 15.0٪ و 16.6٪ پوشش خطی (Line Coverage) دست مییابند. نتایج ما نشان میدهد که E9AFL میتواند پوشش کد (Code Coverage) قابلمقایسهای با AFL-gcc ایجاد کند. در مقایسه با سایر ابزارهای ابزارگذاری صرفا باینری (Binary-Only Instrumentation)، از جمله موارد آزمایشی AFL-dyninst که بهدرستی کار میکردند، E9AFL در مجموع بهترین پوشش کد (Code Coverage) را به دست میآورد.
ج) مقیاسپذیری (Scalability): از نظر مقیاسپذیری (Scalability)، مشاهده میکنیم که E9AFL تنها ابزاری است که میتواند با موفقیت Chrome را تحت فازینگ قرار دهد. AFL-gcc قابل استفاده نیست، زیرا کد منبع (Source Code) مربوط به Chrome در دسترس نیست (کد منبع Chrome بصورت عمومی و باز در دسترس نیست (Closed Source)). AFL-qemu نیز با شکست مواجه میشود، زیرا شبیهساز QEMU از برخی فراخوانیهای سیستمی (System Call) که Chrome استفاده میکند پشتیبانی نمیکند. همچنین AFL-dyninst نتوانست باینری Chrome را بهدرستی دیساسمبل (Disassemble) کند. E9AFL میتواند باینری Chrome را با موفقیت ابزارگذاری (Instrument) کند و سپس این باینری را میتوان با استفاده از AFL در حالت Headless، یعنی بدون رابط کاربری (User Interface)، تحت فازینگ قرار داد:
$ afl-fuzz ... -- ./chrome.afl --headless @@
شایان ذکر است که Chrome یک باینری بزرگ و چند نخی (Multi-Thread) میباشد و راهاندازی آن سربار زیادی (Startup Overhead) خواهد داشت؛ ازاینرو، هدف ایدهآلی برای فازینگ محسوب نمیشود. با این حال، afl-fuzz میتواند Chrome را که ابزارگذاری شده است، با تعیین محدودیتهای مناسب برای memory (-m) و Timeout (-t) تحت فازینگ قرار دهد.
د) بهینهسازی (Optimization): در نهایت، عملکرد فازینگ را قبل و بعد از اعمال بهینهسازی ارزیابی میکنیم. در اینجا، ستون E9AFL-O0 در جدول ۱ نشاندهنده نتایج قبل از بهینهسازی است. بهطور کلی، مشاهده میکنیم که E9AFL-O0 تنها با 19.5٪ از سرعت AFL-gcc اجرا میشود که عملاً عملکرد کند AFL-qemu را تکرار میکند. در مقابل، E9AFL کاملاً بهینهشده با 77.0٪ از سرعت AFL-gcc اجرا میشود؛ یعنی عملکرد آن تقریباً 4 برابر نسخه بدون بهینهسازی است. این نتایج نشان میدهند که بهینهسازی نهتنها مؤثر است، بلکه برای دستیابی به عملکرد مناسب در فازینگ و همچنین نتیجه مطلوب در پوشش کد (Code Coverage)، ضروری است.
۶. نتیجهگیری (CONCLUSION)
چندین ابزار برای ابزارگذاری باینری (Binary Instrumentation) در AFL پیشنهاد شدهاند. برخی از این ابزارها کند هستند [2]، برخی از زنجیره ابزارهای (Toolchain) اصلاحشده استفاده میکنند [14]، برخی از آنها پشتیبانی محدودی از باینریها دارند (برای مثال، تنها ازفایلهای اجرایی مستقل از موقعیت (Position-Independent Executable) پشتیبانی میکنند) [8]، و برخی دیگر ممکن است مثبت کاذب (False Positive) ایجاد کنند [3]، [9]. در این مقاله، ما E9AFL را معرفی کردیم؛ ابزاری جدید برای درج خودکار ابزارگذاری AFL در کد باینری موجود که از سیستم [4] بازنویسی باینری ایستای E9Patch (یا E9Patch Static Binary Rewriting) استفاده میکند. ما نشان دادیم که E9AFL به 77.0٪ از عملکرد AFL-gcc دست مییابد و در عین حال پوشش کد (Code Coverage) قابلمقایسهای ارائه میدهد؛ آن هم بدون نیاز به کامپایل مجدد و اینکه فرض شود کد منبع در دسترس است. در مقایسه با سایر راهکارهای باینری AFL، ابزار E9AFL سرعت و پوشش کد را بهطور قابلتوجهی بهبود میدهد، مثبت کاذب (یا سایر خطاهای ناشی از بازنویسی باینری (Binary Rewriting)) ایجاد نمیکند و میتواند تا برنامههای بسیار بزرگی مانند Google Chrome [7] مقیاسپذیر باشد.
قدردانی (ACKNOWLEDGEMENTS)
این پژوهش بخشی از حمایت مالی خود را از مرکز ملی برتری در زمینه سامانههای نرمافزاری قابلاعتماد دریافت کرده است. این حمایت توسط بنیاد ملی پژوهش سنگاپور (NRF) و در چارچوب برنامه ملی پژوهش و توسعه امنیت سایبری (NCR) تأمین شده است.
منابع
[1] M. Zalewski. (2021) American Fuzzy Lop. [Online]. Available:https://lcamtuf.coredump.cx/afl
[2] A. Griffiths and M. Zalewski. (2021) AFL QEMU Mode. [Online]. Available: https://github.com/google/AFL/blob/master/qemumode/README.qemu
[3] Cisco Talos. (2021) AFL Dyninst. [Online]. Available: https://github.com/talos-vulndev/afl-dyninst
[4] G. Duck, G. Xiang, and A. Roychoudhury, “Binary Rewriting without Control Flow Recovery,” in Programming Language Design and Implementation. ACM, 2020.
[5] B. Chamith, B. Svensson, L. Dalessandro, and R. Newton, “Instruction Punning: Lightweight Instrumentation for x86-64,” in Program Design and Implementation. ACM, 2017.
[6] Google. (2021) FuzzBench. [Online]. Available: https://github.com/google/fuzzbench
[7] Google. (2021) Google Chrome Web Browser. [Online]. Available:https://www.google.com/chrome
[8] S. Dinesh, N. Burow, D. Xu, , and M. Payer, “RetroWrite : Statically Instrumenting COTS Binaries for Fuzzing and Sanitization,” in Security and Privacy. IEEE, 2020.
[9] S. Nagy, A. Nguyen-Tuong, J. D. Hiser, J. W. Davidson, and M. Hicks,“Breaking Through Binaries: Compiler-quality Instrumentation for Better Binary-only Fuzzing,” in Security Symposium. USENIX, 2021.
[10] Y. Jeon, W. Han, N. Burow, and M. Payer, “FuZZan: Efficient Sanitizer Metadata Design for Fuzzing,” in Annual Technical Conference. USENIX, 2020.
[11] C. Hsu, C. Wu, H. Hsiao, and S. Huang, “Instrim: Lightweight instrumentation for coverage-guided fuzzing,” in NDSS, Workshop on Binary Analysis Research. Internet Society, 2018.
[12] F. Bellard, “QEMU, a Fast and Portable Dynamic Translator,” in Annual Technical Conference. USENIX, 2005.
[13] A. Bernat and B. Miller, “Anywhere, Any-time Binary Instrumentation,”in Workshop on Program Analysis for Software Tools, 2011.
[14] Z. Zhang, W. You, G. Tao, Y. Aafer, X. Liu, and X. Zhang, “StochFuzz:Sound and Cost-effective Fuzzing of Stripped Binaries by Incremental and Stochastic Rewriting,” in Security and Privacy. IEEE, 2021.