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
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
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
From Chrome renderer code exec to kernel with MSG_OOB
https://projectzero.google/2025/08/from-chrome-renderer-code-exec-to-kernel.html
@reverseengine
https://projectzero.google/2025/08/from-chrome-renderer-code-exec-to-kernel.html
@reverseengine
projectzero.google
From Chrome renderer code exec to kernel with MSG_OOB
IntroductionIn early June, I was reviewing a new Linux kernel feature when I learned about the MS...
State divergence enables unauthorized access
https://blog.trailofbits.com/2026/08/25/state-divergence-enables-unauthorized-access
@reverseengine
https://blog.trailofbits.com/2026/08/25/state-divergence-enables-unauthorized-access
@reverseengine
The Trail of Bits Blog
State divergence enables unauthorized access
We found and reported a bug in Provenance Blockchain, a public proof-of-stake chain built on Cosmos SDK, that lets any user grant themselves admin control over marker accounts without holding a single token.
From P-Code to GNN: extract binary code semantics
https://blog.quarkslab.com/from-p-code-to-gnn-extract-binary-code-semantics.html
@reverseengine
https://blog.quarkslab.com/from-p-code-to-gnn-extract-binary-code-semantics.html
@reverseengine
Quarkslab
From P-Code to GNN: extract binary code semantics - Quarkslab's blog
pcode_graph is a Python library, published by Quarkslab, suitable to build semantic graphs from binary code. We present how to use it to detect function similarities in binaries.
Kernel Shield: Reversing the NSecKrnl (Malops.io) Rootkit Driver
https://medium.com/@sushant.m.mane/kernel-shield-reversing-the-nseckrnl-malops-io-rootkit-driver-fb4af6bcb19c
https://medium.com/@sushant.m.mane/kernel-shield-reversing-the-nseckrnl-malops-io-rootkit-driver-fb4af6bcb19c
Medium
Kernel Shield: Reversing the NSecKrnl (Malops.io) Rootkit Driver
Challenge URL: https://malops.io/challenges/kernel-shield
Reverse Engineering a 0day used Against EDRs
https://medium.com/@jehadbudagga/reverse-engineering-a-0day-used-against-crowdstrike-edr-a5ea1fbe3fd4
https://medium.com/@jehadbudagga/reverse-engineering-a-0day-used-against-crowdstrike-edr-a5ea1fbe3fd4
Medium
Reverse Engineering a 0day used Against EDRs
Hello again…
😁3
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
⚔️ 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
An in-depth exploration of DLL Hijacking and related techniques, covering how DLL loading works, how vulnerable applications can be identified, and the differences between classic DLL Hijacking, DLL Sideloading, DLL Proxying, and Phantom DLL Hijacking.
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
⚔️ 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
DarkBit
The Art of DLL Hijacking
An in-depth exploration of DLL Hijacking and related techniques, covering how DLL loading works, how vulnerable applications can be identified, and the differences between classic DLL Hijacking, DLL Sideloading, DLL Proxying, and Phantom DLL Hijacking.
❤3
exit
وقتی یک Process کارش تموم میشه چه اتفاقی میوفته
تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم
حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره
مثلا یه برنامه خیلی ساده داریم:
وقتی main تموم میشه برنامه هم باید به سیستم عامل بفهمونه که
کار من تموم شده
اینجاست که مفهوم exit وارد داستان میشه
exit دقیقا چیکار میکنه
خیلی ساده بخوایم بگیم
exit
به سیستم عامل میگه این Process دیگه کاری نداره و میخواد اجرای خودش رو تموم کنه
مثلا:
اون عدد 0 معمولا یعنی برنامه با موفقیت تموم شده
یعنی
exit(0)
↓
اجرای موفق
ولی اگه مقدار غیر صفر بدیم معمولا یعنی یه وضعیت دیگه اتفاق افتاده
مثلا یه خطا
البته معنی دقیق اون عدد رو خود برنامه مشخص میکنه
پس قرار نیست Kernel بفهمه عدد 1 دقیقا یعنی چه خطایی
این عدد بیشتر یه اطلاعاتیه که Process به Parent میده
بعد از exit چی میشه
اینجا یه نکته مهم داریم
وقتی Process به exit میرسه دیگه قرار نیست دستورهای معمول برنامه رو ادامه بده
یعنی اجرای برنامه تموم شده
ولی Kernel هنوز باید چندتا کار انجام بده
مثلا منابعی که Process استفاده میکرد باید مدیریت بشن
مثل:
منابع مربوط به 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 تموم میشه میتونه یه مقدار به عنوان وضعیت پایان خودش داشته باشه
مثلا
یعنی
یا
یعنی
توی 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 میکنه
وقتی یک 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
مثلا اگه بنویسیم:
وقتی 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
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:
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
or
That is
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
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
ReverseEngineering
exit وقتی یک Process کارش تموم میشه چه اتفاقی میوفته تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره مثلا یه برنامه خیلی ساده داریم: int main() { …
When a process terminates, the kernel cleans up resources that are no longer needed
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:
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
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 خطرناکه
میتونه سوال خیلی بزرگتری بپرسه
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
کدوم 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.
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.