ReverseEngineering
1.32K subscribers
50 photos
11 videos
106 files
888 links
Download Telegram
بخش سی و یکم بافر اور فلو


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 رو قاطی نکنید
این دو تا رو همیشه جدا نگه دارید

Crash
↓
چه اتفاقی افتاد

ولی

Root Cause
↓
چرا این اتفاق افتاد

مثلا:

Crash
↓
SIGSEGV
↓
Invalid Memory Access

هنوز Root Cause مشخص نیست
ممکنه بعد از بررسی بفهمیم

Input
↓
Unsafe Copy
↓
Buffer Overflow
↓
Memory Corruption
↓
Crash

اینجا دیگه علت اصلی رو پیدا کردیم

از دید Reverse Engineer:

وقتی یک Crash دارید مدام این سوال‌ها رو از خودتون بپرسید

CPU
کجا Crash کرد

کدوم Instruction مشکل‌دار بود

کدوم Register یا Address درگیر بود

این مقدار از کجا اومده

چه داده‌ای باعث رسیدن به این وضعیت شده

اولین جایی که رفتار اشتباه ایجاد شده کجاست

آخرین سؤال از همه مهم‌تره

چون ممکنه Crash در یک نقطه اتفاق بیفته ولی Bug چند تابع قبل‌تر ایجاد شده باشه


وقتی یک Crash پیدا کردیم
مستقیم نمیگیم علت باگ رو فهمیدیم
اول محل Crash رو پیدا میکنیم
بعد Instruction مشکل‌دار رو بررسی میکنیم بعد Register ها و Memory رو نگاه میکنیم بعد مسیر داده رو به عقب دنبال میکنیم تا برسیم به جایی که مشکل واقعا ایجاد شده
یعنی

Crash
↓
Where
↓
How
↓
Why
↓
Root Cause

و این دقیقا تفاوت بین دیدن Crash و فهمیدن باگ هست

@reverseengine
ReverseEngineering
بخش سی و یکم بافر اور فلو Root Cause Analysis پیدا کردن علت واقعی Crash توی قسمت قبل یاد گرفتیم وقتی Fuzzer کلی Crash پیدا می‌کنه چطور اون‌ها رو دسته‌بندی کنیم حالا میخوایم یک مرحله عمیق‌تر بشیم این بار فقط نمی‌خوایم بدونیم برنامه کجا Crash کرده می‌خوایم…
Part 31 Buffer Overflow


Root Cause Analysis

Finding the Real Cause of a Crash

In the previous part, we learned how to classify crashes when a Fuzzer finds them. Now we want to go a step deeper. This time, we don't just want to know where the program crashed.

We want to understand.

Why it crashed.

Because saying.

Program crashed.

doesn't really help.

It's almost like a car broke down and the mechanic just said.

The car broke down.

Well, thanks for this great discovery.

Crash.

itself is not a bug.

Suppose the debugger shows this.

SIGSEGV.

This means that the program has reached an invalid memory access, but we still don't know why it happened.

It could be one of these.

Buffer Overflow.

Use After Free.

Out of Bounds Read.

Null Pointer Dereference.

So we have to go after the crash itself. Let's go

Crash
↓
What happened
↓
Why happened
↓
Root Cause

Find the problematic instruction

Suppose the Debugger shows this instruction

mov eax,[rcx]

and the program crashes right here

Here the first question is

What is the value of RCX

For example

RCX = 0x00000000000000000

Well here the program is actually reading something from address zero, so the path could be like this

mov eax,[rcx]
↓
RCX = 0
↓
Invalid Memory Access
↓
Crash

But we haven't found the root cause yet

We need to understand
Why RCX is zero

Checking the Registers
After the Crash, we check the status of the Registers

For example:
RAX = 0x000000001
RBX = 0x00000000
RCX = 0x00000000
RDX = 0x00000020
RIP = 0x401234

Here RIP tells us where the CPU was approximately at the time of the Crash
And RCX can also give us important information about the address that was supposed to be used
But the main question is still the same

RCX
↓
Where did it come from

Let's go back
Suppose we have these instructions before the Crash

mov rcx,[rbp-20h]

mov eax,[rcx]

So RCX got its value from here

mov rcx,[rbp-20h]

