بخش بیست و نهم بافر اورفلو
Binary Only Fuzzing
وقتی سورس کد نداریم
تا اینجا Fuzzing رو بیشتر روی برنامههایی دیدیم که سورس کدشون دستمون بود
ولی توی Reverse Engineering و Fuzzing یه عالمه وقت پیش میاد که فقط یه فایل اجرایی داریم
مثلا
program.exe
یا
program
نه سورس داریم
نه مستندات درست و حسابی
نه حتی دقیقا میدونیم داخل برنامه چه خبره
اینجاست که باید اول برنامه رو بررسی کنیم
بفهمیم ورودی رو از کجا میگیره
بفهمیم اون ورودی چطوری داخل برنامه حرکت میکنه
بعد همون مسیر رو برای Fuzzing آماده کنیم
مرحله اول
باز کردن Binary
اول برنامه رو داخل ابزارهایی مثل Ghidra یا IDA باز میکنیم
هدفمون این نیست که از همون اول کل برنامه رو بفهمیم
قرار نیست بشینیم میلیونها Instruction رو یکی یکی بخونیم
اول دنبال جاهایی میگردیم که برنامه با داده خارجی سر و کار داره
مثلا توی Linux و برنامههای مختلف ممکنه با چیزهایی مثل این برخورد کنیم
read
recv
fread
fgets
scanf
توی Windows هم چیزهایی مثل
ReadFile
recv
fgets
میتونن سرنخ خوبی باشن
ولی اینجا یه نکته مهم داریم
صرفا دیدن اسم یک تابع به این معنی نیست که همونجا ورودی اصلی برنامه قرار داره
ممکنه داده از چندتا تابع رد بشه
ممکنه Wrapper داشته باشیم
ممکنه Parser جداگانه وجود داشته باشه
پس باید مسیر داده رو دنبال کنیم
مرحله دوم
دنبال کردن مسیر ورود داده
فرض کنید داخل Decompiler به چیزی شبیه این رسیدیم
buffer = malloc(256);
read(fd, buffer, size);
process_data(buffer);
اینجا یه مسیر خیلی خوب داریم
External Input
↓
read
↓
buffer
↓
process_data
یعنی داده خارجی وارد برنامه شده
داخل یک Buffer قرار گرفته
بعد رفته داخل تابعی که احتمالا اون داده رو پردازش میکنه حالا دیگه فقط نمیدونیم برنامه ورودی داره بلکه یه سرنخ داریم که ورودی بعد از وارد شدن به برنامه کجا میره
مرحله سوم
بفهمیم ورودی چه شکلیه
این قسمت خیلی مهمه
چون Fuzzer نمیتونه همینطوری یه مشت داده تصادفی بریزه توی برنامه و انتظار داشته باشه برنامه هم با خوشحالی همه رو پردازش کنه
برنامه معمولا انتظار داره ورودی یه ساختار خاص داشته باشه
مثلا ممکنه برنامه یک فایل بخونه
program sample.dat
یا ورودی رو از stdin بگیره
program < sample.dat
یا اصلا ورودی از شبکه بیاد
Client
↓
Network
↓
recv
↓
Parser
پس باید بفهمیم
ورودی از کجا میاد
چه فرمتی داره
کجا Parse میشه
و چه قسمتهایی از برنامه با اون کار میکنن این اطلاعات برای Fuzzing خیلی مهمن چون هرچی بیشتر ساختار ورودی رو بفهمیم
Fuzzer
هم میتونه ورودیهای مفیدتری تولید کنه
مرحله چهارم
ساختن Seed
حالا فرض کنید فهمیدیم برنامه یک فایل با فرمت خاص خودش رو میخونه
مثلا:
sample.dat
اول یه فایل سالم و کوچک پیدا میکنیم
به این میگیم
Seed
بعد Fuzzer همین Seed رو تغییر میده
Seed
↓
Mutation
↓
Program
↓
New Path
یا
Crash
`مثلا یه بخش از فایل تغییر میکنه
بعد برنامه اجرا میشه
Fuzzer بررسی میکنه آیا برنامه وارد مسیر جدیدی شده یا نه
یا مثلا Crash کرده یا نه
چرا اینجا مهمه
اینجا دقیقا جاییه که این به کمک Fuzzing میاد
Fuzzer
به تنهایی همیشه نمیتونه بفهمه ساختار ورودی چیه
ولی ما میتونیم با بررسی Assembly و Decompiler سرنخهایی پیدا کنیم
مثلا فرض کن توی Assembly میبینیم:
cmp byte ptr [rax], 48h
jne invalid
اینجا میبینیم برنامه داره یک Byte مشخص رو بررسی میکنه
یعنی احتمالا یه بخش خاص از ورودی برای برنامه اهمیت داره
بعد یه جای دیگه میبینیم
cmp dword ptr [rax+4], 100h
اینجا برنامه داره مقداری رو در Offset چهار بررسی میکنه
حالا اگه چندتا از این Checkها رو کنار هم بذاریم
کم کم میتونیم حدس بزنیم ورودی چه ساختاری داره
مثلا ممکنه به چیزی شبیه این برسیم:
Offset 0
↓
Magic
Offset 4
↓
Size
Offset 8
↓
Flags
Offset 12
↓
Data
یعنی بدون داشتن Source Code هم میتونیم از روی رفتار Binary کم کم ساختار Input Format رو دربیاریم
البته یه نکته مهم
این چیزی که از Assembly درمیاریم همیشه صددرصد قطعی نیست
ممکنه Compiler کد رو Optimize کرده باشه
ممکنه ساختار پیچیدهتر باشه
ممکنه چندتا مسیر مختلف برای Parse کردن ورودی وجود داشته باشه
پس باید فرضیههامون رو با اجرای واقعی برنامه و Debugging بررسی کنیم
یک نکته خیلی مهم
در Binary Only Fuzzing معمولا سه مرحله خیلی ارزشمند داریم
Input Discovery
↓
Program Analysis