فناوریهای فازینگ (Fuzzing) در سالهای اخیر با سرعت زیادی تکامل یافتهاند و توانستهاند با عمق و سرعتی روزافزون، باگهای (Bug) برنامهها را آشکار کنند. با این حال، برنامههایی که با قالبهای (Format) پیچیده کار میکنند، چالشبرانگیزتر هستند؛ زیرا ورودیها باید دارای ویژگیهای خاصِ وابسته به قالب باشند تا بتوانند از مرحلهی اولیهی تجزیه (parsing) عبور کرده و به رفتارهای عمیقتر برنامه دست یابند.
برخلاف رویکردهای پیشین که بر پایهی مشخصات قالب (Format specifications) نوشته شده به صورت دستی هستند، ما روشی را پیشنهاد میکنیم که به طور خودکار قادر به تولید و جهش (mutation) ورودیها برای قالبهای باینری ناشناخته و مبتنی بر قطعه (chunk) است. در این روش، وابستگیهای میان بایتهای ورودی و دستورات مقایسه شناسایی شده و از آنها برای تخصیص «تگ»هایی (Tag) استفاده میشود که منطق پردازش برنامه را توصیف میکنند. این تگها به عنوان واحدهای سازنده برای اعمال جهشهای آگاه از ساختار (structure-aware) بر روی قطعهها (chunk) و فیلدهای ورودی به کار میروند.
روش پیشنهادی ما میتواند عملکردی قابل مقایسه با روشهای فازینگ آگاه از ساختار که نیازمند دخالت انسان هستند ارائه دهد. پیادهسازی نمونهی ما، Weizz، موفق به کشف ۱۶ باگ ناشناخته در برنامههای پرکاربرد شده است.
۱. مقدمه
در سالهای اخیر، رشد چشمگیری در توسعهی تکنیکهای کارآمد برای آزمون فازینگ (Fuzz Testing) ـ که به اختصار فازینگ (fuzzing) نیز نامیده میشود ـ مشاهده شده است. بهطور خاص، رویکرد «فازینگ جعبه خاکستری مبتنی بر پوشش» (Coverage-based Grey-box Fuzzing یا CGF) به عنوان روشی بسیار مؤثر برای کشف باگها شناخته شده است؛ باگهایی که اغلب نشان دهندهی ضعفهای امنیتی هستند.
در دسترس بودن چارچوب فازینگ AFL [36] راه را برای شکلگیری حجم گستردهای از پژوهشها هموار کرد که نه تنها پیادهسازیهای کارآمدتری را پیشنهاد میدهند، بلکه تکنیکهای جدیدی نیز برای مواجهه با موانع رایج فازینگ —از جمله اعداد جادویی (magic numbers) و جمعآزما (checksum) در برنامهها— ارائه میکنند. با این حال، همچنان چندین سناریوی کاربردی متداول وجود دارد که حتی برای پیشرفتهترین فازرهای CGF نیز موارد دشواری محسوب میشوند.
فازرهای CGF با اعمال جهش (mutation) روی ورودیها در سطح نمایش بیت و بایت عمل میکنند و ورودیهای جهشیافته را زمانی «جالب» تلقی میکنند که منجر به اجرای بخشهای جدیدی از برنامه شوند. اگرچه این رویکرد برای ورودیهای فشرده و بدون ساختار [30] عملکرد بسیار خوبی دارد، اما در مواجهه با ورودیهای به شدت ساخت یافته —که باید از یک دستور زبان یا نوعی مشخصات تبعیت کنند— کارایی خود را از دست میدهد. به صورت شهودی، جهشهای «بیقاعده» باعث میشوند فازر زمان قابل توجهی را صرف تولید ورودیهایی کند که معمولاً در مراحل اولیهی تفسیر (pars) توسط برنامه رد میشوند؛ در نتیجه، بهبود پوشش کد بسیار ناچیز یا حتی صفر خواهد بود.
از این رو، پژوهشگران مشخصات (specification) ارائه شده توسط کاربر را به این فرآیند اضافه کردهاند تا ورودیهای معنادار تولید و اولویتبندی شوند. نسخههای پیشرفتهی CGF از این نوع، هم برای قالبهای مبتنی بر دستور زبان [4, 30] و هم برای قالبهای مبتنی بر قطعه (chunk) [24] در دسترس هستند؛ که نوع دوم به نظر میرسد در نرمافزارهای دنیای واقعی شیوع بیشتری داشته باشد.
نقاط ضعف این رویکرد به وضوح قابل مشاهده است. معمولاً برنامهها همراه با مشخصات قالبی که برای این منظور مناسب باشد ارائه نمیشوند. درخواست از کاربران برای نوشتن چنین مشخصاتی نیز با یکی از عوامل کلیدی موفقیت فازرهای CGF در تضاد است؛ یعنی اینکه این فازرها با حداقل دانش اولیه عمل میکنند.
علاوه بر این، چنین درخواستی میتواند پرهزینه باشد و در سناریوهای امنیتی —که کاربران با قالبهای اختصاصی یا مستندسازی نشده سروکار دارند — عملاً کاربرد چندانی ندارد. محدودیت دوم این است که اگر تنها ورودیهایی آزمایش شوند که کاملاً با مشخصات مطابقت دارند، ممکن است نادقیق بودنهای موجود در پیادهسازی از قلم بیفتند؛ در حالی که ورودیهایی که تا حدی خارج از این مشخصات هستند، میتوانند این نواقص را آشکار کنند [8].
رویکرد ما. ویژگی اصلی رویکرد پیشنهادی ما را میتوان اینگونه خلاصه کرد: ما تلاش میکنیم بر اساس نحوهی پردازش بایتهای ورودی توسط برنامه، ساختار احتمالی یک ورودی مبتنی بر قطعه (chunk) را یاد بگیریم. ابعادی از این ایده پیشتر در [8] نیز مطرح شده است، جایی که پوشش کد برای استنتاج ساختار گرامر به کار میرود، و تا حدی در برخی توسعههای عمومی فازینگ نیز مشاهده میشود. برای مثال، رهگیری آلودگی (taint tracking) میتواند مشخص کند کدام بایتهای ورودی در مقایسه با توالیهای جادویی (magic sequences) نقش دارند، در حالی که روابط بین ورودی به حالت (input-to-state relationships) میتوانند با استفاده از مقادیر مشاهده شده در عملگرهای مقایسه، فیلدهای جمعآزما (checksum) را نیز شناسایی کنند.
ما بر این شهود تکیه میکنیم که در میان مقایسههایی که روی دادههای مشتق شده از ورودی انجام میشوند، برخی را میتوان به طور نزدیک به یک بخش خاص از ساختار ورودی نسبت داد. همچنین بهصورت تجربی مشاهده کردهایم که ترتیب اجرای این بررسیها توسط برنامه میتواند جزئیات ساختاری، مانند محل مشخص کنندهی نوع (type specifier) یک قطعه (chunk) را آشکار کند.
در این رویکرد، ما بایتهای ورودی را با مقایسهای که بیشترین نماینده (most representative) را برای پردازش آنها دارد، برچسبگذاری میکنیم و به صورت ابتکاری و اکتشافی (heuristic)، مرزهای محتمل برای قطعهها (chunk) و فیلدها را استنتاج میکنیم. با استفاده از این خط لوله (pipeline) خودکار، میتوانیم جهشهای آگاه از ساختار را برای فازینگ جعبه خاکستری مبتنی بر قطعه [24] اعمال کنیم، بدون آنکه نیازی به مشخصات قالب ارائه شده توسط کاربر باشد.
ما کار را با شناسایی دستورالعملهای کاندید آغاز میکنیم که میتوان آنها را نسبت به یک بایت ورودی «مرتبط» در نظر گرفت. به جای استفاده از رهگیری آلودگی (taint tracking)، در هر بایت ورودی تغییرات بیتی (bit flipping) اعمال کرده، برنامه را اجرا میکنیم و برای عملوندهای موجود در محل دستورالعملهای مقایسه، یک بردار وابستگی (dependency vector) میسازیم.
سپس این وابستگیها را تحلیل میکنیم تا روابط بین ورودی و حالت (input-to-state relationships) و همچنین موانع (roadblocks) را شناسایی کرده و بر اساس آنها به بایتهای ورودی «تگ» اختصاص دهیم. فرآیند تخصیص تگ بر ویژگیهای مکانی (spatial) و زمانی (temporal) تکیه دارد، چرا که برنامهها معمولاً برای تجزیه (pars) اجزای متمایز، از دستورالعملهای متفاوت – در قالب مقایسه- استفاده میکنند.
برای رفع ابهام در موارد چندگزینهای، ما به دستورالعملهای قدیمیتر اولویت میدهیم، زیرا اعتبارسنجی قالب معمولاً در مراحل ابتدایی اجرای برنامه انجام میشود. در نهایت، این تگها فرآیند استنتاج یک ساختار تقریبی مبتنی بر قطعه (Chunk) از ورودی را هدایت میکنند و امکان اعمال جهشهای آگاه از ساختار را در مراحل بعدی فراهم میسازند.
برخلاف سناریوهای مهندسی معکوس که هدف آنها بازسازی مشخصات قالبهای ورودی [12, 18, 19] است، نتایج تجربی نشان میدهد که در اینجا فرآیند استنتاج لزوماً نیازی به دقت کامل ندارد؛ زیرا فازینگ میتواند تا حدی با نویز و عدمدقت کنار بیاید. نکتهی مهم دیگر این است که یک مشخصات الزاماً نحوهی پیادهسازی آن را مشخص نمیکند. توسعهدهندگان ممکن است کد را میان چندین قابلیت به اشتراک بگذارند و در نتیجه باگهای ظریفی ایجاد شود؛ برای مثال، ما این پدیده را در برنامههایی مشاهده کردهایم که با چندین قالب ورودی مختلف کار میکنند.
همچنین ممکن است نسخههای بهینهسازی شدهای از یک قابلیت خاص طراحی شود که مثلاً برای بازههای مشخصی از مقادیر یک فیلد ورودی بهینه شدهاند. از آنجا که ما در هر مرحله به یک نمونه ورودی تگ اختصاص داده و فرآیند استنتاج را انجام میدهیم، این امکان را داریم که هنگام تلاش برای افزایش پوشش کد، چنین الگوهایی را شناسایی کرده و مورد آزمون قرار دهیم.
مشارکتها. با بهرهگیری از اطلاعاتی که در جریان معمول کار یک فازر بهطور مستقیم در دسترس است، ما روشی را پیشنهاد میکنیم که قادر است ساختاری تقریبی برای ورودیهای مبتنی بر قطعه (chunk) استنتاج کرده و سپس آن را به صورت کاملاً خودکار دچار جهش (mutation) کند. در این مقاله، موارد زیر ارائه میشوند:
- یک تکنیک شناسایی وابستگی (dependency identification) که در مرحلهی جهشهای قطعی (deterministic mutation) در فازینگ جعبه خاکستری (Grey-Box Fuzzing) تعبیه شده است؛
- یک مکانیزم تخصیص تگ (tag assignment) که با استفاده از این وابستگیها، موانع فازینگ را برطرف کرده و از یک طرح استنتاج ساختار برای قالبهای مبتنی بر قطعه (chunk) پشتیبانی میکند؛
- یک پیادهسازی از این رویکرد با نام Weizz. در آزمایشهای ما، Weizz عملکردی بهتر یا همسطح با یک روش CGF مبتنی بر قطعه (که نیازمند مشخصات قالب است) ارائه میدهد و همچنین نسبت به چندین فازر عمومی، روی برنامههای مورد بررسی، عملکرد برتری دارد.
ما Weizz را به صورت متنباز در https://github.com/andreafioraldi/weizz-fuzzer در دسترس قرار دادهایم.
۲. پیشرفتهترین روشهای موجود (State of the Art)
در سالهای اخیر، حجم گستردهای از کارهای مرتبط [37] با فازینگ منتشر شده است. فازرهای جعبه خاکستری (Grey-Box) به تدریج جایگزین رویکردهای اولیهی فازینگ جعبه سیاه (Black-Box) شدهاند؛ در آن روشها تصمیمهای مربوط به جهش ورودی بدون در نظر گرفتن تأثیر آنها بر مسیر اجرای برنامه [29] انجام میشد.
فازینگ جعبه خاکستری مبتنی بر پوشش (Coverage-based Grey-box Fuzzing یا CGF) از ابزارگذاری با سربار کم یا سبک (lightweight instrumentation) برای اندازهگیری پوشش کد استفاده میکند. این اطلاعات برای تشخیص «جالب بودن» ورودیهای جهشیافته به کار میرود؛ به این صورت که تغییرات در جریان کنترل (control-flow) بررسی میشوند.
CGF در کشف باگها در نرمافزارهای واقعی [24] بسیار مؤثر است، اما در مواجهه با موانع (roadblocks) مانند اعداد جادویی (magic numbers) و جمعآزماها (checksums) با مشکل روبهرو میشود. همچنین هنگام جهش روی قالبهای ورودی ساختیافته مبتنی بر دستور زبان [8] یا مبتنی بر قطعه نیز ممکن است کارایی آن کاهش یابد.
در این بخش، عملکرد موتور رایج فازینگ جعبه خاکستری (CGF) را که Weizz بر پایهی آن توسعه یافته است، به همراه رویکردهای پیشنهادی اخیر که با چالشهای ذکرشده در بالا مقابله میکنند، توضیح خواهیم داد. در پایان نیز جایگاه Weizz را در این چشمانداز (مجموعه رویکردهای موجود) مورد بحث قرار میدهیم.
۲.۱ فازینگ مبتنی بر پوشش (Coverage-Based Fuzzing)
American Fuzzy Lop یا AFL یک فازر CGF بسیار شناخته شده است [36] و چندین کار پژوهشی نیز برای بهبود کارایی آن بر پایهی این ابزار توسعه یافتهاند. برای تولید ورودیهای جدید، AFL از یک صف (queue) شامل بذرهای (seed) اولیهی ارائه شده توسط کاربر و همچنین ورودیهای تولید شدهی قبلی استفاده میکند و دو مرحلهی جهش را اجرا میدهد. هر دو مرحلهی جهش – هم مرحلهی قطعی (deterministic) و هم مرحلهی غیرقطعی (havoc)- به دنبال بهبود پوشش کد و همچنین کشف توقفهای غیرعادی یا کرشها (Crash) هستند. AFL ورودیهایی را که باعث افزایش پوشش برنامه شوند به صف اضافه میکند و ورودیهای منجر به توقفهای غیرعادی را به عنوان شواهدی از وجود باگ به کاربر گزارش میدهد.
پوشش (Coverage). AFL برای هر برنامهی دودویی (binary)، ابزارگذاری (instrumentation) انجام میدهد تا هنگام عبور اجرای برنامه از یک شاخه (branch)، آن را رهگیری کند. برای ردیابی کارآمد تعداد دفعات عبور از هر شاخه، از یک نقشهی پوشش (coverage map) استفاده میکند که شاخهها را با یک هش از آدرسهای بلوکهای پایهی مبدأ و مقصد (source and destination basic block addresses) شاخصگذاری (index) میکند. AFL زمانی یک ورودی را «جالب» (interesting) در نظر میگیرد که مسیر اجرای برنامه به شاخهای برسد که تعداد دفعات عبور از آن، مقداری باشد که پیشتر مشاهده نشده است. برای محدود کردن تعداد ورودیهای تحت نظارت و حذف اجراهای احتمالاً مشابه، شمارش دفعات عبور از شاخهها نرمالسازی میشود (با استفاده از دستهبندیهای توانهای دو / power-of-two buckets) تا برای جستوجو قابل مدیریتتر شود.
جهشها (Mutations). مرحلهی قطعی (deterministic) در AFL، ورودی را به صورت ترتیبی پیمایش کرده و در هر موقعیت (position) مجموعهای از جهشها را اعمال میکند؛ از جمله تغییر بیت یا بایت (bit/byte flipping)، افزایش و کاهش حسابی مقادیر (arithmetic increments/decrements)، جایگزینی با ثابتهای رایج (مانند 0، -1، MAX_INT) یا مقادیری از یک دیکشنری ارائه شده توسط کاربر. AFL هر جهش را به صورت مستقل آزمایش میکند؛ به این صورت که برنامه را روی ورودی حاصل اجرا کرده و پوشش (coverage) را بررسی میکند، سپس تغییر را به حالت اولیه بازمیگرداند و به موقعیت بعدی میرود.
در مقابل، مرحلهی غیرقطعی (havoc) در AFL یک دنباله (یا پشتهای) غیرقطعی از جهشها را پیش از اجرای برنامه اعمال میکند. در این مرحله، تعداد جهشها بهصورت تصادفی (بین ۱ تا ۲۵۶) انتخاب میشود و شامل عملیاتی مانند تغییر، افزایش، کاهش، حذف و موارد مشابه است که در موقعیتهای تصادفی داخل ورودی انجام میشوند.
۲.۲ موانع (Roadblocks)
موانع (roadblocks) الگوهای مقایسهای روی ورودیهایی میباشند که به طور ذاتی عبور از آنها با جهشهای کور (blind mutations) بسیار دشوار است. دو نمونهی رایج از این موانع عبارتاند از: اعداد جادویی (magic numbers) که معمولاً در فیلدهای هدر (header fields) دیده میشوند، و جمعآزماها (checksums) که بهطور معمول برای بررسی یکپارچگی دادهها استفاده میشوند.
دیکشنریهای وابسته به قالب (format-specific dictionaries) میتوانند در مواجهه با اعداد جادویی کمک کننده باشند، اما همچنان فازر باید تشخیص دهد این مقادیر دقیقاً در کجا باید در ورودی قرار بگیرند. در طول سالها، پژوهشگران چندین رویکرد برای مدیریت اعداد جادویی و جمعآزماها در فازرهای جعبهخاکستری ارائه کردهاند.
پروفایلسازی زیردستور (Sub-instruction Profiling). اگرچه درک اینکه چگونه مقدار زیادی از منطق برنامه میتواند در یک مقایسهی واحد (comparison) رمزگذاری شود کار سادهای نیست، اما میتوان مقایسههای چندبایتی را به بررسیهای تکبایتی [1] (یا حتی تکبیتی [22]) شکست تا هنگام تلاش برای تطبیق مقادیر ثابت، بتوان پیشرفت را دقیقتر ردیابی کرد.
ابزارهای LAF-Intel و CompareCoverage افزونههای کامپایلری هستند که باینریهایی مناسب برای این نوع فازینگ تولید میکنند. HonggFuzz [28] این تکنیک را زمانی بهکار میگیرد که کد منبع در دسترس باشد، در حالی که ++AFL [16] میتواند بهطور خودکار در زمان فازینگ، باینریها را تبدیل کند. Steelix [21] از تحلیل ایستا (static analysis) استفاده میکند تا مقایسههایی را که احتمالاً بیاهمیت هستند از فرایند پروفایلسازی حذف کند.
اگرچه پروفایلسازی زیردستور برای عبور از تستهایی مانند اعداد جادویی (magic number) مفید است، اما در مواجهه با جمعآزماها کارایی ندارد.
تحلیل آلودگی (Taint Analysis). تحلیل آلودگی پویا (Dynamic Taint Analysis – DTA) [26] ردیابی میکند که کدام بخشهای ورودی و در چه زمانی بر دستورهای برنامه تأثیر میگذارند. Vuzzer از DTA برای بررسی اعداد جادویی در کد باینری استفاده میکند. این مقایسهها معمولاً به صورت دستورهای مقایسهای هستند که در آن یکی از عملوندها وابسته به ورودی است و دیگری مقدار ثابت دارد. در این حالت، Vuzzer [25] مقدار ثابت را در همان بخشی از ورودی قرار میدهد که مستقیماً به عملوند وابسته به ورودی انتشار (propagate) پیدا میکند.
Angora [10] دو بهبود مهم ارائه میدهد. اول اینکه میتواند اعداد جادویی را که در ورودی پیوسته (contiguous) نیستند، با استفاده از یک شکل چندبایتی از DTA شناسایی کند و دوم از گرادیان نزولی (gradient descent) برای جهش کارآمد بایتهای آلوده (tainted bytes) استفاده میکند. با این حال، این روش نیازمند تغییرات کامپایلری (compiler transformations) است تا چنین جهشهایی امکانپذیر شوند.
اجرای نمادین (Symbolic Execution). تکنیکهای تحلیل آلودگی (DTA) در بهترین حالت میتوانند آزمونهای مربوط به جمعآزما (checksum) را شناسایی کنند، اما اطلاعات کافی برای حل آنها ارائه نمیدهند. چندین رویکرد (برای مثال [23, 27, 31, 35]) از اجرای نمادین (Symbolic Execution) [6] برای شناسایی و تلاش جهت حل محدودیتهای پیچیدهی ورودی که در موانع عمومی (roadblocks) وجود دارند استفاده میکنند.
TaintScope با استفاده از DTA، جمعآزماهای احتمالی را شناسایی کرده، آنها را برای ادامهی فازینگ موقتاً حذف (patch) میکند و سپس تلاش میکند ورودیها را با اجرای نمادین اصلاح (repair) کند. این روش نیازمند آن است که کاربر بذرهای (seed) مشخصی ارائه دهد و همچنین در معرض مثبتهای کاذب (false positives) قرار دارد [23, 27].
تحلیلهای تقریبی (Approximate Analyses). یک روند اخیر، بررسی راهحلهایی است که به صورت تقریبی همان اطلاعاتی را استخراج میکنند که تحلیل آلودگی (DTA) یا اجرای نمادین میتواند فراهم کند، اما با سرعت بالاتر. RedQueen [5] بر این مشاهده تکیه دارد که در بسیاری از موارد، بایتهای ورودی بهصورت مستقیم یا پس از کدگذاریهای ساده (مثل جابهجایی برای سازگاری با ترتیب بایتها (endianness)) وارد عملوندهای دستورالعملها میشوند. این تطابق ورودی-به-حالت (Input-to-State یا I2S) میتواند به جای DTA و اجرای نمادین برای مقابله با بایتهای جادویی (magic bytes)، مقایسههای چندبایتی و جمعآزماها مورد استفاده قرار گیرد.
RedQueen با تغییر بایتهای ورودی به مقادیر تصادفی (colorization)، نوعی تقریب از DTA انجام میدهد تا آنتروپی یا میزان بینظمی ورودی افزایش یابد. سپس به دنبال الگوهای همخوانی میان عملوندهای مقایسه و بخشهای ورودی میگردد؛ الگوهایی که نشان دهندهی یک وابستگی هستند.
وقتی هر دو عملوند یک مقایسه تغییر کنند، اما فقط برای یکی از آنها رابطهی ورودی-به-حالت (I2S) وجود داشته باشد، RedQueen آن را به عنوان یک آزمون احتمالی جمعآزما در نظر میگیرد. سپس این عملیات را موقتاً وصله (patch) میکند و سپس تلاش میکند ورودی را با استفاده از حدسهای هوشمندانه —که شامل جهش مقادیر مشاهده شدهی عملوند مقایسه است— بازسازی کند. ترتیب وضعیت وصلهها نیز برای مدیریت جمعآزماهای تو در تو به کار میرود.
SLF [34] تلاش میکند تحلیل وابستگی انجام دهد تا بتواند زمانی که هیچ تست معناداری در دسترس نیست، بذرهای (seed) ورودی معتبر تولید کند. این روش با یک ورودی تصادفی کوچک شروع میکند و در هر بایت، بیتها را به صورت جداگانه تغییر میدهد و برنامه را اجرا میکند تا مقادیر عملوندهای مقایسه جمعآوری شوند.
اگر بایتهای متوالی ورودی در طول این جهشها بر همان مجموعه از مقایسهها تأثیر بگذارند، آنها به عنوان بخشی از یک فیلد واحد در نظر گرفته میشوند. سپس SLF از اکتشاف روی مقادیر مشاهدهشده استفاده میکند تا بررسیها را بر اساس رابطهشان با ورودیهای فعال شده طبقهبندی کند. تمرکز اصلی آن شناسایی آفستها (offset)، شمارندهها (counts) و طول فیلدهای ورودی است، زیرا این موارد معمولاً برای تحلیلگرهای نمادین دشوار هستند.
T-Fuzz یک گام فراتر میرود و نیاز به بذرهای خاص را حذف میکند. همچنین مجموعهی بررسیهای غیرفعال شده در طول فازینگ را به هر نوع بررسی «سلامتسنجی» (sanity check) غیرحیاتی که عبور از آن برای فازر دشوار است اما برای محاسبات اصلی ضروری نیست، گسترش میدهد.
برای مثال، اعداد جادویی (magic numbers) را میتوان با اطمینان نادیده گرفت تا فازینگ بهتر انجام شود، در حالی که بررسی طول یک فیلد نباید حذف شود. همانند TaintScope، ورودیهایی که باعث از کار افتادن برنامه (Crash) میشوند، با استفاده از اجرای نمادین (Symbolic Execution) یا تحلیل دستی (manual analysis) اصلاح میشوند.
Driller در عوض از اجرای نمادین به عنوان یک استراتژی جایگزین برای کاوش ورودیها استفاده میکند؛ به این صورت که زمانی که فازینگ در یک بازهی مشخص هیچ بهبود پوششی ایجاد نمیکند، به اجرای نمادین سوئیچ میکند. بهطور مشابه، QSYM بر پایهی اجرای همزمان نمادین و با استفاده از ابزارگذاری دودویی پویا (dynamic binary instrumentation) ساخته شده است تا میان جامعیت (exhaustiveness) و سرعت یک توازن برقرار کند.
جدول ۱. مقایسه با رویکردهای مرتبط:
Eclipser در زمینهی فازرهای کونکولیک (concolic fuzzers)، محدودیتهای مسیر (path constraints) روی هر بایت ورودی را تا حدی سادهسازی (relax) میکند. این ابزار یک برنامه را چندین بار اجرا میکند و در هر بار، با جهش دادن (mutate) تکتک بایتهای ورودی، شاخههایی (branches) که تحت تأثیر قرار میگیرند را شناسایی میکند. سپس محدودیت حاصل از این شاخهها را جمعآوری میکند، اما تنها روابط خطی (linear) و یکنواخت (monotone) در نظر گرفته میشوند؛ زیرا سایر انواع محدودیتها معمولاً نیازمند یک حلکنندهی کامل SMT (SMT solver) هستند.
در ادامه، Eclipser یکی از شاخهها را انتخاب کرده و محدودیتهای مربوط به آن را بازمیگرداند (flip) تا یک ورودی جدید تولید شود، که این فرایند نوعی شبیهسازی از اجرای نمادین پویا (dynamic symbolic execution) [6] است.
۲.۳ فازینگ آگاه از قالب (Format-Aware Fuzzing)
تکنیکهای کلاسیک CGF در مواجهه با قالبهای ورودی ساختیافته که در فایلها مشاهده میشوند، بخشی از کارایی خود را از دست میدهند. از آنجا که جهشها در سطح نمایش بیتهای ورودی انجام میشوند، این روشها بهسختی میتوانند تغییرات ساختاری لازم برای کشف بخشهای جدید از منطق پردازش داده در یک برنامه را ایجاد کنند.
با این حال، آگاهی از ساختار قالب (بندی) (format awareness) میتواند عملکرد CGF را بهبود دهد. در ادبیات این حوزه، میتوان بین دو دسته تکنیک تمایز قائل شد: روشهایی که قالب بندیهای مبتنی بر دستور زبان (grammar-based) را هدف قرار میدهند، که در آنها ورودیها مطابق یک دستور زبان هستند، و روشهای مبتنی بر قطعه (chunk)، که در آنها ورودیها از یک ساختار درختی پیروی میکنند و شامل دادههای قطعه-مانند شبیه ساختارهای C هستند که هر گره (node) را تشکیل میدهند.
فازینگ مبتنی بر دستور زبان (Grammar-Based Fuzzing). LangFuzz [17] ورودیهای معتبر برای یک تجزیه کننده JavaScript را با استفاده از یک دستور زبان تولید میکند و به صورت جعبه سیاه (Black-Box)، قطعات کد نمونه و موارد آزمون (Test Case) را با هم ترکیب میکند. Nautilus [4] و Superion [30] از جدیدترین پیشنهادهای فازینگ جعبه خاکستری (Gray-Box) هستند که میتوانند تجزیه کنندههای زبان را بدون نیاز به مجموعهی بزرگی از ورودیهای معتبر یا قطعات آماده، و تنها با یک فایل دستور زبان ANTLR مورد آزمون قرار دهند.
Grimoire در ادامه این نیاز به مشخصات دستور زبان را حذف میکند. این روش با الهام از RedQueen، قطعاتی را از مجموعهی اولیهی ورودیها که باعث ایجاد پوشش جدید میشوند شناسایی کرده و آنها را از بخشهایی که باعث از دست رفتن پوشش نمیشوند جدا میکند. Grimoire این شکافها را ثبت میکند و سپس تلاش میکند به صورت بازگشتی (recursively) بخشهایی را که در موقعیتهای دیگر مشاهده شدهاند درون ورودیها درج کند؛ به این ترتیب، ترکیبهای مشابه دستور زبان را شبیهسازی میکند.
فازینگ مبتنی بر قطعه (Chunk-Based Fuzzing). بایستی اذعان داشت که Spike [3] به کاربران اجازه میدهد پروتکل شبکهی مورد استفاده در یک برنامه را توصیف کنند تا فازینگ جعبه سیاه بهبود یابد. Peach [15] این ایده را تعمیم میدهد و با استفاده از یک مشخصهی ورودی تعریف شده توسط کاربر (که با نام peach pit شناخته میشود)، جهشهای آگاه از قالب را روی مجموعهای اولیه از ورودیهای معتبر اعمال میکند. از آنجا که این فازرها نسبت به ورودی آگاه هستند، در برخی منابع به آنها «فازرهای هوشمند جعبه سیاه (black-box fuzzers smart) [24] گفته میشود.
با این حال، یک نسخهی هوشمند جعبه خاکستری (smart grey-box variant) میتواند نسبت به آنها عملکرد بهتری داشته باشد، زیرا به دلیل نبود بازخورد (مانند پوشش کد)، این روشها ورودیهای جالب را بهطور مداوم مورد جهش قرار نمیدهند. AFLSmart [24] این فرضیه را تأیید میکند و با افزودن جهشهای هوشمند (یا مرتبه بالا) به AFL، عملیات افزودن، حذف و جایگزینی قطعهها (chunk) در ورودی را انجام میدهد.
AFLSmart با استفاده از peach pit، یک ساختار مجازی از ورودی فعلی نگه میدارد که به صورت یک درخت مفهومی مدل میشود؛ در این درخت، گرههای داخلی نشان دهندهی قطعهها (chunk) و برگها نشاندهندهی مشخصه یا ویژگیها (attribute) هستند. قطعهها با موقعیت شروع و پایان در ورودی، نوع قالبمحور، و فهرستی از ویژگیها و قطعههای تودرتو تعریف میشوند. ویژگی (attribute) یک فیلد است که میتواند بدون تغییر ساختار کلی، جهش یابد.
افزودن قطعه (chunk) شامل اضافه کردن یک قطعه به عنوان یک همسطح یا همتراز (sibling) است که از ورودی دیگری گرفته میشود و هر دو دارای والد (parent) همنوع هستند. حذف قطعه نیز به معنای حذف بخش متناظر از بایتهای ورودی است.
جایگزینی قطعه (Chunk splicing)، دادههای داخل یک قطعه را با استفاده از قطعهای از همان نوع از یک ورودی دیگر جایگزین میکند.
از آنجایی که ساختار مجازی (virtual structure) پرهزینه است، AFLSmart انجام جهشهای هوشمند (smart mutations) را بر اساس مدت زمانی که از آخرین کشف مسیر جدید گذشته به تعویق میاندازد؛ زیرا اگر این جهشها روی هر ورودی امتحان شوند، AFLSmart از فازینگ کلاسیک جعبه خاکستری (grey-box fuzzing) عقب میماند.
۲.۴ بحث (Discussion)
جدول ۱ نشان میدهد که Weizz در میان تکنیکهای فازینگ جعبه خاکستری (CGF) معرفی شده در بخشهای قبلی در کجای وضعیت موجود (state of the art) قرار میگیرد. فازرهای مدرن CGF عمومی (که برای آنها یک زیرمجموعهی نماینده شامل پنج مورد اول انتخاب شده است) قادر به مدیریت اعداد جادویی (magic bytes) هستند، اما تنها RedQueen یک راهحل مؤثر برای جمعآزماهای (checksum) عمومی ارائه میدهد.
در زمینهی فازرهای CGF مبتنی بر دستور زبان، Grimoire در حال حاضر تنها راه حل کاملاً خودکار (یعنی بدون نیاز به مشخصات قالب) محسوب میشود و میتواند هم اعداد جادویی و هم جمعآزماها را مدیریت کند، که این قابلیت را مدیون زیرساخت مبتنی بر RedQueen است. در این میان، ما با Weizz همین نوع بهبودها را به حوزهی قالبهای مبتنی بر قطعه (chunk) تعمیم میدهیم و روشی ارائه میکنیم که همزمان میتواند اعداد جادویی و جمعآزماها را مدیریت کند، و همچنین نیاز به مشخصات قالب -که در AFLSmart وجود دارد- را حذف میکند.
۳. روششناسی (Methodology)
منطق فازینگ در Weizz شامل دو مرحله است که در شکل ۱ نمایش داده شدهاند. هر دو مرحله از یک صف مشترک (shared queue) استفاده میکنند که شامل ورودیهایی است که در تکرارهای قبلی هر یک از این دو مرحله پردازش شدهاند.
«مرحلهی جراحی» (surgical stage) وابستگیها میان ورودی و مقایسههایی که در برنامه انجام میشوند را شناسایی میکند. این مرحله این وابستگیها را با قرار دادن تگهایی روی بایتهای ورودی خلاصه میکند و سپس تنها روی بایتهایی که مشخص میشود بر عملوندهای دستورات مقایسه تأثیر دارند، جهشهای قطعی (deterministic mutations) اعمال میکند.
مرحلهی «آگاه از ساختار» (structure-aware stage) رفتار غیرقطعی (nondeterministic) مشابه AFL را گسترش میدهد و با استفاده از تگهای از پیش اختصاصیافته، محل فیلدها و قطعهها را در ورودی استنتاج کرده و آنها را جهش میدهد. در بخشهای بعدی، جزئیات عملکرد داخلی این دو مرحله بهطور کامل توضیح داده خواهد شد.
۳.۱ مرحلهی جراحی (Surgical Stage)
مرحلهی جراحی، بخش قطعی (deterministic) عملکرد AFL را جایگزین میکند و در این فرآیند، اطلاعات وابستگی مربوط به دستورالعملهای مقایسه را که در دسترس فوری یک فازر قرار دارد، استخراج میکند. Weizz این اطلاعات را برای مرحلهی بعد با قرار دادن تگهایی روی بایتهای ورودی خلاصه میکند و همچنین از آن برای شناسایی روابط ورودی-به-حالت (Input-to-State یا I2S) (بخش ۲.۲) و تشخیص جمعآزماها (checksum) استفاده میکند.
این تحلیل زمینهمحور (context-sensitive) است؛ یعنی هنگام تحلیل یک مقایسه، زمینهی فراخوانی آن نیز در نظر گرفته میشود [14]. محل (site) یک مقایسه بهصورت حاصل XOR انحصاری (exclusive OR) بین آدرس دستورالعمل و کلمهای که برای کدگذاری زمینهی فراخوانی استفاده شده است، محاسبه میشود.
Weizz همچنین یک ساختار سراسری به نام CI نگه میدارد تا دستورالعملهای مقایسهای را ردیابی کند که تحلیل یک یا چند ورودی نشان داده ممکن است در آزمونهای جمعآزما نقش داشته باشند. برای تکمیل توضیح هر بخش از مرحلهی اصلاح، از مثال جاری شکل ۲ استفاده خواهیم کرد. پیش از بررسی جزئیات هر جزء، ابتدا یک نمای کلی از جریان کاری این مرحله هنگام ورود یک ورودی ارائه میکنیم.
۳.۱.۱ نمای کلی (Overview)
با در نظر گرفتن یک ورودی (I) که از صف انتخاب شده است، Weizz تلاش میکند وابستگیها میان هر بیت از (I) (بهصورت جداگانه) و دستورالعملهای مقایسه در برنامه را تعیین کند. رویهی GetDeps دو ساختار داده ایجاد میکند که هر دو با استفاده از یک تابع هش و بر اساس «مکانهای مقایسه» (comparison sites) شاخص گذاری (Index) میشوند.
یک جدول مقایسه به نام CT (Comparison Table) مقادیر عملوندهای مربوط به نمونههای مشاهده شده از دستورالعملهای مقایسه در مکانهای مختلف را ذخیره میکند. در کنار آن، برای این عملوندها، ساختار Deps مشخص میکند که کدام بایتهای ورودی میتوانند بر آنها تأثیر بگذارند (یعنی وابستگی ورودی به آن عملوندها را نگه میدارد).
سپس Weizz به تحلیل اطلاعات ثبت شده میپردازد. برای هر محل (site)، این مرحله روی هر نمونهی موجود در جدول CT تکرار میشود و به دنبال وابستگیهای ورودی-به-حالت (I2S) (رویهی DetectI2S 7 → R) و اطلاعات مربوط به جمعآزماها (MarkChecksums 7 → CI) میگردد، و بایتهایی را که میتوانند عملوندهای مقایسه را تغییر دهند، با استفاده از مقادیر ثبت شده جهش میدهد (FuzzOperands).
سپس مرحله وارد فاز ساخت تگها میشود؛ در ابتدا همهی بایتهای ورودی بدون تگ هستند. این مرحله، مکانهای مقایسه (comparison sites) را بر اساس زمانی که برای اولین بار مشاهده شدهاند مرتب کرده و آنها را به همین ترتیب پردازش میکند. رویهی PlaceTags به هر بایت ورودی یک تگ اختصاص میدهد، با در نظر گرفتن روابط I2S (R)، اطلاعات جمعآزما (CI)، وابستگیهای اولیهی محاسبه شده (Deps)، و دادههای مرتبط با سایتهای مقایسه (CT).
مانند سایر فازرها [5, 31]، Weizz جمعآزماها را با وصله کردن دستورالعملهای مرتبط در هنگام شناسایی آنها اعمال میکند و فرآیند اصلاح ورودی (FixChecksums) که برای برآورده کردن شرط جمعآزما بدون تغییر باقیماندن آن لازم است، را به انتهای مرحلهی جراحی موکول میکند. در این راستا، از تکنیکی بسیار مشابه روش RedQueen (بخش ۲.۲) استفاده میشود.
خروجی مرحلهی اصلاح، ورودیای است که با تگهای کشف شده نشانهگذاری شده و برای برآورده کردن جمعآزماها اصلاح شده است؛ این ورودی سپس وارد صف میشود تا توسط مرحلهی آگاه از ساختار (structure-aware stage) پردازش شود. رویههایی که نام آنها بهصورت زیرخطدار آمده است، در این مقاله تنها بهصورت غیررسمی توضیح داده شدهاند؛ خواننده میتواند شبهکد کامل آنها را در گزارش فنی آنلاین تکمیلی مشاهده کند.
۳.۱.۲ شناسایی وابستگی (Dependency Identification)
یکی از ویژگیهای کلیدی که یک فازر را مؤثر میسازد، توانایی درک این موضوع است که جهشهای اعمال شده روی ورودی چگونه بر اجرای برنامه تأثیر میگذارند. در این مقاله، ما مسیر تحلیلهای سریع را دنبال میکنیم تا اطلاعات وابستگی تقریبی را استخراج کنیم. روشی را پیشنهاد میکنیم که مشخص میکند کدام بایتهای ورودی بهصورت مستقل بر دستورالعملهای مقایسه تأثیر میگذارند. این روش میتواند حقایق I2S را مشابه تکنیک RedQueen تشخیص دهد، اما همچنین روابط غیر I2S را نیز پوشش میدهد که در عمل برای تخصیص تگ به بایتهای ورودی به همان اندازه مهم هستند.
الگوریتم ۱ رویهی GetDeps را که برای این هدف استفاده میشود، توصیف میکند. در ابتدا، برنامه روی ورودی فعلی اجرا میشود و تمام دستورالعملهای مقایسه ابزارگذاری (instrumentation) میگردند. خروجی این مرحله یک جدول مقایسه (CT) است که عملوندهای مربوط به آخرین (|J|) نمونه (instance) از هر مکان مقایسه را ثبت میکند. برای هر مکان مقایسه، جدول CT یک برچسب زمانی (timestamp) نگه میدارد که مشخص میکند RunInstr چه زمانی برای اولین بار آن مکان را مشاهده کرده است، و همچنین تعداد دفعات مواجهه با آن مکان در طول اجرا را ثبت میکند (Hits در خط ۷).
الگوریتم ۱: مرحله شناسایی وابستگی:
CT : Sit es × J × {op1, op2 } → V [cmp site instance & operand → value]
Deps: Sit es × J × {op1, op2 } → A [cmp site inst. & operand → array of |input| booleans]
function GetDeps(I ):
CT ← RunInstr(I)
foreach b ∈ {0 ... len(I)-1} do
Deps(s, j, op)[b] ← false ∀ (s, j, op) ∈ dom(CT)
foreach k ∈ {0 ... 7} do
CT′ ← RunInstr(BitFlip(I, b, k))
foreach (s, j, op) ∈ dom(CT) do
if Hits(CT, s) , Hits(CT′, s) then continue
Deps(s, j, op)[b] ← Deps(s, j, op)[b] ∨ CT(s, j, op) , CT′(s, j, op)
return CT, Deps
سپس Weizz تلاش میکند مشخص کند کدام بایتهای ورودی، به صورت مستقیم یا از طریق مقادیر مشتق شده، در تولید عملوندهای موجود در سایتهای مقایسه نقش دارند. با تغییر دادن بایتها بهصورت تکبهتک، Weizz بررسی میکند که آیا در اثر هر جهش، مقادیر عملوندهای یک مکان مقایسه تغییر میکنند یا نه. الگوریتم با پیمایش بیتهای هر بایت، آنها را یکییکی باز میگرداند (flip) و در هر بار، یک جدول جدید ′CT از اجرای ابزارگذاری شده با ورودی جدید به دست میآورد (خط ۵).
اگر مقدار یک بایت در ′CT نسبت به CT تغییر کرده باشد، آن بایت به عنوان وابسته (dependency) برای عملوند یک نمونه از یک مکان مقایسه علامتگذاری میشود (خط ۸). با این حال، تغییر بیتها ممکن است باعث انحراف در اجرای برنامه شود؛ زیرا برخی مقایسهها میتوانند مسیر برنامه را به شاخههای متفاوتی هدایت کنند (و در صورتی که چنین ورودیای «جالب» باشد، به صف اضافه میشود؛ رجوع شود به بخش ۲.۱).
پیش از به روزرسانی اطلاعات وابستگی، بررسی میکنیم که آیا یک محل مقایسه s در دو اجرای مختلف، تعداد متفاوتی از نمونهها را مشاهده کرده است یا نه (خط ۷). این معیار به عنوان یک شاخص استفاده میشود تا مشخص گردد آیا اجرا در سطح همان مکان دچار واگرایی محلی شده است یا خیر.
ما یک سیاست محلی (local policy) را بهجای الزام بر برابری کامل تعداد نمونهها در همهی مکانهای موجود در CT و ′CT ترجیح میدهیم. در واقع، برای یک ورودی جهشیافته ممکن است برخی بخشهای کد شاهد تغییر در عملوندهای مقایسه باشند (که نشاندهندهی وابستگی است)، بدون اینکه انحراف کنترل جریان (control-flow divergence) رخ دهد؛ در حالی که در بخشهای دیگر ممکن است دو اجرا آنقدر متفاوت باشند که اساساً امکان بررسی همبستگی وجود نداشته باشد.
مثال:
در این بخش، یک کد سادهی تفسیر (Figure 2a) برای یک قالب ورودی سفارشی را بررسی میکنیم که شامل سه فیلد id، size و checksum است. هر یک از این فیلدها با ۲ بایت نمایش داده میشوند و یک فیلد داده (data) با طول متغیر نیز وجود دارد که طول آن توسط فیلد اندازه (size) تعیین میشود (شکل 2b).
تجزیه کننده (Parser) ما به عنوان ورودی، یک اشارهگر input به بایتهای ورودی دریافتی و همچنین تعداد آنها (len) را دریافت میکند. فرض میشود که بافر ورودی حداقل ۶ بایت دارد، که حداقل اندازه برای یک ورودی معتبر با دادهی خالی است. کد به صورت زیر عمل میکند:
- فیلدهای شناسه (id) و اندازه (size) را از بافر میخواند؛
- اعتبار فیلد شناسه را بررسی میکند (برچسب CMP_A);
- بررسی میکند که آیا بافر حداقل شامل 6+size بایت هست تا بتواند فیلد داده و سایر فیلدها را نگه دارد (برچسب CMP_B);
- مقدار جمعآزما را با پیمایش روی بایتهای مربوط به شناسه (id)، اندازه (size) و داده (data) محاسبه میکند (برچسب CMP_C);
- جمعآزما مورد انتظار را از بافر میخواند و آن را با مقدار محاسبه شده مقایسه و اعتبارسنجی میکند (برچسب CMP_D).
شکل 2c نمونههای مقایسهای (comparison instances) را نشان میدهد که توسط Weizz هنگام اجرای تابع parser یا تجزیه کننده روی بذر (seed) اولیه جمعآوری شدهاند. برای اختصار، برچسبهای زمانی (timestamp) و تعداد hitها حذف شدهاند و فرض شده است که زمینهی فراخوانی (calling context) اهمیتی ندارد؛ بنابراین میتوان از برچسبهای مقایسه برای شناسایی مکانها (site) استفاده کرد. برای مکان مربوط به CMP_C چندین نمونه مشاهده میشود، زیرا این دستور درون یک حلقه چندین بار اجرا میشود.
شکل 2d نمونههای مقایسهای را پس از تغییر یک بیت در اولین بایت بذر (seed) نشان میدهد، که تغییرات بصورت پررنگ مشخص شدهاند. با این تغییرات، Weizz تشخیص میدهد که اولین عملوند در هر دو مقایسهی CMP_A و CMP_D تحت تأثیر قرار گرفته است؛ که این موضوع وابستگی بین این دو مکان و اولین بایت ورودی را آشکار میکند.
با ادامهی تغییرات (flips)، ورودیهایی تولید میشوند که وابستگیهای بیشتری را آشکار میکنند:
- عملوند اول CMP_A به بایتهای شناسه (id) وابسته است؛
- عملوند اول CMP_B به بایتهای سایز (size) وابسته است؛
- عملوند دوم CMP_C به بایت اول سایز (size) وابسته است؛
- عملوند اول CMP_D به بایتهای شناسه (id) و داده (data) و همچنین به بایت اول سایز (size) ابسته است؛ در حالی که عملوند دوم آن تحت تأثیر بایتهای فیلد جمعآزما قرار دارد.
۳.۱.۳ تحلیل نمونههای محلهای مقایسه (Analysis of Comparison Site Instances)
پس از ساختن وابستگیها، Weizz شروع به پردازش دادههای ثبت شده برای حداکثر (|J|) نمونه (instance) از هر مکان (site) مقایسه میکند. اولین مرحلهی این تحلیل، شناسایی تطابقهای I2S با استفاده از DetectI2S است. RedQueen از این مفهوم برای مقابله با موانع (roadblocks) استفاده میکند؛ بر این اساس که بخشی از ورودی میتواند بهصورت مستقیم وارد وضعیت برنامه در حافظه یا ثباتها شود و در مقایسهها مورد استفاده قرار گیرد (بخش ۲.۲).
برای مثال، بایتهای جادویی (magic bytes) که در هدرها یافت میشوند، معمولاً همانطور که هستند یا پس از یک تبدیل سادهی کدگذاری [5] در دستورالعملهای مقایسه شرکت میکنند. ما DetectI2S را روی هر عملوند در هر نمونه از محل مقایسه اعمال میکنیم و ساختار دادهی R را با حقایق جدید I2S که کشف شدهاند، بهروزرسانی میکنیم.
MarkChecksums شامل شناسایی توالیهای محاسبهی جمعآزماها (Checksum) است. مشابه RedQueen، ما یک دستور مقایسه را زمانی به عنوان «احتمالاً مربوط به جمعآزما» علامتگذاری میکنیم که شرایط زیر برقرار باشند:
(۱) یکی از عملوندها دارای رابطهی I2S باشد و اندازهی آن حداقل ۲ بایت باشد؛
(۲) عملوند دیگر I2S نباشد و GetDeps وابستگیهایی از آن به برخی بایتهای ورودی را آشکار کرده باشد؛
(۳) مقدار زیر برقرار باشد (یعنی مجموعههای وابستگی بایتی آنها از هم مجزا (disjoint) باشند):
∧b (Deps (s, j, op1) [b], Deps (s, j, op2) [b]) = false
ایدهی اصلی این است که کد، عملوند I2S (که فرض میشود مقدار مورد انتظار است) را با مقداری که از بایتهای ورودی مشتق شده مقایسه میکند، و این بایتها بر عملوند I2S تأثیر نمیگذارند؛ در غیر این صورت یک وابستگی حلقوی (circular dependency) مشاهده میشد. ما حداقل اندازهی ۲ بایت را برای کاهش مثبتهای کاذب انتخاب میکنیم.
مانند کارهای پیشین، آزمونهای جمعآزمای داوطلب را وصله میکنیم تا همیشه برقرار شوند، و هم شناسایی موارد مثبت کاذب و هم فرآیند اصلاح ورودی (input repairing) مورد نیاز برای برآورده ساختن شرطهای نسخهی اصلیِ بدون وصله را به مرحله بعدی موکول میکنیم.
در نهایت، FuzzOperands به جای جهشهای قطعی AFL، از جایگزینیهای بایت جراحی (surgical byte substitutions) استفاده میکند که بر اساس دادههای مشاهده شده در محلهای مقایسه هدایت میشوند. برای هر بایت ورودی، تمام ورودیهای جدول CT (یعنی هر عملوند از هر نمونه در محل مقایسه) که آن بایت بر آنها تأثیر میگذارد، تعیین میشود.
سپس بایت موردنظر را جایگزین میکنیم و بسته به اندازه عملوند (operand)، بایتهای اطراف آن را نیز با استفاده از مقداری که برای عملوند دیگر ثبت شده است تغییر میدهیم. این جایگزینی میتواند مقدار را به همان صورت استفاده کند، آن را برای ترتیب بایتها (endianness) بچرخاند، یک واحد افزایش یا کاهش دهد، بسط صفر یا بسط علامت (zero/sign extension) اعمال کند، یا تبدیل ارقام به/از کد ASCII را انجام دهد. هر یک از این جایگزینیها یک ورودی تولید میکند که اگر اجرای آن باعث بهبود پوشش شود، به صف اضافه میشود.
مثال:
Weizz در هنگام تحلیل تابع parser یا تجزیه کننده، قادر است تشخیص دهد که عملوند اول در CMP_A و CMP_B و همچنین عملوند دوم در CMP_D دارای رابطهی I2S هستند. از سوی دیگر، عملوند دوم در CMP_C و عملوند اول در CMP_D رابطهی I2S ندارند، هرچند همچنان به برخی از بایتهای ورودی وابسته هستند.
Weizz، مقایسهی CMP_D را به عنوان یک نمونهی احتمالی مرتبط با جمعآزما علامتگذاری میکند، زیرا سه شرط موردنیاز برقرار هستند: عملوند دوم I2S است و اندازهی آن ۲ بایت است؛ عملوند اول I2S نیست اما به چندین بایت ورودی وابسته است؛ و دو عملوند وابستگیهایی به مجموعههای جدا (disjoint) از بایتها نشان میدهند.
در مرحلهی بعد، FuzzOperands جایگزینیهای هدفمند (surgical substitutions) را بر اساس مقادیر مشاهده شدهی عملوندها انجام میدهد. برای مثال، این روش میتواند مقدار 0xAAAA که در محل مقایسهی CMP_A ثبت شده است را بدون تغییر در بایتهای مربوط به فیلد id قرار دهد و در نتیجه دستور exit(1) داخلی برنامه را فعال کند.
۳.۱.۴ قراردهی تگ (Tag Placement)
Weizz به صورت خلاصه وابستگیهای میان بایتهای ورودی و مقایسههای انجام شده را با برچسبگذاری یا یادداشت نویسی (annotation) این بایتها با «تگهایی» که برای استنتاج ساختار در مراحل بعدی ضروری هستند، نمایش میدهد. برای بایت ورودی bام، ساختار Tads[b] موارد زیر را نگهداری میکند:
- Id: (آدرس) دستورالعمل مقایسهای که از میان تمام مقایسههایی که بایت (b) بر آنها اثر میگذارد، به عنوان مهمترین مورد انتخاب شده است؛
- Ts: برچسب زمانی (timestamp) مربوط به دستورالعمل id در لحظهای که برای اولین بار مشاهده شده است.
- Parent: دستورالعمل مقایسهای که باعث آخرین تخصیص تگ پیش از مقدار Tads[b] برای بایت (b) شده است؛
- dependsOn: در صورتی که بایت (b) شامل مقدار جمعآزما باشد، این فیلد به دستورالعمل مقایسهای مربوط به درونیترین (innermost) جمعآزما تودرتو اشاره میکند که صحت این بایت را بررسی میکند، در صورت وجود؛
- Flags: مشخص میکند کدام عملوند توسط بایت (b) تحت تأثیر قرار گرفته است، در صورتی که رابطهی I2S داشته باشد، یا اینکه این بایت بخشی از یک فیلد جمعآزما باشد؛
- numDeps: تعداد بایتهای ورودیای که عملوند موجود در فلگها (flags) به آنها وابسته است.
فرآیند تخصیص تگ به اتکا بر «مجاورت مکانی و زمانی» (spatial and temporal locality) در نحوهی اجرای دستورالعملهای مقایسه انجام میشود. Weizz تلاش میکند ویژگیهای ساختاری ورودی را بر این اساس استنتاج کند که یک برنامه معمولاً از دستورالعملهای متمایز برای پردازش عناصر ساختاری متمایز استفاده میکند.
بنابراین، زمانی که یک یا چند بایت ورودی از طریق یک مقایسهی یکسان (بهصورت مستقیم یا از طریق دادههای مشتق شده) پردازش شوند، میتوان آنها را مرتبط (related) در نظر گرفت و برای آنها تگ مشترک اختصاص داد.
در صورت وجود چندین داوطلب، از اطلاعات زمانی استفاده میشود تا دستورالعملهایی با برچسب زمانی (timestamp) کمتر در اولویت قرار گیرند، زیرا اعتبارسنجی قالب معمولاً در مراحل ابتدایی اجرای برنامه و در کد تفسیر انجام میشود.
در ادامه، این روش گسترش داده میشود تا جمعآزماها نیز در نظر گرفته شوند و در صورتی که اکتشافات داوطلب بهتری نسبت به تگ فعلی تشخیص دهند، امکان بازتخصیص (reassignment) تگ وجود داشته باشد. اطلاعات زمانی همچنین میتواند به عنوان معیاری برای روابط سلسلهمراتبی (hierarchical relationships) در قالب تگهای والد (parent) نیز استفاده شود.
الگوریتم PlaceTags روی محلهای مقایسه که بر اساس زمان اولین مشاهده در اجرا مرتب شدهاند پیمایش میکند و برای هر بایت ورودی تلاش میکند یک استنتاج انجام دهد. این الگوریتم روی عملوندهای دستورالعملهای هر site (s) اعمال میشود. اگر برای بایت فعلی (b) هیچ وابستگی در میان تمام نمونههای ثبتشدهی Deps(s, j, op) یافت نشود، چرخه به بایت بعدی میرود؛ در غیر این صورت، یک کاندید numDeps محاسبه میشود، یعنی تعداد (n) بایت ورودی که بر آن دستورالعمل اثر میگذارند، که به صورت زیر محاسبه میشود:
n←∑k (1 if Vj Deps(s,j,op)[k] else 0)
که در آن (k) اندیس طول ورودی است. اگر بایت هنوز بدون تگ باشد، با دستورالعمل فعلی تگگذاری میشود؛ در غیر این صورت، امکان بازتخصیص بررسی میشود. اگر تگ فعلی Tads[b] مربوط به یک تست جمعآزما نباشد و مقدار (n) از مقدار قبلی Tads[b].numDeps کوچکتر باشد، تگ جدید جایگزین میشود؛ زیرا وابستگی کمتر نشان میدهد آن دستورالعمل احتمالاً نمایندهی بهتری برای آن بایت است.
زمانی که یک مقایسه تشخیص داده شود که با مقدار جمعآزما در ارتباط است، همیشه همان به عنوان تگ بایت انتخاب میشود. برای پر کردن فیلد dependsOn از مرتبسازی وضعیت وابستگیهای Deps روی هر بایت ورودی استفاده میشود؛ به این معنا که مشخص میشود یک بایت که بخشی از مقدار جمعآزما است، همچنین توسط جمعآزماهای بیرونیتر برای بررسی صحت استفاده میشود.
در ادامه، اصلاح ورودیها از درونیترین جمعآزما آغاز میشود و فیلد dependsOn مشخص میکند کدام مقایسه باید بعد از آن پردازش شود.
مثال:
در این مثال، بذر (seed) شکل 2C را در نظر میگیریم. تحلیل وابستگیها منجر به تخصیص تگهای زیر میشود:
- بایتهای مربوط به فیلد id بر سایتهای CMP_A و CMP_D تأثیر میگذارند: از میان آنها، CMP_A به عنوان تگ انتخاب میشود، زیرا در اجرای برنامه زودتر مشاهده شده است؛
- بایتهای مربوط به فیلد size بر CMP_B، CMP_C و CMP_D تأثیر دارند: در این حالت CMP_B به عنوان تگ انتخاب میشود، زیرا از نظر زمانی (temporal) پیش از سایر سایتها قرار میگیرد؛
- بایتهای مربوط به data و checksum تنها بر CMP_D اثر میگذارند، بنابراین CMP_D به عنوان شناسه (id) تگ آنها انتخاب میشود؛ با این حال تگهای مربوط به این دو گروه در فیلد flags متفاوت خواهند بود، زیرا بایتهای مربوط به جمعآزما به عنوان بخشی از یک فیلد جمعآزما علامتگذاری میشوند.
۳.۱.۵ اعتبارسنجی جمعآزما و اصلاح ورودی (Checksum Validation & Input Repair)
آخرین گام در مرحلهی اصلاح شامل اعتبارسنجی جمعآزما (checksum) و اصلاح ورودی برای جمعآزماهایی است که در تکرارهای قبلی در برنامه پچ شدهاند. از آنجا که این تکنیک از نظر مفهومی مشابه روش RedQueen است، آن را بهصورت مختصر توضیح میدهیم.
الگوریتم FixChecksums از تگهایی استفاده میکند که قبلاً به عنوان مرتبط با فیلدهای جمعآزما علامتگذاری شدهاند و براساس وضعیت مرتب شدهاند. برای هر تگ، ابتدا مقدار جمعآزما محاسبه شده توسط برنامه (یعنی مقدار عملوندی که از ورودی مشتق شده و در مقایسه استفاده میشود) استخراج میشود و همچنین مشخص میگردد که مقدار عملوند دیگر (که الزاماً دارای رابطهی I2S است؛ رجوع شود به بخش ۳.۱.۳) در کدام بخش از ورودی ذخیره شده است.
سپس این بایتها با مقدار مورد انتظار جایگزین میشوند. پس از آن، وصله (Patch) مربوطه غیرفعال شده و برنامه با ورودی اصلاح شده اجرا میشود. اگر جمعآزما به درستی مدیریت شده باشد، همان مسیر اجرایی قبلی مشاهده خواهد شد؛ در غیر این صورت، آن جمعآزما یک مثبت کاذب بوده و باید از ادامه کار صرفنظر کنیم.
در پایان این فرآیند، Weizz وصلههایی را که به عنوان مثبت کاذب تشخیص داده نشدهاند، مجدداً اعمال میکند. این کار هم برای مرحلهی دوم و هم برای تکرارهای بعدی مرحلهی اصلاح ورودیهای دیگر مفید خواهد بود. همچنین، پچهایی که مربوط به جمعآزماهای تازه کشف شده توسط MarkChecksums در همین مرحلهی اصلاح هستند نیز اعمال میشوند. به این ترتیب، در دفعات بعدی که همان ورودی (یا ورودیهای مشابه تولید شده توسط FuzzOperands) تحلیل شود، Weizz قادر خواهد بود این موانع را نیز پشت سر بگذارد.
۳.۲ مرحلهی آگاه از ساختار (Structure-Aware Stage)
مرحلهی دوم در Weizz ورودیهای جدید را از طریق جهشهای غیرقطعی (nondeterministic mutations) تولید میکند که میتوانند بر اساس تگهای اختصاص یافته در مرحلهی اصلاح عمل کنند. مشابه رویکرد AFL، تعداد ورودیهای تولید شده به یک امتیاز انرژی (energy score) بستگی دارد که زمانبند توان (power scheduler) به ورودی اولیه اختصاص میدهد [24].
هر ورودی حاصل تعداد متغیری (۱ تا ۲۵۶) از جهشهای پیاپی و انباشته شده است، به طوری که Weizz در هر مرحله بهصورت غیرقطعی یک طرح جهش havoc، فیلد (field) یا قطعه (chunk) را انتخاب میکند.
حفظ جهشهای غیرقطعی (havoc) از AFL، همانند کارهای پیشین در حوزهی قطعه [24]، به این دلیل است که ترکیب آنها با جهشهای آگاه از ساختار، مقیاسپذیری (scalability) روش را بهبود میدهد.
جهشهای سطح فیلد بر پایهی تگهای مرحلهی اصلاح عمل میکنند تا به صورت هدفمند بایتهایی را تغییر دهند که احتمالاً یک واحد اطلاعاتی واحد را تشکیل میدهند. در مقابل، جهشهای سطح قطعه به تغییرات ساختاری بزرگتر و سطحبالا میپردازند که توسط تگها هدایت میشوند.
از آنجا که استراتژی ما برای استنتاج فیلد و قطعه (chunk) قابل اتکا (sound) نیست، بهویژه در مقایسه با مشخصات نوشته شده بهصورت دستی، ما در فرایند شناسایی به Weizz آزادی عمل میدهیم. این ابزار هرگز ساختار ورودی را بهطور کامل بازسازی نمیکند، بلکه تنها هنگام اعمال جهش، بهصورت غیرقطعی فیلدها یا قطعههای منفرد را شناسایی میکند.
نقطه ضعف این انتخاب آن است که ممکن است Weizz برخی کارها را تکرار کند یا در میان جهشها تصمیمهای متناقض بگیرد، زیرا تاریخچهی انتخابهای قبلی را نگه نمیدارد. با این حال، مزیت آن این است که Weizz به تصمیمهای احتمالی نادرست متعهد نمیشود؛ در نتیجه، تأثیر این تصمیمها به همان جهشهای بعدی محدود میشود و در مجموع، فضای بیشتری برای کاوش سناریوهای مختلف در فرآیند فازینگ فراهم میشود.
الگوریتم ۲: شناسایی فیلد و جهش:
function FieldMutation(Tags, I ):
b ← pick u.a.r. from {0 ... len(I)}
for i ∈ {b ... len(I)-1} do
if Tags[i].id==0 then continue
start ← GoLeftWhileSameTag(Tags, i)
with probab. PrI 2S or if !I2S(Tags[start]) then
end ← FindFieldEnd(Tags, start, 0)
I ← Mutate(I, start, end); break
return I
در ادامه توضیح میدهیم که چگونه از تگهای ورودی برای شناسایی فیلدها و قطعهها استفاده میکنیم و چه نوع جهشهایی را اعمال میکنیم. در پایان این فرآیند، برنامه را با ورودی تولید شده از دنبالهی جهشها اجرا میکنیم تا کرشها و بهبودهای پوشش را شناسایی کنیم. ورودیهایی که منجر به توقفهای غیرعادی جدید شوند، بلافاصله اصلاح (repair) میشوند؛ در حالی که سایر ورودیها زمانی که در نهایت وارد مرحلهی اصلاح شوند، تحت فرآیند اصلاح قرار میگیرند.
۳.۲.۱ فیلدها (Fields)
شناسایی فیلدها یک فرآیند اکتشافی است که بر اساس الگوهای رایجی طراحی شده که Weizz باید آنها را تشخیص دهد. الگوی اول ساده و رایج است: در این حالت، برنامه یک فیلد را با استفاده از یک دستور مقایسهی واحد بررسی میکند. ما معتقدیم این الگو در نرمافزارهای واقعی بسیار متداول است. الگوی دوم حالتی است که در آن، برنامه هر بایت از یک فیلد را با استفاده از دستورالعملهای مقایسهی جداگانه بررسی میکند؛ همانند نمونهی زیر که از کتابخانهی lodepng گرفته شده است:
unsigned char lodepng_chunk_type_equals ( const unsigned char *
chunk , const char * type ) {
if ( strlen ( type ) != 4) return 0;
return ( chunk [4]== type [0] && chunk [5]== type [1] && chunk [6]==
type [2] && chunk [7]== type [3]) ; }
این کد هر بایت از رشتهی ورودی در یک قطعه را با استفاده از یک دستور مقایسهی مجزا بررسی میکند. چنین الگویی معمولاً در برنامهها برای در نظر گرفتن تفاوتهای ترتیب بایتها (endianness) مشاهده میشود.
این دو الگو میتوانند با یکدیگر ترکیب شوند و الگوی سومی را تشکیل دهند (مطابق بخش پایینی شکل ۳) که در این تکنیک نیز در نظر گرفته شده است. اکنون توضیح میدهیم که Weizz چگونه الگوهای نشان داده شده در شکل ۳ را شناسایی میکند:
- الگوی اول: از آنجا که یک دستورالعمل واحد تمام بایتهای فیلد را بررسی میکند، انتظار داریم که تمام بایتهای متناظر با این فیلد، با یک تگ یکسان علامتگذاری شده باشند.
- الگوی دوم: انتظار میرود بایتهای متوالی دارای تگهای متفاوت باشند، اما برچسبهای زمانی (timestamp) متناظر آنها متوالی و پشتسرهم (consecutive) باشند، زیرا هیچ دستور مقایسهی دیگری بین آنها اجرا نشده است.
- در شکل، یک فیلد از بایتهایی با شناسهی تگهای {A, E, G, B} و برچسبهای زمانی {5, 6, 7, 8} نشان داده شده است.
- الگوی سوم: شامل (دو یا چند) زیردنباله است که در هر کدام تگها یکسان هستند، اما بین زیردنبالهها تگ تغییر میکند و اختلاف برچسبهای زمانی بین آنها تنها یک واحد است.
رویهی FieldMutation (الگوریتم ۲) به صورت غیرقطعی (nondeterministic) یک فیلد را با انتخاب یک موقعیت تصادفی (b) در ورودی جستجو میکند. اگر بایت فعلی فاقد تگ باشد، مکاننما (cursor) به جلو حرکت میکند تا به یک بایت تگدار برسد (خط ۳). در غیر این صورت، اگر بایت دارای تگ باشد، Weizz بررسی میکند که آیا این بایت، اولین بایت در فیلد داوطلب است یا بایتهای قبلی با همان تگ وجود دارند؛ در حالت دوم، به چپ بازمیگردد تا به اولین بایت با آن تگ برسد (خط ۴).
پس از تعیین ابتدای فیلد، Weizz تصمیم میگیرد که آیا آن را جهش دهد یا نه. اگر بایت اولیه دارای رابطهی I2S باشد، جهش تنها با یک احتمال مشخص انجام میشود، زیرا چنین بایتی ممکن است نمایانگر یک عدد جادویی (magic number) باشد. تغییر دادن این نوع مقادیر معمولاً باعث هدایت برنامه به مسیرهایی میشود که ورودیهای نامعتبر را پردازش میکنند، که معمولاً برای فازر جذابیت کمتری دارد.
محدودهی فیلد برای اعمال جهش توسط رویهی کمکی FindFieldEnd تعیین میشود، که بهدنبال دنبالههایی از بایتهای تگدار میگردد که یکی از سه الگوی مطرحشده را برآورده کنند. در نهایت، FieldMutation فیلد را با انتخاب یکی از ۱۲ تبدیل havoc مربوط به AFL که طول داده را حفظ میکنند، تغییر میدهد.
۳.۲.۲ قطعهها (Chunks)
Weizz به صورت اکتشافی (heuristically) مرزهای محتمل قطعهها را در دنبالهی ورودی شناسایی کرده و سپس جهشهای سطح بالا (higher-order) را روی قطعههای داوطلب اعمال میکند. به عنوان مثال، ساختاری را بررسی میکنیم که نمایندهی بسیاری از قطعهها در قالبهای باینری است. در حالت اول (شکل 4a)، ساختار شامل چهار فیلد است:
یک فیلد type با یک مقدار ثابت شناخت هشده، دو فیلد دادهای x و y، و یک مقدار cksm برای بررسی صحت (integrity). فرض کنید برنامه ابتدا جمعآزما این ساختار را محاسبه کرده و آن را با فیلد مربوطه مقایسه میکند، سپس فیلدهای type و x را بهترتیب بررسی میکند، بعد از آن تعدادی مقایسهی غیرمرتبط انجام میدهد (که بر تگهای این ساختار اثری ندارند)، و در ادامه مقایسهای وابسته به y انجام میدهد.
برای سادگی، فرض میکنیم پردازش فیلدها مطابق الگوی اول بخش قبل است (یعنی برای هر فیلد یک مقایسه وجود دارد). خروجی الگوریتم PlaceTags در این حالت با نمایش گرافیکی شکل 4d سازگار خواهد بود. اکنون توضیح میدهیم که این تکنیک چگونه با استفاده از تگها و برچسب زمانی (timestamp) آنها، مرزهای این قطعه (chunk) را شناسایی میکند:
مشابه شناسایی فیلد، یک موقعیت تصادفی (b) در ورودی انتخاب میشود و بایتهای قبل از آن بررسی میشوند؛ مکاننما تا زمانی که تگ یکسان باقی بماند، به عقب بازمیگردد. اگر (b) درون ۴ بایت فیلد type قرار گیرد، Weizz همان شاخص (index) شروع قطعه را تشخیص خواهد داد. برای یافتن انتهای قطعه، از رویهی FindChunkEnd (الگوریتم ۳) استفاده میشود:
- در ابتدا، این رویه ۴ بایت مربوط به type را شناسایی میکند (خط ۱)؛
- سپس به صورت بازگشتی بهدنبال فیلدهای مجاور میگردد، به شرطی که برچسب زمانی آنها از فیلد فعلی بیشتر باشد (خطوط ۲-۳). این مرحله فیلدهای x و y را شناسایی میکند؛
- فیلد cksm دارای برچسب زمانی کمتری است (زیرا برنامه آن را قبل از بررسی type پردازش کرده است)، اما خطوط ۴-۵ میتوانند آن را نیز در قطعه لحاظ کنند، زیرا دادههای والد (parent) را بررسی میکنند.
در اینجا، والد (parent) به دستور مقایسهای اشاره دارد که در فرآیند تخصیص تگ، قبل از تگ فعلی انتخاب شده است. در اولین فراخوانی FindChunkEnd، مشاهده میشود که تگ اولین بایت cksm با والد (parent) مربوط به تگ type یکسان است؛ بنابراین این فیلد نیز به عنوان بخشی از همان قطعه در نظر گرفته میشود.
برای توضیح خطوط ۶ تا ۹ در الگوریتم ۳، به نمونههایی از این ساختار توجه میکنیم که این بخش از الگوریتم را فعال میکنند. در شکل 4b، فیلد cksm قبل از type قرار دارد. در این حالت، الگوریتم از خطوط ۲-۳ عبور میکند بدون اینکه مقدار end را افزایش دهد، و در نتیجه فیلدهای x و y را شناسایی نمیکند. با این حال، خطوط ۸-۹ میتوانند بایتهای مربوط به فیلدهای مجاور را به قطعه اضافه کنند، به شرطی که برچسبهای زمانی آنها نسبت به برچسبهای زمانی مربوط به تگ بایت kام (که در اینجا اولین بایت type است) روند افزایشی داشته باشد.
در شکل 4c، یک آرایهی data با طول ۶۴ بایت به ساختار اضافه شده است. ممکن است Weizz چنین بلاکهای باینری را بدون تگ باقی بگذارد، اگر برنامه روی آنها یا مقادیر مشتقشده از آنها هیچ مقایسهای انجام ندهد. در این حالت، خط ۷ این امکان را فراهم میکند که چنین دنبالههایی نیز به قطعه اضافه شوند، بهطوری که مقدار end تا مقدار جدید (′end) که در شکل 4d نشان داده شده است، گسترش یابد.
الگوریتم ۳: شناسایی مرزها (Boundary) برای قطعهها:
function FindChunkEnd(Tags, k):
end ← GoRightWhileSameTag(Tags, k)
while Tags[end+1].ts >= Tags[k].ts do
end ← FindChunkEnd(Tags, end+1)
while Tags[end+1].id == Tags[k].parent do
end ← end +1
with probability Pre x t end do
while Tags[end+1].id == 0 do end ← end +1
while Tags[end+1].ts >= Tags[k].ts do
end ← FindChunkEnd(Tags, end+1)
return end
با استفاده از FindChunkEnd، ما یک طرح استنتاج (inference scheme) پیشنهاد میکنیم که از ساختار قالبهای رایج الهام گرفته است. معمولاً اولین فیلد در یک قطعه، همان فیلدی است که برنامه نیز ابتدا آن را پردازش میکند، و ما از این ویژگی برای حدس زدن محل شروع قطعه استفاده میکنیم.
اگر برنامه قبل از دسترسی به این فیلد، یک جمعآزما (checksum) را بررسی کند، ما با استفاده از اطلاعات والد (parent) آن را به جمعآزما مرتبط میکنیم؛ در غیر این صورت، جمعآزماهایی که «پس از آن» بررسی میشوند، به عنوان فیلدهای دادهی معمولی در نظر گرفته میشوند.
خطوط ۷ تا ۹ الگوریتم، جمعآزماها را برای پوشش دادن چیدمانهای مختلف و ترتیبهای متفاوت تگها گسترش میدهند، اما این کار تنها با یک احتمال مشخص انجام میشود تا از گسترش بیشازحد جلوگیری شود. این الگوریتم همچنین میتواند بلاکهای دادهای که تنها بهصورت جزئی تگگذاری شدهاند را از طریق گامهای بازگشتی شناسایی کند.
با این حال، باید توجه داشت که در برخی قالبها ممکن است انواع خاصی از چانکها غالب باشند، و انتخاب تصادفی موقعیت شروع ممکن است با احتمال کمتری به شناسایی قطعههای غیرغالب منجر شود. برای بهبود این وضعیت، یک اکتشاف جایگزین طراحی شده است:
در این روش، بهصورت تصادفی یکی از دستورالعملهای مقایسهای که در تگها ظاهر شده انتخاب میشود، سپس با استفاده از FindChunkEnd و شروع از بایتی که با آن دستورالعمل تگگذاری شده، قطعههای غیرهمپوشان ساخته میشوند، و در نهایت یکی از این قطعهها انتخاب میشود.
از آنجا که Weizz هیچ دانشی از ویژگیهای قالب ندارد، انتخاب بین این دو اکتشاف به صورت احتمالی انجام میشود. برای قطعه انتخاب شده، Weizz یکی از جهشهای سطح بالای زیر را اعمال میکند (بخش ۲.۳):
- افزودن (Addition): در این عملیات، یک قطعه از ورودی دیگری به قطعهای که قطعه فعلی را در بر میگیرد اضافه میشود. Weizz یک ورودی تگگذاری شده (′I) را از صف انتخاب میکند و در آن به دنبال قطعههایی میگردد که با بایتهایی شروع میشوند که دارای همان تگ والد (parent) مربوط به بایتهای ابتدایی قطعه فعلی هستند. سپس یکی از آنها را به صورت تصادفی انتخاب کرده و بایتهای متناظر آن را قبل یا بعد از قطعه فعلی به ورودی اضافه میکند. در اینجا، تگ والد به عنوان جایگزینی برای اطلاعات تودرتو (nesting) عمل میکند که در ابزار AFLSmart بهصورت صریح وجود دارد.
- حذف (Deletion): در این عملیات، بایتهای مربوط به قطعه از ورودی حذف میشوند.
- جایگزینی (Splicing): در این روش، یک قطعه مشابه از ورودی دیگری انتخاب شده و جایگزین قطعه فعلی میشود. Weizz آن ورودی را اسکن میکند تا قطعههایی را بیابد که با همان تگ (در حالی که AFLSmart از اطلاعات type استفاده میکند) قطعه فعلی شروع میشوند، سپس یکی را به صورت تصادفی انتخاب کرده و بایتهای آن را جایگزین میکند.
۳.۲.۳ بحث (Discussion)
اکنون میتوانیم توضیح دهیم که چرا برای پشتیبانی از جهشهای فیلد و قطعهای که پیشتر معرفی شدند، نمیتوانیم به تکنیکهای شناسایی وابستگی در RedQueen یا SLF تکیه کنیم.
فرض کنید RedQueen یک ورودی را با تعدادی بایت A رنگآمیزی (colorization) کرده و در ابتدا یک عملیات مقایسهی cmp A, B را ثبت میکند. در این حالت، RedQueen تلاش میکند هر وقوع A را با B جایگزین کند و با اجرای برنامه روی ورودی جدید (اینبار بدون لاگگیری)، اعتبار آن را از طریق بررسی بهبود پوشش ارزیابی کند. این استراتژی زمانی خوب عمل میکند که هدف صرفاً جایگزینیهای I2S باشد، اما برای شناسایی فیلدها دو مشکل اساسی وجود دارد:
- اگر چندین بایت B در ورودی وجود داشته باشد، نمیتوان تشخیص داد کدامیک فیلد مربوط به آن مقایسه را تشکیل میدهد؛
- اگر مقدار B از قبل در ورودی دیگری در صف وجود داشته باشد، جایگزینی منجر به بهبود پوشش نمیشود، و در نتیجه RedQueen یک وابستگی مستقیم در ورودی فعلی را از دست میدهد.
در حالی که جهشهای ساختاری ما نیازمند رهگیری مداوم مقایسهها برای استخراج هم روابط I2S و هم غیر I2S هستند. از سوی دیگر، SLF میتواند وابستگیها را مشابه روش GetDeps شناسایی کند، اما تنها بخشهای خاصی از ورودی را – که میتوان آنها را به عنوان فیلد در نظر گرفت – بر اساس نحوهی پردازش برنامه تشخیص میدهد.
SLF سه دسته از بررسیهای برنامه را پشتیبانی میکند و میتواند بخشهایی از ورودی را مثلاً با تکرار بخشهایی که در یک بررسی شمارشی (count check) نقش دارند، جهش دهد (بخش ۲.۲). در حالی که Weizz ممکن است چنین کاری را از طریق افزودن قطعه انجام دهد، اما علاوه بر آن، فیلدها – و حتی کل قطعههایی – را نیز جهش میدهد که توسط SLF پوشش داده نمیشوند.
۴. پیادهسازی (Implementation)
ما ایدههای خود را بر پایه AFL نسخه 2.52b و QEMU نسخه 3.1.0 برای اهداف لینوکس x86-64 پیادهسازی کردهایم. برای پوشش شاخهها (branch coverage) و ساخت جدولهای مقایسه، از یک «shadow call stack یا به اصطلاح پشتهٔ فراخوانی سایهای» استفاده میکنیم تا اطلاعات حساس به زمینه (context-sensitive) را محاسبه کنیم [14]؛ این روش میتواند به یک فازر کمک کند تا برنامهها را بهصورت گستردهتر و عمیقتر کاوش کند [10]. ما نقشهی پوشش (coverage map) را با استفاده از آدرسهای بلوکهای پایهی مبدأ و مقصد و همچنین یک هش از پشتهی فراخوانی (call stack) شاخص گذاری میکنیم.
مقاصد طبیعی برای پر کردن جدولهای مقایسه، دستورالعملهای cmp و sub هستند. ما تا سقف ∣J∣=256|J| ورودی برای هر محل ذخیره میکنیم. مشابه RedQueen، ما همچنین فراخوانیهای تابعی را که ممکن است نقش توابع مقایسهگر را داشته باشند ثبت میکنیم: بررسی میکنیم آیا محل آرگومانهای اول و دوم شامل اشارهگرهای معتبر هستند یا نه، و برای هر عملوند ۳۲ بایت استخراج (dump) میکنیم.
در نظر گرفتن چنین فراخوانیهایی به عنوان عملوند (برای مثال توابعی که رفتاری مشابه memcmp دارند) میتواند پوشش را بهبود دهد، بهخصوص زمانی که فازر بهگونهای تنظیم شده باشد که کتابخانههای خارجی را ابزارگذاری نکند.
یک بهینهسازی مهم شامل به تعویق انداختن فازینگ اصلاح (surgical fuzzing) با یک مکانیزم مبتنی بر timeout است؛ به این صورت که اگر در یک بازهی زمانی مشخص (پنجرهی ۵۰ ثانیهای) یک ورودی جالب کشف شود، ورودیها با احتمال کاهشی میتوانند به مرحله دوم منتقل شوند.
توجه شود که ورودیهای بدون تگ فقط میتوانند در مرحله دوم جهشهای havoc را دریافت کنند؛ بنابراین ما مفهوم تگهای مشتق شده (derived tags) را معرفی میکنیم که در واقع حدسی آگاهانه درباره تگ واقعی هستند و بر اساس تگهای مشاهدهشده در ورودیهای مشابه ساخته میشوند. این تگهای مشتق شده سرعت فازر را افزایش میدهند و زمانی که ورودی نهایتاً وارد مرحله اصلاح شود، تگهای واقعی جایگزین آنها خواهند شد.
دو سناریو میتواند منجر به ایجاد تگهای مشتق شده شود:
- زمانی که از یک ورودی I، جهشهای اصلاح FuzzOperands(I) یک یا چند ورودی ′I تولید میکنند که پوشش (coverage) را بهبود میدهند و به صف اضافه میشوند. پس از اینکه PlaceTags روی I اجرا شد و آن را تگگذاری کرد، این تگها به ′I کپی میشوند و به عنوان تگ مشتق شده علامتگذاری میشوند (زیرا FuzzOperands از جهشهای «محلی یا Local» استفاده میکند و ساختار را حفظ میکند).
- بهطور مشابه، یک ورودی تگدار I1 میتواند تحت جهشهای سطحبالا قرار گیرد که در آن بایتهایی از یک ورودی تگدار دیگر I2 قرض گرفته میشوند، یعنی عملیات افزودن (addition) یا جایگزینی (splicing) انجام میشود. در این حالت، بایتهای اضافه/جایگزینشده در ورودی جهشیافته I1′ که از I2 آمدهاند، همان تگهای مشاهده شده در I2 را دریافت میکنند، در حالی که سایر بایتها تگهای مربوط به I1 را حفظ میکنند.
۵. ارزیابی (Evaluation)
در آزمایشهای خود، ما به پرسشهای پژوهشی زیر میپردازیم:
- پرسش اول. Weizz در مقایسه با فازرهای پیشرفتهی موجود (state-of-the-art) در اهدافی که قالبهای مبتنی بر قطعه را پردازش میکنند، چه عملکردی دارد؟
- پرسش دوم. آیا Weizz قادر به کشف باگهای جدید است؟
- پرسش سوم. تگها چه ارتباطی با قالب واقعی ورودی دارند؟ و نقش جهشهای ساختاری (structural mutations) و دور زدن موانع (roadblock bypassing) در بهبودهای مشاهده شده چیست؟
بنچمارکها (Benchmarks). ما برنامههای زیر را (با نسخه و قالب ورودی مربوطه) در نظر میگیریم:
wavpack (نسخه 5.1.0، قالب WAV)، decompress (نسخه 2.3.1، قالب JP2)، ffmpeg (نسخه 4.2.1، قالب AVI)، libpng (نسخه 1.6.37، قالب PNG)، readelf (نسخه 2.3.0، قالب ELF)، djpeg (کامیت 5db6a68، قالب JPEG)، objdump (نسخه 2.31.51، قالب ELF)، mpg321 (نسخه 0.3.2)، oggdec (نسخه 1.4.0)، tcpdump (نسخه 4.9.2، قالب PCAP)، gif2rgb نسخه 5.2.1، قالب GIF).
شش برنامهی اول در ارزیابیهای قبلی مربوط به AFLSmart مورد استفاده قرار گرفتهاند و برای آنها مشخصات قالب (format specification) در دسترس بوده است. هشت برنامهی آخر نیز به طور رایج در ارزیابی فازرهای اهداف عمومی (general-purpose fuzzers) استفاده میشوند.
تنظیمات آزمایش (Experimental Setup). ما آزمایشها را روی یک سرور با مشخصات زیر اجرا کردیم:
- دو پردازنده Intel Xeon E5-4610v2 @ 2.30GHz
- 256 گیگابایت RAM
- سیستمعامل Debian 9.2
ما پوشش تجمعی بلوکهای پایه (cumulative basic block coverage) را برای فازرهای مختلف اندازهگیری کردیم؛ این اندازهگیری بر اساس اجرای برنامه روی کل مجموعه ورودیهایی انجام شد که هر فازر تولید کرده بود. هر آزمایش ۵ بار تکرار شد و مقدار میانه (median) نتایج در نمودارها گزارش شده است. برای مرحله دوم Weizz، پارامترهای Prfield = Prchunk = 1/15 (مطابق شکل ۱) تنظیم شدهاند که مشابه احتمال اعمال جهشهای هوشمند در AFLSmart در کار [24] است.
۵.۱ پرسش اول: قالبهای مبتنی بر قطعه (Chunk-Based Formats)
ما Weizz را با بهترین روش موجود برای قالبهای مبتنی بر قطعه یعنی AFLSmart مقایسه میکنیم. این ابزار از جهشهای سطح بالا (higher-order mutations) روی یک ساختار ورودی مجازی استفاده میکند (بخش ۲.۳). سپس فازرهای اهداف عمومی (general-purpose fuzzers) را نیز در نظر میگیریم، زیرا مطالعات قبلی [11, 24] نشان دادهاند که این دسته از ابزارها هنوز در عمل روی این نوع برنامهها عملکرد نسبتاً مؤثری دارند.
۵.۱.۱ AFLSmart
ما برای AFLSmart از نسخه انتشار 604c40c استفاده کردیم و از peach pitهایی که توسط نویسندگان آن برای ساختارهای ورودی مجازی (virtual input structures) ارائه شدهاند بهره گرفتیم. ما میزان پوشش کد (code coverage) را برای موارد زیر اندازهگیری کردیم:
ما میزان پوشش کد (code coverage) را برای موارد زیر اندازهگیری کردیم:
(a) AFLSmart با جهشهای انباشته (stacked mutations)، اما بدون مراحل قطعی (deterministic stages) همانطور که در مستندات آن پیشنهاد شده است؛
(b) Weizz در پیکربندی استاندارد خود؛
(c) نسخهای از Weizz با نام †Weizz که در آن قابلیتهای FuzzOperands، وصله جمعآزما (checksum patching) و ترمیم ورودی (input repairing) غیرفعال شدهاند.
نسخه †Weizz به ما اجازه میدهد تمرکز را روی اثر جهشهای مبتنی بر تگ (tag-based mutations) بگذاریم و در عین حال از قابلیتهای دور زدن موانع (roadblock bypassing) که در AFLSmart وجود ندارد صرفنظر کنیم.
برای موارد (a) و (c)، یک دیکشنری از توکنهای مرتبط با قالب تحت بررسی ارائه کردیم، مشابه ارزیابیهای قبلی AFLSmart. برای بذرها (seed) از موارد آزمون AFL استفاده کردیم، به جز در مورد wavpack و ffmpeg که در آنها از فایلهای حداقلی و از نظر نحوی (syntax) معتبر استفاده شده است [9].
شکل ۵ میانهی پوشش بلوکهای پایه (basic block coverage) را پس از ۵ ساعت اجرا نمایش میدهد. در مقایسه با AFLSmart ،Weizz در ۳ مورد از ۶ برنامه (readelf، libpng، ffmpeg) پوشش به طور قابل توجهی بالاتری ارائه میدهد، در wavpack کمی بهتر است، در decompress (از حالت فشرده خارج شده) عملکردی مشابه دارد و در djpeg اندکی ضعیفتر عمل میکند. برای درک این نتایج، ابتدا جایگاه نسخه †Weizz را بررسی میکنیم و سپس مزایای ویژگیهای اضافی Weizz را توضیح میدهیم.
پوشش بالاتر در †Weizz ناشی از رویکرد متفاوت آن در جهشهای ساختاری است. AFLSmart بر اساس مشخصات قالب (format specification) عمل میکند، در حالی که ما بر نحوهی برخورد برنامه با بایتهای ورودی تکیه داریم: †Weizz میتواند رفتارهایی را آشکار کند که ویژگیهای پیادهسازی واقعی را توصیف میکنند و لزوماً در مشخصات قالب پیشبینی نشدهاند.
اولین پیامد این است که †Weizz فقط بخشهایی را جهش میدهد که برنامه در طول اجرا قبلاً آنها را پردازش کرده است، زیرا تگها از مقایسههای اجراشده استخراج میشوند. پیامد دوم این است که عدم دقت در استنتاج ویژگیهای ساختاری ممکن است در واقع به نفع †Weizz باشد. نویسندگان AFLSmart نیز تأیید میکنند که مشخصات سهلگیرانه میتواند پیادهسازیهای نادقیق را آشکار کند [24]؛ ما در بخش ۶ به این موضوع بازمیگردیم.
†Weizz برای برنامههای readelf، libpng و ffmpeg گزینهی بهتری نسبت به AFLSmart محسوب میشود. وقتی تکنیکهای غیرفعال شده در †Weizz را نیز در نظر بگیریم، Weizz به دو دلیل پوشش بالاتری ارائه میدهد:
- اطلاعات I2S به Weizz کمک میکند تا باتهای جادویی را دقیقاً در جای درست جایگزین کند، در حالی که دیکشنریها ممکن است ناقص باشند. این موضوع به ویژه در برنامههای چندقالبی مانند ffmpeg اهمیت دارد.
- دور زدن جمعآزما (checksum bypassing) امکان تولید ورودیهای معتبر را برای برنامههایی فراهم میکند که صحت داده را بررسی میکنند، مانند libpng که مقادیر CRC-32 را روی دادهها محاسبه میکند.
ما همچنین کرشهای (Crash) کشف شده را بررسی کردیم که فقط در wavpack و ffmpeg مشاهده شدند. در مورد اول، هر سه فازر باگهای یکسانی را پیدا کردند. در ffmpeg، یک باگ تقسیم بر صفر را در بخشی از کد که چندین قالب را مدیریت میکند کشف کرد؛ ما این باگ را گزارش دادیم و توسعهدهندگان آن را به سرعت اصلاح کردند.
ویژگیهای مرتبط با I2S در Weizz بسیار مؤثر بودند در تولید ورودیهایی که بهطور قابل توجهی از بذر (seed) اولیه فاصله میگیرند؛ برای مثال تبدیل یک فایل AVI به MPEG-4. در بخش ۵.۲ این ادعا با یک مطالعه موردی (case study) پشتیبانی میشود.
۵.۱.۲ فازرهای جعبه خاکستری (Grey-Box Fuzzers)
ما Weizz را با ۸ برنامهٔ محبوب که قالبهای مبتنی بر قطعه (chunk-based) را مدیریت میکنند مقایسه کردیم؛ این ابزارها بهطور گسترده توسط جامعه [2] آزمایش شدهاند و در ارزیابیهای قبلی مربوط به فازرهای پیشرفته (state-of-the-art fuzzers) نیز مورد استفاده قرار گرفتهاند [5، 21، 24، 25].
ما فازرهای زیر را آزمایش کردیم:
(a) AFL 2.53b در حالت QEMU،
(b) AFL++ 2.54c در حالت QEMU با فعالسازی قابلیت مقایسه پوشش (CompareCoverage)،
و (c) Eclipser با آخرین نسخهای که هنگام اجرای آزمایشها در دسترس بود (کامیت 8a00591) و با تنظیمات پیشفرض آن.
از آنجا که Weizz برنامههای باینری (binary programs) را هدف قرار میدهد، فازرهایی مانند Angora را که به ابزارگذاری (instrumentation) در سطح کد منبع نیاز دارند کنار گذاشتیم. برای پروفایلگیری در سطح زیردستور (sub-instruction profiling)، چون Steelix بهصورت عمومی در دسترس نیست، بهجای آن از ++AFL استفاده کردیم.
در حالی که بررسی RedQueen میتواند جذاب باشد، استفاده از ابزارگذاری سختافزاری آن ممکن است مزیت ناعادلانهای ایجاد کند، زیرا در اینجا ما پوشش روشهای مبتنی بر QEMU را مقایسه میکنیم و تفاوت سربار (overhead) میتواند علت واقعی تفاوت در پوشش را مخدوش کند. ما یک آزمایش در بخش ۵.۳ انجام میدهیم که در آن Weizz را طوری تنظیم میکنیم که شبیه ویژگیهای آن شود و سپس در بخش ۶ این دو تکنیک را مستقیماً مقایسه میکنیم.
از آنجا که فازرهای رقیب هیچ مکانیزمی برای جهشهای ساختاری (structural mutations) ندارند، یک بودجهی زمانی بزرگتر (۲۴ ساعت) در نظر گرفتیم تا ببینیم آیا در طول زمان میتوانند از نظر پوشش بهبود پیدا کنند یا خیر. مطابق ارزیابیهای قبلی [11]، از یک بذر (seed) اولیه شامل رشتهای از کاراکتر ASCII “0” که ۷۲ بار تکرار شده استفاده کردیم.
با این حال، برای برنامههای libpng و tcpdump از ورودی اولیهی معتبر استفاده کردیم (در libpng فایل not_kitty.png از AFL و در tcpdump یک فایل PCAP چند ثانیهای)، زیرا هیچ فازری — به جز Weizz در مورد libpng — با بذر (seed) مصنوعی پوشش قابل توجهی به دست نیاورد.
همچنین برای AFL دیکشنریهای مختص قالب (format-specific dictionaries) فراهم کردیم تا در تشخیص اعداد جادویی کمک کند. شکل ۶ میانهی پوشش بلوکهای پایه (basic block coverage) را در طول زمان نشان میدهد. Weizz در ۵ مورد از ۸ هدف (libpng، oggdec، tcpdump، objdump و readelf) بهطور قابل توجهی پوشش کد بالاتری نسبت به سایر فازرها به دست آورده است.
سه مورد اول قالبهایی را پردازش میکنند که دارای فیلدهای جمعآزما (checksum) هستند و تنها Weizz قادر به اصلاح کردن آنها است، اگرچه به نظر میرسد Eclipser در طول زمان برای oggdec به آن نزدیک میشود. تواناییهای جهش ساختاری (structural mutation) در کنار این عامل میتواند شکاف عملکردی بین Weizz و سایر فازرها را توضیح دهد.
برای objdump و readelf، میتوان فرض کرد که اطلاعات I2S باعث تقویت عملکرد Weizz شدهاند؛ مشابه چیزی که در ارزیابی RedQueen مشاهده شد که در آن نسبت به سایر فازرها عملکرد بهتری داشت [5] (در objdump، لاگگیری آرگومانهای تابع نقش مهمی داشت [5]).
در mpg321 وgif2rgb، فازرهای مبتنی بر AFL عملکردی بسیار مشابه دارند و فاصلهی قابل توجهی با Eclipser نشان میدهند، که این موضوع تأیید میکند جهشهای استاندارد AFL میتوانند روی برخی قالبهای مبتنی بر قطعه (chunk) نیز مؤثر باشند.
در نهایت، Weizz در djpeg پیشتاز است، هرچند سایر فازرها فاصلهی زیادی با آن ندارند. بهطور کلی، در مورد djpeg و libpng و gif2rgb، بهترین جایگزین برای Weizz به طرز جالبی AFL است. در بررسی توقفهای غیرعادی (crash) نیز مشاهده شد که Weizz و ++AFL در mpg321 توقفهای یکسانی تولید کردند، در حالی که فقط Weizz یک توقف در objdump کشف کرد.
۵.۲ پرسش دوم: باگهای جدید
برای بررسی اثربخشی، Weizz را به مدت ۳۶ ساعت روی چندین هدف دنیای واقعی اجرا کردیم؛ از جمله برنامههایی که ورودیهایی را پردازش میکنند که الزاماً به پارادایم مبتنی بر قطعه (chunk) پایبند نیستند. Weizz در مجموع ۱۶ باگ در ۹ برنامهی پرکاربرد و به خوبی تست شده کشف کرد: objdump، CUPS (۲ باگ)، libmirage (۲)، dmg2img (۳)، jbig2enc، mpg321،ffmpeg (۳ باگ در libavformat و ۱ باگ در libavcodec)، sleuthkit و libvmdk.
بهطور کلی:
- ۶ باگ از نوع NULL pointer dereferences یا ارجاع به اشارهگر NULL (CWE-476)
- ۱ مورد unaligned realloc یا تخصیص حافظه بدون همترازی (CWE-761)
- ۲ مورد buffer overflow یا سرریز بافر (CWE-122)
- ۲ مورد out-of-bounds read یا خواندن خارج از محدوده (CWE-125)
- ۲ مورد division by zero یا تقسیم بر صفر (CWE-369)
- ۳ مورد integer overflow یا سرریز عدد صحیح (CWE-190)
در ادامه، دو مورد جالبتر را توضیح میدهیم:
CUPS. در حالی که رابط HTML سیستم چاپ UNIX (CUPS) به صورت قطعهمحور (chunk-oriented) طراحی نشده است، ما بررسی کردیم آیا Weizz میتواند درخواستهای HTTP مرتبط با آن را جهش دهد یا نه. Weizz موفق شد یک درخواست دستکاری شده تولید کند که باعث شد CUPS یک بافر تحت کنترل کاربر را مجدداً تخصیص دهد، و در نتیجه مسیر حملهی House of Spirit [7] ممکن شد. کلید کشف این باگ این بود که FuzzOperands برخی بایتهای ورودی را با اطلاعات I2S جایگزین کرد (مثلاً هدر ‘Accept-Language’ که به عنوان عملوند در فراخوانی تابع cups_strcasecmpــ ثبت شده بود)، و این کار یک فیلد معتبر از درخواست را ایجاد کرد که بعداً توسط جهشهای قطعهای تکثیر شد. شرکت Apple این باگ را تأیید کرد و در نسخه CUPS v2.3b8 آن را اصلاح نمود.
libMirage. در libmirage یک باگ بحرانی کشف شد؛ این کتابخانه دسترسی یکپارچه به قالبهای ایمیج CD-ROM را فراهم میکند. یک مهاجم میتواند یک سرریز بافر مبتنی بر هیپ (heap-based buffer overflow) ایجاد کند که در ادامه میتواند فراداده تخصیص دهنده (allocator) را خراب کند و حتی به دسترسی root منجر شود، زیرا daemon مربوط به CDEmu که از این کتابخانه استفاده میکند معمولاً با دسترسی root اجرا میشود. ما از یک ایمیج ISO به عنوان بذر (seed) اولیه استفاده کردیم؛ Weizz این باگ را در تجزیه کننده (parser) مربوط به قالب NRG آشکار کرد و نشان داد که حتی میتواند به طور قابل توجهی بین قالبهای ورودی مختلف، فقط بر اساس نحوهی پردازش بایتها توسط برنامه، رفتارهای متفاوتی کشف کند.
۵.۳ پرسش سوم: درک تأثیر تگها
در این بخش، اثر تگها را بررسی میکنیم. برنامهها ممکن است در نحوه پردازش بایتهای ورودی در قالبهای مختلف تفاوتهای قابل توجهی داشته باشند، اما برای ما جالب است که بررسی کنیم چرا Weizz در یک هدف مشخص میتواند مؤثر باشد.
ما دو مطالعهی موردی را بررسی میکنیم: در یکی نشان میدهیم که تگها چگونه میتوانند به شناسایی فیلدها و قطعهها در libpng کمک کنند، و در دیگری نشان میدهیم که چگونه جهشهای هوشمند (smart mutations) و دور زدن موانع (roadblocks bypassing) برای ffmpeg ضروری هستند، اما هیچکدام به تنهایی برای کارایی کافی نیستند.
برای درک فرآیند شناسایی فیلدها، ما تگهای حاصل از مرحلهی جراحی (surgical stage) را در این نمونه بررسی کردیم. از اولین بایت شروع میکنیم و در هر موقعیت، تابع FindFieldEnd را به صورت تکراری و پس از پایان آخرین فیلد شناسایی شده اجرا میکنیم.
در شکل ۷، هر فیلد شناسایی شده را با کروشه مشخص کردهایم و بایتهایی را که Weizz آنها را متعلق به فیلدهای جمعآزما میداند زیرخطدار کردهایم. Weizz مرزهای درست را در بیشتر موارد بهدرستی تشخیص میدهد، هرچند چند استثناء وجود دارد. سه فیلد آخر در فایل شناسایی نمیشوند، زیرا libpng هرگز به آن قطعه دسترسی پیدا نمیکند.
در برخی موارد، یک فیلد داده با مقدار جمعآزما مجاور ادغام میشود. در ابتدا libpng روی بایتهای داده مقایسهای انجام نمیدهد، بلکه ابتدا جمعآزما آنها را محاسبه کرده و سپس آن را با مقدار مورد انتظار بررسی میکند؛ این مقایسه باعث میشود که بایتهای داده نیز بهطور غیرمستقیم (از طریق وابستگی جریان داده) درگیر شوند.
با این حال، تحلیل وابستگی عملوندها (بخش ۳.۱.۳) جمعآزما را به درستی برای FixChecksums شناسایی میکند و پس از اینکه ورودی تعمیر شده و دوباره وارد مرحله جراحی میشود، libpng مقایسههای جدیدی روی بایتهای داده انجام میدهد و در نتیجه Weizz میتواند این فیلد داده را بهدرستی تشخیص داده و تگگذاری کند.
در نهایت، بخشهای خاکستری (IDAT DATA) یک تودهی باینری هستند که توسط Weizz به فیلدهای جداگانه شکسته شدهاند و بین آنها شکافهایی از بایتهای بدون تگ وجود دارد، زیرا همهی توابعی که این دادهها را پردازش میکنند روی کل این بلوک مقایسه انجام نمیدهند.
تحلیل مرزهای قطعه (chunk boundaries) چالشبرانگیزتر است، زیرا تابع FindFieldEnd به موقعیت اولیه حساس است. برای مثال، اگر تحلیل از روی ۸ بایت اعداد جادویی (magic number) شروع شود، این تابع به درستی کل مورد آزمون (Test Case) را به عنوان یک قطعه واحد شناسایی میکند؛ یا اگر از یک فیلد length آغاز شود، ممکن است تمام فیلدهای دیگر مربوط به همان قطعه را نیز پوشش دهد. با این حال، اگر تحلیل از یک فیلد type آغاز شود، ممکن است یک قطعه ناقص ساخته شود.
با وجود این، حتی در شرایطی که تشخیص مرزها دقیق نیست، Weizz همچنان میتواند به دو دلیل جهشهای (mutations) مفیدی انجام دهد. اول اینکه جهشهایی که بایتها را از سایر موارد آزمون قرض میگیرند، همان تگ اولیه (leading tag) را بررسی میکنند و این باعث میشود مثلاً عملیات جایگزینی (splicing) روی قطعههای ناقص اما احتمالاً سازگار انجام شود. دوم اینکه این عدم دقت میتواند به پوشش بهتر حالتهای لبهای (corner cases) یا کد تجزیه (parsing code) مشترک کمک کند؛ موضوعی که در بخش ۶ دوباره به آن برمیگردیم. در مورد libpng، ما پوشش کد بهتری نسبت به سایر فازرها، از جمله AFLSmart، به دست آوردیم.
جهشهای ساختاری در برابر دور زدن موانع (Roadblocks). برای بررسی عمیقتر نتایج بخش ۵.۱، یک سؤال مهم این است: چه مقدار از بهبود پوشش کد ناشی از جهشهای ساختاری است و چه مقدار ناشی از دور زدن موانع (roadblock bypassing)؟
ما ffmpeg را به عنوان مطالعهی موردی در نظر میگیریم. علاوه بر Weizz و نسخهی †Weizz که فاقد تکنیکهای دور زدن موانع است اما از جهشهای ساختاری استفاده میکند، یک نسخهی دیگر به نام ‡Weizz معرفی میکنیم که میتواند از اطلاعات I2S برای دور زدن موانع مانند RedQueen استفاده کند، اما فاقد جهشهای مبتنی بر تگ است.
در شکل ۸، پوشش کد و اندازهی صف ورودی (input queue size) پس از ۵ ساعت اجرا گزارش شده است. زمانی که هر کدام از این دو ویژگی بهصورت جداگانه بررسی شوند، به نظر میرسد جهشهای مبتنی بر تگ (tag-based mutations) نسبت به تکنیکهای دور زدن موانع برتری دارند (17٪+ پوشش بیشتر).
۶. نتیجهگیری (Concluding Remarks)
Weizz ایدههای جدیدی برای محاسبهٔ اطلاعات وابستگی بایتها (byte dependency information) معرفی میکند تا همزمان بتواند موانع موجود را برطرف کند و از فازینگ کاملاً خودکار (fully automatic fuzzing) برای قالبهای باینری مبتنی بر قطعه (chunk) پشتیبانی کند. نتایج آزمایشها امیدوارکننده به نظر میرسند: ما با روشهایی که نیاز به کمک انسان دارند رقابت میکنیم و همچنین باگهای جدیدی را در نرمافزارهایی که به خوبی تست شدهاند شناسایی کردیم.
رویکرد ما دو مزیت عملی دارد: نخست اینکه فازرها از قبل در مرحلههای قطعی اقدام به انجام bitflip میکنند، و ابزارگذاری (instrumentation) مقایسهها نیز برای عبور از موانع (roadblocks) به یک روش رایج تبدیل شده است. ما از این تحلیلها استفاده میکنیم تا رفتار برنامه را هنگام فازینگ بهتر توصیف کنیم و در نتیجه، مکانیزم انتساب تگ (tag assignment) را ممکن میسازیم.
رویکردهای قبلی اطلاعات کافی برای این هدف ارائه نمیکردند: حتی در مورد RedQueen، روش colorization» [5]» فقط بخشهای I2S (input-to-state) ورودی را شناسایی میکند (که برای عبور ازموانع (roadblock) مهم است)، اما نمیتواند وابستگیهای مربوط به بایتهای غیر I2S را آشکار کند.
یکی از نقاط ضعف این روش این است که bit flipping یا تغییر بیت روی ورودیهای بزرگ میتواند هزینهبر باشد. با این حال، زمان اجرای هر مورد توسط برنامه نیز عامل مهمی است. در آزمایشهای ما، Weizz مرحلهی اصلاح را روی ورودیهایی تا ۳ کیلوبایت اعمال کرد و در عین حال پوشش کد آن قابل مقایسه یا بهتر از سایر فازرهای مورد بررسی بود. ما بررسی استفاده از روشهای bit-level DTA (یا DTA سطح بیت) را به عنوان جایگزینی برای ورودیهای “پر هزینه” به کارهای آینده واگذار میکنیم.
ممکن است Weizz وابستگیهای مربوط به مقایسههایی را که با دستورالعملهای غیر ابزارگذاری شده (uninstrumented) انجام میشوند از دست بدهد. این حالت میتواند در کدهای بهینهسازی شده رخ دهد که از عملیات حسابی و منطقی برای تنظیم فلگهای (CPU flags) CPU به منظور اتخاذ تصمیم در شاخهگیری (branch decision) استفاده میکنند.
برای شناسایی این موارد میتوان از تحلیل ایستا درونرویهای (intra-procedural static analysis) [25] استفاده کرد (زیرا ثبت کردن همه آنها بهصورت کورکورانه میتواند پرهزینه باشد)، اما در حال حاضر ما ترجیح دادهایم برخی ناهماهنگیها در اکتشافهای خود را بپذیریم؛ برای مثال، در تابع FindChunkEnd ممکن است یک بایت را نادیده بگیریم وقتی که بقیه بایتها با الگوهای مورد انتظار همخوانی داشته باشند.
مرحلهی آگاه به ساختار (structure-aware) ما، علاوه بر اینکه نیازی به مشخصات (specification) ندارد، در نحوهی اعمال عملگرهای مرتبه بالا (high-order operators) نیز با AFLSmart متفاوت است. AFLSmart قطعهها را بهصورت جعبه سیاه (black-box) جهش میدهد؛ یعنی هیچ شواهدی در اختیار ندارد که آیا برنامه واقعاً آن بخش از ورودی را در طول اجرا پردازش کرده است یا خیر. در مقابل، Weizz نیز قطعههایی را انتخاب میکند که به طور غیرمستقیم توسط دستورهای مقایسهای اجرا شده در برنامه آشکار میشوند.
ما به سختی میتوانیم استدلال کنیم که کدام استراتژی در حالت کلی بهتر است. تفاوت مهم دیگر این است که چون طرحهای استنتاج ما قابل اتکا نیستند، ممکن است ورودیها را به شکلهای غیرعادی جهش دهیم؛ برای مثال، ممکن است فقط بخشهایی از یک قطعه را با یک جزء قابل مقایسه از یک ورودی دیگر جایگزین کنیم.
در مقالهی AFLSmart توضیح داده شده که برخی باگها تنها بهدلیل استفاده از مشخصات آزادتر (relaxed specification) کشف شدهاند [24]. این موضوع با تجربهی Grimoire در زمینهی دستور زبانها نیز سازگار است؛ جایی که مسیرهایی خارج از مشخصات باعث کشف خطاهای برنامه شدهاند [8].
ما قصد داریم به عنوان کارهای آینده، مجموعهی بزرگتری از برنامهها را بررسی کنیم و تأثیر تکنیکهای آگاه به ساختار (structure-aware) را روشنتر سازیم: اینکه این تکنیکها چگونه بر پوشش کد (coverage) اثر میگذارند، کدامین آنها برای یک برنامه مؤثرتر هستند و ورودیهای جهشیافته هر چند وقت یکبار با مشخصات مطابقت ندارند.
پاسخ دادن به این پرسشها چندان ساده نیست، زیرا فازینگ دارای توان پردازشی بالا (high throughput) و آنتروپی (entropy – بینظمی، عدم قطعیت) زیادی است. همچنین امکان بالایی برای گسترش روشهای استنتاج قطعه (chunk inference) با اکتشافهای جدید یا بهینهتر کردن روشهای فعلی وجود دارد. برای مثال، ما در حال بررسی یک نسخهی امیدوارکننده هستیم که در آن شروع یک قطعه را در فیلدی قرار میدهیم که از بایتهای I2S تشکیل شده است؛ این فیلد ممکن است نشاندهندهی یک مقدار جادویی (magic value) برای نوع آن قطعه باشد.
منابع
[1] 2016. Circumventing Fuzzing Roadblocks with Compiler Transfor-mations. https://lafintel.wordpress.com/2016/08/15/circumventing-fuzzing-roadblocks-with-compiler-transformations/. [Online; accessed 10-Sep-2019].
[2] 2019. Google OSS-Fuzz: continuous fuzzing of open source software. https://github.com/google/oss-fuzz. [Online; accessed 10-Sep-2019].
[3] Dave Aitel. 2002. The Advantages of Block-Based Protocol Analysis for Security Testing. https://www.immunitysec.com/downloads/advantages_of_block_based_analysis.html. [Online; accessed 10-Sep-2019].
[4] Cornelius Aschermann, Tommaso Frassetto, Thorsten Holz, Patrick Jauernig, Ahmad-Reza Sadeghi, and Daniel Teuchert. 2019. NAUTILUS: Fishing for Deep Bugs with Grammars. In 26th Annual Network and Distributed System Secu-rity Symposium, NDSS. https://www.ndss-symposium.org/ndss-paper/nautilus-fishing-for-deep-bugs-with-grammars/
[5] Cornelius Aschermann, Sergej Schumilo, Tim Blazytko, Robert Gawlik, and Thorsten Holz. 2019. REDQUEEN: Fuzzing with Input-to-State Correspondence. In 26th Annual Network and Distributed System Security Symposium,NDSS. https://www.ndss-symposium.org/ndss-paper/redqueen-fuzzing-with-input-to-state-correspondence/
[6] Roberto Baldoni, Emilio Coppa, Daniele Cono D’Elia, Camil Demetrescu, and Irene Finocchi. 2018. A Survey of Symbolic Execution Techniques. Comput. Surveys 51, 3, Article 50 (2018), 39 pages. https://doi.org/10.1145/3182657
[7] Blackngel. 2019. MALLOC DES-MALEFICARUM. http://phrack.org/issues/66/10.html. [Online; accessed 10-Sep-2019].
[8] Tim Blazytko, Cornelius Aschermann, Moritz Schlögel, Ali Abbasi, Sergej Schumilo, Simon Wörner, and Thorsten Holz. 2019. GRIMOIRE: Synthesizing Structure while Fuzzing. In 28th USENIX Security Symposium (USENIX Security 19). 1985–2002. https://www.usenix.org/system/files/sec19-blazytko.pdf
[9] Mathias Bynens. 2019. Smallest possible syntactically valid files of different types. https://github.com/mathiasbynens/small. [Online; accessed 10-Sep-2019].
[10] P. Chen and H. Chen. 2018. Angora: Efficient Fuzzing by Principled Search. In 2018 IEEE Symposium on Security and Privacy (SP). 711–725. https://doi.org/10. 1109/SP.2018.00046
[11] Jaeseung Choi, Joonun Jang, Choongwoo Han, and Sang Kil Cha. 2019. Greybox Concolic Testing on Binary Code. In Proceedings of the 41st International Conference on Software Engineering (ICSE ’19). 736–747. https://doi.org/10.1109/ICSE.2019.00082
[12] Weidong Cui, Marcus Peinado, Karl Chen, Helen J. Wang, and Luis Irun-Briz. 2008. Tupni: Automatic Reverse Engineering of Input Formats. In Proceedings of the 15th ACM Conference on Computer and Communications Security (CCS ’08). 391–402. https://doi.org/10.1145/1455770.1455820
[13] Daniele Cono D’Elia, Emilio Coppa, Simone Nicchi, Federico Palmaro, and Lorenzo Cavallaro. 2019. SoK: Using Dynamic Binary Instrumentation for Security (And How You May Get Caught Red Handed). In Proceedings of the 2019 ACM Asia Conference on Computer and Communications Security (Asia CCS ’19). 15–27. https://doi.org/10.1145/3321705.3329819
[14] Daniele Cono D’Elia, Camil Demetrescu, and Irene Finocchi. 2016. Mining Hot Calling Contexts in Small Space. Software: Practice and Experience 46 (2016),1131–1152. https://doi.org/10.1002/spe.2348
[15] M. Eddington. [n.d.]. Peach fuzzing platform. https://web.archive.org/web/20180621074520/http://community.peachfuzzer.com/WhatIsPeach.html. [Online; accessed 10-Sep-2019].
[16] Marc Heuse, Heiko Eißfeldt, and Andrea Fioraldi. 2019. AFL++. https://github.com/vanhauser-thc/AFLplusplus. [Online; accessed 10-Sep-2019].
[17] Christian Holler, Kim Herzig, and Andreas Zeller. 2012. Fuzzing with Code Fragments. In Proceedings of the 21st USENIX Conference on Security Symposium (SEC’12). 38–38. http://dl.acm.org/citation.cfm?id=2362793.2362831
[18] Matthias Höschele and Andreas Zeller. 2016. Mining Input Grammars from Dynamic Taints. In Proceedings of the 31st IEEE/ACM International Conference on Automated Software Engineering (ASE 2016). 720–725. https://doi.org/10.1145/2970276.2970321
[19] Matthias Höschele and Andreas Zeller. 2017. Mining Input Grammars with AUTOGRAM. In Proceedings of the 39th International Conference on Software Engineering Companion (ICSE-C ’17). 31–34. https://doi.org/10.1109/ICSE-C. 2017.14
[20] Mateusz Jurczyk. 2019. CompareCoverage. https://github.com/googleprojectzero/CompareCoverage/. [Online; accessed 10-Sep-2019].
[21] Yuekang Li, Bihuan Chen, Mahinthan Chandramohan, Shang-Wei Lin, Yang Liu, and Alwen Tiu. 2017. Steelix: Program-state Based Binary Fuzzing. In Proceedings of the 2017 11th Joint Meeting on Foundations of Software Engineering (ESEC/FSE 2017). 627–637. https://doi.org/10.1145/3106237.3106295
[22] Tavis Ormandy. 2009. Making Software Dumberer. http://taviso.decsystem.org/making_software_dumber.pdf. [Online; accessed 10-Sep-2019].
[23] H. Peng, Y. Shoshitaishvili, and M. Payer. 2018. T-Fuzz: Fuzzing by Program Transformation. In 2018 IEEE Symposium on Security and Privacy (SP). 697–710. https://doi.org/10.1109/SP.2018.00056
[24] V. Pham, M. Boehme, A. E. Santosa, A. R. Caciulescu, and A. Roychoudhury. 2019. Smart Greybox Fuzzing. IEEE Transactions on Software Engineering (2019). https://doi.org/10.1109/TSE.2019.2941681
[25] Sanjay Rawat, Vivek Jain, Ashish Kumar, Lucian Cojocar, Cristiano Giuffrida, and Herbert Bos. 2017. VUzzer: Application-aware Evolutionary Fuzzing. In 24th Annual Network and Distributed System Security Symposium, NDSS. https://www.ndss-symposium.org/ndss2017/ndss-2017-programme/vuzzer-application-aware-evolutionary-fuzzing/
[26] Edward J. Schwartz, Thanassis Avgerinos, and David Brumley. 2010. All You Ever Wanted to Know About Dynamic Taint Analysis and Forward Symbolic Execution (but Might Have Been Afraid to Ask). In Proceedings of the 2010 IEEE Symposium on Security and Privacy (SP 2010). 317–331. https://doi.org/10.1109/SP.2010.26
[27] Nick Stephens, John Grosen, Christopher Salls, Audrey Dutcher, Ruoyu Wang, Jacopo Corbetta, Yan Shoshitaishvili, Christopher Kruegel, and Giovanni Vigna. 2016. Driller: Augmenting Fuzzing Through Selective Symbolic Exe- cution.. In 23th Annual Network and Distributed System Security Symposium, NDSS. https://www.ndss-symposium.org/wp-content/uploads/2017/09/driller-augmenting-fuzzing-through-selective-symbolic-execution.pdf
[28] Robert Swiecki. 2017. honggfuzz. https://github.com/google/honggfuzz. [Online; accessed 10-Sep-2019].
[29] Ari Takanen, Jared D. Demott, and Charles Miller. 2018. Fuzzing for Software Security Testing and Quality Assurance (2nd ed.). Artech House, Inc., Norwood, MA, USA.
[30] 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 (ICSE ’19). 724–735. https://doi.org/10.1109/ICSE.2019.00081
[31] T. Wang, T. Wei, G. Gu, and W. Zou. 2010. TaintScope: A Checksum-Aware Directed Fuzzing Tool for Automatic Software Vulnerability Detection. In 2010 IEEE Symposium on Security and Privacy (SP). 497–512. https://doi.org/10.1109/SP.2010.37
[32] B. Yadegari and S. Debray. 2014. Bit-Level Taint Analysis. In 2014 IEEE 14th International Working Conference on Source Code Analysis and Manipulation (SCAM). 255–264. https://doi.org/10.1109/SCAM.2014.43
[33] B. Yadegari, B. Johannesmeyer, B. Whitely, and S. Debray. 2015. A Generic Approach to Automatic Deobfuscation of Executable Code. In 2015 IEEE Symposium on Security and Privacy (SP). 674–691. https://doi.org/10.1109/SP.2015.47
[34] Wei You, Xuwei Liu, Shiqing Ma, David Perry, Xiangyu Zhang, and Bin Liang. 2019. SLF: Fuzzing without Valid Seed Inputs. In Proceedings of the 41st International Conference on Software Engineering (ICSE ’19). 712–723. https://doi.org/10.1109/ICSE.2019.00080
[35] Insu Yun, Sangho Lee, Meng Xu, Yeongjin Jang, and Taesoo Kim. 2018. QSYM: A Practical Concolic Execution Engine Tailored for Hybrid Fuzzing. In Proceedings of the 27th USENIX Conference on Security Symposium (SEC’18). 745–761. http://dl.acm.org/citation.cfm?id=3277203.3277260
[36] Michał Zalewski. 2019. American Fuzzy Lop. https://github.com/Google/AFL. [Online; accessed 10-Sep-2019].
[37] A. Zeller, R. Gopinath, M. Böhme, G. Fraser, and C. Holler. 2019. The Fuzzing Book. https://www.fuzzingbook.org/. [Online; accessed 10-Sep-2019].