Forwarded from Sec Note
My New Blog Post
Evading Sysmon Dns Monitoring In 2026 | binary-win
DNSevade : https://github.com/binary-win/DNSevade.git
#sysmon #bypass
Your Notes are HERE
Evading Sysmon Dns Monitoring In 2026 | binary-win
DNSevade : https://github.com/binary-win/DNSevade.git
Sysmon gets its DNS telemetry from theMicrosoft-Windows-DNS-ClientETW provider.
Let's hide in plain sight.
#sysmon #bypass
Your Notes are HERE
❤2🔥1
_mastering-system-programming-with-c-files-processes-and-ipc.pdf
1.3 MB
Mastering System Programming with C Files, Processes, and IPC
Scheduling Metrics
از کجا بفهمیم Scheduler خوبه؟
توی قسمت قبل فهمیدیم Scheduler مسئول اینه که مشخص کنه کدوم Thread یا Process نوبتش بشه و روی CPU اجرا بشه
اما یه سؤال مهم داریم:
از کجا بفهمیم یک الگوریتم Scheduling واقعا خوبه؟
فرض کنید سه تا Process داریم:
A
B
C
Scheduler
میتونه اینها رو با ترتیبهای مختلف اجرا کنه
پس باید یه سری معیار داشته باشیم تا بتونیم عملکرد الگوریتمهای مختلف رو با هم مقایسه کنیم
اینجاست که میرسیم به Scheduling Metrics
Turnaround Time
اولین مفهوم:
Turnaround Time
به زبان ساده یعنی:
از لحظهای که یک Job وارد سیستم میشه تا لحظهای که کارش کاملا تموم میشه چقدر زمان گذشته؟
فرمولش:
Turnaround Time =
Completion Time - Arrival Time
مثلا:
Arrival Time = 0
Completion Time = 10
پس:
Turnaround Time = 10 - 0
= 10
یعنی این Process از لحظه ورود تا پایان 10 واحد زمانی داخل سیستم بوده
Waiting Time
حالا یه سؤال دیگه:
Process
چه مقدار از زمان رو فقط منتظر CPU بوده؟
اینجا میرسیم به:
Waiting Time
فرض کنید وضعیت یک Process این شکلی باشه:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
هر مدتی که Process توی Ready Queue منتظر CPU باشه، جزو Waiting Time حساب میشه
پس خیلی ساده:
Waiting Time =
مدت زمانی که Process منتظر CPU بوده
Response Time
این یکی مخصوصا برای برنامههای تعاملی خیلی مهمه
Response Time
از وقتی Job وارد سیستم میشه تا اولین باری که CPU بهش میرسه چقدر طول کشیده؟
مثلا:
Arrival
│
│ 5ms
▼
First Run
پس:
Response Time = 5ms
حالا تفاوتش با Turnaround Time رو خوب دقت کنید
Response Time
فقط تا اولین اجرا:
Arrival → First Run
Turnaround Time
تا اخر کامل کار:
Arrival → Completion
پس ممکنه یک Process خیلی سریع اولین بار اجرا بشه ولی برای تمام شدن کارش مدت زیادی زمان لازم داشته باشه
یک مثال کامل
فرض کنیم سه تا Job داریم:
Job A
Arrival = 0
Runtime = 5
Job B
Arrival = 0
Runtime = 3
Job C
Arrival = 0
Runtime = 2
فرض کنیم Scheduler تصمیم گرفته این ترتیب رو اجرا کنه:
A → B → C
پس Timeline میشه:
0 5 8 10
|--------|--------|--------|
A B C
حالا یکییکی حساب کنیم
Job A
A
از زمان:
0
شروع شده و در:
5
تموم شده
پس:
Turnaround = 5 - 0
= 5
چون A بلافاصله اجرا شده:
Response = 0
Job B
B
در زمان:
5
شروع شده و در:
8
تموم شده.
پس:
Turnaround = 8 - 0
= 8
و چون B از زمان 0 تا 5 منتظر CPU بوده:
Response = 5
Job C
C
در زمان:
8
شروع شده و در:
10
تموم شده
پس:
Turnaround = 10 - 0
= 10
و:
Response = 8
چون C تا زمان 8 منتظر مونده تا بالاخره CPU بهش برسه
میانگین هم مهمه
معمولا قرار نیست فقط یک Process رو بررسی کنیم
میخوایم ببینیم الگوریتم ما در مجموع چه عملکردی داشته
پس میانگین میگیریم
مثلا:
Turnaround:
A = 5
B = 8
C = 10
میانگین:
(5 + 8 + 10) / 3
= 7.67
پس:
Average Turnaround Time = 7.67
همین کار رو میتونیم برای معیارهای دیگه مثل Response Time هم انجام بدیم
مشکل یک Scheduler ساده
حالا یه حالت جالبتر رو ببین.
فرض کنید داریم:
A = 100ms
B = 2ms
C = 2ms
و Scheduler هم بگه:
A → B → C
Timeline
میشه:
0 100 102 104
|------------------------------|------|------|
A B C
یعنی B و C باید منتظر بمونن تا A کارش تموم بشه
در نتیجه:
Response Time B = 100ms
Response Time C = 102ms
در حالی که خود B و C فقط 2ms کار دارن
یعنی یه Job سنگین میتونه باعث بشه Jobهای کوچیک مدت زیادی منتظر بمونن
اینجا Shortest Job First جالب میشه
اگر از قبل بدونیم مدت اجرای Job ها تقریبا چقدره میتونیم اول Job های کوتاهتر رو اجرا کنیم
مثلا:
A = 100ms
B = 2ms
C = 2ms
پس به جای:
A → B → C
میتونیم داشته باشیم:
B → C → A
Timeline:
0 2 4 104
|----|----|--------------------------------|
B C A
حالا B و C خیلی سریع اولین پاسخ خودشون رو میگیرن
این ایده پایه الگوریتم:
Shortest Job First (SJF)
هست
ولی اینجا یک مشکل داریم
سیستمعامل معمولا از قبل دقیقا نمیدونه:
این Process قراره چند میلیثانیه CPU مصرف کنه؟
مثلا یک برنامه ممکنه مدام بین CPU و I/O جا به جا بشه:
از کجا بفهمیم Scheduler خوبه؟
توی قسمت قبل فهمیدیم Scheduler مسئول اینه که مشخص کنه کدوم Thread یا Process نوبتش بشه و روی CPU اجرا بشه
اما یه سؤال مهم داریم:
از کجا بفهمیم یک الگوریتم Scheduling واقعا خوبه؟
فرض کنید سه تا Process داریم:
A
B
C
Scheduler
میتونه اینها رو با ترتیبهای مختلف اجرا کنه
پس باید یه سری معیار داشته باشیم تا بتونیم عملکرد الگوریتمهای مختلف رو با هم مقایسه کنیم
اینجاست که میرسیم به Scheduling Metrics
Turnaround Time
اولین مفهوم:
Turnaround Time
به زبان ساده یعنی:
از لحظهای که یک Job وارد سیستم میشه تا لحظهای که کارش کاملا تموم میشه چقدر زمان گذشته؟
فرمولش:
Turnaround Time =
Completion Time - Arrival Time
مثلا:
Arrival Time = 0
Completion Time = 10
پس:
Turnaround Time = 10 - 0
= 10
یعنی این Process از لحظه ورود تا پایان 10 واحد زمانی داخل سیستم بوده
Waiting Time
حالا یه سؤال دیگه:
Process
چه مقدار از زمان رو فقط منتظر CPU بوده؟
اینجا میرسیم به:
Waiting Time
فرض کنید وضعیت یک Process این شکلی باشه:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
هر مدتی که Process توی Ready Queue منتظر CPU باشه، جزو Waiting Time حساب میشه
پس خیلی ساده:
Waiting Time =
مدت زمانی که Process منتظر CPU بوده
Response Time
این یکی مخصوصا برای برنامههای تعاملی خیلی مهمه
Response Time
از وقتی Job وارد سیستم میشه تا اولین باری که CPU بهش میرسه چقدر طول کشیده؟
مثلا:
Arrival
│
│ 5ms
▼
First Run
پس:
Response Time = 5ms
حالا تفاوتش با Turnaround Time رو خوب دقت کنید
Response Time
فقط تا اولین اجرا:
Arrival → First Run
Turnaround Time
تا اخر کامل کار:
Arrival → Completion
پس ممکنه یک Process خیلی سریع اولین بار اجرا بشه ولی برای تمام شدن کارش مدت زیادی زمان لازم داشته باشه
یک مثال کامل
فرض کنیم سه تا Job داریم:
Job A
Arrival = 0
Runtime = 5
Job B
Arrival = 0
Runtime = 3
Job C
Arrival = 0
Runtime = 2
فرض کنیم Scheduler تصمیم گرفته این ترتیب رو اجرا کنه:
A → B → C
پس Timeline میشه:
0 5 8 10
|--------|--------|--------|
A B C
حالا یکییکی حساب کنیم
Job A
A
از زمان:
0
شروع شده و در:
5
تموم شده
پس:
Turnaround = 5 - 0
= 5
چون A بلافاصله اجرا شده:
Response = 0
Job B
B
در زمان:
5
شروع شده و در:
8
تموم شده.
پس:
Turnaround = 8 - 0
= 8
و چون B از زمان 0 تا 5 منتظر CPU بوده:
Response = 5
Job C
C
در زمان:
8
شروع شده و در:
10
تموم شده
پس:
Turnaround = 10 - 0
= 10
و:
Response = 8
چون C تا زمان 8 منتظر مونده تا بالاخره CPU بهش برسه
میانگین هم مهمه
معمولا قرار نیست فقط یک Process رو بررسی کنیم
میخوایم ببینیم الگوریتم ما در مجموع چه عملکردی داشته
پس میانگین میگیریم
مثلا:
Turnaround:
A = 5
B = 8
C = 10
میانگین:
(5 + 8 + 10) / 3
= 7.67
پس:
Average Turnaround Time = 7.67
همین کار رو میتونیم برای معیارهای دیگه مثل Response Time هم انجام بدیم
مشکل یک Scheduler ساده
حالا یه حالت جالبتر رو ببین.
فرض کنید داریم:
A = 100ms
B = 2ms
C = 2ms
و Scheduler هم بگه:
A → B → C
Timeline
میشه:
0 100 102 104
|------------------------------|------|------|
A B C
یعنی B و C باید منتظر بمونن تا A کارش تموم بشه
در نتیجه:
Response Time B = 100ms
Response Time C = 102ms
در حالی که خود B و C فقط 2ms کار دارن
یعنی یه Job سنگین میتونه باعث بشه Jobهای کوچیک مدت زیادی منتظر بمونن
اینجا Shortest Job First جالب میشه
اگر از قبل بدونیم مدت اجرای Job ها تقریبا چقدره میتونیم اول Job های کوتاهتر رو اجرا کنیم
مثلا:
A = 100ms
B = 2ms
C = 2ms
پس به جای:
A → B → C
میتونیم داشته باشیم:
B → C → A
Timeline:
0 2 4 104
|----|----|--------------------------------|
B C A
حالا B و C خیلی سریع اولین پاسخ خودشون رو میگیرن
این ایده پایه الگوریتم:
Shortest Job First (SJF)
هست
ولی اینجا یک مشکل داریم
سیستمعامل معمولا از قبل دقیقا نمیدونه:
این Process قراره چند میلیثانیه CPU مصرف کنه؟
مثلا یک برنامه ممکنه مدام بین CPU و I/O جا به جا بشه:
CPU
↓
I/O
↓
CPU
↓
I/O
↓
CPU
یا مدت اجرای برنامه به ورودی کاربر وابسته باشه
پس اینکه از قبل دقیقا بدونیم هر Job چقدر CPU لازم داره همیشه ممکن نیست
اینجاست که Scheduling از یک صف ساده خیلی پیچیده تر میشه
چرا این موضوع برای ما مهمه؟
شاید اولش فکر کنید این بحث چه ربطی به Reverse Engineering داره
ولی وقتی یک برنامه چند تا Thread داشته باشه اجرای واقعی برنامه فقط به ترتیب دستورهای داخل Binary بستگی نداره
مثلا:
Thread A
↓
Running
↓
Preempted
همزمان:
Thread B
↓
Running
↓
Blocked
بعد دوباره:
Thread A
↓
Running
یعنی اجرای برنامه میتونه بین Thread های مختلف جابهجا بشه
این موضوع وقتی مهم تر میشه که با چیزهایی مثل اینها طرف باشیم:
Multi-threading
Race Condition
Deadlock
Timing Bug
Synchronization
مثلا ممکنه یک Bug فقط زمانی اتفاق بیفته که دو Thread تقریبا همزمان به یک بخش خاص از برنامه برسن
پس فقط نباید بپرسیم:
این دستور چی کار میکنه
باید بپرسیم:
این Thread کی اجرا شده؟ قبلش چه Thread ی اجرا شده؟ چرا اجرای این Thread متوقف شده و بعد دوباره برگشته؟
این دید بعدا برای تحلیل Bug های مربوط به Concurrency خیلی مهم میشه
فعلاً این سه معیار رو خوب یاد بگیرید:
Turnaround Time
Arrival → Completion
یعنی کل زمانی که Job از ورود تا پایان داخل سیستم بوده
Waiting Time
مدت انتظار برای CPU
یعنی چه مقدار از زمان Process در Ready Queue منتظر CPU بوده
Response Time
Arrival → First Run
یعنی چقدر طول کشیده تا Process برای اولین بار CPU بگیره.
به شکل خلاصه:
Arrival
│
├──── Waiting ────┐
│ │
▼ ▼
First Run Running
│ │
│ │
└── Response │
│
▼
Completion
Turnaround = Arrival → Completion
پس وقتی میخوایم دو الگوریتم Scheduling رو با هم مقایسه کنیم فقط نمیگیم:
کدوم سریعتره؟
باید ببینیم:
چقدر منتظر موندیم؟
چقدر سریع اولین جواب رو گرفتیم؟
چقدر طول کشید کار کامل بشه؟
اینها همون معیار هایی هستن که کمک میکنن بفهمیم یک Scheduler واقعا چقدر خوب عمل میکنه
@reverseengine
↓
I/O
↓
CPU
↓
I/O
↓
CPU
یا مدت اجرای برنامه به ورودی کاربر وابسته باشه
پس اینکه از قبل دقیقا بدونیم هر Job چقدر CPU لازم داره همیشه ممکن نیست
اینجاست که Scheduling از یک صف ساده خیلی پیچیده تر میشه
چرا این موضوع برای ما مهمه؟
شاید اولش فکر کنید این بحث چه ربطی به Reverse Engineering داره
ولی وقتی یک برنامه چند تا Thread داشته باشه اجرای واقعی برنامه فقط به ترتیب دستورهای داخل Binary بستگی نداره
مثلا:
Thread A
↓
Running
↓
Preempted
همزمان:
Thread B
↓
Running
↓
Blocked
بعد دوباره:
Thread A
↓
Running
یعنی اجرای برنامه میتونه بین Thread های مختلف جابهجا بشه
این موضوع وقتی مهم تر میشه که با چیزهایی مثل اینها طرف باشیم:
Multi-threading
Race Condition
Deadlock
Timing Bug
Synchronization
مثلا ممکنه یک Bug فقط زمانی اتفاق بیفته که دو Thread تقریبا همزمان به یک بخش خاص از برنامه برسن
پس فقط نباید بپرسیم:
این دستور چی کار میکنه
باید بپرسیم:
این Thread کی اجرا شده؟ قبلش چه Thread ی اجرا شده؟ چرا اجرای این Thread متوقف شده و بعد دوباره برگشته؟
این دید بعدا برای تحلیل Bug های مربوط به Concurrency خیلی مهم میشه
فعلاً این سه معیار رو خوب یاد بگیرید:
Turnaround Time
Arrival → Completion
یعنی کل زمانی که Job از ورود تا پایان داخل سیستم بوده
Waiting Time
مدت انتظار برای CPU
یعنی چه مقدار از زمان Process در Ready Queue منتظر CPU بوده
Response Time
Arrival → First Run
یعنی چقدر طول کشیده تا Process برای اولین بار CPU بگیره.
به شکل خلاصه:
Arrival
│
├──── Waiting ────┐
│ │
▼ ▼
First Run Running
│ │
│ │
└── Response │
│
▼
Completion
Turnaround = Arrival → Completion
پس وقتی میخوایم دو الگوریتم Scheduling رو با هم مقایسه کنیم فقط نمیگیم:
کدوم سریعتره؟
باید ببینیم:
چقدر منتظر موندیم؟
چقدر سریع اولین جواب رو گرفتیم؟
چقدر طول کشید کار کامل بشه؟
اینها همون معیار هایی هستن که کمک میکنن بفهمیم یک Scheduler واقعا چقدر خوب عمل میکنه
@reverseengine
ReverseEngineering
Scheduling Metrics از کجا بفهمیم Scheduler خوبه؟ توی قسمت قبل فهمیدیم Scheduler مسئول اینه که مشخص کنه کدوم Thread یا Process نوبتش بشه و روی CPU اجرا بشه اما یه سؤال مهم داریم: از کجا بفهمیم یک الگوریتم Scheduling واقعا خوبه؟ فرض کنید سه تا Process داریم:…
Scheduling Metrics
How do we know if a Scheduler is good?
In the previous section, we learned that the Scheduler is responsible for determining which Thread or Process will get its turn and run on the CPU
But we have an important question:
How do we know if a Scheduling algorithm is really good?
Suppose we have three Processes:
A
B
C
Scheduler
can run these in different orders
So we need to have a set of criteria so that we can compare the performance of different algorithms
This is where we come to Scheduling Metrics
Turnaround Time
The first concept:
Turnaround Time
In simple terms, it means:
How much time has passed from the moment a Job enters the system to the moment its work is completely completed?
The formula is:
Turnaround Time =
Completion Time - Arrival Time
For example:
Arrival Time = 0
Completion Time = 10
So:
Turnaround Time = 10 - 0
= 10
That means this Process has been in the system for 10 time units from the moment it entered until the end
Waiting Time
Now another question:
How much time did the Process
just wait for the CPU?
Here we come to:
Waiting Time
Assume the state of a Process is as follows:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
Any time the Process waits in the Ready Queue for the CPU is counted as Waiting Time
So very simple:
Waiting Time =
The amount of time the Process has been waiting for the CPU
Response Time
This one is especially important for interactive programs
Response Time
How long did it take from the time the Job entered the system until the first time the CPU reached it?
For example:
Arrival
│
│ 5ms
▼
First Run
So:
Response Time = 5ms
Now pay close attention to the difference with Turnaround Time
Response Time
Only until the first run:
Arrival → First Run
Turnaround Time
Until the complete end of the job:
Arrival → Completion
So a Process may run very quickly the first time, but it may take a long time to finish its work
A complete example
Let's assume we have three Jobs:
Job A
Arrival = 0
Runtime = 5
Job B
Arrival = 0
Runtime = 3
Job C
Arrival = 0
Runtime = 2
Let's assume the Scheduler decides to run in this order:
A → B → C
So the Timeline becomes:
0 5 8 10
|--------|--------|--------|
A B C
Now let's calculate one by one
Job A
A
started at:
0
and finished at:
5
So:
Turnaround = 5 - 0
= 5
Since A was executed immediately:
Response = 0
Job B
B
started at:
5
and finished at:
8
.
So:
Turnaround = 8 - 0
= 8
And since B waited for the CPU from time 0 to 5:
Response = 5
Job C
C
started at time:
8
and finished at:
10
So:
Turnaround = 10 - 0
= 10
And:
Response = 8
Because C waited until time 8 to finally get the CPU
The average is also important
Usually we are not going to examine just one Process
We want to see how our algorithm performed overall
So we take the average
For example:
Turnaround:
A = 5
B = 8
C = 10
Average:
(5 + 8 + 10) / 3
= 7.67
So:
Average Turnaround Time = 7.67
We can do the same for other metrics like Let's do Response Time too
A Simple Scheduler Problem
Now let's look at a more interesting case.
Suppose we have:
A = 100ms
B = 2ms
C = 2ms
And the Scheduler says:
A → B → C
Timeline
It becomes:
0 100 102 104
|-------------------------|------|------|
A B C
That means B and C have to wait until A finishes its work
As a result:
Response Time B = 100ms
Response Time C = 102ms
While B and C themselves only have 2ms of work
That means a heavy job can make small jobs wait for a long time
This is where Shortest Job First comes in handy
If we know in advance how long the jobs will take, we can execute the shorter jobs first
For example:
A = 100ms
B = 2ms
C = 2ms
So instead of:
A → B → C
we can have:
B → C → A
Timeline:
0 2 4 104
|----|----|--------------------------------|
B C A
Now B and C get their first response very quickly
This is the basic idea of the algorithm:
Shortest Job First (SJF)
But here we have a problem
The operating system usually does not know in advance exactly:
How do we know if a Scheduler is good?
In the previous section, we learned that the Scheduler is responsible for determining which Thread or Process will get its turn and run on the CPU
But we have an important question:
How do we know if a Scheduling algorithm is really good?
Suppose we have three Processes:
A
B
C
Scheduler
can run these in different orders
So we need to have a set of criteria so that we can compare the performance of different algorithms
This is where we come to Scheduling Metrics
Turnaround Time
The first concept:
Turnaround Time
In simple terms, it means:
How much time has passed from the moment a Job enters the system to the moment its work is completely completed?
The formula is:
Turnaround Time =
Completion Time - Arrival Time
For example:
Arrival Time = 0
Completion Time = 10
So:
Turnaround Time = 10 - 0
= 10
That means this Process has been in the system for 10 time units from the moment it entered until the end
Waiting Time
Now another question:
How much time did the Process
just wait for the CPU?
Here we come to:
Waiting Time
Assume the state of a Process is as follows:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
Any time the Process waits in the Ready Queue for the CPU is counted as Waiting Time
So very simple:
Waiting Time =
The amount of time the Process has been waiting for the CPU
Response Time
This one is especially important for interactive programs
Response Time
How long did it take from the time the Job entered the system until the first time the CPU reached it?
For example:
Arrival
│
│ 5ms
▼
First Run
So:
Response Time = 5ms
Now pay close attention to the difference with Turnaround Time
Response Time
Only until the first run:
Arrival → First Run
Turnaround Time
Until the complete end of the job:
Arrival → Completion
So a Process may run very quickly the first time, but it may take a long time to finish its work
A complete example
Let's assume we have three Jobs:
Job A
Arrival = 0
Runtime = 5
Job B
Arrival = 0
Runtime = 3
Job C
Arrival = 0
Runtime = 2
Let's assume the Scheduler decides to run in this order:
A → B → C
So the Timeline becomes:
0 5 8 10
|--------|--------|--------|
A B C
Now let's calculate one by one
Job A
A
started at:
0
and finished at:
5
So:
Turnaround = 5 - 0
= 5
Since A was executed immediately:
Response = 0
Job B
B
started at:
5
and finished at:
8
.
So:
Turnaround = 8 - 0
= 8
And since B waited for the CPU from time 0 to 5:
Response = 5
Job C
C
started at time:
8
and finished at:
10
So:
Turnaround = 10 - 0
= 10
And:
Response = 8
Because C waited until time 8 to finally get the CPU
The average is also important
Usually we are not going to examine just one Process
We want to see how our algorithm performed overall
So we take the average
For example:
Turnaround:
A = 5
B = 8
C = 10
Average:
(5 + 8 + 10) / 3
= 7.67
So:
Average Turnaround Time = 7.67
We can do the same for other metrics like Let's do Response Time too
A Simple Scheduler Problem
Now let's look at a more interesting case.
Suppose we have:
A = 100ms
B = 2ms
C = 2ms
And the Scheduler says:
A → B → C
Timeline
It becomes:
0 100 102 104
|-------------------------|------|------|
A B C
That means B and C have to wait until A finishes its work
As a result:
Response Time B = 100ms
Response Time C = 102ms
While B and C themselves only have 2ms of work
That means a heavy job can make small jobs wait for a long time
This is where Shortest Job First comes in handy
If we know in advance how long the jobs will take, we can execute the shorter jobs first
For example:
A = 100ms
B = 2ms
C = 2ms
So instead of:
A → B → C
we can have:
B → C → A
Timeline:
0 2 4 104
|----|----|--------------------------------|
B C A
Now B and C get their first response very quickly
This is the basic idea of the algorithm:
Shortest Job First (SJF)
But here we have a problem
The operating system usually does not know in advance exactly:
ReverseEngineering
Scheduling Metrics از کجا بفهمیم Scheduler خوبه؟ توی قسمت قبل فهمیدیم Scheduler مسئول اینه که مشخص کنه کدوم Thread یا Process نوبتش بشه و روی CPU اجرا بشه اما یه سؤال مهم داریم: از کجا بفهمیم یک الگوریتم Scheduling واقعا خوبه؟ فرض کنید سه تا Process داریم:…
How many milliseconds of CPU is this process going to consume?
For example, a program might be constantly switching between CPU and I/O:
For example, a program might be constantly switching between CPU and I/O:
CPU
↓
I/O
↓
CPU
↓
I/O
↓
CPU
Or the execution time of the program depends on user input
So it is not always possible to know in advance exactly how much CPU each job will need
This is where Scheduling becomes much more complex than a simple queue
9. Why is this important to us?
At first you might think what does this have to do with Reverse Engineering
But when a program has multiple threads, the actual execution of the program does not depend only on the order of the instructions in the binary
For example:
Thread A
↓
Running
↓
Preempted
Simultaneously:
Thread B
↓
Running
↓
Blocked
Then again:
Thread A
↓
Running
That is, the execution of the program can be switched between different threads
This becomes more important when we are dealing with things like:
Multi-threading
Race Condition
Deadlock
Timing Bug
Synchronization
For example, a bug may only occur when two threads reach a specific part of the program almost simultaneously
So we should not just ask:
What does this instruction do
We should ask:
When did this thread run? What thread ran before it? Why did this thread stop and then start again?
This view will be very important later for analyzing concurrency-related bugs
For now, learn these three metrics well:
Turnaround Time
Arrival → Completion
The total time the Job spent in the system from entry to completion
Waiting Time
The waiting time for CPU
How much time the Process spent in the Ready Queue waiting for the CPU
Response Time
Arrival → First Run
How long it took for the Process to get the CPU for the first time.
In summary:
Arrival
│
├─── Waiting ─────┐
│ │
▼ ▼
First Run Running
│ │
│ │
└── Response │
│
▼
Completion
Turnaround = Arrival → Completion
So when we want to compare two scheduling algorithms, we don't just say:
Which one is faster?
We need to see:
How long did we wait?
How fast did we get the first response?
How long did it take to complete the task?
These are the metrics that help us understand how well a Scheduler really performs
@reverseengine
↓
I/O
↓
CPU
↓
I/O
↓
CPU
Or the execution time of the program depends on user input
So it is not always possible to know in advance exactly how much CPU each job will need
This is where Scheduling becomes much more complex than a simple queue
9. Why is this important to us?
At first you might think what does this have to do with Reverse Engineering
But when a program has multiple threads, the actual execution of the program does not depend only on the order of the instructions in the binary
For example:
Thread A
↓
Running
↓
Preempted
Simultaneously:
Thread B
↓
Running
↓
Blocked
Then again:
Thread A
↓
Running
That is, the execution of the program can be switched between different threads
This becomes more important when we are dealing with things like:
Multi-threading
Race Condition
Deadlock
Timing Bug
Synchronization
For example, a bug may only occur when two threads reach a specific part of the program almost simultaneously
So we should not just ask:
What does this instruction do
We should ask:
When did this thread run? What thread ran before it? Why did this thread stop and then start again?
This view will be very important later for analyzing concurrency-related bugs
For now, learn these three metrics well:
Turnaround Time
Arrival → Completion
The total time the Job spent in the system from entry to completion
Waiting Time
The waiting time for CPU
How much time the Process spent in the Ready Queue waiting for the CPU
Response Time
Arrival → First Run
How long it took for the Process to get the CPU for the first time.
In summary:
Arrival
│
├─── Waiting ─────┐
│ │
▼ ▼
First Run Running
│ │
│ │
└── Response │
│
▼
Completion
Turnaround = Arrival → Completion
So when we want to compare two scheduling algorithms, we don't just say:
Which one is faster?
We need to see:
How long did we wait?
How fast did we get the first response?
How long did it take to complete the task?
These are the metrics that help us understand how well a Scheduler really performs
@reverseengine
قسمت سی و سوم بافر اورفلو
Taint Analysis on Binary
در Assembly از کجا بفهمیم داده کجا میره
میخوایم Taint Analysis رو روی باینری اجرا کنیم
اینجا دیگه خبری از اسم متغیر های مثل
input و buffer نیستباید با Registerها و ادرسهای حافظه کار کنیم
پیدا کردن Source
اول باید بفهمیم داده از کجا وارد برنامه شده مثلا داخل یک برنامه لینوکسی ممکنه به این برسیم
call read
یا
call recv
فرض کنید قبلش اینو داریم
lea rsi,[rbp-40h]
mov edx,40h
call read
اینجا میتونیم مسیر سادهای داشته باشیم
External Input
↓
read()
↓
[rbp-40h]
پس داده ورودی داخل
[rbp-40h] قرار میگیرهاین نقطه میتونه Source ما باشه
دنبال کردن داده
حالا چند دستور پایین تر میبینیم
mov rax,[rbp-40h]
mov rcx,rax
پس مقدار از حافظه اومده داخل
RAXبعد وارد
RCX شدهمسیر فعلا اینه
[rbp-40h]
↓
RAX
↓
RCX
دنبال کردن انتقال داده
فرض کنید بعدش اینو ببینیم
mov rdx,rcx
call process_data
حالا
[rbp-40h]
↓
RAX
↓
RCX
↓
RDX
↓
process_data()
یعنی دادهای که از بیرون اومده وارد
process_data شدهحالا باید داخل اون تابع ادامه مسیر رو دنبال کنیم
رسیدن به Sink
فرض کنید داخل تابع به این برسیم
mov [rdi],rax
اینجا یک Memory Write داریم
پس مسیر کلی ما میشه
Input
↓
read()
↓
Stack / Memory
↓
Register
↓
Function
↓
Memory Write
این نقطه میتونه Sink موردنظر ما باشه
البته رسیدن داده به یک Write به تنهایی به معنی وجود آسیب پذیری نیست
باید اندازه و محدوده دسترسی هم بررسی بشه
Taint
کردن Register ها
فرض کنید
mov rax,[rbp-40h]
و [rbp-40h] شامل داده خارجی باشه
پس
RAX = Tainted
بعد
mov rcx,rax
حالا
RCX = Tainted
بعد
add rcx,8
مقدار RCX تغییر کرده ولی همچنان به داده اولیه وابسته است
پس مسیر Taint قطع نشده
اما یک نکته مهم
همه دستورها باعث انتقال ساده Taint نمیشن
مثلا:
cmp rax,10h
اینجا داده مستقیما به Register دیگری منتقل نشده
ولی مقدار RAX روی تصمیم برنامه تاثیر گذاشته
مثلا:
cmp rax,10h
ja invalid
پس اینجا یک Control Dependency داریم یعنی داده خارجی روی مسیر اجرای برنامه اثر گذاشته
یک مثال کامل تر:
lea rsi,[rbp-40h]
mov edx,40h
call read
movzx eax,byte ptr [rbp-40h]
cmp eax,20h
ja invalid
mov edi,eax
call process_data
مسیر رو مرحله به مرحله ببینید
read()
↓
[rbp-40h]
↓
EAX
↓
cmp
↓
Control Flow
↓
process_data()
اینجا مقدار ورودی هم روی داده تاثیر گذاشته
هم روی مسیر اجرای برنامه
در Ghidra چه کار میکنیم
وقتی به یک مقدار مشکوک رسیدیم
اول References اون مقدار رو بررسی میکنیم بعد استفادههای بعدی رو پیدا میکنیم
مثلا:
Source
↓
Reference
↓
Register
↓
Memory Access
↓
Function Call
بعد همین کار رو برای تابع بعدی ادامه میدیم تا مسیر کامل مشخص بشه یک نکته مهم لازم نیست همیشه کل Taint Analysis رو دستی انجام بدید
در برنامه های بزرگ این کار خیلی سخت میشه برای همین ابزارهای مختلفی وجود دارن که میتونن این کار رو تا حدی خودکار کنن
مثلا:
angr
Triton
DynamoRIO
Intel Pin
هرکدوم روش و کاربرد خودشون رو دارن
@reverseengine
ReverseEngineering
قسمت سی و سوم بافر اورفلو Taint Analysis on Binary در Assembly از کجا بفهمیم داده کجا میره میخوایم Taint Analysis رو روی باینری اجرا کنیم اینجا دیگه خبری از اسم متغیر های مثل input و buffer نیست باید با Registerها و ادرسهای حافظه کار کنیم پیدا کردن…
Part 33 Buffer Overflow
Taint Analysis on Binary
In Assembly, how do we know where the data goes
We want to run Taint Analysis on binary
Here, there is no more mention of variable names like input and buffer
We have to work with registers and memory addresses
Finding the Source
First, we need to find out where the data entered the program from, for example, in a Linux program, we might get this
call read
or
call recv
Suppose we have this before
lea rsi,[rbp-40h]
mov edx,40h
call read
Here we can have a simple path
External Input
↓
read()
↓
[rbp-40h]
So the input data is placed in [rbp-40h]
This point can be our Source
Following the data
Now we see a few commands below
mov rax,[rbp-40h]
mov rcx,rax
So the value came from memory Inside RAX
Then entered RCX
The current path is
[rbp-40h]
↓
RAX
↓
RCX
Following data transfer
Suppose we see this later
mov rdx,rcx
call process_data
Now
[rbp-40h]
↓
RAX
↓
RCX
↓
RDX
↓
process_data()
That is, the data that came from outside entered process_data
Now we have to continue the path inside that function
Reaching Sink
Suppose we reach this inside the function
mov [rdi],rax
Here we have a Memory Write
So our general path becomes
Input
↓
read()
↓
Stack / Memory
↓
Register
↓
Function
↓
Memory Write
This point can be our desired Sink
Of course, reaching a Write alone does not mean there is a vulnerability
The size and access range should also be checked
Taint
Registers
Assuming
mov rax,[rbp-40h]
and [rbp-40h] contains external data
so
RAX = Tainted
then
mov rcx,rax
now
RCX = Tainted
then
add rcx,8
The RCX value has changed but is still dependent on the original data
so the Taint path is not broken
but one important point
not all instructions cause a simple Taint transfer
for example:
cmp rax,10h
here the data is not directly transferred to another register
but the RAX value affects the program's decision
for example:
cmp rax,10h
ja invalid
so here we have a Control Dependency, that is, the external data affects the program's execution path
A more complete example:
lea rsi,[rbp-40h]
mov edx,40h
call read
movzx eax,byte ptr [rbp-40h]
cmp eax,20h
ja invalid
mov edi,eax
call process_data
See the path step by step
read()
↓
[rbp-40h]
↓
EAX
↓
cmp
↓
Control Flow
↓
process_data()
Here the input value affects both the data and the execution path
What do we do in Ghidra
When we reach a suspicious value
First we check the References of that value, then we find the next uses
For example:
Source
↓
Reference
↓
Register
↓
Memory Access
↓
Function Call
Then we continue the same for the next function to determine the full path. One important point is that you don't always have to do the entire Taint Analysis manually
In large programs, this can be very difficult, so there are various tools that can automate this task to some extent
For example:
angr
Triton
DynamoRIO
Intel Pin
Each has its own method and application
@reverseengine
Memory Allocation
و تغییر RW → RX
تا اینجا دربارهی Call Stack گفتیم حالا یک لایه پایین تر میریم سراغ خود حافظه
یکی از چیزهایی که EDR ها در تحلیل رفتار Processها زیر نظر میگیرن
Permission
حافظه و تغییراتشه
اصلا RW و RX یعنی چی؟
هر بخش از حافظه میتونه Permission های مختلفی داشته باشه:
R = Read
W = Write
X = Execute
مثلا:
RW
یعنی:
Read + Write
برنامه میتونه اطلاعات داخل این قسمت رو بخونه و تغییر بده ولی قرار نیست از همون قسمت کد اجرا کنه
یا:
RX
یعنی:
Read + Execute
میتونه اطلاعات رو بخونه و از اون قسمت دستورهای CPU رو اجرا کنه ولی نباید بتونه محتوای اون صفحه رو تغییر بده
بهصورت مفهومی ممکنه چنین زنجیرهای ببینیم:
Memory Allocation
↓
RW
↓
Write Data
↓
Permission Change
↓
RX
↓
Execution
یعنی یک بخش از حافظه اول قابل نوشتن بوده و بعد Permission اون به حالتی تغییر کرده که قابلیت Execute داشته باشه
همین تغییر بهتنهایی به معنی Malware بودن نیست مثلا بعضی نرمافزارهای کاملا legitimate هم به دلایل مختلف کد تولید یا کامپایل میکنن
JIT
یکی از مثالهای مهمه
بعضی Runtime ها از تکنولوژی به اسم:
JIT = Just-In-Time Compilation
استفاده میکنن اینجا برنامه ممکنه در زمان اجرا بخشی از کد رو تولید کنه و برای اجرای اون نیاز به حافظهی مناسب داشته باشه
پس:
RW → RX
میتونه در یک نرم افزار سالم هم اتفاق بیفته
بنابراین یک EDR خیلی خوب نباید بگه:
RW → RX = Malware
چون این مدل منطق تقریبا همونقدر درسته که بگیم هر کسی هودی پوشید هکره 😂
چیزی که مهمه Context هست
EDR
معمولا قط یک Event رو نگاه نمیکنه
مثلا فرض کنید چنین زنجیرهای وجود داشته باشه:
Process
↓
Memory Allocation
↓
Write
↓
Protection Change
↓
RX Memory
↓
Thread Activity
↓
Call Stack
↓
Network Activity
هرکدوم به تنهایی ممکنه کاملا عادی باشه
ولی وقتی چند رفتار غیرمعمول کنار هم قرار میگیرن ارزش تحلیلی بیشتری پیدا میکنن
به همین ددلیل Detection مدرن بیشتر شبیه اینه:
Event
+
Context
+
Process History
+
Memory Information
+
Thread Information
+
Call Stack
RWX
بعضی وقتا ممکنه Permission یک بخش به شکل زیر باشه:
RWX
یعنی:
Read
Write
Execute
در این حالت همون Memory Region هم قابل تغییر و هم قابل اجراست
از دید امنیتی این وضعیت معمولا حساس تره چون اصل مهمی به نام:
W^X
وجود داره
یعنی:
Writable XOR Executable
به زبان ساده:
البته باز هم:
RWX ≠ Malware
و:
RW → RX ≠ Malware
اینها فقط Signal هستن نه کار پایانی
DEP
یکی از مکانیزم های مهم Windows:
Data Execution Prevention
هدفش اینه که بخشهایی از Memory که نباید کد اجرا کنن قابل Execute نباشن
مثلا اگر یک بخش فقط:
RW
باشه انتظار نداریم CPU از اونجا مثل یک Code Region معمولی دستور اجرا کنه
پس Permission های Memory بخشی از مدل امنیتی سیستم عامل هستن نه صرفا یک عدد داخل Debugger
EDR
دقیقا به چه چیزهایی توجه میکنه؟
برای تحلیل یک Memory Region ممکنه اطلاعاتی مثل اینها اهمیت داشته باشه:
Process
├── Memory Type
├── Protection
├── Allocation History
├── Thread
├── Thread Start Address
├── Call Stack
├── Loaded Modules
├── Parent Process
└── Process Behavior
مثلا این دو وضعیت از نظر تحلیلی یکی نیستن:
Application
↓
JIT Activity
↓
RW → RX
در مقایسه با:
Unknown Process
↓
Private Memory
↓
Write
↓
RW → RX
↓
Suspicious Thread Activity
EDR
دقیقا به همین تفاوت Context اهمیت میده
نکته مهم
وقتی داخل Debugger یا Memory Viewer یک Region میبینید فقط به Address نگاه نکنید
این موارد رو هم بررسی کنید:
Address
Size
Protection
Type
State
Backing
Owner Process
Thread Relation
مثلا:
0x000001A...
Size: 0x5000
Protection: RX
Type: ...
State: ...
این اطلاعات کمک میکنن بفهمید با چه نوع Memory Region طرف هستید
پس فعلا این مدل ذهنی رو داشته باشید:
Memory
│
├── R
├── W
└── X
و یک تغییر Permission به تنهایی چیزی رو ثابت نمیکنه
و تغییر RW → RX
تا اینجا دربارهی Call Stack گفتیم حالا یک لایه پایین تر میریم سراغ خود حافظه
یکی از چیزهایی که EDR ها در تحلیل رفتار Processها زیر نظر میگیرن
Permission
حافظه و تغییراتشه
اصلا RW و RX یعنی چی؟
هر بخش از حافظه میتونه Permission های مختلفی داشته باشه:
R = Read
W = Write
X = Execute
مثلا:
RW
یعنی:
Read + Write
برنامه میتونه اطلاعات داخل این قسمت رو بخونه و تغییر بده ولی قرار نیست از همون قسمت کد اجرا کنه
یا:
RX
یعنی:
Read + Execute
میتونه اطلاعات رو بخونه و از اون قسمت دستورهای CPU رو اجرا کنه ولی نباید بتونه محتوای اون صفحه رو تغییر بده
بهصورت مفهومی ممکنه چنین زنجیرهای ببینیم:
Memory Allocation
↓
RW
↓
Write Data
↓
Permission Change
↓
RX
↓
Execution
یعنی یک بخش از حافظه اول قابل نوشتن بوده و بعد Permission اون به حالتی تغییر کرده که قابلیت Execute داشته باشه
همین تغییر بهتنهایی به معنی Malware بودن نیست مثلا بعضی نرمافزارهای کاملا legitimate هم به دلایل مختلف کد تولید یا کامپایل میکنن
JIT
یکی از مثالهای مهمه
بعضی Runtime ها از تکنولوژی به اسم:
JIT = Just-In-Time Compilation
استفاده میکنن اینجا برنامه ممکنه در زمان اجرا بخشی از کد رو تولید کنه و برای اجرای اون نیاز به حافظهی مناسب داشته باشه
پس:
RW → RX
میتونه در یک نرم افزار سالم هم اتفاق بیفته
بنابراین یک EDR خیلی خوب نباید بگه:
RW → RX = Malware
چون این مدل منطق تقریبا همونقدر درسته که بگیم هر کسی هودی پوشید هکره 😂
چیزی که مهمه Context هست
EDR
معمولا قط یک Event رو نگاه نمیکنه
مثلا فرض کنید چنین زنجیرهای وجود داشته باشه:
Process
↓
Memory Allocation
↓
Write
↓
Protection Change
↓
RX Memory
↓
Thread Activity
↓
Call Stack
↓
Network Activity
هرکدوم به تنهایی ممکنه کاملا عادی باشه
ولی وقتی چند رفتار غیرمعمول کنار هم قرار میگیرن ارزش تحلیلی بیشتری پیدا میکنن
به همین ددلیل Detection مدرن بیشتر شبیه اینه:
Event
+
Context
+
Process History
+
Memory Information
+
Thread Information
+
Call Stack
RWX
بعضی وقتا ممکنه Permission یک بخش به شکل زیر باشه:
RWX
یعنی:
Read
Write
Execute
در این حالت همون Memory Region هم قابل تغییر و هم قابل اجراست
از دید امنیتی این وضعیت معمولا حساس تره چون اصل مهمی به نام:
W^X
وجود داره
یعنی:
Writable XOR Executable
به زبان ساده:
یک Memory Region بهتره همزمان هم Writable و هم Executable نباشه
البته باز هم:
RWX ≠ Malware
و:
RW → RX ≠ Malware
اینها فقط Signal هستن نه کار پایانی
DEP
یکی از مکانیزم های مهم Windows:
Data Execution Prevention
هدفش اینه که بخشهایی از Memory که نباید کد اجرا کنن قابل Execute نباشن
مثلا اگر یک بخش فقط:
RW
باشه انتظار نداریم CPU از اونجا مثل یک Code Region معمولی دستور اجرا کنه
پس Permission های Memory بخشی از مدل امنیتی سیستم عامل هستن نه صرفا یک عدد داخل Debugger
EDR
دقیقا به چه چیزهایی توجه میکنه؟
برای تحلیل یک Memory Region ممکنه اطلاعاتی مثل اینها اهمیت داشته باشه:
Process
├── Memory Type
├── Protection
├── Allocation History
├── Thread
├── Thread Start Address
├── Call Stack
├── Loaded Modules
├── Parent Process
└── Process Behavior
مثلا این دو وضعیت از نظر تحلیلی یکی نیستن:
Application
↓
JIT Activity
↓
RW → RX
در مقایسه با:
Unknown Process
↓
Private Memory
↓
Write
↓
RW → RX
↓
Suspicious Thread Activity
EDR
دقیقا به همین تفاوت Context اهمیت میده
نکته مهم
وقتی داخل Debugger یا Memory Viewer یک Region میبینید فقط به Address نگاه نکنید
این موارد رو هم بررسی کنید:
Address
Size
Protection
Type
State
Backing
Owner Process
Thread Relation
مثلا:
0x000001A...
Size: 0x5000
Protection: RX
Type: ...
State: ...
این اطلاعات کمک میکنن بفهمید با چه نوع Memory Region طرف هستید
پس فعلا این مدل ذهنی رو داشته باشید:
Memory
│
├── R
├── W
└── X
و یک تغییر Permission به تنهایی چیزی رو ثابت نمیکنه
چیزی که برای تحلیل مهم تره اینه:
Memory
+
Permission Change
+
Thread
+
Call Stack
+
Process Context
+
Behavior
@reverseengine
Memory
+
Permission Change
+
Thread
+
Call Stack
+
Process Context
+
Behavior
@reverseengine
ReverseEngineering
Memory Allocation و تغییر RW → RX تا اینجا دربارهی Call Stack گفتیم حالا یک لایه پایین تر میریم سراغ خود حافظه یکی از چیزهایی که EDR ها در تحلیل رفتار Processها زیر نظر میگیرن Permission حافظه و تغییراتشه اصلا RW و RX یعنی چی؟ هر بخش از حافظه میتونه…
Memory Allocation and RW → RX Transitions
We have discussed the call stack; now, let’s go a layer deeper and look at memory itself.
One of the things EDRs monitor when analyzing process behavior is memory permissions and their changes.
What exactly do RW and RX mean? Each memory segment can have different permissions:
R = Read
W = Write
X = Execute
For example:
RW
Means:
Read + Write
The program can read and modify the data in this section but is not intended to execute code from it.
Or:
RX
Means:
Read + Execute
It can read data and execute CPU instructions from that section but should not be able to modify the page's content.
Conceptually, we might see a sequence like this:
Memory Allocation
↓
RW
↓
Write Data
↓
Permission Change
↓
RX
↓
Execution
This means a memory segment was initially writable, and then its permission was changed to allow execution.
This change alone does not imply the presence of malware; for instance, some entirely legitimate software applications generate or compile code for various reasons.
JIT
is a key example.
Some runtimes utilize a technology called:
JIT = Just-In-Time Compilation
Here, the program might generate a portion of code during runtime and require suitable memory to execute it.
So:
RW → RX
can occur even in legitimate software.
Therefore, a high-quality EDR should not simply conclude:
RW → RX = Malware
Because that logic is about as accurate as saying anyone wearing a hoodie is a hacker 😂
What matters is the context.
An EDR
usually doesn't look at just a single event.
For example, imagine a sequence like this:
Process
↓
Memory Allocation
↓
Write
↓
Protection Change
↓
RX Memory
↓
Thread Activity
↓
Call Stack
↓
Network Activity
Each step might be perfectly normal on its own,
but when several unusual behaviors... ...they acquire greater analytical value.
For this reason, modern detection looks more like this:
Event
+
Context
+
Process History
+
Memory Information
+
Thread Information
+
Call Stack
RWX
Sometimes, a memory segment's permissions might look like this:
RWX
Meaning:
Read
Write
Execute
In this case, the memory region is both modifiable and executable.
From a security perspective, this situation is usually more critical because of an important principle known as:
W^X
Meaning:
Writable XOR Executable
Simply put:
It is best for a memory region not to be both Writable and Executable at the same time.
Of course:
RWX ≠ Malware
And:
RW → RX ≠ Malware
These are merely signals, not a definitive conclusion.
DEP
One of Windows' key mechanisms:
Data Execution Prevention
Its goal is to ensure that memory segments not intended to run code cannot be executed.
For instance, if a segment is only:
RW
...we do not expect the CPU to execute instructions from it as it would a standard code region.
Thus, memory permissions are part of the operating system's security model, not just a value within a debugger.
EDR
What exactly does it focus on? When analyzing a memory region, information such as the following can be significant:
Process
├── Memory Type
├── Protection
├── Allocation History
├── Thread
├── Thread Start Address
├── Call Stack
├── Loaded Modules
├── Parent Process
└── Process Behavior
For instance, from an analytical standpoint, these two scenarios are not the same:
Application
↓
JIT Activity
↓
RW → RX
Compared to:
Unknown Process
↓
Private Memory
↓
Write
↓
RW → RX
↓
Suspicious Thread Activity
EDR systems focus precisely on this contextual difference.
Important Note:
When viewing a memory region in a debugger or memory viewer, do not look solely at the address.
Examine these attributes as well:
Address
Size
Protection
Type
State
Backing
Owner Process
Thread Relation
For example:
0x000001A...
Size: 0x5000
Protection: RX
Type: ...
State: ...
This information helps you understand the nature of the memory region you are dealing with.
So, for now, keep this mental model in mind:
We have discussed the call stack; now, let’s go a layer deeper and look at memory itself.
One of the things EDRs monitor when analyzing process behavior is memory permissions and their changes.
What exactly do RW and RX mean? Each memory segment can have different permissions:
R = Read
W = Write
X = Execute
For example:
RW
Means:
Read + Write
The program can read and modify the data in this section but is not intended to execute code from it.
Or:
RX
Means:
Read + Execute
It can read data and execute CPU instructions from that section but should not be able to modify the page's content.
Conceptually, we might see a sequence like this:
Memory Allocation
↓
RW
↓
Write Data
↓
Permission Change
↓
RX
↓
Execution
This means a memory segment was initially writable, and then its permission was changed to allow execution.
This change alone does not imply the presence of malware; for instance, some entirely legitimate software applications generate or compile code for various reasons.
JIT
is a key example.
Some runtimes utilize a technology called:
JIT = Just-In-Time Compilation
Here, the program might generate a portion of code during runtime and require suitable memory to execute it.
So:
RW → RX
can occur even in legitimate software.
Therefore, a high-quality EDR should not simply conclude:
RW → RX = Malware
Because that logic is about as accurate as saying anyone wearing a hoodie is a hacker 😂
What matters is the context.
An EDR
usually doesn't look at just a single event.
For example, imagine a sequence like this:
Process
↓
Memory Allocation
↓
Write
↓
Protection Change
↓
RX Memory
↓
Thread Activity
↓
Call Stack
↓
Network Activity
Each step might be perfectly normal on its own,
but when several unusual behaviors... ...they acquire greater analytical value.
For this reason, modern detection looks more like this:
Event
+
Context
+
Process History
+
Memory Information
+
Thread Information
+
Call Stack
RWX
Sometimes, a memory segment's permissions might look like this:
RWX
Meaning:
Read
Write
Execute
In this case, the memory region is both modifiable and executable.
From a security perspective, this situation is usually more critical because of an important principle known as:
W^X
Meaning:
Writable XOR Executable
Simply put:
It is best for a memory region not to be both Writable and Executable at the same time.
Of course:
RWX ≠ Malware
And:
RW → RX ≠ Malware
These are merely signals, not a definitive conclusion.
DEP
One of Windows' key mechanisms:
Data Execution Prevention
Its goal is to ensure that memory segments not intended to run code cannot be executed.
For instance, if a segment is only:
RW
...we do not expect the CPU to execute instructions from it as it would a standard code region.
Thus, memory permissions are part of the operating system's security model, not just a value within a debugger.
EDR
What exactly does it focus on? When analyzing a memory region, information such as the following can be significant:
Process
├── Memory Type
├── Protection
├── Allocation History
├── Thread
├── Thread Start Address
├── Call Stack
├── Loaded Modules
├── Parent Process
└── Process Behavior
For instance, from an analytical standpoint, these two scenarios are not the same:
Application
↓
JIT Activity
↓
RW → RX
Compared to:
Unknown Process
↓
Private Memory
↓
Write
↓
RW → RX
↓
Suspicious Thread Activity
EDR systems focus precisely on this contextual difference.
Important Note:
When viewing a memory region in a debugger or memory viewer, do not look solely at the address.
Examine these attributes as well:
Address
Size
Protection
Type
State
Backing
Owner Process
Thread Relation
For example:
0x000001A...
Size: 0x5000
Protection: RX
Type: ...
State: ...
This information helps you understand the nature of the memory region you are dealing with.
So, for now, keep this mental model in mind:
ReverseEngineering
Memory Allocation و تغییر RW → RX تا اینجا دربارهی Call Stack گفتیم حالا یک لایه پایین تر میریم سراغ خود حافظه یکی از چیزهایی که EDR ها در تحلیل رفتار Processها زیر نظر میگیرن Permission حافظه و تغییراتشه اصلا RW و RX یعنی چی؟ هر بخش از حافظه میتونه…
Memory
│
├── R
├── W
└── X
And remember: a permission change in isolation does not prove anything on its own
│
├── R
├── W
└── X
And remember: a permission change in isolation does not prove anything on its own
What is more important for analysis is:
Memory
+
Permission Change
+
Thread
+
Call Stack
+
Process Context
+
Behavior
@reverseengine
Memory
+
Permission Change
+
Thread
+
Call Stack
+
Process Context
+
Behavior
@reverseengine
Struct Layout
و Offset Analysis در Reverse Engineering
وقتی Source Code رو داشته باشیم فهمیدن Struct خیلی راحت تره
مثلا:
struct User {
int id;
int age;
char *name;
};
اینجا دقیقا میدونیم هر Field چیه و کجا قرار گرفته.
ولی توی Reverse Engineering معمولا خبری از این کد نیست
کامپایلر Struct رو تبدیل کرده به دستورهای Machine Code و Assembly و چیزی که ما میبینیم ممکنه فقط این باشه:
mov eax, [rdi+0x4]
mov rax, [rdi+0x8]
اینجا دیگه کامپایلر نمیاد بهمون بگه:
+0x04 = age
+0x08 = name
پس باید خودمون از روی نحوه استفاده از Memory بفهمیم هر Offset احتمالا مربوط به چه Field ی بوده
این کار یکی از بخشهای مهم برای بازسازی ساختارهای ناشناخته است
Offset
دقیقا چیه؟
فرض کنید آدرس شروع یک Object این باشه:
0x1000
حالا توی Assembly داریم:
mov eax, [rdi+0x4]
اگر RDI برابر 0x1000 باشه CPU میره سراغ:
0x1000 + 0x4 = 0x1004
یعنی:
Object Base
|
v
0x1000
+0x00
+0x04 <-- accessed
+0x08
+0x10
اون +0x4 همون Offset هست
پس وقتی توی RE میبینید:
[Register + Offset]
یکی از اولین سوالها اینه:
این Register به کجا اشاره میکنه؟
این Offset چه Field ی رو نشون میده؟
در خیلی از مواقع چیزی که داریم دنبالش میگردیم همینه:
Base Address + Offset
|
v
Field
حالا یک Struct ساده رو ببینیم
مثلا:
struct User {
int id;
int age;
char *name;
};
در یک محیط 64 بیتی معمولی که int چهار بایت و Pointer هشت بایت باشه Layout مفهومی میتونه این شکلی باشه:
Offset Size Field
+0x00 4 bytes id
+0x04 4 bytes age
+0x08 8 bytes name
پس وقتی Compiler میخواد age رو بخونه:
mov eax, [rdi+0x4]
و برای خوندن name:
mov rax, [rdi+0x8]
ما از روی همین Offsetها میتونیم کم کم Layout Struct رو بازسازی کنیم
البته این Layout به ABI و Alignment هم وابسته است پس همیشه نباید یک Layout رو برای همه Binary ها قطعی فرض کنیم
Read و Write
رو از هم تشخیص بده
این دستور:
mov eax, [rdi+0x4]
یعنی مقدار موجود در Object + 0x4 خونده شده
پس:
read field at +0x4
ولی این:
mov [rdi+0x4], eax
یعنی مقدار جدیدی در Object + 0x4 نوشته شده
پس:
write field at +0x4
این تفاوت توی RE خیلی مهمه
مثلا اگر در چند جای برنامه ببینیم:
mov eax, [rdi+0x4]
و جای دیگه:
add dword ptr [rdi+0x4], 1
میتونیم حدس بزنیم که +0x4 احتمالا یک مقدار عددی یا چیزی با رفتار عددی هست
چرا؟
چون برنامه داره مستقیما روی اون Memory به شکل یک مقدار 32 بیتی عملیات انجام میده
اندازه Memory Access هم سرنخ میده
مثلا:
mov eax, [rdi+0x4]
اینجا داریم یک مقدار 4 بایتی میخونیم.
در مقابل:
mov rax, [rdi+0x8]
یک مقدار 8 بایتی خونده میشه
در یک Binary 64 بیتی این مقدار میتونه یک Pointer یا یک مقدار 64 بیتی باشه
حالا اگر بعدش ببینیم:
mov rax, [rdi+0x8]
mov rcx, [rax]
اینجا یک سرنخ مهم داریم
مقداری که از:
Object + 0x8
خوندیم بعدا به عنوان Address استفاده شده
پس احتمال Pointer بودنش بیشتر میشه:
+0x8 = pointer ?
ولی فعلا علامت سؤال رو نگه دارید 😁
در Reverse Engineering بهتره با یک Instruction سریع نتیجه قطعی نگیریم
Assembly
به تنهایی خیلی چیزها رو نمیگه باید Context رو هم ببینیم
دنبال Offsetهای تکراری بگرد
یکی از روشهای خیلی خوب اینه که یک Offset رو در چند تابع مختلف بررسی کنیم
مثلا:
Function A:
mov eax, [rdi+0x4]
و:
Function B:
cmp dword ptr [rdi+0x4], 0
و:
Function C:
add dword ptr [rdi+0x4], 1
اگر RDI در این توابع واقعا به Object مشابهی اشاره کنه احتمال زیادی وجود داره که:
+0x4
یک Field مشترک از همون Struct باشه
حالا مثلا میبینیم:
mov rax, [rdi+0x8]
و جای دیگه:
call [rdi+0x8]
اینجا باید بیشتر بررسی کنیم
ممکنه +0x8 یک Pointer باشه
ممکنه Function Pointer باشه
یا ممکنه بخشی از یک Object Layout خاص باشه
پس فقط از روی Offset اسمگذاری نکنید
رفتار اون Field رو هم بررسی کنید
Base Pointer
رو درست پیدا کنید
قبل از اینکه بریم سراغ Offset ها باید بفهمیم Register پایه واقعا به چی اشاره میکنه
مثلا:
mov eax, [rdi+0x4]
ممکنه RDI به یک Struct اشاره کنه
ولی صرفا چون اسمش RDI هست نمیتونیم چنین نتیجهای بگیریم
باید مسیر مقدار رو دنبال کنیم
مثلا:
mov rdi, rbx
call process_user
و Offset Analysis در Reverse Engineering
وقتی Source Code رو داشته باشیم فهمیدن Struct خیلی راحت تره
مثلا:
struct User {
int id;
int age;
char *name;
};
اینجا دقیقا میدونیم هر Field چیه و کجا قرار گرفته.
ولی توی Reverse Engineering معمولا خبری از این کد نیست
کامپایلر Struct رو تبدیل کرده به دستورهای Machine Code و Assembly و چیزی که ما میبینیم ممکنه فقط این باشه:
mov eax, [rdi+0x4]
mov rax, [rdi+0x8]
اینجا دیگه کامپایلر نمیاد بهمون بگه:
+0x04 = age
+0x08 = name
پس باید خودمون از روی نحوه استفاده از Memory بفهمیم هر Offset احتمالا مربوط به چه Field ی بوده
این کار یکی از بخشهای مهم برای بازسازی ساختارهای ناشناخته است
Offset
دقیقا چیه؟
فرض کنید آدرس شروع یک Object این باشه:
0x1000
حالا توی Assembly داریم:
mov eax, [rdi+0x4]
اگر RDI برابر 0x1000 باشه CPU میره سراغ:
0x1000 + 0x4 = 0x1004
یعنی:
Object Base
|
v
0x1000
+0x00
+0x04 <-- accessed
+0x08
+0x10
اون +0x4 همون Offset هست
پس وقتی توی RE میبینید:
[Register + Offset]
یکی از اولین سوالها اینه:
این Register به کجا اشاره میکنه؟
این Offset چه Field ی رو نشون میده؟
در خیلی از مواقع چیزی که داریم دنبالش میگردیم همینه:
Base Address + Offset
|
v
Field
حالا یک Struct ساده رو ببینیم
مثلا:
struct User {
int id;
int age;
char *name;
};
در یک محیط 64 بیتی معمولی که int چهار بایت و Pointer هشت بایت باشه Layout مفهومی میتونه این شکلی باشه:
Offset Size Field
+0x00 4 bytes id
+0x04 4 bytes age
+0x08 8 bytes name
پس وقتی Compiler میخواد age رو بخونه:
mov eax, [rdi+0x4]
و برای خوندن name:
mov rax, [rdi+0x8]
ما از روی همین Offsetها میتونیم کم کم Layout Struct رو بازسازی کنیم
البته این Layout به ABI و Alignment هم وابسته است پس همیشه نباید یک Layout رو برای همه Binary ها قطعی فرض کنیم
Read و Write
رو از هم تشخیص بده
این دستور:
mov eax, [rdi+0x4]
یعنی مقدار موجود در Object + 0x4 خونده شده
پس:
read field at +0x4
ولی این:
mov [rdi+0x4], eax
یعنی مقدار جدیدی در Object + 0x4 نوشته شده
پس:
write field at +0x4
این تفاوت توی RE خیلی مهمه
مثلا اگر در چند جای برنامه ببینیم:
mov eax, [rdi+0x4]
و جای دیگه:
add dword ptr [rdi+0x4], 1
میتونیم حدس بزنیم که +0x4 احتمالا یک مقدار عددی یا چیزی با رفتار عددی هست
چرا؟
چون برنامه داره مستقیما روی اون Memory به شکل یک مقدار 32 بیتی عملیات انجام میده
اندازه Memory Access هم سرنخ میده
مثلا:
mov eax, [rdi+0x4]
اینجا داریم یک مقدار 4 بایتی میخونیم.
در مقابل:
mov rax, [rdi+0x8]
یک مقدار 8 بایتی خونده میشه
در یک Binary 64 بیتی این مقدار میتونه یک Pointer یا یک مقدار 64 بیتی باشه
حالا اگر بعدش ببینیم:
mov rax, [rdi+0x8]
mov rcx, [rax]
اینجا یک سرنخ مهم داریم
مقداری که از:
Object + 0x8
خوندیم بعدا به عنوان Address استفاده شده
پس احتمال Pointer بودنش بیشتر میشه:
+0x8 = pointer ?
ولی فعلا علامت سؤال رو نگه دارید 😁
در Reverse Engineering بهتره با یک Instruction سریع نتیجه قطعی نگیریم
Assembly
به تنهایی خیلی چیزها رو نمیگه باید Context رو هم ببینیم
دنبال Offsetهای تکراری بگرد
یکی از روشهای خیلی خوب اینه که یک Offset رو در چند تابع مختلف بررسی کنیم
مثلا:
Function A:
mov eax, [rdi+0x4]
و:
Function B:
cmp dword ptr [rdi+0x4], 0
و:
Function C:
add dword ptr [rdi+0x4], 1
اگر RDI در این توابع واقعا به Object مشابهی اشاره کنه احتمال زیادی وجود داره که:
+0x4
یک Field مشترک از همون Struct باشه
حالا مثلا میبینیم:
mov rax, [rdi+0x8]
و جای دیگه:
call [rdi+0x8]
اینجا باید بیشتر بررسی کنیم
ممکنه +0x8 یک Pointer باشه
ممکنه Function Pointer باشه
یا ممکنه بخشی از یک Object Layout خاص باشه
پس فقط از روی Offset اسمگذاری نکنید
رفتار اون Field رو هم بررسی کنید
Base Pointer
رو درست پیدا کنید
قبل از اینکه بریم سراغ Offset ها باید بفهمیم Register پایه واقعا به چی اشاره میکنه
مثلا:
mov eax, [rdi+0x4]
ممکنه RDI به یک Struct اشاره کنه
ولی صرفا چون اسمش RDI هست نمیتونیم چنین نتیجهای بگیریم
باید مسیر مقدار رو دنبال کنیم
مثلا:
mov rdi, rbx
call process_user
اینجا ممکنه RBX یک Pointer به Object باشه که به عنوان Argument به process_user داده شده
Calling Convention
هم خیلی مهمه
مثلاً در System V x86-64 معمولا Argumentهای ابتدایی از این Register ها عبور میکنن:
RDI
RSI
RDX
RCX
R8
R9
ولی در Windows x64 چهار Argument اول معمولا از:
RCX
RDX
R8
R9
عبور میکنن
پس مثلا توی یک Binary ویندوزی:
mov eax, [rcx+0x8]
میتونه سرنخ مهمی باشه که RCX یک Pointer به Object یا Struct ورودی تابع باشه
البته باز هم باید Context تابع رو بررسی کنیم
از روی نحوه استفاده از Field نوعش رو حدس بزنید
فرض کنید داریم:
mov eax, [rcx+0x4]
cmp eax, 10
این میتونه این احتمال رو ایجاد کنه:
+0x4 = integer-like field
حالا:
mov rax, [rcx+0x8]
mov rdx, [rax]
اینجا احتمال Pointer بودن +0x8 بیشتره
یا:
mov rax, [rcx+0x10]
call rax
اینجا احتمال Function Pointer بودن مقدار +0x10 مطرح میشه
یا:
mov rax, [rcx+0x18]
lea rdx, [rax+...]
ممکنه +0x18 یک Pointer به Buffer یا Object دیگه باشه
ولی یک نکته مهم:
هیچکدوم از اینها به تنهایی اثبات قطعی نیستن
باید Instruction های اطراف Cross-Reference ها تابع های دیگه و مسیر Data Flow رو هم بررسی کنیم
Nested Struct ها
Struct
میتونه داخل Struct دیگه قرار بگیره
مثلا:
struct Address {
int city;
int zip;
};
struct User {
int id;
struct Address address;
};
حالا ممکنه توی Binary ببینیم:
mov eax, [rcx+0x8]
و جای دیگه:
mov eax, [rcx+0xC]
اگر Context مناسب باشه ممکنه به این نتیجه برسیم که:
User
|
+0x00 id
|
+0x08 address
|
+0x00 city
+0x04 zip
یعنی:
User + 0x08 = Address
User + 0x0C = Address.zip
در Source Code ممکنه خیلی راحت ببینیم:
user->address.zip
ولی در Binary معمولا چنین اسمی وجود نداره.
چیزی که میبینیم ممکنه فقط این باشه:
mov eax, [rcx+0xC]
پس باید از روی Offset ها و نحوه استفاده از داده ساختار منطقی برنامه رو دوباره بسازیم
Padding و Alignment
رو فراموش نکنید
یکی از اشتباهات رایج اینه که فکر کنیم Field های Struct همیشه دقیقا پشت سر هم قرار میگیرن
مثلا:
struct Example {
char flag;
void *ptr;
};
ممکنه روی یک ABI معمول 64 بیتی Layout چیزی شبیه این باشه:
+0x00 flag 1 byte
+0x01 padding
+0x02 padding
+0x03 padding
+0x04 padding
+0x05 padding
+0x06 padding
+0x07 padding
+0x08 ptr 8 bytes
چرا؟
چون Pointer معمولا نیاز به Alignment مناسب خودش داره
پس نباید فکر کنیم:
char
pointer
یعنی Pointer حتما از +0x01 شروع میشه
ممکنه Compiler بین Field ها Padding قرار بده
بنابراین وقتی Offset ها رو بازسازی میکنید همیشه این دو مورد رو هم در نظر بگیرید:
Alignment
Padding
Array
ها یک سرنخ خیلی خوب هستن
فرض کنید توی Binary اینو ببینیم:
mov eax, [rcx+rax*4]
اینجا تقریبا داریم این مدل Address Calculation رو میبینیم:
base + index * 4
این میتونه نشون بده که با Array ای از عناصر 4 بایتی طرفیم
مثلا:
Element 0 → +0x00
Element 1 → +0x04
Element 2 → +0x08
Element 3 → +0x0C
حالا اگر ببینیم:
mov rax, [rcx+rax*8]
احتمالا هر Element هشت بایت فاصله داره
اما اگر ببینیم:
mov eax, [rcx+rax*0x20]
میتونیم حدس بزنیم فاصله بین شروع Elementها حدودا:
0x20 bytes
هست.
به این فاصله میگیم:
Stride
یعنی فاصله شروع یک Element تا شروع Element بعدی
مثلا:
Element 0
|
| 0x20
v
Element 1
|
| 0x20
v
Element 2
این موضوع موقع Reverse Engineering ساختارهای ناشناخته خیلی به درد میخوره
یک مثال واقعی تر
فرض کنید این Assembly رو پیدا کردیم:
mov eax, [rcx+0x4]
test eax, eax
je short loc_140001250
mov rax, [rcx+0x8]
mov rdx, [rax]
call rdx
mov rax, [rcx+0x10]
mov eax, [rax+0x4]
حالا بدون Source Code چی میتونیم بفهمیم؟
اول:
mov eax, [rcx+0x4]
یعنی یک مقدار 32 بیتی از Offset 0x4 خونده شده
بعد:
test eax, eax
و نتیجه برای یک شرط استفاده شده
پس میتونیم حدس بزنیم:
+0x4 = integer-like / flag-like field
بعد:
mov rax, [rcx+0x8]
یک مقدار 64 بیتی از +0x8 خونده شده
بعد:
mov rdx, [rax]
یعنی مقدار خوندهشده به عنوان Address استفاده شده
و بعد:
call rdx
این Address برای یک Call استفاده شده
پس احتمال Pointer بودن Field در +0x8 بیشتر میشه و حتی ممکنه Pointer به چیزی باشه که از طریق اون یک Function Pointer یا جدول Functionها قابل دسترسیه
بعد میرسیم به:
Calling Convention
هم خیلی مهمه
مثلاً در System V x86-64 معمولا Argumentهای ابتدایی از این Register ها عبور میکنن:
RDI
RSI
RDX
RCX
R8
R9
ولی در Windows x64 چهار Argument اول معمولا از:
RCX
RDX
R8
R9
عبور میکنن
پس مثلا توی یک Binary ویندوزی:
mov eax, [rcx+0x8]
میتونه سرنخ مهمی باشه که RCX یک Pointer به Object یا Struct ورودی تابع باشه
البته باز هم باید Context تابع رو بررسی کنیم
از روی نحوه استفاده از Field نوعش رو حدس بزنید
فرض کنید داریم:
mov eax, [rcx+0x4]
cmp eax, 10
این میتونه این احتمال رو ایجاد کنه:
+0x4 = integer-like field
حالا:
mov rax, [rcx+0x8]
mov rdx, [rax]
اینجا احتمال Pointer بودن +0x8 بیشتره
یا:
mov rax, [rcx+0x10]
call rax
اینجا احتمال Function Pointer بودن مقدار +0x10 مطرح میشه
یا:
mov rax, [rcx+0x18]
lea rdx, [rax+...]
ممکنه +0x18 یک Pointer به Buffer یا Object دیگه باشه
ولی یک نکته مهم:
هیچکدوم از اینها به تنهایی اثبات قطعی نیستن
باید Instruction های اطراف Cross-Reference ها تابع های دیگه و مسیر Data Flow رو هم بررسی کنیم
Nested Struct ها
Struct
میتونه داخل Struct دیگه قرار بگیره
مثلا:
struct Address {
int city;
int zip;
};
struct User {
int id;
struct Address address;
};
حالا ممکنه توی Binary ببینیم:
mov eax, [rcx+0x8]
و جای دیگه:
mov eax, [rcx+0xC]
اگر Context مناسب باشه ممکنه به این نتیجه برسیم که:
User
|
+0x00 id
|
+0x08 address
|
+0x00 city
+0x04 zip
یعنی:
User + 0x08 = Address
User + 0x0C = Address.zip
در Source Code ممکنه خیلی راحت ببینیم:
user->address.zip
ولی در Binary معمولا چنین اسمی وجود نداره.
چیزی که میبینیم ممکنه فقط این باشه:
mov eax, [rcx+0xC]
پس باید از روی Offset ها و نحوه استفاده از داده ساختار منطقی برنامه رو دوباره بسازیم
Padding و Alignment
رو فراموش نکنید
یکی از اشتباهات رایج اینه که فکر کنیم Field های Struct همیشه دقیقا پشت سر هم قرار میگیرن
مثلا:
struct Example {
char flag;
void *ptr;
};
ممکنه روی یک ABI معمول 64 بیتی Layout چیزی شبیه این باشه:
+0x00 flag 1 byte
+0x01 padding
+0x02 padding
+0x03 padding
+0x04 padding
+0x05 padding
+0x06 padding
+0x07 padding
+0x08 ptr 8 bytes
چرا؟
چون Pointer معمولا نیاز به Alignment مناسب خودش داره
پس نباید فکر کنیم:
char
pointer
یعنی Pointer حتما از +0x01 شروع میشه
ممکنه Compiler بین Field ها Padding قرار بده
بنابراین وقتی Offset ها رو بازسازی میکنید همیشه این دو مورد رو هم در نظر بگیرید:
Alignment
Padding
Array
ها یک سرنخ خیلی خوب هستن
فرض کنید توی Binary اینو ببینیم:
mov eax, [rcx+rax*4]
اینجا تقریبا داریم این مدل Address Calculation رو میبینیم:
base + index * 4
این میتونه نشون بده که با Array ای از عناصر 4 بایتی طرفیم
مثلا:
Element 0 → +0x00
Element 1 → +0x04
Element 2 → +0x08
Element 3 → +0x0C
حالا اگر ببینیم:
mov rax, [rcx+rax*8]
احتمالا هر Element هشت بایت فاصله داره
اما اگر ببینیم:
mov eax, [rcx+rax*0x20]
میتونیم حدس بزنیم فاصله بین شروع Elementها حدودا:
0x20 bytes
هست.
به این فاصله میگیم:
Stride
یعنی فاصله شروع یک Element تا شروع Element بعدی
مثلا:
Element 0
|
| 0x20
v
Element 1
|
| 0x20
v
Element 2
این موضوع موقع Reverse Engineering ساختارهای ناشناخته خیلی به درد میخوره
یک مثال واقعی تر
فرض کنید این Assembly رو پیدا کردیم:
mov eax, [rcx+0x4]
test eax, eax
je short loc_140001250
mov rax, [rcx+0x8]
mov rdx, [rax]
call rdx
mov rax, [rcx+0x10]
mov eax, [rax+0x4]
حالا بدون Source Code چی میتونیم بفهمیم؟
اول:
mov eax, [rcx+0x4]
یعنی یک مقدار 32 بیتی از Offset 0x4 خونده شده
بعد:
test eax, eax
و نتیجه برای یک شرط استفاده شده
پس میتونیم حدس بزنیم:
+0x4 = integer-like / flag-like field
بعد:
mov rax, [rcx+0x8]
یک مقدار 64 بیتی از +0x8 خونده شده
بعد:
mov rdx, [rax]
یعنی مقدار خوندهشده به عنوان Address استفاده شده
و بعد:
call rdx
این Address برای یک Call استفاده شده
پس احتمال Pointer بودن Field در +0x8 بیشتر میشه و حتی ممکنه Pointer به چیزی باشه که از طریق اون یک Function Pointer یا جدول Functionها قابل دسترسیه
بعد میرسیم به:
mov rax, [rcx+0x10]
mov eax, [rax+0x4]
اینجا مقدار +0x10 هم به عنوان Pointer استفاده شده و بعد از Object مقصد Offset +0x4 خونده شده
پس احتمالاً +0x10 یک Pointer به یک ساختار یا Object دیگه است
در نتیجه فعلا میتونیم یک مدل اولیه داشته باشیم:
Object
|
+0x00 unknown
+0x04 integer / flag
+0x08 pointer-like
+0x10 pointer-like
ولی این هنوز Struct نهایی نیست
این فقط یک Hypothesis هست
باید با Instructionهای بیشتر Cross-Referenceها و توابع دیگه بررسیش کنیم
حالا بریم سراغ
Ghidra
وقتی Offsetهای مختلف رو پیدا کردیم میتونیم توی Ghidra یک Struct بسازیم و Field ها رو بهش اضافه کنیم
مثلا اول کار ممکنه فقط این اطلاعات رو داشته باشیم:
User
0x00 unknown
0x04 field_4
0x08 field_8
0x10 field_10
بعد از بررسی بیشتر متوجه بشیم:
User
0x00 unknown
0x04 id
0x08 name
0x10 address
یعنی چیزی که اول فقط این بود:
field_8
کم کم تبدیل میشه به:
name
و این دقیقا یکی از بخشهای جذاب Reverse Engineering هست
چون هدف فقط این نیست که بفهمیم:
mov rax, [rcx+0x8]
چه کاری انجام میده
هدف اینه که از بین همین Instruction ها دوباره یک مدل منطقی از برنامه بسازیم
یعنی از:
[rcx+0x8]
برسیم به چیزی مثل:
user->name
برای بازسازی یک Struct ناشناخته این مسیر رو توی ذهنتون داشته باشید:
Base Pointer رو پیدا کنید
Offset های دسترسی رو جمع کنید
Read و Write رو تشخیص بدید
اندازه Memory Access رو بررسی کنید
نحوه استفاده از مقدار رو بررسی کنید
Offset
های تکراری رو در توابع مختلف پیدا کنید
Pointer
ها رو از Integer ها تفکیک کنید
Nested Structure ها رو بررسی کنید
Padding و Alignment رو در نظر بگیرید
10. Arrayها و Strideها رو بررسی کن
11. یک Hypothesis بساز
12. با Cross-Reference و تحلیل بیشتر تأیید یا اصلاحش کن
در Source Code ممکنه خیلی راحت داشته باشیم:
user->name
ولی در Binary ممکنه فقط این رو ببینیم:
mov rax, [rcx+0x8]
کار Reverse Engineer اینه که از همین اطلاعات کمکم بفهمه:
RCX
↓
User Object
↓
+0x8
↓
name
یعنی از چندتا Offset و Instruction پراکنده دوباره ساختار منطقی برنامه رو سرهم کنیم
@reverseengine
mov eax, [rax+0x4]
اینجا مقدار +0x10 هم به عنوان Pointer استفاده شده و بعد از Object مقصد Offset +0x4 خونده شده
پس احتمالاً +0x10 یک Pointer به یک ساختار یا Object دیگه است
در نتیجه فعلا میتونیم یک مدل اولیه داشته باشیم:
Object
|
+0x00 unknown
+0x04 integer / flag
+0x08 pointer-like
+0x10 pointer-like
ولی این هنوز Struct نهایی نیست
این فقط یک Hypothesis هست
باید با Instructionهای بیشتر Cross-Referenceها و توابع دیگه بررسیش کنیم
حالا بریم سراغ
Ghidra
وقتی Offsetهای مختلف رو پیدا کردیم میتونیم توی Ghidra یک Struct بسازیم و Field ها رو بهش اضافه کنیم
مثلا اول کار ممکنه فقط این اطلاعات رو داشته باشیم:
User
0x00 unknown
0x04 field_4
0x08 field_8
0x10 field_10
بعد از بررسی بیشتر متوجه بشیم:
User
0x00 unknown
0x04 id
0x08 name
0x10 address
یعنی چیزی که اول فقط این بود:
field_8
کم کم تبدیل میشه به:
name
و این دقیقا یکی از بخشهای جذاب Reverse Engineering هست
چون هدف فقط این نیست که بفهمیم:
mov rax, [rcx+0x8]
چه کاری انجام میده
هدف اینه که از بین همین Instruction ها دوباره یک مدل منطقی از برنامه بسازیم
یعنی از:
[rcx+0x8]
برسیم به چیزی مثل:
user->name
برای بازسازی یک Struct ناشناخته این مسیر رو توی ذهنتون داشته باشید:
Base Pointer رو پیدا کنید
Offset های دسترسی رو جمع کنید
Read و Write رو تشخیص بدید
اندازه Memory Access رو بررسی کنید
نحوه استفاده از مقدار رو بررسی کنید
Offset
های تکراری رو در توابع مختلف پیدا کنید
Pointer
ها رو از Integer ها تفکیک کنید
Nested Structure ها رو بررسی کنید
Padding و Alignment رو در نظر بگیرید
10. Arrayها و Strideها رو بررسی کن
11. یک Hypothesis بساز
12. با Cross-Reference و تحلیل بیشتر تأیید یا اصلاحش کن
در Source Code ممکنه خیلی راحت داشته باشیم:
user->name
ولی در Binary ممکنه فقط این رو ببینیم:
mov rax, [rcx+0x8]
کار Reverse Engineer اینه که از همین اطلاعات کمکم بفهمه:
RCX
↓
User Object
↓
+0x8
↓
name
یعنی از چندتا Offset و Instruction پراکنده دوباره ساختار منطقی برنامه رو سرهم کنیم
@reverseengine
ReverseEngineering
Struct Layout و Offset Analysis در Reverse Engineering وقتی Source Code رو داشته باشیم فهمیدن Struct خیلی راحت تره مثلا: struct User { int id; int age; char *name; }; اینجا دقیقا میدونیم هر Field چیه و کجا قرار گرفته. ولی توی Reverse Engineering…
Struct Layout
and Offset Analysis in Reverse Engineering
It is much easier to understand Struct when we have the Source Code
For example:
struct User {
int id;
int age;
char *name;
};
Here we know exactly what each Field is and where it is located.
But in Reverse Engineering, this code is usually not mentioned
The compiler converts Struct into Machine Code and Assembly instructions and what we see may be just this:
mov eax, [rdi+0x4]
mov rax, [rdi+0x8]
Here, the compiler no longer tells us:
+0x04 = age
+0x08 = name
So we have to figure out what Field each Offset probably belongs to based on how Memory is used
This is one of the important parts of reconstructing unknown structures
What exactly is Offset?
Suppose the starting address of an Object is:
0x1000
Now in Assembly we have:
mov eax, [rdi+0x4]
If RDI is 0x1000, the CPU goes to:
0x1000 + 0x4 = 0x1004
That is:
Object Base
|
v
0x1000
+0x00
+0x04 <-- accessed
+0x08
+0x10
That +0x4 is the Offset
So when you see in RE:
[Register + Offset]
One of the first questions is:
Where does this Register point to?
What Field does this Offset indicate?
In many cases, what we are looking for is this:
Base Address + Offset
|
v
Field
Now let's look at a simple Struct
For example:
struct User {
int id;
int age;
char *name;
};
In a typical 64-bit environment where int is four bytes and pointer is eight bytes, the conceptual layout can be like this:
Offset Size Field
+0x00 4 bytes id
+0x04 4 bytes age
+0x08 8 bytes name
So when the Compiler wants to read age:
mov eax, [rdi+0x4]
And to read name:
mov rax, [rdi+0x8]
We can gradually rebuild the Layout Struct based on these Offsets
Of course, this Layout also depends on ABI and Alignment, so we should not always assume a definitive Layout for all Binaries
Distinguish Read and Write
This command:
mov eax, [rdi+0x4]
means the value in Object + 0x4 is read
So:
read field at +0x4
But this:
mov [rdi+0x4], eax
means a new value in Object + 0x4 is written
So:
write field at +0x4
This difference is very important in RE
For example, if we see in several places of the program:
mov eax, [rdi+0x4]
And in another place:
add dword ptr [rdi+0x4], 1
We can guess that +0x4 is probably a numeric value or something with numeric behavior
Why?
Because the program is directly operating on that Memory as a 32-bit value
The size of the Memory Access also gives a clue
For example:
mov eax, [rdi+0x4]
Here we are reading a 4-byte value.
In contrast:
mov rax, [rdi+0x8]
An 8-byte value is read
In a 64-bit binary this value can be a Pointer or a 64-bit value
Now if we see later:
mov rax, [rdi+0x8]
mov rcx, [rax]
Here we have an important clue
The value we read from:
Object + 0x8
was later used as an Address
So it is more likely to be a Pointer:
+0x8 = pointer ?
But keep the question mark for now 😁
In Reverse Engineering, it is better not to draw a definitive conclusion with a quick instruction
Assembly
alone does not tell much, we need to see the Context
Look for duplicate Offsets
One of the very good methods is to check an Offset in several different functions
For example:
Function A:
mov eax, [rdi+0x4]
And:
Function B:
cmp dword ptr [rdi+0x4], 0
And:
Function C:
add dword ptr [rdi+0x4], 1
If RDI in these functions really points to the same Object, there is a high probability that:
+0x4
is a common Field of the same Struct
Now, for example, we see:
mov rax, [rdi+0x8]
And elsewhere:
call [rdi+0x8]
Here we need to check more
Maybe +0x8 is a Pointer Okay
It could be a Function Pointer
Or it could be part of a specific Object Layout
So don't just name it by Offset
Check the behavior of that Field too
Find the Base Pointer
Before we go into Offsets, we need to understand what the Base Register actually points to
For example:
mov eax, [rdi+0x4]
RDI could be pointing to a Struct
But just because it's called RDI, we can't conclude that
We need to follow the value path
For example:
mov rdi, rbx
call process_user
and Offset Analysis in Reverse Engineering
It is much easier to understand Struct when we have the Source Code
For example:
struct User {
int id;
int age;
char *name;
};
Here we know exactly what each Field is and where it is located.
But in Reverse Engineering, this code is usually not mentioned
The compiler converts Struct into Machine Code and Assembly instructions and what we see may be just this:
mov eax, [rdi+0x4]
mov rax, [rdi+0x8]
Here, the compiler no longer tells us:
+0x04 = age
+0x08 = name
So we have to figure out what Field each Offset probably belongs to based on how Memory is used
This is one of the important parts of reconstructing unknown structures
What exactly is Offset?
Suppose the starting address of an Object is:
0x1000
Now in Assembly we have:
mov eax, [rdi+0x4]
If RDI is 0x1000, the CPU goes to:
0x1000 + 0x4 = 0x1004
That is:
Object Base
|
v
0x1000
+0x00
+0x04 <-- accessed
+0x08
+0x10
That +0x4 is the Offset
So when you see in RE:
[Register + Offset]
One of the first questions is:
Where does this Register point to?
What Field does this Offset indicate?
In many cases, what we are looking for is this:
Base Address + Offset
|
v
Field
Now let's look at a simple Struct
For example:
struct User {
int id;
int age;
char *name;
};
In a typical 64-bit environment where int is four bytes and pointer is eight bytes, the conceptual layout can be like this:
Offset Size Field
+0x00 4 bytes id
+0x04 4 bytes age
+0x08 8 bytes name
So when the Compiler wants to read age:
mov eax, [rdi+0x4]
And to read name:
mov rax, [rdi+0x8]
We can gradually rebuild the Layout Struct based on these Offsets
Of course, this Layout also depends on ABI and Alignment, so we should not always assume a definitive Layout for all Binaries
Distinguish Read and Write
This command:
mov eax, [rdi+0x4]
means the value in Object + 0x4 is read
So:
read field at +0x4
But this:
mov [rdi+0x4], eax
means a new value in Object + 0x4 is written
So:
write field at +0x4
This difference is very important in RE
For example, if we see in several places of the program:
mov eax, [rdi+0x4]
And in another place:
add dword ptr [rdi+0x4], 1
We can guess that +0x4 is probably a numeric value or something with numeric behavior
Why?
Because the program is directly operating on that Memory as a 32-bit value
The size of the Memory Access also gives a clue
For example:
mov eax, [rdi+0x4]
Here we are reading a 4-byte value.
In contrast:
mov rax, [rdi+0x8]
An 8-byte value is read
In a 64-bit binary this value can be a Pointer or a 64-bit value
Now if we see later:
mov rax, [rdi+0x8]
mov rcx, [rax]
Here we have an important clue
The value we read from:
Object + 0x8
was later used as an Address
So it is more likely to be a Pointer:
+0x8 = pointer ?
But keep the question mark for now 😁
In Reverse Engineering, it is better not to draw a definitive conclusion with a quick instruction
Assembly
alone does not tell much, we need to see the Context
Look for duplicate Offsets
One of the very good methods is to check an Offset in several different functions
For example:
Function A:
mov eax, [rdi+0x4]
And:
Function B:
cmp dword ptr [rdi+0x4], 0
And:
Function C:
add dword ptr [rdi+0x4], 1
If RDI in these functions really points to the same Object, there is a high probability that:
+0x4
is a common Field of the same Struct
Now, for example, we see:
mov rax, [rdi+0x8]
And elsewhere:
call [rdi+0x8]
Here we need to check more
Maybe +0x8 is a Pointer Okay
It could be a Function Pointer
Or it could be part of a specific Object Layout
So don't just name it by Offset
Check the behavior of that Field too
Find the Base Pointer
Before we go into Offsets, we need to understand what the Base Register actually points to
For example:
mov eax, [rdi+0x4]
RDI could be pointing to a Struct
But just because it's called RDI, we can't conclude that
We need to follow the value path
For example:
mov rdi, rbx
call process_user
❤1