ReverseEngineering
1.32K subscribers
50 photos
11 videos
106 files
888 links
Download Telegram
برای این مرحله معمولا میریم سراغ ابزارهایی مثل

GDB
Ghidra
IDA
ASan


پس مسیر کلی کار میتونه این شکلی باشه

Seed Input
│
▼
AFL++
│
▼
New Paths
│
▼
Interesting Input
│
▼
Crash
│
▼
GDB
│
▼
Root Cause Analysis

یعنی Fuzzer یه ورودی جالب پیدا میکنه
بعد ممکنه همین ورودی باعث Crash بشه
از اینجا به بعد ما وارد ماجرا میشیم و باید بفهمیم علت واقعی مشکل چی بوده


AFL++
با فرستادن ورودی های مختلف فقط دنبال Crash نیست مسیرهای جدید برنامه رو هم دنبال میکنه

Instrumentation
کمک میکنه بفهمیم یه ورودی چه بخش هایی از برنامه رو اجرا کرده
هر مسیر جدید میتونه یه راه جدید برای رسیدن به یه باگ پنهان باشه
و وقتی Crash پیدا شد
کار Fuzzer تموم میشه
و نوبت ما میرسه که علت واقعی مشکل رو پیدا کنیم

تمرین:

همین برنامه رو با یه شرط سوم تغییر بدید
مثلا بعد از !HELLO یه شرط جدید اضافه کنید بعد روی کاغذ فکر کنید AFL++ چه ورودی هایی ممکنه به ترتیب پیدا کنه تا هر سه مسیر برنامه رو پوشش بده

@reverseengine
ReverseEngineering
بخش بیست و هشتم بافر اورفلو AFL++ و Instrumentation چطور Fuzzer میفهمه داخل برنامه چه خبره توی بخش قبل با libFuzzer و Coverage Guided Fuzzing آشنا شدیم حالا میریم سراغ AFL++ AFL++ یکی از معروف ترین ابزارهای Fuzzing هست که مخصوصا برای تست برنامه های C…
Part 28 Buffer Overflow


AFL++ and Instrumentation

How does a Fuzzer understand what is going on inside a program

In the previous section, we learned about libFuzzer and Coverage Guided Fuzzing

Now let's move on to AFL++

AFL++
is one of the most famous fuzzing tools, which is especially used for testing C, C++, and binary programs. We want to understand how AFL++ understands that a program input has entered a new path

AFL++
does not just send random input
Suppose we have a program that has different paths

Input
│
▼
┌────────┐
│ Check A │
└────┬────┘
│
┌─────┴───┐
▼ ▼
Path 1 Path 2
│
▼
┌───────┐
│ Check B │
└──────┬───┘
│
┌──────┐
│ ┌──────┐
▼ ▼
Path 3 Path 4

If AFL++ sends an input and the program enters Path 1

That path is recorded

Then AFL++ changes the input and tries it again

If the new input causes the program to enter Path 2

AFL++
notices that a new path has been found

This new input becomes valuable and keeps it

What does Instrumentation mean

Here we come to the important part

Instrumentation
means adding a series of mechanisms to the program so that we can understand what happened inside it during execution

For example, AFL++ can collect information about the path of the program execution with Instrumentation

In simple terms:

Before Instrumentation

Input
│
▼
Program
│
▼
Result

After Instrumentation

Input
│
▼
Program
│
▼
Path Tracking
│
▼
Result

That means the program is still doing its job, but now someone is also checking which paths the program has taken
I named this task Instrumentation to make it seem a little more scientific

An example Simple:

Suppose we have this program

C
#include <stdio.h>
#include <string.h>

int main(void)
{
char input[32];

if (!fgets(input, sizeof(input), stdin))
return 0;

if (strncmp(input, "HELLO", 5) == 0)
{
puts("First check passed");

if (input[5] == '!')
{
puts("Second check passed");
}
}

return 0;
}


What is AFL++ looking for here?
First it might try simple inputs

AAAA

This input just executes a normal path
Then AFL++ starts modifying the input
If it reaches

HELLO

a new branch is executed
So AFL++ realizes that this input is interesting
Then it modifies this input further
For example:

HELLO!

