LibFuzzer یک موتور فازینگ (Fuzzing) درونفرآیندی مبتنی بر پوشش کد و با رویکرد تکاملی است. LibFuzzer همراه با کتابخانهای که قرار است مورد آزمایش قرار گیرد، لینک میشود و ورودیهای فازشده (fuzzed inputs) را از طریق یک نقطهی ورود مشخص برای فازینگ (که به آن «تابع هدف» یا target function نیز گفته میشود) به کتابخانه ارسال میکند. سپس فازر مسیرهای طیشده در کد را ردیابی کرده و با ایجاد تغییرات (جهشها) در مجموعه دادههای ورودی، تلاش میکند پوشش کد را به حداکثر برساند. اطلاعات مربوط به پوشش کد برای LibFuzzer توسط ابزار SanitizerCoverage در چارچوب LLVM فراهم میشود.
1. وضعیت
نویسندگان اصلی LibFuzzer دیگر بهصورت فعال بر روی این پروژه کار نمیکنند و تمرکز خود را به موتور فازینگ دیگری با نام Centipede منتقل کردهاند. که البته در حال حاضر این پروژه هم آرشیو شده و با پروژه fuzztest گوگل ادغام شده است.
با این حال، LibFuzzer همچنان بهطور کامل پشتیبانی میشود و خطاهای مهم در آن برطرف خواهند شد. با این وجود، انتظار ارائهی ویژگیهای عمدهی جدید یا بازبینی کد (Code Review) – بهجز برای رفع باگها – را نداشته باشید.
2. نسخهها
برای استفاده از LibFuzzer، لازم است نسخهی آن با نسخهی Clang مطابقت داشته باشد.
3. libFuzzer
libFuzzer یک گزینه واضح و ساده برای فازینگ برنامههای C++/C است، زیرا بخشی از پروژه LLVM محسوب میشود و روی اکثر پلتفرمها در دسترس است.
- توصیه میشود در صورت امکان روی Linux فازینگ انجام دهید، زیرا این پلتفرم بهترین پشتیبانی را از libFuzzer دارد (بهعنوان مثال، libFuzzer در XCode روی macOS بهصورت پیشفرض نصب نیست).
- کامپایلر MSVC مایکروسافت نیز اخیراً از libFuzzer پشتیبانی میکند.
توجه داشته باشید که libFuzzer از اواخر سال ۲۰۲۲ در حالت نگهداری صرف (maintenance-only) قرار دارد، بنابراین ویژگیهای جدیدی اضافه نمیشود. با این حال، نصب و استفاده از آن نسبت به جایگزینها آسانتر است، پشتیبانی گسترده دارد و همچنان در آینده نزدیک نگهداری خواهد شد. به همین دلیل، Trail of Bits توصیه میکند که برای اولین تجربههای فازینگ خود از libFuzzer استفاده کنید.
نسخه پرکاراتر ++AFL با harnessهای نوشته شده برای libFuzzer سازگار است. این بدان معناست که انتقال از libFuzzer به ++AFL آسان است و تنها کافی است کامپایلر خود را از ++clang به ++afl-clang-fast تغییر دهید.
این بخش از Testing Handbook بر اساس فازینگ باینریهای C++/C در Ubuntu x86_64 نوشته شده است. در صورت امکان، توصیه میشود روی یک VM محلی x86_64 یا سرویسهای ابری مانند DigitalOcean، AWS، Hetzner و غیره فازینگ انجام دهید.
4. مراحل نصب
sudo apt install build-essential gdb curl wget make cmake git
sudo apt install clang llvm
#include
#include
void check_buf(char *buf, size_t buf_len) {
if(buf_len > 0 && buf[0] == 'a') {
if(buf_len > 1 && buf[1] == 'b') {
if(buf_len > 2 && buf[2] == 'c') {
abort();
}
}
}
}
#ifndef NO_MAIN
int main() {
char target[] = "123";
size_t len = strlen(target);
check_buf(target, len);
return 0;
}
#endif // NO_MAIN
محتوی زیر را در فایلی با نام harness.cc به ذخیره کنید.
#include
#include
#include
void check_buf(char *buf, size_t buf_len);
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
check_buf((char*) data, size); // Invoke the SUT; in our case that is the check_buf function
return 0; // Return 0, which means that the test case was processed correctly
}
پس از ذخیره دو سورس فایل فوق با دستور زیر آنرا جهت فاز، کامپایل میکنیم. ایجاد یک باینری که SUT (سیستم تحت آزمون یا System Under Test) را فاز کند ساده است. باینری حاصل از harness و زماناجرای libFuzzer بهره خواهد برد. اگر از کامپایلر Clang استفاده میکنید، دستور زیر یک باینری به نامfuzz در پوشه کاری فعلی تولید میکند:
clang++ -DNO_MAIN -g -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
توجه: اگر در SUT (سیستم تحت آزمون) یا در harness تغییری ایجاد کنید، لازم است برنامه را دوباره کامپایل کنید.
فلگ (flag) کلیدی در اینجا fsanitize=fuzzer- است، که به کامپایلر اعلام میکند از libFuzzer استفاده کند. هنگام استفاده از این گزینه، چندین فرایند در پسزمینه انجام میشود:
- libFuzzer زماناجرا به باینری لینک میشود، که تابع main مخصوص خود را دارد و اجرای فازر را مدیریت میکند.
- ابزار SanitizerCoverage برای جمعآوری دادههای مربوط به پوشش کد (code coverage) به کار گرفته میشود.
- توابع داخلی (built-in) خاصی از طریق پرچمهایی مانند -fno-builtin-memcmp در Clang غیرفعال میشوند تا رفتار فازینگ دقیقتر باشد.
- سایر گزینههای کامپایل نیز ممکن است بر اساس معماری هدف تغییر کنند (میتوانید در کد منبع LLVM موارد مرتبط را جستوجو کنید).
فلگ DNO_MAIN- یک ماکرو تعریف میکند که تابع main پیشفرض مثال ما را غیرفعال میسازد، زیرا libFuzzer تابع main خودش را ارائه میدهد. بسته به ساختار پروژه، ممکن است لازم باشد ماکروی مشابهی را هنگام فاز کردن یک باینری اضافه کنید (اگرچه در مورد کتابخانهها معمولاً نیازی به این کار نیست).
همچنین، با استفاده از گزینهی g-، نمادهای اشکالزدایی (debug symbols) فعال میشوند، و سطح بهینهسازی روی O2- تنظیم میگردد. این سطح، برای فازینگ مناسب است، زیرا معمولاً همان سطح بهینهسازی است که در نسخههای نهایی (production) به کار میرود.
در صورتی که پروژهی شما وابسته به کامپایلر GCC است، پیشنهاد میشود بهجای libFuzzer، از ++AFL به همراه gcc_plugin استفاده کنید.
4.2 نحوه استفاده
./fuzz
orpus_dir میتواند یک پوشه خالی باشد. در حالت ایدهآل باید نمونههای اولیه (seed test cases) را فراهم کنید. برای مثال، اگر در حال فاز کردن یک کتابخانه PNG هستید، ممکن است بخواهید مجموعهای از تصاویر نمونه PNG قرار دهید (برای نمونههای عملی به بخش «مثالهای واقعی» مراجعه کنید).
بهطور پیشفرض، پس از پیدا شدن یک کرش، libFuzzer فازینگ را ادامه نمیدهد. این رفتار را میتوان با افزودن پرچمهای تجربی زیر تغییر داد:
-fork=1 -ignore_crashes=1
(فلگهای (Flag) مرتبط -ignore_timeouts و ignore_ooms- بهصورت پیشفرض فعال هستند). هرچند این پرچمها تجربی محسوب میشوند، ولی بهطور گستردهای استفاده میشوند. بنابراین توصیه میشود برای اجرای یک کمپین فازینگ طولانیمدت از دستور زیر استفاده کنید:
./fuzz -fork=1 -ignore_crashes=1
مثل
./fuzz -fork=1 -ignore_crashes=1 corpus/
از آنجا که مثالِ مورد بحث نسبتاً ساده است، یک پوشه خالی برای corpus کافی است:
mkdir corpus/
همچنین میتوان پوشه corpus را حذف کرد؛ در این حالت تنها کرشها روی دیسک ذخیره میشوند و خود corpus ذخیره نخواهد شد. بنابراین پس از اتمام یک کمپین فازینگ، corpus از بین میرود.
./fuzz corpus/
با توجه به سادگی مثال، به سرعت یک کرش مشاهده خواهید کرد. خروجی شامل آمارهایی درباره اجرای فعلی در هر ثانیه (executions per second) و اندازه corpus است.
1INFO: Running with entropic power schedule (0xFF, 100).
2INFO: Seed: 3517090860
3INFO: Loaded 1 modules (9 inline 8-bit counters): 9 [0x55c248efafa0, 0x55c248efafa9),
4INFO: Loaded 1 PC tables (9 PCs): 9 [0x55c248efafb0,0x55c248efb040),
5INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
6INFO: A corpus is not provided, starting from an empty corpus
7#2 INITED cov: 3 ft: 4 corp: 1/1b exec/s: 0 rss: 26Mb
8#57 NEW cov: 4 ft: 5 corp: 2/4b lim: 4 exec/s: 0 rss: 26Mb L: 3/3 MS: 5 CrossOver-ShuffleBytes-InsertByte-CrossOver-InsertByte-
9#73 REDUCE cov: 4 ft: 5 corp: 2/3b lim: 4 exec/s: 0 rss: 26Mb L: 2/2 MS: 1 EraseBytes-
10#16921 NEW cov: 7 ft: 8 corp: 5/9b lim: 163 exec/s: 0 rss: 26Mb L: 2/3 MS: 2 ChangeBinInt-EraseBytes-
11==11672== ERROR: libFuzzer: deadly signal
12
13 [ ... Stacktrace ...]
14
15SUMMARY: libFuzzer: deadly signal
16MS: 4 CrossOver-CrossOver-EraseBytes-ChangeBit-; base unit: 3f786850e387550fdab836ed7e6dc881de23001b
170x61,0x62,0x63,
18abc
19artifact_prefix='./'; Test unit written to ./crash-a9993e364706816aba3e25717850c26c9cd0d89d
20Base64: YWJj
در شکل بالا خروجی اجرای libFuzzer را مشاهده می کنید که:
در هنگام اجرای libFuzzer، خروجیای در ترمینال نمایش داده میشود. برای جزئیات بیشتر دربارهی محتوای این خروجی، میتوانید به مستندات رسمی libFuzzer مراجعه کنید.
بخشی از متن برجستهشده (highlighted text) در خروجی، مسیر فایل تستی را نشان میدهد که باعث وقوع کرش شده است. این فایل معمولاً در پوشهای ذخیره میشود تا بتوانید بعداً آن را برای بازتولید و تحلیل خطا مورد استفاده قرار دهید.
4.3 شروع و پیکربندی فازر
در ابتدای اجرای فازر، برخی اطلاعات دربارهی پیکربندی آن نمایش داده میشود، از جمله seed (مقداری اولیه برای تولید ورودیها). اگر بخواهید یک کمپین libFuzzer را بازتولید کنید، میتوانید از پرچم خط فرمان زیر استفاده کنید:
-seed=3517090860
توجه داشته باشید که این نتایج فقط در صورتی باز تولیدپذیر هستند که فازینگ روی یک هسته (single-core) اجرا شود؛ در فازینگ چندهستهای (multi-core)، اشتراکگذاری تستهای جالب بین هستهها غیرقطعی (non-deterministic) است.
4.4 مشاهده خروجی در حین اجرا
هنگام اجرای فازر، خطوطی که با # شروع میشوند در ترمینال چاپ میشوند. برای توضیح دقیقتر دربارهی این دادهها، به مستندات libFuzzer مراجعه کنید.
4.5 خروجی در پایان اجرای فازر
در پایان خروجی libFuzzer، مسیر فایلی که باعث کرش شده است نمایش داده میشود. همچنین، ورودی مربوطه به صورتهای زیر نمایش داده میشود:
- Hexadecimal: مانند 0x61, 0x62, 0x63
- UTF-8: مانند abc
- Base64: مانند YWJj
همچنین نام base unit (ورودی پایه که بر اساس آن جهشها ایجاد شدهاند) نمایش داده میشود. در مثال ما، base unit با شناسه 3f7868… شامل رشتهی “ab” بود و فازر این رشته را تغییر داد تا به ورودی کرشکنندهی “abc” برسد.
توجه داشته باشید که libFuzzer پس از یافتن یک باگ بهطور خودکار مجدداً اجرا نمیشود. بنابراین، قبل از اجرای مجدد فازر، باید هر باگی که پیدا شده است را برطرف کنید. اگر قصد دارید کمپینهای طولانیمدت فازینگ اجرا کنید، میتوانید از ++AFL استفاده کنید که پس از یافتن باگ، بهصورت خودکار ادامه میدهد.
5. اجرای مجدد یک تست کیس
برای اجرای مجدد یک تست کیس میتوانید از دستور زیر استفاده کنید:
./fuzz
به عنوان مثال، دستور زیر یک کرش را مجدداً اجرا میکند:
./fuzz ./crash-a9993e364706816aba3e25717850c26c9cd0d89d
این روش به شما کمک میکند باگهای پیدا شده را دستهبندی و بررسی کنید (triage). اگر بخواهید یک پوشه از تست کیسها را بدون انجام فازینگ واقعی اجرا کنید (با پرچم -runs=0)، میتوانید دستور زیر را اجرا کنید:
./fuzz -runs=0
5.1 گزینههای فازر (Fuzzer options)
چندین گزینه را میتوان با افزودن پرچمهای خط فرمان هنگام اجرای ./fuzz تنظیم کرد. برای مشاهده همه گزینهها میتوان از دستور زیر استفاده کرد:
-help=1
مهمترین گزینهها:
- -max_len=4000
حداکثر طول ورودی تست. بهطور پیشفرض، libFuzzer تلاش میکند این مقدار را حدس بزند. توصیه میشود این مقدار حداقل چند برابر طول کمینه ورودی واقعی باشد. به عنوان یک قانون کلی، ابتدا یک ورودی واقعی و کوچک پیدا کنید و سپس طول آن را دو برابر کنید. توجه داشته باشید که ورودیهای بزرگتر زمان اجرای بیشتری میگیرند و لزوماً باعث افزایش فضای جستجوی ورودی نمیشوند. - -timeout=2
libFuzzerپس از n ثانیه اجرای یک تست کیس را متوقف میکند. منطقی است که این مقدار را نسبتاً پایین تنظیم کنید تا ورودیهایی که باعث گیر کردن (hang) SUT برای مدت طولانی میشوند، شناسایی شوند. به عنوان مثال، پردازش یک تصویر PNG با اندازه معقول نباید بیشتر از چند صد میلیثانیه طول بکشد، بنابراین تنظیم این مقدار روی چند ثانیه معمولاً کافی است تا خطاهای کاذب ایجاد نشود. - -dict=./dict.dict
مشخصکننده یک فایل دیکشنری است که فازر را هدایت میکند و باعث میشود سریعتر تست کیسهای جالب کشف شوند. برای جزئیات بیشتر، به بخش Fuzzing dictionary مراجعه کنید. - -jobs=10
اجرای ۱۰ کمپین فازینگ به ترتیب. برای جزئیات بیشتر به بخش Multi-core fuzzing مراجعه کنید. - -workers=2
کمپینهای تعریفشده با -jobs را با دو worker اجرا میکند. این مقدار پیشفرض، نصف تعداد هستههای CPU است. جزئیات بیشتر در بخش Multi-core fuzzing موجود است. - -fork=1 -ignore_crashes=1
اجازه میدهد libFuzzer پس از یافتن یک کرش به فازینگ ادامه دهد. هرچند پرچم -fork رسماً تجربی محسوب میشود، ولی به طور گسترده استفاده میشود و امن است. - -close_fd_mask=3
استاندارد ورودی و خروجی را میبندد. اگر SUT خروجی زیادی تولید میکند، این کار باعث افزایش سرعت فازینگ میشود.
5.2 فازینگ چندهستهای (Multi-core fuzzing)
libFuzzer بهطور ساده از فازینگ چندهستهای پشتیبانی میکند. میتوان تعداد کمپینها و سطح موازیسازی را با پرچمهای زیر کنترل کرد:
- -jobs=n: تعداد کمپینهای فازینگ که بهصورت متوالی اجرا میشوند.
برای مثال، با تنظیم -jobs=10، فازر ۱۰ کمپین پشت سر هم اجرا میکند و هر کمپین جدید پس از پیدا شدن یک کرش شروع میشود. - -workers=m: تعداد فرآیندهای موازی برای اجرای کمپینها.
با تنظیم -workers=2، کمپینها با دو فرآیند بهصورت موازی اجرا میشوند. تست کیسهایی که در طول فازینگ پیدا میشوند بین کمپینها به اشتراک گذاشته میشوند. اگر بخواهید اشتراکگذاری را غیرفعال کنید، میتوانید از پرچم زیر استفاده کنید:
-reload=0
پرچمهای -jobs و -workers را میتوان با پرچم -fork که در بخش Usage معرفی شد، ترکیب کرد.
برای مثال، با تنظیم تعداد workers و jobs روی ۴ و فعال کردن forking، libFuzzer بهصورت مداوم با دو فرآیند فازینگ انجام میدهد:
./fuzz -jobs=4 -workers=4 -fork=1 -ignore_crashes=1
بهصورت جایگزین، میتوان از ویژگی فورکینگ (forking) libFuzzer نیز استفاده کرد:
./fuzz -fork=4 -ignore_crashes=1
5.3 توصیه در مورد multi-core fuzzing
توصیه میشود از فلگهای jobs=4 workers=4- به جای -fork=4 استفاده کنید، زیرا ویژگی forking رسماً تجربی است. با این حال، اگر فازینگ چندهستهای برای شما اولویت دارد، بهتر است به فازرهای قدرتمندتری مانند AFL++، Hongfuzz یا LibAFL مراجعه کنید.
5.4 AddressSanitizer (معروف به ASan)
ASan به شناسایی خطاهای حافظه کمک میکند که در غیر این صورت ممکن است نادیده گرفته شوند. برای مقدمهای کلی درباره ASan، به مستندات AddressSanitizer مراجعه کنید.
به عنوان مثال، heap buffer overflow زیر معمولاً بدون ASan قابل شناسایی نیست:
- اگرچه به بافر تخصیص داده شده خارج از محدوده دسترسی پیدا میکنیم، در عمل حافظهای که به آن دسترسی پیدا میکنیم (که ممکن است بخشی از metadata تخصیص دیگری باشد) هنوز در فضای پروسه نگاشته شده است، بنابراین برنامه با segmentation fault کرش نمیکند.
ASan این نوع خطاها را شناسایی کرده و به توسعهدهنده هشدار میدهد، حتی زمانی که کرش آشکاری رخ نمیدهد.
محتوی زیر را در فایلی با نام main_asan.cc به ذخیره کنید.
#include
#include
#include
void check_buf(char *buf, size_t buf_len) {
char *last;
if(buf_len > 0 && buf[0] == 'a') {
if(buf_len > 1 && buf[1] == 'b') {
if(buf_len > 2 && buf[2] == 'c') {
last = (char*)malloc(1 * sizeof(char)); // Allocate memory
last[0] = 'c'; // Write the character 'c'
last[1] = '\0'; // Write terminating null byte. A heap-buffer overflow is happening here!
printf("%s", last); // Print the string
free(last); // Free allocated memory
}
}
}
}
#ifndef NO_MAIN
int main() {
char target[] = "123";
size_t len = strlen(target);
check_buf(target, len);
return 0;
}
#endif // NO_MAIN
در شکل بالا موارد زیر را مشاهده میکنید:
در فایل main_asan.cc: مثال از باگی را که با فلگ ASan قابل شناسایی است. در این برنامه، در خط ۹ از check_buf یک دسترسی خارج از محدوده (out-of-bounds) رخ میدهد، زیرا تنها یک بایت حافظه تخصیص داده شده است، اما حداقل ۲ بایت نوشته میشود.
بدون ASan، این خطا معمولاً آشکار نمیشود و برنامه ممکن است بدون کرش اجرا شود، اما ASan آن را شناسایی و گزارش میکند. محتوای زیر را در فایلی با نام harness.cc به ذخیره کنید.
#include
#include
#include
void check_buf(char *buf, size_t buf_len);
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
check_buf((char*) data, size); // Invoke the SUT; in our case that is the check_buf function
return 0; // Return 0, which means that the test case was processed correctly
}
برای فعالسازی ASan هنگام استفاده از libFuzzer، فلگ -fsanitize=address را به کامپایلر بدهید. همچنین باید _FORTIFY_SOURCE را غیرفعال کنید (توجه داشته باشید که آندرلاین پیشرو مهم است)، زیرا ممکن است توزیع شما این فلگ را بهصورت پیشفرض فعال کند و این میتواند باعث ایجاد نتایج مثبت و منفی کاذب شود، زیرا توابع تقویتشده توسط ASan ابزارسنجی نمیشوند.
برای مثال، برای استفاده از ASan جهت پیدا کردن باگ خرابشدن حافظه در main_asan.cc، فلگ -fsanitize=address- را هنگام کامپایل اضافه کنید:
clang++ -DNO_MAIN -g -O2 -fsanitize=fuzzer -fsanitize=address harness.cc main_asan.cc -U_FORTIFY_SOURCE -o fuzz
هنگام اجرای fuzzer با ASan، با یک کرش (Crash) مواجه خواهید شد، همانطور که در زیر نشان داده شده است.
==1276163==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000c4ab1 at pc 0x55555568631b bp 0x7fffffffda10 sp 0x7fffffffda08
WRITE of size 1 at 0x6020000c4ab1 thread T0
#0 0x55555568631a in check_buf(char*, unsigned long) /root/handbook/libfuzzer/main_asan.cc:13:25
#1 0x5555556860bf in LLVMFuzzerTestOneInput /root/handbook/libfuzzer/harness.cc:7:3
(...)
(BuildId: b171fea7226b2f316f8138a7947857763d78aa1d)
0x6020000c4ab1 is located 0 bytes after 1-byte region [0x6020000c4ab0,0x6020000c4ab1)
allocated by thread T0 here:
#0 0x555555648142 in malloc (/root/handbook/libfuzzer/fuzz+0xf4142) (BuildId: b171fea7226b2f316f8138a7947857763d78aa1d)
#1 0x55555568621f in check_buf(char*, unsigned long) /root/handbook/libfuzzer/main_asan.cc:11:31
#2 0x5555556860bf in LLVMFuzzerTestOneInput /root/handbook/libfuzzer/harness.cc:7:3
(...)
SUMMARY: AddressSanitizer: heap-buffer-overflow /root/handbook/libfuzzer/main_asan.cc:13:25 in check_buf(char*, unsigned long)
Shadow bytes around the buggy address:
0x6020000c4800: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x6020000c4880: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fd
0x6020000c4900: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fa
0x6020000c4980: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x6020000c4a00: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
=>0x6020000c4a80: fa fa 03 fa fa fa[01]fa fa fa fa fa fa fa fa fa
0x6020000c4b00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6020000c4b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6020000c4c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6020000c4c80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6020000c4d00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==1276163==ABORTING
در تصویر فوق مثال خروجی فازر را مشاهده میکنید. به ASan در خط اول توجه کنید که علت کرش را توضیح میدهد:
AddressSanitizer: heap-buffer-overflow
5.5 مثالهای واقعی
5.5.1 libpng
کتابخانه libpng یک کتابخانه متنباز است که برای خواندن و نوشتن فایلهای تصویری PNG (Portable Network Graphics) استفاده میشود. فازینگ این پارسر مفید است، زیرا اغلب در موقعیتهایی بهکار میرود که ورودیهای غیرقابل اعتماد پردازش میشوند. بنابراین هر باگی در پارسر میتواند منجر به مشکلات امنیتی شود.
اگر قصد فازینگ پروژههای C را دارید که کتابخانههای استاتیک تولید میکنند، میتوانید از این دستورالعمل پیروی کنید:
- فایل INSTALL موجود در کد پروژه (یا مستندات مناسب دیگر) را مطالعه کنید تا نحوه ایجاد یک کتابخانه استاتیک را بیابید.
- کامپایلر را روی Clang تنظیم کنید و در هنگام کامپایل، فلگهای اضافی را به کامپایلر بدهید.
- کتابخانه استاتیک را بسازید و فلگ -fsanitize=fuzzer-no-link را به کامپایلر C بدهید. این فلگ، ابزارسنجی مرتبط با فازینگ را فعال میکند بدون اینکه موتور فازینگ لینک شود. Runtime، که شامل نماد main است، بعداً هنگام استفاده از فلگ -fsanitize=fuzzer لینک میشود. مرحله ساخت، یک کتابخانه استاتیک ایجاد میکند که در ادامه آن را $static_library مینامیم. علاوه بر این، فلگ -fsanitize=address را نیز اضافه کنید تا ASan فعال شده و خرابیهای حافظه شناسایی شوند.
- کتابخانه استاتیک ساختهشده در مرحله ۳ را پیدا کنید و دستور زیر را اجرا کنید:
clang++ -fsanitize=fuzzer -fsanitize=address $static_library harness.cc -o fuzz
- با اجرای دستور زیر میتوانید فازینگ را شروع کنید:
./fuzz
بیایید این دستورالعملها را روی کتابخانه معروف libpng اجرا کنیم. ابتدا، کد منبع را دریافت میکنیم:
curl -L -O https://downloads.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.xz
tar xf libpng-1.6.37.tar.xz
cd libpng-1.6.37/
قبل از اینکه بتوانیم libpng را کامپایل کنیم، باید وابستگیهای آن را نصب کنیم:
apt install zlib1g-dev
export CC=clang CFLAGS="-fsanitize=fuzzer-no-link -fsanitize=address" # Set C compiler and the flag for fuzzing
export CXX=clang++ CXXFLAGS="$CFLAGS" # Set C++ compiler and use C flags
./configure --enable-shared=no # Configure to compile a static library
make # Run compilation
بهصورت پیشفرض، اسکریپت پیکربندی سطح بهینهسازی را روی -O2 قرار میدهد، که همان چیزی است که در بخش [16]Compile a Fuzz test توصیه کردهایم.
توجه داشته باشید که بسته به محیط فازینگ شما، ممکن است لازم باشد وابستگیهای ناقص را نصب کنید تا کامپایل موفقیتآمیز انجام شود. برای مثال، در یک نصب ساده از Ubuntu، ممکن است نیاز باشد بسته zlib1g-dev را همانطور که در بالا توضیح داده شد، نصب کنید.
سپس، یک harness را از GitHub دانلود میکنیم. معمولاً باید خودتان یک harness بنویسید. با این حال، برای این مثال، یک harness موجود از نویسندگان libpng کافی است.
curl -O https://raw.githubusercontent.com/glennrp/libpng/f8e5fa92b0e37ab597616f554bee254157998227/contrib/oss-fuzz/libpng_read_fuzzer.cc
از اینجا، یک corpus آماده میکنیم تا کار یافتن باگها توسط fuzzer سادهتر شود. این مرحله اختیاری است، زیرا libFuzzer میتواند از یک corpus خالی شروع کند. با این حال، مفید است که یک corpus شامل ورودیهای واقعی آماده کنیم تا fuzzer مجبور نباشد از صفر شروع کند. شروع با یک فایل PNG معتبر، همانطور که در زیر نشان داده شده، بهطور قابل توجهی اثر بخشی فازینگ را افزایش میدهد.
mkdir corpus/
curl -o corpus/input.png https://raw.githubusercontent.com/glennrp/libpng/acfd50ae0ba3198ad734e5d4dec2b05341e50924/contrib/pngsuite/iftp1n3p08.png
همچنین یک dictionary برای فرمت PNG دانلود میکنیم تا fuzzer بهتر هدایت شود. یک dictionary به fuzzer اطلاعات اولیهای درباره ساختار فایل میدهد، مانند اینکه PNG از کدام magic bytes استفاده میکند.
curl -O https://raw.githubusercontent.com/glennrp/libpng/2fff013a6935967960a5ae626fc21432807933dd/contrib/oss-fuzz/png.dict
در نهایت، libpng ابزارسنجیشده، harness و runtime libFuzzer را با هم لینک میکنیم.
$CXX -fsanitize=fuzzer -fsanitize=address libpng_read_fuzzer.cc .libs/libpng16.a -lz -o fuzz
کمپین fuzzing را میتوان با اجرای دستور زیر راهاندازی کرد:
./fuzz -close_fd_mask=3 -dict=./png.dict corpus/
6. پروژه مبتنی بر CMake
فرض کنیم از CMake برای ساخت برنامهای که در مقدمه ذکر شد استفاده میکنیم. یک هدف (target) در CMake اضافه میکنیم که فایلهای main.cc و harness.cc را بسازد و این هدف را با libFuzzer لینک کند. توجه داشته باشید که ما تابع main را با فلگ NO_MAIN مستثنی کردهایم؛ در غیر این صورت، برنامه دو تابع main خواهد داشت، زیرا libFuzzer نیز یکی فراهم میکند.
project(BuggyProgram)
cmake_minimum_required(VERSION 3.0)
add_executable(buggy_program main.cc)
add_executable(fuzz main.cc harness.cc)
target_compile_definitions(fuzz PRIVATE NO_MAIN=1)
target_compile_options(fuzz PRIVATE -g -O2 -fsanitize=fuzzer)
target_link_libraries(fuzz -fsanitize=fuzzer)
در تصویر CMakeLists: نمونه فایل CMake برای کامپایل یک برنامه و فازر مربوط به آن را مشاهده میکنید. میتوان پروژه را با استفاده از دستورات زیر ساخت:
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .
cmake --build .
کمپین fuzzing را میتوان با اجرای دستور زیر راهاندازی کرد:
./fuzz