بخش سی و یکم بافر اور فلو
Root Cause Analysis
پیدا کردن علت واقعی Crash
توی قسمت قبل یاد گرفتیم وقتی Fuzzer کلی Crash پیدا میکنه چطور اونها رو دستهبندی کنیم حالا میخوایم یک مرحله عمیقتر بشیم این بار فقط نمیخوایم بدونیم برنامه کجا Crash کرده
میخوایم بفهمیم
چرا Crash کرده
چون اینکه بگیم
Program crashed
خیلی کمک خاصی نمیکنه
تقریبا مثل اینه که ماشین خراب شده باشه و مکانیک فقط بگه
ماشین خراب شد
خب ممنون از این کشف بزرگ😂
Crash
خودش باگ نیست
فرض کنید Debugger اینو نشون بده
SIGSEGV
این یعنی برنامه به یک دسترسی نامعتبر به حافظه رسیده ولی هنوز نمیدونیم چرا این اتفاق افتاده
ممکنه علتش یکی از اینها باشه
Buffer Overflow
Use After Free
Out of Bounds Read
Null Pointer Dereference
پس باید از خود Crash عقب تر بریم
Crash
↓
What happened
↓
Why happened
↓
Root Cause
پیدا کردن دستور مشکل دار
فرض کنید Debugger این دستور رو نشون بده
mov eax,[rcx]
و دقیقاً همینجا برنامه Crash کنه
اینجا اولین سوال اینه
RCX چه مقداری داشته
مثلا
RCX = 0x0000000000000000
خب اینجا برنامه عملا داره از آدرس صفر چیزی بخونه پس مسیر میتونه این شکلی باشه
mov eax,[rcx]
↓
RCX = 0
↓
Invalid Memory Access
↓
Crash
ولی هنوز علت اصلی رو پیدا نکردیم
باید بفهمیم
RCX چرا صفر شده
بررسی Register ها
بعد از Crash وضعیت Registerها رو بررسی میکنیم
مثلا:
RAX = 0x00000001
RBX = 0x00000000
RCX = 0x00000000
RDX = 0x00000020
RIP = 0x401234
اینجا RIP به ما میگه CPU موقع Crash تقریبا کجا بوده
و RCX هم میتونه اطلاعات مهمی درباره آدرسی که قرار بوده استفاده بشه بده
ولی سوال اصلی هنوز همونه
RCX
↓
از کجا اومده
برگردیم عقب
فرض کنید قبل از Crash این دستورات رو داریم
mov rcx,[rbp-20h]
mov eax,[rcx]
پس RCX از اینجا مقدار گرفته
mov rcx,[rbp-20h]
حالا باید یک مرحله دیگه عقب بریم
و ببینیم
[rbp-20h]
چطور مقدار گرفته
مثلا ممکنه قبلتر چیزی شبیه این داشته باشیم
mov [rbp-20h], rdi
حالا میپرسیم
RDI از کجا اومده
و همینطور عقب میریم
Crash
↓
RCX
↓
[rbp-20h]
↓
RDI
↓
Function Argument
↓
Input
اینجا کمکم داریم به منشا مشکل نزدیک میشیم
مسیر کلی تحلیل
این مسیریه که موقع Root Cause Analysis زیاد باهاش کار میکنیم
Crash
↓
Faulting Instruction
↓
Register / Memory
↓
Previous Instruction
↓
Data Origin
↓
Root Cause
یعنی فقط به آخرین دستور نگاه نمیکنیم
دنبال این میگردیم که این وضعیت چطور به وجود اومده
اینجاست که Reverse Engineering واقعا خودش رو نشون میده
یک مثال ساده:
فرض کنید کد اینه
C
void process(char *input)
{
char buffer[8];
strcpy(buffer, input);
printf("%s", buffer);
}
حالا فرض کنید ورودی خیلی بزرگ باشه
input
↓
strcpy
↓
buffer
↓
Stack Corruption
↓
Crashممکنه برنامه کمی بعدتر مثلاً داخل
printf Crash کنهاگر فقط بگیم
Crash در printf
تحلیلمون ناقصه
چون ممکنه مشکل اصلی خیلی قبلتر ایجاد شده باشه
Root Cause کجاست
اینجا
buffer = 8 bytes
input = larger than 8 bytes
strcpy
↓
No bounds checking
↓
Stack Buffer Overflow
یعنی
strcpy بیشتر از ظرفیت buffer داده نوشتهو باعث خراب شدن حافظه شده
پس
Crash
≠
Root Cause
Crash
نتیجه نهایی اتفاقه
Root Cause
جاییه که مشکل واقعا ایجاد شده
یک مثال دیگه
فرض کنید Debugger اینو نشون بده
mov rax,[rbx]
و
RBX = 0
ممکنه بگیم
Null Pointer Dereference
ولی باز هم بهتره همینجا متوقف نشیم
باید بررسی کنیم
RBX
↓
چطور مقدار گرفته
↓
چرا NULL شده
↓
آیا باید قبلش بررسی میشده
↓
Root Cause
ممکنه متوجه بشیم یک تابع مقدار
NULL برگردونده ولی برنامه بدون بررسی اون رو استفاده کرده پس خود mov rax,[rbx] فقط جاییه که مشکل خودش رو نشون داده نه لزواما جایی که مشکل ایجاد شدهچرا این مرحله برای Fuzzing مهمه
فرض کنید Fuzzer اینو پیدا کرده
Crash found
input_0421.dat
خب حالا چی
هنوز نمیدونیم با چه چیزی طرفیم
ممکنه:
Crash
↓
Heap Buffer Overflow
یا
Crash
↓
Use After Free
یا
Crash
↓
Out of Bounds Read
یا
Crash
↓
Null Pointer Dereference
برای همین بعد از پیدا شدن Crash باید وارد مرحله تحلیل بشیم
Fuzzer
↓
Crash
↓
Debugger
↓
Root Cause Analysis
↓
Bug Classification
Root Cause و Crash رو قاطی نکنید