Now the second condition is also met
So another new path has been found
Simply we can see this process like this

AAAA
│
▼
Normal path
│
▼
AFL++ changes
│
▼
HELLO
│
▼
New path
│
▼
AFL++ changes again
│
▼
HELLO!
│
▼
Newer path

Seed or Corpus
What does it mean

AFL++
Usually starts with a series of initial inputs, we call these initial inputs Seed
For example, a simple file

project
├── input
│ └── seed1
└── output

Inside seed1 can be just this

AAAA

Then AFL++ changes this input repeatedly
deletes
adds
changes bytes
and checks which change caused a new path to be found
Inputs that are worth it can be placed in Corpus

Corpus
means a set of interesting inputs that the Fuzzer can use to continue Fuzzing
So we can imagine this process like this

Seed
│
▼
Mutation
│
├── Input A ──► Not a new path
│
├── Input B ──► New path
│ │
│ ▼
│ Corpus
│
└── Input C ──► Path Newer
│
▼
Corpus

An example of file structure
Suppose we have this folder

project
├── input
│ └── seed1
└── output

Inside seed1 can be just this

AAAA

Then AFL++ starts with this input and gradually generates new inputs
An important point
When AFL++ finds a Crash
The work is not over yet, the interesting part begins
We need to find out
What input caused the Crash
Where exactly did the Crash happen
What function was involved
Is the problem really a Memory Bug or not
For this step, we usually go to tools like

GDB

Ghidra

IDA

ASan


So the general path of the work can be like this

Seed Input
│
▼
AFL++
│
▼
New Paths
│
▼
Interesting Input
│
▼
Crash
│
▼
GDB
│
▼
Root Cause Analysis


That is, the Fuzzer finds an interesting input
Then this input may cause a Crash
From here on, we get into the story and we need to find out what the real cause of the problem was

AFL++
By sending different inputs, it does not only look for Crash, it also follows new paths of the program

Instrumentation
helps us understand what parts of the program an input has executed
Each new path can be a new way to reach a hidden bug
And when a Crash is found
The Fuzzer's work is finished
And it's our turn to find the real cause of the problem

Exercise:

Change the same program with a third condition
For example, add a new condition after !HELLO On paper, think about what inputs AFL++ might find in order to cover all three paths of the program

@reverseengine
Forwarded from DarkBit
📌 مقاله جدید منتشر شد

⚔️ The Art of DLL Hijacking

در این مقاله به بررسی عمیق مکانیزم‌های DLL Hijacking در ویندوز پرداخته‌ام؛ از مبانی نحوه بارگذاری DLLها و DLL Search Order Hijacking تا تکنیک‌های پیشرفته‌تر مانند:

◾️ DLL Search Order Hijacking
◾️ DLL Substitution (Replacement)
◾️ DLL Side-Loading
◾️ DLL Proxying
◾️ Phantom DLL Hijacking

در این مقاله بررسی می‌کنیم که Windows Loader چگونه DLLها را resolve و بارگذاری می‌کند، چگونه شرایط مناسب برای DLL Hijacking شناسایی می‌شوند، و چرا برخی برنامه‌های معتبر می‌توانند به بخشی از زنجیره بارگذاری تبدیل شوند.

همچنین یک رویکرد عملی برای تحلیل و شناسایی Phantom DLLها ارائه شده است.

مطالعه مقاله:

https://darkbitx.github.io/posts/the-art-of-dll-hijacking/

✍️ نویسنده: دارک‌بیت | DarkBit
💬 Forum
📣 DarkBit

#CyberSecurity #WindowsInternals #Persistence #Maldev #RedTeam
DarkBit
📌 مقاله جدید منتشر شد ⚔️ The Art of DLL Hijacking در این مقاله به بررسی عمیق مکانیزم‌های DLL Hijacking در ویندوز پرداخته‌ام؛ از مبانی نحوه بارگذاری DLLها و DLL Search Order Hijacking تا تکنیک‌های پیشرفته‌تر مانند: ◾️ DLL Search Order Hijacking ◾️ DLL…
📌 New article released

⚔️ The Art of DLL Hijacking

In this article, I have taken an in-depth look at the mechanisms of DLL Hijacking in Windows; from the basics of how DLLs are loaded and DLL Search Order Hijacking to more advanced techniques such as:

