فازینگ (Fuzzing) نوعی تست خودکار است که به طور مداوم ورودیهای یک برنامه را دستکاری میکند تا باگها را پیدا کند. فازینگ در زبان Go از هدایت پوشش (Coverage Guidance) استفاده میکند تا به طور هوشمندانه در کدی که در حال فاز شدن است حرکت کند و خرابیها را پیدا کرده و به کاربر گزارش دهد. از آنجا که این روش میتواند به موارد خاصی (Edge Cases) دست پیدا کند که انسانها اغلب آنها را از دست میدهند، تست فازینگ میتواند به ویژه برای یافتن اکسپلویتهای امنیتی و آسیبپذیریها بسیار ارزشمند باشد.
نگارش تستهای فازینگ (Writing Fuzz Tests)
الزامات (Requirements)
در زیر قوانینی آورده شده است که تستهای فازینگ باید از آنها پیروی کنند:
-
یک تست فازینگ باید تابعی با نامی شبیه به
FuzzXxxباشد که فقط یک*testing.Fرا میپذیرد و هیچ مقدار بازگشتی ندارد. -
تستهای فازینگ برای اجرا شدن باید در فایلهایی با پسوند
*_test.goقرار داشته باشند. -
یک هدف فاز (Fuzz Target) باید یک فراخوانی متد به
(*testing.F).Fuzzباشد که یک*testing.Tرا به عنوان اولین پارامتر و به دنبال آن آرگومانهای فازینگ را میپذیرد. هیچ مقدار بازگشتی وجود ندارد. -
باید دقیقاً یک هدف فاز به ازای هر تست فازینگ وجود داشته باشد.
-
تمام ورودیهای پیکره اولیه (Seed Corpus) باید دارای انواعی دقیقاً مشابه با آرگومانهای فازینگ و به همان ترتیب باشند. این موضوع برای فراخوانیهای
(*testing.F).Addو هر فایل پیکرهای در دایرکتوریtestdata/fuzzمربوط به تست فازینگ صادق است. -
آرگومانهای فازینگ فقط میتوانند انواعی از موارد زیر باشند:
-
string,[]byte -
int,int8,int16,int32/rune,int64 -
uint,uint8/byte,uint16,uint32,uint64 -
float32,float64 -
bool
-
پیشنهادات (Suggestions)
در زیر پیشنهاداتی آورده شده است که به شما کمک میکند بیشترین بهره را از فازینگ ببرید:
-
اهداف فاز باید سریع و قطعی (Deterministic) باشند تا موتور فازینگ بتواند به طور کارآمد کار کند و خرابیهای جدید و پوشش کد به راحتی قابل بازتولید باشند.
-
از آنجا که هدف فاز به صورت موازی در چندین کارگر (Workers) و به ترتیب غیرقطعی فراخوانی میشود، وضعیت یک هدف فاز نباید فراتر از پایان هر فراخوانی باقی بماند (Persist نکند) و رفتار یک هدف فاز نباید به وضعیت سراسری (Global State) وابسته باشد.
اجرای تستهای فازینگ (Running Fuzz Tests)
دو حالت برای اجرای تست فازینگ شما وجود دارد: به عنوان یک تست واحد (تست پیشفرض go test)، یا همراه با فازینگ (go test -fuzz=FuzzTestName).
تستهای فازینگ به طور پیشفرض بسیار شبیه به یک تست واحد اجرا میشوند. هر ورودی پیکره اولیه در برابر هدف فاز تست خواهد شد و هر گونه خرابی قبل از خروج گزارش میشود.
برای فعال کردن فازینگ، go test را با پرچم -fuzz اجرا کنید و یک عبارت منظم (Regex) که با یک تست فازینگ واحد مطابقت دارد را ارائه دهید. به طور پیشفرض، تمام تستهای دیگر در آن پکیج قبل از شروع فازینگ اجرا خواهند شد. این کار برای اطمینان از این است که فازینگ هیچ مسئلهای را که قبلاً توسط یک تست موجود شناسایی میشد، گزارش نکند.
💡 نکته: این که چه مدت فازینگ را اجرا کنید به عهده خودتان است. این احتمال وجود دارد که اجرای فازینگ در صورت پیدا نکردن هیچ خطایی به طور نامحدود ادامه یابد. در آینده پشتیبانی برای اجرای مداوم این تستهای فازینگ با استفاده از ابزارهایی مانند OSS-Fuzz فراهم خواهد شد (شماره مسئله #50192 را ببینید).
⚠️ نکته: فازینگ باید روی پلتفرمی اجرا شود که از ابزار دقیق پوشش (Coverage Instrumentation) پشتیبانی میکند (در حال حاضر AMD64 و ARM64) تا پیکره بتواند با پیشرفت اجرا به طور معناداری رشد کند و در طول فازینگ کد بیشتری پوشش داده شود.
خروجی خط فرمان (Command Line Output)
در حالی که فازینگ در جریان است، موتور فازینگ ورودیهای جدیدی تولید کرده و آنها را در برابر هدف فاز ارائه شده اجرا میکند. به طور پیشفرض، کار را تا زمانی که یک ورودی خرابی پیدا شود یا کاربر فرآیند را لغو کند (مثلاً با Ctrl+C) ادامه میدهد.
خروجی چیزی شبیه به این خواهد بود:
~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok foo 12.692s
خطوط اول نشان میدهند که «پوشش مبنا» (Baseline Coverage) قبل از شروع فازینگ جمعآوری میشود. برای جمعآوری پوشش مبنا، موتور فازینگ هم پیکره اولیه و هم پیکره تولید شده را اجرا میکند تا مطمئن شود هیچ خطایی رخ نداده است و پوشش کدی که پیکره موجود از قبل فراهم میکند را درک کند.
خطوط بعدی بینشی نسبت به اجرای فعال فازینگ فراهم میکنند:
-
elapsed: مقدار زمانی که از شروع فرآیند سپری شده است. -
execs: کل تعداد ورودیهایی که در برابر هدف فاز اجرا شدهاند (همراه با میانگینexecs/secاز آخرین خط گزارش). -
new interesting: کل تعداد ورودیهای «جالب» (interesting) که در طول این اجرای فازینگ به پیکره تولید شده اضافه شدهاند (همراه با کل اندازه کل پیکره).
ورودی شکستخورده (Failing Input)
ممکن است خرابی به چند دلیل در طول فازینگ رخ دهد:
-
یک
panicدر کد یا تست رخ داده است. -
هدف فاز متد
t.Failرا فراخوانی کرده است، چه به طور مستقیم و چه از طریق متدهایی مانندt.Errorیاt.Fatal. -
یک خطای غیرقابل بازیابی رخ داده است، مانند
os.Exitیا سرریز پشته (Stack Overflow). -
تکمیل هدف فاز زمان زیادی طول کشیده است. در حال حاضر، مهلت زمانی (Timeout) برای اجرای یک هدف فاز ۱ ثانیه است. این ممکن است به دلیل بنبست (Deadlock)، حلقه بینهایت یا رفتار مورد انتظار در کد شکست بخورد. این یکی از دلایلی است که پیشنهاد میشود هدف فاز شما سریع باشد.
اگر خطایی رخ دهد، موتور فازینگ تلاش خواهد کرد تا ورودی را به کوچکترین و خواناترین مقدار ممکن برای انسان که همچنان خطا تولید میکند، کوچکسازی (Minimize) کند.
پس از اتمام کوچکسازی، پیام خطا ثبت میشود و خروجی با چیزی شبیه به این پایان مییابد:
Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
To re-run:
go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL foo 0.839s
موتور فازینگ این ورودی شکستخورده را در پیکره اولیه برای آن تست فازینگ مینویسد و اکنون به طور پیشفرض با go test اجرا خواهد شد و پس از رفع باگ به عنوان یک تست رگرسیون (Regression Test) عمل میکند.
تنظیمات سفارشی (Custom Settings)
تنظیمات پیشفرض دستور go باید برای اکثر موارد استفاده از فازینگ کار کند. بنابراین معمولاً اجرای فازینگ در خط فرمان به این صورت خواهد بود:
$ go test -fuzz={FuzzTestName}
با این حال، دستور go چند تنظیمات مفید دیگر را هنگام اجرای فازینگ فراهم میکند:
-
-fuzztime: کل زمان یا تعداد تکرارهایی که هدف فاز قبل از خروج اجرا خواهد شد (پیشفرض: نامحدود). -
-fuzzminimizetime: زمان یا تعداد تکرارهایی که هدف فاز در طول هر تلاش کوچکسازی اجرا خواهد شد (پیشفرض:60sec). -
-parallel: تعداد فرآیندهای فازینگ که به طور همزمان اجرا میشوند (پیشفرض:$GOMAXPROCS).
فرمت فایل پیکره (Corpus File Format)
فایلهای پیکره در یک فرمت ویژه رمزگذاری میشوند. این فرمت هم برای پیکره اولیه و هم برای پیکره تولید شده یکسان است.
مثالی از یک فایل پیکره:
go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)
سطر اول برای مطلع کردن موتور فازینگ از نسخه رمزگذاری فایل استفاده میشود. هر یک از سطرهای بعدی مقادیری هستند که ورودی پیکره را تشکیل میدهند. آسانترین راه برای مشخص کردن مقادیر پیکره اولیه، استفاده از متد (*testing.F).Add است:
f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))
منابع (Resources)
-
آموزش (Tutorial):
-
برای بررسی عمیق مفاهیم جدید، آموزش فازینگ با Go را امتحان کنید.
-
برای یک آموزش مقدماتی کوتاهتر در مورد فازینگ با Go، لطفاً پست وبلاگ را مطالعه کنید.
-
-
مستندات (Documentation):
-
مستندات پکیج
testingنوعtesting.Fرا توصیف میکند که هنگام نوشتن تستهای فازینگ استفاده میشود. -
مستندات پکیج
cmd/goپرچمهای مرتبط با فازینگ را توصیف میکند.
-
-
جزئیات فنی (Technical details):
-
پیشنویس طراحی (Design draft)
-
پروپوزال (Proposal)
-
واژهنامه تخصصی (Glossary)
-
corpus entry (ورودی پیکره): ورودیای در پیکره که میتواند در طول فازینگ استفاده شود. این میتواند یک فایل با فرمت خاص یا یک فراخوانی به
(*testing.F).Addباشد. -
coverage guidance (هدایت پوشش): روشی از فازینگ که از توسعه در پوشش کد استفاده میکند تا تعیین کند کدام ورودیهای پیکره برای استفاده در آینده ارزش نگه داشتن دارند.
-
failing input (ورودی شکستخورده): ورودی شکستخورده، ورودیای از پیکره است که هنگام اجرا در برابر هدف فاز، باعث بروز خطا یا پنیک (Panic) میشود.
-
fuzz target (هدف فاز): تابع مربوط به تست فازینگ که برای ورودیهای پیکره و مقادیر تولید شده در هنگام فازینگ اجرا میشود. این تابع با ارسال آن به
(*testing.F).Fuzzبه تست فازینگ ارائه میشود. -
fuzz test (تست فازینگ): تابعی در یک فایل تست به فرمت
func FuzzXxx(*testing.F)که میتواند برای فازینگ استفاده شود. -
fuzzing (فازینگ): نوعی تست خودکار که به طور مداوم ورودیهای یک برنامه را دستکاری میکند تا مواردی مانند باگها یا آسیبپذیریهایی را که ممکن است کد نسبت به آنها مستعد باشد، پیدا کند.
-
fuzzing arguments (آرگومانهای فازینگ): انواعی که به هدف فاز ارسال میشوند و توسط جهشدهنده (Mutator) تغییر میکنند.
-
fuzzing engine (موتور فازینگ): ابزاری که فازینگ را مدیریت میکند؛ از جمله نگهداری پیکره، فراخوانی جهشدهنده، شناسایی پوشش جدید و گزارش خرابیها.
-
generated corpus (پیکره تولید شده): پیکرهای که توسط موتور فازینگ در طول زمان و هنگام فازینگ نگهداری میشود تا پیشرفت را پیگیری کند. این در مسیر
$GOCACHE/fuzzذخیره میشود. این ورودیها فقط در طول فازینگ استفاده میشوند. -
mutator (جهشدهنده): ابزاری که در طول فازینگ استفاده میشود و پیش از ارسال ورودیها به هدف فاز، آنها را به طور تصادفی دستکاری میکند.
-
package (پکیج): مجموعهای از فایلهای سورس در یک دایرکتوری که با هم کامپایل میشوند. بخش Packages را در مشخصات زبان Go ببینید.
-
seed corpus (پیکره اولیه): پیکرهای ارائهشده توسط کاربر برای یک تست فازینگ که میتواند برای هدایت موتور فازینگ استفاده شود. این از ورودیهای پیکره ارائهشده توسط فراخوانیهای
f.Addدر داخل تست فازینگ و فایلهای دایرکتوریtestdata/fuzz/{FuzzTestName}در داخل پکیج تشکیل شده است. این ورودیها به طور پیشفرض باgo testاجرا میشوند، چه فازینگ در حال اجرا باشد و چه نباشد. -
test file (فایل تست): فایلی با فرمت
xxx_test.goکه ممکن است حاوی تستها، بنچمارکها، مثالها و تستهای فازینگ باشد. -
vulnerability (آسیبپذیری): یک ضعف حساس به امنیت در کد که میتواند توسط یک مهاجم اکسپلویت شود.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات