ReverseEngineering
1.32K subscribers
50 photos
11 videos
106 files
888 links
Download Telegram
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
ReverseEngineering
قسمت سی و دوم بافر اورفلو Program Slicing فقط کدهایی که واقعا به درد تحلیل میخورن یعنی یک داده رو از نقطه ورود تا محل استفاده دنبال کردیم حالا فرض کنید برنامه چند هزار دستور داره قرار نیست همه رو خط به خط بخونیم Program Slicing کمک میکنه فقط قسمت‌ هایی…
Part 32 Buffer Overflow


Program Slicing

Focusing only on the code relevant to the analysis.
It involves tracing a piece of data from its entry point to where it is used.
Imagine a program with thousands of instructions;
we don't need to read through every single line.

Program Slicing
helps isolate only those parts of the program that affect a specific value.

Program Slicing

Suppose we have a value:

user_input

and we want to know which parts of the program affect this value.

For example:

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

Instead of examining the entire program,
we examine only this specific path.

Two main types:
Forward Slice
We start with a value and move forward.
The key question:

Where is this value used?

For example:

input
↓
buffer
↓
memcpy
↓
target



Backward Slice
We start from a critical point and trace back.

For example, we have a crash:

Crash
↑
memcpy
↑
buffer
↑
input

The key question:

Where did this value come from?


A simple example:

Suppose the code is:
C
int process(int input)
{
int a = input;
int b = a + 10;
int c = b * 2;

int unrelated = 500;

return c;
}

If our goal is the value of c,
we don't need to examine unrelated,
because it has no effect on c.

The slice
consists roughly of this part:

input
↓
a
↓
b
↓
c


Why is it important in Reverse Engineering? Suppose you have a crash,
and the stack trace points to a large function—

for example:

process_packet()

This function might contain hundreds of lines of code,
yet the crash depends on only one specific value—

for instance:

length

Now, instead of examining the entire function,

we trace the path of length:

length
↓
check
↓
calculation
↓
buffer operation
↓
Crash


Data dependency

For example:
C
int size = input_length;
int copy_size = size + 8;

memcpy(buffer, input, copy_size);

Here, copy_size depends on size,
and size depends on input_length;
so, the data path is:

input_length
↓
size
↓
copy_size
↓
memcpy

This is precisely what we aim to identify during slicing.

Control dependency is also important;
sometimes a value isn't passed directly but influences a program decision—

for example:
C++
if (size > 100)
{
process_large_input(); }

Here, the size determines which execution path is taken;
so, in addition to Data Dependency,
we also have a Control Dependency.

In tools like Ghidra or IDA,
once you identify a significant variable or register,
you can examine its references and
usage paths:

For example:

Variable
↓
References
↓
Functions
↓
Instructions

You then gradually construct the desired slice;
in more advanced tools, this process can be automated.

Difference between Data Flow and Slicing

Simply put:
Data Flow
describes how data moves.

Program Slicing
identifies exactly which parts of the program are relevant
to analyzing a specific value.

So:

Data Flow
↓
Data movement path

Program Slicing
↓
The portion of the code related to a specific piece of data or result

When dealing with a large binary,
there is no need to examine the entire program;
we can select a key value—such as

Input
Length
Pointer
Crash Value

and trace its dependency paths.
Consequently, large sections of code unrelated to our specific problem are excluded,
making the analysis much faster.

@reverseengine
Taint Analysis
روی Memory و Pointer ها
تا اینجا Taint Analysis رو روی متغیرهای ساده انجام دادیم
ولی توی برنامه واقعی داستان یکم پیچیده تر میشه چون داده فقط بین چند تا متغیر جابه جا نمیشه

ممکنه وارد Memory بشه
داخل یه Buffer قرار بگیره
از طریق Pointer جابه جا بشه
و بعد چند تا تابع مختلف از همون Memory استفاده کنن
اینجاست که Taint Analysis یکم سخت تر میشه

فرض کنید اینو داریم
C
char buffer[32];

buffer[0] = input;

اینجا داده ای که از input اومده وارد Memory شده

پس میتونیم بگیم

input
↓
buffer[0]

حالا فرض کنید یه Pointer به این Buffer داریم
C
char *p = buffer;

بعد هم

char x = p[0];

اینجا داده دوباره از Memory خونده شده
پس مسیرمون میشه

input
↓
buffer[0]
↓
p[0]
↓
x