◾️ DLL Search Order Hijacking
◾️ DLL Substitution (Replacement)
◾️ DLL Side-Loading
◾️ DLL Proxying
◾️ Phantom DLL Hijacking

In this article, we will examine how the Windows Loader resolves and loads DLLs, how suitable conditions for DLL Hijacking are identified, and why some legitimate programs can become part of the loading chain.

A practical approach to analyzing and identifying Phantom DLLs is also presented.

Read the article:
https://darkbitx.github.io/posts/the-art-of-dll-hijacking

✍️ Author: Darkbit | DarkBit

💬 Forum
📣 DarkBit

#CyberSecurity #WindowsInternals #Persistence #Maldev #RedTeam
❤3
exit

وقتی یک Process کارش تموم میشه چه اتفاقی میوفته

تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم

حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره

مثلا یه برنامه خیلی ساده داریم:

int main() {
printf("Hello");
return 0;
}


وقتی main تموم میشه برنامه هم باید به سیستم عامل بفهمونه که

کار من تموم شده

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

exit دقیقا چیکار میکنه

خیلی ساده بخوایم بگیم

exit
به سیستم عامل میگه این Process دیگه کاری نداره و میخواد اجرای خودش رو تموم کنه

مثلا:

exit(0);


اون عدد 0 معمولا یعنی برنامه با موفقیت تموم شده

یعنی

exit(0)
↓
اجرای موفق

ولی اگه مقدار غیر صفر بدیم معمولا یعنی یه وضعیت دیگه اتفاق افتاده

مثلا یه خطا

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

پس قرار نیست Kernel بفهمه عدد 1 دقیقا یعنی چه خطایی

این عدد بیشتر یه اطلاعاتیه که Process به Parent میده

بعد از exit چی میشه

اینجا یه نکته مهم داریم

وقتی Process به exit میرسه دیگه قرار نیست دستورهای معمول برنامه رو ادامه بده

یعنی اجرای برنامه تموم شده

ولی Kernel هنوز باید چندتا کار انجام بده

مثلا منابعی که Process استفاده میکرد باید مدیریت بشن

مثل:

Memory

File Descriptor


منابع مربوط به I O

و اطلاعات مربوط به خود Process

ولی یه نکته خیلی مهم

همه چیز همون لحظه کامل تموم نمیشه

این همون جاییه که Zombie Process دوباره وارد داستان میشه

فرض کنید این وضعیت رو داریم

Parent
│
└──── Child
│
▼
exit()
│
▼
اجرای Child تموم شد
│
▼
Zombie

Child دیگه اجرا نمیشه

CPU
هم دیگه بهش برای اجرای برنامه CPU Time نمیده

ولی یه مقدار اطلاعات مربوط به پایانش هنوز توسط Kernel نگه داشته میشه

چرا

چون Parent باید بتونه بفهمه Child چطوری تموم شده

مثلا Exit Status اون چی بوده

بعد Parent میاد و wait میکنه

Child
│
▼
exit()
│
▼
Zombie
│
▼
Parent calls wait()
│
▼
Exit Status دریافت میشه
│
▼
Reaping
│
▼
Zombie از بین میره

به این مرحله آخر میگیم

Reaping

یعنی Parent وضعیت پایان Child رو میگیره و Kernel دیگه اطلاعات باقی مونده مربوط به اون Child رو لازم نداره

پس یه نکته خیلی مهم رو یادتون باشه

Termination با Reaping یکی نیست

Termination

یعنی Process دیگه اجرا نمیشه

Reaping

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

این تفاوت دقیقاً دلیل وجود Zombie Process هست

Exit Status چیه

وقتی یه Process تموم میشه میتونه یه مقدار به عنوان وضعیت پایان خودش داشته باشه

مثلا

exit(0);


یعنی

Exit Status = 0


یا

exit(1);


یعنی

Exit Status = 1


توی Unix و Linux معمولا 0 یعنی همه چیز خوب تموم شده

و مقدارهای غیر صفر معمولا برای وضعیت‌های دیگه استفاده میشن

Parent
میتونه این مقدار رو با wait یا waitpid دریافت کنه

