فازینگ (Fuzzing) مؤثرِ برنامههایی که ورودیهای باینریِ ساختیافته (Structured) مانند فایلهای چندرسانهای را پردازش میکنند، کاری چالشبرانگیز است؛ زیرا این برنامهها انتظار دارند ورودی دقیقاً مطابق یک قالب (Format) بسیار مشخص باشد. با این حال، فازرهای موجود عمدتاً مستقل از قالببندی (format-agnostic) هستند؛ این ویژگی اگرچه آنها را انعطافپذیر میکند، اما در سناریوهایی که رعایت یک قالب خاص ضروری است، کارایی آنها را کاهش میدهد.
در اینجا، ابزار FormatFuzzer معرفی میشود؛ یک مولد برای ساخت فازرهای وابسته به قالببندی (format-specific). این ابزار یک قالب باینری (binary template) – که در واقع مشخصهی قالب و مورد استفاده در 010 Editor است – را به عنوان ورودی دریافت کرده و آن را به کد ++C کامپایل میکند. کد حاصل میتواند به عنوان تجزیه کننده (parser)، جهش دهنده (mutator) و همچنین یک مولد (generator) بسیار کارآمد ورودی که با قواعد قالب سازگار است، عمل کند.
فازر تولید شدهی وابسته به قالب میتواند به صورت مستقل، به عنوان یک مولد (generator) یا جهش دهنده در سناریوهای جعبه سیاه (black-box)- جایی که هیچ بازخورد یا راهنمایی از برنامه هدف در دسترس نیست – استفاده شود. علاوه بر این، با فراهم کردن بذرهای تصمیم قابل جهش (mutable decision seeds)، میتوان آن را به سادگی با فازرهای مستقل از قالب مانند AFL یکپارچه کرد تا آنها را نسبت به قالببندی آگاه (format-aware) نمود.
FormatFuzzer در ارزیابی انجام شده روی قالبهای پیچیدهای مانند MP4 یا ZIP نشان داد که توانایی بالایی در تولید ورودیهای معتبر دارد و همچنین موفق به کشف خطاهای حافظهای ناشناخته قبلی در ابزارهایی مانند FFmpeg و ++TiMidity شده است.
۱. مقدمه (INTRODUCTION)
ابزارهای فازینگ مبتنی بر بازخورد مانند AFL [58] و libFuzzer [43] در یافتن باگها و آسیبپذیریها در نرمافزارهای پرکاربرد موفقیتهای قابل توجهی داشتهاند. این ابزارها معمولاً مستقل از قالببندی (format-agnostic) هستند، به این معنا که هنگام تولید یا جهش ورودیها (mutating inputs) هیچ فرضی درباره ساختار قالببندی ورودی (input format) ندارند. همین ویژگی باعث میشود راهاندازی و استفاده از آنها ساده باشد. با این حال، این رویکرد دامنه کاربرد آنها را به شرایطی محدود میکند که در آنها: (۱) فایلهای نمونه برای جهش در دسترس باشند؛ (۲) ابزارگذاری (instrumentation) و بازخورد از برنامهی تحت آزمون فراهم باشد؛ و (۳) هزینهی تولید تعداد زیادی ورودی نامعتبر قابل تحمل باشد.
در مقابل، جایگزین این رویکرد، استفاده از یک فازر وابسته به قالببندی (format-specific fuzzer) است که با بهرهگیری از دانش دقیق قالب، ورودیهای معتبر تولید میکند. با این حال، توسعه چنین فازری مستلزم صرف تلاش قابل توجهی است؛ به ویژه زمانی که هدف، استفاده مجدد از قابلیتهای هدایت (guidance) و تحلیل موجود در فازرهای هدایت شده با بازخورد باشد.
در این مقاله، ما رویکردی نوین ارائه میکنیم که انعطافپذیری فازرهای مستقل از قالببندی (format-agnostic) را با کارایی فازرهای وابسته به قالببندی (format-specific) ترکیب میکند. چارچوب FormatFuzzer با بهرهگیری از قالبهای باینری موجود (binary templates) – که در واقع مشخصات قالبهای ورودی از JPEG تا PCAP هستند – قادر است فایلهای ورودی معتبر را تولید کرده یا روی آنها جهش (mutation) اعمال کند؛ حتی در سناریوهای جعبه سیاه (black-box) که هیچگونه راهنمایی یا بازخوردی از برنامه در دسترس نیست.
علاوه بر این، FormatFuzzer میتواند با فازرهای مستقل از قالببندی مانند AFL یکپارچه شود و آنها را نسبت به قالببندی آگاه (format-aware) سازد. در تمامی این حالتها، استفاده از FormatFuzzer منجر به افزایش پوشش (coverage) در برنامهی تحت آزمون میشود؛ به طوری که بخشهایی از کد که توسط فازرهای مستقل از قالببندی قابل دسترسی نیستند، پوشش داده میشوند و در نتیجه احتمال کشف باگها و آسیب پذیریها افزایش مییابد.
FormatFuzzer چگونه کار میکند؟ ورودیهای FormatFuzzer در واقع مشخصات (specifications) موجودِ قالببندیهای ورودی هستند.
ابزار 010 Editor یک ویرایشگر تجاری است [47] که به کاربران اجازه میدهد فایلهای باینری را همراه با اطلاعات ساختاری دقیق مشاهده و ویرایش کنند. این ابزار برای تجزیه کردن (parse) ورودیها، به فایلهای مشخصهای که توسط انسان نوشته شدهاند و با نام قالبهای باینری (binary templates) [46] شناخته میشوند، متکی است. این قالبها بخشهای مختلف یک فایل را توصیف میکنند.
این قالبهای باینری در قالب زبانی شبیه به C نوشته میشوند و طی بیش از ده سال توسط جامعه توسعه داده شدهاند؛ به طوری که امروزه مخزنی شامل بیش از ۲۰۰ مشخصه مختلف برای قالبهای باینری پرکاربرد [45] وجود دارد. بنابراین، برای بسیاری از اهداف رایج در فازینگ، از قبل یک قالب باینری که ساختار ورودی را توصیف میکند، بهصورت آنلاین در دسترس است.
برای مثال، بخش عمدهای از باگهای ثبت شده در «bug trophy case» مربوط به AFL از برنامههایی ناشی میشوند [58] که قالبهای باینری رایج را پردازش میکنند—قالبهایی که مشخصات عمومی و در دسترس دارند. در این میان، قالبهای چندرسانهای مانند تصاویر، صوت و ویدئو اهمیت ویژهای دارند؛ زیرا اغلب از منابع غیرقابل اعتماد دریافت میشوند و در نتیجه سطح حمله (attack surface) گستردهای ایجاد میکنند که میتواند پیامدهای امنیتی جدی به همراه داشته باشد.
به عنوان نمونهای از یک قالب باینری، به «لیست ۱» توجه کنید که بخشی از قالب اصلی فایلهای PNG را نشان میدهد. همانطور که مشاهده میشود (مشابه یک برنامه به زبان C)، در آن نوعهایی مانند PNG_PALETTE_PIXEL تعریف شدهاند که از سه مقدار بایتی تشکیل میشود. در ادامه، بخشی از یک ساختار (struct) تعریف شده است که یک قطعه (chunk) از این پیکسلها را با نام PNG_CHUNK_PLTE مشخص میکند که طول کلی آن (به صورت پارامتری) برابر با chunkLen است.
قالب کامل PNG شامل دهها تعریف از این نوع قطعهها (chunk) است که همگی در قالب باینری PNG مشخص شدهاند. این قالبها همچنین شامل کدهای اجرایی هستند که اطلاعات وابسته به زمینه (context-sensitive) مانند جمعآزماها (checksum) را محاسبه میکنند—موضوعی که استنتاج آن یکی از موانع اصلی برای فازرهای مستقل از قالببندی (format-agnostic) به شمار میرود.
FormatFuzzer یکی از این قالبهای باینری -برای مثال قالب PNG- را به عنوان ورودی دریافت میکند و سپس آن را به موارد زیر کامپایل میکند:
(1) یک مولد بسیار کارآمد که خروجیها را مطابق قالب مشخص شده تولید میکند (یعنی یک فازر PNG)؛
و
(2) یک تجزیه کننده (parser) که ورودیهای موجود در همان قالب را میخواند (یعنی یک تجزیه کننده PNG).
فهرست ۱. قالب باینری PNG (استخراج)
typedef struct {
byte btRed;
byte btGreen;
byte btBlue;
} PNG_PALETTE_PIXEL;
struct PNG_CHUNK_PLTE (int32 chunkLen) {
PNG_PALETTE_PIXEL plteChunkData[chunkLen/3];
};
تجزیه کننده (parser) میتواند بلافاصله در فازرهای موجودِ تقویت شده با تجزیه کننده مانند AFLSmart [39] مورد استفاده قرار گیرد و در عمل یک فازر وابسته به قالب در اختیار ما قرار دهد که مثلاً با استفاده از ورودیهای اولیه PNG تکامل پیدا میکند. این فرایند میتواند برای صدها قالب مختلف تکرار شود و در نتیجه، صدها فازر وابسته به قالب و هدایت شده با پوشش (coverage-guided) تولید شود.
در مقابل، مولد (generator) میتواند بهصورت مستقل به عنوان یک ابزار تولید ورودی استفاده شود؛ به طوری که تمام ورودیهای تولید شده کاملاً مطابق با مشخصات قالب باشند. این قابلیت در سناریوهای جعبه سیاه (black-box) بسیار مفید است، جایی که امکان استفاده از فازرهای هدایت شده با بازخورد وجود ندارد.
علاوه بر این، ترکیب تجزیه کننده و مولد به FormatFuzzer اجازه میدهد تا جهش (mutation) روی ورودیهای موجود را به گونهای انجام دهد که همچنان معتبر باقی بمانند. در نهایت، FormatFuzzer امکان یکپارچهسازی فازرهای مستقل از قالب را نیز فراهم میکند و آنها را به فازرهای آگاه به قالببندی (format-aware) تبدیل میسازد؛ این کار یا از طریق اعمال جهشهای آگاه به قالببندی (format-aware mutations) روی ورودیها انجام میشود، یا از طریق جهش روی بذرهای تصمیم (decision seeds) – که رشتههایی از بایتها هستند و تصمیمهای اتخاذ شده در فرآیند تجزیه و تولید را نمایش میدهند.
این دو نوع یکپارچهسازی، بهترین ویژگیهای هر دو جهان را ترکیب میکند: هم هدایت مبتنی بر بازخورد در فازرهای مستقل از قالب، و هم جهشهای آگاه به ساختار قالب. در نتیجه، گستره (reach) فازرهای مستقل از قالب به شکل قابل توجهی افزایش پیدا میکند.
با این حال، همچنان مقداری تلاش دستی لازم است. صدها قالب باینری (binary template) موجود در حال حاضر برای تجزیه کردن (parsing) عملکرد خوبی دارند؛ اما برای ساخت مولدهای مؤثر (effective generators) لازم است این قالبها تا حدی بیشتر اصلاح و دقیقسازی شوند. با این وجود، این اصلاحات به راحتی قابل نوشتن هستند و زبان غنی و کامل قالب باینریها (binary template) به اندازهای قدرتمند است که تقریباً تمام ویژگیهای موردنیاز قالبهای فایل باینری را بیان کند؛ از جمله فیلدهای طول (length fields)، جمعآزماها (checksum)، ساختارهای پیچیده، و قیود بین متغیرها.
گسترش یک قالب باینری (binary template) و کامپایل کردن آن، برای هر قالب تنها یک تلاش یکباره (one-time effort) است. پس از آن، اگر مثلاً یک مشخصه کامل برای PNG داشته باشیم، در عمل به یک فازر PNG، یک تجزیه کننده PNG، یک جهش دهنده PNG، و حتی نسخهای از فازرهای محبوب مستقل از قالب و هدایت شده با بازخورد دست پیدا میکنیم که میتواند هزاران فایل PNG معتبر را در هر ثانیه تولید کند آن هم با کسری از تلاشی که برای ساخت چنین فازری از ابتدا لازم بود.
در ادامه مقاله، تکنیکهای پشت FormatFuzzer و نحوه بهبود وضعیت موجود در فازینگ را توضیح میدهیم. بهطور مشخص، ما مشارکتهای (contributions) زیر را ارائه میکنیم:
(1) یک قالب توصیف جدید برای تولید ورودیهای باینری. صدها قالب باینری با دقت بالا در حال حاضر وجود دارند که تعداد زیادی از قالبهای فایل باینری را توصیف میکنند. در بخش ۲ ساختار آنها را شرح میدهیم و در بخش ۳ آنها را با قابلیتهای جدید برای تولید ورودیهای معتبر گسترش میدهیم.
(2) بذرهای تصمیم (decision seeds) برای همگامسازی مولدها و تجزیه کنندهها. FormatFuzzer از قالبهای باینری (binary template) استفاده میکند تا کد ++C تولید کند (شکل ۲) که در نهایت در بخش ۴ به مولدها و تجزیه کنندههای تخصصی و بسیار کارآمد کامپایل میشود؛ مولدها و تجزیه کنندههایی که شامل چندین ویژگی وابسته به زمینه (context-sensitive) هستند (بخش ۳). مولد و تجزیه کننده از طریق بذرهای تصمیم (decision seeds) با یکدیگر همگام میشوند، یعنی دنبالهای از بایتها که بیانگر تصمیمهای اتخاذ شده در زمان تولید یا تجزیه کردن هستند (بخش ۵).
پس از تجزیه یک ورودی، بذر تصمیم (decision seed) حاصل این امکان را فراهم میکند که مولد همان تصمیمها را (اما این بار در مرحله تولید) دوباره اتخاذ کند و در نتیجه همان ورودی را به صورت دقیق بازتولید کند. تا جایی که میدانیم، FormatFuzzer نخستین چارچوبی است که این تبدیل دوطرفه بین بذرهای تصمیم (decision seeds) و فایلهای باینری را ممکن میسازد. این قابلیت امکان اعمال جهشهای هوشمند (smart mutations) جدید (بخش ۶.۲) را روی بذرهای تصمیمگیری فراهم میکند؛ آن هم بر پایه یک مجموعه (corpus) با کیفیت از فایلهای باینری.
از یک قالب باینری (1) که قالب ورودی F را مشخص میکند، FormatFuzzer (2) کد ++C تولید میکند (3) که در نهایت به یک مولد بسیار کارآمد (4) برای قالب F کامپایل میشود. در یک سناریوی آزمون جعبه سیاه (black-box)، این مولد از یک منبع بایتهای تصادفی (5) استفاده میکند تا یک ورودی فازینگ (fuzz input) (6) در قالب F برای برنامه تحت آزمون (7) تولید کند. FormatFuzzer همچنین از همان قالب باینری، کد ++C را (8) برای یک تجزیه کننده (parser) (9) قالب F تولید میکند.
در یک سناریوی جهش (mutation)، این تجزیه کننده یک نمونه ورودی (10) را به یک بذر تصمیم (decision seed) (11) تبدیل میکند؛ بذر تصمیم رشتهای از بایتها است که تصمیمهای اتخاذ شده در حین تجزیه کردن نمونه را کُدگذاری میکند.
FormatFuzzer میتواند این بذر را جهش دهد (12) (برای مثال، با جایگزینی یک قطعه (chunk)) و سپس با تغذیه بذر جهش یافته (13) به مولد، یک ورودی جهش یافته تولید میشود که همچنان با قالب F سازگار است.
در نهایت، در یک سناریوی تکاملی (evolutionary)، جهش دهنده میتواند یک فازر مستقل از قالببندی (format-agnostic) (14) مانند AFL باشد که بذرها را بر اساس بازخورد اجرای برنامه (15) جهش داده و به تدریج تکامل میدهد. چنین فازری همچنین میتواند جهشهای خارج از قالببندی را مستقیماً روی خود ورودی فازینگ اعمال کند.
(3) آگاهسازی فازرهای مستقل از قالببندی نسبت به قالب ورودی. جهش دادن بایتها در بذر تصمیم (decision seed) امکان کاوش کل دامنه ورودیها را فراهم میکند. چنانچه یک فازر مستقل از قالببندی (format-agnostic) برای جهش و تکامل همین بذرهای تصمیم تنظیم شود، FormatFuzzer میتواند هر جهش ایجاد شده را به یک ورودی باینری معتبر تبدیل کند.
این قابلیت یک رویکرد مبتنی بر مولد (generator-based) برای یکپارچهسازی FormatFuzzer با هر فازر مستقل از قالببندی فراهم میکند و به طور مستقیم از استراتژیهای جهش و تکامل آنها بهره میگیرد (بخش ۶). علاوه بر این، FormatFuzzer جهشهای هوشمند (smart mutations) جدیدی ارائه میدهد که روی بذرهای تصمیم (decision seed) عمل میکنند و در نتیجه میتوانند اطلاعات زمینهای (contextual information) موجود در فایلهای جهشیافته را حفظ کنند.
این موضوع یک روش بسیار مؤثر دیگر برای بهبود هر فازر مستقل از قالببندی فراهم میکند؛ زیرا امکان بهرهبرداری از جهشهای هوشمند وابسته به قالببندی (format-specific smart mutations) ارائه شده توسط FormatFuzzer را فراهم میسازد (بخش ۶).
(4) ارزیابی جامع از تمام اجزای FormatFuzzer. ما FormatFuzzer را (بخش ۷) روی ۱۰ برنامه و قالب فایل مختلف ارزیابی کردیم، شامل: PNG، JPEG، GIF، MIDI، MP4، ZIP، PCAP، AVI، WAV و BMP. نتایج نشان میدهد که:
- FormatFuzzer به سرعت ورودیهای معتبر تولید میکند، چه به صورت یک مولد مستقل و چه به عنوان یک جهش دهنده (mutator).
- FormatFuzzer به خوبی با فازرهای مستقل از قالببندی مانند AFL یکپارچه میشود و به خطوطی از کد دسترسی پیدا میکند که در حالت عادی پوشش داده نمیشوند. در آزمایشهای اولیه فازینگ، FormatFuzzer موفق شد تعدادی خرابی تقسیمبندی حافظه (segmentation faults) و خطاهای خاتمه ناگهانی (abort) را که پیشتر ناشناخته و بالقوه قابل سوءاستفاده بودند، در برنامههای FFmpeg و TiMidity شناسایی کند.
FormatFuzzer به صورت متنباز (open source) در دسترس است و جامعه کاربری آن به سرعت در حال رشد است. برای جزئیات بیشتر، به نشانی زیر مراجعه کنید:
https://github.com/uds-se/FormatFuzzer
۲. زبان قالب باینری (Binary Template Language)
ما بررسی خود را درباره FormatFuzzer با توضیح مهمترین ورودی آن آغاز میکنیم: قالبهای باینری (binary template). زبان قالب باینری [48] به گونهای طراحی شده است که بتواند هر قالب باینری را به طور کامل توصیف کند. نحو (syntax) و معنای (semantics) این زبان به زبان برنامهنویسی C بسیار نزدیک است؛ با این تفاوت اساسی که این زبان به جای توصیف برنامهها (programs)، برای توصیف قالببندیهای داده (data formats) به کار میرود. اجزای اصلی این زبان عبارتاند از:
اعلان متغیرها (دادههای ورودی). هر متغیری که در قالبهای باینری اعلان میشود، بهصورت مستقیم به یک دنبالهای از بایتها در ورودی نگاشت (map) میگردد. اگر یک قالب (template) با عبارت uint32 len; chars[len]; int64 n; آغاز شود، این بدان معناست که ورودی شامل موارد زیر است:
- یک عدد صحیح بدون علامت ۳۲ بیتی (len) در چهار بایت ابتدایی؛
- یک آرایه از کاراکترها با طول len در len بایت بعدی؛
- یک عدد صحیح ۶۴ بیتی (n) در چهار بایت بعدی.
توجه داشته باشید که اندازه آرایهها میتواند هر عبارت دلخواهی باشد، از جمله متغیرها و توابع؛ این ویژگی امکان مشخصکردن طولهای متغیر (variable-length) را فراهم میکند.
متغیرهای محلی (Local variables). زمانی که یک متغیر با کلیدواژهی local (محلی) تعریف میشود، این متغیر به محتوای ورودی نگاشت نمیشود، بلکه مانند یک متغیر سنتی عمل میکند که در حافظه ذخیره شده است. چنین متغیری میتواند آزادانه در محاسبات و تصمیمگیریهای جریان کنترل (control-flow) در قالب باینری (binary template) مورد استفاده قرار گیرد.
اعلان نوعها (Type declarations). مانند زبان C، میتوان با استفاده از ساختارهای typedef و struct انواع جدید تعریف کرد. در «لیست ۱» نوعی به نام PNG_PALETTE_PIXEL تعریف شده است که از سه بایت مستقل تشکیل میشود. علاوه بر انواع پایه (native types) مانند uint32 و string و همچنین نوعهای struct، متغیرها میتوانند از نوع enum نیز باشند؛ در این حالت، مجموعه مقادیر مجاز بهصورت صریح مشخص میشود.
پارامترها (Parameters). برخلاف C، اعلان نوعها و متغیرها میتوانند دارای پارامتر باشند. برای مثال، تعریف PNG_CHUNK_PLTE در «لیست ۱» با پارامتری به نام chunkLen پارامتردهی شده است که اندازه آرایهی داخل آن را تعیین میکند.
شرطها (Conditionals). قالبهای باینریها (binary template) تمام ساختارهای رایج جریان کنترل (control-flow) مانند if، else، while، switch و غیره را شامل میشوند. بدنهی این ساختارها نیز میتواند شامل اعلان متغیرها باشد. از این قابلیت برای بیان انتخابها (alternatives) و حلقهها (loops) در ساختار ورودی استفاده میشود. به عنوان مثال، برای بیان دنبالهای از قطعهها (chunk) که نوع واقعی آنها به یک هدر (header) وابسته است، میتوان از حلقهها و شرطها استفاده کرد، همانطور که در «لیست ۲» نشان داده شده است.
فهرست ۲. حلقهها و جایگزینها در یک قالب باینری:
while (!FEof()) {
uint32 length;
char type[4];
if (type == "IHDR")
PNG_CHUNK_IHDR ihdr;
else if (type == "PLTE")
PNG_CHUNK_PLTE plte(length);
/* ... */
if (type == "IEND")
break; /* break out of while loop */
}
توابع داخلی (Built-in functions). شرط حلقهی while در «لیست ۲» تابعی به نام ()FEof را فراخوانی میکند تا وضعیت «پایان فایل» (end-of-file) را بررسی کند. این زبان مجموعهای از چنین توابعی را ارائه میدهد تا کنترل بیشتری در هنگام تجزیه کردن (Pars) فراهم شود و زبان را به اندازهای گویا (expressive) کند که بتواند ساختارهای پیچیدهی موجود در صدها قالب باینری را پشتیبانی کند. از جمله این توابع میتوان به ()FTell اشاره کرد که موقعیت فعلی فایل را باز میگرداند، ()FSeek که امکان پرش به یک موقعیت دیگر در فایل را برای تجزیه غیرخطی (nonsequential parsing) فراهم میکند، و ()FileSize که اندازه فایل را باز میگرداند. توابع پیشرفتهی جستوجوی روبه جلو (lookahead) مانند ()FindFirst و ()FindAll امکان جستوجوی توکنهای خاص در فایل را فراهم میکنند. توابع lookahead به تجزیه کننده اجازه میدهند بدون تغییر دادن موقعیت فعلی فایل، چند بایت بعدی را بررسی کند و از این اطلاعات برای تصمیمگیری در جریان کنترل (control-flow) استفاده شود. برای مثال، بررسی اولیه چند بایت ابتدای یک struct میتواند کمک کند نوع struct مورد نظر برای تجزیه (Pars) مشخص شود. نمونههایی از توابع lookahead شامل ()ReadByte و ()ReadBytes هستند. علاوه بر توابع ورودی/خروجی (I/O)، مجموعه گستردهای از توابع رشتهای و ریاضی نیز در دسترس است، از جمله توابع تبدیل (conversion) و الگوریتمهای محاسبهی جمعآزما (checksum).
توابع سفارشی (Custom functions). این زبان همچنین اجازه میدهد توابع سفارشی خود کاربر تعریف شوند که از نظر نحو (syntax) و معنا (semantics) از ساختار توابع در زبان C پیروی میکنند و همچنان امکان اعلان متغیرها برای توصیف دادههای ورودی را فراهم میسازند. از آنجا که تمام عملگرهای زبان C نیز در این زبان پشتیبانی میشوند، این موضوع باعث میشود قالبهای باینری (binary template) به یک توصیف تورینگ کامل (Turing-complete) از قالبهای ورودی تبدیل شوند.
مدیریت خطا (Error handling). در صورت بروز خطاهای غیرقابلبازیابی در فرآیند تجزیه کردن، این زبان امکان خاتمهی زودهنگام (early termination) را نیز فراهم میکند که از طریق دستور return در سطح بالا (top-level) انجام میشود. یک تعریف کامل از نحو (syntax) و معناشناسی (semantics) زبان قالبهای باینری (binary template) به صورت آنلاین در دسترس است [48]. لازم به ذکر است که انتخاب ما برای استفاده از قالبهای باینری به عنوان پایهی FormatFuzzer کاملاً عملگرایانه (pragmatic) بوده است. این قالبها نشان دادهاند که میتوانند به طور دقیق صدها قالب مختلف را توصیف کنند؛ همچنین به خوبی مستندسازی شدهاند؛ و همانطور که در این مقاله نشان خواهیم داد، تطبیق آنها برای فازینگ مؤثر نیز چندان دشوار نیست. این که آیا میتوان تمام ظرافتها و پیچیدگیهای قالبهای ورودی را به شکلی زیباتر یا مفیدتر توصیف کرد یا نه، هنوز موضوعی است که اثبات نشده باقی مانده است.
۳. استفاده از قالب باینری برای تولید (BINARY TEMPLATES FOR GENERATION)
به منظور استفاده از قالبهای باینری (binary template) در تولید فایلهای جدید، لازم است برخی تصمیمها در طول فرآیند تولید اتخاذ شوند. مهمترین مورد این است که باید برای هر متغیر تعریف شده یک مقدار انتخاب شود، و همچنین برای هر تابع lookahead که فراخوانی میشود نیز مقداردهی انجام گیرد. اگر این مقادیر به صورت کاملاً یکنواخت (uniform) و تصادفی از کل بازهی ممکن انتخاب شوند (برای مثال، 2³² مقدار ممکن برای یک uint32)، در اغلب موارد مقادیر تولید شده بیمعنا خواهند بود و در نتیجه فایل حاصل نامعتبر میشود. بنابراین، ما زبان قالبهای باینری را از چند جهت گسترش میدهیم:
انتخاب مقادیر معتبر (Choices of valid values). اولین توسعهای که ارائه میدهیم این است که امکان مشخص کردن یک آرایه از مقادیر معتبر برای انتخاب فراهم میشود. هنگام اعلان یک متغیر، میتوان این آرایهی انتخابها را به عنوان یک لیست مقداردهی اولیه (initialization list) اختصاص داد. برای مثال، خط ۱۱ در «لیست ۳» به مولد (generator) میگوید که مقدار متغیر bits را به صورت یکنواخت (uniform) از میان پنج مقدار ممکن انتخاب کند.
شمارشها (Enumerations). برای مدیریت شمارشها (enumerations)، مولد یک مقدار را به صورت یکنواخت از میان مجموعه مقادیر تعریف شده انتخاب میکند (برای مثال، PNG_COLOR_SPACE_TYPE در لیست ۳)، که معمولاً رفتار مناسبی است. با این حال، امکان مشخص کردن مجموعهای متفاوت از گزینهها از طریق یک فهرست مقداردهی اولیه نیز وجود دارد.
lookahead. برای توابع lookahead، زبان را بهگونهای گسترش میدهیم که اجازه دهد یک آرگومان جدید مشخص شود؛ این آرگومان مجموعهای از مقادیر ممکن را تعیین میکند که باید انتخاب از میان آنها صورت گیرد. برای مثال، آرایهی colors که به عنوان آرگومان به فراخوانی ()ReadByte در خط ۹ «لیست ۳» پاس داده میشود، نمونهای از این قابلیت است.
اجازه دادن به انتخابهای نامعتبر (Allowing invalid choices). در نهایت، ما به مولد اجازه میدهیم تا از رفتار مشخص شده منحرف شود و با احتمال کم (حدود ۱٪)، هر مقداری را از کل دامنهی مقادیر ممکن برای یک ubyte (یعنی ۲⁸ مقدار ممکن) انتخاب کند. این کار از طریق عملیاتی انجام میشود که آن را «تصمیم شرورانه یا نادرست» (evil decision) مینامیم. این تصمیمهای «نادرست» به صورت پیشفرض فعال هستند، اما میتوان آنها را در طول فرآیند تولید با فراخوانی تابع ()SetEvilBit روشن یا خاموش کرد. این تابع مقدار جدید فلگ (flag) را به عنوان ورودی دریافت میکند و مقدار قبلی آن را بازمیگرداند. زمانی که این قابلیت فعال باشد، میتواند تنوع ورودیهای تولید شده توسط فازر را افزایش دهد؛ زیرا اجازه میدهد تعداد کمی انتخاب نامعتبر نیز در حین تولید بررسی و تجربه شوند، و در نتیجه فضای جستوجوی فازر گستردهتر میشود.
لیست ۳. محتوای chunk IHDR، که در آن مقدار bits به color_type وابسته است:
typedef enum {
GrayScale=0, TrueColor=2, Indexed=3, AlphaGrayScale=4, AlphaTrueColor=6
} PNG_COLOR_SPACE_TYPE;
typedef struct {
uint32 width ;
uint32 height ;
local byte colors[] = { GrayScale, TrueColor, Indexed, /* ... */ };
switch (ReadByte(FTell() + 1, colors)) { /* color_type */
case GrayScale:
ubyte bits = { 1, 2, 4, 8, 16 };
break;
case TrueColor:
ubyte bits = { 8, 16 };
break;
case Indexed:
ubyte bits = { 1, 2, 4, 8 };
break;
/* ... */
}
PNG_COLOR_SPACE_TYPE color_type;
PNG_COMPR_METHOD compr_method;
PNG_FILTER_METHOD filter_method;
PNG_INTERLACE_METHOD interlace_method;
} PNG_CHUNK_IHDR;
اکنون اجازه دهید این توسعهها را به کار بگیریم و نشان دهیم چه تغییراتی باید در قالب باینری (binary template) اعمال شود تا از تولید ورودیهای معتبر پشتیبانی شود. مثال مورد استفاده ما، یعنی PNG، نمایانگر تغییراتی است که برای سایر قالبهای فایل نیز مورد نیاز است. این بخش به صورت جامع تمام تغییراتی را که برای تولید PNGهای معتبر لازم بودهاند، شرح میدهد. برای هر قالبی که تاکنون بررسی کردهایم، تمام اصلاحات آن نیز در یکی از دستههای توضیح داده شده در این بخش قرار میگیرند (تمامی تغییرات کدها در مقاله اصلی با رنگ آبی مشخص شدهاند).
۳.۱ مقادیر جادویی (Magic Values)
سادهترین ویژگی رایج در قالبهای باینری، استفاده از مقادیر جادویی (magic values) است؛ یعنی برخی بایتهای مشخص در فایل باید مقدار دقیق و از پیش تعیین شدهای داشته باشند. برای مثال، یک فایل PNG باید با امضایی آغاز شود که شامل بایتهای “\x89504E470D0A1A0A” است.
همانطور که در «لیست ۴» نشان داده شده است، در قالب اصلی PNG، از قبل یک بررسی وجود داشت که این امضا را با مقدار مورد انتظار مقایسه میکرد و در صورت نامعتبر بودن امضا، اجرای قالب را متوقف میساخت.
در این حالت، FormatFuzzer میتواند هنگام تحلیل کد منبع قالب باینری (binary template) به صورت خودکار چنین مقایسههایی (با عملگرهای != یا ==) را استخراج کند و مقادیر جادویی مورد نیاز را به خاطر بسپارد. برای مثال، مقدار 0x8950 به عنوان یک مقدار معتبر برای اندیس 0 در آرایهی btPngSignature ذخیره میشود. این نوع مقایسهها همچنین میتوانند در داخل تعریف structها و توابع نیز استخراج (mining) شوند.
لیست ۴. امضای PNG: بایتهای جادویی به طور خودکار استخراج میشوند:
typedef struct {
uint16 btPngSignature[4];
} PNG_SIGNATURE;
local int evil = SetEvilBit(false);
PNG_SIGNATURE sig;
SetEvilBit(evil);
if (sig.btPngSignature[0] != 0x8950 ||
sig.btPngSignature[1] != 0x4E47 ||
sig.btPngSignature[2] != 0x0D0A ||
sig.btPngSignature[3] != 0x1A0A) {
Warning("File is not a PNG image.");
return -1;
}
تنها تغییری که در قالب باینری (binary template) برای تولید امضا (signature) صورت گرفت، افزودن دو فراخوانی از تابع ()SetEvilBit بود. این فراخوانیها به طور موقت تصمیمهای نادرست (evil decisions) را در زمان تولید مقدار sig غیرفعال میکنند و پس از آن دوباره آنها را فعال میسازند.
این کار عملاً تولید متغیر sig را به صورت «سختگیرانه» (strict) علامتگذاری میکند؛ موضوعی که مفید است، زیرا هر فایلی که دارای امضای نادرست PNG باشد، یک فایل PNG معتبر محسوب نمیشود و بلافاصله توسط برنامه هدف رد خواهد شد.
۳.۲ فیلدهای اندازه (Size Fields)
یکی دیگر از ویژگیهای رایج در قالبهای باینری، وجود فیلدهای اندازه (size fields) است؛ یعنی مقادیری که نشان دهنده اندازه یک ساختار خاص در فایل، یا یک موقعیت یا اندیس مشخص درون فایل هستند. این نوع فیلدها ویژگیهایی وابسته به زمینه (context-sensitive) محسوب میشوند و قالبهای باینری که از آنها استفاده میکنند، با دستور زبانهای مستقل از متن (context-free grammars) قابل بیان نیستند. اهمیت تولید صحیح فیلدهای اندازه بسیار بالاست، زیرا مقدار نادرست برای این فیلد میتواند به طور کامل تفسیر بایتهای بعدی فایل را تغییر دهد.
همان طور که در خط ۲ «لیست ۵» مشاهده میشود، در قالب PNG، طول هر قطعه (chunk) به عنوان اولین فیلد در هر قطعه قرار دارد. در اینجا، تعریف متغیر length با فراداده (metadata) اضافی min=1 و max=16 گسترش یافته است تا دامنه انتخاب مقادیر این متغیر به یک بازه کوچک محدود شود. این موضوع، حائز اهمیت است، چرا که این متغیر به عنوان طول یک آرایه استفاده میشود، همان طور که در خط ۱۱ «لیست ۵» مشاهده میشود. اگر چنین کرانهایی مشخص نشوند، FormatFuzzer همچنان مقادیر متغیرهای صحیح (integer) را از یک توزیع سوگیرانه (skewed distribution) نمونهبرداری میکند که اعداد صحیح کوچک و مثبت را ترجیح میدهد، اما در عین حال با احتمال کمتر اجازه تولید اعداد بزرگ یا منفی را نیز میدهد.
یکی از چالشها در تولید قطعه (chunk) یا قطعههای PNG -که در بسیاری از قالبهای فایل دیگر نیز رایج است- این میباشد که فیلد طول (length field) قبل از دادهای قرار میگیرد که این طول به آن اشاره میکند. در نتیجه پیش از آن که خود آن دادهها تولید شوند، نمیتوان همیشه از قبل پیشبینی کرد که طول برخی دادهها دقیقاً باید چه مقداری باشد.
علاوه بر این، یک انتخاب نامناسب برای مقدار طول میتواند باعث شود اندازهی یک آرایه بیش از حد بزرگ یا حتی منفی شود؛ برای مثال، زمانی که یک سرریز مثبت و منفی عدد صحیح (integer Overflow\underflow) در خط ۴ «لیست ۶» رخ میدهد.
استراتژی ما برای حل این مشکلات این است که مقدار اولیهای که برای length نمونهبرداری میشود فقط به عنوان یک راهنما (hint) در نظر گرفته شود. بنابراین به عنوان مثال اگر هنگام تعریف آرایهی frame_data چنین سرریز منفی رخ دهد، ما مقدار length را نادیده میگیریم و به جای آن یک عدد صحیح کوچک و مثبت را به عنوان طول آرایه انتخاب میکنیم. این رفتار میتواند در قالب باینری (binary template) با فراخوانی تابع ()ChangeArrayLength فعال شود.
اکنون، از آنجا که مقدار انتخاب شده برای length تنها به عنوان یک راهنما (hint) استفاده میشود، ممکن است دادههای تولید شده در نهایت بزرگتر یا کوچکتر از مقدار مورد انتظار شوند. این مسئله باید اصلاح شود، زیرا در غیر این صورت، قطعه (chunk) دارای طولی متفاوت از مقداری خواهد بود که در متغیر length مشخص شده است؛ و در نتیجه تمام قطعههای (chunk) بعدی به صورت نادرست تفسیر خواهند شد.
لیست ۵. تعریف یک تکه PNG، که در آن میتوانیم تغییرات را برای اصلاح طول قطعه (chunk) و crc ببینیم:
typedef struct {
uint32 length ;
local int64 start = FTell();
char type[4];
if (type == "IHDR")
PNG_CHUNK_IHDR ihdr;
else if (type == "PLTE")
PNG_CHUNK_PLTE plte(length);
/* ... */
else if (length > 0 && type != "IEND")
ubyte data[length];
local int64 end = FTell();
local uint32 correct = end - start - 4;
if (length != correct) { /* Fix it */
FSeek(start - 4);
local int evil = SetEvilBit(false);
uint32 length = { correct };
SetEvilBit(evil);
FSeek(end);
}
local uint32 crc_calc = Checksum(CHECKSUM_CRC32, start, end-start);
uint32 crc = { crc_calc };
if (crc != crc_calc)
Warning("Bad CRC %
} PNG_CHUNK;
لیست ۶. شمارههای ترتیبی (Sequence numbers) نیازمند یک شمارندهی سراسری هستند:
local uint32 seq_num = 0;
struct PNG_CHUNK_FDAT {
uint32 sequence_number = { seq_num++ };
ubyte frame_data[length-4];
};
مقدار length صحیح است. همانطور که در خطوط ۱۳ تا ۲۰ «لیست ۵» نشان داده شده است، در صورت وجود هرگونه عدم تطابق، به عقب بازمیگردیم و مقدار length را با مقدار صحیح بازنویسی میکنیم. در اینجا، تابع ()FSeek برای پرش به یک موقعیت دیگر در فایل استفاده میشود.
توجه داشته باشید که هنگام اصلاح مقدار length، تصمیمهای نادرست (evil decisions) به طور موقت غیرفعال میشوند، زیرا هر مقدار اشتباه میتواند به طور کامل درخت تجزیه (parse tree) فایل را تغییر دهد. چنین اصلاحات رایج مربوط به اندازه (size fixes) میتواند با استفاده از یک ماکرو به نام ()FIX_VARIABLE سادهسازی شود، به طوری که کل این بلوک کد با یک فراخوانی از این ماکرو جایگزین گردد.
// arguments: type , name , correct value , position
FIX_VARIABLE(uint32, length, end - start - 4, start - 4);
۳.۳ قیود و محدودیتهای پیچیده (Complex Constraints)
در ادامه، نحوهی برآوردهسازی برخی قیود پیچیدهتر بین متغیرها در قالب (template) را بررسی میکنیم.
رابطه بین متغیرها (Relationship between variables). یکی از الگوهای رایج زمانی رخ میدهد که مقدار یک متغیر x، دامنهی مقادیر مجاز برای متغیر دیگر y را محدود میکند. چنانچه x ابتدا تولید شود، میتوان به سادگی از مقدار آن در تصمیمهای جریان کنترل (control-flow) یا لیستهای مقداردهی اولیه استفاده کرد تا نحوه تولید y تعیین شود. برای مثال، یک قطعه (chunk) در قالب PNG از نوع “IEND” باید طول (length) برابر صفر داشته باشد. این موضوع میتواند با استفاده از مقدار type در یک تصمیم جریان کنترل در خط ۱۰ «لیست ۵» تضمین شود؛ بهگونهای که اگر نوع برابر “IEND” باشد، هیچ دادهای تولید نگردد.
در صورتی که x تنها پس از y تولید شود، همچنان میتوان ابتدا مقدار x را با استفاده از یک تابع lookahead بررسی کرد. «لیست ۳» پیادهسازی chunk IHDR را نشان میدهد، جایی که مقادیر ممکن برای متغیر bits توسط مقدار متغیر بعدی یعنی color_type محدود میشود.
با استفاده از ()ReadByte در تابع lookahead در خط ۹، میتوان ابتدا مقدار موجود در موقعیت FTell()+1 (یعنی یک بایت بعد از موقعیت فعلی) را مشاهده کرد. این مقدار همان color_type خواهد بود و میتواند در یک دستور switch برای تعیین مجموعه مقادیر مجاز برای bits مورد استفاده قرار گیرد.
در حالت تولید (generation)، تابع ()ReadByte از آرایهی colors که به عنوان آرگومان به آن داده شده است استفاده میکند تا مشخص کند چه مقداری باید در موقعیت FTell()+1 در فایل تولید شود. پس از آن که یک مقدار توسط تابع lookahead انتخاب گردید، این مقدار ثابت (fixed) میشود و دیگر قابل بازنویسی نیست. بنابراین زمانی که متغیر color_type بعداً در خط ۲۱ اعلان میشود، تضمین خواهد شد که همان مقداری را داشته باشد که پیشتر توسط ()ReadByte انتخاب شده است.
جمعآزماها (Checksum). جمعآزماها یکی از ویژگیهای رایج و وابسته به زمینه (context-sensitive) در قالبهای باینری هستند. خوشبختانه، زبان قالب باینری (binary template) از قبل یک تابع کمکی به نام ()Checksum ارائه میدهد که الگوریتمهای رایج جمعآزما (Checksum) را پیادهسازی میکند. بنابراین همانطور که در خط ۲۲ از «لیست ۵» نشان داده شده است، میتوان جمعآزماها را به صورت ساده با محاسبه مقدار صحیح جمعآزما و تعیین آن به عنوان مقدار مورد انتظار برای متغیر جمعآزما مدیریت کرد.
وضعیت سراسری (Global state). برخی ویژگیها، مانند شمارههای ترتیبی (sequence numbers) که هر بار افزایش مییابند، برای تولید صحیح نیازمند نوعی وضعیت سراسری (global state) هستند. این مسئله را میتوان با تعریف یک متغیر محلی (local) در سطح سراسری (global scope) حل کرد، همانطور که در «لیست ۶» نشان داده شده است.
۳.۴ ترتیب قطعهها (Chunk Ordering)
یکی از چالشهای مهم در تولید فایلهای معتبر، انتخاب یک دنباله معتبر از انواع قطعهها (chunk) است. مجموعه انواع ممکن برای قطعه بعدی معمولاً وابسته به زمینه (context-sensitive) است و به این بستگی دارد که چه قطعههایی قبلاً تولید شدهاند. برای مثال در قالب PNG، قطعههای IHDR، IDAT و IEND اجباری هستند.
- اولین قطعه باید حتماً IHDR باشد.
- آخرین قطعه باید حتماً IEND باشد.
- برخی قطعههای اختیاری مانند tIME میتوانند هم قبل و هم پس از IDAT ظاهر شوند.
- برخی دیگر مانند bKGD فقط میتوانند پیش از IDAT قرار گیرند.
اگر فقط هدف، تجزیه کردن (parsing) فایلهای PNG (و نه تولید آنها) باشد، پیادهسازی زیر برای تحلیل یک دنباله از قطعهها (chunk) کافی است.
while (!FEof())
PNG_CHUNK chunk;
با این حال، برای تولید یک دنباله معتبر از قطعههای PNG، ما به روشی نیاز داریم تا بتوان مجموعهای از انتخابهای ممکن را مشخص کرد و اجازه داد این انتخابها برای هر قطعه (chunk) جدید تغییر کنند. به منظور حل این مشکل، تابع lookahead یعنی ()ReadBytes را با ۳ آرگومان جدید گسترش دادهایم:
- یک آرایه از مقادیر ترجیحی (preferred values)
- یک آرایه از مقادیر محتمل (possible values)
- یک احتمال برای انتخاب از میان مقادیر ترجیحی
همچنین عملگرهای جدید =- و =+ اضافه شدهاند که میتوانند مقادیر را به چنین آرایههایی اضافه یا از آنها حذف کنند. «لیست ۷» یک قالب باینری (binary template) را نشان میدهد که میتواند یک دنباله از قطعههای PNG را تولید کند که محدودیتهای ترتیب (ordering constraints) را رعایت میکند.
در اینجا، تابع ()ReadBytes در خط ۵ باید مقدار ۴ بایتی را انتخاب کند که در موقعیت FTell()+4 در فایل نوشته خواهد شد؛ یعنی همان جایی که آرایه type در قطعه (chunk) بعدی قرار دارد. مقدار انتخاب شده همچنین در متغیر chunk_type که به صورت ارجاعی به تابع ارسال شده است، بازگردانده میشود.
آرایهی ترجیحی (preferred array) برای مشخص کردن این است که قطعه (chunk) الزامی بعدی کدام است، در حالی که آرایهی ترجیحی شامل تمام قطعههایی است که میتوانند در موقعیت بعدی درج شوند.
با احتمال 0.25 تابع ()ReadBytes تلاش میکند مقداری را از آرایهی ترجیحی انتخاب کند تا مطمئن شود فرایند تولید به سمت تکمیل شدن پیش میرود و زمان زیادی صرف ساخت تعداد زیادی قطعه (chunk) اختیاری نمیشود.
با احتمال مکمل یعنی 1−0.25، تابع ()ReadBytes مقداری را از آرایهی محتمل (possible array) انتخاب میکند (یا حتی در صورت انجام یک تصمیم اشتباه و نادرست، یک مقدار کاملاً تصادفی را برمیگزیند).
اگر آرایهی ترجیحی خالی باشد، ()ReadBytes میتواند یک مقدار از آرایهی محتمل (possible array) انتخاب کند، اما همچنین مجاز است هیچ مقداری انتخاب نکند، هیچ مقداری در فایل ننویسد و مقدار false را بازگرداند و از حلقهی while خارج شود. این وضعیت نشان میدهد که فایل اکنون کامل شده است و دیگر هیچ قطعه (chunk) جدیدی به آن اضافه نخواهد شد.
با عدم وجود تصمیمهای نادرست (evil decisions)، تضمین میشود که یک ترتیب معتبر از قطعهها تولید شود. اولین قطعه باید IHDR باشد، زیرا در خطوط ۳ و ۴ تنها گزینهی ممکن همین مقدار تعریف شده است.
اگر یک IHDR با مقدار color_type = GrayScale تولید شود، قطعه ترجیحی بعدی IDAT خواهد بود (خط ۱۱)، در حالی که چندین قطعه ممکن دیگر نیز در دسترس هستند (خط ۱۲)، از جمله tIME و bKGD.
پس از تولید IDAT، قطعه بعدی ترجیحی IEND خواهد بود (خط ۱۸) و آرایهی گزینههای ممکن با استفاده از عملگرهای =- و =+ در خطوط ۱۹ و ۲۰ به روزرسانی میشود تا مقادیر جدید حذف یا اضافه شوند. برای مثال، قطعه bKGD حذف میگرددد، چرا که پس از IDAT اجازهی حضور آن وجود نخواهد داشت. در نهایت، زمانی که یک IEND تولید میشود، هر دو آرایه خالی خواهند شد (خطوط ۲۴ و ۲۵) تا نشان دهند که تابع ()ReadBytes باید مقدار false را بازگرداند و حلقه متوقف شود.
پیادهسازی ما از تابع ()ReadBytes اجازه میدهد همان حلقهی while که در «لیست ۷» آمده است، هم برای تجزیه (Parsing) و هم برای تولید (generation) مورد استفاده قرار گیرد. در حالت تجزیه، میتوان به راحتی بررسی کرد که آیا بایتهای خوانده شده از فایل متعلق به آرایهی ترجیحی هستند، یا در آرایهی محتمل قرار دارند، یا اینکه در هیچکدام از این دو مجموعه نیستند (برای مثال، اگر موقعیت این بایتها فراتر از انتهای فایل باشد).
این امکان باعث میشود بتوانیم به طور موفقیتآمیز تصمیمهای تصادفی را که برای تولید چنین فایلی لازم بودهاند بازسازی کنیم. رویکرد ما در استفاده از ()ReadBytes برای تعریف ترتیب قطعهها (chunk) بسیار کلی و عمومی از آب درآمده است، بهطوری که در پنج قالب توسعه یافته مورد استفاده قرار گرفته است: PNG، JPEG، MP4، ZIP و AVI.
۳.۵ دادههای فشرده و کدگذاری شده (Compressed and Encoded Data)
با تمام تغییراتی که تاکنون نشان داده شد، قالب باینری (binary template) اصلاح شدهی ما قادر است تقریباً تمام قطعههای (chunk) یک فایل PNG را به درستی تولید کند. تنها قطعه IDAT همچنان یک چالش است، زیرا شامل یک جریان داده (datastream) فشرده شده با zlib از دادههای پیکسل تصویر است.
برای مدیریت دادههای فشرده (یا به طور کلی کدگذاری شده)، باید توابع فشرده سازی (compression) و از حالت فشرده خارج شده (decompression) به گونهای مشخص شوند که مولد (generator) بتواند ابتدا دادههای غیرفشرده (uncompressed) را تولید و سپس تابع فشردهسازی (compression) را روی آن اعمال کند تا جریان دادهای که در فایل نوشته میشود به دست آید.
ما این رویکرد را به صورت دستی برای قطعه IDAT در PNG با فراخوانی توابع مناسب از کتابخانه zlib پیادهسازی کردهایم. این کار به FormatFuzzer اجازه میدهد PNGهای کاملاً معتبر (fully valid) تولید کند.
قالب PNG تا اینجا تنها قالبی بوده است که به این نوع مدیریت ویژه برای فشردهسازی نیاز داشته است، زیرا جالب است که در سایر قالبها مشاهده کردیم که تولید صرفاً بایتهای تصادفی برای جریانهای فشرده شده نیز به طور شگفتانگیزی اغلب کافی میباشد تا این جریانها با احتمال بالا به درستی قابل decompress یا از حالت فشرده خارج شدن باشند.
۴. پیادهسازی (IMPLEMENTATION)
FormatFuzzer در حوزهی فازرهای وابسته به زبان (language-specific fuzzers)، یک «کامپایلر مولد (generator compiler)» است که انعطافپذیری مشخصات زبان را با کارایی کد تولیدی کامپایل شده ترکیب میکند. FormatFuzzer یک قالب باینری (binary template) را به کد ++C برای تولید و تجزیه (parsing) ورودیها در قالب مشخصی کامپایل میکند. این کار با بهرهگیری از کتابخانههای موجود برای تحلیل [13] و پردازش [12] قالبهای باینری انجام میشود. کد ++C تولید شده برای یک قالب مشخص میتواند به یک اجرای مستقل (standalone executable) یا یک کتابخانه اشتراکی (shared library) تبدیل شود که توسط فازرهای دیگر مانند AFL++ [19] قابل بارگذاری است.
در این رویکرد، هر تعریف struct در قالب باینری به یک کلاس ++C تبدیل میشود. همچنین برای انواع دادهی پایهای (native types) مانند uint32 و آرایهها نیز کلاسهای ++C جداگانه ایجاد میشود. هر بار که یک متغیر در قالب تعریف میگردد، این عمل باعث فراخوانی متد ()generate مربوط به کلاس متناظر آن میشود. FormatFuzzer از تمام قابلیتهای مهم قالب باینری پشتیبانی میکند، از جمله:
- اندیسدهی به نمونههای قبلی یک متغیر
- structهای بازگشتی (recursive structs)، جایی که یک struct میتواند شامل نمونهای از همان نوع خودش باشد
همچنین از متغیرهای صحیح (integer) در حالتهای big-endian و little-endian پشتیبانی میشود، و نیز از bitfieldها که میتوانند تنها بخشی از یک بایت را اشغال کنند. پیادهسازی ما اجازه میدهد برای فایل تولید شده یک حداکثر اندازه (maximum size) بر حسب بایت تعیین شود. این موضوع در فازینگ اهمیت دارد، زیرا فایلهای کوچکتر هم سریعتر تولید میشوند و هم سریعتر توسط برنامهی هدف پردازش میشوند.
در آزمایشهای ما، حداکثر اندازه فایل روی ۶۴ کیلوبایت تنظیم شده است، چرا که مشاهده کردیم که حتی با فایلهایی در حد چند کیلوبایت نیز میتوان تمام انواع قطعهها (chunk) و ساختارهای داخلی را در فایلها ایجاد کرد. در حین تولید، هرگونه تلاش برای تولید دادهی بیش از حد —برای مثال تعریف یک آرایه با طول بسیار بزرگ— باعث ایجاد یک استثنا شده و فرآیند تولید را متوقف (abort) میکند. همچنین تولید در شرایط زیر نیز متوقف میشود:
- اگر مولد (generator) تلاش کند بیش از تعداد بایتهای موجود در بذر تصیم (decision seed) مصرف کند.
- اگر عملیات نامعتبری انجام دهد، مانند دسترسی به یک فیلد غیرموجود در یک struct.
حالت دوم ممکن است در برخی قالبها رخ دهد، زیرا تصمیمهای مربوط به جریان کنترل (control-flow) میتوانند تعیین کنند که کدام فیلدها داخل یک struct تولید شوند. ما همچنین در FormatFuzzer تستهای تضمین کیفیت (quality assurance) را یکپارچه کردهایم تا رفتار صحیح مولدها (generator) و تجزیه کنندههای (parser) خود را بررسی کنیم. ما از آزمون چرخه کامل (round-trip) استفاده میکنیم تا مطمئن شویم هر زمان که یک فایل با موفقیت تولید میشود، همان فایل میتواند به درستی نیز تجزیه (parse) گردد و بالعکس. همچنین اطمینان حاصل میکنیم که فازرهای ما قادر هستند به درستی یک مجموعه (corpus) از فایلهای واقعی مربوط به هر قالب را که به صورت دستی از GitHub جمعآوری شدهاند، پردازش و تحلیل کنند.
۵. بذرهای تصمیم (Decision Seeds)
روششناسی مورد استفاده در FormatFuzzer به این صورت است: برای تولید یک فایل، ابتدا یک آرایه از بایتهای تصادفی به آن داده میشود که به آن بذر تصمیم (decision seed) گفته میشود. این آرایه منبع تولید تصادفی بودن برای تصمیمهای مولد (generator) است. چنین بذرهای (seed) تصمیمی همچنین میتوانند توسط FormatFuzzer هنگام انجام عملیات تجزیه (parsing) بازسازی شوند.
۵.۱ استفاده از بذرهای تصمیم برای تولید (Using Decision Seeds for Generation)
هر بار که مولد (generator) نیاز به گرفتن یک تصمیم تصادفی دارد – مثلاً اینکه چه مقداری برای یک متغیر یا تابع lookahead استفاده شود – به تعداد لازم از بذر تصمیم (decision seed) بایت خوانده میشود. برای مثال، شکل ۳ یک بذر تصمیم ممکن و محتوای حاصل از فایل تولید شده برای PNG_CHUNK_IHDR (تعریفشده در لیست ۳) را نشان میدهد. برای هر یک از هفت متغیر تولید شده در این قطعه (chunk)، یک بایت تصمیم برای تعیین اینکه آیا باید یک تصمیم نادرست (evil decision) گرفته شود یا نه، استفاده میشود.
اگر بایت b0 خوانده شود، در صورتی یک تصمیم اشتباه (evil decision) گرفته میشود که b0 mod 128 = 127 باشد، که این اتفاق با احتمال 1/128 رخ میدهد. در حالت معمول، هیچ تصمیم اشتباهی اتخاذ نمیشود.
برای متغیرهای width و height که با <min=1, max=24> تعریف شدهاند، این بدان معناست که بایت بعدی b1 برای محاسبه b1 mod 24 استفاده خواهد شد تا به طور یکنواخت از بین 24 مقدار ممکن، یکی را انتخاب کند.
برای متغیرهای bits و color_type، فراخوانی تابع ()ReadByte در خط ۹ از «لیست ۳» به ما میگوید که ابتدا باید متغیر bits را نادیده بگیریم (از آن عبور کنیم) و سپس یک مقدار مناسب برای color_type از آرایهی colors انتخاب کنیم؛ آرایهای که شامل مقادیر معتبر برای نوع فضای رنگی PNG_COLOR_SPACE_TYPE است.
برای انجام این کار، ابتدا یک بایت تصمیم (decision byte) مصرف میکنیم که به عنوان یک پدینگ گذرا (temporary padding) برای بایتی که هنگام پرش به موقعیت FTell()+1 در فایل نادیده گرفته میشود، استفاده میگردد. سپس دو بایت تصمیم بعدی برای انتخاب مقدار در موقعیت FTell()+1 (که همان color_type خواهد بود) مصرف میشوند: یکی برای تعیین اینکه آیا یک تصمیم نادرست (evil decision) گرفته شود یا نه، و دیگری برای انتخاب یک مقدار مناسب از میان پنج گزینهی موجود در آرایهی colors. چنانچه دومین مقدار آرایه را انتخاب کنیم، که همان TrueColor با مقدار 2 است، آنگاه خط ۱۴ از «لیست ۳» مشخص میکند که دو مقدار ممکن برای متغیر bits وجود دارد: 8 یا 16. در نهایت، زمانی که به خط ۲۱ میرسیم که متغیر color_type را تعریف میکند، دیگر نیازی به مصرف هیچ بایت تصمیم اضافی نداریم، زیرا مقدار آن قبلاً در فراخوانی قبلی ()ReadByte انتخاب و تثبیت شده است.
با توجه به یک بذر تصمیم (decision seed)، فرایند تولید کاملاً قطعی (deterministic) است. این بذر تصمیم، ویژگیهای اساسی یک فایل مشخص را در قالبی کدگذاری میکند که برای فازینگ و با هدف کاوش جامع و کامل ساختار فایل طراحی شده است. ارتباط بین بذرهای تصمیم و فایلهای تولید شده را میتوان بهصورت یک تابع جزئی در نظر گرفت:
f : Seeds → Files
برای برخی از مقادیر بذر (seed)، فرایند تولید ممکن است با شکست مواجه شود. برای مثال، فرض کنید یک تصمیم نادرست (evil) گرفتهایم، مانند انتخاب یک color_type نامعتبر در قطعه PNG IHDR. این موضوع باعث میشود که در ادامه تولید داده برای یک قطعه دیگر غیرممکن شود. اما اگر فرایند تولید موفقیتآمیز باشد، آنگاه بذر s را به یک فایل یکتای مشخص f(s) نسبت میدهد. با این حال تضمین نمیشود که فایل حاصل f(s) کاملاً مطابق با مشخصات قالب باشد (برای مثال به دلیل تصمیمهای نادرست)، اما با احتمال بالا معتبر است. این نگاشت f یکبهیک (injective) نیست، اما در حالت ایدهآل باید هر فایل معتبر قابل تولید باشد؛ یعنی Valid ⊆ f(Seeds).
این ویژگی تمامیت و کامل بودن (completeness) را میتوان به صورت تجربی بررسی کرد؛ به این صورت که فایلهای معتبر —مثلاً فایلهای دانلودشده از اینترنت— با FormatFuzzer تلاش شوند تا تجزیه (Pars) شوند. همانطور که در بخش بعد توضیح داده میشود، هر زمان که تجزیه با موفقیت انجام شود، ما همچنین یک بذر تصمیم (decision seed) برای آن فایل به دست میآوریم. این یعنی آن فایل میتواند با استفاده از تصمیمهای مناسب، به عنوان خروجی مولد در FormatFuzzer نیز تولید شود.
۵.۲ ایجاد بذرهای تصمیم هنگام تجزیه (Creating Decision Seeds During Parsing)
زمانی که FormatFuzzer در حالت تجزیه (parsing) اجرا میشود، ما نه تنها درخت تجزیه (parse tree) مربوط به فایل ورودی را به دست میآوریم، بلکه یک بذر تصمیم (decision seed) نیز تولید میکنیم که میتواند برای تولید همان فایل ورودی استفاده شود؛ همانطور که در نمودار شکل ۲ نشان داده شده است.
این کار با بازسازی بایتهای تصمیم که برای تولید محتوای فایل هدف در هر مرحله مورد نیاز هستند، انجام میشود. برای مثال، در شکل ۳، بایتهای تصمیم در سمت چپ دقیقاً همانهایی هستند که با تجزیه محتوای فایل در سمت راست به دست میآیند.
برای هر متغیر، بررسی میکنیم که آیا یک تصمیم نادرست (evil decision) لازم است یا خیر؛ این کار با این بررسی انجام میشود که آیا مقدار آن متغیر در میان مقادیر ممکن (possible values) که از قبل مشخص شدهاند قرار دارد یا خیر.
در حالت رایج، نیازی به تصمیم نادرست نیست و میتوان بایت تصمیم بعدی را به صورت شاخص (index) مقدار انتخاب شده از آرایهی مقادیر ممکن بازسازی کرد. این بذر تصمیم بازسازی شده یکتا نیست. برای مثال، برای متغیرهای width و height تعداد ۲۴ مقدار معتبر وجود دارد؛ بنابراین هر بایت تصمیمی که در یک کلاس همنهشتی (congruence class) نسبت به ۲۴ باشد، منجر به تولید همان مقدار خواهد شد.
یکی از اصول مهم چارچوب ما این است که یک فایل زمانی و تنها زمانی میتواند توسط FormatFuzzer با موفقیت تجزیه (parse) شود که بتوان آن را از یک بذر تصمیم (decision seed) مناسب تولید کرد. همچنین مولد (generator) و تجزیه کننده (parser) باید همیشه روی یک درخت تجزیه (parse tree) یکسان برای فایل توافق داشته باشند.
به همین دلیل، هنگام اصلاح فیلد length در خط هفدهم «لیست ۵»، نمیتوانیم اجازهی تصمیمهای نادرست را بدهیم. چنانچه مولد مقدار length را با یک مقدار نادست بازنویسی کند، مولد و تجزیه کننده دربارهی موقعیت شروع قطعه (chunk) بعدی دچار اختلاف خواهند شد.
به طور کلی، تنها متغیرهایی که اندازهها یا موقعیتها را در فایل تعیین میکنند (همانطور که در بخش ۳.۲ توضیح داده شد) باید بهصورت کاملاً سختگیرانه (strict) و بدون استفاده از تصمیمهای اشتباه تولید شوند. آزمایشهای چرخه کامل (round-trip) ما نشان دادهاند که فایلهای تولید شده حتی زمانی که به همهی متغیرهای دیگر اجازهی داشتن مقدار اشتباه داده میشود، همچنان میتوانند به درستی تجزیه شوند (و بالعکس).
۶. استراتژیهای فازینگ (FUZZING STRATEGIES)
مولدها و تجزیه کنندههای FormatFuzzer میتوانند در چندین استراتژی مختلف برای فازینگ مورد استفاده قرار گیرند. در این بخش چند مورد از آنها را بررسی میکنیم.
۶.۱ تولید فایلهای تصادفی (Generating Random Files)
سادهترین رویکرد برای فازینگ با استفاده از FormatFuzzer این است که از بذرهای کاملاً تصادفی (مانند /dev/urandom) برای تولید فایلها به صورت جعبه سیاه (black-box) استفاده شود. بخش قابل توجهی از فایلهای تولید شده در این حالت مطابق با مشخصات قالببندی (format specification) معتبر خواهند بود و از نظر ویژگیهای معنایی (semantic) نیز توزیع متنوعی از قابلیتها را پوشش میدهند. برای مثال، همانطور که در «لیست ۳» توضیح داده شده است، تمام مقادیر ممکن برای color_type با احتمال یکسان انتخاب میشوند، و همینطور مقادیر ممکن برای bits (با توجه به color_type مشخص) نیز به صورت یکنواخت انتخاب خواهند شد.
۶.۲ جهش ورودیها (Mutating Inputs)
یکی دیگر از استراتژیهای فازینگ که توسط FormatFuzzer امکانپذیر شده است، استفاده از جهشهای هوشمند (smart mutations) است؛ در این روش، «قطعهها (chunk)» میتوانند در یک فایل انتزاع، جایگزین، حذف یا درج شوند، همانطور که در شکل ۴ نشان داده شده است. در اینجا، واژهی «قطعه» به صورت کلی برای اشاره به هر struct یا متغیری که در قالب باینری تعریف شده است به کار میرود. ما مجموعهای از عملیاتهای جدید برای جهشهای هوشمند تعریف میکنیم که بر روی بذرهای تصمیم (decision seed) عمل میکنند؛ به این ترتیب، امکان در نظر گرفتن اطلاعات زمینهای (contextual information) هنگام تولید فایل جهش یافته فراهم میشود.
۶.۲.۱ انتزاع هوشمند (Smart Abstraction)
نخستین عملیاتی که بررسی میکنیم، انتزاع هوشمند است که امکان تبدیل یک قطعه (chunk) مشخص c1 به یک نسخهی تصادفی جدید c2 را فراهم میکند. این عملیات به این صورت انجام میشود که ابتدا فایل اصلی تجزیه شده و بذر تصمیم آن به دست میآید. سپس با تکرار تمام تصمیمهایی که پیش از قطعه هدف c1 گرفته شدهاند، یک فایل جدید تولید میکنیم و برای تولید قطعه هدف، از بایتهای تصادفی منبع /dev/urandom استفاده میشود. پس از آنکه تشخیص دادیم قطعه هدف تولید شده است، مجدداً از همان بایتهای تصمیمی استفاده میکنیم که در فایل اولیه پس از c1 به کار رفته بودند.
بررسیهای ما نشان میدهد که انتزاعهای هوشمند بیشترین کارایی را در دستیابی به پوششهای جدید داشتهاند. این قابلیت تنها به لطف چارچوب FormatFuzzer امکانپذیر است، چرا که این چارچوب FormatFuzzer، امکان تولید (generation) و تجزیه (parsing) همگام را فراهم میکند.
۶.۲.۲ جایگزینی هوشمند (Smart Replacement)
عملگر بعدی، جایگزینی هوشمند (smart replacement) است که در آن یک قطعه (chunk) به نام c1 از فایل اول (file1) با یک قطعه دیگر c2 از همان نوع، اما از فایل دوم (file2) جایگزین میشود. در اینجا نیز ابتدا هر دو فایل را تجزیه (Pars) میکنیم تا بذرهای تصمیم متناظر آنها، یعنی seed1 و seed2، به دست آید. سپس یک بذر جهش یافته (mutated seed) ساخته میشود؛ به این صورت که بایتهای تصمیمی که قطعه c1 را تولید کردهاند با بایتهایی جایگزین میشوند که قطعه c2 را تولید میکنند، در حالی که سایر بخشهای بذر همانطور که در عملگر جایگزینی هوشمند در شکل ۴ نشان داده شده است، بدون تغییر باقی میمانند.
این بذر جهش یافته سپس توسط مولد (generator) استفاده میشود تا فایل جهشیافته تولید گردد. برای درک اینکه چرا انجام این عملیات روی بذرهای تصمیم (decision seed) به تولید فایلهای معتبر کمک میکند، فرض کنید c1 یک فیلد داخلی از یک قطعه در PNG باشد. اگر c1 با نمونهی جدید c2 از همان نوع اما با مقدار و اندازهی متفاوت جایگزین شود، مولد همچنان میتواند جمعآزما (checksum) و اندازهی صحیح را برای قطعه اصلاح شده محاسبه کند. در حالی که اگر صرفاً محتوای c2 را مستقیماً در c1 کپی کنیم، مقدار جمعآزما و اندازه نامعتبر باقی خواهند ماند.
این موضوع به ویژه برای انجام موفق جهشهای هوشمند در قالبهایی مانند MP4 اهمیت دارد، که از ساختارهایی به نام box تشکیل شدهاند و این boxها بهصورت بازگشتی شامل boxهای دیگر هستند.
۶.۲.۳ حذف هوشمند (Smart Deletions)
عملگر حذف هوشمند (smart delete) شامل حذف یک قطعه (chunk) از فایل است؛ این کار با حذف بایتهای تصمیمی انجام میشود که مسئول تولید آن قطعه بودهاند. چنین عملیاتی تنها زمانی معنادار است که تشخیص دهیم قطعه هدف c یک قطعه اختیاری (optional) است.
ما یک قطعه را اختیاری در نظر میگیریم اگر درست قبل از تولید آن، در قالب یک تابع lookahead فراخوانی شده باشد. این نشان میدهد که بسته به نتیجهی آن lookahead، ممکن است تصمیم گرفته شود که اصلاً قطعه c تولید نشود.
برای مثال، متغیر قطعه که در خط ۶ از «لیست ۷» تعریف شده است، اختیاری در نظر گرفته میشود، زیرا تعریف آن بعد از یک فراخوانی به ()ReadBytes آمده است. ما اجازه میدهیم حذف هوشمند فقط زمانی روی یک قطعه c انجام شود که یک تابع lookahead هم قبل و هم بعد از تولید آن فراخوانی شده باشد (همانطور که در شکل ۴ نشان داده شده است). به این ترتیب، در بذر جهش یافته جدید، فراخوانی تابع lookahead بایتهایی را مصرف خواهد کرد که در حالت اولیه، در فراخوانی lookahead بعد از قطعه c مصرف شده بودند.
۶.۲.۴ درج هوشمند (Smart Insertions)
در مقابل، درج هوشمند (smart insert) به عنوان عمل معکوس حذف هوشمند تعریف میشود و هدف آن افزودن یک قطعه جدید c به فایل است. در این حالت، فایل اولیه باید در موقعیت درج، یک فراخوانی به تابع lookahead داشته باشد، به طوری که نتیجهی این فراخوانی بتواند برای تصمیمگیری در مورد ایجاد یا عدم ایجاد قطعه c استفاده شود. همچنین قطعه درج شده c نیز باید از نوع اختیاری (optional) باشد.
هنگام انجام درج و جایگزینی هوشمند، FormatFuzzer بررسی میکند که آیا تعداد صحیحی از بایتهای تصمیم در فرآیند تولید قطعه جدید مصرف شده است یا خیر. چنانچه جهشها به خوبی عمل نکنند —برای مثال زمانی که قطعه جدید در موقعیت موردنظر قرار نمیگیرد— این موضوع توسط FormatFuzzer شناسایی و گزارش میشود.
برای نمونه، تلاش برای کپی یک قطعه از نوع bKGD (که رنگ پسزمینه را مشخص میکند) ممکن است با مشکل مواجه شود؛ مثلاً اگر یک فایل از نوع رنگی TrueColor استفاده کند (که به سه مقدار نیاز دارد) و فایل دیگر از نوع GrayScale باشد (که تنها به یک مقدار نیاز دارد).
۶.۲.۵ عملیات بینفایلی (Cross-File Operations)
FormatFuzzer رویههایی را برای تجزیه (parse) یک فهرست از فایلها پیادهسازی میکند و تمام اطلاعات مربوط به قطعهها را ذخیره میکند تا بتوان بعداً از آنها برای جهشهای هوشمند (smart mutations) استفاده کرد.
همچنین، یک رویه برای اعمال یک جهش هوشمند روی یک فایل انتخاب شده وجود دارد. در اینجا، به صورت تصادفی تعیین میکنیم که چه نوع جهشی اعمال شود و کدام قطعهها در این فرآیند درگیر باشند.
۶.۳ یکپارچهسازی با فازرهای مستقل از قالببندی (Integrating with Format-Agnostic Fuzzers)
FormatFuzzer علاوه بر تولید (generation) و جهش (mutation)، میتواند به روشهای مختلف با فازرهای موجودِ مستقل از قالببندی، مانند AFL [58] یکپارچه شود. ما برای آزمایشهای خود، FormatFuzzer را با AFL++ [19] نسخه 2.60c بهصورت زیر یکپارچه کردهایم.
AFL+FFGen از AFL برای جهش (mutate) و تکامل (evolve) بذرهای تصمیم (decision seeds) استفاده میکند؛ این بذرها سپس به مولدِ FormatFuzzer داده میشوند تا ورودیهایی برای برنامه هدف تولید شود. AFL میتواند از بازخورد پوشش (coverage feedback) برنامه استفاده کند تا یاد بگیرد چگونه بذرهای تصمیم را به طور مؤثر جهش دهد. مزیت کار با چنین بذرهایی این است که نسبت به فایلهای باینری سادهتر هستند، زیرا هر بایت متناظر با یک تصمیم منحصربهفرد است و این تصمیمها به صورت ترتیبی و به همان ترتیبی که در بذر ظاهر میشوند اتخاذ میگردند. از آنجا که مولدِ FormatFuzzer از قبل مسئول رسیدگی به ساختار صحیح ورودی است -مانند محاسبه جمعآزماهای (Checksum) صحیح، درج مقادیر جادویی (magic values) مناسب و تنظیم فیلدهای اندازه به صورت صحیح- AFL میتواند به طور انحصاری بر تصمیمات سطح بالا که نمایانگر فایل هستند تمرکز کند.
AFL+FFMut به AFL اجازه میدهد فایلهایی را که قرار است به برنامه هدف داده شوند مانند حالت معمول جهش دهد، اما در عین حال عملیات جهش جدیدی نیز اضافه میکند که همان جهشهای هوشمند (smart mutations) ارائه شده توسط FormatFuzzer هستند. هر ورودی جالب (interesting input) که پوشش جدیدی (new coverage) را بهدست آورد، در صف AFL ذخیره میشود. زمانی که چنین ورودی قرار است برای فازینگ جهش داده شود، ما آن را با FormatFuzzer نیز تجزیه میکنیم، به طوری که امکان استفاده از آن برای جهشهای هوشمند فراهم شود.
۷. ارزیابی (EVALUATION)
ارزیابی ما بر روی پرسشهای پژوهشی زیر متمرکز است:
- پرسش اول: میزان تلاش لازم جهت راهاندازی یک فایل قالب باینری (binary template file) برای تولید ورودی چقدر است؟
- پرسش دوم: FormatFuzzer تا چه حد در تولید ورودیها کارآمد است؟
- پرسش سوم: FormatFuzzer تا چه حد در تولید ورودیها دقیق است؟
- پرسش چهارم: جهشهای هوشمند اعمال شده توسط FormatFuzzer تا چه حد دقیق هستند؟
- پرسش پنجم: FormatFuzzer به عنوان یک فازر جعبه سیاه مستقل (standalone black-box fuzzer) تا چه حد کارآمد است؟
- پرسش ششم: یکپارچهسازی FormatFuzzer با یک فازر مستقل از قالببندی (format-agnostic fuzzer) تا چه حد کارآمد است؟
- پرسش هفتم: FormatFuzzer در مقایسه با سایر فازرهای آگاه از قالببندی (format-aware fuzzers) چگونه عمل میکند؟
- پرسش هشتم: آیا FormatFuzzer باگهای واقعی را پیدا میکند؟
برای پاسخ به این پرسشها، مجموعهای از آزمایشها را اجرا کردیم. تمام آزمایشهای ما روی یک ماشین مجهز به پردازنده Intel Xeon E5-4650L با ۶۴ هسته و ۷۵۶ گیگابایت RAM، با سیستمعامل Debian 10 انجام شد. فازرها روی یک پردازنده (single processor) به مدت ۲۴ ساعت اجرا شدند. ما هر آزمایش را در مجموع ۱۰ بار تکرار کردیم.
جدول ۱. تعداد خطوط کد مورد نیاز برای هر قالب:
ما در ارزیابی خود، FormatFuzzer را با فازر مستقل از قالببندی AFL++ [19] و همچنین فازر پیشرفتهی قالبهای فایل باینری یعنی AFLSmart [39] مقایسه میکنیم. ما فازرهای مبتنی بر دستور زبان مانند Superion [53]، Nautilus [3] و Grimoire [5] را در این مقایسه وارد نکردهایم، زیرا این ابزارها عمدتاً فقط بر روی قالبهای ورودی متنی مبتنی بر دستور زبان —مانند زبانهای نشانهگذاری (XML) یا زبانهای برنامهنویسی (JavaScript، PHP، Ruby، Lua، C، nasm، SQL، SMT)— ارزیابی شدهاند. این قالبهای متنی فاقد ویژگیهای رایج در قالبهای باینری هستند، مانند فیلدهای اندازه (size fields)، جمعآزماها (checksums) یا بیتفیلدها (bitfields).
بنابراین، مشخص نیست که چنین فازرهایی تا چه حد میتوانند این ویژگیها را مدیریت کنند. برای مثال، در مورد Superion و Nautilus، لازم است برای هر قالب باینری جدید، یک مشخصهی کامل قالب از ابتدا نوشته شود. از سوی دیگر، Grimoire قادر به استخراج (mine) مشخصات قالب است، اما تنها از زبانهای متنی پشتیبانی میکند، زیرا ورودیها را بر اساس کاراکترهای خاصی مانند پرانتزهای باز و بسته یا علامت نقلقول تقسیمبندی میکند. در مقالهی Grimoire نیز اشاره شده است که این ابزار میتواند جهشهای مؤثری بر روی کد منبع Lua اعمال کند، اما برای بایتکد Lua چنین قابلیتی ندارد.
ما برای ارزیابی، ۱۰ قالب رایج فایلهای باینری را انتخاب کردهایم؛ از جمله محبوبترین قالبها برای آرشیوهای فشرده (ZIP)، تصاویر (PNG، JPG) و ویدئوها (MP4)، به همراه چند قالب دیگر که توسط AFLSmart نیز پشتیبانی میشوند.
۷.۱ پاسخ به پرسش اول: میزان تلاش برای توسعه فایلهای قالب (Effort for Extending Template Files)
ما با پرسش اول آغاز میکنیم: میزان تلاش لازم به منظور راهاندازی یک فایل قالب باینری برای تولید ورودی چقدر است؟ برای پاسخ به این پرسش، جدول ۱ تعداد خطوط کد مورد نیاز برای هر مشخصات قالببندی (format specification) را فهرست میکند.
ما اندازههای قالب باینری اصلی و اولیه (original binary template) و نسخه اصلاح شدهای را که از تولید (generation) پشتیبانی میکند نیز ارائه میدهیم. همچنین تعداد خطوطی که اضافه یا حذف شدهاند را به طور دقیق مشخص میکنیم. برای بیشتر قالبها، تعداد تغییرات در مقایسه با حجم کدی که میتوان از قالبهای باینری موجودِ صرفاً تجزیه کننده (parsing-only binary templates) استفاده کرد، اندک است.
تمام قالبهایی که در جدول ۱ نشان داده شدهاند، به استثنای PNG، توسط سه دانشجوی کارشناسی (BSc) مختلف توسعه داده شدند که هیچ تجربه قبلی در زمینه قالبهای باینری نداشتند. پس از آن که آنها در حین ایجاد اولین قالب، زبان قالب باینری (binary template language) را یاد گرفتند، توسعه سادهتر شد، زیرا بسیاری از الگوها در چندین قالب مشترک هستند. بر اساس تجربه آنها، برای به روزرسانی بیشتر قالبها جهت پشتیبانی از تولید (generation)، چند روز کافی است. با این حال، چند قالب پیچیده بیش از یک هفته زمان برای اصلاح نیاز داشتند. این مورد درباره MP4 صادق بود، که از چندین نوع قطعه (chunk) مختلف تشکیل شده است و بسیاری از آنها به طور کامل در قالب باینری اولیه (original binary template) توصیف نشده بودند.
با این حال، باید توجه داشت که این میزان تلاش به سختی قابل اجتناب است: یک فازر تصادفی (random fuzzer) تنها به ندرت میتواند یک فایل MP4 معتبر تولید کند، چه برسد به اینکه بتواند به صورت نظاممند همه انواع قطعهها (chunk) را پوشش دهد؛ و یک تولید کننده MP4 که به صورت دستی پیادهسازی شده باشد نیز به همان میزان یا حتی تلاش بیشتری برای مشخصات (specification) نیاز خواهد داشت (و در هیچکدام از این دو حالت، نه یک تجزیه کننده (parser) و نه یک جهش دهنده (mutator) به دست نمیآید).
جدول ۲. عملکرد میانگین فازرها از نظر سرعت و اعتبار (Validity):
جدول ۱ تعداد خطوط کد ++C مختص هر قالببندی را که به صورت خودکار توسط FormatFuzzer تولید شدهاند، فهرست میکند. ما دریافتیم که استفاده از یک زبان دامنه-اختصاصی (domain-specific language) برای قالبهای باینری (binary templates) باعث میشود مشخصات ما بسیار خلاصهتر (succinct) و خواناتر (readable) از حالتی باشد که از ابتدا در یک زبان عمومی مانند ++C توسعه داده شدهاند.
تنها برای قالب PNG مجبور شدیم کد ++C را به صورت مستقیم ویرایش کنیم تا از فشردهسازی داده (data compression) پشتیبانی شود؛ در آینده قصد داریم پشتیبانی بومی (native support) برای فشردهسازی را به FormatFuzzer اضافه کنیم تا دیگر چنین تغییرات دستی مورد نیاز نباشد.
برای ارائه یک دید کلی، نویسنده اول این مقاله پیش از دیدن قالب دودویی PNG [45]، یک تولید کننده PNG را کاملاً از ابتدا و در ++C پیادهسازی کرده بود؛ این کار با مطالعه مشخصات PNG [14] انجام شده بود، سندی که حدود ۵۰ صفحه دارد. این پیادهسازی دستی شامل ۵۹۶ خط کد ++C بود، که به مراتب بیشتر از تعداد خطوطی است که با شروع از یک قالب دودویی PNG موجود و استفاده از چارچوب FormatFuzzer نیاز به تغییر داشت. و این تنها یک مولد (generator) بود، بدون هیچ قابلیت تجزیه کردن (parsing) یا جهش (mutation)، چه برسد به هدایت شدن توسط پوشش (coverage guidance).
نتیجهگیری ۱. توسعه دادن قالبهای باینری (binary templates) کار کمتری نسبت به نوشتن یک مولد (generator) از ابتدا نیاز دارد و علاوه بر آن، یک تجزیه کننده (parser) و جهش دهنده (mutator) نیز در اختیار قرار میدهد.
۷.۲ پاسخ به پرسش دوم: سرعت مولد (Generator Speed)
برای پاسخ به پرسش دوم (اینکه FormatFuzzer تا چه حد در تولید ورودیها کارآمد است؟)، ما سرعت مولدها (generators) و تجزیهکنندههای (parsers) خود را اندازهگیری کرده و اعتبار (validity) فایلهای تولید شده را با ارسال آنها به یک برنامه هدف ارزیابی میکنیم.
مسئله تعریف اعتبار فایلها میتواند تا حدی چالشبرانگیز باشد. برنامههای مختلفی که یک قالب مشخص را پردازش میکنند، معمولاً در مورد اینکه کدام ورودیها را میپذیرند با یکدیگر توافق ندارند و اغلب نیز از مشخصات رسمی پیروی نمیکنند. برنامههایی مانند پخشکنندههای رسانهای (media players) معمولاً انعطافپذیر هستند و حتی پس از تشخیص برخی خرابیهای داده (data corruption) نیز به پردازش ورودی ادامه میدهند.
ما در راستای تحقق هدف اصلی خود برای رسیدن به عمق بیشتری از کد در برنامه، تنها زمانی یک فایل را نامعتبر (invalid) در نظر میگیریم که یک خطای بحرانی (critical error) ایجاد کند که مانع از ادامه پردازش فایل شود. به عنوان مثال، برای تصاویر از دستور identify -verbose در ImageMagick استفاده کردیم و خروجی را بررسی نمودیم تا ببینیم آیا برنامه توانسته است اطلاعات دقیق تصویر را با موفقیت چاپ کند، حتی اگر برخی خطاها گزارش شده باشند.
جدول ۲ نتایج ما را خلاصه میکند. ما برای هر فازر آگاه به قالببندی (format-aware fuzzer)، ۱۰٬۰۰۰ فایل تولید کردیم، سپس آنها را تجزیه کرده و با یک برنامه هدف آزمایش نمودیم تا اعتبار آنها بررسی شود (validation command). در اینجا مشاهده میشود که FormatFuzzer میتواند هم تولید (generation) و هم تجزیه (parse) ورودیها را با سرعت هزاران نمونه در ثانیه برای تمام قالببندیهای آزمایش شده انجام دهد.
این سرعت قابل مقایسه است و گاهی حتی سریعتر از سرعتی است که فازرهای مستقل از قالببندی (format-agnostic fuzzers) مانند AFL میتوانند ورودیها را اجرا کنند. بنابراین، تولیدکنندهها و تجزیه کنندههای ما میتوانند به راحتی در فازرهای موجود ادغام گردند، بدون اینکه به گلوگاه عملکردی (performance bottleneck) تبدیل شوند.
نتیجهگیری ۲. FormatFuzzer بسیار سریع است و میتواند هزاران ورودی را در هر ثانیه تولید و تجزیه (parse) کند.
۷.۳ پاسخ به پرسش سوم: اعتبار و تصدیق ورودی (Input Validity)
جدول ۲ همچنین نشان میدهد چه کسری از تولیدها منجر به یک خروجی شدهاند (یعنی تولید با موفقیت کامل شده است) و چه کسری از آن خروجیها بهعنوان معتبر (valid) طبقهبندی شدهاند؛ و بدین ترتیب به پرسش سوم پاسخ میدهد: FormatFuzzer تا چه حد در تولید ورودیها دقیق است؟
نتایج حاکی از آن است که فازرهای ما تقریباً در همه موارد در تولید ورودیها موفق هستند و این ورودیها احتمال بالایی برای پذیرفته شدن توسط برنامه هدف دارند. میزان اعتبار حتی زمانی بالاتر است که تصمیمات نادرست (evil decisions) غیرفعال شوند. با این حال، تصمیمات نادرست به طور خاص در فازینگ مفید هستند، زیرا میتوانند برخی رفتارهای غیرمنتظره را تحریک کنند.
نتیجهگیری ۳. بخش عمدهٔ فایلهایی که توسط FormatFuzzer تولید میشوند معتبر (valid) هستند.
برای قرار دادن این میزان اعتبار در یک دیدگاه مقایسهای، آن را با ورودیهای تولید شده توسط فازر مستقل از قالب AFL که در ستون «AFL» گزارش شده است مقایسه میکنیم. مشاهده میشود که AFL حتی با وجود فایلهای نمونه (sample files)، تعداد بسیار کمتری ورودی معتبر تولید میکند. در واقع، اگر نسبت فایلهای معتبر تولید شده توسط FormatFuzzer را به فایلهای معتبر تولید شده توسط AFL محاسبه کنیم، میانگین ۴.۹ به دست میآید (با حداقل ۱.۲ و حداکثر ۳۶.۰).
نتیجهگیری ۴. FormatFuzzer به طور میانگین حدود پنج برابر ورودیهای معتبر بیشتری نسبت به یک فازر مستقل از قالببندی (format-agnostic fuzzer) تولید میکند.
در بخش بعدی، ما همچنین اعتبار (validity) ورودیهای تولید شده توسط FormatFuzzer را با ورودیهای تولید شده توسط AFLSmart مقایسه میکنیم.
۷.۴ پاسخ به پرسش چهارم: تصدیق جهش (Mutation Validity)
ما برای پاسخ به پرسش چهارم (اینکه جهشهای هوشمند اعمال شده توسط FormatFuzzer تا چه حد دقیق هستند؟)، میزان موفقیت جهشهای هوشمند خود را در مقایسه با جهشهای سادهتر که از بذرهای تصمیم (decision seeds) استفاده نمیکنند ارزیابی میکنیم.
از میان چهار جهش هوشمند تعریف شده در بخش ۶.۲، سه مورد از آنها (جایگزینی (replace)، درج (insert)، حذف (delete)) میتوانند به صورت ساده و با اعمال مستقیم همان عملیات (جایگزینی، درج، حذف) بر روی قطعههای (chunk) فایلها پیادهسازی شوند، بدون آن که از بذرهای تصمیم استفاده گردد. چنین پیادهسازی در کارهای پیشین نیز رایج است، مانند AFLSmar [39].
ما به منظور ارزیابی جهشهای هوشمند، هر نوع جهش را ۱۰٬۰۰۰ بار بر روی مجموعهی اولیهای از فایلهای معتبر برای هر قالب اعمال کردهایم. جدول ۳ نتایج ما را نشان میدهد که در آن اعداد به صورت درصد بیان شدهاند. برای هر نوع جهش هوشمند، ابتدا نرخ موفقیت گزارش میشود (یعنی چه درصدی از تلاشها منجر به تولید یک فایل شدهاند). برای جهشهای جایگزینی (replace) و درج (insert)، همچنین در داخل پرانتز درصد دفعاتی ارائه شده است که در آنها دقیقاً به همان تعداد مورد انتظار از بایتهای تصمیم (decision bytes) هنگام تولید قطعههای (chunk) هدف مصرف شده است. در این موارد، انتظار میرود فایل حاصل از نظر معنایی، نتیجهی صحیح اعمال جهش مورد نظر باشد.
با این حال، حتی زمانی که این شرایط برقرار نیست (برای مثال، وقتی قطعه موردنظر برای درج در موقعیت هدف قابل جایگیری نباشد)، در اکثریت قریب به اتفاق موارد همچنان موفق به تولید یک فایل میشویم که برای فازینگ مفید است. در واقع، نرخ موفقیت در تولید فایل تقریباً همیشه بالاتر از ۹۱٪ است.
از میان مواردی که در آنها یک فایل با موفقیت تولید میشود، گزارش میکنیم که چه درصدی از آن فایلها معتبر (valid) در نظر گرفته میشوند، و همچنین چه درصدی از آنها در صورتی معتبر خواهند بود که نسخهٔ سادهٔ جهش متناظر را اعمال کنیم. همانطور که در جدول ۳ نشان داده شده است، هنگام اعمال جهشهای هوشمند ما بر روی بذرهای تصمیم (decision seeds)، تقریباً در همه موارد به اعتبار بالاتری نسبت به نسخههای سادهٔ همان جهشها دست مییابیم.
این نتیجه قابل انتظار است، زیرا جهشهای هوشمند ما قادرند اطلاعات زمینهای (contextual information) را در نظر بگیرند؛ برای مثال میتوانند جمعآزمای (checksum) صحیح یک قطعه (chunk) را دوباره محاسبه کنند، حتی زمانی که محتوای آن قطعه به صورت جزئی تغییر یافته است. همچنین گزارش میکنیم که در چه درصدی از موارد، فایلهای حاصل از جهشهای هوشمند ما با آنچه از اعمال جهش ساده به دست میآید متفاوت هستند.
در نهایت، لازم به تأکید است که جهشهای انتزاعی هوشمند (smart abstract mutations) ما تنها به دلیل توانایی FormatFuzzer در تولید قطعههای جدید به طور کامل از ابتدا ممکن شدهاند، و این انتزاعها نرخ اعتبار بسیار بالایی تولید میکنند و سودمندترین نوع جهش در رسیدن به پوشش جدید (new coverage) در طول فازینگ هستند.
نتیجهگیری ۵. جهشهای هوشمندی که —همانند روش FormatFuzzer— بر روی بذرهای تصمیم اعمال میشوند، در مقایسه با جهشهای هوشمندی که صرفاً محتوای قطعهها را از فایلها کپی میکنند (مانند رویکردهای پیشین از جمله AFLSmart)، با احتمال بسیار بیشتری منجر به تولید ورودیهای معتبر میشوند.
جدول ۳. جهشهای هوشمند: موفقیت (Suc.)، درستی معنایی chunk هدف (Cor.)، اعتبار (Val.)، اعتبارِ جهش سادهٔ متناظر (Sim.)، و سهم مواردی که جهشهای هوشمند ما با نسخههای ساده متفاوت هستند (Diff.):
این امر تنها به دلیل توانایی FormatFuzzer در تولید قطعههای (chunk) جدید به طور کامل از ابتدا ممکن شده است، و این انتزاعها (abstractions) نرخ اعتبار بسیار بالایی تولید میکنند و در میان انواع جهشها، سودمندترین نوع برای دستیابی به پوشش جدید (new coverage) در طول فازینگ هستند.
نتیجهگیری ۵. جهشهای هوشمند اعمال شده بر روی بذرهای تصمیم (decision seeds)، همانطور که در FormatFuzzer انجام میشود، در مقایسه با جهشهای هوشمندی که صرفاً محتوای قطعهها را از فایلها کپی میکنند -مانند روشهای استفاده شده در کارهای پیشین، نظیر AFLSmart- به مراتب احتمال بیشتری برای تولید ورودیهای معتبر دارند.
۷.۵ پاسخ به پرسش پنجم: فازینگ جعبه سیاه (Black-Box Fuzzing)
اکنون اثربخشی FormatFuzzer را، به ویژه از نظر دستیابی به پوشش (coverage) در موضوعات آزمایشی خود، ارزیابی میکنیم. به طور کلی، هرچه پوشش یک مولد آزمایشی (test generator) بالاتر باشد، احتمال آن برای کشف باگها نیز بیشتر است —به ویژه از این جهت که اگر کدی پوشش داده نشود، باگهای آن نیز کشف نخواهند شد.
ما با پرسش پنجم آغاز میکنیم: FormatFuzzer به عنوان یک فازر جعبه سیاه مستقل (standalone black-box fuzzer) تا چه حد کارآمد است؟ ما دو حالت جعبه سیاه را ارزیابی میکنیم:
- FFGen از FormatFuzzer به عنوان یک مولد ورودی مستقل (standalone input generator) استفاده میکند، به طوری که هیچ نیازی به دانش یا بازخورد (feedback) از برنامه تحت آزمون ندارد (توجه داشته باشید که این حالتی است که تنها یک فازر مبتنی بر مشخصات (specification-based fuzzer) مانند FormatFuzzer میتواند در آن موفق عمل کند).
- FFMut از FormatFuzzer برای تجزیه کردن (Pars) یک مجموعه اولیه از فایلهای ورودی استفاده میکند و سپس جهشهای هوشمند (smart mutations) را بر روی آنها اعمال میکند، که این نیز بدون هیچگونه هدایت (guidance) انجام میشود.
ابتدا پوشش زبان (language coverage) را ارزیابی میکنیم، یعنی اینکه کدام ویژگیهای زبان قالب باینری (binary template) واقعاً در فایلهای تولید شده وجود دارند. برای این منظور، ما با استفاده از استراتژی تولید جعبه سیاه FFGen، تعداد ۱۰٬۰۰۰ فایل تولید کردیم و اندازهگیری نمودیم که چه درصدی از دستورات اعلان متغیر (variable declaration statements) در قالب باینری پوشش داده شدهاند؛ زیرا اعلانهای متغیر همان نقاطی هستند که در آنها یک گره جدید (new node) به درخت تجزیه (parse tree) اضافه میشود.
جدول ۵ پوشش زبان حاصل را نشان میدهد که برای تقریباً تمام قالبها حداقل ۹۴٪ است. تنها قالب JPG پوشش پایینتری برابر با ۷۹٪ داشت. این کمبود پوشش عمدتاً ناشی از برخی قطعههای اختیاری (optional chunks) بود که برای تجزیه کردن فعال شده بودند، اما هنوز برای تولید محتوای معتبر اصلاح نشده بودند. ما این نوع قطعهها را فقط با احتمال بسیار کم تولید میکنیم تا اطمینان حاصل شود که بیشتر تولیدها منجر به فایلهای معتبر میشوند.
نتیجهگیری ۶. FormatFuzzer بهتنهایی، پوشش گستردهای از ویژگیهای زبانی موجود در ورودیهای معتبر فراهم میکند.
جدول ۴. برنامههای تحت آزمون:
جدول ۵. پوشش زبان: پوشش اعلانهای متغیر (درصد %) در قالب باینری (binary template):
جدول ۶. پوشش خطی (Line Coverage) بر حسب درصد (%) برای FormatFuzzer در تنظیمات جعبهسیاه (black-box settings):
ما در مرحله بعد، ارزیابی میکنیم که فازر در برنامههای دنیای واقعی چقدر خوب عمل میکند. جدول ۴ برنامهها را بههمراه دستور دقیق مورد استفاده برای فازینگ فهرست میکند. آزمایشهای فازینگ با محدودیت زمانی ۲۴ ساعته انجام شدهاند. برای تمامی روشهایی که به یک مجموعه اولیه از ورودیها (initial corpus) نیاز دارند، از یک مجموعهی یکسان از فایلها استفاده کردهایم؛ این فایلها شامل نمونههای کوچک از هر قالب بودهاند که به صورت دستی از GitHub دانلود شدهاند. هر مجموعه به طور میانگین شامل ۱۱ فایل بوده است (حداقل ۵ و حداکثر ۳۱ فایل).
جدول ۶ پوشش خطی (line coverage) حاصل را که با ابزار LCOV اندازهگیری شده است (میانگین تمام اجراها) برای تنظیمات جعبه سیاه (black-box settings) نشان میدهد. یک نکته جالب این است که در هر دو حالت، تمام ورودیها به صورت ساختاری (syntactically) معتبر تولید میشوند؛ و در واقع بررسیها نشان میدهد که کد مربوط به مدیریت خطا (error handling code) تقریباً اصلاً پوشش داده نمیشود.
نتیجهگیری ۷. FormatFuzzer حتی در تنظیمات جعبه سیاه (black-box) و بدون هیچگونه راهنمایی از برنامه تحت آزمون، پوشش (coverage) قابل قبولی به دست میآورد.
نیمهٔ پایینی جدول ۶ تفاوت پوشش بین دو حالت را نشان میدهد؛ در اینجا A \ B به خطوطی اشاره دارد که در A پوشش داده شدهاند اما در B پوشش داده نشدهاند. برتری FFGen نسبت به FFMut در قالبهای MIDI و PCAP به وضوح قابل مشاهده است، جایی که قالب باینری (binary template) شامل محتوای غیرمعمول (exotic contents) است که در فایلهای نمونهٔ ما وجود ندارند. در مقابل، پوشش اضافی FFMut نسبت به FFGen احتمالاً به برخی ویژگیهای موجود در پیکرهٔ ورودیها (input corpus) مربوط میشود که در قالبهای باینری (binary templates) توصیف نشدهاند.
نتیجهگیری ۸. بعید است برخی از ویژگیهای موجود در مشخصات، در فایلهای موجود در محیط عملی یافت شوند.
جدول ۷. پوشش خطی (Line Coverage) بر حسب درصد (%) برای FormatFuzzer یکپارچه شده با AFL:
علاوه بر این، مشاهده کردهایم که برای تمام قالبها، پوشش نهایی به دست آمده پس از تولید فایلها با رویکرد FFGen از پوشش پیکرهٔ دانلود شدهٔ ما بیشتر است (به طور متوسط با ضریب ×۱٫۴).
۷.۶ پاسخ به پرسش ششم: یکپارچهسازی با فازرهای مستقل از قالببندی (Integrating Format-Agnostic Fuzzers)
در عمل، ما اغلب با شرایطی مواجه میشویم که در آن بازخورد (feedback) از برنامه تحت آزمون در دسترس است و در نتیجه FormatFuzzer میتواند با فازرهای مستقل از قالببندی (format-agnostic fuzzers) موجود یکپارچه شود. بنابراین به پرسش ششم میپردازیم: یکپارچهسازی FormatFuzzer با یک فازر مستقل از قالببندی تا چه حد کارآمد است؟ ما FormatFuzzer را با فازر محبوب ++AFL به دو روش مختلف [19]، همانطور که در بخش ۶.۳ ارائه شده است، یکپارچه کردهایم:
- AFL+FFGen از AFL برای جهش دادن بذر تصمیم (decision seed) استفاده میکند که سپس به FormatFuzzer داده میشود و برای تولید ورودیها برای برنامه هدف به کار میرود. از آنجا که تنها بذر تصمیم جهش داده میشود، ورودیهای تولیدی میبایست با قالب مشخص شده سازگار باشند.
- AFL+FFMut به این صورت است که AFL از FormatFuzzer برای انجام جهشهای هوشمند (smart mutations) روی فایل ورودی استفاده میکند. FormatFuzzer فایلهایی را که قرار است فاز شوند تجزیه (parse) میکند و اطلاعات مربوط به قطعههای (chunks) موجود در آن فایلها را به خاطر میسپارد تا بتواند بعداً از آنها برای انجام جهشهای هوشمند استفاده کند. توجه داشته باشید که تمام جهشهای معمول AFL نیز استفاده میشوند که میتوانند ورودیهای نامعتبر تولید کنند و به پوشش کد مربوط به مدیریت خطا (error handling code) کمک میکنند. جهشهای هوشمند FormatFuzzer به عنوان یک جهش دهندهٔ سفارشی (custom mutator) اضافه میشوند.
جدول ۷ پوشش حاصل را فهرست میکند. ما در آزمایشهای فازینگ خود، برای مقایسهای منصفانه با تنظیمات بهینهٔ ++AFL، در صورت مناسب بودن، دیکشنریهای AFL را فعال کردهایم.
ما برای رویکرد AFL+FFGen از دیکشنریها استفاده نکردهایم، زیرا این فازر بذرهای تصمیم (decision seeds) را جهش میدهد و نه فایلهای دودویی را به صورت مستقیم. دیکشنریها برای تمام قالبها فعال بودهاند، به جز MIDI، زیرا برای این قالب هیچ دیکشنریای توسط ++AFL ارائه نشده بود.
برای بیشتر موضوعات (subjects)، تفاوتهای مجموعهای نشان میدهد که هر دو استراتژی نسبت به AFL ساده باعث افزایش پوشش (coverage) میشوند؛ این موضوع نشان میدهد که FormatFuzzer در تبدیل فازرهای مستقل از قالببندی (format-agnostic fuzzers) به فازرهای آگاه از قالببندی (format-aware fuzzers) مؤثر است. در شش مورد از ده موضوع، این یکپارچهسازی حتی به پوشش بالاتری بهصورت مطلق نیز دست یافته است.
جدول ۷ همچنین اندازه اثر Vargha–Delaney با نماد A12 را گزارش میکند که احتمال برتری AFL+FFMut نسبت به تکنیک رقیب یعنی AFL را برآورد میکند. مقادیر بزرگتر از ۰٫۵ نشان میدهند که احتمال برتری AFL+FFMut در این مقایسهٔ پوشش بیشتر است. آثار معنادار از نظر آماری بر اساس آزمون رتبه بندی امضا شده ویلکاکسون (Wilcoxon signed-rank) به صورت پررنگ مشخص شدهاند.
همانطور که مشاهده میشود، AFL+FFMut در پنج بنچمارک عملکرد بسیار بهتری داشته است، در حالی که در پنج مورد باقیمانده تفاوت آماری معناداری مشاهده نشده است. در سه بنچمارک، AFL برتری جزئی داشته است. دلیل این موضوع این بود که جهشهای هوشمند (smart mutations) در FormatFuzzer نسبت به جهشهای مستقل از قالببندی (format-agnostic mutations) به زمان محاسباتی بیشتری نیاز دارند، بنابراین AFL توانست صرفاً به این دلیل که تعداد اجرای بیشتری (executions) انجام دهد، پوشش بیشتری به دست آورد. با این حال، برتری AFL در این بنچمارکها از نظر آماری معنادار نیست.
نتیجهگیری ۹. فازینگ آگاه از قالب با استفاده از FormatFuzzer به خطوطی دست پیدا میکند که فازینگ مستقل از قالب قادر به رسیدن به آنها نیست.
با این حال، خود AFL نیز خطوطی را پوشش میدهد که یکپارچهسازی AFL و FormatFuzzer به آنها دست نیافته است. یکی از دلایل این موضوع دوباره این است که AFL ورودیهای نامعتبر متعددی تولید میکند که باعث پوشش کد مربوط به مدیریت خطا (error-handling code) میشود؛ در حالی که AFL+FFGen به صورت ذاتی از این حالت اجتناب میکند.
تصمیمات نادرست (evil decisions) در FormatFuzzer امکان ایجاد یک کلاس محدود از ورودیهای نامعتبر را فراهم میکنند، اما این تصمیمات پراکنده هستند و اجازه ندارند به طور کامل ساختار ورودی را تخریب کنند؛ برای مثال، نمیتوانند ورودیهایی تولید کنند که به دلیل اندازهٔ نادرست قطعه (incorrect chunk size) قابل تجزیه نباشند. بنابراین، جهشهای تصادفی انجام شده توسط AFL همچنان برای پوشش دادن تمام خطاهای تجزیه (Parsing) در برنامه هدف ضروری هستند.
نتیجهگیری ۱۰. فازینگ آگاه به قالببندی (format-aware) و فازینگ مستقل از قالببندی (format-agnostic) یکدیگر را تکمیل میکنند.
با مقایسه ادغام AFL با FormatFuzzer به صورت مستقل، مشاهده میکنیم که ادغام راهنمای پوشش AFL در فازینگ آگاه به قالب، پوشش را نسبت به یک محیط جعبه سیاه خالص به طور قابل توجهی بهبود میبخشد. از این رو، اگر ورودیهای نمونه و بازخورد پوشش در دسترس باشند، چنین ادغامی یک تنظیم ترجیحی است.
نکته کلیدی ۱۱. ادغام FormatFuzzer با فازرهای هدایت شده با پوشش، پوشش را در تنظیمات جعبه سیاه بهبود میبخشد.
۷.۷ پاسخ به پرسش هفتم: راهبردهای جایگزین آگاه از قالببندی (Alternate Format-Aware Strategies)
ارزیابی بعدی ما به پرسش هفتم میپردازد: FormatFuzzer در مقایسه با سایر فازرهای آگاه از قالببندی چگونه عمل میکند؟ رقیب در اینجا [39] AFLSmart است که از اطلاعات قالب موجود در مشخصات Peach برای تعیین مرزهای قطعهها (chunk) در فایلهای ورودی استفاده میکند و در نتیجه جهشهای هوشمندتری ایجاد میکند.
AFLSmart از مشخصات ورودی تنها برای تجزیه کردن استفاده میکند و نه برای تولید ورودیهای معتبر. نتایج پوشش در جدول ۸ ارائه شدهاند. با مقایسه AFLSmart با AFL+FFMut مشاهده میکنیم که یکپارچهسازی AFL با FormatFuzzer در پنج مورد از هشت موضوع عملکرد بهتری دارد، در حالی که در دو موضوع دیگر (GIF و BMP) امکان اجرای AFLSmart وجود نداشت. جدول ۸ همچنین اندازه اثر Vargha-Delaney با نماد A12 و آزمون Wilcoxon signed-rank را برای بررسی آماری شامل میشود. مشاهده میکنیم که AFL+FFMut در چهار بنچمارک به طور قابل توجهی بهتر عمل کرده است، در حالی که AFLSmart در دو بنچمارک برتری بیشتری داشته است. همانطور که بار دیگر از تفاوتها مشخص است، حتی در مواردی که AFLSmart عملکرد بهتری داشته، AFL+FFMut همچنان برخی خطوط کد را پوشش داده است که توسط AFLSmart پوشش داده نشدهاند.
جدول ۹. پوشش خط (Line Coverage) بر حسب درصد (%) برای FormatFuzzer و AFLSmart در یک ساعت فازینگ:
یکی از عوامل مؤثر در اینجا، کامل بودن مشخصات قالببندی (format specifications) است. یکی از مزایای FormatFuzzer این است که میتوانیم از قالبهای باینری (binary templates) بسیار دقیقی که پیشتر برای تجزیه کردن توسعه داده شدهاند بهرهمند شویم. از سوی دیگر، سازندگان AFLSmart مجبور بودهاند مشخصات قالب خود (Peach pits) را از ابتدا بنویسند، بنابراین این مشخصات کاملتر نیستند.
برای مثال، Peach pit مربوط به PNG تنها محتوای داخلی سه نوع قطعه (chunk) را به طور خاص تعریف میکند: IHDR، cHRM و IEND، در حالی که قالب دودویی PNG پیش از هرگونه تغییر، ۱۵ نوع قطعه را بهطور کامل تعریف کرده بود.
با این حال، بر اساس تجربه ما، مشخصات قالب دقیقتر تنها در صورتی به بهبود اندکی در فازینگ منجر میشوند که صرفاً برای تجزیه کردن استفاده شوند، همانطور که در AFLSmart انجام میشود. FormatFuzzer بیشترین بهره را از این مشخصات دقیق به دلیل توانایی خود در تولید ورودیهای معتبر میبرد؛ قابلیتی که در FFMut برای اعمال جهشهای هوشمندِ از نظر معنایی معتبر (semantically valid smart mutations) که به اطلاعات زمینهای (contextual information) احترام میگذارند استفاده میشود.
نتیجهگیری ۱۲. استفاده از قالبهای ورودی برای تجزیه (parsing)، جهش (mutation) و تولید (generation) —همانطور که در FormatFuzzer انجام میشود— در مقایسه با استفاده از آنها صرفاً برای تجزیه، همانند رویکرد AFLSmart، منجر به پوشش بیشتری میشود.
همچنین مشاهده کردهایم که FormatFuzzer در تولید ورودیهای باکیفیت بالا و دستیابی سریع به پوشش بالا در طول کمپین فازینگ (fuzzing campaign) بسیار کارآمد است. جدول ۹ همان اطلاعات جدول ۸ را نشان میدهد، اما فقط برای یک ساعت اول فازینگ (و دوباره میانگین ۱۰ اجرا را در نظر گرفتهایم). مشاهده میکنیم که در بازهٔ کوتاه یک ساعته، FormatFuzzer در شش مورد از هشت بنچمارک بصورت قابل توجهی بهتر از AFLSmart عمل کرده و در هیچ موردی بدتر نبوده است.
نتیجهگیری ۱۳. FormatFuzzer به طور خاص در یافتن پوشش (coverage) در شرایطی با محدودیت زمانی، عملکرد بسیار خوبی دارد.
۷.۸ پاسخ به پرسش هشتم: باگهای کشف شده (Bugs Found)
ارزیابی خود را با پاسخ به پرسش هشتم به پایان میرسانیم: آیا FormatFuzzer باگهای واقعی را پیدا میکند؟ پاسخ این است: بله! در آزمایشهای اولیه فازینگ با FormatFuzzer، موارد زیر را کشف و گزارش نمودیم:
۱۶ خطای متمایز در قطعهبندی (توسط ردگیریهای مختلف پشته (stack traces)) و ۸ خطای متمایز در ffmpeg (MP4 و AVI)، که مشخص شد حداقل ۸ باگ متمایز هستند که تا به امروز توسط توسعهدهندگان FFmpeg برطرف شدهاند. این موارد زمانی رخ میدهند که ffmpeg با pthreads (که گزینه پیشفرض است) کامپایل میشود و fuzzer با محدودیت حافظه ۲۰۰ مگابایت (با استفاده از گزینه m- در AFL) اجرا میشود. این اشکالات در طیف گستردهای از مراحل مختلف اجرا قرار دارند: تخصیص، مقداردهی اولیه برشهای h264، چند رشتهای، نوشتن بستههای mov، رمزگذاری ویدیو، رمزگشایی دادههای طیفی و پاکسازی زمینه.
۱۹ خطای حافظه متمایز (بر اساس ردگیریهای مختلف پشته ) در ++TiMidity (قالب MIDI). این مقاله پس از برطرف شدن این باگها به روزرسانی خواهد شد. توجه داشته باشید که FFmpeg «بخشی از جریان کاری صدها پروژه نرمافزاری دیگر است، و کتابخانههای آن بخش اصلی نرمافزارهای پخش رسانه مانند VLC media player محسوب میشوند، و در پردازش هستهای سرویسهایی مانند YouTube و iTunes نیز مورد استفاده قرار گرفتهاند» [1].
نتیجهگیری ۱۴. FormatFuzzer قادر به کشف باگها در نرمافزارهای مهم و مرتبط است.
۷.۹ بحث (Discussion)
بهرهگیری از مشخصات قالب ورودی (input format specifications) ارزشمند است. FormatFuzzer در تمامی حالات، پوشش (coverage) بیشتری را کاوش کرده است — و در نتیجه فرصتهای بیشتری برای کشف باگها و آسیبپذیریها فراهم کرده است. FormatFuzzer میتواند به عنوان یک تولیدکننده مستقل، به ویژه در تنظیمات جعبه سیاه (black-box settings)، مورد استفاده قرار گیرد؛ و یکپارچهسازی آن با فازرهای مبتنی بر بازخورد (feedback-driven fuzzers) مانند AFL بهترین ویژگیهای فازینگ آگاه به قالب(بندی) و فازینگ هدایت شده توسط بازخورد را با هم ترکیب میکند.
این ارزیابی همچنین چندمنظوره بودن FormatFuzzer (versatility) را نشان میدهد. اجزای مختلف آن (تجزیه کردن / جهش دادن / تولید کردن) میتوانند به صورت مستقل استفاده شوند (برای مثال، تولید در یک محیط جعبه سیاه)، یا به طور کامل با فازرهای مستقل از قالب یکپارچه شوند (جایی که ابزار محبوب AFL حتی میتواند با هر فازر مستقل از قالب کارآمدتر دیگری جایگزین شود).
این انعطافپذیری و ماژولار بودن، FormatFuzzer را به بستری (platform) برای ایجاد سریع و یکپارچهسازی راهبردهای جدید فازینگ و تطبیق آنها با شرایط موردنظر تبدیل میکند — در مقابل فازرهایی که هیچ دانشی از ساختار ورودی ندارند یا از مشخصات ورودی صرفاً برای تجزیه کردن یا فقط تولید استفاده میکنند. همانطور که ارزیابی ما نشان میدهد، هر دو رویکرد آگاه از قالب و مستقل از قالب نقاط قوت خاص خود را دارند، که این موضوع نیاز به یک پلتفرم یکپارچه را بیش از پیش توجیه میکند.
جنبه مهم دیگری که پیشتر به آن پرداخته نشده، این است که FormatFuzzer به صورت ذاتی به آزمونگر (tester) امکان کنترل بر آنچه باید آزمایش شود را میدهد. FormatFuzzer با کامنتگذاری (commenting out) بخشهای خاصی از قالب دودویی (binary template)، این امکان را فراهم میکند که روی ویژگیهای مشخصی تمرکز گردد —برای مثال، بخشهایی که اخیراً تغییر کردهاند یا از اهمیت بالایی برخوردار هستند. این قابلیت نیز به انعطافپذیری فازینگ با FormatFuzzer میافزاید.
۸. کارهای مرتبط (RELATED WORK)
۸.۱ فازینگ با استفاده از مشخصات ورودی (Fuzzing with Input Specifications)
استفاده از مشخصات زبان (language specifications) برای تولید ورودیها ایدهای قدیمی است، به ویژه در مورد دستور زبانهای مستقل از متن (context-free grammars). دستور زبانهای مختلفی مانند دستور زبانهای منظم (regular)، مستقل از متن (context-free)، وابسته به متن (context-sensitive) و بدون محدودیت (unconstrained) توسط Noam Chomsky در دهه ۱۹۵۰ و به طور خاص برای کاربردهای زبانی (linguistic applications) و تجزیه کردن معرفی شدهاند.[ ۹]
استفاده از دستور زبانهای برای تولید ورودی نخستین بار توسط Burkhardt [6]، Hanford [26] و Purdom [40] در اواخر دهه ۱۹۶۰ و اوایل دهه ۱۹۷۰ پیشنهاد گردید. زبانهای منطقی (logic languages) [24] مانند Prolog که از دهه ۱۹۷۰ شناخته شدهاند، این امکان را فراهم میکنند که کل برنامه به نوعی وارونه شود و خود برنامه برای تولید همان نوع ورودیهایی که میپذیرد مورد استفاده قرار گیرد.
مولدهای آزاد (free generators) [22] یک چارچوب صوری نسبتاً جدید هستند که همانند FormatFuzzer، تجزیه و تولید را یکپارچه میکنند. با این حال، تمرکز آنها بر تولید ساختارهای داده (data structures) است ــبرای مثال، type-classها در Haskellــ در حالی که FormatFuzzer بر قالبهای دودویی تمرکز دارد و با فازرهای مستقل از قالب نیز یکپارچه میشود.
۸.۲ دستور زبانهای مستقل از متن (Context-Free Grammars)
آزمون مبتنی بر دستور زبان (grammar-based testing) در هزارهٔ جدید مورد توجه گسترده قرار گرفت، زمانی که پژوهشگران مجدداً به کارایی فازینگ پی بردند و نقش دستور زبان در بهبود اثربخشی آن را شناسایی کردند. یکی از نخستین افرادی که کاربرد دستور زبان را در فازینگ مطرح کرد، Patrice Godefroid [21] بود که فازینگ جعبه سفید (whitebox fuzzing) را توسط دستور زبانها تقویت کرد.
از دیگر فازرهای مبتنی بر دستور زبان قابل توجه میتوان به Gramfuzz [25]، Grammarinator [28]، Dharma [34]، Domato [20] و CSS Fuzz [41] اشاره کرد، و همچنین PolyGlot [7] که دستور زبانهای مستقل از متن را با حاشیهنویسیهای معنایی (semantic annotations) تقویت میکند. LangFuzz [29] از مشخصات زبان برای جمعآوری قطعهکدهایی استفاده میکند که میتوانند به عنوان جهشهای هوشمند به کار روند. تمام این روشها بر زبانهای مستقل از متن متکی هستند، که برای مشخصسازی قالبهای دودویی کافی نیستند.
۸.۳ سایر زبانهای مشخصات (Other Specification Languages)
علاوه بر دستور زبانهای مستقل از متن، فازرها از انواع دیگری از مشخصات ورودی نیز استفاده کردهاند، مانند زبانهای منظم (regular languages) (یعنی ماشینهای حالت متناهی (finite state automata) [11,54]، یا زبانهای مبتنی بر قیود (constraint languages) [15]. SISL [50] از یک مشخصهٔ ناقص (partial specification) استفاده میکند که با بهرهگیری از آزمون کانکولیک (concolic testing) [42] تکمیل میشود.
MoWF [38] از یک مشخصه استفاده میکند که بهصورت قید یا محدودیت (constraint) روی فضای ورودی تعریف شده است. این ابزار از اجرای نمادین انتخابی (selective symbolic execution) برای شناسایی شاخههای پوشش داده نشده استفاده میکند و میتواند فیلدهای طول (length fields)، جمعآزماها (checksums) و سایر فیلدهای اعتبارسنجی را اصلاح کند.
فازر ارائه شده توسط Pan و همکاران [۳۷] از دستور زبانهای دارای ویژگی (صفتی) مرتبه بالا (higher-order attribute grammars) برای توصیف فیلدهای طول، جمعآزما و سایر فیلدهای اعتبارسنجی استفاده میکند و از آنها برای تولید ورودیهای قالب فایل مانند PNG بهره میبرد. Parsifal [32] هم برای تجزیه و هم تولید قالبهای دودویی طراحی شده است، اما محدود به قالبهایی با اندازه ثابت است.
Nail [4] یک مشخصه برای مولد تجزیهگر (parser generator) تعریف میکند و قالبهای باینری (معمولاً پروتکلها) را هدف قرار میدهد که شامل فیلدهای آفست (offset) و جمعآزما (checksums) هستند. برخلاف Parsifal، این رویکرد قادر است ساختارهای پیچیدهتری را نیز مدیریت کند.
David Underwood [51] دستور زبانهای مستقل از متن را با دستور زبانهای دارای ویژگی (attribute grammars) گسترش میدهد و یک نگاشت از مشخصات قالب دودویی به دستور زبانهای دارای ویژگی ارائه میکند. این نوع دستور زبانها هم برای تجزیه و هم برای تولید قابل استفاده هستند. Beginner’s Luck [30] یک زبان برای مولدها ارائه میدهد که امکان ادغام محدودیتهای نمونهگیری (sampling constraints) را در حین فازینگ فراهم میکند.
libprotobuf-mutator [44] از بافرهای پروتکل (protocol buffers) برای توصیف قالب فایلها استفاده میکند و اجازه میدهد جهشها روی نمایش فشردهٔ پروتکل بافر انجام شوند. با این حال، یکی از معایب آن نسبت به FormatFuzzer نیاز به نوشتن یک مبدل (converter) بین پروتکل بافر و فایل دودویی متناظر است.
در اصل، FormatFuzzer میتواند از هر یک از این مشخصات قالب استفاده کند و همچنان قابلیتهای منحصربهفرد خود مانند تولید همزمان تجزیه کننده و مولد، یا یکپارچهسازی با فازرهای مستقل از قالب را حفظ کند. با این حال، با استفاده از قالبهای دودویی (binary templates)، این ابزار میتواند بر صدها مشخصهٔ قالب تکیه کند که طی بیش از دو دهه توسط یک جامعهٔ فعال و مشتاق توسعه و بهبود یافتهاند.
۸.۴ راهبردهای فازینگ (Fuzzing Strategies)
CSmith [57] نشان میدهد که جاسازی (embedding) کل مشخصات زبان درون مولد میتواند به فازینگ مؤثری منجر شود. با این حال، در این حالت، خاصیت تعمیمپذیری (generality) و امکان هدفگیری انواع دیگر ورودیها از بین میرود.
از دیگر پژوهشهای مهم در حوزه فازرهای مبتنی بر دستور زبان میتوان به LangFuzz [29]، Blendfuzz [56] و Skyfire [52] اشاره کرد. در Zest [36] و JQF [35]، توالیهای پارامتر (parameter sequences) انتخابهای مولد را مشابه بذرهای تصمیم (decision seed) ما کدگذاری میکنند و میتوانند برای تست مبتنی بر مولد در سبک QuickCheck [10] مورد استفاده قرار گیرند. همچنین Crowbar [16] و CGPT [31] نیز به طور مشابه تست مبتنی بر مولد را با بازخورد پوشش (coverage feedback) ترکیب میکنند و امکان هر دو نوع فازینگ مبتنی بر تولید (generation-based) و مبتنی بر جهش (mutation-based) را فراهم میسازند.
با این حال، استخراج بذرهای تصمیم از طریق تجزیه (parsing) فایلهای موجود، و همچنین اعمال جهشهای هوشمند بر روی آنها (بخش 6.2)، یک نوآوری جدید متعلق به FormatFuzzer محسوب میشود. چندین فازر مبتنی بر دستور زبان نیز بازخورد پوشش (coverage feedback) از AFL را ادغام میکنند. نمونههای قابل توجه شامل Superion [53]، Nautilus [۳] و Grimoire [۵] هستند.
با این حال، این فازرها عمدتاً برای قالبهای ورودی دستور زبان مبتنی بر متن (text-based grammar input formats) بهکار رفتهاند؛ مانند زبانهای نشانهگذاری (markup languages) مانند XML یا زبانهای برنامهنویسی مانند JavaScript، PHP، Ruby، Lua، C، nasm، SQL و SMT. از این رو، احتمالاً قادر به پشتیبانی از ویژگیهای موردنیاز قالبهای دودویی نیستند؛ ویژگیهایی مانند فیلدهای اندازه (size fields)، جمعآزماها (checksums) یا فیلدهای بیتی (bitfields). Superion و Nautilus نیاز دارند برای هر قالب جدید، یک مشخصه کامل از ابتدا نوشته شود. در مقابل، Grimoire میتواند مشخصات قالب را استخراج کند، اما این کار را فقط برای زبانهای متنی انجام میدهد.
۸.۵ فازینگ قالبهای باینری (Fuzzing Binary Formats)
WEIZZ [18] و FFAFuzz [8] فازرهایی هستند که قالبهای باینری مبتنی بر قطعه (chunk) را هدف قرار میدهند. در طول فرآیند فازینگ، این ابزارها تلاش میکنند نحوهٔ مشخص شدن (تعریف) این قالبهای مبتنی بر قطعه را بیاموزند. Peach [17] نیز یک فازر دیگر است که قالبهای باینری را هدف قرار میدهد. AFLSmart [39] فازری است که جهشهای آگاه از قطعه (chunk-aware mutations) را روی ورودی اعمال میکند و برای قالبهای دودویی مناسب است. این کار را با نگه داشتن یک ساختار مجازی از ورودی در حال فاز شدن در حافظه انجام میدهد.
AFLSmart نزدیکترین کار مرتبط به FormatFuzzer است. با این حال، از آنجا که مشخصات قالب آن (Peach pits) باید از ابتدا برای استفاده در AFLSmart نوشته میشد، این مشخصات نسبت به قالبهای باینری ما ناقصتر و کمتر جامع است. از سوی دیگر، FormatFuzzer میتواند از قالبهای باینری خود نه تنها برای تجزیه، بلکه برای تولید ورودی نیز استفاده کند، این قابلیت امکان راهبردهای فازینگ اضافی را فراهم میکند که در این مقاله مورد بررسی قرار گرفتند.
۹. نتیجهگیری (CONCLUSION)
به منظور فاز کردن (fuzz) ورودیهای باینری، قدرتمندترین راهکار این است که یک فازر اختصاصی طراحی شود. با این حال، ساخت چنین فازرهایی نیازمند تلاش و زمان قابل توجهی است. با استفاده از FormatFuzzer میتوان از صدها قالب باینری موجود (binary templates) استفاده کرد تا فازرهای فعلی را از قالببندی آگاه (format-aware) ساخت. FormatFuzzer میتواند این قالبهای باینری را مستقیماً به عنوان تجزیه کننده (parser) بهکار گیرد، که ساختار کامل ورودیها را آشکار میکند.
گسترش این قالبهای باینری موجود برای تولید ورودی امکان فازینگ در حالت جعبه سیاه (black-box) را فراهم میکند؛ حالتی که برای فازرهای مستقل از قالببندی (format-agnostic) عملاً غیرممکن است. FormatFuzzer با فراهم کردن بذرهای تصمیم (decision seed) و جهشهای هوشمند (smart mutations)، میتواند هر فازری را از قالببندی آگاه کند.
در حالی که FormatFuzzer یک انتخاب بسیار کارآمد برای فازینگ تخصصی دامنه (domain-specific fuzzing) است، ماژولار بودن آن همچنین آن را به یک پلتفرم مفید برای ساخت فازرهای آتی تبدیل میکند. علاوه بر توسعه قالبهای بیشتر برای FormatFuzzer، کارهای آینده ما بر موضوعات زیر تمرکز خواهد داشت:
راهبردهای جعبه سیاه (Black-box strategies): پیشرفتهای اخیر در فازینگ مبتنی بر دستور زبان، مانند دستیابی سیستماتیک به پوشش دستور زبان (grammar coverage) [28] یا یادگیری و استفاده از توزیعهای احتمالاتی [۴۹]، میتوانند به راحتی برای فازینگ مبتنی بر قالبهای دودویی نیز بهکار گرفته شوند.
فازینگ مبتنی بر جستوجو (Search-based fuzzing): قالب بذرهای تصمیم امکان ادغام آسان راهبردهای فازینگ جایگزین را فراهم میکند. بهطور خاص، بهینهسازی ژنتیکی (genetic optimization) در آزمون مبتنی بر جستوجو بسیار مهم است، زیرا FormatFuzzer از قبل عملیات جهش (mutation) و تقاطع (crossover) را پیادهسازی کرده است.
استخراج قالبهای باینری یا دودویی (Mining binary formats): تکنیکهای جدید برای استخراج دستور زبانهای مستقل از متن از روی تجزیهکنندهها [۲۳،۳۳] میتوانند برای قالبهای دودویی نیز تطبیق داده شوند و ساخت قالبها را سادهتر کنند.
استخراج قیود و محدودیتهای دودویی (Mining binary constraints): با گسترش یک قالب نحوی (syntactic template) استخراج شده، میتوان پردازش عناصر ورودی را با استفاده از دستکاری و تحلیل پویا (dynamic tainting و dynamic analysis) ردیابی کرد و محدودیتهای وابسته به زمینه (context-sensitive constraints) را از پردازشگرهای ورودی استخراج نمود.
درگیر کردن جامعه (Engaging the community): ما قصد داریم آموزشها و مطالب دیگری را برای مشارکت جامعه در نوشتن قالبهای دودویی ایجاد کنیم. فهرست ویکیپدیا از قالبهای فایل [۲] بیش از ۱۰۰۰ قالب را شامل میشود – بنابراین هنوز کار زیادی باقی مانده است!
FormatFuzzer و تمام ابزارها و منابع مرتبط به صورت متنباز (open source) در دسترس هستند. برای اطلاعات بیشتر درباره FormatFuzzer، به صفحه پروژه آن مراجعه کنید.
منابع
[1] 2021. Wikipedia: ffmpeg. Retrieved from https://en.wikipedia.org/wiki/FFmpeg. Accessed: 13 August 2021.
[2] 2021. Wikipedia: List of File Formats. Retrieved from https://en.wikipedia.org/wiki/List_of_file_formats. Accessed: 13 August 2021.
[3] Cornelius Aschermann, Tommaso Frassetto, Thorsten Holz, Patrick Jauernig, Ahmad-Reza Sadeghi, and Daniel Teuchert. 2019. NAUTILUS: Fishing for deep bugs with grammars. In Proceedings of the NDSS 2019. Retrieved from https://www.ndss-symposium.org/ndss-paper/nautilus-fishing-for-deep-bugs-with-grammars/
[4] Julian Bangert and Nickolai Zeldovich. 2014. Nail: A practical tool for parsing and generating data formats. In Proceedings of the 11th Symposium on Operating Systems Design and Implementation. 615–628.
[5] Tim Blazytko, Cornelius Aschermann, Moritz Schlögel, Ali Abbasi, Sergej Schumilo, Simon Wörner, and Thorsten Holz. 2019. {GRIMOIRE}: Synthesizing structure while fuzzing. In Proceedings of the 28th Security Symposium. 1985–2002.
[6] W. H. Burkhardt. 1967. Generating test programs from syntax. Computing 2, 1 (1967), 53–73. DOI:https://doi.org/10. 1007/BF02235512
[7] Yongheng Chen, Rui Zhong, Hong Hu, Hangfan Zhang, Yupeng Yang, Dinghao Wu, and Wenke Lee. 2021. One engine to fuzz ’em All: Generic language processor testing with semantic validation (to appear). In Proceedings of the 42nd IEEE Symposium on Security and Privacy. San Francisco, CA.
[8] Zehan Chen, Yuliang Lu, Kailong Zhu, Lu Yu, and Jiazhen Zhao. 2022. Fast format-aware fuzzing for structured input applications. Applied Sciences 12, 18 (2022), 9350.
[9] Noam Chomsky. 1956. Three models for the description of language. IRE Transactions on Information Theory 2, 3 (1956), 113–124. Retrieved from https://chomsky.info/wp-content/uploads/195609-.pdf
[10] Koen Claessen and John Hughes. 2011. QuickCheck: A lightweight tool for random testing of Haskell programs. ACM SIGPLAN Notices 46, 4 (2011), 53–64.
[11] Baojiang Cui, Shurui Liang, Shilei Chen, Bing Zhao, and Xiaobing Liang. 2014. A novel fuzzing method for Zigbee based on finite state machine. International Journal of Distributed Sensor Networks 10, 1 (2014), 762891.
[12] James "d0c_s4vage" Johnson. 2020. GitHub - d0c-s4vage/pfp: pfp - Python Format Parser - a Python-based 010 Editor Template Interpreter. Retrieved August 1, 2021 from https://github.com/d0c-s4vage/pfp. (2020).
[13] James "d0c_s4vage" Johnson. 2020. GitHub - d0c-s4vage/py010parser: A Modified Pycparser to Parse 010 Templates. Retrieved August 1, 2021 from https://github.com/d0c-s4vage/py010parser. (2020).
[14] Oxford Brookes University (Second Edition) David Duce. 2003. Portable Network Graphics (PNG) Specification (Second Edition). Retrieved November 15, 2021 from https://www.w3.org/TR/PNG/. (2003).
[15] Kyle Dewey, Jared Roesch, and Ben Hardekopf. 2014. Language fuzzing using constraint logic programming. Proceedings of the 29th ACM/IEEE International Conference on Automated Software Engineering. 725–730.
[16] Stephen Dolan. 2021. Crowbar. Retrieved November 15, 2021 from https://github.com/stedolan/crowbar. (2021).
[17] 2018. Peach Fuzzer: Discover unknown vulnerabilities. Retrieved from https://www.peach.tech/. Accessed 29 August 2018.
[18] Andrea Fioraldi, Daniele Cono D’Elia, and Emilio Coppa. 2020. WEIZZ: Automatic grey-box fuzzing for structured binary formats. In Proceedings of the 29th ACM SIGSOFT International Symposium on Software Testing and Analysis.1–13.
[19] Andrea Fioraldi, Dominik Maier, Heiko Eißfeldt, and Marc Heuse. 2020. AFL++: Combining incremental steps of fuzzing research. In Proceedings of the 14th USENIX Workshop on Offensive Technologies. USENIX Association.
[20] Ivan Fratric. 2019. Domato A DOM Fuzzer. (2019). Retrieved from https://github.com/googleprojectzero/domato. Accessed: 13 August 2021.
[21] Patrice Godefroid, Adam Kiezun, and Michael Y. Levin. 2008. Grammar-based whitebox fuzzing. ACM, New York, NY, USA, 206–215.
[22] Harrison Goldstein and Benjamin C. Pierce. 2022. Parsing randomness. Proceedings of the ACM on Programming Languages 6, OOPSLA (2022), 89–113.
[23] Rahul Gopinath, Björn Mathis, and Andreas Zeller. 2020. Mining input grammars from dynamic control flow. In Proceedings of the 28th ACM Joint Meeting on European Software Engineering Conference and Symposium on the Foundations of Software Engineering. 172–183.
[24] Claude Cordell Green. 1970. The Application of Theorem Proving to Question-answering Systems. Number 96. Management Information Services.
[25] Tao Guo, Puhan Zhang, Xin Wang, and Qiang Wei. 2013. Gramfuzz: Fuzzing testing of web browsers based on grammar analysis and structural mutation. In Proceedings of the 2013 Second International Conference on Informatics & Applications. IEEE, 212–215.
[26] Kenneth V. Hanford. 1970. Automatic generation of test cases. IBM Systems Journal 9, 4 (1970), 242–257. DOI:https://doi.org/10.1147/sj.94.0242
[27] Nikolas Havrikov and Andreas Zeller. 2019. Systematically covering input structure. In Proceedings of the 34th IEEE/ACM International Conference on Automated Software Engineering. IEEE, 189–199. DOI:https://doi.org/10.1109/ASE.2019.00027
[28] Renáta Hodován, Ákos Kiss, and Tibor Gyimóthy. 2018. Grammarinator: A grammar-based open source fuzzer. In Proceedings of the 9th ACM SIGSOFT International Workshop on Automating TEST Case Design, Selection, and Evaluation. ACM, 45–48.
[29] Christian Holler, Kim Herzig, and Andreas Zeller. 2012. Fuzzing with code fragments. In Proceedings of the 21st USENIX Conference on Security Symposium. USENIX Association, Berkeley, CA, 38–38.
[30] Leonidas Lampropoulos, Diane Gallois-Wong, Cătălin Hriţcu, John Hughes, Benjamin C Pierce, and Li-yao Xia. 2017. Beginner’s luck: A language for property-based generators. In Proceedings of the 44th ACM SIGPLAN Symposium on Principles of Programming Languages. 114–129.
[31] Leonidas Lampropoulos, Michael Hicks, and Benjamin C. Pierce. 2019. Coverage guided, property based testing. Proceedings of the ACM on Programming Languages 3, OOPSLA (2019), 1–29.
[32] Olivier Levillain. 2014. Parsifal: A pragmatic solution to the binary parsing problems. In Proceedings of the 2014 IEEE Security and Privacy Workshops. IEEE, 191–197.
[33] Björn Mathis, Rahul Gopinath, Michaël Mera, Alexander Kampmann, Matthias Höschele, and Andreas Zeller. 2019. Parser-directed fuzzing. In Proceedings of the 40th ACM SIGPLAN Conference on Programming Language Design and Implementation. 548–560.
[34] Mozilla. 2019. Dharma: A Generation-based, Context-free Grammar Fuzzer. (2019). Retrieved from https://blog.mozilla.org/security/2015/06/29/dharma/. Accessed: 13 August 2021.
[35] Rohan Padhye, Caroline Lemieux, and Koushik Sen. 2019. JQF: Coverage-guided property-based testing in Java. In Proceedings of the 28th ACM SIGSOFT International Symposium on Software Testing and Analysis. 398–401.
[36] Rohan Padhye, Caroline Lemieux, Koushik Sen, Mike Papadakis, and Yves Le Traon. 2019. Semantic fuzzing with zest. In Proceedings of the 28th ACM SIGSOFT International Symposium on Software Testing and Analysis. 329–340.
[37] Fan Pan, Ying Hou, Zheng Hong, Lifa Wu, and Haiguang Lai. 2013. Efficient model-based fuzz testing using higher-order attribute grammars. JSW 8, 3 (2013), 645–651.
[38] Van-Thuan Pham, Marcel Böhme, and Abhik Roychoudhury. 2016. Model-based whitebox fuzzing for program binaries. In Proceedings of the 31st IEEE/ACM International Conference on Automated Software Engineering. 543–553.
[39] Van-Thuan Pham, Marcel Böhme, Andrew Edward Santosa, Alexandru Razvan Caciulescu, and Abhik Roychoudhury. 2019. Smart greybox fuzzing. IEEE Transactions on Software Engineering 47, 9 (2019), 1980–1997.
[40] Paul Purdom. 1972. A sentence generator for testing parsers. BIT Numerical Mathematics 12, 3 (1972), 366–375. DOI:https://doi.org/10.1007/BF01932308
[41] Jesse Ruderman. 2007. Introducing Jsfunfuzz. (2007). Retrieved from http://www.squarefree.com/2007/08/02/introducing-jsfunfuzz/. Accessed: 13 August 2021.
[42] Koushik Sen, Darko Marinov, and Gul Agha. 2005. CUTE: A concolic unit testing engine for C. In Proceedings of the ESEC/FSE’05.
[43] Kosta Serebryany. 2016. Continuous fuzzing with libfuzzer and addresssanitizer. In Proceedings of the 2016 IEEE Cybersecurity Development. IEEE, 157–157.
[44] 2021. GitHub - google/libprotobuf-mutator: Library for structured fuzzing with protobuffers. Retrieved from https://github.com/google/libprotobuf-mutator. (2021). Accessed: 13 August 2021
[45] SweetScape Software. 2021. 010 Editor - Binary Template Repository - Download Binary Templates. Retrieved August 1, 2021 from https://www.sweetscape.com/010editor/repository/templates/. (2021).
[46] SweetScape Software. 2021. 010 Editor - Binary Templates - Parsing Binary Files. Retrieved August 1, 2021 from https://www.sweetscape.com/010editor/templates.html. (2021).
[47] SweetScape Software. 2021. 010 Editor - Pro Text/Hex Editor | Edit 160+ Formats | Fast & Powerful. Retrieved August 1, 2021 from https://www.sweetscape.com/010editor/. (2021).
[48] SweetScape Software. 2021. 010 Editor Manual - Writing Templates. Retrieved August 1, 2021 from https://www.sweetscape.com/010editor/manual/IntroTemplates.htm. (2021).
[49] Ezekiel Soremekun, Esteban Pavese, Nikolas Havrikov, Lars Grunske, and Andreas Zeller. 2020. Inputs from Hell: Learning input distributions for grammar-based test generation. IEEE Transactions on Software Engineering 48, 4 (2020), 1138–1153. DOI:https://doi.org/10.1109/TSE.2020.3013716
[50] Sören Tempel, Vladimir Herdt, and Rolf Drechsler. 2022. SISL: Concolic testing of structured binary input formats via partial specification. In Proceedings of the Automated Technology for Verification and Analysis: 20th International Symposium, ATVA 2022, Virtual Event, October 25–28, 2022, Proceedings. Springer, 77–82.
[51] William Underwood. 2012. Grammar-based specification and parsing of binary file formats. International Journal of Digital Curation 7, 03 (2012), 95–106. DOI:https://doi.org/10.2218/ijdc.v7i1.217
[52] Junjie Wang, Bihuan Chen, Lei Wei, and Yang Liu. 2017. Skyfire: Data-driven seed generation for fuzzing. In Proceedings of the 2017 IEEE Symposium on Security and Privacy. IEEE, 579–594.
[53] Junjie Wang, Bihuan Chen, Lei Wei, and Yang Liu. 2019. Superion: Grammar-aware greybox fuzzing. In Proceedings of the 41st International Conference on Software Engineering. IEEE, 724–735.
[54] Ming-Hung Wang, Han-Chi Wang, You-Ru Chen, and Chin-Laung Lei. 2017. Automatic test pattern generator for fuzzing based on finite state machine. Security and Communication Networks 2017, 1 (2017), 1–11.
[55] David HD Warren, Luis M Pereira, and Fernando Pereira. 1977. Prolog-the language and its implementation compared with Lisp. ACM SIGPLAN Notices 12, 8 (1977), 109–115.
[56] Jingbo Yan, Yuqing Zhang, and Dingning Yang. 2013. Structurized grammar-based fuzz testing for programs with highly structured inputs. Security and Communication Networks 6, 11 (2013), 1319–1330.
[57] Xuejun Yang, Yang Chen, Eric Eide, and John Regehr. 2011. Finding and understanding bugs in C compilers. In Proceedings of the 32nd ACM SIGPLAN Conference on Programming Language Design and Implementation. ACM, 283–294.
[58] Michał Zalewski. 2016. American Fuzzy Lop. Retrieved October 1, 2016 from http://lcamtuf.coredump.cx/afl. (2016).