بخش سی ام بافر اورفلو
Crash Triage
تا اینجا یاد گرفتیم با Fuzzing کلی Crash پیدا کنیم
ولی اینجا یه مشکل خیلی بزرگ داریم
Fuzzer
ممکنه برای فقط یک باگ صدها یا حتی هزاران Crash مختلف تولید کنه
پس بعد از Fuzzing باید بشینیم بفهمیم
کدوم Crash واقعا مهمه
کدوم Crash ها در اصل مربوط به یه باگ مشترکن
Crash
دقیقا کجای برنامه اتفاق افتاده
و مهمتر از همه
علت اصلی Crash چی بوده
به این مرحله میگیم Crash Triage
یعنی مرتب کردن و بررسی Crash ها تا بفهمیم کدوم ها واقعا ارزش بررسی دارن
اولین چیزی که بررسی میکنیم
فرض کنید Fuzzer با این ورودی ها باعث Crash شده
AAAA
BBBBBBBB
test123
AAAAAAAAAAAAAAAA
hello_world
نباید سریع نتیجه بگیریم که اینجا 5 تا باگ داریم
ممکنه هر پنج ورودی در اخر به یک دستور مشخص از برنامه برسن
مثلا
0x401234: mov byte ptr [rax], dl
و همین دستور باعث Crash بشه
پس چیزی که برای ما مهمه فقط فایل ورودی نیست
باید ببینیم Crash کجا اتفاق افتاده و تحت چه شرایطی اتفاق افتاده
یعنی
Input
↓
Program
↓
Fault
↓
Where did it happen
↓
Why did it happen
Faulting Instruction
یکی از مهم ترین چیزایی که موقع بررسی Crash باید پیدا کنیم
"Faulting Instruction" هست
یعنی دستوری که CPU موقع اتفاق افتادن Exception یا Fault در حال اجرای اون بوده
مثلا
Program received signal SIGSEGV
RIP = 0x401234
0x401234:
mov byte ptr [rax], dl
اینجا فقط اینکه برنامه "SIGSEGV" داده برامون کافی نیست
باید بپرسیم
RAX
چه مقداری داشته
چرا این مقدار نامعتبر بوده
این مقدار از کجا اومده
مثلا ممکنه "RAX" به یه آدرس نامعتبر اشاره کنه
پس به جای اینکه فقط بگیم
Program crashed
باید بریم یه مرحله عمیقتر
Crash
↓
Faulting Instruction
↓
Invalid Register / Memory
↓
Why did this happen
Crash خودش علت نیست
Crash نتیجه یک اتفاق قبلیه
Stack Trace
بعد میریم سراغ "Stack Trace"
مثلا
#0 0x401234 in process_data()
#1 0x401567 in handle_input()
#2 0x401789 in main()
یعنی مسیر رسیدن برنامه به Crash تقریبا این بوده
main()
↓
handle_input()
↓
process_data()
↓
CRASH
این اطلاعات برای ما خیلی مهمه
چون حالا میدونیم باید اول کدوم قسمت برنامه رو بررسی کنیم
مثلا میتونیم داخل IDA یا Ghidra بریم سراغ "process_data()" و ببینیم دقیقا قبل از Crash چه اتفاقی افتاده
Crash Deduplication
حالا فرض کنید Fuzzer هزار تا Crash به ما داده
اگر برای هر Crash اطلاعاتی مثل این داشته باشیم
Signal
Faulting Address
Faulting Instruction
Stack Trace
میتونیم Crashهای شبیه به هم رو کنار هم قرار بدیم
مثلا
Crash 1 → process_data → 0x401234
Crash 2 → process_data → 0x401234
Crash 3 → process_data → 0x401234
Crash 4 → parse_packet → 0x402010
احتمالا Crash های اول تا سوم یه خانواده هستن
Crash 1
Crash 2
Crash 3
↓
Crash Family A
ولی Crash چهارم مسیر متفاوتی داره
Crash 4
↓
Crash Family B
البته یه نکته مهم اینجاست
برابر بودن "Faulting Address" به تنهایی ثابت نمیکنه که دو Crash حتما یک باگ هستن
ممکنه چند مسیر متفاوت به یک دستور برسن
پس برای Deduplication معمولا چند تا نشونه رو کنار هم بررسی میکنیم
مثل
Faulting Instruction
Call Stack
Registers
Memory State
Crash Type
Input Behavior
هدف اینه که Crashهای تکراری رو از Crashهای واقعاً متفاوت جدا کنیم
Sanitizerها اینجا خیلی کمک میکنن
مثلاً اگه برنامه با "AddressSanitizer" یا همون "ASan" اجرا شده باشه
به جای اینکه فقط یه پیام ساده مثل این ببینیم
Segmentation fault
ممکنه اطلاعات خیلی بیشتری داشته باشیم
مثلا
ERROR: AddressSanitizer:
heap-buffer-overflow
READ of size 4
#0 process_data
#1 handle_input
#2 main
ASan
میتونه اطلاعات بیشتری درباره نوع خطا و محل دسترسی غیرمجاز بده و در بعضی خطاها اطلاعات مربوط به allocation و محل access رو هم گزارش کنه
در نتیجه مسیر بررسی میتونه از این
Crash
برسه به
Crash
↓
Error Type
↓
Invalid Access
↓
Stack Trace
↓
Suspicious Function
↓
Root Cause
یعنی به جای اینکه فقط بدونیم برنامه از بین رفته
میفهمیم تقریبا چه اتفاقی باعث از بین رفتت برنامه شده
یک مثال ساده
این برنامه رو ببینید
#include <stdio.h>
void process(int index) {
int data[4] = {10, 20, 30, 40};
printf("%d\n", data[index]);
}
int main(void) {
process(10);
return 0;
}
اینجا آرایه فقط 4 عضو داره
data[0]
data[1]
data[2]
data[3]
ولی برنامه میخواد اینو بخونه
data[10]
پس برنامه داره خارج از محدوده آرایه به حافظه دسترسی پیدا میکنه