حالا کل داستان رو کنار هم بذاریم

تا الان چندتا تکه مهم از چرخه Process رو یاد گرفتیم

Parent
│
fork()
│
▼
Child
│
exec()
│
▼
New Program
│
exit()
│
▼
Zombie
│
wait()
│
▼
Reaped

یعنی اول Parent یه Child میسازه

بعد Child میتونه با exec یه Program جدید رو اجرا کنه

Program کارش رو انجام میده

بعد exit میکنه

Process دیگه اجرا نمیشه

ممکنه برای مدت کوتاهی Zombie بشه

بعد Parent با wait وضعیتش رو میگیره

و در نهایت Child Reap میشه

حالا یه سوال مهم

آیا exit یعنی Process همون لحظه از همه جا پاک میشه؟

نه

اینجا یکی از اون جاهاییه که سیستم عامل یه کم از چیزی که در نگاه اول به نظر میاد پیچیده تره

وقتی Process terminate میشه

دیگه اجرا نمیشه

ولی Kernel ممکنه یه مقدار اطلاعات محدود از اون رو نگه داره

مهمترین دلیلش هم اینه که Parent باید بتونه وضعیت پایان Child رو بفهمه

پس این دوتا رو قاطی نکنید

Termination
↓
Process دیگه اجرا نمیشه

Reaping
↓
Parent
وضعیت Process رو میگیره
↓
اطلاعات باقی مونده جمع میشه

یه نکته دیگه درباره منابع

وقتی Process تموم میشه Kernel منابعی که دیگه لازم نیستن رو Cleanup میکنه
مثلا Address Space مربوط به Process دیگه مورد استفاده اون Process نیست

File Descriptor
های باز هم در جریان پایان Process بسته میشن

ولی این به معنی این نیست که خود Resource حتماً همون لحظه نابود میشه

مثلا ممکنه یه File توسط Process دیگه هم باز باشه

پس File Descriptor با خود Resource یکی نیست

این موضوع رو بعدا توی Linux Internals خیلی بیشتر میبینیم

یه نکته جالب درباره return و exit

مثلا اگه بنویسیم:

int main() {
return 0;
}


وقتی main تموم بشه برنامه هم به شکل عادی به پایان میرسه

اما اگه بنویسیم

exit(0);

اینجا مستقیما درخواست پایان برنامه داده شده

برای همین توی برنامه های ساده ممکنه نتیجه هر دو تقریبا یکی به نظر برسه

ولی از نظر مسیر اجرای برنامه دقیقا یکی نیستن

یه تفاوت جالب دیگه هم داریم

exit
با exit_ یکی نیست

توی C تابع exit قبل از پایان برنامه میتونه بعضی Cleanupهای مربوط به User Space رو انجام بده

مثلا Handlerهایی که با atexit ثبت شدن رو اجرا کنه

یا Bufferهای stdio رو Flush کنه

ولی exit_ این Cleanup های User Space رو انجام نمیده و مستقیم تر Process رو terminate میکنه

این تفاوت وقتی وارد بحث fork و Buffering و System Call بشیم خیلی مهم میشه

یه نکته دیگه هم اینه که Process فقط با exit تموم نمیشه

ممکنه Process در اثر Signal هم terminate بشه

پس داستان کلی میتونه این شکلی باشه

Process
│
┌─────────┴─────────┐
│ │
exit() Signal
│ │
└─────────┬─────────┘
▼
Termination
│
▼
Exit Information
│
▼
Zombie
│
wait()
│
▼
Reaped

حالا بریم سمت Reverse Engineering

فرض کنید داری یه Binary رو بررسی میکنید

توی Trace میبینید یه اتفاقاتی شبیه این افتاده

fork()
↓
exec()
↓
Program Execution
↓
exit()

حالا دیگه میتونید یه تصویر ذهنی از چیزی که اتفاق افتاده داشته باشید

Process جدید ساخته شد
↓
Child شروع به اجرا کرد
↓
Program جدید اجرا شد
↓
Program کار خودش رو انجام داد
↓
Process تموم شد

اگه بعدش Parent رو ببینی که wait میکنه

Child
│
▼
exit()
│
▼
Zombie
│
▼
Parent
│
wait()
│
▼
Exit Status
│
▼
Reaping