Now we need to go back one more step
And see

[rbp-20h]

How did it get its value
For example, we may have had something like this before

mov [rbp-20h], rdi

Now we ask
Where did RDI come from
And we go back like this

Crash
↓
RCX
↓
[rbp-20h]
↓
RDI
↓
Function Argument
↓
Input

Here we are getting closer to the source of the problem

General Analysis Path
This is the path we work with a lot during Root Cause Analysis

Crash
↓
Faulting Instruction
↓
Register / Memory
↓
Previous Instruction
↓
Data Origin
↓
Root Cause

That is, we do not just look at the last instruction
We look at how this situation arose

This is where Reverse Engineering really shows itself

A simple example:

Suppose the code is

C
void process(char *input)
{
char buffer[8];

strcpy(buffer, input);

printf("%s", buffer);

}

Now suppose the input is too large

input
↓
strcpy
↓
buffer
↓
Stack Corruption
↓
Crash

The program may crash later, for example, inside printf

If we just say

Crash in printf

Our analysis is incomplete

Because the main problem may have occurred much earlier

Where is the Root Cause

Here

buffer = 8 bytes

input = larger than 8 bytes

strcpy
↓
No bounds checking
↓
Stack Buffer Overflow

That is, strcpy wrote more data than the buffer capacity

and caused memory corruption

So

Crash
≠
Root Cause

Crash
is the final result

Root Cause

Where the problem actually occurred

Another example

Suppose the debugger shows

mov rax,[rbx]

and

RBX = 0

We may say

Null Pointer Dereference

But it is still better to stop here Let's sit

We need to check

RBX
↓
How did it get its value
↓
Why did it become NULL
↓
Should it have been checked before
↓
Root Cause

We may find that a function returned a NULL value but the program used it without checking it, so mov rax,[rbx] itself is only where the problem showed itself, not the problem itself

Why is this step important for Fuzzing

Suppose the Fuzzer found this

Crash found

input_0421.dat

So what now
We still don't know what we are dealing with
It may be:

Crash
↓
Heap Buffer Overflow

or

Crash
↓
Use After Free

or

Crash
↓
Out of Bounds Read

or

Crash
↓
Null Pointer Dereference
ReverseEngineering
بخش سی و یکم بافر اور فلو Root Cause Analysis پیدا کردن علت واقعی Crash توی قسمت قبل یاد گرفتیم وقتی Fuzzer کلی Crash پیدا می‌کنه چطور اون‌ها رو دسته‌بندی کنیم حالا میخوایم یک مرحله عمیق‌تر بشیم این بار فقط نمی‌خوایم بدونیم برنامه کجا Crash کرده می‌خوایم…
That's why after finding the Crash, we need to enter the analysis stage

Fuzzer
↓
Crash
↓
Debugger
↓
Root Cause Analysis
↓
Bug Classification

Don't confuse Root Cause and Crash

Always keep these two separate

Crash
↓
What happened

But

Root Cause
↓
Why did it happen

For example:

Crash
↓
SIGSEGV
↓
Invalid Memory Access

The Root Cause is still unknown

We may find out after investigation

Input
↓
Unsafe Copy
↓
Buffer Overflow
↓
Memory Corruption
↓
Crash

Here we have found the root cause

From the Reverse Engineer's perspective:

When you have a Crash, keep asking yourself these questions

Where did the CPU crash

Which instruction was problematic

Which register or address was involved

Where did this value come from

What data caused this situation

Where was the first place where the wrong behavior occurred

The last question is the most important

Because the Crash may have occurred at one point but the Bug may have occurred several functions earlier

When We found a crash
We don't say we found the cause of the bug
First we find the location of the crash
Then we examine the problematic instruction, then we look at the registers and memory, then we follow the data path back to where the problem actually occurred
That is

Crash
↓
Where
↓
How
↓
Why
↓
Root Cause

And this is exactly the difference between seeing a crash and understanding a bug

@reverseengine
Packt.Mobile.App.Reverse.Engineering.pdf
17.3 MB
Mobile App Reverse Engineering: Get started with discovering, analyzing, and exploring the internals of Android and iOS apps

@reverseengine
قسمت سی و دوم بافر اورفلو


