ReverseEngineering
Pointer چیه و چرا در Binary Exploitation مهمه؟ تا اینجا دربارهی Heap و باگ هایی مثل UAF Double Free و Heap Overflow صحبت کردیم اما برای اینکه بفهمیم این باگها چطور میتونن روی رفتار برنامه تاثیر بذارن باید یک مفهوم پایه ای رو خیلی خوب بلد باشیم: Pointer…
What is Pointer
and why is it important in Binary Exploitation?
So far we have talked about Heap and bugs like UAF Double Free and Heap Overflow
But to understand how these bugs can affect the behavior of the program, we need to know a basic concept very well: Pointer
Pointer
In simple terms, a Pointer is a variable that holds the address of that data in memory instead of its own data
For example:
Here:
So ptr itself does not have the value 100
But it knows where 100 is located in memory
Why is Pointer so important in C and C++?
Because many low-level tasks are done with Pointers
For example, when we write:
The buffer variable is a Pointer
That is, malloc() provides a section of the Heap to the program and the address of that section is placed in the buffer
In simple terms:
When the Pointer no longer points to a valid memory
For example:
Using a buffer after free() can be problematic
Because the Pointer may still have an address, but the memory associated with it is no longer valid
This was the same concept we saw in Use-After-Free
Dangling Pointer
A Pointer that is not valid to another memory area is usually called a Dangling Pointer
For example:
But that address should no longer be used as a valid Object
What is the relationship between Pointer and Heap Overflow
Suppose there are several Objects in the Heap:
That is, a write error can subsequently cause a read or write error elsewhere in memory
That is why pointers are very important in memory corruption analysis
Is a pointer the same as an address?
Roughly, but to be more precise:
A pointer
is a variable or data value that holds a memory address
For example:
Here ptr is a pointer and its value is considered an address
The pointer itself is also stored in memory
That is:
Why is this important for exploitation?
Because in Binary Exploitation we are not just looking to corrupt data
One of the important questions is:
Can we change something that the program will later use as an address?
If the answer is yes, a Memory Corruption can have a much greater effect than changing a regular number
That is why when analyzing the Heap, we should pay special attention to the Pointers in:
Pointer is a variable that holds a memory address. In C and C++ programs, pointers play a very important role in Heap management and accessing objects. If a Pointer becomes corrupted or invalid, the program may later access the wrong address. That is why understanding Pointers is essential to understanding UAF Heap Overflow and many Memory Corruptions
@reverseengine
and why is it important in Binary Exploitation?
So far we have talked about Heap and bugs like UAF Double Free and Heap Overflow
But to understand how these bugs can affect the behavior of the program, we need to know a basic concept very well: Pointer
Pointer
In simple terms, a Pointer is a variable that holds the address of that data in memory instead of its own data
For example:
int value = 100;
int *ptr = &value;
Here:
value
│
│ 100
▼
[ 100 ]
ptr
│
└────────► address of value
So ptr itself does not have the value 100
But it knows where 100 is located in memory
Why is Pointer so important in C and C++?
Because many low-level tasks are done with Pointers
For example, when we write:
char *buffer = malloc(100);
The buffer variable is a Pointer
That is, malloc() provides a section of the Heap to the program and the address of that section is placed in the buffer
In simple terms:
bufferWhere does the problem start?
│
▼
Heap
+----------------+
| 100 bytes |
+----------------+
When the Pointer no longer points to a valid memory
For example:
char *buffer = malloc(100);
free(buffer);
Using a buffer after free() can be problematic
Because the Pointer may still have an address, but the memory associated with it is no longer valid
This was the same concept we saw in Use-After-Free
Dangling Pointer
A Pointer that is not valid to another memory area is usually called a Dangling Pointer
For example:
bufferThe buffer itself may still have the previous value
│
▼
[ Chunk ]
│
free()
│
▼
[ Freed ]
But that address should no longer be used as a valid Object
What is the relationship between Pointer and Heap Overflow
Suppose there are several Objects in the Heap:
+----------------+If a memory corruption causes a pointer to change, the program may later use that pointer to access a different address
| Object A |
+----------------+
| Pointer |
+----------------+
| Object B |
+----------------+
That is, a write error can subsequently cause a read or write error elsewhere in memory
That is why pointers are very important in memory corruption analysis
Is a pointer the same as an address?
Roughly, but to be more precise:
A pointer
is a variable or data value that holds a memory address
For example:
ptr = 0x12345678
Here ptr is a pointer and its value is considered an address
The pointer itself is also stored in memory
That is:
PointerSo the pointer itself is data and can naturally be affected by memory corruption
│
▼
[ address ]
Why is this important for exploitation?
Because in Binary Exploitation we are not just looking to corrupt data
One of the important questions is:
Can we change something that the program will later use as an address?
If the answer is yes, a Memory Corruption can have a much greater effect than changing a regular number
That is why when analyzing the Heap, we should pay special attention to the Pointers in:
Objects
Data structures
Heap metadata
Tables and references
Pointer is a variable that holds a memory address. In C and C++ programs, pointers play a very important role in Heap management and accessing objects. If a Pointer becomes corrupted or invalid, the program may later access the wrong address. That is why understanding Pointers is essential to understanding UAF Heap Overflow and many Memory Corruptions
@reverseengine
Direct Syscall vs Indirect Syscall
تا اینجا فهمیدیم Direct Syscall یعنی برنامه تلاش میکنه مسیر معمول User-Mode API رو کوتاه تر کنه
حالا سوال:
Indirect Syscall
ایده اصلی
در Direct Syscall اجرای دستور
بهصورت مفهومی:
اما در Indirect Syscall ایده اینه که اجرای
هدف مفهومی این تکنیک تغییر شکل Call Stack و مسیر User-Mode execution نسبت به Direct Syscall هست
چرا این موضوع برای EDR مهمه؟
EDR
فقط نمیپرسه:
syscall اتفاق افتاد؟
بلکه میتونه سوالهای بیشتری بپرسه:
چه Process ی syscall رو انجام داده؟
Call Stack چطور بوده؟
Thread از کجا شروع شده؟
Memory مربوط به کجاست؟
قبل و بعد از syscall چه اتفاقی افتاده؟
پس:
Direct Syscall
≠
Invisible
و:
Indirect Syscall
EDR Bypass
این دو بیشتر تغییر مسیر اجرای User Mode هستن نه حذف کامل visibility
حالا یک لایه پایینتر
اینجا میرسیم به یکی از مهمترین چیزهایی که باید برای فهم EDR Evasion بلد باشید:
Memory Permissions
هر Memory Region میتونه مجوزهایی مثل این داشته باشه:
R = Read
W = Write
X = Execute
مثلا:
RW
یعنی قابل خوندن و نوشتنه ولی نباید به عنوان کد اجرا بشه
و:
RX
یعنی قابل خوندن و اجراست ولی نوشتن روی اون مجاز نیست
پس RWX چیه؟
RWX
یعنی یک ناحیه همزمان:
داره
این نوع Memory Permission میتونه برای ما جالب باشه چون کدی که همزمان قابل تغییر و اجراست در بعضی سناریوها ریسک بیشتری ایجاد میکنه البته RWX بهتنهایی به معنی بدافزار بودن نیست بعضی نرمافزارها کاملا legitimate هم ممکنه چنین Memory هایی داشته باشن
نکته مهم این قسمت
EDR
ها معمولا فقط به این نگاه نمیکنن که این Memory چه Permission ی داره
بلکه تغییرات Permission و رفتار اطرافش هم اهمیت داره
مثلا از دید تحلیلی
همین زنجیره میتونه مهمتر از دیدن یک Memory Region به تنهایی باشه
زنجیره مون اینه:
@reverseengine
تا اینجا فهمیدیم Direct Syscall یعنی برنامه تلاش میکنه مسیر معمول User-Mode API رو کوتاه تر کنه
حالا سوال:
Indirect Syscall
ایده اصلی
در Direct Syscall اجرای دستور
syscall از کدی انجام میشه که خود برنامه یا یک Stub مشخص فراهم کردهبهصورت مفهومی:
Application
│
▼
Custom Syscall Stub
│
▼
syscall
│
▼
Kernel
اما در Indirect Syscall ایده اینه که اجرای
syscall از یک مسیر/Stub موجود در فضای User Mode انجام بشهApplication
│
▼
Indirect Path
│
▼
Known syscall stub
│
▼
Kernel
هدف مفهومی این تکنیک تغییر شکل Call Stack و مسیر User-Mode execution نسبت به Direct Syscall هست
چرا این موضوع برای EDR مهمه؟
EDR
فقط نمیپرسه:
syscall اتفاق افتاد؟
بلکه میتونه سوالهای بیشتری بپرسه:
چه Process ی syscall رو انجام داده؟
Call Stack چطور بوده؟
Thread از کجا شروع شده؟
Memory مربوط به کجاست؟
قبل و بعد از syscall چه اتفاقی افتاده؟
پس:
Direct Syscall
≠
Invisible
و:
Indirect Syscall
EDR Bypass
این دو بیشتر تغییر مسیر اجرای User Mode هستن نه حذف کامل visibility
حالا یک لایه پایینتر
اینجا میرسیم به یکی از مهمترین چیزهایی که باید برای فهم EDR Evasion بلد باشید:
Memory Permissions
هر Memory Region میتونه مجوزهایی مثل این داشته باشه:
R = Read
W = Write
X = Execute
مثلا:
RW
یعنی قابل خوندن و نوشتنه ولی نباید به عنوان کد اجرا بشه
و:
RX
یعنی قابل خوندن و اجراست ولی نوشتن روی اون مجاز نیست
پس RWX چیه؟
RWX
یعنی یک ناحیه همزمان:
Read + Write + Execute
داره
این نوع Memory Permission میتونه برای ما جالب باشه چون کدی که همزمان قابل تغییر و اجراست در بعضی سناریوها ریسک بیشتری ایجاد میکنه البته RWX بهتنهایی به معنی بدافزار بودن نیست بعضی نرمافزارها کاملا legitimate هم ممکنه چنین Memory هایی داشته باشن
نکته مهم این قسمت
EDR
ها معمولا فقط به این نگاه نمیکنن که این Memory چه Permission ی داره
بلکه تغییرات Permission و رفتار اطرافش هم اهمیت داره
مثلا از دید تحلیلی
Memory Allocation
↓
Write
↓
Permission Change
↓
Execution
همین زنجیره میتونه مهمتر از دیدن یک Memory Region به تنهایی باشه
زنجیره مون اینه:
API Hooking
↓
User Mode / Kernel Mode
↓
Direct Syscall
↓
Indirect Syscall
↓
Memory Permissions
@reverseengine
ReverseEngineering
Direct Syscall vs Indirect Syscall تا اینجا فهمیدیم Direct Syscall یعنی برنامه تلاش میکنه مسیر معمول User-Mode API رو کوتاه تر کنه حالا سوال: Indirect Syscall ایده اصلی در Direct Syscall اجرای دستور syscall از کدی انجام میشه که خود برنامه یا یک Stub…
Direct Syscall vs Indirect Syscall
So far we have understood that Direct Syscall means that the application tries to shorten the usual path of the User-Mode API
Now the question:
Indirect Syscall
The main idea
In Direct Syscall, the execution of the syscall command is done from the code that the application itself or a specific stub provides
Conceptually:
But in Indirect Syscall, the idea is to execute the syscall from a path/stub existing in the User Mode space
The conceptual goal of this technique is to change the Call Stack shape and the User-Mode execution path compared to Direct Syscall
Why is this important for EDR?
EDR
does not just ask:
Did the syscall happen?
It can ask more questions:
What process made the syscall?
What was the call stack like?
Where did the thread start?
Where is the memory?
What happened before and after the syscall?
So:
Direct Syscall
≠
Invisible
And:
These two are more of a redirection of User Mode execution than a complete removal of visibility
Now one layer lower
Here we come to one of the most important things you need to know to understand EDR Evasion:
Memory Permissions
Each Memory Region can have permissions like this:
For example:
RW
It can be read and written, but it should not be executed as code
And:
RX
It can be read and executed, but writing to it is not allowed
So what is RWX?
RWX
It means a simultaneous region:
Read + Write + Execute
This type of Memory Permission can be interesting for us because code that can be modified and executed simultaneously poses a higher risk in some scenarios. Of course, RWX alone does not mean it is malware. Some completely legitimate software may also have such memories.
The important point of this section
EDRs usually do not only look at what permission this memory has.
But the permission changes and the behavior around it are also important.
For example, from an analytical point of view
This chain can be more important than looking at a Memory Region alone.
Our chain is:
@reverseengine
So far we have understood that Direct Syscall means that the application tries to shorten the usual path of the User-Mode API
Now the question:
Indirect Syscall
The main idea
In Direct Syscall, the execution of the syscall command is done from the code that the application itself or a specific stub provides
Conceptually:
Application
│
▼
Custom Syscall Stub
│
▼
syscall
│
▼
Kernel
But in Indirect Syscall, the idea is to execute the syscall from a path/stub existing in the User Mode space
Application
│
▼
Indirect Path
│
▼
Known syscall stub
│
▼
Kernel
The conceptual goal of this technique is to change the Call Stack shape and the User-Mode execution path compared to Direct Syscall
Why is this important for EDR?
EDR
does not just ask:
Did the syscall happen?
It can ask more questions:
What process made the syscall?
What was the call stack like?
Where did the thread start?
Where is the memory?
What happened before and after the syscall?
So:
Direct Syscall
≠
Invisible
And:
Indirect Syscall
EDR Bypass
These two are more of a redirection of User Mode execution than a complete removal of visibility
Now one layer lower
Here we come to one of the most important things you need to know to understand EDR Evasion:
Memory Permissions
Each Memory Region can have permissions like this:
R = Read
W = Write
X = Execute
For example:
RW
It can be read and written, but it should not be executed as code
And:
RX
It can be read and executed, but writing to it is not allowed
So what is RWX?
RWX
It means a simultaneous region:
Read + Write + Execute
This type of Memory Permission can be interesting for us because code that can be modified and executed simultaneously poses a higher risk in some scenarios. Of course, RWX alone does not mean it is malware. Some completely legitimate software may also have such memories.
The important point of this section
EDRs usually do not only look at what permission this memory has.
But the permission changes and the behavior around it are also important.
For example, from an analytical point of view
Memory Allocation
↓
Write
↓
Permission Change
↓
Execution
This chain can be more important than looking at a Memory Region alone.
Our chain is:
API Hooking
↓
User Mode / Kernel Mode
↓
Direct Syscall
↓
Indirect Syscall
↓
Memory Permissions
@reverseengine
بخش بیست و شیشم بافر اورفلو
Valgrind
چیه و چه فرقی با ASan داره
توی قسمت قبل با ASan آشنا شدیم
اینجا میخوایم بریم سراغ Valgrind و ببینیم چطور میتونه Memory Bugها رو پیدا کنه
بعد هم خیلی ساده ASan و Valgrind رو با هم مقایسه میکنیم
Valgrind
برنامه رو زیر نظر میگیره و دسترسی های حافظه رو بررسی میکنه
مثلا میتونه مواردی مثل اینا رو پیدا کنه
یک مثال ساده:
فایل
C
اینجا فقط برای 4 عدد حافظه گرفتیم
ولی داریم عضو شماره 5 رو مینویسیم
پس یک Out of Bounds Write داریم
کامپایل
shell
بعد با Valgrind اجراش میکنیم
shell
Valgrind
گزارش میده که برنامه یک دسترسی غیرمجاز به حافظه داشته
قسمت مهمش برای Reverse Engineering
فرض کنید یک برنامه پیچیده دارید
برنامه کرش میکنه ولی هنوز نمیدونید مشکل دقیقا کجاست
Valgrind
میتونه Stack Trace و اطلاعات مربوط به دسترسی اشتباه رو نشون بده
بعد
میتونید همون تابع رو داخل Ghidra یا IDA باز کنید و Assembly اون قسمت رو بررسی کنید
یعنی دوباره این مسیر رو داریم
برنامه
در مقابل ASan
خیلی ساده بخوایم بگیم
ASan
معمولا سریع تره و برای Fuzzing و تستهای مداوم خیلی کاربردیه
Valgrind
نیازی به کامپایل با ASan نداره و ابزارهای مختلفی برای تحلیل رفتار برنامه در اختیارمون میذاره البته Valgrind معمولا سربار اجرایی بیشتری داره
یک نکته مهم:
Valgrind
و ASan جای Reverse Engineering رو نمیگیرن
اونا فقط کمک میکنن سریع تر بفهمیم
کجا باید دنبال مشکل بگردیم
بعد کار اصلی ما شروع میشه
یعنی رفتن داخل Ghidra یا IDA و فهمیدن اینکه چرا این Memory Bug اتفاق افتاده
تا اینجا سه ابزار مهم رو داریم
Fuzzer
↓
Crash پیدا میکنه
ASan / Valgrind
↓
Memory Bug رو تحلیل میکنن
Ghidra / IDA
↓
علت Bug رو از روی Binary بررسی میکنیم این دقیقا همون ترکیبیه که یک Reverse Engineer برای تحلیل Memory Bug ها باید کم کم بهش مسلط بشه
@reverseengine
Valgrind
چیه و چه فرقی با ASan داره
توی قسمت قبل با ASan آشنا شدیم
اینجا میخوایم بریم سراغ Valgrind و ببینیم چطور میتونه Memory Bugها رو پیدا کنه
بعد هم خیلی ساده ASan و Valgrind رو با هم مقایسه میکنیم
Valgrind
برنامه رو زیر نظر میگیره و دسترسی های حافظه رو بررسی میکنه
مثلا میتونه مواردی مثل اینا رو پیدا کنه
Invalid Readاستفاده نادرست از حافظه
Invalid Write
Use After Free
Memory Leak
یک مثال ساده:
فایل
C
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *data = malloc(4 * sizeof(int));
data[5] = 100;
free(data);
return 0;
}
اینجا فقط برای 4 عدد حافظه گرفتیم
ولی داریم عضو شماره 5 رو مینویسیم
پس یک Out of Bounds Write داریم
کامپایل
shell
gcc -g file22_demo.c -o file22_demo
بعد با Valgrind اجراش میکنیم
shell
valgrind ./file22_demo
Valgrind
گزارش میده که برنامه یک دسترسی غیرمجاز به حافظه داشته
قسمت مهمش برای Reverse Engineering
فرض کنید یک برنامه پیچیده دارید
برنامه کرش میکنه ولی هنوز نمیدونید مشکل دقیقا کجاست
Valgrind
میتونه Stack Trace و اطلاعات مربوط به دسترسی اشتباه رو نشون بده
بعد
میتونید همون تابع رو داخل Ghidra یا IDA باز کنید و Assembly اون قسمت رو بررسی کنید
یعنی دوباره این مسیر رو داریم
برنامه
↓Valgrind
Valgrind
↓
Memory Error
↓
Stack Trace
↓
Ghidra / IDA
↓
Assembly Analysis
در مقابل ASan
خیلی ساده بخوایم بگیم
ASan
معمولا سریع تره و برای Fuzzing و تستهای مداوم خیلی کاربردیه
Valgrind
نیازی به کامپایل با ASan نداره و ابزارهای مختلفی برای تحلیل رفتار برنامه در اختیارمون میذاره البته Valgrind معمولا سربار اجرایی بیشتری داره
یک نکته مهم:
Valgrind
و ASan جای Reverse Engineering رو نمیگیرن
اونا فقط کمک میکنن سریع تر بفهمیم
کجا باید دنبال مشکل بگردیم
بعد کار اصلی ما شروع میشه
یعنی رفتن داخل Ghidra یا IDA و فهمیدن اینکه چرا این Memory Bug اتفاق افتاده
تا اینجا سه ابزار مهم رو داریم
Fuzzer
↓
Crash پیدا میکنه
ASan / Valgrind
↓
Memory Bug رو تحلیل میکنن
Ghidra / IDA
↓
علت Bug رو از روی Binary بررسی میکنیم این دقیقا همون ترکیبیه که یک Reverse Engineer برای تحلیل Memory Bug ها باید کم کم بهش مسلط بشه
@reverseengine
❤2
Part 26 Buffer Overflow
What is Valgrind and how is it different from ASan?
We met ASan in the previous section.
Here we want to go to Valgrind and see how it can find Memory Bugs.
Then we will compare ASan and Valgrind very simply.
Valgrind
monitors the program and checks memory accesses.
For example, it can find things like these:
Invalid Read
Invalid Write
Use After Free
Memory Leak
Incorrect memory usage
A simple example:
C file
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *data = malloc(4 * sizeof(int));
data[5] = 100;
free(data);
return 0;
}
Here we only got 4 memory slots
But we are writing member number 5
So we have an Out of Bounds Write
Compile
shell
gcc -g file22_demo.c -o file22_demo
Then we run it with Valgrind
shell
valgrind ./file22_demo
Valgrind
reports that the program has an illegal memory access
The important part is for Reverse Engineering
Suppose you have a complex program
The program crashes but you still don't know exactly where the problem is
Valgrind
can show Stack Trace and information about the incorrect access
Then
You can open the same function in Ghidra or IDA and check the Assembly of that part
That means we have this path again
Program
↓
Valgrind
↓
Memory Error
↓
Stack Trace
↓
Ghidra / IDA
↓
Assembly Analysis
Valgrind vs. ASan
To put it simply
ASan
is usually faster and for Fuzzing and continuous testing are very useful
Valgrind
Does not require compilation with ASan and provides us with various tools to analyze the behavior of the program, of course Valgrind usually has more execution overhead
An important point:
Valgrind
and ASan do not replace Reverse Engineering
They only help us understand faster
Where to look for the problem
Then our main work begins
That is, going into Ghidra or IDA and understanding why this Memory Bug occurred
So far we have three important
tools
Fuzzer
↓
Finds a crash
ASan / Valgrind
↓
Analyzes the Memory Bug
Ghidra / IDA
↓
We examine the cause of the Bug from the Binary This is exactly the combination that a Reverse Engineer should gradually master to analyze Memory Bugs
@reverseengine
Reverse Engineering Mobile Apps: 15 Tools & Tips to Master Security Analysis
https://medium.verylazytech.com/reverse-engineering-mobile-apps-15-tools-tips-to-master-security-analysis-37f50b1c155a
https://medium.verylazytech.com/reverse-engineering-mobile-apps-15-tools-tips-to-master-security-analysis-37f50b1c155a
Medium
Reverse Engineering Mobile Apps: 15 Tools & Tips to Master Security Analysis
Ever wondered how hackers crack open mobile apps, extract secrets, and find vulnerabilities? You’d be shocked how many modern apps leak…
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…
Memory Instruction Scheduling for Lock-Stepped Kernels on AMD Instinct MI300X: Introducing the Series
https://rocm.blogs.amd.com/software-tools-optimization/scheduling_memory_ops_gfx942/README.html
@reverseengine
https://rocm.blogs.amd.com/software-tools-optimization/scheduling_memory_ops_gfx942/README.html
@reverseengine
ROCm Blogs
Memory Instruction Scheduling for Lock-Stepped Kernels on AMD Instinct™ MI300X: Introducing the Series
Explore how instruction scheduling eases stalls on AMD Instinct MI300X in a new blog series. This intro covers the motivating example and methodology.
How Trail of Bits helps verify the integrity of your Signal chats
https://blog.trailofbits.com/2026/08/11/how-trail-of-bits-helps-verify-the-integrity-of-your-signal-chats
@reverseengine
https://blog.trailofbits.com/2026/08/11/how-trail-of-bits-helps-verify-the-integrity-of-your-signal-chats
@reverseengine
The Trail of Bits Blog
How Trail of Bits helps verify the integrity of your Signal chats
Signal recently launched Automatic Key Verification, a feature that helps validate that your chats are secure without requiring direct safety number comparison. Trail of Bits built and operates one of the three auditors that make this system trustworthy.
LLM Poisoning [1/3] - Reading the thoughts of a Transformer
https://www.synacktiv.com/publications/llm-poisoning-13-lire-les-pensees-dun-transformer
@reverseengine
https://www.synacktiv.com/publications/llm-poisoning-13-lire-les-pensees-dun-transformer
@reverseengine
Synacktiv
LLM Poisoning [1/3] - Lire les pensées d'un Transformer
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.
Instruction Substitution
وقتی یک دستور رو با چند دستور دیگه عوض میکنیم
تا اینجا درباره روشهای مختلفی صحبت کردیم که یک باینری میتونه طوری ساخته بشه که تحلیلش سخت تر بشه
حالا بریم سراغ یکی از تکنیکهای ساده ولی خیلی کاربردی:
Instruction Substitution
ایدهاش خیلی ساده ست:
بهجای اینکه از یک دستور مشخص استفاده کنیم کاری میکنیم که چند دستور دیگه دقیقا همون نتیجه رو تولید کنه
یعنی مثلا اگر یک عملیات رو بتونیم با یک دستور انجام بدیم عمدا اون رو با چند دستور جایگزین میکنیم
یک مثال ساده:
فرض کنیم در Assembly میخایم یک رجیستر رو صفر کنیم
روش معمول:
بعد از اجرا:
اما میتونیم همین نتیجه رو به شکل دیگه ای ایجاد کنیم:
باز هم:
از نظر نتیجه نهایی هر دو یکی ان
ولی از نظر شکل کد متفاوتن
یک مثال دیگه
فرض کنیم:
میتونه با روشهای دیگه ای هم به صفر برسه:
یا:
پس یک منطق واحد میتونه چندین representation مختلف داشته باشه
این دقیقا همون چیزیه که Instruction Substitution روی اون حساب میکنه
چرا این کار مهمه؟
اینجا یک نکته مهم وجود داره
اگر یک Analyst یا ابزار تحلیل انتظار داشته باشیم یک عملیات رو با شکل خاصی ببینیم Instruction Substitution میتونه اون الگو رو تغییر بده
مثلا تحلیلگر ممکنه که در یک باینری دنبال:
بگرده
اما سازنده باینری به جای اون نوشته:
نتیجه یکیه ولی pattern عوض شده
یک مثال کمی جالبتر
فرض کنیم میخواهیم:
رو انجام بدیم
میتونیم از:
استفاده کنیم
هر دو مقدار EAX رو یک واحد افزایش میدن
یعنی:
قبل:
بعد:
اما Assembly متفاوته
حالا یک نکته مهم تر:
Instruction Substitution
فقط برای یک دستور ساده نیست
بعضی وقتا یک عملیات رو میتونیم با چند دستور پیاده کنیم
مثلا:
میتونیم به شکل دیگه ای بنویسیمش:
از نظر نتیجه میتونن معادل باشن هر چند این مثال عمدا ساده و ناکار آمده
هدف اینجا سرعت نیست
هدف اینه که:
یک عملیات چند representation متفاوت داشته باشیم
چرا مهمه؟
چون نباید فقط به شکل دستور نگاه کنه
باید بپرسید:
این چند دستور در اخر چه کاری انجام میدن
مثلا اگر ببینیم:
خیلی سریع میگیم:
اما اگر ببینیم:
باز هم باید همون مفهوم رو تشخیص بدیم
پس در Reverse Engineering باید کم کم از:
Instruction Thinking
به:
Semantic Thinking
حرکت کنیم
یعنی بهجای اینکه فقط بگیم:
این دستور چیه؟
بپرسیم:
نتیجه منطقی این قسمت از برنامه چیه؟
این موضوع در Decompiler هم مهمه
Decompiler
ها تلاش میکنن Assembly رو دوباره به چیزی شبیه کد سطح بالا تبدیل کنن
مثلا:
ممکنه به چیزی شبیه:
تبدیل بشه
اگر همین کار با چند دستور مختلف انجام شده باشه Decompiler باید بفهمه که این مجموعه در اخر چه معنایی دارن
پس:
اما یک نکته خیلی مهم
Instruction Substitution
همیشه کاملا معادل نیست
ممکنه دو دستور از نظر مقدار نهایی یکسان باشن ولی روی:
تاثیر متفاوتی داشته باشن
مثلا:
و:
از نظر مقدار EAX شبیهن اما رفتار Flag هاشون دقیقا یکی نیست
پس وقتی میگیم:
دو instruction equivalent هستند
باید مشخص کنیم:
از چه نظر؟
فقط نتیجه رجیستر؟
یا تمام State پردازنده؟
این تفاوت در Reverse Engineering خیلی مهمه
Instruction Substitution
در Obfuscation
حالا برگردیم به بحث اصلی
فرض کنیم یک برنامه این عملیات رو انجام میده:
در حالت عادی خیلی راحت قابل تشخیصه
اما اگر سازنده برنامه از روش های مختلف برای تولید همون نتیجه استفاده کنه الگوی Assembly تغییر میکنه
مثلا:
یا در شرایط مناسب ترکیب های پیچیده تر
حالا اگر همین کار در قسمتهای مختلف برنامه با شکل های مختلف تکرار بشه تحلیل pattern-based سخت تر میشه
مثلا:
در حالی که:
Semantic = یکی هست
@reverseengine
وقتی یک دستور رو با چند دستور دیگه عوض میکنیم
تا اینجا درباره روشهای مختلفی صحبت کردیم که یک باینری میتونه طوری ساخته بشه که تحلیلش سخت تر بشه
حالا بریم سراغ یکی از تکنیکهای ساده ولی خیلی کاربردی:
Instruction Substitution
ایدهاش خیلی ساده ست:
بهجای اینکه از یک دستور مشخص استفاده کنیم کاری میکنیم که چند دستور دیگه دقیقا همون نتیجه رو تولید کنه
یعنی مثلا اگر یک عملیات رو بتونیم با یک دستور انجام بدیم عمدا اون رو با چند دستور جایگزین میکنیم
یک مثال ساده:
فرض کنیم در Assembly میخایم یک رجیستر رو صفر کنیم
روش معمول:
xor eax, eax
بعد از اجرا:
EAX = 0
اما میتونیم همین نتیجه رو به شکل دیگه ای ایجاد کنیم:
sub eax, eax
باز هم:
EAX = 0
از نظر نتیجه نهایی هر دو یکی ان
ولی از نظر شکل کد متفاوتن
یک مثال دیگه
فرض کنیم:
mov eax, 0
میتونه با روشهای دیگه ای هم به صفر برسه:
xor eax, eax
یا:
sub eax, eax
پس یک منطق واحد میتونه چندین representation مختلف داشته باشه
این دقیقا همون چیزیه که Instruction Substitution روی اون حساب میکنه
چرا این کار مهمه؟
اینجا یک نکته مهم وجود داره
اگر یک Analyst یا ابزار تحلیل انتظار داشته باشیم یک عملیات رو با شکل خاصی ببینیم Instruction Substitution میتونه اون الگو رو تغییر بده
مثلا تحلیلگر ممکنه که در یک باینری دنبال:
xor eax, eax
بگرده
اما سازنده باینری به جای اون نوشته:
sub eax, eax
نتیجه یکیه ولی pattern عوض شده
یک مثال کمی جالبتر
فرض کنیم میخواهیم:
add eax, 1
رو انجام بدیم
میتونیم از:
inc eax
استفاده کنیم
هر دو مقدار EAX رو یک واحد افزایش میدن
یعنی:
قبل:
EAX = 10
بعد:
EAX = 11
اما Assembly متفاوته
حالا یک نکته مهم تر:
Instruction Substitution
فقط برای یک دستور ساده نیست
بعضی وقتا یک عملیات رو میتونیم با چند دستور پیاده کنیم
مثلا:
add eax, 5
میتونیم به شکل دیگه ای بنویسیمش:
push 5
pop ecx
add eax, ecx
از نظر نتیجه میتونن معادل باشن هر چند این مثال عمدا ساده و ناکار آمده
هدف اینجا سرعت نیست
هدف اینه که:
یک عملیات چند representation متفاوت داشته باشیم
چرا مهمه؟
چون نباید فقط به شکل دستور نگاه کنه
باید بپرسید:
این چند دستور در اخر چه کاری انجام میدن
مثلا اگر ببینیم:
xor eax, eax
خیلی سریع میگیم:
EAX = 0
اما اگر ببینیم:
sub eax, eax
باز هم باید همون مفهوم رو تشخیص بدیم
پس در Reverse Engineering باید کم کم از:
Instruction Thinking
به:
Semantic Thinking
حرکت کنیم
یعنی بهجای اینکه فقط بگیم:
این دستور چیه؟
بپرسیم:
نتیجه منطقی این قسمت از برنامه چیه؟
این موضوع در Decompiler هم مهمه
Decompiler
ها تلاش میکنن Assembly رو دوباره به چیزی شبیه کد سطح بالا تبدیل کنن
مثلا:
xor eax, eax
ممکنه به چیزی شبیه:
eax = 0
تبدیل بشه
اگر همین کار با چند دستور مختلف انجام شده باشه Decompiler باید بفهمه که این مجموعه در اخر چه معنایی دارن
پس:
Assemblyیکی از بخشهای مهم Reverse Engineering همین تشخیص semantic هست
↓
Instructions
↓
Semantic Analysis
↓
High-Level Representation
اما یک نکته خیلی مهم
Instruction Substitution
همیشه کاملا معادل نیست
ممکنه دو دستور از نظر مقدار نهایی یکسان باشن ولی روی:
Flags
Registers
Memory
Exception behavior
Timing
تاثیر متفاوتی داشته باشن
مثلا:
inc eax
و:
add eax, 1
از نظر مقدار EAX شبیهن اما رفتار Flag هاشون دقیقا یکی نیست
پس وقتی میگیم:
دو instruction equivalent هستند
باید مشخص کنیم:
از چه نظر؟
فقط نتیجه رجیستر؟
یا تمام State پردازنده؟
این تفاوت در Reverse Engineering خیلی مهمه
Instruction Substitution
در Obfuscation
حالا برگردیم به بحث اصلی
فرض کنیم یک برنامه این عملیات رو انجام میده:
xor eax, eax
در حالت عادی خیلی راحت قابل تشخیصه
اما اگر سازنده برنامه از روش های مختلف برای تولید همون نتیجه استفاده کنه الگوی Assembly تغییر میکنه
مثلا:
sub eax, eax
یا در شرایط مناسب ترکیب های پیچیده تر
حالا اگر همین کار در قسمتهای مختلف برنامه با شکل های مختلف تکرار بشه تحلیل pattern-based سخت تر میشه
مثلا:
Operation A
↓
Representation 1
Operation A
↓
Representation 2
Operation A
↓
Representation 3
در حالی که:
Semantic = یکی هست
@reverseengine
ReverseEngineering
Instruction Substitution وقتی یک دستور رو با چند دستور دیگه عوض میکنیم تا اینجا درباره روشهای مختلفی صحبت کردیم که یک باینری میتونه طوری ساخته بشه که تحلیلش سخت تر بشه حالا بریم سراغ یکی از تکنیکهای ساده ولی خیلی کاربردی: Instruction Substitution ایدهاش…
Instruction Substitution
When we replace an instruction with several other instructions
So far we have talked about different ways that a binary can be made to be more difficult to analyze
Now let's move on to one of the simple but very useful techniques:
Instruction Substitution
The idea is very simple:
Instead of using a specific instruction, we make several other instructions produce exactly the same result
That is, for example, if we can perform an operation with one instruction, we deliberately replace it with several instructions
A simple example:
Suppose we want to zero a register in Assembly
The usual method:
After execution:
But we can create the same result in another way:
Again:
In terms of the final result, both are the same
But in terms of the code format, they are different
Another example
Suppose:
Can also be zeroed in other ways:
or:
So a single logic can have multiple different representations
This is exactly what Instruction Substitution is counting on
Why is this important?
Here is an important point
If an Analyst or Analysis Tool expects to see an operation in a specific form, Instruction Substitution can change that pattern
For example, the Analyst might look for:
in a binary
but the Binary Builder instead writes:
The result is the same but the pattern has changed
A slightly more interesting example
Suppose we want to:
We can use:
Both increase the value of EAX by one
That is:
Before:
After:
But Assembly is different
Now a more important point:
Instruction Substitution
is not just for a simple instruction
Sometimes we can implement an operation with multiple instructions
For example:
We can do it in a different way Let's write it:
They can be equivalent in terms of the result, although this example is intentionally simple and ineffective.
The goal here is not speed.
The goal is to:
Have several different representations of an operation.
Why is it important?
Because you shouldn't just look at the form of the instruction.
You should ask:
What do these instructions do in the end?
For example, if we see:
we say very quickly:
But if we see:
we still have to recognize the same concept.
So in Reverse Engineering, we should gradually move from:
Instruction Thinking
to:
Semantic Thinking
That is, instead of just saying:
What is this instruction?
we should ask:
What is the logical result of this part of the program?
This is also important in Decompiler
Decompilers try to convert Assembly back to something like high-level code
For example:
may be converted to something like:
If the same thing is done with several different instructions, the Decompiler needs to understand what this set means in the end
So:
High-Level Representation
One of the important parts of Reverse Engineering is this semantic recognition
But one very important point
Instruction Substitution
is not always completely equivalent
Two instructions may be the same in terms of final value but have different effects on:
For example:
and:
are similar in terms of EAX value but their Flag behavior is not exactly the same
So when we say:
two instructions are equivalent Are
We need to specify:
In what sense?
Only the result of the register?
Or the entire processor state?
This difference is very important in Reverse Engineering
Instruction Substitution
In Obfuscation
Now let's return to the main discussion
Suppose a program performs this operation:
In the normal case, it is very easy to recognize
But if the program creator uses different methods to produce the same result, the Assembly pattern changes
For example:
When we replace an instruction with several other instructions
So far we have talked about different ways that a binary can be made to be more difficult to analyze
Now let's move on to one of the simple but very useful techniques:
Instruction Substitution
The idea is very simple:
Instead of using a specific instruction, we make several other instructions produce exactly the same result
That is, for example, if we can perform an operation with one instruction, we deliberately replace it with several instructions
A simple example:
Suppose we want to zero a register in Assembly
The usual method:
xor eax, eax
After execution:
EAX = 0
But we can create the same result in another way:
sub eax, eax
Again:
EAX = 0
In terms of the final result, both are the same
But in terms of the code format, they are different
Another example
Suppose:
mov eax, 0
Can also be zeroed in other ways:
xor eax, eax
or:
sub eax, eax
So a single logic can have multiple different representations
This is exactly what Instruction Substitution is counting on
Why is this important?
Here is an important point
If an Analyst or Analysis Tool expects to see an operation in a specific form, Instruction Substitution can change that pattern
For example, the Analyst might look for:
xor eax, eax
in a binary
but the Binary Builder instead writes:
sub eax, eax
The result is the same but the pattern has changed
A slightly more interesting example
Suppose we want to:
add eax, 1
We can use:
inc eax
Both increase the value of EAX by one
That is:
Before:
EAX = 10
After:
EAX = 11
But Assembly is different
Now a more important point:
Instruction Substitution
is not just for a simple instruction
Sometimes we can implement an operation with multiple instructions
For example:
add eax, 5
We can do it in a different way Let's write it:
push 5
pop ecx
add eax, ecx
They can be equivalent in terms of the result, although this example is intentionally simple and ineffective.
The goal here is not speed.
The goal is to:
Have several different representations of an operation.
Why is it important?
Because you shouldn't just look at the form of the instruction.
You should ask:
What do these instructions do in the end?
For example, if we see:
xor eax, eax
we say very quickly:
EAX = 0
But if we see:
sub eax, eax
we still have to recognize the same concept.
So in Reverse Engineering, we should gradually move from:
Instruction Thinking
to:
Semantic Thinking
That is, instead of just saying:
What is this instruction?
we should ask:
What is the logical result of this part of the program?
This is also important in Decompiler
Decompilers try to convert Assembly back to something like high-level code
For example:
xor eax, eax
may be converted to something like:
eax = 0
If the same thing is done with several different instructions, the Decompiler needs to understand what this set means in the end
So:
Assembly
↓
Instructions
↓
Semantic Analysis
↓
High-Level Representation
One of the important parts of Reverse Engineering is this semantic recognition
But one very important point
Instruction Substitution
is not always completely equivalent
Two instructions may be the same in terms of final value but have different effects on:
Flags
Registers
Memory
Exception behavior
Timing
For example:
inc eax
and:
add eax, 1
are similar in terms of EAX value but their Flag behavior is not exactly the same
So when we say:
two instructions are equivalent Are
We need to specify:
In what sense?
Only the result of the register?
Or the entire processor state?
This difference is very important in Reverse Engineering
Instruction Substitution
In Obfuscation
Now let's return to the main discussion
Suppose a program performs this operation:
xor eax, eax
In the normal case, it is very easy to recognize
But if the program creator uses different methods to produce the same result, the Assembly pattern changes
For example:
sub eax, eax
ReverseEngineering
Instruction Substitution وقتی یک دستور رو با چند دستور دیگه عوض میکنیم تا اینجا درباره روشهای مختلفی صحبت کردیم که یک باینری میتونه طوری ساخته بشه که تحلیلش سخت تر بشه حالا بریم سراغ یکی از تکنیکهای ساده ولی خیلی کاربردی: Instruction Substitution ایدهاش…
Or in appropriate conditions, more complex combinations
Now, if the same thing is repeated in different parts of the program with different forms, pattern-based analysis becomes more difficult
For example:
Representation 3
While:
Semantic = is one
@reverseengine
Now, if the same thing is repeated in different parts of the program with different forms, pattern-based analysis becomes more difficult
For example:
Operation A
↓
Representation 1
Operation A
↓
Representation 2
Operation A
↓
Representation 3
While:
Semantic = is one
@reverseengine
ReverseEngineering
Instruction Substitution وقتی یک دستور رو با چند دستور دیگه عوض میکنیم تا اینجا درباره روشهای مختلفی صحبت کردیم که یک باینری میتونه طوری ساخته بشه که تحلیلش سخت تر بشه حالا بریم سراغ یکی از تکنیکهای ساده ولی خیلی کاربردی: Instruction Substitution ایدهاش…
نکته طلایی
وقتی با کد Obfuscated مواجه شدید دنبال شکل ظاهری instruction ها نباشید
دنبال اثر اونها روی Machine State باشید
یعنی بررسی کنید:
Registerها چی شدن؟
Memory چه تغییری کردن؟
Flagها چی شدن؟
Control Flow کجا رفت؟
اگر اینها رو بفهمید حتی اگر شکل Assembly کاملا عجیب باشه میتونید منطق واقعی برنامه رو استخراج کنید
یک تمرین خیلی خوب
این چهار قطعه رو ببینید:
همه رو فقط از یک زاویه نگاه نکنید
برای هر کدوم بررسی کنید:
EAX چی میشه؟
Flags چی میشن؟
Stack چه تغییری میکنه؟
Memory چه تغییری میکنه؟
Instruction Substitution:
یک عملیات رو با instruction یا instruction های دیگه ای پیاده سازی کنیم بدون اینکه semantic مورد نظر تغییر کنه
هدف میتونه:
تغییر شکل کد
سختتر کردن Pattern Matching
افزایش تنوع instruction ها
Obfuscation
سختتر کردن Static Analysis
باشده
اما برای ما یک درس خیلی مهم داره:
Assembly
رو نباید فقط بر اساس اسم instruction ها تحلیل کنیم باید semantic و اثر واقعی اونها رو روی CPU بفهمیم
و این دقیقا ساختار ماست:
@reverseengine
وقتی با کد Obfuscated مواجه شدید دنبال شکل ظاهری instruction ها نباشید
دنبال اثر اونها روی Machine State باشید
یعنی بررسی کنید:
Registerها چی شدن؟
Memory چه تغییری کردن؟
Flagها چی شدن؟
Control Flow کجا رفت؟
اگر اینها رو بفهمید حتی اگر شکل Assembly کاملا عجیب باشه میتونید منطق واقعی برنامه رو استخراج کنید
یک تمرین خیلی خوب
این چهار قطعه رو ببینید:
xor eax, eax
sub eax, eax
mov eax, 0
push 0
pop eax
همه رو فقط از یک زاویه نگاه نکنید
برای هر کدوم بررسی کنید:
EAX چی میشه؟
Flags چی میشن؟
Stack چه تغییری میکنه؟
Memory چه تغییری میکنه؟
Instruction Substitution:
یک عملیات رو با instruction یا instruction های دیگه ای پیاده سازی کنیم بدون اینکه semantic مورد نظر تغییر کنه
هدف میتونه:
تغییر شکل کد
سختتر کردن Pattern Matching
افزایش تنوع instruction ها
Obfuscation
سختتر کردن Static Analysis
باشده
اما برای ما یک درس خیلی مهم داره:
Assembly
رو نباید فقط بر اساس اسم instruction ها تحلیل کنیم باید semantic و اثر واقعی اونها رو روی CPU بفهمیم
و این دقیقا ساختار ماست:
Assembly Basics
↓
Disassembly
↓
Instruction Semantics
↓
Obfuscation
↓
Deobfuscation
@reverseengine
ReverseEngineering
Instruction Substitution When we replace an instruction with several other instructions So far we have talked about different ways that a binary can be made to be more difficult to analyze Now let's move on to one of the simple but very useful techniques:…
Golden Tip
When you encounter Obfuscated code, don't look for the appearance of the instructions
Look for their effect on the Machine State
That is, check:
What happened to the Registers?
What changed in Memory?
What happened to the Flags?
Where did the Control Flow go?
If you understand these, even if the Assembly form is completely strange, you can extract the real logic of the program
A very good exercise
Look at these four pieces:
Don't look at everything from just one angle
For each, check:
What happens to EAX?
What happens to Flags?
What changes to the Stack?
What changes to Memory?
Instruction Substitution:
Implement an operation with another instruction or instructions without changing the intended semantics
The goal can be:
Change the shape of the code
Make Pattern Matching harder
Increase the variety of instructions
Obfuscation
Make Static Analysis harder
But it has a very important lesson for us:
We should not analyze assembly based only on the name of the instructions, we should understand their semantics and their real effect on the CPU
And this is exactly our structure:
@reverseengine
When you encounter Obfuscated code, don't look for the appearance of the instructions
Look for their effect on the Machine State
That is, check:
What happened to the Registers?
What changed in Memory?
What happened to the Flags?
Where did the Control Flow go?
If you understand these, even if the Assembly form is completely strange, you can extract the real logic of the program
A very good exercise
Look at these four pieces:
xor eax, eax
sub eax, eax
mov eax, 0
push 0
pop eax
Don't look at everything from just one angle
For each, check:
What happens to EAX?
What happens to Flags?
What changes to the Stack?
What changes to Memory?
Instruction Substitution:
Implement an operation with another instruction or instructions without changing the intended semantics
The goal can be:
Change the shape of the code
Make Pattern Matching harder
Increase the variety of instructions
Obfuscation
Make Static Analysis harder
But it has a very important lesson for us:
We should not analyze assembly based only on the name of the instructions, we should understand their semantics and their real effect on the CPU
And this is exactly our structure:
Assembly Basics
↓
Disassembly
↓
Instruction Semantics
↓
Obfuscation
↓
Deobfuscation
@reverseengine
Parent
چطور منتظر Child میمونه؟
تا اینجا دو تا از مهم ترین قسمت های Process API رو یاد گرفتیم:
fork() → ساخت Child
exec() → اجرای یک برنامه جدید داخل Child
حالا یک سؤال:
Parent
از کجا بفهمد Child کارش تموم شده؟
اینجاست که wait() وارد میشه
wait() چیکار میکنه؟
خیلی ساده:
wait()
باعث میشه Parent منتظر بمونه تا یکی از Child هاش تموم شه
مثلا:
├── کار خودش رو انجام میده
│
└── تموم میشه
│
▼
Parent ادامه میده
یعنی Parent نمیگه:
من دیگه کاری باهات ندارم
پس میگه:
کارت که تموم شد من ادامه میدم
یک مثال خیلی ساده:
فرض کنید یک برنامه داریم که میخاد یک Child بسازه:
اینجا:
Child
برنامه جدید رو اجرا میکنه
Parent
با wait() منتظر میمونه
وقتی Child تموم شد Parent از wait() برمیگرده و اجرای خودش رو ادامه میده
چرا اصلا باید منتظر بمونیم؟
فرض کنید Shell رو باز کردید و مینویسید:
python test.py
اگر Shell بدون هیچ هماهنگی فورا کارهای بعدی رو انجام بده ممکنه خروجی و ترتیب اجرای برنامهها چیزی نباشه که انتظار داریم
برای اجرای معمولی یک دستور Shell میتونه:
Child بسازه
2 برنامه رو داخل Child اجرا کنه
3 منتظر پایان Child بمونه
4 دوباره Prompt رو نمایش بده
تقریبا:
یک نکته مهم:
wait() Process رو متوقف نمیکنه
اینجا یک اشتباه رایج وجود داره
وقتی Parent
wait()
میکنه به این معنی نیست که کل سیستم عامل متوقف شده
فقط همون Process منتظر میمونه
CPU
میتونه در همین مدت
Process
های دیگه ای رو اجرا کنه
مثلا:
سیستم عامل همچنان به بقیه Process ها CPU میده
اگر Child زودتر تموم بشه چی؟
اگر Child قبل از اینکه Parent به wait() برسه تموم شده باشه سیستم عامل اطلاعات لازم مربوط به پایان Child رو نگه میداره تا Parent بتونه وضعیت پایان اون رو بگیره
اینجا به مفهوم مهمی به نام Zombie Process میرسیم
Zombie
یعنی Process ی که اجرای خودش رو تموم کرده اما هنوز Parent وضعیت پایان اون رو نگرفته
بعدا درباره Zombie و wait() دقیق تر صحبت میکنیم
حالا سه تایی اصلی رو کنار هم بذاریم
تا اینجا داریم:
ساختن Child:
جایگزین کردن برنامه داخل Child:
منتظر پایان Child میمونه:
پس الگوی کلاسیک میشه:
البته ترتیب دقیق wait() نسبت به اجرای Child به نحوه پیاده سازی برنامه بستگی دارده این دیاگرام فقط الگوی مفهومی رو نشون میده
چرا این قسمت مهمه؟
وقتی یک برنامه رو روی Linux تحلیل میکنید و داخل کد به:
میخورید حالا میتونید حدس بزنید چه اتفاقی در حال رخ دادنه
مثلا:
fork()
↓
یک Process جدید
↓
exec()
↓
اجرای برنامه دیگه
↓
exit()
↓
wait()
↓
Parent ادامه میده
دیگه فقط اسم چند تابع رو نمیبینید منطق پشت اونها رو هم میفهمید
تا اینجا Process API رو با سه مفهوم اصلی یاد گرفتیم:
fork() → ساخت Child
exec() →
جایگزین کردن برنامه داخل Process
wait() →
انتظار Parent برای پایان Child
@reverseengine
چطور منتظر Child میمونه؟
تا اینجا دو تا از مهم ترین قسمت های Process API رو یاد گرفتیم:
fork() → ساخت Child
exec() → اجرای یک برنامه جدید داخل Child
حالا یک سؤال:
Parent
از کجا بفهمد Child کارش تموم شده؟
اینجاست که wait() وارد میشه
wait() چیکار میکنه؟
خیلی ساده:
wait()
باعث میشه Parent منتظر بمونه تا یکی از Child هاش تموم شه
مثلا:
Parent
│
├── fork()
│
▼
Child
│
├── کار خودش رو انجام میده
│
└── تموم میشه
│
▼
Parent ادامه میده
یعنی Parent نمیگه:
من دیگه کاری باهات ندارم
پس میگه:
کارت که تموم شد من ادامه میدم
یک مثال خیلی ساده:
فرض کنید یک برنامه داریم که میخاد یک Child بسازه:
pid = fork();
if (pid == 0) {
// Child
exec(...);
}
else {
// Parent
wait(...);
}
اینجا:
Child
برنامه جدید رو اجرا میکنه
Parent
با wait() منتظر میمونه
وقتی Child تموم شد Parent از wait() برمیگرده و اجرای خودش رو ادامه میده
چرا اصلا باید منتظر بمونیم؟
فرض کنید Shell رو باز کردید و مینویسید:
python test.py
اگر Shell بدون هیچ هماهنگی فورا کارهای بعدی رو انجام بده ممکنه خروجی و ترتیب اجرای برنامهها چیزی نباشه که انتظار داریم
برای اجرای معمولی یک دستور Shell میتونه:
Child بسازه
2 برنامه رو داخل Child اجرا کنه
3 منتظر پایان Child بمونه
4 دوباره Prompt رو نمایش بده
تقریبا:
Shell
│
├── fork()
│
▼
Child
│
└── exec()
│
▼
Program
│
exit()
│
▼
Shell
│
▼
Prompt
یک نکته مهم:
wait() Process رو متوقف نمیکنه
اینجا یک اشتباه رایج وجود داره
وقتی Parent
wait()
میکنه به این معنی نیست که کل سیستم عامل متوقف شده
فقط همون Process منتظر میمونه
CPU
میتونه در همین مدت
Process
های دیگه ای رو اجرا کنه
مثلا:
Parent → Waiting
Child → Running
Chrome → Running
Browser → Running
سیستم عامل همچنان به بقیه Process ها CPU میده
اگر Child زودتر تموم بشه چی؟
اگر Child قبل از اینکه Parent به wait() برسه تموم شده باشه سیستم عامل اطلاعات لازم مربوط به پایان Child رو نگه میداره تا Parent بتونه وضعیت پایان اون رو بگیره
اینجا به مفهوم مهمی به نام Zombie Process میرسیم
Zombie
یعنی Process ی که اجرای خودش رو تموم کرده اما هنوز Parent وضعیت پایان اون رو نگرفته
بعدا درباره Zombie و wait() دقیق تر صحبت میکنیم
حالا سه تایی اصلی رو کنار هم بذاریم
تا اینجا داریم:
fork()
ساختن Child:
Parent
↓
Child
exec()
جایگزین کردن برنامه داخل Child:
Child
↓
New Program
wait()
Parent
منتظر پایان Child میمونه:
Parent
↓
wait()
↓
Child finishes
↓
Parent continues
پس الگوی کلاسیک میشه:
Parent
│
fork()
│
▼
Child
│
exec()
│
▼
New Program
│
exit()
│
▼
Parent
│
wait()
│
▼
Continue
البته ترتیب دقیق wait() نسبت به اجرای Child به نحوه پیاده سازی برنامه بستگی دارده این دیاگرام فقط الگوی مفهومی رو نشون میده
چرا این قسمت مهمه؟
وقتی یک برنامه رو روی Linux تحلیل میکنید و داخل کد به:
fork
exec
wait
exit
میخورید حالا میتونید حدس بزنید چه اتفاقی در حال رخ دادنه
مثلا:
fork()
↓
یک Process جدید
↓
exec()
↓
اجرای برنامه دیگه
↓
exit()
↓
wait()
↓
Parent ادامه میده
دیگه فقط اسم چند تابع رو نمیبینید منطق پشت اونها رو هم میفهمید
تا اینجا Process API رو با سه مفهوم اصلی یاد گرفتیم:
fork() → ساخت Child
exec() →
جایگزین کردن برنامه داخل Process
wait() →
انتظار Parent برای پایان Child
@reverseengine
ReverseEngineering
Parent چطور منتظر Child میمونه؟ تا اینجا دو تا از مهم ترین قسمت های Process API رو یاد گرفتیم: fork() → ساخت Child exec() → اجرای یک برنامه جدید داخل Child حالا یک سؤال: Parent از کجا بفهمد Child کارش تموم شده؟ اینجاست که wait() وارد میشه wait()…
How does Parent
wait for Child?
So far we have learned two of the most important parts of the Process API:
fork() → create Child
exec() → execute a new program inside Child
Now a question:
How does Parent
know when Child is finished?
This is where wait() comes in
What does wait() do?
Very simple:
wait()
makes Parent wait until one of its Children is finished
For example:
That is, Parent does not say:
I have nothing more to do with you
So it says:
When the job is done, I will continue
A very simple example:
Suppose we have a program that wants to create a Child:
Here:
Child
executes the new program
Parent
waits with wait()
When Child finishes, Parent returns from wait() and continues its execution
Why do we need to wait at all?
Suppose you open Shell and type:
python test.py
If Shell immediately executes the following tasks without any coordination, the output and execution order of the programs may not be what we expect
For a typical execution of a command Shell can:
Create a Child
2 Run the program inside the Child
3 Wait for the Child to finish
4 Display the Prompt again
Approximately:
An important point:
wait() does not stop the Process
Here is a common mistake
When Parent
wait()
it does not mean that the entire operating system is stopped
Only that Process waits
The CPU can
execute other Processes
during the same time
For example:
The operating system still gives CPU to other processes
What if the Child terminates early?
If the Child has finished before the Parent reaches wait(), the operating system keeps the necessary information about the end of the Child so that the Parent can get its end status.
Here we come to an important concept called Zombie Process.
Zombie
is a Process that has finished executing but the Parent has not yet received its end status.
We will talk about Zombie and wait() in more detail later.
Now let's put the main three together.
So far we have:
Create Child:
Replace the program inside Child:
Parent Waits for the Child to finish:
Of course, the exact order of wait() in relation to Child execution depends on how the program is implemented. This diagram only shows the conceptual pattern.
Why is this part important?
When you analyze a program on Linux and you come across:
in the code, you can now guess what is happening.
For example:
You no longer just see the names of a few functions, you also understand the logic behind them.
So far, we have learned the Process API with three main concepts:
fork() → Creating a Child
exec() → Replacing a program inside a Process
wait() → Waiting for Parent to terminate Child
@reverseengine
wait for Child?
So far we have learned two of the most important parts of the Process API:
fork() → create Child
exec() → execute a new program inside Child
Now a question:
How does Parent
know when Child is finished?
This is where wait() comes in
What does wait() do?
Very simple:
wait()
makes Parent wait until one of its Children is finished
For example:
Parent
│
├── fork()
│
▼
Child
│
├── does its job
│
└── finishes
│
▼
Parent continues
That is, Parent does not say:
I have nothing more to do with you
So it says:
When the job is done, I will continue
A very simple example:
Suppose we have a program that wants to create a Child:
pid = fork();
if (pid == 0) {
// Child
exec(...);
}
else {
// Parent
wait(...);
}
Here:
Child
executes the new program
Parent
waits with wait()
When Child finishes, Parent returns from wait() and continues its execution
Why do we need to wait at all?
Suppose you open Shell and type:
python test.py
If Shell immediately executes the following tasks without any coordination, the output and execution order of the programs may not be what we expect
For a typical execution of a command Shell can:
Create a Child
2 Run the program inside the Child
3 Wait for the Child to finish
4 Display the Prompt again
Approximately:
Shell
│
├── fork()
│
▼
Child
│
└── exec()
│
▼
Program
│
exit()
│
▼
Shell
│
▼
Prompt
An important point:
wait() does not stop the Process
Here is a common mistake
When Parent
wait()
it does not mean that the entire operating system is stopped
Only that Process waits
The CPU can
execute other Processes
during the same time
For example:
Parent → Waiting
Child → Running
Chrome → Running
Browser → Running
The operating system still gives CPU to other processes
What if the Child terminates early?
If the Child has finished before the Parent reaches wait(), the operating system keeps the necessary information about the end of the Child so that the Parent can get its end status.
Here we come to an important concept called Zombie Process.
Zombie
is a Process that has finished executing but the Parent has not yet received its end status.
We will talk about Zombie and wait() in more detail later.
Now let's put the main three together.
So far we have:
fork()
Create Child:
Parent
↓
Child
exec()
Replace the program inside Child:
Child
↓
New Program
wait()
Parent Waits for the Child to finish:
Parent
↓
wait()
↓
Child finishes
↓
Parent continues
So the classic pattern becomes:
Parent
│
fork()
│
▼
Child
│
exec()
│
▼
New Program
│
exit()
│
▼
Parent
│
wait()
│
▼
Continue
Of course, the exact order of wait() in relation to Child execution depends on how the program is implemented. This diagram only shows the conceptual pattern.
Why is this part important?
When you analyze a program on Linux and you come across:
fork
exec
wait
exit
in the code, you can now guess what is happening.
For example:
fork()
↓
A new Process
↓
exec()
↓
Executing another program
↓
exit()
↓
wait()
↓
Parent continues.
You no longer just see the names of a few functions, you also understand the logic behind them.
So far, we have learned the Process API with three main concepts:
fork() → Creating a Child
exec() → Replacing a program inside a Process
wait() → Waiting for Parent to terminate Child
@reverseengine