میتونید بفهمید که Parent احتمالا منتظر نتیجه اجرای Child بوده

اینجاست که چیزایی که تا الان یاد گرفتیم کم کم دارن به درد Reverse Engineering میخورن

حالا یه تصویر کامل تر از چیزایی که تا اینجا یاد گرفتیم داشته باشید

Parent Process
│
fork()
│
▼
Child Process
│
exec()
│
▼
Program
│
▼
Execution
│
exit()
│
▼
Termination
│
▼
Zombie
│
wait()
│
▼
Reaping



exit
خیلی ساده یعنی

Process
میگه کار من تموم شد

بعد از اون

Process
↓
Termination
↓
Exit Status
↓
Parent با wait وضعیت رو میگیره
↓
Reaping
↓
Zombie از بین میره

پس تا اینجا چهار مفهوم خیلی مهم Process API رو داریم

fork()
↓
ساخت Child Process

exec()
↓
اجرای Program جدید داخل Process

wait()
↓
منتظر موندن یا گرفتن وضعیت Child

exit()
↓
تموم کردن اجرای Process

این چهار مفهوم رو خوب یاد بگیرید

چون وقتی بعدا وارد Linux Internals و System Call ها و Reverse Engineering باینری های ELF بشیم دوباره بارها به همین مفاهیم برمیخوریم

فقط یه نکته رو همیشه یادتون باشه

ما اینجا داریم مدل Unix و Linux رو دنبال میکنیم

جزئیات داخلی Process و PCB و Process Descriptor ممکنه توی سیستم عامل های مختلف فرق داشته باشهن

فعلا همین مدل رو مدل اصلیمون قرار میدیم