Program Slicing
فقط کدهایی که واقعا به درد تحلیل میخورن
یعنی یک داده رو از نقطه ورود تا محل استفاده دنبال کردیم
حالا فرض کنید برنامه چند هزار دستور داره
قرار نیست همه رو خط به خط بخونیم

Program Slicing
کمک میکنه فقط قسمت‌ هایی از برنامه رو که روی یک مقدار مشخص تأثیر دارن جدا کنیم

Program Slicing

فرض کنید یک مقدار داریم

user_input

و میخوایم بدونیم چه قسمت‌هایی از برنامه روی این مقدار تأثیر میذارن

مثلا:

Input
↓
Function A
↓
Function B
↓
Function C
↓
Crash

به جای بررسی کل برنامه
فقط همین مسیر رو بررسی میکنیم

دو نوع اصلی
Forward Slice
از یک مقدار شروع میکنیم و جلو میریم
سؤال اصلی

این مقدار کجا استفاده میشه

مثلا:

input
↓
buffer
↓
memcpy
↓
target



Backward Slice
از یک نقطه مهم شروع میکنیم و به عقب برمیگردیم

مثلا Crash داریم

Crash
↑
memcpy
↑
buffer
↑
input

سوال اصلی

این مقدار از کجا اومده


یک مثال ساده:

فرض کنید کد اینه
C
int process(int input)
{
int a = input;
int b = a + 10;
int c = b * 2;

int unrelated = 500;

return c;
}

اگر هدف ما مقدار c باشه
لازم نیست unrelated رو بررسی کنیم
چون روی c تاثیری نداره

Slice
تقریبا این بخشه

input
↓
a
↓
b
↓
c


در Reverse Engineering چرا مهمه؟

فرض کنید یک Crash دارید
و Stack Trace به یک تابع بزرگ میرسه

مثلا:

process_packet()

این تابع ممکنه صدها خط کد داشته باشه
ولی Crash فقط به یک مقدار خاص وابسته باشه

مثلا:

length

حالا به جای بررسی کل تابع

مسیر length رو دنبال میکنیم

length
↓
check
↓
calculation
↓
buffer operation
↓
Crash


وابستگی داده‌ای

مثلا:
C
int size = input_length;
int copy_size = size + 8;

memcpy(buffer, input, copy_size);

اینجا copy_size به size وابسته است
و size هم به input_length
پس مسیر داده اینه

input_length
↓
size
↓
copy_size
↓
memcpy

این دقیقا چیزی هست که موقع Slicing می‌خوایم پیدا کنیم

وابستگی کنترلی هم مهمه
گاهی مقدار مستقیما منتقل نمیشه ولی روی تصمیم برنامه تاثیر میذاره

مثلا:
C++
if (size > 100)
{
process_large_input();
}

اینجا size تعیین میکنه کدوم مسیر اجرا بشه
پس علاوه بر Data Dependency
یک Control Dependency هم داریم

در Ghidra یا IDA
وقتی یک متغیر یا Register مهم پیدا کردید
می‌تونی References و مسیرهای
استفاده از اون رو بررسی کنید

مثلا:

Variable
↓
References
↓
Functions
↓
Instructions

بعد کم‌ کم Slice موردنظر رو تشکیل میدید
در ابزارهای پیشرفته‌تر هم میشه این کار رو به شکل خودکارتر انجام داد

تفاوت Data Flow و Slicing

خیلی ساده
Data Flow
میگه داده چطور حرکت میکنه

Program Slicing
میگه برای تحلیل یک مقدار خاص دقیقا کدوم قسمت‌های برنامه مهم هستن

پس

Data Flow
↓
مسیر حرکت داده

Program Slicing
↓
بخش مرتبط با یک داده یا نتیجه خاص



وقتی با یک باینری بزرگ روبه‌رو شدیم
لازم نیست کل برنامه رو بررسی کنیم
می‌تونیم یک مقدار مهم مثل

Input
Length
Pointer
Crash Value

رو انتخاب کنیم
بعد مسیر وابستگی‌های اون رو پیدا کنیم
در نتیجه حجم زیادی از کد که ارتباطی با مسئله ما نداره کنار گذاشته میشه
و تحلیل خیلی سریعتر میشه

@reverseengine