خانه » LAVA: افزودن خودکار آسیب‌پذیری در مقیاس بزرگ

LAVA: افزودن خودکار آسیب‌پذیری در مقیاس بزرگ

LAVA: Large-scale Automated Vulnerability Addition

توسط Vulnerlab
9 بازدید
افزودن خودکار آسیب‌پذیری در مقیاس بزرگ - LAVA - آسیب‌پذیری‌ - Vulnerability - فازینگ - Fuzzing

پژوهش‌های مرتبط با خودکارسازی کشف آسیب‌پذیری‌ها (Vulnerability)، از دیرباز به دلیل کمبود مجموعه‌داده‌های مرجع معتبر (Ground Truth) برای ارزیابی ابزارها و روش‌ها با محدودیت مواجه بوده‌اند. فقدان چنین داده‌هایی موجب می‌شود که نه توسعه‌دهندگان ابزارها و نه کاربران آن‌ها نتوانند معیارهای بنیادینی همچون نرخ عدم کشف (Miss Rate) و نرخ هشدار کاذب (False Alarm Rate) را اندازه‌گیری کنند.ما در این مقاله،LAVA  را معرفی می‌کنیم؛ روشی نوین مبتنی بر تحلیل پویای آلودگی (Dynamic Taint Analysis) که با تزریق سریع و خودکار تعداد زیادی باگ (Bug) واقع‌گرایانه به کد منبع برنامه، مجموعه‌داده‌های مرجع معتبر (Ground Truth) تولید می‌کند. هر باگ ایجاد شده توسط LAVA با یک ورودی مشخص همراه است که که موجب بروز آن باگ می‌شود. این در حالی است که احتمال آن‌که ورودی‌های معمولی نیز همان باگ را آشکار کنند، بسیار ناچیز است. این آسیب‌پذیری‌ها اگرچه مصنوعی (Synthetic) هستند، اما واقع‌گرایانه‌اند؛ زیرا در عمق برنامه قرار داده شده‌اند و با ورودی‌های واقعی قابل بروز هستند.ما با استفاده از LAVA، هزاران باگ را به هشت برنامه واقعی، از جمله bash،tshark و GNU coreutils تزریق کرده‌ایم. در یک ارزیابی مقدماتی، مشاهده کردیم که یک فازر  (Fuzzer)شناخته ‌شده و یک ابزار کشف باگ مبتنی بر اجرای نمادین (Symbolic Execution) توانستند بخشی، اما نه تمام، باگ‌های تزریق‌ شده توسط LAVA را شناسایی کنند. همچنین، الگوها و ناهنجاری‌های جالبی در عملکرد آن‌ها از همان مراحل اولیه ارزیابی آشکار شد. این پژوهش، مبنایی برای رویکردی در تولید مجموعه‌داده‌های بزرگ مرجع معتبر (Ground Truth) از آسیب‌پذیری‌ها برحسب تقاضا فراهم می‌کند که امکان ارزیابی دقیق و نظام‌مند ابزارها را فراهم ساخته و هدفی باکیفیت برای توسعه‌دهندگان این ابزارها ارائه می‌دهد.

I . دلایل انجام پژوهش (Motivation)

ابزارهای کشف باگ تقریباً از همان آغاز پیدایش برنامه‌های رایانه‌ای، همواره یکی از حوزه‌های فعال پژوهش بوده‌اند. روش‌هایی نظیر تفسیر انتزاعی (Abstract Interpretation)، فازینگ (Fuzzing) و اجرای نمادین (Symbolic Execution) همراه با حل قیود یا محدودیت‌ها (Constraint Solving) پیشنهاد، توسعه و اعمال شده‌اند. با این حال، ارزیابی این روش‌ها همواره با چالش مواجه بوده است، زیرا مجموعه‌داده‌های مرجع معتبر (Ground Truth) به ‌شدت کمیاب می‌باشند. اگرچه مجموعه‌های مرجع آسیب‌پذیری (Vulnerability Corpora) وجود دارند [10]، اما از نظر تعداد نمونه‌ها و میزان سودمندی، محدود هستند.

این مجموعه‌داده‌ها به دو دسته تقسیم می‌شوند: مجموعه‌های برگرفته از آسیب‌پذیری‌های واقعی (Historic vulnerability) و مجموعه‌های مصنوعی (Synthetic vulnerability). مجموعه‌های برگرفته از آسیب‌پذیری‌های واقعی، به دلیل محدود بودن تعداد نمونه‌ها، برای ارزیابی ابزارهای کشف باگ کارایی چندانی ندارند [۲۷]. با این حال، این دسته بیشترین شباهت را به آنچه مطلوب ماست دارند؛ زیرا باگ‌ها در کدهای واقعی قرار دارند، با ورودی‌های واقعی قابل فعال‌سازی هستند و در بسیاری از موارد، همراه با مستندات دقیقی هستند که محل بروز باگ را به ‌طور دقیق مشخص می‌کنند.

بااین‌حال، ایجاد چنین مجموعه‌داده‌ای فرآیندی دشوار و زمان‌بر است. به گفته نویسندگان یکی از پژوهش‌های پیشین در زمینه ارزیابی ابزارهای کشف باگ، تهیه مجموعه‌ای شامل چهارده باگ واقعیِ ثبت ‌شده که به‌دقت مستندسازی شده‌اند و برای هر یک ورودی فعال‌کننده نیز فراهم شده است، حدود شش ماه زمان برده است [26]. علاوه بر این، مجموعه‌های عمومی این محدودیت را نیز دارند که پس از انتشار به‌سرعت ارزش خود را برای ارزیابی از دست می‌دهند؛ زیرا انتظار می‌رود ابزارهای کشف باگ با استفاده از همین نمونه‌های منتشرشده آموزش دیده یا برای شناسایی آن‌ها بهینه شده باشند. با توجه به اینکه ارزش تجاری آسیب‌پذیری‌های جدید و قابل بهره‌برداری، بنا بر برآوردهای رایج، از ده‌ها هزار دلار آغاز می‌شود [20]، یافتن آسیب‌پذیری‌های واقعی که تاکنون منتشر نشده و در آموزش ابزارها به کار نرفته باشند، برای استفاده در مجموعه مرجع ما بسیار دشوار است.

از سوی دیگر، اگرچه کدهای مصنوعی که باگ‌های آن‌ها به ‌صورت خودکار توسط اسکریپت‌ها ایجاد شده‌اند، اگرچه می‌توانند تعداد زیادی نمونه آزمایشی برای ارزیابی ابزارها فراهم کنند، اما هر نمونه تنها یک برنامهٔ کوچک است و الگوهای به‌کاررفته در آن‌ها غالباً نمایندهٔ مناسبی از کدهای واقعی نیستند [2]، [11].

در عمل، یک ابزار کشف آسیب‌پذیری معمولاً با اجرای آن و بررسی آسیب‌پذیری‌هایی که شناسایی می‌کند، ارزیابی می‌شود. بر این اساس، ابزاری برتر تلقی می‌شود که نسبت به ابزار دیگر تعداد بیشتری باگ را کشف کند. اگرچه با توجه به کمبود مجموعه‌داده‌های مرجع معتبر (Ground Truth)، این شیوه ارزیابی کاملاً قابل درک است، اما مانعی بر سر راه پیشرفت علمی و توسعه روش‌های کشف آسیب‌پذیری به شمار می‌آید. در حال حاضر، هیچ روشی برای اندازه‌گیری شاخص‌های اساسی ارزیابی (Fundamental figures of merit) همچون نرخ عدم کشف (Miss Rate) و نرخ هشدار کاذب (False Alarm Rate) برای یک ابزار کشف باگ وجود ندارد. ما معتقدیم که اگر یک مجموعه مرجع آسیب‌پذیری (Vulnerability Corpus) قرار است برای پژوهش، توسعه و ارزیابی مفید باشد، باگ‌های موجود در آن می‌بایست دارای ویژگی‌های زیر باشند:

  1. تولید آن‌ها کم‌هزینه باشد و بتوان تعداد زیادی از آن‌ها را ایجاد کرد.
  2. در مراحل مختلف اجرای برنامه قابل بروزرسانی باشند.
  3. در مسیرهای متداول و واقع‌گرایانه جریان کنترل (Control Flow) و جریان داده (Data Flow) قرار گرفته باشند.
  4. همراه با ورودی باشند که وقوع باگ را اثبات کند.
  5. تنها برای بخش بسیار کوچکی از ورودی‌های ممکن فعال یا آشکار شوند.

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

رویکرد پیشنهادی ما، ایجاد یک آسیب‌پذیری ساختگی و مصنوعی (Synthetic Vulnerability) از طریق چند ویرایش هدفمند و خودکار در کد منبع یک برنامه واقعی است. در ادامه، جزئیات و نتایج پیاده‌سازی این رویکرد را که تمامی الزامات فوق را برآورده می‌کند و آن را LAVA (Large-scale Automated Vulnerability Addition)  می‌نامیم، ارائه خواهیم کرد.

LAVA می‌تواند در برنامه‌ای مانند file، با حدود ۱۳ هزار خط کد، تنها در حدود ۱۵ ثانیه یک باگ جدی، مانند سرریز بافر (Buffer Overflow) را تزریق کند. باگ‌های ایجاد شده توسط LAVA در سراسر مسیر اجرای برنامه (Execution Trace)، در تمام بخش‌های آن، چه در مسیرهای سطحی و چه در مسیرهای عمیق، ظاهر می‌شوند و از جریان داده (Data Flow) استفاده می‌کنند. LAVA به‌گونه‌ای طراحی شده است که برای هر باگ، یک ورودی مشخص وجود دارد که آن را آشکار می‌کند و تضمین می‌شود هیچ ورودی دیگری نتواند همان باگ را آشکار کند.

۲. دامنه (Scope)

ما با LAVA، توجه خود را به تزریق باگ به کد منبع محدود می‌کنیم. این انتخاب با توجه به هدف ما، یعنی استفاده از آن برای ساخت مجموعه‌داده‌های بزرگ به‌منظور ارزیابی، توسعه تکنیک‌ها و سیستم‌های کشف آسیب‌پذیری، منطقی است. سیستم‌های کشف خودکار باگ می‌توانند روی کد منبع [۱]، [۸]، [۹]، [۲۴] یا باینری‌ها [۳]، [۲۱] کار کنند؛ ما به‌راحتی می‌توانیم ابزارهای تحلیل باینری را با کامپایل کردن کد منبع تغییر‌یافته آزمایش کنیم. تزریق مستقیم باگ به باینری یا بایت‌کد نیز ممکن است با رویکرد مشابه ما امکان‌پذیر باشد، اما در اینجا به آن نمی‌پردازیم. ما تمرکز خود را بیشتر به نرم‌افزارهای متن‌باز لینوکسی نوشته‌ شده به زبان C محدود می‌کنیم، زیرا کد منبع و ابزارهای بازنویسی کد منبع برای آن‌ها در دسترس است. همان‌طور که بعداً توضیح می‌دهیم، رویکردی مشابه برای زبان‌های دیگر نیز قابل استفاده خواهد بود. ما می‌خواهیم باگ‌های تزریق‌شده، باگ‌های جدی باشند؛ یعنی آسیب‌پذیری‌هایی که به‌طور بالقوه قابل بهره‌برداری هستند.

به‌ عنوان یک معیار عملی، در حال حاضر تمرکز ما بر تزریق کدی است که بتواند باعث خواندن و نوشتن خارج از محدوده (Out-of-Bounds Read/Write)  شود و با یک ورودی تحت کنترل مهاجم به وقوع بپیوندد. در بخش ۸، توسعه LAVA برای پشتیبانی از کلاس‌های دیگر باگ را بررسی می‌کنیم. ما برای هر باگی که با موفقیت تزریق می‌کنیم، یک ورودی اثبات مفهوم (Proof-of-Concept Input) تولید می‌کنیم که باعث بروز آن باگ می‌شود؛ با این حال، تلاش نمی‌کنیم یک بهره‌برداری واقعی (Actual Exploit) تولید کنیم. به دلیل اختصار، در این مقاله کلمات «باگ» و «آسیب‌پذیری» را به‌ جای یکدیگر به کار می‌بریم. در هر دو مورد، منظور ما آسیب‌پذیری‌هایی (به‌ویژه عمدتاً خواندن و نوشتن خارج از محدوده) است که موجب کرش‌های بالقوه قابل بهره‌برداری می‌شوند

۳. مروری بر LAVA (LAVA OVERVIEW)

به‌طور کلی، LAVA فرایند افزودن باگ به برنامه‌ها را به شکل زیر انجام می‌دهد. برای این منظور، ابتدا دنباله و رهگیری اجرای برنامه (Execution Trace) مربوط به اجرای برنامه با یک ورودی مشخص را در اختیار داریم و سپس مراحل زیر را دنبال می‌کنیم:

  1. شناسایی نقاطی از دنباله و رهگیری اجرا که بایت‌های ورودی در آن‌ها در دسترس می‌باشند، اما جریان کنترل برنامه را تعیین نمی‌کنند و همچنین تغییرات محاسباتی زیادی روی آن‌ها انجام نشده است.
    این مقادیر را DUA می‌نامیم که مخفف داده‌های مرده، بدون پیچیدگی و در دسترس (Dead, Uncomplicated and Available data)   است؛ یعنی داده‌هایی که مرده (Dead)، ساده از نظر محاسباتی (Uncomplicated) و در دسترس (Available) می‌باشند.
  2. یافتن نقاط حمله بالقوه (Potential Attack Points) که از نظر زمانی پس از یک DUA در رد اجرای برنامه قرار دارند. نقاط حمله، مکان‌هایی در کد منبع برنامه هستند که اگر مقدار یک DUA در آن نقطه نیز در دسترس باشد، می‌توان از آن برای ایجاد آسیب‌پذیری در برنامه استفاده کرد.
  3. افزودن کد به برنامه برای انتقال مقدار DUA به نقطه حمله و استفاده از آن برای فعال‌سازی آسیب‌پذیری.

این سه مرحله در سه بخش بعدی بررسی خواهند شد؛ بخش‌هایی که به مثال اجرایی ارائه‌شده در شکل ۱ ارجاع می‌دهند.

شکل ۱. مثال اجرایی LAVA. هنگام ورود به تابع foo، متغیر a شامل بایت‌های ۰ تا ۳ ورودی، متغیر b شامل بایت‌های ۴ تا ۷ و متغیر n شامل بایت‌های ۸ تا ۱۱ ورودی است. اشاره‌گرهای s و d و نیز بافرهایی که به آن‌ها اشاره می‌کنند، بدون برچسب آلودگی (Untainted) هستند:

				
					void foo(int a, int b, char *s, char *d, int n) {
	int c = a+b;
	if (a != 0xdeadbeef)
		return;
	for (int i=0; i<n; i++)
		c+=s[i];
	memcpy(d,s,n+c); // Original source
	// BUG: memcpy(d+(b==0x6c617661)*b,s,n+c);
}
				
			