پس Taint فقط بین Register و Variable حرکت نمیکنه
ممکنه مسیرش از چند مرحله مختلف رد بشه

Register
↓
Memory
↓
Pointer
↓
Memory
↓
Register

این موضوع توی Reverse Engineering خیلی مهمه
چون وقتی Assembly رو نگاه میکنید ممکنه اصلا چیزی به اسم input یا buffer نبینید

مثلا ممکنه همچین چیزی ببینید

mov [rbp-40h], eax
lea rcx, [rbp-40h]
movzx eax, byte ptr [rcx]

اینجا باید خودتون رابطه بین این دستورها رو پیدا کنید

یعنی

EAX
↓
[rbp-40h]
↓
RCX
↓
[RCX]
↓
EAX

یعنی داده یه بار وارد Memory شده
بعد یه Pointer به اون قسمت ساخته شده
و دوباره داده از طریق همون Pointer خونده شده

Pointer
خودش Tainted هست یا داده ای که بهش اشاره میکنه
اینجا باید خیلی دقت کنیم

فرض کنید
C
char *p = buffer;
char x = *p;

ممکنه Buffer حاوی داده Tainted باشه
ولی خود Pointer یعنی p لزوما Tainted نیست

مثلا:

Address = Clean

Data at Address = Tainted

این دوتا با هم فرق دارن
پس وقتی Memory رو تحلیل میکنید همیشه دو تا سوال جدا از خودتون بپرسید

این Address از کجا اومده

محتوای این Address از کجا اومده

مثلا ممکنه داشته باشیم

Pointer

0x500000

خود این Address ممکنه کاملا عادی و قابل اعتماد باشه

ولی محتوای

Memory[0x500000]

ممکنه از ورودی کاربر اومده باشه
پس Address میتونه Clean باشه
ولی Data داخل اون Address Tainted باشه

یه مثال ساده تر:

C
int process(char *input)
{
char buffer[32];

buffer[0] = input[0];

int x = buffer[0] + 10;

return x;
}

اگر input رو Tainted کنیم
مسیر داده میشه

input[0]
↓
buffer[0]
↓
x
↓
return

پس حتی اگر داده برای یه مدت داخل Stack قرار گرفته باشه
Taint
خودش از بین نمیره
تا وقتی که داده جدید همچنان به اون مقدار وابسته باشه
حالا همین رو توی Assembly ببینیم
ممکنه چیزی شبیه این ببینیم

movzx eax, byte ptr [rcx]
mov byte ptr [rbp-20h], al
movzx eax, byte ptr [rbp-20h]
add eax, 10

اینجا باید مسیر رو خودمون از روی دستورها بسازیم

[RCX]
↓
AL
↓
[rbp-20h]
↓
EAX
↓
ADD 10

اگر [RCX] حاوی داده Tainted باشه
پس این مسیر هم باید توی تحلیل ما دنبال بشه
چرا Pointer Analysis مهم میشه
چون توی برنامه های بزرگ ممکنه داده مستقیم از یه Variable به Variable بعدی نره
ممکنه مسیر این شکلی باشه

Input
↓
Buffer
↓
Pointer
↓
Function A
↓
Memory
↓
Pointer
↓
Function B
↓
Comparison

اگر فقط Registerها رو نگاه کنی ممکنه وسط مسیر ارتباط داده رو گم کنی
برای همین توی Reverse Engineering خیلی مهم

Data Flow و Memory Analysis
خیلی به هم وابسته هستن

یه نکته مهم
هر Memory Access به معنی انتقال Taint نیست
مثلا:
C++
int x = 100;
int y = x + 20;

اینجا هیچ داده ای از Input نیومده

پس

x = Clean
y = Clean

ولی اگر داشته باشیم
C++
int x = input;
int y = x + 20;

اینجا داریم

x = Tainted
y = Tainted

پس همیشه باید منشا داده رو دنبال کنیم
فقط اینکه یه مقدار وارد Memory شده به تنهایی کافی نیست
باید ببینیم اون مقدار از کجا اومده و بعد کجا استفاده شده

تمرین:

این کد رو بررسی کنید
C
int process(char *input)
{
char buffer[32];

buffer[0] = input[0];

int x = buffer[0] ^ 0x55;

int clean = 500;
clean += 20;

int result = x + 10;

return result;
}

فرض کنید input[0] نقطه شروع Taint باشه

مسیر رو پیدا کنید

input[0]
↓
?
↓
?
↓
?
↓
return

