ReverseEngineering
1.32K subscribers
50 photos
11 videos
106 files
888 links
Download Telegram
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
ReverseEngineering
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…
Call Stack Analysis

EDR

Call Stack
Suppose we have a program

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

That is, main has called Function_A

Function_A
also called Function_B

Function_B
also called Function_C

Now when Function_C is executing

We can have a picture of the call path that led the program to this point

In very simple language

We can ask

Where did this Function get here from
A simple picture of the Call Stack

┌─ Stack

Of course, in a real system, the issue is a bit more complicated than this simple picture

Because the Stack really doesn't just hold the names of the Functions, various information such as Stack Frame, Return Address, Local Data, and other things can be in the Stack

For now, to get the concept right

This simple picture is enough

Why is the Call Stack important for EDR

Suppose a sensitive API is executed

The API itself alone does not always provide us with complete information about what path led to this point
This is where the Call Stack can provide more Context

For example:

Sensitive Operation
↑
Function_B
↑
Function_A
↑
Application

Now EDR can think about

What path did this operation take?

Does this path fit with the normal behavior of this program

For example, suppose we have a Backup software

Backup.exe
↓
BackupEngine.dll
↓
Memory Manager

This path can be completely normal

Because the Backup program is doing its job

Now imagine another situation Do

Unknown Memory
↓
Unusual Execution
↓
Sensitive Operation

Here the story gets a bit more suspicious
Because the path of the operation execution is different from what we expect

But a very important point
The unusualness of the Stack alone does not mean that an attack has occurred

It may have a completely legitimate reason
So a professional EDR will not come to issue a ruling just by seeing a strange Stack

Stack Walking
Now suppose a security tool wants to check the Stack of a Thread
It can very simply check such a path

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

That is, the tool checks the addresses in the Thread execution path
Then it asks
Where does this Address belong to
For example, we may see something like this

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

The first two may be completely normal
But the last one may need more investigation Okay

Why

Because now we are seeing one of the execution path addresses to a private and executable Memory Region

Again
This alone is not proof of an attack
It is just a Signal
Here is an important point about Stack

Stack
is not an absolute truth
That is, you cannot just look at the Stack and say
Then something bad must have happened
Real programs can have very complex Stacks

For example:
JIT
Different Runtimes
Debugger
Instrumentation
Exception Handling
Thread Pool
All of these can cause unusual paths in the Stack
So professional Detection usually does not rely on just one Signal
It may put several things together
For example:
Process
Thread
Memory
Call Stack
Network
Timing
And other information
Now we come to the more interesting part

Stack, Memory and Thread

So far we have talked about these separately
But in the real world, these are not separate
An EDR can see this information together

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

In other words, instead of just saying, "This API is dangerous," you can ask a much bigger question.
Which process did this? Which thread did it? Which memory region was used? What was the call stack like? When did this happen? What happened before and after? This is exactly one of the important differences between Signature Detection and Behavioral Detection. Signature Detection is more about looking for a specific pattern. For example: This Hash This String This Pattern This Signature While Behavioral Detection is more about looking for the relationship between events. That is, this Process This Thread This Memory This Stack and this Behavior Do they create an unusual pattern when put together? This is also very important from the Red Team's perspective. Suppose you change a specific Event to be less visible. You might think, "Well, that's it." But no, because EDR may not just look at the Event itself. It may look at the relationship between several different layers. For example:

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

That is, even if a particular event is less visible
The relationship between other events may still create a certain pattern
So when you study EDR Evasion
Don't just memorize the names of the techniques
For example, you don't know what a particular technique is
It is more important to understand
Which Visibility Layer exactly does this technique affect
For example, you should be able to ask yourself

Where does EDR see this from
From Process
From Thread
From Memory
From API
From Call Stack
From Kernel
From Network
Or a combination of several
In this way, you gradually move away from memorizing techniques
And you start to understand what the security system really sees
Now let's put the whole discussion of this post together

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


The more Context we have of an Event, the more accurate the behavior analysis can be, so an API alone may not give us much information
But when we say
This API is used by this Thread
Inside this Process
From this Memory Region
With this Call Stack
At this Time
And with this Network Activity
Now we have a much more complete picture
And this is exactly what makes Behavioral Detection Analysis so much more interesting than just looking at a simple Signature

The Call Stack
is not just a simple list of Functions
But a clue to the path that Execution took to get to the current point
So when a critical operation happens

Don't just ask
What was executed
Ask a more important question
Where did it get here
And when you put this question aside from Process and Thread and Memory and Network
You are starting to get closer to the view that modern EDRs use for behavior analysis

@reverseengine
بخش بیست و نهم بافر اورفلو


Binary Only Fuzzing