۳.۱ DUA – داده‌های مرده، بدون پیچیدگی و در دسترس (Dead, Uncomplicated and Available data)

از آنجا که DUAها تضمین می‌کنند داده‌های تحت کنترل مهاجم برای تأثیرگذاری بر رفتار برنامه در دسترس می‌باشند، این مقادیر به ‌عنوان ورودی اولیه خام برای ساخت باگ‌ها مورد استفاده قرار می‌گیرند. ما DUAها را در یک برنامه، با اجرای آن برنامه تحت یک تحلیل پویای آلودگی (Dynamic Taint Analysis) [14] برای یک ورودی مشخص، شناسایی می‌کنیم. این تحلیل آلودگی دارای چند ویژگی مهم است:

  • به هر بایت از ورودی، یک برچسب (Label) اختصاص داده می‌شود. بنابراین، اگر یک مقدار داخلی در برنامه آلوده (Tainted) باشد، می‌توان آن مقدار را به بخش مشخصی از ورودی که منشأ آن بوده است، نگاشت کرد.
  • تحلیل آلودگی تا حد امکان کامل و صحیح (Complete and Correct) انجام می‌شود. تمامی کدهای برنامه، شامل کتابخانه‌ها (Libraries) و هسته سیستم‌عامل (Kernel)، تحت تحلیل آلودگی قرار می‌گیرند. همچنین چندین نخ (Multiple Threads) و فرایند (Processes) نیز به‌ درستی مدیریت می‌شوند تا جریان‌های آلودگی (Taint Flows)  از دست نروند.
  • تحلیل آلودگی، مجموعه‌ای از برچسب‌ها (Labels) را برای هر بایت از داده‌های برنامه نگهداری می‌کند. این ویژگی باعث می‌شود بتواند محاسباتی را که در آن چندین بایت ورودی با یکدیگر ترکیب می‌شوند، نمایش دهد.

هر متغیر آلوده (Tainted Variable) در برنامه، تابعی از بایت‌های ورودی است. ما میزان پیچیدگی این تابع را با استفاده از یک معیار جدید به نام عدد محاسبات آلودگی (Taint Compute Number – TCN) تخمین می‌زنیم. TCN به ‌سادگی عمق درخت محاسباتی موردنیاز برای به‌دست آوردن یک مقدار از بایت‌های ورودی را دنبال می‌کند. هرچه مقدار TCN برای یک مقدار برنامه‌ای کمتر باشد، آن مقدار از نظر محاسباتی به ورودی اولیه نزدیک‌تر است. اگر مقدار TCN برابر با ۰ باشد، آن مقدار یک کپی مستقیم از بایت‌های ورودی است. ایده اصلی پشت این معیار آن است که برای اینکه بتوانیم از DUAها با نتایج قابل پیش‌بینی استفاده کنیم، به مقادیری نیاز داریم که از نظر محاسباتی به ورودی اولیه نزدیک باشند. توجه داشته باشید کهTCN  یک معیار ایده‌آل نیست. بدیهی است که موقعیت‌هایی وجود دارند که در آن‌ها درخت محاسبات (Computation Tree) عمیق است، اما مقدار حاصل ‌شده کاملاً قابل پیش‌بینی بوده و به اندازه مقدار اولیه، دارای آنتروپی (Entropy) است. با این حال، TCN این مزیت را دارد که محاسبه آن در سطح دستور به دستور (Instruction-by-Instruction) ساده است.

افزودن خودکار آسیب‌پذیری در مقیاس بزرگ - LAVA - آسیب‌پذیری‌ - Vulnerability - فازینگ - Fuzzing
شکل ۲. نمونه‌هایی از عدد محاسبات آلودگی (Taint Compute Number - TCN) در مثال اجرایی LAVA. مقدار TCN برابر با عمق درخت محاسباتی است که برای تولید یک مقدار از ورودی‌های آلوده طی می‌شود. در این مثال، TCN(c) پس از اجرای خط ۲ برابر ۱ و پس از اجرای خط ۶ (در زمان خروج از حلقه) برابر با n+1 است.

هرگاه سیستم آلودگی (Taint System) نیاز به محاسبه اجتماع مجموعه‌های برچسب آلودگی (Union of Taint Label Sets) برای نمایش محاسبات داشته باشد، TCN مرتبط با مجموعه حاصل یک واحد بیشتر از بیشینه TCN مجموعه‌های ورودی است. در مثال اجرایی و همان‌طور که در شکل ۲ نشان داده شده، ۱TCN(c) =  پس از خط ۱ است، زیرا از مقادیر a و b محاسبه شده که مستقیماً از ورودی مشتق شده‌اند. بعداً، درست قبل از خط ۷ و پس از حلقه، TCN(c) = n + 1 است، زیرا هر تکرار حلقه عمق درخت محاسبات را یک واحد افزایش می‌دهد.

معیار دیگر مبتنی بر آلودگی (Taint-Based Measure) که LAVA معرفی می‌کند، زنده و فعال‌ ‌بودن (Liveness) است که به برچسب‌های آلودگی (Taint Labels) یعنی خود بایت‌های ورودی، اختصاص داده می‌شود. این معیار به‌صورت مستقیم، تعداد شاخه‌هایی (Branches) را شمارش می‌کند که یک بایت از ورودی در تصمیم‌گیری آن‌ها نقش داشته است. بنابراین، اگر برچسب مربوط به یک بایت ورودی خاص، هرگز در هیچ‌ یک از مجموعه‌های برچسب آلودگی (Taint Label Sets) مرتبط با بایت‌هایی که برای تصمیم‌گیری یک شاخه (Branch) استفاده شده‌اند، مشاهده نشود، مقدار فعال یا زنده ‌بودن (Liveness) آن برابر با صفر خواهد بود.

یکDUA  که به‌طور کامل از بایت‌هایی با مقدار فعال یا زنده ‌بودن (Liveness) صفر یا بسیار پایین تشکیل شده باشد، از این دیدگاه که تأثیر اندکی بر جریان کنترل (Control Flow) در این دنباله اجرای برنامه (Program Trace) دارد، می‌تواند مرده (Dead) در نظر گرفته شود. چنانچه بایت‌های ورودی مرده (Dead) را تحت فازینگ (Fuzzing) قرار دهیم، انتظار می‌رود برنامه نسبت به تغییرات آن‌ها بی‌تفاوت باشد و همان دنباله اجرا (Execution Trace) را طی کند. در مثال مورد بررسی، پس از خط ۳، مقدار LIV (0..3) = 1 است؛ زیرا مقدارa  یک کپی مستقیم از بایت‌های ورودی ۰ تا ۳ است. پس از هر بار اجرای حلقه، مقدار فعال و زنده ‌بودن بایت‌های ۸ تا ۱۱، که حد حلقه (Loop Bound) را تعیین می‌کنند، یک واحد افزایش می‌یابد. بنابراین پس از پایان حلقه LIV (8..11) = n خواهد بود.

شکل‌های ۳ و ۴، نمودارهای فعال‌ و زنده بودن (Liveness) و عدد محاسبات آلودگی (Taint Compute Number) را برای برنامه file در حال پردازش ورودی /bin/ls نمایش می‌دهند. در هر دو نمودار، محور افقی نشان‌دهنده تعداد دستورهای بازپخش‌ (Replay Instructions) و پردازش‌شده است و از این رو، به ‌صورت تقریبی، معادل زمان در نظر گرفته می‌شود. محور عمودی نشان‌ دهنده موقعیت فایل (File Position) است؛ به‌طوری‌که در پایین نمودار، اولین بایت از /bin/ls و در بالای آن، بایت هفتادم قرار دارد.

شکل ۳. نمودار تغییرات زنده‌بودن (Liveness) بایت‌های ورودی فایل /bin/ls در طول اجرای برنامه file و هنگام پردازش این فایل
شکل ۴. نمایش تغییرات عدد محاسبات آلودگی (TCN) بایت‌های ورودی فایل /bin/ls در گذر زمان، هنگام پردازش این فایل توسط برنامه file

بدیهی است که فایل /bin/ls بیش از ۷۰ بایت حجم دارد. با این حال، همان‌طور که انتظار می‌رود، تنها هدر ۶۴ بایتی ELF دارای مقادیر قابل توجهی از فعال‌بودن یا عدد محاسبات آلودگی است. بنابراین، در این نمودارها، توجه خود را به همین بخش محدود کرده‌ایم. در هر دو نمودار، مقادیر بالاتر برای فعال‌بودن و عدد محاسبات آلودگی با نواحی تیره‌تر نمایش داده شده‌اند. بر این اساس، بخش‌هایی که در بازه‌های افقی طولانی از سمت چپ نمودار بسیار روشن هستند، در هر دو نمودار نشان‌دهنده DUAها می‌باشند. برای مثال، بایت‌های ۲۸ تا ۳۲ که به ابتدای جدول هدرهای برنامه (Program Header Table) اشاره می‌کنند، در هیچ شاخه یا محاسباتی دخیل نیستند. بنابراین، احتمالاً این بایت‌ها یک DUA مناسب خواهند بود و منطقی به نظر می‌رسد که برنامه file کاربرد زیادی برای این اطلاعات ندارد.

در مقابل، بایت‌های ۱۹ و ۲۰ که مجموعه دستورالعمل (Instruction Set) را مشخص می‌کنند، پس از حدود ۵۵ میلیون دستور (55M Instructions) دارای میزان فعال‌بودن بسیار بالایی هستند. این موضوع منطقی است، زیرا برنامهfile  باید این اطلاعات را در قالبی قابل فهم برای انسان، مانند x86_64، نمایش دهد.

با این حال، بایت‌های ۱۰ تا ۱۶ را در نظر بگیرید که ظاهراً در تعدادی از شاخه‌ها مورد استفاده قرار گرفته‌اند. این موضوع با توجه به اینکه در مشخصات ELF Header به‌ عنوان فیلدهای استفاده‌ نشده (Unused) علامت‌گذاری شده‌اند، غیرعادی به نظر می‌رسد. این نوع اختلاف‌ها، استدلال خوبی برای استفاده از معیارهای مبتنی بر آلودگی (Taint-Based Measures) به ‌منظور تزریق باگ‌ها فراهم می‌کنند، به‌جای آنکه صرفاً به مشخصات منتشرشده و مستندات رسمی اعتماد کنیم.

ترکیب داده‌های برنامه‌ای بدون پیچیدگی (Uncomplicated) باTCN  پایین و داده‌های مرده (Dead) با فعال و زنده ‌بودن (Liveness) پایین، ترکیب قدرتمندی برای تزریق آسیب‌پذیری (Vulnerability Injection) محسوب می‌شود. DUAهایی که این ترکیب شناسایی می‌کند، کمیت‌های داخلی برنامه هستند که اغلب یک کپی مستقیم از بایت‌های ورودی محسوب می‌شوند و می‌توان مقدار آن‌ها را به هر مقدار دلخواه تنظیم کرد، بدون اینکه برنامه را به سمت یک مسیر اجرای متفاوت هدایت کنیم. این ویژگی، آن‌ها را به محرک‌های بسیار مناسبی برای آسیب‌پذیری‌ها (Vulnerability Triggers) تبدیل می‌کند.

در مثال مورد بررسی، بایت‌های ۰ تا ۳ و ۸ تا ۱۱ تا حدی زنده و دارای فعالیت هستند، زیرا مشاهده شده است که برای تصمیم‌گیری درباره انشعاب‌ها مورد استفاده قرار گرفته‌اند. بنابراین، آرگومان‌هایa  و n  یش از حد زنده و فعال می‌باشند و برای تزریق آسیب‌پذیری مناسب نیستند. در مقابل، آرگومان b دارای TCN برابر با صفر است و بایت‌های ورودی که این مقدار از آن‌ها مشتق شده است، یعنی بایت‌های ۴ تا ۷، کاملاً مرده هستند. بنابراین، این آرگومان یک محرک ایده‌آل برای کنترل یک آسیب‌پذیری محسوب می‌شود.

اینکه دقیقاً کدام DUAها، بر اساس مقادیر فعال‌بودن و TCN، باید انتخاب شوند، یک آستانه قابل پیکربندی و تنظیم (Configurable Threshold) در LAVA است. ما تأثیر آستانه‌های مختلف را (از نظر اینکه آیا باگ می‌تواند با موفقیت تزریق و اعتبارسنجی شود یا خیر) در بخش VI-A بررسی خواهیم کرد.

۳.۲ نقطه حمله (The Attack Point)

انتخاب نقطه حمله، تابعی از نوع آسیب‌پذیری است که قرار است تزریق شود. تنها شرط لازم این است که بتوان در نقطه حمله، با استفاده از داده‌های مرده (Dead Data)، یک باگ ایجاد کرد. این داده‌ها می‌توانند از طریق ایجاد جریان داده جدید (New Dataflow)، در بخش‌های بعدی دنباله اجرای برنامه در دسترس قرار گیرند. بدیهی است که این موضوع به این معناست که نقطه حمله باید از نظر زمانی پس از یک رخداد DUA در دنباله اجرا (Trace) قرار داشته باشد.

چنانچه هدف، تزریق یک سرریز خواندن (Read Overflow) باشد، عملیات‌هایی مانند خواندن از طریق رفع ارجاع اشاره‌گر (Pointer Dereference)، اندیس آرایه (Array Index) و کپی حجیم حافظه (Bulk Memory Copy)، به‌ عنوان مثال، نقاط حمله مناسبی محسوب می‌شوند. اگر هدف، تزریق آسیب‌پذیری تقسیم بر صفر (Divide-by-Zero) باشد، عملیات‌های محاسباتی شامل عملگر تقسیم مورد هدف قرار می‌گیرند. همچنین ممکن است هدف، کنترل یک یا چند آرگومان یک تابع کتابخانه‌ای (Library Function Arguments) باشد. به عنوان نمونه، در مثال مورد بررسی، در خط ۷، فراخوانی تابع memcpy می‌تواند به ‌عنوان یک نقطه حمله مورد استفاده قرار گیرد؛ زیرا در دنباله اجرا مشاهده شده است که این فراخوانی پس از یک DUA قابل استفاده، یعنی آرگومان b، قرار دارد. با اضافه کردنb  می‌توان هر یک از آرگومان‌های این تابع را کنترل کرد و در نتیجه، احتمال ایجاد و فعال‌سازی یک سرریز بافر (Buffer Overflow) وجود خواهد داشت.

   ۳.۳ تزریق باگ مبتنی بر جریان داده (Data-flow Bug Injection)

