پیشرفتهای حوزه آزمون فاز (fuzz testing) باعث شده است که بتوان به صورت سریع و جامع، خطاهای بحرانی از نظر امنیتی را در سیستمهای نرمافزاری شناسایی کرد. با این حال، برخی از این تکنیکها به دسترسی به کد منبع (source-code) متکی هستند که غالبا بصورت عملی ممکن نیست.
ما در این مقاله، به بررسی روشهایی میپردازیم که بتوانند عمق و کارایی فازرهایی که به کد منبع دسترسی دارند را، از طریق ابزارگذاری ایستای باینری (static binary instrumentation) بازتولید کنند. توسعه چنین ابزارگذاری دشوار است، زیرا فرایند کامپایل یک فرایند اتلافی (lossy process) است و بسیاری از معناهای سطح کد منبع (source-level semantics) که این تکنیکها به آنها متکی هستند، در باینریها وجود ندارند. ما بخش زیادی از این اطلاعات را با استفاده از بازسازی جریان کنترل اکتشافی (heuristic control flow reconstruction)، استفاده از یک پشته سایه (shadow stack) برای شناسایی توابع و یک تکنیک جدید برای ابزارگذاری دستورات مقایسه (instrumenting comparison instructions) بازیابی میکنیم.
ما ابزار RWFUZZ را روی مجموعه داده LAVA-M ارزیابی کردیم و به اثربخشی مشابه بهترین فازرهای مبتنی بر کد منبع با تنها ۳.۴ برابر سربار زمانی اجرا (که کمتر از روشهای موجود مبتنی بر فازینگ پویا است) دست یافتیم. به این ترتیب، نشان میدهیم که تکنیکهای فازینگ باینری میتوانند به توانمندی عملکردی فازینگ با کد منبع در دسترس (source-available fuzzing) نزدیک شوند.
1. مقدمه (INTRODUCTION)
آزمون فاز یا فازینگ (Fuzz testing- fuzzing) یک تکنیک خودکار است که با تولید ورودیهای جالب و جدید از روی ورودیهای سالمِ شناخته شده، به کشف حالتهای مرزی (corner cases) و باگهایی (Bug) کمک میکند که معمولاً توسط آزمونهای نوشته شده دستی (manually-written test cases) نادیده گرفته میشوند. فازرها با دنبال کردن ورودیهایی که پوشش کد (code coverage) بالایی ایجاد میکنند و با استفاده از روشهای ابتکاری برای تولید ورودیهای جدید، به صورت کارآمد فضای حالتهای برنامه را جستجو میکنند. پیشرفتهای اخیر در فازینگ منجر به پوشش کاملتر حالتها و وضعیتهای برنامه (program states) [11] و تکنیکهای مؤثرتر برای تولید ورودیهای جدید [31]، [24]، [22]، [11]، [6] شدهاند. بسیاری از پروژههای محبوب و مهم در حال حاضر از فازینگ برای جلوگیری از رسیدن آسیبپذیریها به کاربران نهایی (end users) استفاده میکنند و کشف بسیاری از آسیبپذیریهای نرمافزاری را میتوان به کرشهایی (crash) نسبت داد که توسط فازرها شناسایی شدهاند [31].
فازرها اغلب از دسترسی به کد منبع (source code) برای تولید ابزارگذاری (instrumentation) کارآمد و کشف سریع باگها استفاده میکنند [31]، [3]، [11]. برای مثال، Chen و همکارانش اخیراً ابزار Angora را معرفی کردهاند [11] که از اطلاعات نوعدهی (typing) و جریان کنترل (control flow) در سطح کد منبع (source-level) برای تولید ابزارگذاری مؤثر استفاده میکند.
در بسیاری از موارد، فرض دسترسی به کد منبع منطقی است. برای نمونه، توسعهدهندهای که از فازینگ برای شناسایی و رفع باگها در نرمافزار خود استفاده میکند، طبیعتاً به کد منبع دسترسی دارد. با این حال، در برخی سناریوها این فرض چندان قابل اتکا نیست. بهطور کلی، دو حالت اصلی وجود دارد که در آنها فازینگ روی برنامههای اجرایی بدون دسترسی به کد منبع انجام میشود: (۱) تولید اکسپلویت خصمانه (adversarial exploit) علیه نرمافزارهای شخص ثالث و (۲) ارزیابی و تضمین امنیت نرمافزار (software assurance) روی سیستمهای قدیمی (legacy) یا نرمافزارهای اختصاصی (proprietary) که کد منبع آنها در دسترس نیست. در نبود کد منبع، بسیاری از تکنیکها دیگر قابل استفاده نیستند. دلایل اصلی این موضوع دو مورد هستند:
(۱) فازرها با وارد کردن ابزارگذاری (instrumentation) در برنامهها کار میکنند. کامپایلرهای مدرن یک نمایش میانی intermediate representation)) ارائه میدهند که امکان وارد کردن ابزارگذاری را همراه با ارائه جریان کنترل (control flow) و اطلاعات نوعدهی (typing information) فراهم میکند. این ویژگی، ابزارگذاری با کد منبع را نسبتاً ساده میکند. کامپایل یک فرآیند اتلافی (lossy) است و فقدان اطلاعات جریان کنترل در کد باینری، انجام ابزارگذاری ایستای باینری (static binary instrumentation) را دشوارتر میکند. فازرهای باینری (Binary fuzzers) [6]، [22]، [25]، [2]، [1]، [19] تا به امروز همگی به چارچوبهای ابزارگذاری پویا (dynamic instrumentation frameworks) مانند Pin [20] یا QEMU [10] متکی بودهاند که هزینه کارایی (performance penalty) را به دنبال دارند.
(۲) فازرها از روشهای ابتکاری (heuristics) برای کشف و ارزیابی ورودیهای جدید و جالب استفاده میکنند. موفقترین این روشها تا امروز، از جریان کنترل (control-flow) و اطلاعات نوعدهی (typing) موجود در کد منبع برنامه استفاده میکنند تا ورودیهای جدید را کشف و را ارزیابی کند. ابزارگذاریهای باینری موجود به اندازه کافی قادر به بازیابی اطلاعاتی که در فرآیند کامپایل از دست رفتهاند نیستند؛ بنابراین، تکنیکهایی که به این اطلاعات وابستهاند، قابل استفاده نخواهند بود.
در این کار، ما تکنیکهای جدیدی برای فازینگ باینری ارائه میدهیم که امکان استفاده از فازرهای مبتنی بر کد منبع را روی باینریهایی که کد منبع آنها در دسترس نیست فراهم میکند؛ بهطوری که تکنیکهای موجود بتوانند بهصورت کارآمد ورودیهای جالب و باگها را ردیابی و کشف کنند.
ابزار ما، RWFUZZ، فایلهای باینری را برای فازینگ با استفاده از تکنیکهایی که قبلاً محدود به فازرهای دارای دسترسی به کد در منبع بودند، ابزارگذاری میکند. RWFUZZ بر اساس کارهای منتشر شده اخیر در زمینه ابزارگذاری باینری (binary instrumentation) [9] ساخته شده است و رویکردهای اضافی مربوط به ابزارگذاری فازینگ را پیادهسازی میکند.
باینریهایی که توسط RWFUZZ ابزارگذاری شدهاند، میتوانند با ابزارهای موجود و با حداقل تغییرات فاز کرد. این رویکرد تفاوت قابل توجهی با کارهایی مانند Angora [11] و RedQueen [6] دارد؛ چرا که آنها به دنبال ارائه تکنیکهای جدید فازینگ هستند، در حالی که هدف ما این است که تکنیکهای اثبات شده را در زمینههای گستردهتری قابل استفاده کنیم.
کار ما در حوزه فازینگ باینری ایستا (static binary fuzzing) با سه چالش اصلی مواجه است:
(1) ابزارسازی (instrumentation) جریان کنترل با پوشش کامل (full coverage)، (2) ردگیری زمینه فراخوانی تابع (tracking function call context) بدون در نظر گرفتن فرضیات در مورد لایهبندی حافظه (memory layout) و (۳) مقایسههای ابزارگذاری (instrumenting comparisons) با دقتی معادل رویکردهای سطح منبع (source-level approaches). در پاسخ به این چالشها، RWFUZZ با دقتی در سطح منبع، باینریها را ابزارگذاری میکند و در عین حال، سربار (overhead) عملکرد را نسبت به رویکردهای پیشین کاهش میدهد.
ما RWFUZZ را در مقایسه با فازرهای سطح منبع و سطح باینری ارزیابی میکنیم. در مورد باگهایی که به صورت دستی در برنامههای C تزریق شدهاند، RWFUZZ تعداد باگهای بیشتری نسبت به سایر فازرهای مورد آزمایش کشف میکند؛ از جمله یک باگ وابسته به کامپایلر (compiler-dependent) که به همین دلیل توسط فازرهای مبتنی بر سورس قابل شناسایی نیست و از دست میرود.
RWFUZZ در مجموعه دادهLAVA-M [13]، با استفاده از ردگیری آلودگی (taint tracking) مبتنی بر Pintool در Angora و بدون دسترسی به کد منبع، باگهای مشابهی را شناسایی کرده است. همچنین در همین مجموعه، RWFUZZ نسبت به Angora حدود 3.4 برابر سربار عملکردی (overhead) ایجاد میکند که این مقدار کمتر از سایر رویکردهای فازینگ باینری مبتنی بر ابزارگذاری پویا (dynamic instrumentation) است.
در برخی موارد نیز، ابزارگذاری باینری RWFUZZ به طور متوسط باگهای بیشتری نسبت به نتایج منتشر شده Angora، صرفنظر از حالت اجرایی آن، پیدا میکند. ارزیابی ما نشان میدهد که RWFUZZ بدون نیاز به دسترسی به کد منبع، عملکردی قابل مقایسه با فازرهای سطح منبع در کشف باگها ارائه میدهد.
در جمعبندی، مشارکتهای اصلی این کار به شرح زیر است:
- ما روشهایی را برای باینریهای ابزارگذاری ایستا (static instrumentation) جهت فازینگ، با دقتی معادل سطح منبع توسعه دادهایم.
- ما تکنیکهایی را برای به حداقل رساندن سربار عملکرد (performance overhead) ابزارگذاری فازینگ باینری ارائه میدهیم.
- ما RWFUZZ را روی باگهای تولید شده به صورت دستی و خودکار در برنامهها ارزیابی کردهایم و نشان دادهایم که این ابزار با حداقل سربار، باگهایی همتراز با یک فازر مدرن با کد منبع در دسترس (source-available) را کشف میکند.
RWFUZZ امکان استفاده از پیشرفتهترین تکنیکهای فازینگ موجود را روی باینریها فراهم میکند. این قابلیت پیامدهای گستردهای برای تضمین امنیت نرمافزار (software assurance) دارد: جامعه دیگر محدود به بررسی امنیت نرمافزارهای متنباز نیست. پژوهشگران و تستکنندگان نرمافزار میتوانند از فازرهای باینری برای ارزیابی نرمافزارهای اختصاصی (proprietary) و قدیمی (legacy) استفاده کنند، بدون آنکه به کد منبع توسعهدهندگان نیاز داشته باشند.
علاوه بر این، فازینگ باینری پیامدهای خصمانه (adversarial) نیز دارد؛ زیرا دیگر پنهان نگه داشتن کد منبع مانع بهرهبرداری از آسیبپذیریها توسط فازرها (fuzzers) نمیشود. این موضوع، مفهوم «امنیت از طریق ابهام» (security through obscurity) را که اغلب مبنای انگیزه برای توسعه نرمافزارهای متنبسته (closed-source) است، به چالش میکشد.
۲. پیشزمینه (Background)
فازینگ (Fuzz Testing) یک رویکرد تخصصی برای تولید تصادفی موارد آزمون (testcase) است. اقدامات اولیه در زمینه تولید موارد آزمون تصادفی بر این فرض استوار بودند که ورودیهای برنامهها اغلب از یک قالب مشخص پیروی میکنند؛ قالبی که توسط یک دستور زبان مستقل از متن (Context-Free Grammar – CFG) تعریف میشود [16].
با داشتن مشخصات CFG ورودی یک برنامه، میتوان موارد آزمون را با دنبال کردن قواعد دستور زبان تولید کرد، تا زمانی که تنها ترمینالها (terminals) یعنی همان رشتههای نهایی (string literals) باقی بمانند. رشتههای تصادفی که با CFG یک برنامه سازگار باشند، به عنوان ورودیهای نیمه معتبر (semi-valid) شناخته میشوند و قادرند بخشهایی از برنامه را فراتر از مرحله اولیهی تجزیه (parsing stage) اجرا کنند و مورد آزمایش قرار دهند.
تولید موارد آزمون (Test Case) مبتنی بر CFG با دو محدودیت اساسی مواجه است: (۱) تعریف CFG نیازمند تحلیل دستی برنامهی مورد آزمایش است، و (۲) ورودیهایی که یک CFG را برای یک برنامه برآورده میکنند همچنان ممکن است به صورت بدیهی نامعتبر باشند، زیرا رفتار برنامههای معنادار (meaningful programs) معمولاً مستقل از متن (context-free) نیست.
فازینگ (Fuzz Testing) رویکرد متفاوتی را اتخاذ میکند که از محدودیتهای ذکر شده در بالا رنج نمیبرد. فازرها از یک حلقه فازینگ (fuzzing loop) استفاده میکنند؛ در این حلقه، یک ورودی معتبر برنامه (که توسط کاربر ارائه شده است) به صورت تکراری انتخاب شده و به طور تصادفی جهش (mutate) داده میشود (فاز میشود) تا یک ورودی جدید تولید گردد [14].
به صورت شهودی، تغییرات جزئی در ورودیهای معتبر برنامه باید ورودیهایی تولید کند که همچنان معتبر هستند، اما در عین حال ممکن است باعث شوند برنامه رفتار متفاوتی از خود نشان دهد. اگرچه فازینگ میتواند با بهبود روشهای جهش (mutation) ورودیها را بهبود بخشد، اما اصل اساسی این تکنیک، جهش تصادفی (random mutation) ورودیهای معتبر است [15].
فازرهای جعبه سیاه (black-box) بدون داشتن هیچ دانشی از برنامه، ورودیها را تغییر میدهند [21]. در مقابل، رویکردهای جدید با استفاده از دسترسی بیشتر به برنامه، ورودیها را به شکل مؤثرتری جهش میدهند. این فازرهای جعبه خاکستری (grey-box) برنامه را ابزارگذاری (instrument) میکنند تا اجرای آن را پایش کرده و تعیین کنند که کدام جهشها رفتارهای جدیدی را فعال میکنند.
ما فازرهای جعبه خاکستری (grey-box) را به دو دسته (۱) فازرهای مبتنی بر کد منبع در دسترس (source-available fuzzers)، یعنی آنهایی که نیازمند دسترسی به کد منبع هستند، و (۲) فازرهای باینری (binary fuzzers)، یعنی آنهایی که از ابزارگذاری باینری استفاده میکنند، تقسیم میکنیم.
۲.۱ فازینگ با کد منبع در دسترس (Source-available fuzzing)
زمانی که کد منبع کد برنامه در دسترس باشد، یک فازر جعبه خاکستری (grey-box fuzzer) میتواند از اطلاعات غنی موجود در منبع کد برای ابزارگذاری برنامه جهت فازینگ بهره گیرد. این ابزارگذاری ممکن است شامل اندازهگیری پوشش کد (code coverage)، ردگیری جریان اطلاعات (track information flow) و ثبت مقادیر متغیرها برای استفاده توسط فازر باشد.
در این کار، ما عمدتاً تکنیکهای فازینگ در حالت دسترسی به کد منبع را همانطور که در Angora پیادهسازی شدهاند، بررسی میکنیم. اگرچه فازرهای مدرنی مانند REDQUEEN نیز عملکرد رقابتی دارند، رویکرد انعطافپذیر Angora آن را به نمایندهای شاخص از تکنیکهای پیشرفته (state-of-the-art) در این حوزه تبدیل کرده است.
فازرهایی که به کد منبع دسترسی دارند، برنامهها را در زمان کامپایل (compile-time) تحلیل و ابزارگذاری میکنند و از اطلاعات سطحبالای (high-level) موجود در کد منبع بهره میبرند. این موضوع به آنها اجازه میدهد اطلاعات مفیدی درباره رفتار برنامه استخراج کنند که در باینریهای کامپایل شده به سادگی قابل مشاهده نیست. از تکنیکهای برجسته در فازینگ مبتنی بر دسترسی به کد منبع میتوان به موارد زیر اشاره کرد:
(۱) ابزارگذاری پوشش کد (Code coverage instrumentation). پوشش جریان کنترل (control flow coverage) در هر اجرای برنامه ثبت میشود. چنانچه دو ورودی، پوشش مشابهی ایجاد کنند، فقط یکی از آنها نگه داشته میشود. این کار عملاً یک مجموعه حداقلی (minimal set) از ورودیها را شکل میدهد که تمام رفتارهای کشف شده را تحریک میکنند.
(۲) ردگیری آلوگی (Taint tracking). جریان اطلاعات هر بایت ورودی ردگیری میشود. این کار به فازر اجازه میدهد فقط آن دسته از بایتهای ورودی را تغییر دهد که مستقیماً روی سایر ابزارگذاریها تأثیر میگذارند. این ابزارگذاری میتواند در زمان کامپایل (به عنوان مثال با استفاده از LLVM DataFlowSanitizer) و یا توسط ابزارگذاریهای باینری پویا مانند Intel Pin انجام شود.
(۳) ابزارگذاری مقایسه (Comparison instrumentation). جریان کنترل یک برنامه نتیجه دستورالعملهای جریان کنترل آن است. در نتیجه، فازرها میتوانند به سادگی با جهش ورودیها بهگونهای که بر یک دستور شرطی (conditional instruction) خاص تأثیر بگذارند، ورودیهای جدید را کشف کنند. LAF-Intel و Angora ورودیها و نتایج هر مقایسه را ثبت میکنند، هرچند روشهای متفاوتی برای جهش ورودیها بر اساس این مقادیر به کار میبرند. بر اساس این ابزارگذاری، بسیاری از فازرهای سطح کد منبع در نحوه ساخت ورودیهای جدید از اطلاعات جمعآوری شده با یکدیگر تفاوت دارند.
LAF-Intel بین ابزارگذاری مقایسه و پوشش کد ارتباط ایجاد میکند و زمانی که یک ورودی بخشی از یک شرط (conditional) را تا حدی حل (solve) کند، پوشش آن ورودی را به صورت مصنوعی افزایش میدهد. این کار از طریق شکستن هر مقایسه به چندین مقایسه تودرتو (nested comparisons) انجام میشود که هرکدام احتمال بیشتری برای حل شدن با جهش تصادفی (random mutation) دارند.
Angora از گرادیان نزولی (gradient descent)، به عنوان یک تکنیک بهینهسازی، برای حل مستقیم شروط استفاده میکند. مقادیر مشخصی که در مقایسهها استفاده میشوند به همراه آفستهایی (offset – انحرافهایی) از ورودی که در ایجاد آن شرط نقش دارند، ثبت میشوند. این رویکرد نیازمند ابزارگذاری هر شرط و همچنین ردگیری آلودگی (taint tracking) است تا مشخص شود کدام بایتهای ورودی بر اجرای برنامه تأثیر میگذارند.
۲.۲ ابزارگذاری فازینگ باینری (Binary Fuzzing Instrumentation)
زمانی که تنها کد باینری در دسترس باشد، ابزارگذاری برای فازینگ جعبه خاکستری (grey-box fuzzing) دشوارتر میشود، زیرا اطلاعات سطح کد منبع در فرآیند کامپایل حذف میشوند. این موضوع زمانی پیچیدهتر میشود که باینریها و یا نمادهای دیباگ (debug symbols) حذف شده باشد. فازرهای باینری تلاش میکنند بدون دسترسی به کد منبع، تکنیکهای فازرهای مبتنی بر کد منبع را تقریب بزنند (approximate). به طور کلی، برای پشتیبانی از فازینگ در سطح باینری میتوان از دو نوع ابزارگذاری استفاده کرد: ابزارگذاری پویا (dynamic instrumentation) و ابزارگذاری ایستا (static instrumentation).
(۱) ابزارگذاری بصورت پویا (Instrument dynamically). فازرهای باینری موجود [6]، [22]، [25]، [2]، [1]، [19] از یک چارچوب ابزارگذاری باینری مانند Pin [20]، QEMU [10] یا DynInst [1] استفاده میکنند. ابزارهای ابزارگذاری پویا دارای سربار عملکردی (performance overhead) ذاتی میباشند، زیرا باید اجرای برنامه را تفسیر کرده و نقاط ابزارگذاری (instrumentation points) را در زمان اجرا (runtime) تعیین کنند. این سربار را میتوان با استفاده از کامپایل به موقع (just-in-time compilation) کاهش داد، اما به طور کامل نمیتوان حذف کرد.
(۲) افزودن ابزارگذاری به صورت ایستا (Static instrumentation). یک برنامه میتواند به صورت ایستا بازنویسی (statically rewritten) شود تا ابزارگذاری فازینگ مستقیماً داخل فایل اجرایی (executable) قرار گیرد. این امر مستلزم تحلیل ایستا (static analysis) برای استخراج اطلاعات مربوط به دستورالعملها (instruction) و جریان کنترل (control-flow) است. در حالی که ابزارگذاری ایستا از سربار زمان اجرا (runtime overhead) که ابزارگذاری پویا تولید میکند، جلوگیری به عمل میآورد، اما انجام آن دشوار است؛ چرا که اطلاعات جریان کنترل و نوع دهی (typing information) در فرآیند کامپایل از بین میرود و بهطور کلی بدون اجرای برنامه قابل استنتاج دقیق نیست. RWFUZZ تکنیکهایی را برای ابزارگذاری ایستای فازینگ باینری ارائه میدهد. دینش (Dinesh) و همکاران [12] او، کاربردهای ابزارگذاری ایستا در فازینگ را بررسی کردهاند. تمرکز آنها بر اطمینان از صحت (soundness) و کارایی (performance) در فرآیند بازنویسی است. در مقابل، کار ما چالشهایی را بررسی میکند که به طور خاص به اعمال تکنیکهای فازینگ سطح کد منبع روی باینریها مربوط میشود.
برخی فازرهای باینری بر بازتولید تکنیکهای سطح کد منبع با استفاده از تقریب (approximation) تمرکز دارند. AFL-DynInst به صورت پویا برنامه را برای AFL ابزارگذاری میکند و Steelix نیز تکنیکهایی مشابه LAF-Intel را به صورت پویا پیادهسازی میکند. اینگونه کارها عموماً تلاش میکنند در هر اجرا همان کرشها (crashes) را نسبت به همتایان مبتنی بر کد منبع خود کشف کنند، در حالی که سربار زمانی اجرا (execution time overhead) را به حداقل میرسانند. در مواردی که تکنیکها از ابزارسازی سطح کد منبع بهرهای نمیبرند، فازرها مستقیماً تکنیکهای خود را روی باینریها نمایش میدهند. REDQUEEN و VUzzer به طور مستقیم با استفاده از ابزارگذاری پویا پیادهسازی شدهاند.
۲.۳ ابزارگذاری باینری ایستا (Static Binary Instrumentation)
کار ما در فازینگ باینری بر پایه پیشرفتهای موجود در حوزه ابزارگذاری باینری ایستا (static binary instrumentation) استوار است؛ حوزهای که با چندین چالش مهم همراه میباشد. باینریها شامل هر دو نوع داده (data) و کد (code) هستند. یکی از بزرگترین چالشهای ابزارگذاری ایستا، تعیین معنای این بایتها بدون اجرای برنامه است؛ به ویژه زمانی که باینریها از اطلاعات دیباگ (debug information) محروم شده باشند (stripped).
یک بازنویس (rewriter) باید بتواند دیساسمبلی صحیح (correct disassembly) از فایل اجرایی به دست آورد و دستورها را به گونهای تغییر دهد که عملکرد اصلی برنامه دچار اختلال نشود. چارچوبهای مختلف با درجات موفقیت متفاوتی به این مسئله پرداختهاند [28]، [29]، [27].
تعیین آفستهای اجرایی معتبر (valid executable offsets) در یک برنامه برای استخراج کدِ واقعا اجرا شده ضروری است. در حالی که این کار در معماریهایی که از دستورالعملهای همتراز با کلمه (word-aligned instructions) استفاده میکنند ساده است، در مجموعه دستورالعملهای با طول متغیر مانند x86 [7] دشوارتر میشود. شکل ۱، یک تابع نمونه و فایل باینری حاصل از آن را به همراه دستورالعملهایی که در فایل باینری وجود دارند اما در برنامه اصلی نیستند، نشان میدهد.
اغلب کامپایلرها دستورالعملها را بر اساس مرزهای توابع (function boundaries) همتراز میکنند، و UROBOROS از این ویژگی برای استخراج دستورالعملهای معتبر استفاده میکند [28]، [29]. نویسندگان اشاره میکنند که کارایی ابزار آنها با بهبود تشخیص مرز توابع افزایش مییابد.
با این حال، حتی روشهای پیشرفته برای استخراج مرز توابع نیز در شرایط خصمانه (adversarial conditions) مانند پنهانسازی همترازی توابع (disguised function alignment) نمیتوانند مرزها را به صورت دقیق شناسایی کنند [23[، [8]. فازرهای باینری ذاتاً ابزارهای خصمانهای هستند و باید در برابر چنین تکنیکهایی مقاوم باشند. از این رو، استفاده از ابزار بازنویسی (rewriting tool) که به مرزهای توابع وابسته است، عملی و قابل اتکا نیست.
ابزارهای تحلیل باینری اخیر بر تعیین آفستهای دستورالعملها (instruction offsets) بدون اتکا به شناسایی توابع (function identification) تمرکز بیشتری داشتهاند [26]. هرچند همانطور که وارتل و همکارانش اشاره کردند [30]، دیساسمبلی ایستای ایمن و صحیح (sound static disassembly) در حالت کلی تصمیمناپذیر (undecidable) است، این تکنیکها در عمل میتوانند اکثر برنامهها را دیساسمبل کنند.
در نتیجه، بازنویسهایی مانند RAMBLR توسعه داده شدهاند که متکی بر تکنیکهای جدید هستند. RAMBLR فرضهای کمتری درباره محل آفست دستورالعملها دارد، بنابراین روی برنامههای بیشتری بهدرستی عمل میکند؛ با این حال، کاملاً در برابر مبهمسازی (obfuscation) مقاوم نیست و برای تعیین محل دقیق دستورالعملها به روشهای ابتکاری (heuristics) متکی است.
یک رویکرد جایگزین توسط باومن و همکارانش [9] پیشنهاد شده است. آنها استدلال میکنند که بهجای اتکا به روشهای ابتکاری (heuristics) برای تعیین اینکه کدام دستورالعملها دیساسمبل (disassemble) شدهاند، میتوان تمام دستورالعملهای معتبر موجود در باینری را دیساسمبل کرد. این مجموعه یک اَبَرمجموعه (superset) از کد مفید واقعی در فایل اجرایی است، که همین موضوع انگیزه نامگذاری آن به دیساسمبلی اَبَرمجموعه (Superset Disassembly) را فراهم کرده است.
باومن و همکارانش این مفهوم را در MULTIVERSE پیادهسازی کردهاند، که بدون استفاده از روشهای ابتکاری اقدام به بازنویسی باینریها میکند. اگرچه باینریهای بازنویسی شده به این روش، دارای سربار نسبتاً بالای اندازه (size overhead) هستند، اما سربار زمان اجرایی(execution time overhead) کمی ایجاد میکنند و در عین حال تضمین میکنند که تمام مسیرهای اجرایی ممکن ابزارگذاری شدهاند. RWFUZZ بر پایه MULTIVERSE ساخته شده است و با تحلیل هر دستورالعمل به صورت مستقل، ابزارگذاری فازینگ را درون آن تزریق میکند.
۳. بازنویسی باینری برای فازینگ (Binary Rewriting for Fuzzing)
ما ابزارگذاری باینری ایستا (static binary instrumentation) برای فازینگ را از طریق بازنویسی باینری (binary rewriting) بررسی میکنیم. در حالی که دستیابی به ابزارگذاری ایستا (static instrumentation) نسبت به ابزارگذاری پویا (dynamic instrumentation) دشوارتر است، نشان میدهیم که میتوان از ابزارگذاری ایستا برای پیادهسازی تکنیکهای فازینگ استفاده کرد که پیشتر تنها در سطح کد منبع در دسترس بودند.
ابزار ما، RWFUZZ، با یک بازنویسی باینری موجود یعنی MULTIVERSE یکپارچه میشود تا باینریهای حذف شده را با قرار دادن تقریبهایی از ابزارگذاری زمان کامپایل (compile-time instrumentation) در سطح باینری برای فازینگ ابزارگذاری کند. برنامه ابزارگذاری شده شامل باینری اصلی به همراه ابزارگذاری درونخطی (inline instrumentation) و همچنین یک زمان اجرا (runtime) ابزارگذاری است که از یک فازر موجود گرفته شده است. شکل ۲ این یکپارچگی را نشان میدهد که شامل چندین مرحله است:
(۱) زمان اجرا (runtime) ارائه شده توسط فازر با استفاده از LLVM کامپایل میشود [18]. این زمان اجرا (runtime) به زبانهای سطحبالا مانند C یا Rust نوشته شده است و در زمان کامپایل (compile-time) به برنامه هدف متصل میشود. در مقابل، RWFUZZ یک باینری باینری با پیوند ایستا (static-linked) تولید میکند. این مرحله اجازه میدهد زمان اجرای فازرهای موجود با حداقل تغییرات مورد استفاده قرار گیرد.
(۲) نمادهایی (symbols) که توسط زمان اجرا (runtime) صادر (export) شدهاند استخراج میشوند. این نمادها سپس برای ابزارگذاری درونخطی (inline instrumentation) در دسترس قرار میگیرند.
(۳) هر دستورالعمل ممکن در فایل ELF هدف توسط RWFUZZ به عنوان یک کاندید برای ابزارگذاری پردازش میشود. RWFUZZ برای دستورالعملهای جریان کنترل (control flow) و مقایسهها (comparisons)، کد اسمبلی ابزارگذاری تولید میکند و همچنین فراخوانیهایی به توابع موجود در زمان اجرا (runtime) اضافه میکند تا اطلاعات مربوط به این دستورالعملها ثبت شود.
(۴) بازنویس (rewriter)، یک بخش متنی (text segment) جدید حاوی دستورالعملهای اصلی و ابزارگذاری فازینگ درونخطی (inline fuzzing instrumentation) را مجدد تولید و اسمبل (reassembles) میکند.
(۵) زمان اجرای (runtime) فازینگ و بخشهای متن بازنویسی شده در یک فایل ELF جدید همراه با بخشهای اصلی (original sections) از باینری هدف قرار داده میشوند. متن بازنویسی شده شامل ارجاعات ایستای معتبر (valid static references) به زمان اجرای (runtime) ابزارگذاری شده است که به ابزارگذاری درونخطی در بخش «.newtext» اجازه میدهد به قابلیتهایی دسترسی داشته باشد که در یک زبان سطحبالا پیادهسازی شدهاند.
(۶) باینری ELF نهاییِ ابزارگذاری شده به یک فازر (fuzzer) منتقل میشود و فازر آن را طوری فاز میکند که گویا از قبل در زمان کامپایل (compile) ابزارگذاری شده است.
بازنویسی باینری (binary rewriting) امکان ابزارگذاری برنامهها برای فازینگ را فراهم میکند، در حالی که عملکرد (performance) و سازگاری با فازرهای موجود حفظ میشود. از آنجا که برنامهی حاصل نیازمند حداقل تغییر در خود فازر است، بهبودهای آینده در خود فازر میتوانند به طور خودکار روی برنامههای ابزارگذاری شده توسط RWFUZZ اعمال شوند. توانایی RWFUZZ در ابزارگذاری ایستای باینریها، یک مزیت کلیدی نسبت به تکنیکهای موجود در فازینگ باینری محسوب میشود.
۳.۱ چالشهای ابزارگذاری ایستا (Challenges to Static Instrumentation)
کامپایل، به منظور اجرای کارآمد برنامهها، آنها را به شکل فشردهای (compact) تولید میکند. همین موضوع، ابزارگذاری را دشوار میکند، چرا که اطلاعات مربوط به جریان داده (data flow) و جریان کنترل (control flow) از بین میرود. به منظور ابزارگذاری ایستای برنامهها جهت فازینگ، RWFUZZ بایستی اطلاعات سطح کد منبع را مهندسی معکوس (reverse-engineer) کرده، استنتاج (infer) کند و یا تقریب (approximate) بزند. در این حوزه، چندین چالش وجود دارد که به طور خاص مربوط به فازینگ میباشند:
(۱) اندازهگیری پوشش (Coverage Measurement). فازرهایی که کد منبع آنها در دسترس (source-available) میباشند، در زمان کامپایل، یالهای جریان کنترل (control flow edges) را ابزارگذاری میکنند و امکان اندازهگیری پوشش کد (code coverage) را فراهم میسازند. به طور مشابه، فازرهای باینری موجود این اطلاعات را به صورت پویا (dynamic) ثبت میکنند. قرار دادن این اطلاعات به صورت ایستا در باینریها ساده نیست، زیرا مکان بلوکهای پایه (basic blocks) در برنامه را نمیتوان بهصورت دقیق استنتاج کرد. RWFUZZ ابزارسازی پوشش را بهگونهای پیادهسازی میکند که دقت (precision) را با حداقل سربار زمان اجرا (runtime overhead) حفظ میکند.
(۲) حساسیت به زمینه (Context Sensitivity). کارهای اخیر [11] نشان دادهاند که صرفاً ردیابی یالهای جریان کنترل (control-flow edges) برای پوشش کامل رفتار یک برنامه کافی نیست؛ بنابراین علاوه بر آن، زمینه (context) این یالها نیز بر اساس پشتهی فراخوانی توابع (function call stack) ردیابی میشود. کامپایلر اطلاعات مربوط به فراخوانی توابع را در اختیار دارد، بنابراین افزودن این قابلیت در حالت دسترسی به کد منبع ساده است. اما در ابزارگذاری باینری، دسترسی دقیق به این اطلاعات ممکن نیست؛ در نتیجه RWFUZZ از یک پشته سایه (shadow stack) برای ردیابی زمینه استفاده میکند.
(۳) استنتاج مقایسهها (Inferring Comparisons). فازرهای سطح کد منبع، ورودیها و نتایج مقایسهها را ثبت میکنند که این اطلاعات به هدایت استراتژیهای جهش (mutation) کمک میکند. همچنین اطلاعات مربوط به مقایسهها در اختیار ابزارگذاری کامپایلر قرار میگیرد. در سطح باینری، این مقایسهها به طور کلی به صورت ایستا (static) قابل استنتاج دقیق نیستند. RWFUZZ با بازتابدادن (mirroring) وضعیت پردازنده (processor state)، مقایسهها را مؤثرتر از راهکارهای پویای موجود ردیابی میکند و دقتی قابلمقایسه با ابزارگذاری سطح کد منبع ارائه میدهد.
۳.۲ رویکرد (Approach)
(۱) اندازهگیری پوشش (Coverage Measurement): فازرها (Fuzzers) با یافتن رفتارهای جدید و جالب در برنامه کار میکنند. از آنجا که رفتارهای جدید برنامه معمولاً ناشی از تفاوت در جریان کنترل (Control Flow) هستند، اندازهگیری پوشش جریان کنترل برای فازرهای مدرن ضروری است [31]، [11[، [6]. برای هر اجرای برنامه، مجموعهی یالهای (edges) جریان کنترل که طی شدهاند ثبت میشود و فازر، ورودیهایی را نگه میدارد که بیشترین مجموعه از این یالهای جریان کنترل را فعال کنند. در هنگام ابزارگذاری (instrumentation) در سطح کد منبع، یالهای جریان کنترل مستقیم در مرحلهی کامپایل به راحتی قابل شناسایی هستند.
در هنگام ابزارگذاری ایستا (static instrumentation) روی باینریها برای تحلیل جریان کنترل، این روش قابل استفاده نیست؛ زیرا نمیتوان بلوکهای پایه (basic blocks) یا اهداف جریان کنترل یک برنامه را به صورت صحیح و قابل اتکا استنتاج کرد.
RWFUZZ تلاش میکند با وجود این محدودیت اطمینان حاصل کند که تمام یالهای جریان کنترل (control flow edge) مورد ابزارگذاری قرار میگیرند. نکتهی کلیدی این است که اگر ابزارگذاری یک یال جریان کنترل از دست برود، ممکن است ورودیهای جالب و مهم از نظر فازر نادیده گرفته شوند؛ اما در مقابل، اعمال ابزارگذاری بیش از حد صرفاً باعث افزایش سربار زمان اجرای برنامه (execution time overhead) میشود.
حال مجموعهی یالهای جریان کنترلی را که توسط فازر ردگیری میشوند در نظر بگیرید. اگر یک یال جریان کنترل در این مجموعه تکرار شود (یعنی به صورت دو یال متفاوت شمارش شود)، این امر بر ورودیهایی که نگه داشته میشوند تأثیری نخواهد گذاشت.
در نتیجهی این بینش، ما به صورت راهبردی تمام دستورات ممکنِ جریان کنترل را بیشازحد ابزارگذاری (over-instrument) میکنیم. RWFUZZ ابزارگذاری مربوط به پوشش (coverage instrumentation) را قبل و بعد از هر دستور جریان کنترل وارد میکند، بدون اینکه مرزهای بلوکهای پایه (basic block boundaries) را مشخص یا استخراج کند.
در شکل ۳، نقاط ابزارگذاری در اطراف هر دستور جریان کنترل در موقعیتهای ۱ تا ۶ قرار داده شدهاند. یالهای جریان کنترل به صورت زوجهایی از این نقاط ثبت میشوند (به عنوان مثال، در حالتی که که a = 0 باشد، بصورت (1، 5)).
RWFUZZ با ابزارگذاری بیشازحد (over-instrumentation) تضمین میکند که تمام یالهای جریان کنترل (control flow edges) مورد ابزارگذاری قرار میگیرند. با این حال، این کار سربار زمانی اجرای برنامه را افزایش میدهد. این یالهای تکراری باید توسط فازر ثبت و پردازش شوند، که باعث میشود مدت زمان هر اجرا، بیشتر گردد.
برای مثال، در شکل ۳، یال (2, 3) یک یال جریان کنترل تکراری محسوب میشود، زیرا هر دو نقطهی آن درون یک بلوک پایهی واحد قرار دارند. از آنجا که یالهای جریان کنترل در یک بیتمپ (bitmap) با اندازه محدود ذخیره میشوند، یالهای تکراری ممکن است یک خانه (slot) را اشغال کنند که میتوانست برای یک یال واقعی دیگر استفاده شود؛ در نتیجه، فازر ممکن است قادر به ردیابی برخی یالهای واقعی دیگر نباشد.
این موضوع یک تعادل (trade-off) قابلقبول در ازای امکان فازینگ باینریها محسوب میشود، و روشهایی برای کاهش سربار ناشی از ردیابی پوشش نیز در کارهای پیشین مورد بررسی قرار گرفتهاند [31]، [3].
الف) پرشهای غیرمستقیم (Indirect Jumps): در برخی موارد، جریان کنترل از طریق پرشهای غیرمستقیم انجام میشود که نمیتوان آنها را به صورت ایستا (statically) استنتاج کرد. برای مثال، دستور switch در زبان C اغلب به یک جدول (table) کامپایل میشود که توسط یک پرش غیرمستقیم مورد استفاده قرار میگیرد. سایر اشکال غیرمستقیمسازی (مانند اشارهگرهای تابع / function pointers) نیز حتی در صورت دسترسی به کد منبع، ممکن است بهصورت صحتمند قابل تحلیل نباشند. به منظور پوشش این موارد، RWFUZZ مقاصد پرشهای غیرمستقیم را در زمان اجرا (runtime) تعیین کرده و یالهای مناسب جریان کنترل را ثبت میکند. این کار تضمین میکند که تمام جریانهای کنترل ردیابی شوند، اما در صورتی که پرشهای غیرمستقیم به طور گسترده استفاده شوند، ممکن است باعث پر شدن ساختارهای دادهای (data structures) شود.
(۲) حساسیت به زمینه (Context Sensitivity): پوشش یالهای جریان کنترل به تنهایی نمیتواند رفتار کامل برنامه را توصیف کند. در یک مثال (شکل 5C)، ممکن است یک فازر به پوشش کامل یالهای جریان کنترل دست یابد، اما همچنان موفق به کشف باگ نگردد. به منظور کاهش این مشکل، فازرها پوشش یالهای جریان کنترل را بهصورت حساس به زمینه (context-sensitive) گسترش دادهاند؛ به این صورت که زمینهی فراخوانی (call context) هر یال جریان کنترل نیز ثبت میشود. هنگام ابزارگذاری در سطح کد منبع، این زمینهی فراخوانی همراه با سایر اطلاعات محلی (Local) روی پشته (stack) ردیابی میشود [11]. در حالی که افزودن اطلاعات جدید به پشتهی برنامه در زمان کامپایل امکانپذیر است، انجام این کار روی باینری بدون تغییر در چیدمان پشته (stack layout) و تأثیرگذاری بر برنامهی اصلی امکانپذیر نیست.
برخلاف فازرهای حساس به زمینهی موجود، RWFUZZ برای ردیابی زمینهی فراخوانی توابع از یک پشتهی سایه(shadow stack) استفاده میکند. هر فراخوانی (call) و بازگشت (return) به صورت ابزارگذاری شده تغییراتی در این پشته ایجاد میکند و زمینهی فعلی نیز همراه با یالهای جریان کنترل به فازر گزارش میشود.
این رویکرد برای برنامههایی که از دستورالعملهای x86-64 مانند call و ret به صورت استاندارد استفاده میکنند، به خوبی میتواند زمینهی فراخوانی را ردگیری کند. با این حال، زمانی که این دستورات برای اهداف غیرمرتبط با فراخوانی/بازگشت استفاده شوند یا اصلاً برای این منظور به کار نمیروند، ممکن است دقت این روش کاهش یابد.
آدرسهای توابعی که در پشتهی سایه (shadow stack) ذخیره میشوند برای محاسبهی زمینهی فراخوانی استفاده میشوند؛ به این صورت که با استفاده از هش زنجیرهای (chained hashing) روی تمام فراخوانیهای موجود در پشته، یک مقدار زمینهی فراخوانی تولید میشود. سپس این مقدار در اطلاعات پوشش (coverage) ادغام میشود. به این ترتیب (برای مثال در شکل 5C)، فراخوانی یک تابع در زمینههای متفاوت، برای اهداف پوششدهی (coverage purposes) به صورت جداگانه در نظر گرفته میشود.
(۳) استنتاج مقایسهها (Inferring Comparisons): پیشرفتهای اخیر در فازینگ مبتنی بر کد منبع [3], [11] و فازینگ باینری [22], [19], [6] به ابزارگذاری (instrumentation) متکی هستند تا مقادیر ورودی، نتایج هر مقایسه، و همچنین نوع مقایسهی در حال انجام را ثبت کنند (جدول ۱). این اطلاعات میتواند برای نگهداشتن که تا حدی شرایط را برآورده میکنند [3[، [19]، شناسایی مقادیر بایت جادویی (magic byte) [22] یا استنتاج روابط بین ورودی و وضعیت برنامه (program state) [6] مورد استفاده قرار گیرد.
فازرهای سطح منبع که مقایسهها را ابزارگذاری میکنند، معمولاً این کار را در زمان کامپایل انجام میدهند؛ در این حالت، اطلاعات کامل مربوط به یک مقایسه در یک نقطهی واحد در دسترس است. اما ابزارگذاری باینری پویا (dynamic binary instrumentation) پیچیدهتر است، زیرا مقایسهها در معماری x86 در قالب دو دستور جدا از هم انجام میشوند (شکل ۴). در ابتدا دستور cmp (مقایسه) اجرا میشود که نتیجهی آن را در ثَبات (register) FLAGS قرار میدهد و این ثَبات را با تمامی نتایج ممکن مقایسه پر میکند. سپس در مرحله بعد، یک دستور شرطی مانند پرش شرطی (conditional jump) یا انتقال شرطی (conditional move) اجرا میشود. این دستور از نتایج آخرین دستور اجرا شده که ثَبات FLAGS را مقداردهی کرده استفاده میکند.
روشهای موجود برای ابزارگذاری مقایسه در باینریها معمولاً تنها دستور cmp را در نظر میگیرند، که این موضوع دو ضعف اصلی دارد: (1) در حالی که بسیاری از دستورالعملها میتوانند ثبات FLAGS را مقداردهی کنند، ممکن است همیشه این نتایج مورد استفاده قرار نگیرند (مثبتهای کاذب /false positives)، و (2) نوع واقعی مقایسهی انجام شده توسط دستور شرطی تعیین میشود، اما اطلاعات مربوط به این دستور در نظر گرفته و جمعآوری نمیشود.
از آنجا که کارهای موجود در فازینگ باینری عمدتاً مقایسههای برابری (equality comparisons) را در نظر میگیرند، نیاز چندانی به دانستن نوع دقیق مقایسهی در حال انجام وجود ندارد. علاوه بر این، ابزارهای موجود برای کاهش سربار (overhead)، تنها دستوراتی را ابزارگذاری میکنند که معمولاً برای جریان کنترل استفاده میشوند [22].
با این حال، RWFUZZ قصد دارد مقایسهها را برای کاربردهای عمومی فازینگ ابزارگذاری کند؛ از جمله تکنیکهایی که پیشتر در سطح منبع استفاده میشدند، مانند گرادیان نزولی (gradient descent). این روشها از نظر محاسباتی بسیار پرهزینه هستند و اجرای آنها روی مقایسههای نامرتبط (irrelevant comparisons) عملی نیست و در نتیجه نیازمند دانستن نوع دقیق مقایسهی انجام شده هستند.
راهحل ما رفتار CPU در محاسبهی ثَبات FLAGS را شبیهسازی میکند. زمانی که یک دستور مقایسهای (compare instruction) مقداردهی به ثبات FLAGS را انجام میدهد، مقادیر ورودیِ مقایسه در حافظه ذخیره میشوند. اگر یک دستور شرطی به FLAGS دسترسی پیدا کند، این مقادیر به همراه نوع مقایسهی انجام شده (بر اساس دستور اجرایی) به فازر گزارش داده میشوند.
سپس این اطلاعات توسط فازر برای انجام جهشهای هدفمند (targeted mutations) روی ورودیها استفاده میشود. در مورد Angora، راهحلها با استفاده از گرادیان نزولی (gradient descent) شناسایی میشوند. این رویکرد باعث کاهش مقایسههای مثبت کاذب (false positives) گزارش شده به فازر شده و در مقایسه با روشهای موجود، اطلاعات کاملتری ارائه میدهد. از آنجا که میزان مثبت کاذب (false positives)کاهش مییابد، میتوانیم بهصورت حریصانه (greedy) تمام دستوراتی را که ثَبات FLAGS را مقداردهی میکنند ابزارگذاری کنیم، بدون آن که سربار عملکرد قابل توجهی ایجاد شود.
۴. پیادهسازی (Implementation)
RWFUZZ به صورت یک نسخهی اصلاح شده از بازنویس باینری MULTIVERSE پیادهسازی شده است. بهطور پیشفرض، RWFUZZ یک ابزارگذاری را تولید میکند که با فازر Angora سازگار است، هرچند ابزارگذاریهای سازگار با AFL و LAF-Intel نیز مورد آزمایش قرار گرفتهاند. RWFUZZ قابلیتهای ابزارگذاری MULTIVERSE را با پشتیبانی از یک زمان اجرای ابزارگذاری شده (instrumentation runtime) که با یک زبان سطح بالا نوشته شده است، بهبود میبخشد. علاوه بر این، ملاحظات مربوط به عملکرد و کارایی (performance) بر نحوهی پیادهسازی ابزارگذاری فازینگ تأثیر گذاشتهاند.
۴.۱ زمان اجرای ابزارگذاری (Instrumentation Runtime)
فازینگ نیازمند یک زمان اجرای (runtime) کامپایل شده است که در حین اجرای برنامه فراخوانی میشود. این زمان اجرا (runtime) با حلقهی فازینگ (fuzzing loop) در ارتباط است؛ حلقهای که به طور مکرر برنامهی تحت آزمون را با ورودیهای مختلف اجرا میکند.
در صورتی که تنها اطلاعات پایهی پوشش (coverage) جمعآوری شود، این زمان اجرا میتواند ساده باشد. اما اندازهگیری و ردیابی شرطها (conditionals) مسئلهای پیچیدهتر است. در ابزارهای ابزارگذاری سطح منبع (Source-level instrumentation tooling)، این زمان اجرا هنگام کامپایل باینری در فرآیند لینککردن (linking) ادغام میشود. با این حال، هنگام ابزارگذاری یک باینری موجود، ادغام این زمان اجرا (runtime) به سادگی امکانپذیر نیست، زیرا رویههای استاندارد لینککردن برای باینریهایی که از قبل لینک شدهاند طراحی نشدهاند.
ما یک چارچوب سبک (lightweight framework) توسعه دادیم که امکان درج یک زمان اجرای ابزارگذاری شده (instrumentation runtime) را در یک باینری بازنویسی شده، در کنار کد ابزارگذاری شده فراهم میکند. سپس نمادهای (symbols) مربوط به این زمان اجرا (runtime) در اختیار هوکهای (hook) ابزارگذاری قرار میگیرند؛ این هوکها، مونتاژ شده و بهصورت درون خطی (inline) در کنار برنامهی اصلی درج میگردند. فرآیند تولید ابزارگذاری در دو مرحله انجام میشود:
- کامپایل کردن (Compilation). ابزارگذاری که نیازی به درج درون خطی (inline) ندارد، کامپایل میشود. سپس فایل اجرایی که قرار است بازنویسی شود تحلیل شده و یک ناحیهی آزاد در حافظهی مجازی (virtual memory) شناسایی میگردد. در ادامه، کد ابزارگذاری و کامپایل شده به صورت کد اجرایی غیرقابل جابهجایی(non-relocatable executable code) در باینری خروجی لینک میشود. از آنجا که ویرایش اطلاعات کتابخانههای پویا (dynamic library information) در برنامهی در حال فاز شدن به صورت قابل اعتماد ممکن نیست، این ابزارگذاری نمیتواند به هیچ کتابخانهی پویا وابسته باشد. به همین دلیل، کدهای C و Rust به صورت ایستا (statically) با استفاده از musl کامپایل میشوند؛ که یک پیادهسازی ایستا از libc است.
- بازنویسی (Rewriting). هوکهای ابزارگذاری (instrumentation hooks) که به صورت درون خطی (inline) در برنامهی در حال فاز شدن قرار میگیرند، قبل و بعد از دستورات مرتبط (یعنی دستورهایی که بر جریان کنترل اثر میگذارند) درج میشوند. این هوکها قطعات کوچک اسمبلی هستند که در حین بازنویسی برنامهی در حال فاز شدن، با استفاده از Keystone [4] اسمبل (assemble) میشوند. پیش از انجام بازنویسی، نمادها (symbols) از زمان اجرای (runtime) ابزارگذاری استخراج میشوند؛ این کار امکان لینک شدن این قطعات کوچک به کتابخانهی بزرگتر ابزارگذاری را فراهم میکند. در نتیجه، ابزارگذاری بازنویسی شده میتواند به قابلیتهای پیچیده دسترسی داشته باشد، حتی اگر خود آن تنها شامل چند دستور پیوسته (contiguous instructions) باشد.
زمان اجرای کامپایل شده (compiled runtime) و برنامهی بازنویسی شده به صورت یک باینری اجرایی واحد خروجی گرفته میشوند. این کار باعث میشود ابزارهای فازینگ موجود بتوانند با حداقل تغییرات از برنامهی ابزارگذاری شده استفاده کنند.
۴.۲ کارایی ابزارگذاری (Instrumentation Performance)
فازینگ یک برنامه شامل ارسال تعداد زیادی ورودی به آن برای کشف رفتارهای جدید و خطاهای احتمالی است. بنابراین اثربخشی یک فازر تا حد زیادی به دو عامل وابسته است: اینکه از هر اجرای برنامه چه میزان اطلاعات میتوان بهدست آورد، و سرعت اجرای برنامهها (توان عملیاتی (throughput)) تا چه اندازه بالا است.
در ابزارگذاری سطح منبع (Source-level instrumentation)، میتوان از نمایش میانی (Intermediate Representation یا IR) کامپایلر برای درج ابزارگذاری استفاده کرد تا توان عملیاتی (throughput) بالایی روی باینریها حاصل شود. علاوه بر این، ابزارگذاری ایستا در سطح IR قبل از مرحلهی بهینهسازی (optimization) انجام میشود؛ در نتیجه، بهینهسازیهای بعدی میتوانند ابزارگذاری را متناسب با برنامهی خاص بهبود بخشند. اما در سطح باینری، RWFUZZ مجبور است مستقیماً باینریهای کامپایل شده را تغییر دهد و در این حالت انجام بهینهسازیها بهسادگی امکانپذیر نیست.
یکی از مزیتهای کلیدی ابزارگذاری در سطح IR، امکان استفادهی کارآمد از ثَباتها (registers) است. ابزارهای فازینگ که از بیتکد LLVM (LLVM bitcode) استفاده میکنند میتوانند دستورهای انتزاعی (abstract instructions) اضافه کنند که سپس به ثَباتهای پردازندهی بدون استفاده نگاشت میشوند. در مقابل، RWFUZZ نمیتواند هیچ فرضی دربارهی استفادهی یک برنامه از ثَباتها داشته باشد. از آنجا که کد ابزارگذاری ناچار است ثباتها را تغییر دهد، هر هوک (hook) ابزارگذاری میبایست قبل از اجرا وضعیت پردازنده را ذخیره کرده و پس از آن بازیابی کند. این موضوع سربار عملکردی قابل توجهی ایجاد میکند. به منظور کاهش این اثر، RWFUZZ از مجموعهای حداقلی از ثباتهای یکتا برای ابزارگذاری خود استفاده میکند تا میزان هزینهی ذخیره و بازیابی وضعیت پردازنده کاهش یابد.
فازینگ مبتنی بر گرادیان نزولی (gradient descent) نیازمند آن است که مشخص شود کدام بخشهای ورودی بر هر مقایسه تأثیر میگذارند. این کار از طریق ردگیری آلودگی (taint tracking) انجام میشود که جریان اطلاعات را به صورت پویا اندازهگیری میکند. ابزارگذاری مربوط به ردگیری آلودگی میتواند یا در زمان کامپایل درج شود و یا در زمان اجرا با استفاده از ابزارهای ابزارگذاری پویا مانند Pin [20] صورت پذیرد. در حالت بازنویسی باینری برای فازینگ، ابزارگذاری در زمان کامپایل در دسترس نیست. برای سادگی، RWFUZZ از ابزارگذاری پویای موجود در حلقهی فازینگ Angora استفاده میکند که بر پایهی libdft [17] بنا شده است.
۵. ارزیابی (Evaluation)
ارزیابی ما با هدف نشان دادن توانایی RWFUZZ در ابزارگذاری باینریها برای فازینگ انجام شده است، بهگونهای که اثربخشی آن با فازرهای سطح منبع (source-level fuzzers) قابل مقایسه باشد. تمرکز ما بر پرسشهای زیر است:
(1) آیا RWFUZZ میتواند همان دسته از باگهایی را پیدا کند که فازرهای سطح منبع هدف قرار میدهند؟
(2) تعادل عملکردی (performance trade-offs) در ابزارگذاری باینری، از نظر پوشش کد (code coverage) و تعداد باگهای کشف شده چیست؟
۵.۱ ابزارگذاری فازینگ (Fuzzing Instrumentation)
در ابتدا RWFUZZ را روی چهار برنامهی نمونه (نشان داده شده در شکل ۵) ارزیابی میکنیم تا عملکرد آن تأیید شود و همچنین انواع شرطهایی که میتوانند برای کشف باگها حل شوند، نمایش داده شوند. این چهار ورودی، برنامههایی با پیچیدگی افزایشی هستند که به تدریج برای مسئلهی کشف باگ دشوارتر میشوند. ما عملکرد RWFUZZ را با فازرهای AFL [31]، LAF-INTEL [3] و Angora [11] روی چهار برنامه مقایسه میکنیم:
(۱) simple: یک باگ سادهی سرریز بافر (buffer-overrun) که بهصورت بدیهی قابل رخ دادن است. این باگ توسط تمام ابزارهای آزمایش شده بهسرعت کشف شده است. یافتن این نوع باگ نیازی به حل محدودیتها (constraint solving) مانند آنچه در Angora پیادهسازی شده (چه در نسخهی ابزارگذاری شده از کد منبع و چه با استفاده از RWFUZZ) ندارد، زیرا جهشهای تصادفی (random mutations) برای فعالسازی آن کافی هستند.
(۲) Magic: شامل یک مقایسه با یک مقدار جادویی ۳۲ بیتی (32-bit magic value) است. این مورد مشابه با باگهایی است که در مجموعهدادهی LAVA-M تزریق شدهاند. ابزار AFL نمیتواند این باگ را در زمان معقول پیدا کند، در حالی که سایر ابزارها موفق به کشف آن میشوند.
(۳) Context: مشابه مورد magic است، با این تفاوت که تنها در اولین فراخوانی تابع foo باعث کرش (Crash) میشود. از آنجایی که a و b اعداد صحیح بدون علامت (unsigned integers) هستند، عمل a − b بهصورت تفریق بدون علامت انجام میشود و نتیجهی آن همواره نامنفی خواهد بود. LAF-INTEL نمیتواند این باگ را به صورت پایدار و قابلاعتماد فعالسازی (trigger) کند. در مقابل، RWFUZZ و Angora هر دو از شمارش انشعابهای حساس به زمینه (context-sensitive branch counts) پشتیبانی میکنند، بنابراین هر دو قادر به کشف این باگ هستند.
(۴) Undef: یک ارجاع (دهی) به اشارهگر تهی (null-pointer dereference) است که در زمان کامپایل قابل تشخیص میباشد. اگرچه هر دو کامپایلر GCC و Clang این بخش را به صورت پیشفرض بهینه سازی نمیکنند، اما اضافه شدن کد ابزارگذاری مورد استفاده در Angora باعث میشود گذرهای بهینهسازی (optimization passes) بیشتر، این باگ را حذف کنند.
در نتیجه، Angora نمیتواند این باگ را شناسایی کند، با وجود اینکه در نسخهی بدون ابزارگذاری برنامه وجود دارد. در مقابل، RWFUZZ در سطح باینری ابزارگذاری میکند و بنابراین رفتار برنامهی بدون ابزارگذاری را به طور دقیق بازتولید میکند.
هر برنامه به مدت یک دقیقه توسط هر فازر مورد آزمایش قرار گرفت. مقایسهی زمان لازم برای یافتن این باگها (جدول ۲) نشان دهندهی اثربخشی فازینگ مبتنی بر بازنویسی باینری است. RWFUZZ توانست تمام باگهایی را که Angora شناسایی کرده بود، در زمانی قابل مقایسه کشف کند. در مقابل، LAF-INTEL و AFL که از ابتکارات سادهتری برای اندازهگیری و کشف موارد آزمون (test cases) جدید استفاده میکنند، در برنامههای نمونهی سختتر موفق به یافتن باگها نشدند. این نتیجه نشان میدهد که RWFUZZ در حال یافتن دستههای مشابهی از باگهای Angora است.
در برخی موارد، RWFUZZ میتواند باگهایی را در برنامههایی پیدا کند که توسط Angora کشف نمیشوند. در برنامهی undef، این باگ ممکن است توسط برخی کامپایلرها از جمله مرحلهی ابزارگذاری (instrumentation pass) مورد استفاده در Angora بهینهسازی و در نتیجه حذف شود. باگهایی که ناشی از رفتار تعریف نشده (undefined behavior) هستند میتوانند در زمان آزمون پنهان بمانند و تنها در نسخههای نهایی و تولیدی (production releases) ظاهر شوند که این موضوع آنها را به طور ویژهای خطرناک و فریبنده (insidious) میکند.
ابزارگذاری در زمان کامپایل به طور ذاتی برنامهی تحت آزمون را تغییر میدهد؛ بنابراین هر باگی که به فرآیند کامپایل وابسته باشد ممکن است توسط فازرهای سطح منبع قابل بازتولید نباشد. در مقابل، فازینگ باینری میتواند در پیکربندی انتشار (release configuration) نرمافزار اعمال شود و صریحا رفتار برنامهی اصلی را تغییر ندهد. این موضوع یک مزیت کلیدی برای بازنویسی باینری در فازینگ محسوب میشود.
۵.۲ عملکرد روی مجموعههای دادهی فازینگ (Performance on Fuzzing Corpora)
در ادامه، عملکرد RWFUZZ را با سایر فازرها روی یک مجموعههای دادهی استاندارد از باگها مقایسه میکنیم. برای این منظور از مجموعههای دادهی LAVA-M [13] استفاده شده است. بهینهسازی Angora برای LAVA-M باعث میشود این مجموعههای داده، گزینهی مناسبی برای ارزیابی RWFUZZ باشد.
مجموعههای دادهی LAVA-M شامل مجموعهای از ابزارهای GNU Coreutils (از جمله base64 ،uniq ،md5sum و who) است که باگهایی به صورت مصنوعی در آنها ایجاد شدهاند. برای تولید کد معیوب، تحلیل جریان داده (data-flow analysis) روی کد منبع برنامه انجام میشود. این باگها به صورت مقادیر جادویی (magic values) بر اساس این تحلیل جریان داده وارد میشوند و بازتولید آنها ساده نیست، زیرا این مقادیر جادویی به طور مستقیم در ورودیهایی که باعث کرش (Crash) میشوند وجود ندارند.
ما چهار فازر (RWFUZZ، Angora، LAF-INTEL و AFL) را روی برنامههای LAVA-M ارزیابی کردیم. همچنین نتایج را با نتایج منتشرشدهی RetroWrite [12] نیز مقایسه کردیم، هرچند این دو ابزار اهداف متفاوتی دارند.
برای هر جفت فازر–برنامه (fuzzer-program)، فازینگ در ۲۰ اجرای مستقل (trial) و هر بار به مدت یک ساعت انجام شد. هر اجرا بهصورت تک رشتهای (single-threaded) روی یک سیستم مجهز به Intel Xeon 6136 با ۳۸۴ گیگابایت حافظه RAM اجرا شد. بذرهای ورودی (seed inputs) برای هر برنامه در تمام فازرها یکسان بودند.
جدول ۳ تعداد میانهی باگهایی را که هر فازر در مدت یک ساعت کشف کرده نشان میدهد. RWFUZZ در هر چهار برنامهی LAVA-M تعداد باگهای بسیار بیشتری نسبت به LAF-INTEL و AFL پیدا میکند و تقریباً به اندازهی Angora در تمام این چهار برنامه باگ کشف میکند.
این نتیجه نشان میدهد که ابزارگذاری (instrumentation) در RWFUZZ از نظر کارایی، با ابزارگذاری معادل در سطح منبع قابل مقایسه است. علاوه بر این، از آنجا که RetroWrite از ابزارگذاری سبک AFL استفاده میکند، RWFUZZ در عمل از نظر تعداد باگهای کشف شده عملکرد بهتری نسبت به آن دارد.
ما همچنین عملکرد زمان اجرای (runtime) RWFUZZ را تحلیل میکنیم. شکل 6a نشان میدهد که تعداد میانهی باگهای کشف شده در طول زمان توسط چهار فازر آزمایش شده چگونه تغییر میکند، و جدول ۴ نیز زمان لازم RWFUZZ برای یافتن تعداد باگهایی برابر با Angora را نشان میدهد.
برنامههایی که توسط RWFUZZ ابزارگذاری شدهاند، رفتار مشابهی با Angora نشان میدهند، به طوری که ورودیهای جدید تقریباً به صورت خطی تا زمانی که برنامه به طور کامل پوشش داده شود، تولید میشوند. RWFUZZ در کل مجموعه داده، سربار زمانی ×3.4 نسبت به Angora دارد.
برای مقایسه، Steelix [19] که یک نسخهی سازگار با باینری از LAF-INTEL [3] است، در مقایسه با نسخهی کامپایلزمانی خود ×7 سربار دارد. از آنجا که فازینگ معمولاً به صورت موازی روی تعداد زیادی سرور انجام میشود، این میزان سربار یک تعادل قابل قبول (acceptable trade-off) برای توانایی یافتن باگها بدون دسترسی به کد منبع محسوب میشود.
ما همچنین تعداد باگهای (bug) کشف شده توسط هر فازر را بهصورت نرمالسازی شده (normalized) نسبت به تعداد فراخوانیهای برنامهی تحت آزمون (program under test) مقایسه میکنیم. این مقایسه (شکل 6b) نشان میدهد که سربار RWFUZZ عمدتاً ناشی از طولانیتر بودن زمان اجرای برنامههای منفرد است. این سربار تا حدی به ناکارآمدیهای بازنویسی ایستای باینری (بخش 4.2) نسبت داده میشود.
علاوه بر این، ما باگهای مشخص کشف شده توسط RWFUZZ و Angora را در تمام اجراها با هم مقایسه کردیم. در این بررسی، یک باگ توسط Angora و نه توسط RWFUZZ کشف شد، در حالی که RWFUZZ موفق به کشف ۵ باگ گردید که Angora آنها را پیدا نکرده بود. همپوشانی قابل توجه در باگهای کشف شده نشان میدهد که RWFUZZ با استفاده از تکنیکهایی که پیشتر محدود به کد منبع بودند، میتواند بهدرستی فایلهای اجرایی را برای فازینگ ابزارگذاری کند.
۶. نتیجهگیری (Conclusions)
تحقیقات و بررسیهای ما نشان میدهد که تکنیکهای فازینگ لزوماً نباید به دسترسی به کد منبع محدود باشند. ارزیابی ما نشان میدهد که RWFUZZ با حداقل سربار عملکردی، به دقت فازینگ در سطح منبع دست مییابد. علاوه بر این، تکنیکهای بهکاررفته در این کار قابلیت تعمیم به کارهای آینده در حوزه فازینگ را دارند.
جامعهی تضمین امنیت نرمافزار (software assurance) میتواند از RWFUZZ برای ممیزی (audit) نرمافزارهای متنبسته (closed-source) به شیوهای مشابه پروژههای متنباز استفاده کند. با این حال، فازینگ باینری پیامدهای منفی نیز برای امنیت نرمافزار به همراه دارد، زیرا دیگر محرمانه نگهداشتن کد منبع نمیتواند به عنوان یک محافظ در برابر بهرهبرداری خودکار (automated exploitation) عمل کند. از این رو، پیشرفت در فازینگ باینری، پژوهشهای آینده در زمینهی «مبهمسازی مبتنی بر فازینگ» (fuzzing-oriented obfuscation) را ضروری میسازد.
قدردانی (Acknowledgement)
این اثر بر پایهی کاری است که با حمایت «برنامهی بورسیهی پژوهشی تحصیلات تکمیلی بنیاد ملی علوم آمریکا (National Science Foundation Graduate Research Fellowship Program)» تحت شمارهی کمک هزینه DGE1255832 انجام شده است. هرگونه دیدگاه، یافته، نتیجهگیری یا توصیههای مطرحشده در این اثر متعلق به نویسنده(ها) است و لزوماً بازتابدهندهی نظرات بنیاد ملی علوم آمریکا نمیباشد.
منابع
[1] “AFL-dyninst,” https://github.com/Cisco-Talos/moflow/tree/master/afl-dyninst, 2015.
[2] “AFL-QEMU,” http://lcamtuf.coredump.cx/afl/technical details.txt, 2015.
[3] “Circumventing fuzzing roadblocks with compiler transformations,” https://lafintel.wordpress.com, 2016.
[4] “Keystone – The Ultimate Assembler,” http://www.keystone-engine.org/,2016.
[5] “google/oss-fuzz,” Nov. 2019. [Online]. Available: https://github.com/google/oss-fuzz
[6] C. Aschermann, S. Schumilo, T. Blazytko, R. Gawlik, and T. Holz,“Redqueen: Fuzzing with input-to-state correspondence.” in NDSS,vol. 19, 2019, pp. 1–15.
[7] G. Balakrishnan and T. Reps, “Wysinwyx: What you see is not what you execute,” ACM Transactions on Programming Languages and Systems (TOPLAS), vol. 32, no. 6, p. 23, 2010.
[8] T. Bao, J. Burket, M. Woo, R. Turner, and D. Brumley, “BYTEWEIGHT: Learning to recognize functions in binary code,” in 23rd USENIX Security Symposium (USENIX Security 14), 2014, pp. 845–860.
[9] E. Bauman, Z. Lin, and K. W. Hamlen, “Superset disassembly: Statically rewriting x86 binaries without heuristics,” in Proc. NDSS, 2018, pp. 40–47.
[10] F. Bellard, “Qemu, a fast and portable dynamic translator.” in USENIX Annual Technical Conference, FREENIX Track, vol. 41, 2005, p. 46.
[11] P. Chen and H. Chen, “Angora: Efficient fuzzing by principled search,” in 2018 IEEE Symposium on Security and Privacy (SP). IEEE, 2018, pp. 711–725.
[12] S. Dinesh, N. Burow, D. Xu, and M. Payer, “Retrowrite: Statically instrumenting cots binaries for fuzzing and sanitization,” in 2020 IEEE Symposium on Security and Privacy (SP). Los Alamitos, CA, USA: IEEE Computer Society, may 2020, pp. 128–142. [Online]. Available:https://doi.ieeecomputersociety.org/10.1109/SP.2020.00009
[13] B. Dolan-Gavitt, P. Hulin, E. Kirda, T. Leek, A. Mambretti, W. Robertson, F. Ulrich, and R. Whelan, “Lava: Large-scale automated vulnerability addition,” in 2016 IEEE Symposium on Security and Privacy (SP). IEEE, 2016, pp. 110–121.
[14] J. E. Forrester and B. P. Miller, “An empirical study of the robustness of windows nt applications using random testing,” in Proceedings of the 4th USENIX Windows System Symposium, vol. 4. Seattle, 2000, pp. 59–68.
[15] P. Godefroid, “Fuzzing: hack, art, and science,” Communications of the ACM, vol. 63, no. 2, pp. 70–76, 2020.
[16] K. V. Hanford, “Automatic generation of test cases,” IBM Systems Journal, vol. 9, no. 4, pp. 242–257, 1970.
[17] V. P. Kemerlis, G. Portokalidis, K. Jee, and A. D. Keromytis, “libdft: Practical dynamic data flow tracking for commodity systems,” in ACM Sigplan Notices, vol. 47, no. 7. ACM, 2012, pp. 121–132.
[18] C. Lattner and V. Adve, “Llvm: A compilation framework for lifelong program analysis & transformation,” in Proceedings of the international symposium on Code generation and optimization: feedback-directed and runtime optimization. IEEE Computer Society, 2004, p. 75.
[19] Y. Li, B. Chen, M. Chandramohan, S.-W. Lin, Y. Liu, and A. Tiu, “Steelix: program-state based binary fuzzing,” in Proceedings of the 2017 11th Joint Meeting on Foundations of Software Engineering. ACM, 2017, pp. 627–637.
[20] C.-K. Luk, R. Cohn, R. Muth, H. Patil, A. Klauser, G. Lowney, S. Wallace, V. J. Reddi, and K. Hazelwood, “Pin: building customized program analysis tools with dynamic instrumentation,” in Acm sigplan notices, vol. 40, no. 6. ACM, 2005, pp. 190–200.
[21] P. Oehlert, “Violating assumptions with fuzzing,” IEEE Security & Privacy, vol. 3, no. 2, pp. 58–62, 2005.
[22] S. Rawat, V. Jain, A. Kumar, L. Cojocar, C. Giuffrida, and H. Bos, “Vuzzer: Application-aware evolutionary fuzzing.” in NDSS, vol. 17, 2017, pp. 1–14.
[23] D. Song, D. Brumley, H. Yin, J. Caballero, I. Jager, M. G. Kang, Z. Liang, J. Newsome, P. Poosankam, and P. Saxena, “Bitblaze: A new approach to computer security via binary analysis,” in International Conference on Information Systems Security. Springer, 2008, pp. 1–25.
[24] N. Stephens, J. Grosen, C. Salls, A. Dutcher, R. Wang, J. Corbetta, Y. Shoshitaishvili, C. Kruegel, and G. Vigna, “Driller: Augmenting fuzzing through selective symbolic execution.” in NDSS, vol. 16, no. 2016, 2016, pp. 1–16.
[25] P. Thompson, “aflpin,” https://github.com/mothran/aflpin, 2015.
[26] F. Wang and Y. Shoshitaishvili, “Angr-the next generation of binary analysis,” in 2017 IEEE Cybersecurity Development (SecDev). IEEE, 2017, pp. 8–9.
[27] R. Wang, Y. Shoshitaishvili, A. Bianchi, A. Machiry, J. Grosen, P. Grosen, C. Kruegel, and G. Vigna, “Ramblr: Making reassembly great again.” in NDSS, 2017.
[28] S. Wang, P. Wang, and D. Wu, “Reassembleable disassembling,” in 24th USENIX Security Symposium (USENIX Security 15), 2015, pp. 627–642.
[29] ——, “Uroboros: Instrumenting stripped binaries with static reassembling,” in 2016 IEEE 23rd International Conference on Software Analysis, Evolution, and Reengineering (SANER), vol. 1. IEEE, 2016, pp. 236– 247.
[30] R. Wartell, Y. Zhou, K. W. Hamlen, and M. Kantarcioglu, “Shingled graph disassembly: Finding the undecideable path,” in Pacific-Asia Conference on Knowledge Discovery and Data Mining. Springer, 2014, pp. 273–285.
[31] M. Zalewski, “American fuzzy lop: a security-oriented fuzzer,” https://lcamtuf.coredump.cx/afl/, 2010.