@reverseengine
ReverseEngineering
exit وقتی یک Process کارش تموم میشه چه اتفاقی میوفته تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره مثلا یه برنامه خیلی ساده داریم: int main() { …
exit

What happens when a Process finishes its work

So far we have been introduced to fork, exec, wait and even Zombie Process

Now suppose we have a Process that has done its work and has nothing more to do

For example, we have a very simple program:

int main() {
printf("Hello");
return 0;
}


When main finishes, the program must also tell the operating system that

My work is finished

This is where the concept of exit comes into play

What exactly does exit do

To put it simply,

exit
tells the operating system that this Process has nothing more to do and wants to finish its execution

For example:

exit(0);

That number 0 usually means that the program has completed successfully

That is

exit(0)
↓
Successful execution

But if we give a non-zero value, it usually means that another situation has occurred

For example, an error

Of course, the exact meaning of that number is determined by the program itself

So the Kernel is not supposed to understand what exactly the number 1 means

This number is more of an information that the Process gives to the Parent

What happens after exit

Here we have an important point

When the Process reaches exit, it is no longer supposed to continue the normal program commands

That means the program execution is finished

But the Kernel still has to do a few things

For example, the resources that the Process used must be managed

Such as:

Memory

File Descriptor

I O resources

And information about the Process itself

But a very important point

Not everything is finished right away

This is where the Zombie Process enters the story again

Suppose we have this situation

Parent
│
└──── Child
│
▼
exit()
│
▼
Child execution is finished
│
▼
Zombie

Child is no longer running

CPU
does not give it CPU Time to run the program

But some information about its termination is still kept by the Kernel

Why

Because Parent needs to be able to understand how Child terminated

For example, what was its Exit Status

Then Parent comes and waits

Child
│
▼
exit()
│
▼
Zombie
│
▼
Parent calls wait()
│
▼
Exit Status is received
│
▼
Reaping
│
▼
Zombie is destroyed

We call this last step

Reaping

That is, Parent gets the termination status of Child and Kernel no longer needs the remaining information about that Child

So remember one very important point

Termination is not the same as Reaping

Termination

That is, Process is no longer running

Reaping

That is, Parent gets the termination status of Process and the remaining information about That Process is collected from Process Management Structures

This difference is exactly why Zombie Process exists

What is Exit Status

When a Process terminates, it can have a value as its exit status

For example

exit(0);




Exit Status = 0


or

exit(1);


That is

Exit Status = 1


In Unix and Linux, it is usually 0, meaning everything is fine

And non-zero values ​​are usually used for other states

Parent
can get this value with wait or waitpid

Now let's put the whole story together

So far, we have learned some important parts of the Process cycle

Parent
│
fork()
│
▼
Child
│
exec()
│
▼
New Program
│
exit()
│
▼
Zombie
│
wait()
│
▼
Reaped

That is, first Parent creates a Child

Then Child can execute a new Program with exec

Program does its job

Then exits

Process is no longer running

It may become Zombie for a short time

Then Parent gets its state with wait

And finally Child Reaps

Now an important question

Does exit mean that Process is deleted from everywhere at once?

No

This is one of those places where the operating system is a little more complicated than it seems at first glance

When a process terminates

it no longer runs

but the kernel may retain a limited amount of information about it

The most important reason is that the parent needs to be able to understand the termination status of the child

so don't mix the two

Termination
↓
Process no longer runs

Reaping
↓
Parent
gets the state of the process
↓
Remaining information is collected

Another point about resources
For example, the Address Space of a Process is no longer used by that Process

File Descriptors
are also closed during the termination of the Process

But this does not mean that the Resource itself is necessarily destroyed at that moment

For example, a File may be open by another Process

So the File Descriptor is not the same as the Resource itself

We will see this issue much more later in Linux Internals

An interesting point about return and exit

For example, if we write:

int main() {
return 0;
}


When main ends, the program ends normally

But if we write

exit(0);

Here, the program termination request is given directly

That's why in simple programs, the result of both may seem almost the same

But in terms of the program execution path, they are not exactly the same

We also have another interesting difference

exit
is not the same as exit_

In C, the exit function can perform some User Space cleanups before the program ends

For example, it can execute Handlers registered with atexit

Or flush the stdio buffers

But exit_ does not perform these User Space cleanups and directly terminates the Process

This difference becomes very important when we get into the discussion of fork, buffering, and system calls

Another point is that a Process does not end only with exit

A Process may also terminate due to a Signal

So the overall story could be like this

Process
│
┌───────────────┐
│ │
exit() Signal
│ │
└─�
▼
Termination
│
▼
Exit Information
│
▼
Zombie
│
wait()
│
▼
Reaped

Now let's move on to Reverse Engineering

Suppose you are examining a Binary

In the Trace you see something like this happen

fork()
↓
exec()
↓
Program Execution
↓
exit()

Now you can have a mental picture of what happened

New Process created
↓
Child started executing
↓
New Program executed
↓
Program did its job
↓
Process terminated

If you then see Parent waiting

Child
│
▼
exit()
│
▼
Zombie
│
▼
Parent
│
wait()
│
▼
Exit Status
│
▼
Reaping

You can understand that Parent was probably waiting for the result of Child execution

This is where the things we have learned so far are starting to come in handy for Reverse Engineering

Now a picture Have more complete than what we have learned so far

Parent Process
│
fork()
│
▼
Child Process
│
exec()
│
▼
Program
│
▼
Execution
│
exit()
│
▼
Termination
│
▼
Zombie
│
wait()
│
▼
Reaping

exit
Very simply,

Process
says I am done

After that

Process
↓
Termination
↓
Exit Status
↓
Parent gets the status with wait
↓
Reaping
↓
Zombie dies

So far we have four very important concepts of Process API

fork()
↓
Creating Child Process

exec()
↓
Executing a new program inside a Process

wait()
↓
Waiting or getting the status of Child

exit()
↓
Completing the execution of Process

Learn these four concepts well

Because when you later enter Linux Internals and System Calls And Reverse Engineering ELF binaries, we will come across the same concepts again and again

Just one thing to always remember

We are following the Unix and Linux model here

The internal details of Process, PCB and Process Descriptor may differ in different operating systems

For now, we will make this model our main model

@reverseengine
Call Stack Analysis

EDR


Call Stack
فرض کنید یه برنامه داریم


main()
↓
Function_A()
↓
Function_B()
↓
Function_C()`

یعنی main اومده Function_A رو صدا زده
Function_A
هم Function_B رو صدا زده

Function_B
هم Function_C رو صدا زده

حالا وقتی Function_C داره اجرا میشه
میشه یه تصویر از مسیر فراخوانی‌هایی که باعث رسیدن برنامه به این نقطه شدن داشت

به زبان خیلی ساده

میتونیم پرسیم

این Function از کجا به اینجا رسید
یه تصویر ساده از Call Stack

┌──────────────────┐
│ Function_C │ ← الان اینجاییم
├──────────────────┤
│ Function_B │
├──────────────────┤
│ Function_A │
├──────────────────┤
│ main() │
└──────────────────┘


به این زنجیره میگیم
Call Stack

البته در سیستم واقعی موضوع کمی پیچیده‌ تر از این تصویر ساده ست
چون Stack واقعا فقط اسم Function ها رو نگه نمیداره اطلاعات مختلفی مثل Stack Frame و Return Address و Local Data و موارد دیگه میتونن در Stack قرار داشته باشن

فعلا برای اینکه مفهوم رو درست بگیریم

همین تصویر ساده کافیه

چرا Call Stack برای EDR مهمه

فرض کنید یه API حساس اجرا شده

خود API به تنهایی همیشه اطلاعات کاملی درباره اینکه چه مسیری باعث رسیدن به اینجا شده در اختیار ما نمیذاره
اینجاست که Call Stack میتونه Context بیشتری بده

مثلا:

Sensitive Operation
↑
Function_B
↑
Function_A
↑
Application

حالا EDR میتونه به این فکر کنه
این عملیات از چه مسیری به وجود اومده
ایا این مسیر با رفتار معمول این برنامه جور درمیاد

مثلا فرض کنی یه نرم افزار Backup داریم

Backup.exe
↓
BackupEngine.dll
↓
Memory Manager

این مسیر کاملا میتونه طبیعی باشه
چون برنامه Backup داره کار مربوط به خودش رو انجام میده
حالا یه حالت دیگه رو تصور کنید

Unknown Memory
↓
Unusual Execution
↓
Sensitive Operation

اینجا داستان یه مقدار مشکوک‌ تر میشه
چون مسیر اجرای عملیات با چیزی که انتظار داریم فرق داره

ولی یه نکته خیلی مهم
غیرعادی بودن Stack به تنهایی یعنی حمله اتفاق افتاده نیست

ممکنه دلیل کاملا قانونی داشته باشه
پس EDR حرفه‌ای فقط با دیدن یه Stack عجیب نمیاد حکم صادر کنه


Stack Walking
حالا فرض کنید یه ابزار امنیتی میخواد Stack یه Thread رو بررسی کنه
به صورت خیلی ساده میتونه چنین مسیری رو بررسی کنه

Thread
↓
Stack
↓
Return Addresses
↓
Loaded Modules
↓
Memory Regions

یعنی ابزار میاد ادرس‌ هایی که توی مسیر اجرای Thread وجود دارن رو بررسی میکنه
بعد میپرسه
این Address متعلق به کجاست
مثلا ممکنه چیزی شبیه این ببینیم

0x7FF... → known DLL
0x7FF... → known DLL
0x1A3... → private executable memory

دو تای اول ممکنه کاملا عادی باشن
ولی مورد آخر میتونه نیاز به بررسی بیشتری داشته باشه

چرا

چون حالا داریم میبینیم یکی از ادرس‌ های مسیر اجرا به یه Memory Region خصوصی و قابل اجرا مربوطه
باز هم
این به تنهایی اثبات حمله نیست
فقط یه Signal محسوب میشه
اینجا یه نکته مهم درباره Stack داریم

Stack
حقیقت مطلق نیست
یعنی نمیشه فقط Stack رو دید و گفت
پس حتما اتفاق بدی افتاده
برنامه‌های واقعی میتونن Stack های خیلی پیچیده‌ای داشته باشن

مثلا:
JIT
Runtimeهای مختلف
Debugger
Instrumentation
Exception Handling
Thread Pool
همه اینا میتونن باعث ایجاد مسیرهای غیرمعمول در Stack بشن
پس Detection حرفه‌ای معمولا فقط به یه Signal تکیه نمیکنه
ممکنه چندتا چیز رو کنار هم بذاره
مثلا:
Process
Thread
Memory
Call Stack
Network
Timing
و اطلاعات دیگه
حالا میرسیم به بخش جالب‌تر

Stack و Memory و Thread

تا اینجا درباره اینا جدا جدا صحبت کردیم
ولی توی دنیای واقعی اینا از هم جدا نیستن
یه EDR میتونه این اطلاعات رو کنار هم ببینه

Process
│
┌──────┴──────┐
▼ ▼
Memory Thread
│ │
│ ▼
│ Call Stack
│ │
└──────┬──────┘
▼
Behavior

یعنی به جای اینکه فقط بگه
این API خطرناکه
میتونه سوال خیلی بزرگتری بپرسه
چه Process ی این کار رو انجام داد
کدوم Thread انجامش داد
از چه Memory Regionی استفاده شد

Call Stack چه شکلی بود

این اتفاق چه زمانی افتاد
قبل و بعدش چه اتفاقاتی افتاد

این دقیقا یکی از تفاوت‌های مهم بین Signature Detection و Behavioral Detection رو نشون میده

Signature Detection
بیشتر دنبال یه الگوی مشخص میگرده

مثلا:
این Hash
این String
این Pattern
این Signature
ولی Behavioral Detection بیشتر دنبال رابطه بین اتفاقاته
یعنی
این Process
این Thread
این Memory
این Stack
و این رفتار
ایا وقتی کنار هم قرار میگیرن یه الگوی غیرعادی ایجاد میکنن
از دید Red Team هم این موضوع خیلی مهمه
فرض کنید یه Event خاص رو طوری تغییر بدید که کمتر قابل مشاهده باشه
ممکنه فکر کنید
خب دیگه تموم شد
ولی نه
چون EDR ممکنه فقط خود Event رو نگاه نکنه
ممکنه رابطه بین چندتا لایه مختلف رو بررسی کنه
مثلا:

Process
+
Memory
+
Thread
+
Call Stack
+
Network
+
Timing
↓
Behavior Pattern

یعنی حتی اگر یه Event خاص کمتر دیده بشه
رابطه بین Eventهای دیگه هنوز ممکنه یه الگوی مشخص ایجاد کنه
پس وقتی درباره EDR Evasion مطالعه میکنید
فقط اسم تکنیک‌ها رو حفظ نکند
مثلا ندونید فقط فلان تکنیک چیه
مهم‌تر اینه که بفهمید
این تکنیک دقیقا کدوم Visibility Layer رو تحت تأثیر قرار میده
مثلا باید بتونید از خودتون بپرسید

EDR
این اتفاق رو از کجا میبینه
از Process
از Thread
از Memory
از API
از Call Stack
از Kernel
از Network
یا ترکیبی از چندتا
اینجوری کم کم از حفظ کردن تکنیک‌ ها فاصله میگیری
و شروع میکنید به فهمیدن اینکه سیستم امنیتی واقعا چه چیزی رو میبینه
حالا کل بحث این پست رو کنار هم بذاریم

API
↓
Call Stack
↓
Thread
↓
Memory
↓
Process
↓
Network
↓
Behavior`

هرچی Context بیشتری از یه Event داشته باشیم تحلیل رفتار هم میتونه دقیق‌تر بشه پس یه API به تنهایی شاید اطلاعات زیادی بهمون نده
ولی وقتی بگیم
این API توسط این Thread
داخل این Process
از این Memory Region
با این Call Stack
در این زمان
و همراه با این Network Activity
اتفاق افتاده
حالا تصویر خیلی کامل‌تری داریم
و این دقیقا همون چیزیه که باعث میشه تحلیل Behavioral Detection خیلی جالب‌تر از فقط نگاه کردن به یه Signature ساده باشه


Call Stack
فقط یه لیست ساده از Function ها نیست

بلکه یه سرنخ از مسیریه که Execution طی کرده تا به نقطه فعلی برسه
پس وقتی یه عملیات حساس اتفاق میفته
فقط نپرسید
چی اجرا شد
یه سوال مهم‌تر هم بپرسد
از کجا به اینجا رسید
و وقتی این سوال رو کنار Process و Thread و Memory و Network بذارید
کم کم به دیدی نزدیک میشید که EDR های مدرن برای تحلیل رفتار استفاده میکنن

@reverseengine