سومین و آخرین مرحله در تزریق باگ LAVA، ایجاد یک رابطه جریان داده (Dataflow Relationship) بین DUA و نقطه حمله (Attack Point) است. چنانچه DUA در محدوده دسترسی (Scope) نقطه حمله قرار داشته باشد، می‌توان به‌سادگی از آن در نقطه حمله استفاده کرد تا آسیب‌پذیری ایجاد شود. اگر DUA در محدوده دسترسی نباشد، کد جدیدی به برنامه اضافه می‌شود تا مقدار DUA را به یک محل امن منتقل کند (برای مثال، در یک ساختار داده ایستا یا سراسری (Static or Global Data Structure)) و سپس در ادامه اجرای برنامه آن را بازیابی کرده و در نقطه حمله مورد استفاده قرار دهد.

با این حال، برای اطمینان از اینکه باگ تنها در موارد بسیار نادر فعال می‌شود (که یکی از الزامات مطرح‌شده در بخش I مقاله است)، یک شرط نگهبان یا محافظ (Guard) اضافه می‌کنیم که تنها در صورتی اجازه استفاده از DUA برای فعال‌سازی آسیب‌پذیری را می‌دهد که مقدار آن با یک مقدار مشخص تطابق داشته باشد. در مثال مورد بررسی، DUA با نام b همچنان در نقطه حمله memcpy در محدوده دسترسی قرار دارد و تنها تغییر لازم در کد منبع این است که از آن برای ایجاد آسیب‌پذیری، در صورت تطابق با یک مقدار خاص، استفاده شود. اگر اولین آرگومان فراخوانی تابع memcpy، یعنی d، را با عبارت d + (b == 0x6c617661) * b جایگزین کنیم، آنگاه یک نوشتن خارج از محدوده (Out-of-Bounds Write) تنها زمانی رخ خواهد داد که بایت‌های ۴ تا ۷ ورودی دقیقاً با مقدار 0x6c617661 مطابقت داشته باشند.

اگرچه این سازوکار برای اطمینان از اینکه هر باگ تنها توسط یک ورودی مشخص فعال می‌شود، تاکنون برای ما عملکرد خوبی داشته است، اما روش‌های دیگری نیز وجود دارند که می‌توان با استفاده از آن‌ها به همین هدف دست یافت. برای مثال، می‌توان به‌ جای این روش، فراخوانی کد دارای باگ را با یک دستور شرطی if محافظت کرد، یا مقایسه با بایت‌های ورودی تشکیل‌دهنده DUA را به‌صورت جداگانه و بایت ‌به ‌بایت انجام داد. اگرچه این روش‌ها از نظر عملکردی معادل هستند، اما سازوکار دقیق مورد استفاده ممکن است باعث شود برخی ابزارها بتوانند با سهولت بیشتری باگ را پیدا کنند. برای نمونه، مقایسه بایت‌های ورودی به ‌صورت جداگانه، این امکان را برای فازرهای بیشینه‌کننده پوشش (Coverage-Maximizing Fuzzers) فراهم می‌کند که با حدس زدن تدریجی یک بایت در هر مرحله، به‌صورت افزایشی باگ را کشف کنند؛ به‌ جای آن که مجبور باشند کل مقدار محرک ۳۲ بیتی را به‌طور هم‌زمان حدس بزنند. برای یک ارزیابی جامع در مقیاس بزرگ از ابزارها، بهتر است باگ‌ها با مجموعه‌ای متنوع از سازوکارهای مختلف فعال‌سازی (Trigger Mechanisms) تزریق شوند. با این حال، در نمونه اولیه فعلی ما، تنها از همان روشی استفاده می‌کنیم که در این بخش توضیح داده شد.

۴. مسیرهایی که انتخاب نشدند (Roads Not Taken)

با توجه به هدف افزودن باگ به برنامه‌های دنیای واقعی به‌صورت خودکار، تعداد زیادی طراحی سیستم (System Design) و رویکرد (Approach) مختلف وجود دارد. به‌ منظور شفاف‌سازی طراحی LAVA، در این بخش به‌طور مختصر، روش‌ها و گزینه‌های جایگزین را بررسی خواهیم کرد.

ابتدا می‌توان این دیدگاه را مطرح کرد که مجموعه‌ای از تبدیل‌های ساده و محلی (لوکال) برنامه (Straightforward, Local Program Transformations) ایجاد کنیم که امنیت برنامه را کاهش می‌دهند. به عنوان مثال، می‌توان تمام نمونه‌های استفاده از توابع strlcpy و strncpy را پیدا کرده و آن‌ها را با تابع strcpy که دارای امنیت کمتری است، جایگزین کرد، یا فراخوانی‌های تابعmalloc را شناسایی کرده و تعداد بایت‌های تخصیص‌یافته را کاهش داد. این رویکرد از آن جهت جذاب است که پیاده‌سازی آن بسیار ساده می‌باشد (برای مثال، می‌توان آن را به‌صورت یک مرحله تبدیل LLVM (LLVM Transformation Pass)  پیاده‌سازی کرد)، اما منبع قابل اعتمادی برای تولید باگ محسوب نمی‌شود. هیچ روش ساده‌ای برای تشخیص اینکه چه ورودی (در صورت وجود) باعث رسیدن اجرای برنامه به کد جدیدِ دارای باگ می‌شود، وجود ندارد. از سوی دیگر، بسیاری از این تبدیل‌ها (جایگزینی‌ها) ممکن است صحت عملکرد برنامه (Program Correctness) را به حدی مختل کنند که برنامه با هر ورودی دچار کرش (Crash) شود. در آزمایش‌های اولیه ما، جایگزینی نمونه‌های strncpy با strcpy در برنامه bash تنها موجب کرش کردن فوری برنامه شد. همچنین، کلاس‌های باگ‌هایی که توسط این رویکرد تولید می‌شوند، ذاتاً محدود هستند و نمایانگر باگ‌های موجود در برنامه‌های مدرن امروزی نیستند.

یک رویکرد پیشرفته‌تر توسط Keromytis [6] پیشنهاد شده است: می‌توان از اجرای نمادین هدفمند (Targeted Symbolic Execution) برای یافتن مسیرهای اجرایی در برنامه استفاده کرد که بالقوه خطرناک هستند، اما در وضعیت فعلی ایمن باقی مانده‌اند. سپس می‌توان محدودیت‌های مسیر نمادین (Symbolic Path Constraints) را تحلیل کرده و از آن‌ها برای حذف بررسی‌های ورودی (Input Checks) استفاده نمود؛ بررسی‌هایی که در حال حاضر مانع ایجاد یک باگ می‌شوند. این رویکرد از نظر شهودی امیدوارکننده است؛ زیرا شامل حداقل تغییرات در برنامه می‌شود و باگ‌های ایجاد شده نیز از این نظر واقع‌گرایانه هستند که می‌توان تصور کرد چنین باگ‌هایی در اثر فراموشی یک برنامه‌نویس برای اعمال صحیح کنترل‌ها و محافظت‌های لازم روی بخشی از کد ایجاد شده باشند. با این حال، ایجاد هر باگ با این روش هزینه محاسباتی بالایی خواهد داشت (به دلیل نیاز به اجرای نمادین (Symbolic Execution) و حل محدودیت‌ها و قیود (Constraint Solving)) و در نتیجه، توانایی آن برای رسیدن به بخش‌های عمیق برنامه محدود خواهد بود. این محدودیت باعث می‌شود تعداد باگ‌هایی که می‌توان به یک برنامه اضافه کرد، کاهش یابد.

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

پیاده‌سازیLAVA  برای تزریق (inject) و اعتبارسنجی (validate) آسیب‌پذیری‌های سرریز بافر (Buffer Overflow) در کد منبع زبان C تحت لینوکس، در چهار مرحله انجام می‌شود:

شکل ۵. معماری پیاده‌سازی LAVA. ابزارهای PANDA و Clang برای انجام تحلیل پویای آلودگی (Dynamic Taint Analysis) به کار گرفته می‌شوند تا نقاط بالقوه تزریق آسیب‌پذیری را به‌صورت جفت‌های DUA–Attack Point شناسایی کنند. سپس، هر یک از این جفت‌ها با اعمال تغییر متناظر در کد منبع توسط Clang اعتبارسنجی می‌شود. در نهایت، هر فایل اجرایی که احتمال وجود آسیب‌پذیری در آن وجود دارد، با اعمال یک تغییر هدفمند در ورودی مورد آزمایش قرار می‌گیرد تا مشخص شود آیا این تغییر واقعاً منجر به بروز یک سرریز بافر (Buffer Overflow) می‌شود یا خیر.
  1. کامپایل نسخه‌ای از برنامه هدف که با کوئری‌های رهگیری آلودگی (Taint Queries) ابزارگذاری (Instrument) شده است.
  2. اجرای نسخه ابزارگذاری ‌شده (instrumented) با مجموعه‌ای از ورودی‌های مختلف، به‌منظور رهگیری آلودگی داده‌ها (Taint Tracking) و جمع‌آوری نتایج پرس‌وجوهای آلودگی و اطلاعات مربوط به نقاط حمله (Attack Points).
  3. تحلیل و استخراج نتایج حاصل از رهگیری آلودگی برای شناسایی DUAها و نقاط حمله، و تهیه فهرستی از آسیب‌پذیری‌های بالقوه قابل تزریق.
  4. کامپایل مجدد برنامه هدف با اعمال تغییرات لازم در کد منبع برای تزریق هر آسیب‌پذیری، و سپس آزمون برنامه به‌منظور بررسی موفقیت‌آمیز بودن فرآیند تزریق آسیب‌پذیری.

این چهار مرحله در شکل ۵ نیز به‌صورت شماتیک نمایش داده شده‌اند.

   ۵.۱ کوئری‌های آلودگی (Taint queries)

کوئری‌های آلودگی درLAVA بر پایه پلتفرم تحلیل پویای PANDA [5] پیاده‌سازی شده‌اند که خود بر مبنای شبیه‌ساز کل سامانه QEMU توسعه یافته است. PANDA سه قابلیت مهم را به QEMU اضافه می‌کند:

  1. ثبت و بازپخش قطعی (Deterministic Record and Replay) را فراهم می‌کند که می‌توان از آن برای اجرای مکرر تحلیل‌های پرهزینه (مانند تحلیل آلودگی (Taint Analysis)) استفاده کرد؛ تحلیل‌هایی که در بسیاری از موارد امکان اجرای برخط (Online) آن‌ها وجود ندارد.
  2. از یک معماری افزونه‌ای (Plugin Architecture) ساده اما قدرتمند بهره می‌برد که امکان توسعه تحلیل‌های پیشرفته و حتی ایجاد تحلیل‌های جدید بر پایه تحلیل‌های موجود را فراهم می‌کند.
  3. قابلیت موجود در S2E [4] را برای تبدیل زبان میانی QEMU (Intermediate Language) به LLVM و انجام تحلیل روی آن، در خود ادغام کرده است.

مهم‌ترین قابلیتی از PANDA که در LAVA مورد استفاده قرار می‌گیرد، یک افزونه سریع و پایدار برای تحلیل پویای آلودگی(Dynamic Taint Analysis Plugin) است که بر روی نسخه LLVM  هر بلوک پایه (Basic Block) از کد شبیه‌سازی‌شده عمل می‌کند. این نسخه مبتنی بر LLVM شامل پیاده‌سازی‌های شبیه‌سازی‌شده تمامی دستورالعمل‌های x86 است که QEMU از آن‌ها پشتیبانی می‌کند. QEMU بسیاری از دستورالعمل‌های پیچیده پردازنده (مانند، دستورالعمل‌های MMX و XMM در معماری x86) را به زبان C پیاده‌سازی می‌کند. این کدها با استفاده از Clang  به LLVM Bitcode کامپایل می‌شوند و در نتیجه، برای انجام تحلیل آلودگی توسط PANDA نیز در دسترس قرار می‌گیرند. این فرایند تضمین می‌کند که تحلیل آلودگی PANDA از این نظر کامل است که می‌تواند جریان داده (Data Flow) را در تمام دستورالعمل‌های برنامه رهگیری کند.

در LAVA از یک افزونه ساده PANDA به نام file_taint استفاده می‌شود که قادر است برچسب‌های آلودگی (Taint Labels) را به بایت‌هایی که در سیستم‌عامل لینوکس (Linux) از فایل‌ها خوانده می‌شوند، اختصاص دهد. این افزونه به نوبه خود از افزونه‌های واکاوی سیستم‌عامل (Operating System Introspection) و افزونه‌های فراخوانی سیستمی  (System Call Plugins) در PANDA  بهره می‌گیرد تا آفست آغازین فایل (Starting File Offset) مربوط به عملیات خواندن و همچنین تعداد واقعی بایت‌های خوانده ‌شده (Number of Bytes Actually Read) را تعیین کند.

این قابلیت به LAVA امکان می‌دهد از اطلاعات آلودگی استفاده کند که کمیت‌های داخلی برنامه (Internal Program Quantities) را به آفست‌های متناظر آن‌ها در فایل (File Offsets) نگاشت می‌کند. پیش از اجرای برنامه هدف تحت PANDA، LAVA ابتدا یک ابزار سفارشی Clang را فراخوانی می‌کند تا کوئری‌های آلودگی (Taint Queries) را در کد منبع، پیش و پس از فراخوانی توابع، درج کند.

هر آرگومان تابع (Function Argument) به مؤلفه‌های lvalue تشکیل‌دهنده خود تجزیه می‌شود و سپس Clang برای هر یک از آن‌ها یک کوئری‌های آلودگی را به‌ صورت یک Hypervisor Call / Hypercall  اضافه می‌کند. این فراخوانی، PANDA را مطلع می‌سازد تا از سامانه آلودگی، وضعیت آلودگی یک متغیر مشخص در سطح کد منبع (Source-Level Variable) را استعلام کند.

برای مقدار بازگشتی تابع (Function Return Value) نیز یک Hypercall  مربوط به کوئری‌های آلودگی درج می‌شود. همچنین،LAVA  ازClang  برای درج فراخوانی‌های Hypervisor در سطح کد منبع (Source Hypervisor Calls) در نقاط بالقوه حمله استفاده می‌کند.

لازم به ذکر است که نقاط کوئری‌هایی که LAVA از آن‌ها استفاده می‌کند، به هیچ وجه جامع و کامل نیستند. برای مثال، انتظار می‌رود انجام کوئری‌های آلودگی در محل‌های رفع ارجاع اشاره‌گر (Pointer Dereferences) بتواند تعداد بیشتری DUA را شناسایی کند.

   ۵.۲ اجرای برنامه (Running the Program)

پس از آنکه برنامه هدف با کوئری‌های آلودگی (Taint Queries) ابزارگذاری (Instrumented) شد، آن را با مجموعه‌ای متنوع از ورودی‌ها اجرا می‌کنیم. از آنجا که رویکرد ما برای جمع‌آوری اطلاعات درباره برنامه، ماهیتی کاملاً پویا (Dynamic) دارد، لازم است ورودی‌ها به‌گونه‌ای انتخاب شوند که پوشش کد (Code Coverage) را تا حد امکان افزایش دهند.