وقتی سورس کد نداریم
تا اینجا Fuzzing رو بیشتر روی برنامه‌هایی دیدیم که سورس کدشون دستمون بود
ولی توی Reverse Engineering و Fuzzing یه عالمه وقت پیش میاد که فقط یه فایل اجرایی داریم
مثلا

program.exe

یا

program

نه سورس داریم
نه مستندات درست و حسابی
نه حتی دقیقا میدونیم داخل برنامه چه خبره
اینجاست که باید اول برنامه رو بررسی کنیم
بفهمیم ورودی رو از کجا میگیره
بفهمیم اون ورودی چطوری داخل برنامه حرکت میکنه
بعد همون مسیر رو برای Fuzzing آماده کنیم

مرحله اول
باز کردن Binary
اول برنامه رو داخل ابزارهایی مثل Ghidra یا IDA باز میکنیم
هدفمون این نیست که از همون اول کل برنامه رو بفهمیم
قرار نیست بشینیم میلیون‌ها Instruction رو یکی یکی بخونیم
اول دنبال جاهایی میگردیم که برنامه با داده خارجی سر و کار داره
مثلا توی Linux و برنامه‌های مختلف ممکنه با چیزهایی مثل این برخورد کنیم

read
recv
fread
fgets
scanf

توی Windows هم چیزهایی مثل

ReadFile
recv
fgets


میتونن سرنخ خوبی باشن
ولی اینجا یه نکته مهم داریم
صرفا دیدن اسم یک تابع به این معنی نیست که همونجا ورودی اصلی برنامه قرار داره
ممکنه داده از چندتا تابع رد بشه
ممکنه Wrapper داشته باشیم
ممکنه Parser جداگانه وجود داشته باشه
پس باید مسیر داده رو دنبال کنیم

مرحله دوم
دنبال کردن مسیر ورود داده
فرض کنید داخل Decompiler به چیزی شبیه این رسیدیم

buffer = malloc(256);
read(fd, buffer, size);
process_data(buffer);

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

External Input
↓
read
↓
buffer
↓
process_data

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

مرحله سوم
بفهمیم ورودی چه شکلیه
این قسمت خیلی مهمه
چون Fuzzer نمیتونه همینطوری یه مشت داده تصادفی بریزه توی برنامه و انتظار داشته باشه برنامه هم با خوشحالی همه رو پردازش کنه
برنامه معمولا انتظار داره ورودی یه ساختار خاص داشته باشه
مثلا ممکنه برنامه یک فایل بخونه

program sample.dat

یا ورودی رو از stdin بگیره

program < sample.dat

یا اصلا ورودی از شبکه بیاد

Client
↓
Network
↓
recv
↓
Parser

پس باید بفهمیم
ورودی از کجا میاد
چه فرمتی داره
کجا Parse میشه
و چه قسمت‌هایی از برنامه با اون کار میکنن این اطلاعات برای Fuzzing خیلی مهمن چون هرچی بیشتر ساختار ورودی رو بفهمیم

Fuzzer
هم میتونه ورودی‌های مفیدتری تولید کنه

مرحله چهارم
ساختن Seed
حالا فرض کنید فهمیدیم برنامه یک فایل با فرمت خاص خودش رو میخونه
مثلا:

sample.dat

اول یه فایل سالم و کوچک پیدا میکنیم
به این میگیم
Seed
بعد Fuzzer همین Seed رو تغییر میده