بعد مشخص کنید clean چرا نباید وارد این مسیر بشه

@reverseengine
ReverseEngineering
Taint Analysis روی Memory و Pointer ها تا اینجا Taint Analysis رو روی متغیرهای ساده انجام دادیم ولی توی برنامه واقعی داستان یکم پیچیده تر میشه چون داده فقط بین چند تا متغیر جابه جا نمیشه ممکنه وارد Memory بشه داخل یه Buffer قرار بگیره از طریق Pointer جابه…
Taint Analysis On Memory and Pointers

So far we have done Taint Analysis on simple variables
But in a real program the story gets a bit more complicated because data is not just moved between a few variables

It may be entered into Memory

It may be placed into a Buffer

It may be moved through a Pointer
And then several different functions may use the same Memory
This is where Taint Analysis gets a bit more difficult

Suppose we have this

C
char buffer[32];

buffer[0] = input;

Here the data that came from input is entered into Memory

So we can say

input
↓
buffer[0]

Now suppose we have a Pointer to this Buffer

C
char *p = buffer;

And then

char x = p[0];

Here the data is read from Memory again
So our path becomes

input
↓
buffer[0]
↓
p[0]
↓
x
So Taint doesn't just move between Register and Variable
Its path may go through several different stages

Register
↓
Memory
↓
Pointer
↓
Memory
↓
Register

This is very important in Reverse Engineering
Because when you look at the Assembly you may not see anything called input or buffer at all

For example, you may see something like this

mov [rbp-40h], eax
lea rcx, [rbp-40h]
movzx eax, byte ptr [rcx]

Here you have to find the relationship between these instructions yourself

That is

EAX
↓
[rbp-40h]
↓
RCX
↓
[RCX]
↓
EAX
That is, the data is entered into Memory once
Then a Pointer is created to that part
And the data is read again through the same Pointer

Pointer
Itself Is it Tainted or the data it points to
Here we have to be very careful

Suppose
C
char *p = buffer;
char x = *p;

Buffer may contain Tainted data
But Pointer itself, i.e. p, is not necessarily Tainted

For example:

Address = Clean

Data at Address = Tainted

These two are different
So when you analyze Memory, always ask yourself two separate questions

Where did this Address come from

Where did the content of this Address come from

For example, we may have

Pointer

0x500000

This Address itself may be completely normal and reliable

But the content of

Memory[0x500000]

may have come from user input

So Address can be Clean

But Data inside that Address is Tainted

A simpler example:

C
int process(char *input)
{
char buffer[32];

buffer[0] = input[0];

int x = buffer[0] + 10;

return x;
}

If we Tainted the input
the data path will be

input[0]
↓
buffer[0]
↓
x
↓
return

So even if the data is in the Stack for a while
Taint
itself will not go away
as long as the new data is still attached to that value
Now let's see the same in Assembly
We may see something like this

movzx eax, byte ptr [rcx]
mov byte ptr [rbp-20h], al
movzx eax, byte ptr [rbp-20h]
add eax, 10

Here we have to create the path ourselves from the instructions

[RCX]
↓
AL
↓
[rbp-20h]
↓
EAX
↓
ADD 10

If [RCX] contains Tainted data
then this path should also be followed in our analysis
Why Pointer Analysis is important
Because in large programs, data may be passed directly from one Variable to the next Variable No
The path may be like this

Input
↓
Buffer
↓
Pointer
↓
Function A
↓
Memory
↓
Pointer
↓
Function B
↓
Comparison

If you only look at the registers, you may miss the data connection in the middle of the path
That is why it is very important in Reverse Engineering

Data Flow and Memory Analysis
are very interrelated

An important point
Not every Memory Access means transferring Taint
For example:
C++
int x = 100;
int y = x + 20;

Here no data came from Input

So

x = Clean
y = Clean

But if we have
C++
int x = input;
int y = x + 20;

Here we have

x = Tainted

y = Tainted

So we always need to trace the origin of the data

Just because a value is entered into Memory is not enough

We need to see where that value came from and where it was used

Exercise:

Examine this code
C
int process(char *input)
{
char buffer[32];

buffer[0] = input[0];

int x = buffer[0] ^ 0x55;

int clean = 500;

clean += 20;

int result = x + 10;

return result;
}

Assume input[0] is the starting point of Taint

Find the path

input[0]
↓
?
↓
?
↓
?
↓
return

Then specify why clean should not enter this path

@reverseengine