برای اجرای برنامه، آن را به‌ صورت یک دیسک نوری مجازی (Virtual CD) در یک ماشین مجازی PANDA بارگذاری می‌کنیم و سپس از طریق یک درگاه سریال مجازی (Virtual Serial Port)، فرمان‌های لازم را بهQEMU  ارسال می‌کنیم تا برنامه را با ورودی موردنظر اجرا کند.

هم‌زمان با اجرای فراخوانی‌های Hypervisor (Hypervisor Calls)  که در برنامه درج شده‌اند، PANDA نتایج کوئری‌های آلودگی و همچنین رخدادهای مربوط به نقاط حمله (Attack Points) را در یک فایل گزارش باینری (Binary Log File) با نام pandalog ثبت می‌کند.

علاوه بر این، اطلاعات مربوط به انتقال‌های جریان کنترل (Control Flow Transfers) که به داده‌های آلوده (Tainted Data) وابسته هستند نیز در pandalog ذخیره می‌شوند تا بعداً بتوان از آن‌ها برای محاسبه فعال‌ و زنده بودن (Liveness) هر بایت ورودی استفاده کرد.

شایان ذکر است که pandalog توسط Hypercallهایی تولید می‌شود که در کد منبع برنامه درج شده‌اند؛ ازاین‌رو، این فایل قادر است اطلاعات سطح کد منبع، مانند نام متغیرها (Variable Names) و محل آن‌ها در فایل کد منبع (Source File Locations)، را به کوئری‌های آلودگی و نقاط حمله مرتبط سازد. این ویژگی باعث می‌شود که در مراحل بعدی، فرایند تزریق باگ بتواند از اطلاعات سطح کد منبع (Source-Level Information) استفاده کند.

   ۵.۳ استخراج Pandalog (Mining the Pandalog)

در مرحله بعد، pandalog را به‌ترتیب زمانی تحلیل می‌کنیم و با تطبیق  DUAها با نقاط حمله، باگ‌های بالقوه قابل تزریق را شناسایی می‌کنیم. برنامه‌ای که این وظیفه را انجام می‌دهد FIB نام دارد که مخفف «Find Injectable Bugs» است و جزئیات آن در شکل ۶ ارائه شده است. FIB ورودی‌های موجود در pandalog را به‌ترتیب زمانی بررسی می‌کند.

ورودی‌های مربوط به پرس‌وجوهای آلودگی (Taint Queries) توسط تابع collect_duas پردازش می‌شوند. این تابع مجموعه‌ای از DUAهای قابل استفاده (Viable DUAs) را نگهداری می‌کند. برای آنکه یک DUA در این مجموعه قرار گیرد، بایستی (۱) تعداد کافی بایت آلوده (Tainted Bytes) را شامل شود، (۲) این بایت‌ها از نظر تعداد برچسب‌های آلودگی (Taint Set Cardinality) و همچنین عدد محاسبات آلودگی (TCN)، کمتر از آستانه‌های تعیین‌شده باشند و (۳) علاوه بر این، مقدار فعال و زنده ‌بودن (Liveness) تمامی بایت‌های ورودی که باعث آلودگی DUA شده‌اند نیز باید کمتر از یک آستانه مشخص باشد.

لازم به ذکر است که هر DUA به یک نقطه مشخص در برنامه (Program Point) و یک نام متغیر (Variable Name) وابسته است و در مجموعه  DUAهای قابل استفاده، تنها آخرین DUA مشاهده ‌شده برای هر متغیر نگهداری می‌شود. به این ترتیب، اگر یک DUA مربوط به متغیری باشد که درون یک حلقه (Loop) قرار دارد یا در تابعی تعریف شده باشد که چندین بار فراخوانی می‌شود، مجموعه تنها یک ورودی (آخرین نمونه مشاهده‌شده) برای آن متغیر و آن محل در کد منبع نگه می‌دارد. این کار تضمین می‌کند که مقدار ذخیره‌شده به‌روز بوده و در صورت نیاز، در یک نقطه حمله قابل استفاده باشد.

اطلاعات مربوط به فعال‌ و زنده بودن (Liveness) بایت‌های ورودی فایل نیز هر زمان که یک ورودی ثبت ‌شده مربوط به دستور شاخه آلوده (Tainted Branch Instruction) در pandalog مشاهده شود، به‌روزرسانی می‌گردد. اطلاعات مربوط به شاخه‌های آلوده موجود در pandalog، در تابع update_liveness، مقدار فعال‌ و زنده بودن  (Liveness)تمامی بایت‌های ورودی دخیل در آن انشعاب را به‌روزرسانی می‌کند

در نهایت، هنگامی که FIB در فایل pandalog با یک نقطه حمله مواجه می‌شود، تابع collect_bugs تمامی DUAهای موجود در مجموعه را بررسی می‌کند. DUAهایی که همچنان از نظر زنده بودن (Liveness) شرایط لازم را داشته باشند، با آن نقطه حمله جفت شده و به‌ عنوان باگ‌های بالقوه قابل تزریق (Potentially Injectable Bugs) در نظر گرفته می‌شوند.

در پیاده‌سازی فعلی LAVA، یک نقطه حمله به‌صورت یکی از آرگومان‌های یک فراخوانی تابع تعریف می‌شود که بتوان با افزودن یک DUA به آن، آن را مستعد ایجاد آسیب‌پذیری کرد. به این معنا که آرگومان مورد نظر می‌تواند یک اشاره‌گر (Pointer) یا یکی از انواع داده صحیح (Integer Types) باشد. ایده اصلی این است که تغییر قابل‌توجه مقدار این آرگومان، احتمالاً بتواند منجر به فعال شدن یک سرریز بافر (Buffer Overflow) شود.

شایان ذکر است که همانند کوئری‌های آلودگی  (Taint Queries)، سازوکار انتخاب نقاط حمله درLAVA نیز به‌هیچ‌وجه کامل و جامع نیست. برای مثال، می‌توان تصور کرد که عملیات خواندن و نوشتن از طریق اشاره‌گرها  (Pointer Reads and Writes)، یا استفاده از آن‌ها در عبارت‌های شرطی (Conditionals) و موارد مشابه نیز به ‌عنوان نقاط حمله مورد هدف قرار گیرند.

شکل ۶. شبه‌کد FIB به سبک زبان Python. Pandalog به‌ترتیب زمانی پردازش می‌شود و نتایج پرس‌وجوهای آلودگی (Taint Queries) مربوط به مقادیر و انشعاب‌ها (Branches) برای به‌روزرسانی مجموعه فعلی DUAها و میزان زنده‌بودن (Liveness) بایت‌های ورودی مورد استفاده قرار می‌گیرند. هرگاه یک نقطه حمله (Attack Point) مشاهده شود، تمام DUAهای موجود که همچنان معتبر هستند، به‌عنوان منابع داده بالقوه برای تزریق آسیب‌پذیری در نظر گرفته می‌شوند.

				
					def check_liveness(file_bytes):
	for file_byte in file_bytes:
		if (liveness[file_byte]
			> max_liveness):
		return False
	return True

def collect_duas(taint_query):
	retained_bytes = []
	for tainted_byte in taint_query:
		if tainted_byte.tcn <= max_tcn
		and
	len(tainted_byte.file_offsets) <= max_card
	and
	check_liveness(tainted_byte.file_offsets)):
		retained_bytes += tainted_byte.file_offsets
	duakey = (taint_query.source_loc,
		taint_query.ast_name)
	duas[duakey] = retained_bytes
def update_liveness(tainted_branch):
	for tainted_file_offset in tainted_branch:
		liveness[tainted_file_offset]++
def collect_bugs(attack_point):
	for dua in duas:
		viable_count = 0
		for file_offset in dua:
			if (check_liveness(file_offset)):
				viable_count ++
			if (viable_count >= bytes_needed):
				bugs.add((dua, attack_point))

for event in Pandalog:
	if event.typ is taint_query:
		collect_duas(event);
	if event.typ is tainted_branch:
		update_liveness(event);
	if event.typ is attack_point:
		collect_bugs(event);
				
			

شکل ۷. کدی که توسط Clang به فایل src/encodings.c در برنامه file تزریق شده است تا مقدار DUA برای استفاده در مراحل بعدی ذخیره شود. تابع lava_set مقدار DUA را در یک متغیر ایستا (Static Variable) ذخیره می‌کند. تحلیل آلودگی PANDA و الگوریتم FIB تشخیص داده‌اند که چهار بایت نخست متغیر bufبرای استفاده در ایجاد آسیب‌پذیری مناسب هستند:

				
					protected int