Seed
↓
Mutation
↓
Program
↓
New Path
یا
Crash`

مثلا یه بخش از فایل تغییر میکنه
بعد برنامه اجرا میشه

Fuzzer بررسی میکنه آیا برنامه وارد مسیر جدیدی شده یا نه
یا مثلا Crash کرده یا نه

چرا اینجا مهمه
اینجا دقیقا جاییه که این به کمک Fuzzing میاد
Fuzzer
به تنهایی همیشه نمیتونه بفهمه ساختار ورودی چیه
ولی ما میتونیم با بررسی Assembly و Decompiler سرنخ‌هایی پیدا کنیم

مثلا فرض کن توی Assembly میبینیم:

cmp byte ptr [rax], 48h
jne invalid

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

cmp dword ptr [rax+4], 100h

اینجا برنامه داره مقداری رو در Offset چهار بررسی میکنه
حالا اگه چندتا از این Checkها رو کنار هم بذاریم
کم کم میتونیم حدس بزنیم ورودی چه ساختاری داره
مثلا ممکنه به چیزی شبیه این برسیم:

Offset 0
↓
Magic

Offset 4
↓
Size

Offset 8
↓
Flags

Offset 12
↓
Data

یعنی بدون داشتن Source Code هم میتونیم از روی رفتار Binary کم کم ساختار Input Format رو دربیاریم
البته یه نکته مهم
این چیزی که از Assembly درمیاریم همیشه صددرصد قطعی نیست
ممکنه Compiler کد رو Optimize کرده باشه
ممکنه ساختار پیچیده‌تر باشه
ممکنه چندتا مسیر مختلف برای Parse کردن ورودی وجود داشته باشه
پس باید فرضیه‌هامون رو با اجرای واقعی برنامه و Debugging بررسی کنیم
یک نکته خیلی مهم

در Binary Only Fuzzing معمولا سه مرحله خیلی ارزشمند داریم

Input Discovery
↓
Program Analysis
Fuzzing

یعنی اول بفهمید
ورودی از کجا میاد
بعد بفهمید
چطور داخل برنامه پردازش میشه
بعد Fuzzing رو روی همون مسیر انجام بده
این خیلی بهتر از اینه که بدون شناخت برنامه فقط یه عالمه داده تصادفی تولید کنیم و امیدوار باشیم چیزی منفجر بشه
چون سیستم عامل و برنامه‌ها متاسفانه برای همکاری با آرزوهای ما طراحی نشدن
وقتی Crash پیدا شد
حالا فرض کنید Fuzzer یه نتیجه بهمون داده

Crash found
input_0421.dat

اینجا کار تموم نشده
اتفاقا تازه بخش جالب ماجرا شروع شده
همون Input رو دوباره با Debugger اجرا میکنیم

Input
↓
Program
↓
Crash
↓
Debugger
↓
Call Stack
↓
Faulting Instruction

حالا میخوایم بفهمیم
دقیقا کجا Crash اتفاق افتاده
کدوم Instruction باعثش شده

Registerها چه وضعیتی داشتن
Memory چه شکلی بوده
Call Stack چه مسیری رو نشون میده
و مهم‌تر از همه

Root Cause چی بوده
مثلا ممکنه اول فقط ببینیم برنامه روی یه Instruction خاص Crash کرده
ولی این هنوز به معنی این نیست که همون Instruction ریشه اصلی باگه
ممکنه چند مرحله قبل‌تر داده خراب وارد برنامه شده باشه و اثرش تازه اینجا خودش رو نشون داده باشه
پس مسیر تحلیل میتونه این شکلی بشه:

Crash
↓
Faulting Instruction
↓
Register State
↓
Memory State
↓
Call Stack
↓
Data Flow
↓
Root Cause

این دقیقا همون جاییه که Fuzzing و Reverse Engineering دست همدیگه رو میگیرن
Fuzzer میگه
این Input باعث Crash شد
Reverse Engineer میگه
خب حالا ببینیم چرا


وقتی Source Code نداریم
اول نمیریم سراغ Fuzzing کورکورانه
اول Binary رو بررسی میکنیم
بعد دنبال Input میگردیم
بعد مسیر حرکت Input داخل برنامه رو پیدا میکنیم بعد تا جایی که میتونیم ساختار Input Format رو میفهمیم
بعد Seed مناسب پیدا میکنیم
بعد Fuzzer رو روی اون مسیر قرار میدیم
و اگر Crash پیدا شد
برمیگردیم سراغ Debugger و Reverse Engineering
کل مسیر رو میشه اینطوری دید

Binary
↓
Reverse Engineering
↓
Input Discovery
↓
Input Analysis
↓
Seed
↓
Fuzzing
↓
Crash
↓
Debugger
↓
Root Cause Analysis

نکته اصلی این پست اینه
وقتی Source Code ندارید
به این معنی نیست که نمیتونید Fuzzing انجام بدی
فقط باید قبل از Fuzzing
خود Binary رو بشناسید
Reverse Engineering اینجا کمک میکنه بفهمی
داده از کجا وارد میشه
کجا Parse میشه
چه بخش‌هایی از Input مهم هستن
و چه مسیرهایی داخل برنامه طی میشن
و همین اطلاعات میتونه کیفیت Fuzzing رو خیلی بهتر کنه

@reverseengine
Binary Only Fuzzing

When we don't have source code
So far, we have mostly seen Fuzzing on programs whose source code we have
But in Reverse Engineering and Fuzzing, there are many times when we only have an executable file
For example

program.exe

or

program

We don't have the source
Or proper documentation
Or we don't even know exactly what's going on inside the program
This is where we need to first examine the program
To understand where it gets its input
To understand how that input moves inside the program
Then prepare the same path for Fuzzing

Step One
Opening the Binary
First, we open the program in tools like Ghidra or IDA
Our goal is not to understand the entire program from the beginning
We are not going to sit down and read millions of instructions one by one
First, we look for places where the program deals with external data
For example, in Linux and various programs, we may encounter things like

read
recv
fread
fgets
scanf
In Windows also has things like

ReadFile

recv

fgets

can be a good clue

But here's an important point

Just seeing the name of a function doesn't mean that the main input to the program is there

Data may be passed through several functions

We may have a Wrapper

There may be a separate Parser

So we need to follow the data path

Step 2
Follow the data path
Suppose we get something like this in the Decompiler

buffer = malloc(256);

read(fd, buffer, size);

process_data(buffer);

Here we have a very good path

External Input
↓
read
↓
buffer
↓
process_data

That is, external data entered the program

It is placed in a Buffer

Then it goes into a function that probably processes that data. Now we not only know that the program has input, but we also have a clue where the input goes after it enters the program

Step 3
Find out what the input looks like

This part is very important

Because the Fuzzer cannot just throw a bunch of random data into the program and expect the program to happily process it all

The program usually expects the input to have a specific structure

For example, the program may read a file

program sample.dat

Or take input from stdin

program < sample.dat

Or even input from the network

Client
↓
Network
↓
recv
↓
Parser

So we need to understand

Where the input comes from

What format it is

Where it is parsed

And what parts of the program work with it. This information is very important for Fuzzing because the more Understand the input structure

Fuzzer
can also generate more useful inputs

Step 4
Creating a Seed
Now suppose we understand that the program reads a file with its own format
For example:

sample.dat

First we find a healthy and small file
We call this
Seed
Then the Fuzzer modifies this Seed

Seed
↓
Mutation
↓
Program
↓
New Path
or
Crash
For example, a part of the file changes
Then the program runs

Fuzzer checks whether the program has entered a new path or not
Or whether it has crashed

Why is this important
This is exactly where Fuzzing comes in handy
Fuzzer
Alone can't always figure out what the input structure is
But we can find clues by examining the Assembly and Decompiler

For example, suppose we see in the Assembly:

cmp byte ptr [rax], 48h
jne invalid

Here we see the program is checking a specific Byte
That is, it is probably A specific part of the input is important to the program
We will see it later in another place

cmp dword ptr [rax+4], 100h

Here the program is checking a value at Offset 4
Now if we put a few of these checks together
We can gradually guess what the input structure is
For example, we might get something like this:

Offset 0
↓
Magic

Offset 4
↓
Size

Offset 8
↓
Flags

Offset 12
↓
Data
That is, without having the Source Code, we can gradually deduce the Input Format structure from the Binary behavior
Of course, one important point
What we get from Assembly is not always 100% certain
The compiler may have optimized the code
The structure may be more complex
There may be several different paths to parse the input
So we need to check our hypotheses by actually running the program and debugging
A very important point
In Binary Only Fuzzing, there are usually three We have a very valuable stage

Input Discovery
↓
Program Analysis
Fuzzing

means first understand
where the input comes from
then understand
how it is processed inside the program
then do Fuzzing on that path
This is much better than just generating a bunch of random data without knowing the program and hoping that something will explode
Because the operating system and programs are unfortunately not designed to cooperate with our wishes
When a Crash is found
Now suppose the Fuzzer gives us a result

Crash found
input_0421.dat

The work is not over here
Incidentally, the interesting part has just begun
We run the same Input again with the Debugger

Input
↓
Program
↓
Crash
↓
Debugger
↓
Call Stack
↓
Faulting Instruction

Now we want to find out
Where exactly the Crash occurred
Which instruction caused it
What state did the registers have
What was the memory like
What path does the Call Stack show
And most importantly
What was the Root Cause
For example, we might first just see the program crash on a specific instruction
But this still does not mean that the same Instruction is the root cause of the bug
It is possible that corrupt data was entered into the program a few steps earlier and its effect has only shown itself here.
So the analysis path can look like this:

Crash
↓
Faulting Instruction
↓
Register State
↓
Memory State
↓
Call Stack
↓
Data Flow
↓
Root Cause

This is exactly where Fuzzing and Reverse Engineering come together
Fuzzer says
This Input caused the Crash
Reverse Engineer says
Well, now let's see why

When we don't have Source Code
First, we don't go to Fuzzing blindly
First, we examine the Binary
Then we look for Input
Then we find the path of Input movement inside the program, then we understand the Input Format structure as much as we can
Then we find the appropriate Seed
Then we put the Fuzzer on that path
And if a Crash is found
We go back to Debugger and Reverse Engineering
The whole path can be seen like this

Binary
↓
Reverse Engineering
↓
Input Discovery
↓
Input Analysis
↓
Seed
↓
Fuzzing
↓
Crash
↓
Debugger
↓
Root Cause Analysis

The main point of this post is that
when you don't have the source code
it doesn't mean you can't do fuzzing
you just need to understand the binary before fuzzing
Reverse Engineering helps you understand
where the data comes from
where it is parsed
what parts of the input are important
and what paths it takes in the program
and this information can greatly improve the quality of fuzzing

@reverseengine