file_encoding(struct magic_set *ms,
			..., const char **type) {
...
	else if
		(({int rv =
			looks_extended(buf, nbytes, *ubuf, ulen);
		if (buf) {
			int lava = 0;
			lava |= ((unsigned char *)(buf))[0]<<(0*8);
			lava |= ((unsigned char *)(buf))[1]<<(1*8);
			lava |= ((unsigned char *)(buf))[2]<<(2*8);
			lava |= ((unsigned char *)(buf))[3]<<(3*8);
			lava_set(lava);
		}; rv;})) {
...
				
			

شکل ۸. کدی که به فایل src/readcdf.c در برنامه file تزریق شده است تا از مقدار DUA برای ایجاد یک آسیب‌پذیری استفاده کند. تابع lava_get آخرین مقدار DUA را که پیش‌تر توسط تابع lava_set ذخیره شده است، بازیابی می‌کند:

				
					...
protected int
file_trycdf(struct magic_set *ms,
		..., size_t nbytes) {
	...
if (cdf_read_header
	(( (&info)) + (lava_get())
	* (0x6c617661 == (lava_get())
		|| 0x6176616c == (lava_get())), &h) == -1)
	return 0;
				
			

   ۵.۴ تزریق و آزمون باگ‌ها (Inject and Test Bugs)

برای هر جفت  DUA/ نقطه حمله (DUA/Attack Point Pair)، با استفاده از یک ابزار سفارشی دیگر مبتنی بر Clang، کد C موردنیاز برای استفاده از DUA به ‌منظور فعال‌سازی باگ تولید می‌شود. در خط کد منبع (Source Line) و برای متغیر مربوط به DUA، کدی تزریق می‌شود که مقدار آن متغیر را در یک متغیر ایستای (Static Variable) نگهداری‌ شده توسط یک تابع کمکی (Helper Function) کپی می‌کند. سپس، در نقطه حمله  (Attack Point)، کدی درج می‌شود که مقدار DUA را بازیابی می‌کند، بررسی می‌کند که آیا این مقدار با یک مقدار جادویی (Magic Value) مطابقت دارد یا خیر، و در صورت تطابق، آن را به یکی از آرگومان‌های تابع اضافه می‌کند.

آخرین مرحله در LAVA، کامپایل و آزمون برنامه اصلاح ‌شده با استفاده از یک فایل ورودی اثبات مفهوم (Proof-of-Concept Input File) است. در این فایل، بایت‌های ورودی که بر اساس تحلیل آلودگی (Taint Analysis) موجب آلودگی DUA شده‌اند، به مقدار صحیح موردنیاز تنظیم می‌شوند. نمونه‌ای از دو قطعه کد تزریق ‌شده در کد منبع به‌ همراه تغییرات اعمال ‌شده روی فایل ورودی که برای تزریق یک باگ در برنامه file لازم است، در شکل‌های ۷ و ۸ نشان داده شده است. ورودی اولیه برنامه file، فایل اجرایی /bin/ls می‌باشد و تنها تغییر مورد نیاز برای فعال‌سازی باگ، این است که چهار بایت نخست این فایل به رشته “lava” تغییر داده شوند. نکته قابل توجه این است که تحلیل آلودگی و ابزار FIB ممکن است DUA را در یک واحد تلفیق (Compilation Unit) شناسایی کنند، در حالی که نقطه حمله در واحد تلفیق دیگری قرار داشته باشد.

۶. نتایج (Results)

ما LAVA را از سه جنبه مورد ارزیابی قرار دادیم. نخست، تعداد زیادی باگ را در چهار برنامه متن‌باز شامل file ،readelf (از مجموعه binutils)،bash و tshark (نسخه خط فرمان ابزارWireshark برای ضبط و تحلیل ترافیک شبکه) تزریق کردیم. برای هر یک از این برنامه‌ها، آمارهای مختلفی را هم در مورد ویژگی‌های برنامه هدف و هم درباره میزان موفقیت LAVA در تزریق باگ‌ها ارائه می‌کنیم.

دوم، توزیع (Distribution) و واقع‌گرایی (Realism) باگ‌های تولید شده توسط LAVA را با تعریف و محاسبه مجموعه‌ای از معیارها مورد ارزیابی قرار دادیم. در نهایت، یک بررسی مقدماتی انجام دادیم تا مشخص شود ابزارهای موجودِ کشف باگ (Bug-Finding Tools) تا چه اندازه در شناسایی باگ‌های تزریق ‌شده توسط LAVA مؤثر هستند. برای این منظور، نرخ کشف (Detection Rate) این باگ‌ها را با استفاده از یک فازر متن‌باز (Open-Source Fuzzer) و یک ابزار کشف باگ مبتنی بر اجرای نمادین (Symbolic Execution-Based Bug Finder) اندازه‌گیری کردیم.

شمارش باگ‌ها (Bug Counting):

پیش از آنکه به بررسی نتایج بپردازیم، لازم است مشخص کنیم منظور ما از «باگ تزریق‌ شده» (injected bug) چیست و چه چیزی باعث می‌شود دو باگ تزریق‌ شده از یکدیگر متمایز تلقی شوند. اگرچه روش‌های متعددی برای تعریف «باگ» وجود دارد، ما تعریفی را انتخاب می‌کنیم که بیشترین تطابق را با کاربرد هدف (Target Use Case) ما داشته باشد؛ یعنی دو باگ زمانی متفاوت در نظر گرفته می‌شوند که یک ابزار خودکار برای شناسایی یا تحلیل آن‌ها ناچار باشد استدلال متفاوتی انجام دهد. بر این اساس، در این پژوهش، یک باگ را به ‌صورت یک جفت یکتا شامل (DUA، نقطه حمله (Attack Point)) تعریف می‌کنیم.

به‌طور دقیق‌تر، یکتایی این جفت به این معناست که مؤلفه‌های فایل کد منبع، شماره خط و نام متغیر مربوط به DUA و فایل کد منبع و شماره خط نقطه حمله (Attack Point) می‌بایست منحصربه‌فرد باشند.

ممکن است برخی استدلال کنند که این تعریف، تعداد باگ‌های تزریق‌ شده به برنامه را به ‌صورت مصنوعی افزایش می‌دهد. برای مثال، بر اساس این تعریف، اگر تنها محل در برنامه که داده ورودی فایل در آن در دسترس قرار می‌گیرد متفاوت باشد، اما در هر دو حالت از همان بایت‌های ورودی فایل استفاده شود، این دو مورد به‌عنوان دو باگ مجزا شمرده می‌شوند.

با این حال، از دیدگاه ما این دو واقعاً باید دو باگ متفاوت محسوب شوند؛ زیرا جریان داده (Data Flow) و جریان کنترل (Control Flow) منتهی به نقطه وقوع DUA در هر یک از این دو حالت، تفاوت قابل‌توجهی خواهد داشت و در نتیجه، ابزارهای کشف آسیب‌پذیری (Vulnerability Discovery Tools) نیز برای تحلیل و شناسایی این دو مورد، ناگزیر به استدلال‌های متفاوتی خواهند بود.

   ۶.۱ آزمایش‌های تزریق (Injection Experiments)

نتایج تزریق باگ در برنامه‌های متن‌باز در جدول ۱ خلاصه شده است. در این جدول، برنامه‌ها بر اساس اندازه آن‌ها، یعنی تعداد خطوط کد زبان C، که با استفاده از ابزار sloccount توسعه‌یافته توسط David Wheeler اندازه‌گیری شده است، مرتب شده‌اند. برای هر برنامه، تنها یک ورودی به‌منظور انجام تحلیل آلودگی (Taint Analysis) و شناسایی باگ‌های قابل تزریق (Injectable Bugs) مورد استفاده قرار گرفت. ورودی برنامه‌های file و readelf، فایل اجرایی ls بود. ورودی برنامه tshark یک فایل ثبت بسته‌های شبکه (Packet Capture) با حجم ۱۶ کیلوبایت بود که از یکی از وب‌سایت‌های ارائه‌دهنده نمونه‌های Packet Capture تهیه شده بود. ورودی برنامه bash نیز یک اسکریپت پوسته (Shell Script) شامل ۱۲۴ خط بود که توسط نویسندگان مقاله نوشته شده بود.

مقادیر N(DUA) و N(ATP) به ‌ترتیب بیانگر تعداد DUAها و نقاط حمله (Attack Points) هستند که توسط تحلیل FIB شناسایی شده‌اند. توجه داشته باشید که برای آنکه یک DUA یا نقطه حمله در این آمار شمارش شود، باید مطابق آنچه در بخش ۵.۳ توضیح داده شد، حداقل برای یک باگ به‌ عنوان یک گزینه قابل استفاده (Viable) تشخیص داده شده باشد. دو ستون Potential Bugs و Validated Bugs در جدول ۱ به‌ترتیب نشان‌دهنده موارد زیر هستند:

  • تعداد باگ‌های بالقوه (Potential Bugs) که توسط FIB شناسایی شده‌اند؛
  • تعداد باگ‌هایی که با اجرای برنامه اصلاح‌شده روی ورودی تغییریافته (Modified Input) اعتبارسنجی شده‌اند و واقعاً کد خروج (Exit Code) متناظر با یک سرریز بافر (Buffer Overflow) را بازگردانده‌اند؛ یعنی -11 برای خطای Segmentation Fault (Segfault) یا -6 برای خرابی هیپ (Heap Corruption).

ستون ماقبل آخر جدول با عنوان Yield، نسبت باگ‌های بالقوه‌ای را نشان می‌دهد که پس از آزمون، مشخص شده است واقعاً منجر به سرریز بافر می‌شوند. آخرین ستون جدول نیز زمان موردنیاز برای آزمون هر تزریق بالقوه باگ در برنامه هدف را نشان می‌دهد.

آزمون جامع (Exhaustive Testing) به چند دلیل امکان‌پذیر نبود. هرچه برنامه هدف بزرگ‌تر باشد، تعداد باگ‌های بالقوه (Potential Bugs) نیز بیشتر خواهد بود و آزمون هر یک از آن‌ها به زمان بیشتری نیاز دارد. برای مثال، برنامهtshark  بیش از یک میلیون باگ بالقوه دارد و آزمون هر یک از آن‌ها تقریباً ۱۰ دقیقه زمان نیاز دارد. دلیل این امر آن است که فرایند آزمون تنها به تزریق مقدار اندکی کد برای ایجاد باگ محدود نمی‌شود، بلکه کامپایل مجدد و سپس اجرای برنامه حاصل را نیز شامل می‌شود. علاوه بر این، در بسیاری از برنامه‌های هدف مشاهده کردیم که فرایند ساخت (Build) به‌صورت ظریفی دچار مشکل بود؛ به‌گونه‌ای که برای اعمال صحیح و قابل‌اعتماد تزریق باگ، لازم بود پیش از هر بار کامپایل، دستور make clean  اجرا شود. این موضوع نیز زمان موردنیاز برای آزمون را بیشتر افزایش می‌داد.

ازاین‌رو، به‌جای انجام آزمون جامع، برای هر برنامه هدف ۲۰۰۰ باگ بالقوه را که به‌صورت تصادفی یکنواخت (Uniformly at Random) انتخاب شده بودند، اعتبارسنجی کردیم. بنابراین، هنگامی که در جدول I گزارش می‌کنیم که نرخ موفقیت (Yield) برای tshark برابر با 17.7٪  است، منظور این است که ۳۰۶ مورد از ۲۰۰۰ باگ آزمون ‌شده، به عنوان باگ معتبر (Valid Bug) تأیید شده‌اند. از آنجا که باگ تزریق ‌شده به‌گونه‌ای طراحی شده است که تنها در صورت برابر بودن چهار بایت مشخص از ورودی با یک مقدار جادویی (Magic Value) فعال شود، برنامه را هم با ورودی اصلی و هم با ورودی تغییریافته‌ای که شامل مقدار محرک (Trigger) بود، مورد آزمون قرار دادیم. در هیچ‌یک از آزمایش‌ها مشاهده نشد که ورودی اصلی باعث بروز کرش در برنامه شود.

نرخ موفقیت (Yield) در برنامه‌های مختلف تفاوت قابل‌توجهی دارد و از کمتر از ۱۰٪ تا بیش از ۵۰٪ متغیر است. برای درک بهتر این موضوع، رابطه میان دو معیار مبتنی بر تحلیل آلودگی (Taint-Based Measures) و نرخ موفقیت را بررسی کردیم. برای هر DUA که در تزریق یک باگ مورد استفاده قرار گرفت، دو مقدار mTCN، بیشترین مقدار TCN  در میان تمامی بایت‌های تشکیل‌دهنده آن DUA و mLIV، بیشترین مقدار فعال و زنده ‌بودن (Liveness) در میان تمام برچسب‌های آلودگی (Taint Labels)  موجود در هر یک از مجموعه‌های برچسب آلودگی (Taint Label Sets) مرتبط با بایت‌های آن DUA را محاسبه کردیم. به بیان ساده‌تر، mTCN نشان می‌دهد که DUA تا چه اندازه حاصل یک تابع محاسباتی پیچیده از بایت‌های ورودی است، در حالی که mLIV  معیاری از میزان تأثیر بایت‌های ورودیِ تعیین‌کننده DUA بر جریان کنترل (Control Flow) برنامه ارائه می‌کند.

جدول ۲، یک هیستوگرام دوبعدی (Two-Dimensional Histogram) را نشان می‌دهد که در آن، بازه‌های mTCN در امتداد محور عمودی و بازه‌های mLIV در امتداد محور افقی قرار گرفته‌اند. سلول بالا-چپ این جدول، تمامی موارد تزریق باگی را شامل می‌شود که در آن‌ها mTCN < 10 و mLIV < 10 بوده است؛ در مقابل، سلول پایین-راست تمامی مواردی را در بر می‌گیرد که mTCN ≥ 1000 و mLIV ≥ 1000 هستند. به خاطر داشته باشید که وقتی mTCN = mLIV = 0 باشد، DUA نه‌تنها یک رونوشت مستقیم (Direct Copy) از بایت‌های ورودی است، بلکه آن بایت‌های ورودی نیز تاکنون در تصمیم‌گیری هیچ‌یک از شاخه‌های برنامه (Program Branches) مورد استفاده قرار نگرفته‌اند. با افزایش هر یک از دو معیار mTCN یا mLIV، نرخ موفقیت (Yield) کاهش می‌یابد. با این حال، برای ما شگفت‌آور بود که حتی در مواردی که مقدار mLIV از 1000 نیز فراتر می‌رود، Yield همچنان در حدود 10٪ باقی می‌ماند.

جدول ۱: نتایج تزریق LAVA برای برنامه‌های متن‌باز با اندازه‌های مختلف

جدول ۲: میزان بازده به‌ عنوان تابعی از هر دو پارامتر mLIV و mTCN

شکل ۹: نمایش شماتیک یک ردپای کامل اجرای برنامه که در آن تعداد دستورالعمل‌های اجراشده در نقطه استخراج DUA برای استفاده بعدی (I(DUA))، نقطه حمله محل استفاده از آن (I(ATP)) و تعداد کل دستورالعمل‌های موجود در ردپا (I(TOT)) مشخص شده‌اند.

   ۶.۲ توزیع باگ‌ها (Bug Distribution)

به نظر می‌رسد LAVA قادر است تعداد بسیار زیادی باگ را در یک برنامه تزریق کند. اگر بر اساس مقادیر Yield ارائه‌شده در جدول ۱ استنتاج کنیم، تخمین می‌زنیم که در صورت آزمون تمامی باگ‌های بالقوه، نزدیک به ۴۰۰ هزار باگ واقعی شناسایی و تأیید شوند. اما پرسش مهم این است که این باگ‌ها تا چه اندازه در سطح برنامه توزیع شده‌اند؟

در برنامه‌هایی مانند file و bash، به‌ ترتیب ۱۱ و ۴۴ فایل کد منبع درگیر حداقل یک باگ بالقوه (Potential Bug) هستند. در این حالت، به نظر می‌رسد باگ‌ها توزیع نسبتاً مناسبی دارند؛ زیرا این اعداد به‌ترتیب ۵۸٪ و ۳۱٪ از کل فایل‌های کد منبع هر برنامه را شامل می‌شوند. در مقابل، وضعیت در readelf و tshark مطلوب نیست. در این دو برنامه، تنها ۲ و ۱۲۲ فایل کد منبع به‌ترتیب شامل حداقل یک باگ بالقوه هستند که معادل ۱۶٫۷٪ و ۹٫۶٪ از کل فایل‌های کد منبع آن‌ها است.

به نظر می‌رسد علت اصلی پایین بودن تعداد فایل‌هایی که در آن‌ها باگ ظاهر شده است، پوشش پویای ناکافی (Poor Dynamic Coverage) باشد. در مورد tshark، بخش قابل توجهی از کد برنامه به تجزیه و تحلیل پروتکل‌های شبکه کمتر متداول (Esoteric Network Protocols) اختصاص یافته است، در حالی که ما تنها از یک فایل ورودی برای اجرای برنامه استفاده کردیم.

به‌طور مشابه، برای bash نیز تنها از یک اسکریپت دست‌نویس استفاده شد و تلاش چندانی برای پوشش دادن بخش عمده‌ای از قابلیت‌های زبان (Language Features) صورت نگرفت. در نهایت، برنامه readelf تنها با یک گزینه خط فرمان (a-) اجرا شد؛ در نتیجه، قابلیت‌هایی مانند تجزیه نمادهای DWARF (DWARF Symbol Parsing) هرگز مورد استفاده قرار نگرفتند و در نتیجه، آن بخش از کد نیز پوشش داده نشد.

   ۶.۳ واقع‌گرایی باگ‌ها (Bug Realism)

هدف از باگ‌های تولیدشده توسط این سامانه، استفاده از آن‌ها به‌ عنوان مجموعه‌داده‌های مرجع معتبر (Ground Truth) برای توسعه و ارزیابی ابزارها و روش‌های کشف آسیب‌پذیری (Vulnerability Discovery) است. ازاین‌رو، ضروری است که این باگ‌ها، از جنبه‌ای، واقع‌گرایانه (Realistic) باشند. با این حال، ارزیابی میزان واقع‌گرایی باگ‌ها کار ساده‌ای نیست. از آنجا که این پژوهش، تا آنجا که نویسندگان اطلاع دارند، نخستین کاری است که مسئله تزریق کاملاً خودکار باگ (Fully Automated Bug Injection) را بررسی می‌کند، معیار استاندارد و پذیرفته‌شده‌ای برای سنجش واقع‌گرایی باگ‌ها در اختیار نداریم.

به همین دلیل، مجموعه‌ای از معیارهای اختصاصی را طراحی کردیم که بر ویژگی‌هایی مانند نحوه توزیع داده‌های ورودیِ دستکاری‌شده (Malformed Data Input) و نقاط محرک و فعال‌سازی (Trigger Points) در طول اجرای برنامه، و همچنین میزان حفظ رفتار اصلی برنامه (Preservation of the Original Program Behavior) تمرکز دارند. برای سنجش واقع‌گرایی، سه جنبه از باگ‌های تزریق‌شده را مورد بررسی قرار دادیم. دو جنبه نخست، موقعیت DUA و موقعیت نقطه حمله (Attack Point) در رهگیری اجرای برنامه (Program Execution Trace) هستند که در شکل ۹ نمایش داده شده‌اند.

به بیان دقیق‌تر، تعیین کردیم که در لحظه‌ای که DUA از مسیر اصلی جریان داده (Data Flow) جدا شده و ذخیره می‌شود (Siphoned Off) و نیز در لحظه‌ای که از آن برای حمله به برنامه، از طریق تخریب یک مقدار داخلی برنامه (Corrupting an Internal Program Value)، استفاده می‌شود، چه کسری از کل دستورالعمل‌های موجود در رهگیری اجرای برنامه تاکنون اجرا شده است.

شکل ۱۰: موقعیت نرمال‌شده DUA در رهگیری اجرا

هیستوگرام مربوط به این دو کمیت، یعنی I(DUA) و I(ATP)، در شکل‌های ۱۰ و ۱۱ ارائه شده است. مقادیر شمارش‌شده مربوط به تمامی باگ‌های بالقوه (Potential Bugs) موجود در پایگاه داده LAVA برای هر پنج برنامه متن‌باز هستند. همان‌طور که مشاهده می‌شود، DUAها و نقاط حمله (Attack Points) در تمام بخش‌های رهگیری اجرای برنامه (Execution Trace) قابل دسترس هستند؛ اگرچه به نظر می‌رسد تعداد آن‌ها در ابتدا و انتهای رهیگری اجرا بیشتر باشد.

این موضوع اهمیت زیادی دارد، زیرا باگ‌هایی که با استفاده از این DUAها ایجاد می‌شوند، تا نقطه I(DUA) دارای جریان کنترل (Control Flow) و جریان داده (Data Flow) کاملاً واقع‌گرایانه هستند. بنابراین، ابزارهای کشف آسیب‌پذیری (Vulnerability Discovery Tools) برای آنکه بتوانند این باگ‌ها را به‌درستی تشخیص دهند، باید رفتار و ساختار تمام بخش‌های برنامه را تا نقطه I(DUA) به‌درستی تحلیل و استدلال کنند.

سومین معیار ما مربوط به بخش موجود در رهگیری (Trace)  میان I(DUA)  و I(ATP)  است. این بخش از اهمیت ویژه‌ای برخوردار است، زیرا در پیاده‌سازی فعلی LAVA، جریان داده بینDUA  و نقطه حمله (Attack Point)  از طریق یک جفت فراخوانی تابع (Function Calls) ایجاد می‌شود.

بنابراین، ممکن است این استدلال مطرح شود که این بخش از رهگیری اجرا، از نظر جریان داده  (Data Flow)، واقع‌گرایانه نیست. کمیت I(DUA)/I(ATP) برای باگ‌های تزریق‌شده‌ای که این منبع غیرواقع‌گرایی را به حداقل می‌رسانند، به مقدار ۱ نزدیک خواهد بود. این حالت مشابه مثال ارائه ‌شده در شکل ۱ است؛ یعنی زمانی که DUA همچنان در محدوده دسترسی (Scope) قرار دارد و چند خط بعدتر، در همان تابع، می‌توان از آن برای تخریب یک اشاره‌گر (Corrupting a Pointer) استفاده کرد.

در این حالت، هیچ جریان داده غیرعادی (Abnormal Data Flow) مورد نیاز نیست. هیستوگرام شکل ۱۲ این اثر را برای تمامی باگ‌های بالقوه LAVA کمی‌سازی (quantifies) می‌کند. همان‌طور که مشخص است، بخش بزرگی از این باگ‌ها دارای مقدار I(DUA)/I(ATP) ≈ 1 می‌باشند و بر اساس این معیار، از واقع‌گرایی بالایی برخوردارند.

   ۶.۴ ارزیابی ابزارهای کشف آسیب‌پذیری (Vulnerability Discovery Tool Evaluation)

ما برای بررسی کاربرد باگ‌های تزریق ‌شده توسطLAVA  در فرایند ارزیابی ابزارها، دو ابزار کشف آسیب‌پذیری (Vulnerability Discovery Tools) را روی این باگ‌ها اجرا کردیم:

  1. فازر هدایت ‌شده بر اساس پوشش (Coverage-Guided Fuzzer) که در ادامه با نام FUZZER  به آن اشاره می‌شود.
  2. اجرای نمادین به همراه حل‌کننده SAT (Symbolic Execution + SAT Solving) که در ادامه با نام SES معرفی می‌شود.

این دو ابزار به ‌طور مشخص انتخاب شدند، زیرا فازینگ (Fuzzing) و اجرای نمادین (Symbolic Execution) دو تکنیک بسیار پرکاربرد و شناخته‌شده برای یافتن باگ‌های واقعی در برنامه‌های دنیای واقعی هستند. هر دو ابزار FUZZER و SES از ابزارهای پیشرفته (State-of-the-Art) و شناخته ‌شده در جامعه پژوهشی و مطرح در سطح روز حوزه خود محسوب می‌شوند.

برای هر یک از این ابزارها، تلاش قابل‌توجهی انجام دادیم تا اطمینان حاصل کنیم که استفاده از آن‌ها به شکل صحیح انجام شده است. این کار شامل مطالعه دقیق مستندات ابزار، پست‌های وبلاگی مرتبط و فهرست‌های ایمیلی (Email Lists)  بود. علاوه بر این، مجموعه‌ای از برنامه‌های کوچک نمونه که دارای باگ بودند ایجاد کردیم و از آن‌ها برای اطمینان از این موضوع استفاده کردیم که هر ابزار، حداقل قادر است باگ‌های شناخته ‌شده و ساده (Known Easy Bugs) را شناسایی کند.

شکل ۱۱: موقعیت نرمال‌شده ATP در رهگیری اجرا
شکل ۱۲: کسری از رهگیری اجرا که دارای جریان داده کاملاً عادی یا واقع‌گرایانه است، I(DUA)/I(ATP)

لازم به ذکر است که نام ابزارهای مورد ارزیابی در گزارش نتایج ارائه نشده است. ارزیابی دقیق، کاری بزرگ و مهم است و ما نمی‌خواهیم این فرایند، چه از نظر راه‌اندازی و استفاده صحیح از ابزارها و چه از نظر ارائه و تحلیل نتایج، به‌صورت سطحی یا ناقص انجام شود. هدف ما در اینجا این است که مشخص کنیم آیا باگ‌های LAVA می‌توانند برای ارزیابی سامانه‌های کشف باگ (Bug Finding Systems) مورد استفاده قرار گیرند یا خیر.

انتظار ما این است که در پژوهش‌های آینده، چه توسط خود ما و چه توسط سایر پژوهشگران، ارزیابی کامل و دقیق ابزارهای واقعی و نام‌دار با استفاده از LAVA انجام شود. اگرچه انجام چنین ارزیابی‌ای خارج از محدوده این مقاله است، اما امیدواریم نشان دهیم که این کار هم امکان‌پذیر و هم ارزشمند خواهد بود.

علاوه بر این، برنامه و امید ما این است که باگ‌های LAVA در تعداد زیاد و در فواصل زمانی به‌روزرسانی منظم در دسترس قرار گیرند تا امکان ارزیابی مستقل (Self-Evaluation) و همچنین بهبود تدریجی عملکرد ابزارها از طریق آزمون و بهینه‌سازی مداوم (Hill Climbing) فراهم شود.

اولین مجموعه داده‌ای (Corpus) که ایجاد کردیم، یعنی LAVA-1، از برنامه هدف file استفاده می‌کند؛ این برنامه کوچک‌ترین برنامه‌ در میان برنامه‌هایی است که با استفاده از LAVA در آن‌ها باگ تزریق کرده‌ایم.

این مجموعه شامل ۶۹ آسیب‌پذیری سرریز بافر (Buffer Overflow) است که با استفاده از LAVA در کد منبع تزریق شده‌اند. هر یک از این باگ‌ها در یک شاخه (Branch) مجزا در یک مخزن Git قرار گرفته‌اند و به همراه کد منبع، یک نسخه فازشده (Fuzzed Version) از ورودی نیز در مخزن ثبت شده است که بررسی شده تا فعال‌سازی کرش (Crash) توسط آن تأیید شود. دو نوع مختلف از سرریز بافر در این مجموعه تزریق شده است که هر دو نوع، برای فعال‌سازی (Trigger) و کنترل (Control )سرریز، از یک DUA چهار بایتی (4-byte DUA) استفاده می‌کنند.

۱) محرک و تنظیم‌کننده (Knob-and-trigger)

در این نوع از باگ، دو بایت از DUA  به‌ عنوان محرک یا فعال‌کننده (Trigger) استفاده می‌شوند. این دو بایت با یک مقدار جادویی (Magic Value) مقایسه میگردند تا مشخص شود آیا سرریز (Overflow) باید رخ دهد یا خیر. دو بایت دیگر از DUA که نقش تنظیم‌کننده (Knob) را دارند، میزان سرریزی که باید ایجاد شود را تعیین می‌کنند. بنابراین، این نوع باگ‌ها زمانی فعال می‌شوند که یک عدد صحیح بدون علامت ۲ بایتی (2-byte Unsigned Integer) در ورودی دارای یک مقدار مشخص باشد، اما تنها در صورتی رخ می‌دهند که دو بایت دیگر از ورودی به اندازه کافی بزرگ باشند تا باعث ایجاد شرایط مخرب شوند.

۲) محدوده (Range)

در این نوع از باگ‌ها، سرریز زمانی فعال می‌شود که مقدار جادویی (Magic Value) در یک محدوده مشخص (Range) قرار داشته باشد. علاوه بر این، از همان مقدار جادویی برای تعیین میزان سرریز نیز استفاده می‌شود. در این حالت، مقدار جادویی یک عدد صحیح بدون علامت ۴ بایتی (4-byte Unsigned Integer) است و محدوده قابل قبول آن می‌تواند متفاوت باشد.

این انواع باگ به‌گونه‌ای طراحی شده‌اند که الگوهای واقعی باگ‌ها (Real Bug Patterns) را شبیه‌سازی کنند. در باگ‌های Trigger-and-Knob (محرک و تنظیم‌کننده)، دو بخش متفاوت از ورودی به دو روش مختلف مورد استفاده قرار می‌گیرند تا وقوع باگ را تعیین کنند. در باگ‌هایRange  (محدوده‌ای)، به‌جای اینکه باگ تنها با یک مقدار مشخص از میان 232 مقدار ممکن فعال شود، اندازه محدوده قابل قبول (Haystack Size) تغییر می‌کند. توجه داشته باشید که یک محدوده (Range) با اندازه ۲۰ معادل همان باگی است که در شکل ۸ ارائه شده است.

جدول ۳: درصد باگ‌های کشف‌شده در مجموعه داده LAVA-1

نتایج این ارزیابی در جدول ۳ خلاصه شده است. برای آزمایش، پنج اندازه مختلف برای محدوده (Range) در نظر گرفته شد:

  • (20)  – یعنی ۱۲ باگ
  • (27)  – یعنی ۱۰ باگ
  • (214) – یعنی ۱۱ باگ
  • (221)  – یعنی ۱۴ باگ
  • (228) – یعنی ۱۲ باگ

همچنین از ۱۰ باگ از نوع Trigger-and-Knob استفاده کردیم. تمامی خروجی‌های تولید شده توسط هر دو ابزار را بررسی کردیم. ابزارFUZZER  برای هر باگ به مدت ۵ ساعت اجرا شد و توانست باگ‌هایی را در محدوده‌های بزرگ‌تر یعنی 214، 221 و 228 شناسایی کند. همچنین این ابزار توانست ۲۰٪ از باگ‌های نوع Trigger-and-Knob را کشف کند؛ که احتمالاً به این دلیل است که در این نوع باگ‌ها، Knob  و Trigger می‌توانند به‌صورت مستقل از یکدیگر توسط فازر مورد آزمایش و فاز (Fuzz) قرار گیرند. ابزار SES نیز برای هر باگ به مدت ۵ ساعت اجرا شد و توانست چندین باگ را در تمامی دسته‌ها، به‌جز محدوده‌های 27 و 228 کشف کند.

نتایج به‌دست‌آمده برای مجموعهLAVA-1  به‌ خوبی با نحوه عملکرد این ابزارها سازگار به نظر می‌رسد. ابزار FUZZER عمدتاً برنامه را به ‌عنوان یک جعبه سیاه (Black Box) در نظر می‌گیرد، بایت‌های منفرد ورودی را به ‌صورت تصادفی تغییر می‌دهد و فرایند کاوش را با استفاده از معیارهای پوشش (Coverage Measurements) هدایت می‌کند.

با توجه به اینکه این باگ‌ها تنها زمانی فعال می‌شوند که یک بخش چهار بایتی مشخص از ورودی دقیقاً برابر با یک مقدار جادویی (Magic Value) باشد، احتمال کشف آن‌ها با این روش بسیار کم است. با این حال، FUZZER  با صرف زمان کافی می‌تواند باگ‌هایی را کشف کند که در صورت قرار گرفتن ورودی در محدوده‌های بایتی بزرگ (Large Byte Ranges) تحریک و فعال می‌شوند. توجه داشته باشید که برای بسیاری از این باگ‌های LAVA، زمانی که محدوده فعال‌سازی بسیار بزرگ باشد، کشف باگ صرفاً با چندین مرتبه فازینگ(Fuzzing)  هر بایت از ورودی نیز امکان‌پذیر است.

در واقع، ممکن است این نوع باگ‌ها با استفاده از یک مجموعه رگرسیون (Regression Suite) برای برنامه‌ای مانندfile  که ورودی‌های دلخواه فایل را می‌پذیرد، به‌صورت کاملاً ساده قابل کشف باشند. در مقابل،SES  قادر است هم باگ‌هایTrigger-and-Knob  و هم باگ‌هایی با محدوده‌های مختلف (Different Ranges)  را شناسایی کند، و اندازه محدوده تأثیری بر تعداد باگ‌های کشف‌ شده ندارد.

دلیل این موضوع آن است که برای یک حل‌کننده SAT (SAT Solver)، یافتن یک ورودی ارضاکننده (Satisfying Input) برای یک محدوده بزرگ، دشوارتر از یافتن ورودی برای یک محدوده کوچک نیست. در مقابل، تعداد باگ‌هایی کهSES  می‌تواند پیدا کند، به میزان عمقی که اجرای نمادین (Symbolic Execution) قادر است در برنامه پیشروی کند، محدود می‌شود.

توجه داشته باشید که قرار دادن هر باگ در یک نسخه جداگانه از برنامه، به این معناست که در هر اجرای یک ابزار کشف باگ (Bug Finding Tool)، تنها یک باگ برای کشف در دسترس است. این یک نوع روش ارزیابی محسوب می‌شود، اما به نظر می‌رسد ابزارهایی مانندFUZZER  وSES  را در شرایط نامساعدی قرار می‌دهد؛ زیرا این ابزارها ظاهراً برای کارکرد طولانی‌مدت روی یک برنامه واحد طراحی شده‌اند؛ برنامه‌ای که ممکن است شامل چندین باگ باشد.

بنابراین، مجموعه داده دومی با نامLAVA-M  ایجاد کردیم که در آن، بیش از یک باگ به‌صورت هم‌زمان در کد منبع برنامه تزریق شده است. چهار برنامه از مجموعهcoreutils  را که ورودی فایل دریافت می‌کردند انتخاب کردیم:

  • base64
  • md5sum
  • uniq
  • who

در هر یک از این برنامه‌ها، تا حد امکان تعداد زیادی باگ تأیید شده (Verified Bugs) تزریق کردیم. از آنجا که برنامه‌هایcoreutils  نسبتاً کوچک هستند و همچنین برای انجام تحلیل آلودگی (Taint Analysis) تنها از یک فایل ورودی برای هر برنامه استفاده کردیم، تعداد کل باگ‌های تزریق ‌شده در هر برنامه معمولاً بسیار محدود بود. تنها استثنا در این الگو، برنامهwho  بود. این برنامه یک فایل باینری را تجزیه‌وتحلیل (Parse) می‌کند که شامل تعداد زیادی فیلد بدون استفاده (Unused Fields) یا حتی فیلدهای مرده (Dead Fields) است؛ بنابراین تعداد زیادیDUA  برای تزریق باگ در دسترس داشت. ما نتوانستیم چندین باگ از دو نوع توضیح ‌داده‌ شده در بخش قبل (Trigger-and-Knob و Range)  را به‌ صورت هم‌زمان تزریق کنیم، زیرا تعامل میان باگ‌ها (Bug Interactions) به یک مشکل تبدیل شد.

بنابراین، تمامی باگ‌های تزریق‌شده از نوع نشان‌داده‌شده در شکل ۸ هستند؛ یعنی باگ‌هایی که تنها با قرار گرفتن یک مقدار مشخص در چهار بایت از ورودی فعال می‌شوند. در نتیجه، مجموعه داده LAVA-M شامل چهار نسخه از کد منبع coreutils نسخه 8.24 است.

  • در یک نسخه، تعداد ۴۴ باگ در برنامه base64  تزریق شده است و همراه آن ۴۴ ورودی ارائه شده که مشخص شده‌اند هر کدام به‌صورت جداگانه قادر به فعال‌سازی یکی از این باگ‌ها هستند.
  • نسخه دوم شامل ۵۷ باگ تزریق‌شده در برنامه md5sum است.
  • نسخه سوم شامل ۲۸ باگ تزریق‌شده در برنامه uniq است.
  • در نهایت، نسخه چهارم شامل ۲۱۳۶ باگ است که همگی به‌صورت هم‌زمان در برنامه who وجود دارند و هر یک از آن‌ها به‌صورت مستقل با یک ورودی مشخص قابل فعال‌سازی هستند.

جدول ۴: باگ‌های یافت‌شده در پیکره LAVA-M

ما ابزارهای FUZZER و SES را روی هر یک از برنامه‌های موجود در مجموعه LAVA-M اجرا کردیم؛ مدت زمان اجرای هر برنامه ۵ ساعت در نظر گرفته شد. برنامه md5sum با آرگومان c- اجرا شد تا صحت هش‌ها (Digests) موجود در یک فایل بررسی شود. همچنین برنامه base64 با آرگومان d- اجرا شد تا عملیات رمزگشایی Base64 (Base64 Decoding) انجام شود. ابزار SES هیچ باگی را در برنامه‌های uniq یا md5sum شناسایی نکرد. در مورد uniq، تصور می‌کنیم دلیل این مسئله این باشد که جریان کنترل برنامه (Control Flow) بیش از حد بدون محدودیت بوده و فضای مسیرهای اجرایی بسیار گسترده است.

در مورد md5sum نیز، SES نتوانست هیچ بخشی از کد را پس از اولین فراخوانی تابع هش (Hash Function) اجرا کند. در مقابل، برنامه‌های base64 و who برای SES نتایج موفقیت‌آمیزتری داشتند. این ابزار توانست ۹ باگ از مجموع ۴۴ باگ تزریق ‌شده در base64 را کشف کند. این باگ‌ها شامل هر دو نوع باگ‌های عمیق و سطحی (Deep and Shallow Bugs) بودند؛ زیرا برنامه base64 به اندازه‌ای ساده است که تحلیل آن برای اجرای نمادین آسان‌تر است. نتایج SES برای برنامهwho  کمی پیچیده‌تر بود. تمام باگ‌هایی که SES در این برنامه پیدا کرد، از یکی از دو DUA استفاده می‌کردند و همگی در بخش‌های بسیار ابتدایی رهگیری اجرای برنامه (Execution Trace) قرار داشتند.

یکی از پیامدهای روش ما برای تزریق هم‌زمان چندین باگ این است که چندین باگ ممکن است یک نقطه حمله مشترک (Shared Attack Point) داشته باشند. اینکه این وضعیت تا چه اندازه نمایانگر باگ‌های واقعی است، قابل بحث است. در عمل، این موضوع باعث می‌شود که SES تنها بتواند یک باگ برای هر نقطه حمله پیدا کند؛ زیرا کشف یک باگ اضافی در همان نقطه حمله الزاماً نیازمند پوشش کد جدیدی نیست. البته می‌توان LAVA را تغییر داد تا هر باگ تزریق‌شده، نیازمند پوشش کد جدید (New Code Coverage) باشد. همچنین می‌توان SES را بهبود داد تا بتواند تمام باگ‌های موجود در هر نقطه حمله را پیدا کند؛ این کار نیازمند تولید چندین ورودی صدق پذیر (Multiple Satisfying Inputs) برای یک مجموعه ثابت از شرایط است.

ابزار FUZZER توانست در تمامی ابزارهای موجود در مجموعه، به‌ جز who، باگ‌هایی را پیدا کند. برخلاف SES، باگ‌های کشف‌شده توسط FUZZER تقریباً به‌صورت یکنواخت در سراسر برنامه توزیع شده بودند؛ زیرا کشف آن‌ها تنها به یافتن محرک چهار بایتی صحیح (Correct 4-byte Trigger) در موقعیت مناسب از فایل ورودی وابسته بود.

شکست FUZZER در یافتن باگ‌های برنامه who تعجب‌ برانگیز است. ما این فرضیه را مطرح می‌کنیم که ممکن است اندازه فایل اولیه (Seed File) مورد استفاده توسط فازر، که شامل ۷۶۸ بایت اول از یک فایل utmp بود، برای کاوش مؤثر از طریق جهش‌های تصادفی (Random Mutation) بیش از حد بزرگ بوده باشد؛ با این حال، برای مشخص شدن علت واقعی، بررسی‌های بیشتری لازم است.

در واقع، چنین رفتارهای غیرمنتظره‌ای در ابزارها (Tool Anomalies) دقیقاً از جمله مواردی هستند که انتظار می‌رود با استفاده از LAVA شناسایی شوند؛ زیرا این موارد نشان‌دهنده نقاطی هستند که ابزارها می‌توانند با بهبود روش‌های خود، به پیشرفت‌های نسبتاً آسانی دست یابند.

همچنین باید اشاره کنیم که هم‌پوشانی بسیار کمی بین باگ‌های کشف‌شده توسط FUZZER و SES وجود داشت؛ به‌طوری‌که تنها ۲ باگ توسط هر دو ابزار شناسایی شدند. این نتیجه برای LAVA بسیار امیدوارکننده است، زیرا نشان می‌دهد باگ‌هایی که LAVA ایجاد می‌کند، برای یک راهبرد خاص کشف باگ (Bug Finding Strategy) یا یک تکنیک مشخص طراحی نشده‌اند و می‌توانند توانایی‌ها و محدودیت‌های روش‌های مختلف کشف آسیب‌پذیری را آشکار کنند.

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

طراحیLAVA  بر اساس نیاز به مجموعه‌داده‌های باگ (Bug Corpora) انجام شده است که دارای سه ویژگی اصلی باشند:

  1. پویا (Dynamic): بتوانند در صورت نیاز، باگ‌های جدید تولید کنند؛
  2. واقع‌گرایانه (Realistic): باگ‌ها در برنامه‌های واقعی رخ دهند و با استفاده از ورودی‌های معمول برنامه قابل فعال‌سازی باشند؛
  3. ‌مقیاس بزرگ (Large): شامل صدها هزار باگ باشند.

در این بخش، مجموعه‌داده‌های موجود برای باگ‌ها را بررسی کرده و آن‌ها را با باگ‌های تولید شده توسط LAVA  مقایسه می‌کنیم. نیاز به مجموعه‌داده‌های واقع‌گرایانه مدت‌هاست که مورد توجه پژوهشگران قرار گرفته است. محققان پیشنهادهایی مانند ایجاد مجموعه‌داده‌های باگ از کدهای دانشجویی (Student Code)، استخراج باگ‌ها از پایگاه‌های گزارش باگ موجود (Existing Bug Report Databases) [12], [13] و [18] و ایجاد یک ثبت عمومی باگ‌ها (Public Bug Registry) [7] را ارائه کرده‌اند. با وجود این پیشنهادها، مجموعه‌داده‌های عمومی باگ همچنان ایستا (Static) و نسبتاً کوچک باقی مانده‌اند.

قدیمی‌ترین پژوهشی که به نظر می‌رسد از مجموعه‌داده‌های باگ برای ارزیابی ابزارها استفاده کرده است، مربوط به Wilander و Kamkar است. آن‌ها یک محیط آزمایشی مصنوعی شامل ۴۴ فراخوانی تابع C [22] و ۲۰ حمله متفاوت سرریز بافر (Buffer Overflow Attack) [23] ایجاد کردند تا به‌ترتیب اثربخشی ابزارهای تشخیص باگ ایستا (Static Bug Detection) و تشخیص باگ پویا (Dynamic Bug Detection) را ارزیابی کنند.

با این حال، این موارد، نمونه‌های مصنوعی (Synthetic Test Cases) هستند و ممکن است بازتاب‌دهنده باگ‌های واقعی دنیای واقعی نباشند. در سال ۲۰۰۴، Zitser و همکاران [27] ابزارهای تشخیص سرریز بافر ایستا (Static Buffer Overflow Detectors)  را ارزیابی کردند. مجموعه‌داده‌های مرجع معتبر(Ground Truth Corpus)  آن‌ها با تلاش فراوان و به‌صورت دستی طی شش ماه ایجاد شد و شامل ۱۴ مورد سرریز بافر مستندسازی ‌شده (Annotated Buffer Overflows) بود.

این مجموعه شامل ورودی‌های محرک باگ (Triggering Inputs)، ورودی‌هایی که باعث فعال شدن باگ نمی‌شدند  (Non-triggering Inputs) و نسخه‌های دارای باگ و نسخه‌های اصلاح‌ شده برنامه (Buggy and Patched Versions) بود.

همین ۱۴ مورد سرریز بافر بعدها برای ارزیابی ابزارهای تشخیص سرریز پویا [25] (Dynamic Overflow Detectors) نیز استفاده شدند. اگرچه این موارد، باگ‌های واقعی از نرم‌افزارهای واقعی بودند، اما این مجموعه داده هم از نظر تعداد باگ‌ها (۱۴ مورد) و هم از نظر اندازه برنامه‌ها بسیار محدود بود. حتی برنامه‌هایی با اندازه متوسط مانند sendmail نیز برای برخی تحلیلگرهای ایستا بیش از حد بزرگ بودند؛ بنابراین، پژوهشگران مجبور شدند مدل‌های بسیار کوچک‌تری ایجاد کنند که تنها ویژگی اصلی هر باگ را در چند صد خط کد استخراج‌شده (Excerpted Code) شبیه‌سازی می‌کردند.

گسترده‌ترین تلاش برای ایجاد یک مجموعه‌داده عمومی از باگ‌ها، توسط پروژه NIST SAMATE (Software Assurance Metrics And Tool Evaluation) [10] انجام شده است. مجموعه ارزیابی این پروژه شاملJuliet  [2]است؛ مجموعه‌ای متشکل از ۸۶٬۸۶۴ برنامه مصنوعی نوشته ‌شده به زبان‌های C و Java که دارای ۱۱۸ نوع مختلف از ضعف‌های امنیتی CWE هستند. با این حال، هر یک از این برنامه‌ها نسبتاً کوتاه هستند و دارای جریان کنترل (Control Flow) و جریان داده (Data Flow) ساده و بدون پیچیدگی می‌باشند.

این مجموعه همچنین شامل مجموعه‌داده IARPA STONESOUP  [19]است که در پشتیبانی از پروژه کاهش آسیب‌پذیری STONESOUP توسعه داده شده است. نمونه‌های آزمایشی موجود در این مجموعه شامل ۱۶۴ قطعه کد کوچک (Code Snippet)  به زبان‌های C و Java هستند که سپس با استفاده از روش الحاق کد (Splicing) به برنامه‌ها اضافه می‌شوند تا یک باگ در آن‌ها تزریق شود. با این حال، باگ‌هایی که به این روش تزریق می‌شوند، از ورودی اصلی برنامه استفاده نمی‌کنند؛ بلکه داده مورد نیاز آن‌ها از فایل‌های اضافی و متغیرهای محیطی (Environment Variables) که به برنامه اضافه می‌شوند، تأمین می‌شود. همچنین، جریان داده بین ورودی و محل ایجاد باگ بسیار کوتاه است و عمق پیچیدگی موجود در برنامه‌های واقعی را منعکس نمی‌کند.

در جدیدترین تلاش، Shiraishi و همکاران [17] یک تحلیل کمی از ابزارهای تجاری تحلیل ایستا (Commercial Static Analysis Tools) انجام دادند. آن‌ها با ساخت ۴۰۰ جفت تابع C، مجموعه‌ای ایجاد کردند که در هر جفت، دو نسخه از یک تابع وجود داشت:

  • یک نسخه دارای باگ  (Buggy Version)
  • یک نسخه بدون باگ  (Non-buggy Version)

باگ‌های موجود در این مجموعه، انواع مختلف خطاها را پوشش می‌دادند، از جمله:

  • سرریزهای بافر ایستا و پویا  (Static/Dynamic Buffer Overruns)
  • خطاهای عدد صحیح  (Integer Errors)
  • باگ‌های هم‌روندی (Concurrency Bugs)

سپس ابزارهای تحلیل را بر اساس میزان اثربخشی رتبه‌بندی کردند و با اضافه کردن اطلاعات قیمت ابزارهای تجاری، معیاری برای کارایی هزینه‌ای (Efficiency) هر ابزار ارائه دادند. با این حال، مانند مجموعه‌داده‌های مصنوعی قبلی، توابع موجود در این مجموعه نیز نسبتاً کوتاه هستند و ممکن است کشف آن‌ها آسان‌تر از آسیب‌پذیری‌هایی باشد که در عمق یک پایگاه کد بزرگ (Large Code Base)  قرار گرفته‌اند. در نهایت، رویکرد کلی تغییر خودکار برنامه برای معرفی خطا (Automatic Program Transformation to Introduce Errors) نیز توسط Rinard و همکاران استفاده شده است.

آن‌ها به‌صورت نظام‌مند شرایط خاتمه حلقه‌ها (Loop Termination Conditions) را در نرم‌افزار ایمیل Pine  تغییر دادند تا خطاهای Off-by-One  ایجاد کنند. هدف آن‌ها بررسی این موضوع بود که آیا نرم‌افزار پس از حذف بررسی‌های صحت (Sanity Checks) و شرط‌ها، همچنان در حضور خطاها قابل استفاده باقی می‌ماند یا خیر.

۸. محدودیت‌ها و کارهای آینده (Limitations and Future Work)

بخش قابل‌توجهی از کارهای آینده LAVA به این موضوع اختصاص خواهد داشت که مجموعه‌داده‌های تولیدشده، شباهت بیشتری به باگ‌هایی پیدا کنند که در برنامه‌های واقعی مشاهده می‌شوند. در حال حاضر، LAVA تنها سرریزهای بافر (Buffer Overflows) را به برنامه‌ها تزریق می‌کند. با این حال، تحلیل مبتنی بر Taint Analysis که در LAVA استفاده می‌شود، اولین مانع مهم برای تزریق هر نوع باگ را برطرف می‌کند؛ یعنی اطمینان از اینکه داده‌های تحت کنترل مهاجم (Attacker-Controlled Data) می‌توانند در بهره‌برداری بالقوه از آسیب‌پذیری مورد استفاده قرار گیرند. در نتیجه، دسته‌های دیگری از آسیب‌پذیری‌ها نیز باید با استفاده از این رویکرد قابل تزریق باشند؛ از جمله آسیب‌پذیری‌های ایمنی زمانی (Temporal Safety Bugs) مانند استفاده مجدد پس از آزادسازی (Use-After-Free) و آسیب‌پذیری‌های مرتبط با کاراکترهای ویژه (Meta-Character Bugs) مانند آسیب‌پذیری‌های رشته فرمت یا قالب‌بندی (Format String Vulnerabilities).

همچنین، هنوز نیاز به کار بیشتری وجود دارد تا جریان داده‌ای که باعث فعال شدن باگ در LAVA می‌شود (Bug-Triggering Data Flow)، واقع‌گرایانه‌تر شود. با این وجود، حتی در وضعیت فعلی، بخش عمده‌ای از اجرای برنامه تغییر‌یافته، کاملاً واقع‌گرایانه باقی می‌ماند. این اجرای واقع‌گرایانه شامل جریان داده‌ای است که تا رسیدن به نقطه ثبت و استخراج DUA ادامه پیدا می‌کند؛ جریانی که در بسیاری از موارد، خود دارای پیچیدگی قابل‌توجهی است.

با وجود چشم‌انداز امیدوارکننده‌ای که برای LAVA به نظر می‌رسد، احتمالاً برخی دسته‌های خاص از باگ‌ها اساساً با استفاده از معیارهای مبتنی برTaint Analysis  قابل تزریق نیستند. برای مثال، خطاهای منطقی (Logic Errors)، ضعف‌های رمزنگاری (Cryptographic Flaws) و آسیب‌پذیری‌های کانال جانبی (Side-Channel Vulnerabilities) همگی ظاهراً در سطحی متفاوت از آسیب‌پذیری‌های مبتنی بر جریان داده (Data-Flow Triggered Vulnerabilities) عمل می‌کنند؛ همان دسته‌ای از آسیب‌پذیری‌ها که LAVA به‌خوبی برای تولید آن‌ها طراحی شده است. بنابراین، ما چندان امیدوار نیستیم که این نوع آسیب‌پذیری‌ها در آینده نزدیک با استفاده از LAVA قابل تزریق باشند.

در جریان استفاده از باگ‌های تولیدشده توسط LAVA برای ارزیابی ابزارهای کشف آسیب‌پذیری، ما چندین مورد را شناسایی کردیم که در آن‌ها LAVA به‌صورت ناخواسته باگ‌های دیگری ایجاد می‌کند؛ از جمله استفاده مجدد وس از آزادسازی (Use-After-Free) و ارجاع به اشاره‌گر مقداردهی‌نشده  (Dereference of an Uninitialized Pointer). این آسیب‌پذیری‌ها در بخشی از کد ایجاد می‌شوند که LAVA برای انتقال و ذخیره مقدار DUA جهت استفاده در آینده برای فعال‌سازی باگ اضافه می‌کند.

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

این اثرات جانبی ناشی از این واقعیت هستند که LAVA هیچ تحلیل ایستای واقعی (Static Analysis) انجام نمی‌دهد تا بررسی کند آیا استفاده از یک اشاره‌گر و ارجاع به آن، حتی تا حدی ایمن است یا خیر؛ در حالی که برای ایجاد جریان داده مورد نیاز جهت تزریق باگ، ممکن است لازم باشد چنین ارجاع‌هایی اضافه شوند.

در بسیاری از موارد، احتمالاً می‌توان این مشکل را به‌طور قابل‌توجهی کاهش داد؛ اما یک راه‌حل کامل احتمالاً نیازمند تحلیل ایستای کل برنامه (Whole-Program Static Analysis) است که از نظر محاسباتی بسیار پرهزینه و عملاً غیرقابل‌حل باشد.

یکی دیگر از محدودیت‌های LAVA این است که در حال حاضر تنها روی کد منبع زبان C کار می‌کند؛ اما هیچ دلیل بنیادی وجود ندارد که این رویکرد به C محدود باشد. از نظر تئوری، روش پیشنهادی ما می‌تواند برای هر زبان برنامه‌نویسی که دارای یک چارچوب مناسب بازنویسی کد منبع به کد منبع (Source-to-Source Rewriting Framework) باشد، مورد استفاده قرار گیرد.

برای مثال، در زبان پایتون (Python) می‌توان به‌سادگی Taint Queryهای مورد استفاده در LAVA را در یک نسخه تغییر‌یافته از مفسر CPython پیاده‌سازی کرد؛ به‌ گونه‌ای که فراخوانی‌های هایپروایزر (Hypervisor Calls) را با استفاده از آدرس یک متغیر در حافظه اجرا کند.

از آنجا که روش ما ارتباط بین خطوط کد منبع (Source Lines) و اجرای بلوک‌های پایه برنامه (Basic Block Execution) را ثبت می‌کند، یافتن محل مناسب برای ویرایش کد Python نیز به همان اندازه‌ای ساده خواهد بود که در C انجام می‌شود. با این حال، در حال حاضر برنامه فوری برای گسترش LAVA به این حوزه‌ها نداریم.

ما در حال برنامه‌ریزی برای انجام ارزیابی‌های تکمیلی هستیم. به‌طور خاص، لازم است یک ارزیابی گسترده از ابزارهای واقعی و نام‌گذاری‌شده (Real, Named Tools) انجام شود. نتایج این ارزیابی می‌تواند نقاط قوت و ضعف دسته‌های مختلف روش‌ها، و همچنین پیاده‌سازی‌های خاص هر ابزار را روشن کند.

همچنین باید توجه داشت که در ارزیابی مقدماتی ما از ابزارهای کشف آسیب‌پذیری، تنها نرخ عدم کشف (Miss Rate) اندازه‌گیری شد و هیچ تلاشی برای سنجش نرخ هشدار کاذب (False Alarm Rate) انجام نشد. برای ابزارهایی که مانند SES و FUZZER یک ورودی محرک باگ (Bug-Triggering Input) تولید می‌کنند، اندازه‌گیری نرخ هشدار کاذب باید ساده باشد. هر ورودی تولیدشده را می‌توان پس از Instrumentation یا ابزارگذاری برنامه برای تشخیص آسیب‌پذیری، دوباره روی برنامه اجرا و بررسی کرد.

در مورد سرریزهای بافر در زبان C، این کار می‌تواند با کامپایل برنامه همراه با بررسی دقیق محدوده حافظه (Fine-Grained Bounds Checking) انجام شود. با این حال، بسیاری از ابزارهای کشف باگ، به‌خصوص تحلیلگرهای ایستا (Static Analyzers) و ابزارهای مبتنی بر تفسیر انتزاعی (Abstract Interpretation)، ورودی فعال‌کننده باگ تولید نمی‌کنند. این ابزارها صرفاً یک خط مشخص از برنامه را نشان می‌دهند و درباره احتمال وجود باگ در آن نقطه اظهار نظر می‌کنند. در چنین شرایطی، روشی برای ارزیابی نرخ هشدار کاذب بدون صرف تلاش گسترده دستی به ذهن ما نمی‌رسد.

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

در این مقاله، ما LAVA را معرفی کردیم؛ یک سیستم کاملاً خودکار که قادر است تعداد زیادی باگ واقع‌گرایانه را با سرعت بالا در برنامه‌های نوشته‌شده به زبان C تزریق کند. LAVA تاکنون برای تزریق بیش از ۴۰۰۰ آسیب‌پذیری سرریز بافر (Buffer Overflow) واقع‌گرایانه در برنامه‌های متن‌باز لینوکسی نوشته‌شده به زبان C مورد استفاده قرار گرفته است؛ برنامه‌هایی که اندازه آن‌ها تا ۲ میلیون خط کد می‌رسد.

ما از مجموعه‌داده‌های تولیدشده توسط LAVA برای ارزیابی توانایی کشف آسیب‌پذیری در ابزارهای پیشرفته و به‌ روز (State-of-the-Art Bug Finding Tools) استفاده کرده‌ایم. معیارهای مبتنی بر تحلیل آلودگی (Taint Analysis) که LAVA برای شناسایی داده‌های تحت کنترل مهاجم (Attacker-Controlled Data) و استفاده از آن‌ها در ایجاد آسیب‌پذیری‌های جدید به کار می‌گیرد، قدرتمند هستند و می‌توانند برای تزریق طیف گسترده‌ای از آسیب‌پذیری‌های متنوع مورد استفاده قرار گیرند. با این حال، احتمالاً محدودیت‌های بنیادی نیز وجود دارد؛ برای مثال، انتظار نمی‌رود که LAVA در آینده نزدیک بتواند خطاهای منطقی (Logic Errors) را به برنامه‌ها تزریق کند.

با وجود این محدودیت‌ها، LAVA آماده استفاده فوری به‌عنوان یک منبع درخواستی برحسب نیاز (On-Demand) برای تولید آسیب‌پذیری‌های واقعی و دارای مجموعه‌داده‌های مرجع معتبر (Ground Truth Vulnerabilities) است؛ به‌ویژه برای دسته‌ای از آسیب‌پذیری‌های جدی که همچنان در کدهای حیاتی و حساس (Mission-Critical Code) به‌وفور مشاهده می‌شوند. امیدواریم LAVA بتواند به توسعه و ارزیابی ابزارها و تکنیک‌های پیشرفته در حوزه کشف آسیب‌پذیری (Vulnerability Discovery) کمک کند.

قدردانی

از Graham Baker، Chris Connelly، Jannick Pewny و Stelios Sidiroglou-Douskos به‌دلیل گفتگوهای اولیه ارزشمند و پیشنهادهای مفیدشان صمیمانه تشکر می‌کنیم. همچنین از Amy Jiang برای مشارکت مهم او در توسعه اولیه Clang و انجام فرایند اشکال‌زدایی (Debugging) تشکر می‌کنیم.

منابع

				
					[1] Cristian Cadar, Daniel Dunbar, and Dawson Engler. KLEE: Unassistedand automatic generation of high-coverage tests for complex systemsprograms. InProceedings of the 8th USENIX Conference on OperatingSystems Design and Implementation, OSDI’08. USENIX Association,2008.
[2] Center for Assured Software. Juliet test suite v1.2 user guide. Technicalreport, National Security Agency, 2012.
[3] Sang Kil Cha, T. Avgerinos, A. Rebert, and D. Brumley. Unleashingmayhem on binary code. InIEEE Symposium on Security and Privacy,2012.
[4] Vitaly Chipounov, Volodymyr Kuznetsov, and George Candea. S2E: Aplatform for in-vivo multi-path analysis of software systems. InArchi-tectural Support for Programming Languages and Operating Systems,2011.
[5] Brendan Dolan-Gavitt, Joshua Hodosh, Patrick Hulin, Timothy Leek,and Ryan Whelan. Repeatable reverse engineering with PANDA. InWorkshop on Program Protection and Reverse Engineering (PPREW),2015.
[6] Keromytis et al. Tunable cyber defensive security mechanisms. https://www.sbir.gov/sbirsearch/detail/825791, August 2015.
[7] Jeffrey Foster. A call for a public bug and tool registry. InWorkshopon the Evaluation of Software Defect Detection Tools, 2005.
[8] Vijay Ganesh, Tim Leek, and Martin Rinard.  Taint-based directedwhitebox fuzzing. InICSE ’09: Proceedings of the 31st InternationalConference on Software Engineering, 2009.
[9] Istvan Haller, Asia Slowinska, Matthias Neugschwandtner, and HerbertBos. Dowsing for overflows: A guided fuzzer to find buffer boundaryviolations.  InProceedings of the 22nd USENIX Security Symposium(USENIX Security ‘13). USENIX, 2013.
[10] Michael Kass.  NIST software assurance metrics and tool evaluation(SAMATE) project. InWorkshop on the Evaluation of Software DefectDetection Tools, 2005.
[11] Kendra Kratkiewicz and Richard Lippmann. Using a diagnostic corpusof C programs to evaluate buffer overflow detection by static analysistools.  InProc. of Workshop on the Evaluation of Software DefectDetection Tools, 2005.
[12] Shan Lu, Zhenmin Li, Feng Qin, Lin Tan, Pin Zhou, and YuanyuanZhou. BugBench: A benchmark for evaluating bug detection tools. InWorkshop on the Evaluation of Software Defect Detection Tools, 2005.
[13] Barmak Meftah. Benchmarking bug detection tools. InWorkshop onthe Evaluation of Software Defect Detection Tools, 2005.
[14] James Newsome and Dawn Song. Dynamic taint analysis for automaticdetection, analysis, and signature generation of exploits on commoditysoftware. InNetwork and Distributed Systems Symposium (NDSS), 2005.
[15] Martin Rinard, Cristian Cadar, and Huu Hai Nguyen. Exploring the ac-ceptability envelope. InCompanion to the 20th Annual ACM SIGPLANConference on Object-oriented Programming, Systems, Languages, andApplications, OOPSLA ’05, pages 21–30, New York, NY, USA, 2005.ACM.
[16] Olatunji Ruwase and Monica S Lam.  A practical dynamic bufferoverflow detector. InNDSS, 2004.
[17] Shin’ichi Shiraishi, Veena Mohan, and Hemalatha Marimuthu.  Testsuites for benchmarks of static analysis tools. InProceedings of the 2015IEEE International Symposium on Software Reliability Engineering,ISSRE ’15, 2015.
[18] Jaime Spacco, David Hovemeyer, and William Pugh. Bug specimens areimportant. InWorkshop on the Evaluation of Software Defect DetectionTools, 2005.
[19] TASC, Inc., Ponte Technologies LLC, and iSW LLC. STONESOUPphase 3 test generation report. Technical report, SAMATE, 2014.
[20] Vlad Tsyrklevich. Hacking team: A zero-day market case study. https://tsyrklevich.net/2015/07/22/hacking-team-0day-market/, July 2015.
[21] Tielei Wang, Tao Wei, Guofei Gu, and Wei Zou.  TaintScope: Achecksum-aware directed fuzzing tool for automatic software vulner-ability detection. InIEEE Symposium on Security and Privacy, 2010.
[22] John Wilander and Mariam Kamkar. A comparison of publicly availabletools for static intrusion prevention. InProceedings of the 7th NordicWorkshop on Secure IT Systems, 2002.
[23] John Wilander and Mariam Kamkar. A comparison of publicly availabletools for dynamic buffer overflow prevention. InProceedings of the 10thNetwork and Distributed System Security Symposium (NDSS), 2003.
[24] F. Yamaguchi, N. Golde, D. Arp, and K. Rieck. Modeling and discov-ering vulnerabilities with code property graphs. InIEEE Symposium onSecurity and Privacy, 2014.
[25] Michael Zhivich, Tim Leek, and Richard Lippmann. Dynamic bufferoverflow detection. InWorkshop on the Evaluation of Software DefectDetection Tools, 2005.
[26] Misha Zitser, Richard Lippmann, and Tim Leek. Personal communica-tion.
[27] Misha Zitser, Richard Lippmann, and Tim Leek. Testing static analysistools using exploitable buffer overflows from open source code. InPro-ceedings of the 12th ACM SIGSOFT Twelfth International Symposiumon Foundations of Software Engineering, SIGSOFT ’04/FSE-12, pages97–106, New York, NY, USA, 2004. ACM

				
			

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

پیام بگذارید

wpChatIcon
wpChatIcon