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
Here RBX may be a Pointer to an Object that is given as an Argument to process_user
Calling Convention
is also very important
For example, in System V x86-64, the initial arguments usually pass through these registers:
RDI
RSI
RDX
RCX
R8
R9
But in Windows x64, the first four arguments usually pass through:
RCX
RDX
R8
R9
So, for example, in a Windows binary:
mov eax, [rcx+0x8]
It can be an important clue that RCX is a Pointer to an Object or Struct input to the function
Of course, we still need to check the Context of the function
Guess its type based on how the Field is used
Suppose we have:
mov eax, [rcx+0x4]
cmp eax, 10
This can create this possibility:
+0x4 = integer-like field
Now:
mov rax, [rcx+0x8]
mov rdx, [rax]
Here the probability of +0x8 being a Pointer is more
Or:
mov rax, [rcx+0x10]
call rax
Here the probability of +0x10 being a Function Pointer is raised
Or:
mov rax, [rcx+0x18]
lea rdx, [rax+...]
It is possible that +0x18 is a Pointer to a Buffer or another Object
But an important point:
None of these alone are definitive proof
We must also examine the instructions around the Cross-References, other functions, and the Data Flow path
Nested Structs
A Struct
can be placed inside another Struct
For example:
struct Address {
int city;
int zip;
};
struct User {
int id;
struct Address address;
};
Now we may see in Binary:
mov eax, [rcx+0x8]
And elsewhere:
mov eax, [rcx+0xC]
If the Context is appropriate, we may conclude that:
User
|
+0x00 id
|
+0x08 address
|
+0x00 city
+0x04 zip
That is:
User + 0x08 = Address
User + 0x0C = Address.zip
In Source Code we may see very easily:
user->address.zip
But in Binary such a name usually does not exist.
What we see might just be:
mov eax, [rcx+0xC]
So we have to rebuild the logical structure of the program based on the offsets and how the data is used
Don't forget about Padding and Alignment
A common mistake is to think that Struct fields are always exactly one after the other
For example:
struct Example {
char flag;
void *ptr;
};
On a typical 64-bit ABI Layout might look something like this:
+0x00 flag 1 byte
+0x01 padding
+0x02 padding
+0x03 padding
+0x04 padding
+0x05 padding
+0x06 padding
+0x07 padding
+0x08 ptr 8 bytes
Why?
Because Pointer usually needs its own alignment
So we should not think:
char
pointer
That means Pointer must start at +0x01
Compiler may put padding between Fields
So when you reconstruct Offsets always consider these two things:
Alignment
Padding
Arrays are a very good clue
Suppose we see this in Binary:
mov eax, [rcx+rax*4]
Here we are almost seeing this Address Calculation model:
base + index * 4
This can show that we are dealing with an Array of 4-byte elements
For example:
Element 0 → +0x00
Element 1 → +0x04
Element 2 → +0x08
Element 3 → +0x0C
Now if we see:
mov rax, [rcx+rax*8]
Each Element is probably eight bytes apart
But if Let's see:
mov eax, [rcx+rax*0x20]
We can guess that the distance between the start of the Elements is approximately:
0x20 bytes
.
We call this distance:
Stride
That is, the distance from the start of one Element to the start of the next Element
For example:
Element 0
|
| 0x20
v
Element 1
|
| 0x20
v
Element 2
This is very useful when reverse engineering unknown structures
A more real-world example
Suppose we find this 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]
Now what can we understand without the source code?
First:
mov eax, [rcx+0x4]
That is, a 32-bit value is read from Offset 0x4
Then:
test eax, eax
And the result is used for a condition
So we can guess:
+0x4 = integer-like / flag-like field
Next:
mov rax, [rcx+0x8]
A 64-bit value is read from +0x8
Next:
mov rdx, [rax]
That is, the value read is used as an Address
And then:
call rdx
This Address is used for a Call
Calling Convention
is also very important
For example, in System V x86-64, the initial arguments usually pass through these registers:
RDI
RSI
RDX
RCX
R8
R9
But in Windows x64, the first four arguments usually pass through:
RCX
RDX
R8
R9
So, for example, in a Windows binary:
mov eax, [rcx+0x8]
It can be an important clue that RCX is a Pointer to an Object or Struct input to the function
Of course, we still need to check the Context of the function
Guess its type based on how the Field is used
Suppose we have:
mov eax, [rcx+0x4]
cmp eax, 10
This can create this possibility:
+0x4 = integer-like field
Now:
mov rax, [rcx+0x8]
mov rdx, [rax]
Here the probability of +0x8 being a Pointer is more
Or:
mov rax, [rcx+0x10]
call rax
Here the probability of +0x10 being a Function Pointer is raised
Or:
mov rax, [rcx+0x18]
lea rdx, [rax+...]
It is possible that +0x18 is a Pointer to a Buffer or another Object
But an important point:
None of these alone are definitive proof
We must also examine the instructions around the Cross-References, other functions, and the Data Flow path
Nested Structs
A Struct
can be placed inside another Struct
For example:
struct Address {
int city;
int zip;
};
struct User {
int id;
struct Address address;
};
Now we may see in Binary:
mov eax, [rcx+0x8]
And elsewhere:
mov eax, [rcx+0xC]
If the Context is appropriate, we may conclude that:
User
|
+0x00 id
|
+0x08 address
|
+0x00 city
+0x04 zip
That is:
User + 0x08 = Address
User + 0x0C = Address.zip
In Source Code we may see very easily:
user->address.zip
But in Binary such a name usually does not exist.
What we see might just be:
mov eax, [rcx+0xC]
So we have to rebuild the logical structure of the program based on the offsets and how the data is used
Don't forget about Padding and Alignment
A common mistake is to think that Struct fields are always exactly one after the other
For example:
struct Example {
char flag;
void *ptr;
};
On a typical 64-bit ABI Layout might look something like this:
+0x00 flag 1 byte
+0x01 padding
+0x02 padding
+0x03 padding
+0x04 padding
+0x05 padding
+0x06 padding
+0x07 padding
+0x08 ptr 8 bytes
Why?
Because Pointer usually needs its own alignment
So we should not think:
char
pointer
That means Pointer must start at +0x01
Compiler may put padding between Fields
So when you reconstruct Offsets always consider these two things:
Alignment
Padding
Arrays are a very good clue
Suppose we see this in Binary:
mov eax, [rcx+rax*4]
Here we are almost seeing this Address Calculation model:
base + index * 4
This can show that we are dealing with an Array of 4-byte elements
For example:
Element 0 → +0x00
Element 1 → +0x04
Element 2 → +0x08
Element 3 → +0x0C
Now if we see:
mov rax, [rcx+rax*8]
Each Element is probably eight bytes apart
But if Let's see:
mov eax, [rcx+rax*0x20]
We can guess that the distance between the start of the Elements is approximately:
0x20 bytes
.
We call this distance:
Stride
That is, the distance from the start of one Element to the start of the next Element
For example:
Element 0
|
| 0x20
v
Element 1
|
| 0x20
v
Element 2
This is very useful when reverse engineering unknown structures
A more real-world example
Suppose we find this 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]
Now what can we understand without the source code?
First:
mov eax, [rcx+0x4]
That is, a 32-bit value is read from Offset 0x4
Then:
test eax, eax
And the result is used for a condition
So we can guess:
+0x4 = integer-like / flag-like field
Next:
mov rax, [rcx+0x8]
A 64-bit value is read from +0x8
Next:
mov rdx, [rax]
That is, the value read is used as an Address
And then:
call rdx
This Address is used for a Call
So the probability of the Field at +0x8 being a Pointer increases and it may even be a Pointer to something through which a Function Pointer or a Function table can be accessed
Then we get to:
Then we get to:
mov rax, [rcx+0x10]
mov eax, [rax+0x4]
Here the value +0x10 is also used as a Pointer and after the destination Object Offset +0x4 is read
So +0x10 is probably a Pointer to another structure or Object
So for now we can have a basic model:
Object
|
+0x00 unknown
+0x04 integer / flag
+0x08 pointer-like
+0x10 pointer-like
But this is not the final Struct yet
This is just a Hypothesis
We need to check it with more instructions, Cross-References and other functions
Now let's move on to
Ghidra
When we find the different Offsets, we can create a Struct in Ghidra and add Fields to it
For example, at first we may only have this information:
User
0x00 unknown
0x04 field_4
0x08 field_8
0x10 field_10
After further investigation, we will find out:
User
0x00 unknown
0x04 id
0x08 name
0x10 address
That is, what was initially just:
field_8
slowly becomes:
name
And this is exactly one of the interesting parts of Reverse Engineering
Because The goal is not just to understand:
mov rax, [rcx+0x8]
what it does
The goal is to rebuild a logical model of the program from these instructions
That is, from:
[rcx+0x8]
to get to something like:
user->name
To reconstruct an unknown struct, keep this path in mind:
Find the Base Pointer
Collect the access offsets
Distinguish Read and Write
Check the size of the Memory Access
Check how the value is used
Find duplicate offsets in different functions
Separate Pointers from Integers
Check Nested Structures
Consider Padding and Alignment
10. Check Arrays and Strides
11. Make a Hypothesis
12. Confirm or modify it with Cross-Reference and further analysis
In the Source Code, we may very easily have:
user->name
But In Binary we may only see this:
mov rax, [rcx+0x8]
The job of the Reverse Engineer is to understand from this information:
RCX
↓
User Object
↓
+0x8
↓
name
That is, we reassemble the logical structure of the program from a few scattered Offsets and Instructions
@reverseengine
mov eax, [rax+0x4]
Here the value +0x10 is also used as a Pointer and after the destination Object Offset +0x4 is read
So +0x10 is probably a Pointer to another structure or Object
So for now we can have a basic model:
Object
|
+0x00 unknown
+0x04 integer / flag
+0x08 pointer-like
+0x10 pointer-like
But this is not the final Struct yet
This is just a Hypothesis
We need to check it with more instructions, Cross-References and other functions
Now let's move on to
Ghidra
When we find the different Offsets, we can create a Struct in Ghidra and add Fields to it
For example, at first we may only have this information:
User
0x00 unknown
0x04 field_4
0x08 field_8
0x10 field_10
After further investigation, we will find out:
User
0x00 unknown
0x04 id
0x08 name
0x10 address
That is, what was initially just:
field_8
slowly becomes:
name
And this is exactly one of the interesting parts of Reverse Engineering
Because The goal is not just to understand:
mov rax, [rcx+0x8]
what it does
The goal is to rebuild a logical model of the program from these instructions
That is, from:
[rcx+0x8]
to get to something like:
user->name
To reconstruct an unknown struct, keep this path in mind:
Find the Base Pointer
Collect the access offsets
Distinguish Read and Write
Check the size of the Memory Access
Check how the value is used
Find duplicate offsets in different functions
Separate Pointers from Integers
Check Nested Structures
Consider Padding and Alignment
10. Check Arrays and Strides
11. Make a Hypothesis
12. Confirm or modify it with Cross-Reference and further analysis
In the Source Code, we may very easily have:
user->name
But In Binary we may only see this:
mov rax, [rcx+0x8]
The job of the Reverse Engineer is to understand from this information:
RCX
↓
User Object
↓
+0x8
↓
name
That is, we reassemble the logical structure of the program from a few scattered Offsets and Instructions
@reverseengine
Manic: Blend between Banking Malware & Spyware
is a new Android banking malware and mobile spyware:
https://www.threatfabric.com/blogs/manic-blend-between-banking-malware-and-spyware
is a new Android banking malware and mobile spyware:
https://www.threatfabric.com/blogs/manic-blend-between-banking-malware-and-spyware
ThreatFabric
Manic: Blend between Banking Malware & Spyware
Manic is a newly identified Android malware family with broad surveillance and remote-control capabilities, introducing an unusual Wi‑Fi mesh technique.
Unmasking EvilTokens: Getting to the root of device code phishing
https://www.microsoft.com/en-us/security/blog/2026/09/22/unmasking-eviltokens-getting-to-the-root-of-device-code-phishing/
https://www.microsoft.com/en-us/security/blog/2026/09/22/unmasking-eviltokens-getting-to-the-root-of-device-code-phishing/
Microsoft News
Unmasking EvilTokens: Getting to the root of device code phishing
EvilTokens has quickly become one of the top PhaaS platforms, enabling device code phishing attacks through AI-assisted lures, automated infrastructure, and token theft. In collaboration with partners, Microsoft Digital Crimes Unit (DCU) facilitated a disruption…
ClickLock Stealer: Paste Once, Lose Everything
https://www.group-ib.com/blog/clicklock-stealer-macos-malware
https://www.group-ib.com/blog/clicklock-stealer-macos-malware
Group-IB
ClickLock Stealer: Paste Once, Lose Everything
Analysis of ClickLock, a modular macOS stealer delivered via ClickFix that uses fake dialogs, kill loops, and a GSocket backdoor to steal passwords, browser data, and crypto wallets.
❤1
باز سازی Inheritance و Multiple Inheritance در Assembly
تا اینجا یاد گرفتیم چطور از روی Memory Access ها و Offset ها یک Struct ناشناخته رو بازسازی کنیم
ولی وقتی با برنامه های + +C طرف میشیم قضیه یک مقدار پیچیده تر میشه
چون ممکنه یک Object فقط چند Field ساده نداشته باشه
ممکنه:
Class
↓
Inheritance
↓
Virtual Functions
↓
vptr
↓
vtable
داشته باشیم
اینجاست که باید از روی Assembly بفهمیم:
این Object چه Class ای هست؟
از چه Class ای ارثبری کرده؟
Virtual Function
ها کجا قرار دارن؟
و Offset های مختلف مربوط به کدوم بخش Object هستن؟
Virtual Function
رو بفهمیم
فرض کنید Source Code این شکلی باشه:
چون "speak" یک Virtual Function هست Compiler معمولا برای این Object یک Pointer به جدول Virtual Function ها در نظر میگیره
به این Pointer معمولا میگیم:
vptr
و جدول:
vtable
به شکل مفهومی:
Object
│
├── vptr
│ │
│ ▼
│ vtable
│ │
│ ├── speak()
│ ├── ...
│
└── Other Fields
یعنی Object معمولا یک Pointer داره که به اطلاعات Virtual Functionهای مربوط به نوع Dynamic Object اشاره میکنه
البته جزئیات دقیق Layout به Compiler و ABI وابسته است.
داخل Assembly دنبال چه چیزی بگردیم؟
فرض کنید داخل Constructor چنین چیزی ببینیم:
mov qword ptr [rcx], offset vtable_Dog
اینجا یک سرنخ خیلی مهم داریم
یعنی مقدار ابتدای Object تنظیم شده:
Object + 0x00
↓
vptr
↓
vtable_Dog
پس میتونیم فعلا بگیم:
Dog Object
+0x00 vptr
+0x08 unknown
+0x10 unknown
هنوز نمیدونیم Field های بعدی چی هستن
ولی حداقل یک چیز مهم فهمیدیم:
+0x00
احتمالا vptr است
vtable
دقیقا چیه؟
خیلی ساده اگر بخوایم نگاهش کنیم:
vptr
│
▼
vtable
│
├── Function Pointer
├── Function Pointer
├── Function Pointer
└──
هر Entry در vtable معمولا یک Pointer به یک Function یا بخشی از مکانیزم Virtual Dispatch هست
مثلا فرض کنید Assembly این شکلی باشه:
بیایم مرحله به مرحله نگاه کنیم
اول:
]
یعنی:
RCX
↓
Object
↓
+0x00
↓
vptr
بعد:
]
یعنی از جدول مربوط به vptr یک Entry در Offset مشخص خونده شده
بعد:
call rax
یعنی اون مقدار برای Call استفاده شده
پس یک مسیر مفهومی داریم:
Object
↓
+0x00
↓
vptr
↓
vtable
↓
Function Pointer
↓
call
این یکی از الگوهای خیلی مهم در Reverse Engineering برنامههای C++ هست.
از روی این الگو چطور Virtual Function رو پیدا کنیم؟
فرض کنید این Assembly رو داریم:
میتونیم فعلا این مدل رو بسازیم:
Object
|
+0x00 → vptr
|
↓
vtable
|
+0x18 → function pointer
اما یک نکته مهم:
نباید سریع بگیم:
+0x18 = Function X
چون هنوز نمیدونیم Entry دقیقا مربوط به چه Virtual Function ای هست
باید خود vtable و Reference های مربوط به اون رو بررسی کنیم
حالا Inheritance وارد میشه
فرض کنید داریم:
از نظر مفهومی:
Animal
↑
|
Dog
Dog
بخشهایی از Animal رو به ارث میبره
ممکنه Object چیزی شبیه این داشته باشه:
Dog Object
+0x00 vptr
+0x08 Animal-related field
+0x0C Dog-related field
این Layout دقیق نیست و صرفا یک مدل مفهومیه
چون Layout واقعی به Compiler، ABI، Alignment و نوع Inheritance بستگی داره
ولی نکته مهم اینه که:
Base Class
+
Derived Class
میتونن در یک Object واحد حضور داشته باشن
از روی Constructor میتونیم رابطه رو پیدا کنیم
فرض کن یک Constructor داریم:
و بعد در Constructor کلاس دیگه ای میبینیم:
call Animal::Animal
mov qword ptr [rcx], offset vtable_Dog
اینجا یک سرنخ خیلی مهم داریم
یعنی Constructor مربوط به Dog ابتدا بخش مربوط به Base Class رو مقدار دهی میکنه و بعد وضعیت Virtual Dispatch مربوط به Derived Class رو تنظیم میکنه
به شکل مفهومی:
Dog Constructor
|
├── Animal Constructor
|
└── Dog-specific initialization
این یکی از چیزهایی هست که میتونه به ما کمک کنه رابطه بین Class ها رو پیدا کنیم
@reverseengine
تا اینجا یاد گرفتیم چطور از روی Memory Access ها و Offset ها یک Struct ناشناخته رو بازسازی کنیم
ولی وقتی با برنامه های + +C طرف میشیم قضیه یک مقدار پیچیده تر میشه
چون ممکنه یک Object فقط چند Field ساده نداشته باشه
ممکنه:
Class
↓
Inheritance
↓
Virtual Functions
↓
vptr
↓
vtable
داشته باشیم
اینجاست که باید از روی Assembly بفهمیم:
این Object چه Class ای هست؟
از چه Class ای ارثبری کرده؟
Virtual Function
ها کجا قرار دارن؟
و Offset های مختلف مربوط به کدوم بخش Object هستن؟
Virtual Function
رو بفهمیم
فرض کنید Source Code این شکلی باشه:
class Animal {
public:
virtual void speak();
};
و:
class Dog : public Animal {
public:
void speak() override;
};
چون "speak" یک Virtual Function هست Compiler معمولا برای این Object یک Pointer به جدول Virtual Function ها در نظر میگیره
به این Pointer معمولا میگیم:
vptr
و جدول:
vtable
به شکل مفهومی:
Object
│
├── vptr
│ │
│ ▼
│ vtable
│ │
│ ├── speak()
│ ├── ...
│
└── Other Fields
یعنی Object معمولا یک Pointer داره که به اطلاعات Virtual Functionهای مربوط به نوع Dynamic Object اشاره میکنه
البته جزئیات دقیق Layout به Compiler و ABI وابسته است.
داخل Assembly دنبال چه چیزی بگردیم؟
فرض کنید داخل Constructor چنین چیزی ببینیم:
mov qword ptr [rcx], offset vtable_Dog
اینجا یک سرنخ خیلی مهم داریم
یعنی مقدار ابتدای Object تنظیم شده:
Object + 0x00
↓
vptr
↓
vtable_Dog
پس میتونیم فعلا بگیم:
Dog Object
+0x00 vptr
+0x08 unknown
+0x10 unknown
هنوز نمیدونیم Field های بعدی چی هستن
ولی حداقل یک چیز مهم فهمیدیم:
+0x00
احتمالا vptr است
vtable
دقیقا چیه؟
خیلی ساده اگر بخوایم نگاهش کنیم:
vptr
│
▼
vtable
│
├── Function Pointer
├── Function Pointer
├── Function Pointer
└──
هر Entry در vtable معمولا یک Pointer به یک Function یا بخشی از مکانیزم Virtual Dispatch هست
مثلا فرض کنید Assembly این شکلی باشه:
mov rax, [rcx]
mov rax, [rax+0x10]
call rax
بیایم مرحله به مرحله نگاه کنیم
اول:
mov rax, [rcx
]
یعنی:
RCX
↓
Object
↓
+0x00
↓
vptr
بعد:
mov rax, [rax+0x10
]
یعنی از جدول مربوط به vptr یک Entry در Offset مشخص خونده شده
بعد:
call rax
یعنی اون مقدار برای Call استفاده شده
پس یک مسیر مفهومی داریم:
Object
↓
+0x00
↓
vptr
↓
vtable
↓
Function Pointer
↓
call
این یکی از الگوهای خیلی مهم در Reverse Engineering برنامههای C++ هست.
از روی این الگو چطور Virtual Function رو پیدا کنیم؟
فرض کنید این Assembly رو داریم:
mov rax, [rcx]
mov rax, [rax+0x18]
call rax
میتونیم فعلا این مدل رو بسازیم:
Object
|
+0x00 → vptr
|
↓
vtable
|
+0x18 → function pointer
اما یک نکته مهم:
نباید سریع بگیم:
+0x18 = Function X
چون هنوز نمیدونیم Entry دقیقا مربوط به چه Virtual Function ای هست
باید خود vtable و Reference های مربوط به اون رو بررسی کنیم
حالا Inheritance وارد میشه
فرض کنید داریم:
class Animal {
public:
virtual void speak();
};
class Dog : public Animal {
public:
void speak() override;
};
از نظر مفهومی:
Animal
↑
|
Dog
Dog
بخشهایی از Animal رو به ارث میبره
ممکنه Object چیزی شبیه این داشته باشه:
Dog Object
+0x00 vptr
+0x08 Animal-related field
+0x0C Dog-related field
این Layout دقیق نیست و صرفا یک مدل مفهومیه
چون Layout واقعی به Compiler، ABI، Alignment و نوع Inheritance بستگی داره
ولی نکته مهم اینه که:
Base Class
+
Derived Class
میتونن در یک Object واحد حضور داشته باشن
از روی Constructor میتونیم رابطه رو پیدا کنیم
فرض کن یک Constructor داریم:
mov qword ptr [rcx], offset vtable_Animal
و بعد در Constructor کلاس دیگه ای میبینیم:
call Animal::Animal
mov qword ptr [rcx], offset vtable_Dog
اینجا یک سرنخ خیلی مهم داریم
یعنی Constructor مربوط به Dog ابتدا بخش مربوط به Base Class رو مقدار دهی میکنه و بعد وضعیت Virtual Dispatch مربوط به Derived Class رو تنظیم میکنه
به شکل مفهومی:
Dog Constructor
|
├── Animal Constructor
|
└── Dog-specific initialization
این یکی از چیزهایی هست که میتونه به ما کمک کنه رابطه بین Class ها رو پیدا کنیم
@reverseengine
ReverseEngineering
باز سازی Inheritance و Multiple Inheritance در Assembly تا اینجا یاد گرفتیم چطور از روی Memory Access ها و Offset ها یک Struct ناشناخته رو بازسازی کنیم ولی وقتی با برنامه های + +C طرف میشیم قضیه یک مقدار پیچیده تر میشه چون ممکنه یک Object فقط چند Field…
Reconstructing Inheritance and Multiple Inheritance in Assembly
So far we have learned how to reconstruct an unknown Struct from Memory Accesses and Offsets
But when we deal with C++ programs, the matter becomes a bit more complicated
Because an Object may not have just a few simple fields
We may have:
Class
↓
Inheritance
↓
Virtual Functions
↓
vptr
↓
vtable
This is where we need to understand from the Assembly:
What Class is this Object?
What Class does it inherit from?
Where are the Virtual Functions
And which part of the Object do the different Offsets belong to?
Understand Virtual Functions
Suppose the Source Code is like this:
class Animal {
public:
virtual void speak();
};
And:
class Dog : public Animal {
public:
void speak() override;
};
Because "speak" is a Virtual Function, the Compiler usually considers a Pointer to the Virtual Function Table for this Object
We usually call this Pointer:
vptr
and the table:
vtable
Conceptually:
Object
│
├── vptr
│ │
│ ▼
│ vtable
│ │
│ ├── speak()
│ ├── ...
│
└── Other Fields
That is, the Object usually has a Pointer that points to the Virtual Function information related to the Dynamic Object type
Of course, the exact details of the Layout depend on the Compiler and ABI.
What should we look for inside the Assembly?
Suppose we see something like this inside the Constructor:
mov qword ptr [rcx], offset vtable_Dog
Here we have a very important clue
That is, the initial value of Object is set:
Object + 0x00
↓
vptr
↓
vtable_Dog
So we can say for now:
Dog Object
+0x00 vptr
+0x08 unknown
+0x10 unknown
We still don't know what the next fields are
But at least we know one important thing:
+0x00
is probably vptr
What exactly is vtable?
Very simply, if we want to look at it:
vptr
│
▼
vtable
│
├── Function Pointer
├── Function Pointer
├── Function Pointer
└──
Each entry in the vtable is usually a pointer to a function or part of the Virtual Dispatch mechanism
For example, suppose the assembly looks like this:
Let's look at it step by step
First:
mov rax, [rcx
]
That is:
RCX
↓
Object
↓
+0x00
↓
vptr
Then:
That is, an entry at a specific offset is read from the vptr table
Then:
call rax
That is, that value is used for Call
So we have a conceptual path:
Object
↓
+0x00
↓
vptr
↓
vtable
↓
Function Pointer
↓
call
This is one of the most important patterns in Reverse Engineering C++ programs.
How do we find the Virtual Function from this pattern?
Suppose we have this Assembly:
We can create this model for now:
Object
|
+0x00 → vptr
|
↓
vtable
|
+0x18 → function pointer
But one important point:
We should not say quickly:
+0x18 = Function X
Because we still do not know exactly what Virtual Function Entry is related to
We should check the vtable itself and its References
Now Inheritance comes in
Suppose we have:
class Animal {
public:
virtual void speak();
};
class Dog : public Animal {
public:
void speak() override;
};
Conceptually:
Animal
↑
|
Dog
Dog
inherits parts of Animal
The Object might look something like this:
Dog Object
+0x00 vptr
+0x08 Animal-related field
+0x0C Dog-related field
This Layout is not exact and is just a conceptual model
Because the actual Layout depends on the Compiler, ABI, Alignment and Inheritance type
But the important point is that:
Base Class
+
Derived Class
can be present in a single Object
We can find the relationship from the Constructor
Suppose we have a Constructor:
mov qword ptr [rcx], offset vtable_Animal
And then in the Constructor we see another class:
call Animal::Animal
mov qword ptr [rcx], offset vtable_Dog
Here we have a very important clue
That is, the Constructor related to Dog first initializes the part related to the Base Class and then the Virtual Dispatch state related to Sets the Derived Class
Conceptually:
Dog Constructor
|
├── Animal Constructor
|
└── Dog-specific initialization
So far we have learned how to reconstruct an unknown Struct from Memory Accesses and Offsets
But when we deal with C++ programs, the matter becomes a bit more complicated
Because an Object may not have just a few simple fields
We may have:
Class
↓
Inheritance
↓
Virtual Functions
↓
vptr
↓
vtable
This is where we need to understand from the Assembly:
What Class is this Object?
What Class does it inherit from?
Where are the Virtual Functions
And which part of the Object do the different Offsets belong to?
Understand Virtual Functions
Suppose the Source Code is like this:
class Animal {
public:
virtual void speak();
};
And:
class Dog : public Animal {
public:
void speak() override;
};
Because "speak" is a Virtual Function, the Compiler usually considers a Pointer to the Virtual Function Table for this Object
We usually call this Pointer:
vptr
and the table:
vtable
Conceptually:
Object
│
├── vptr
│ │
│ ▼
│ vtable
│ │
│ ├── speak()
│ ├── ...
│
└── Other Fields
That is, the Object usually has a Pointer that points to the Virtual Function information related to the Dynamic Object type
Of course, the exact details of the Layout depend on the Compiler and ABI.
What should we look for inside the Assembly?
Suppose we see something like this inside the Constructor:
mov qword ptr [rcx], offset vtable_Dog
Here we have a very important clue
That is, the initial value of Object is set:
Object + 0x00
↓
vptr
↓
vtable_Dog
So we can say for now:
Dog Object
+0x00 vptr
+0x08 unknown
+0x10 unknown
We still don't know what the next fields are
But at least we know one important thing:
+0x00
is probably vptr
What exactly is vtable?
Very simply, if we want to look at it:
vptr
│
▼
vtable
│
├── Function Pointer
├── Function Pointer
├── Function Pointer
└──
Each entry in the vtable is usually a pointer to a function or part of the Virtual Dispatch mechanism
For example, suppose the assembly looks like this:
mov rax, [rcx]
mov rax, [rax+0x10]
call rax
Let's look at it step by step
First:
mov rax, [rcx
]
That is:
RCX
↓
Object
↓
+0x00
↓
vptr
Then:
mov rax, [rax+0x10
]
That is, an entry at a specific offset is read from the vptr table
Then:
call rax
That is, that value is used for Call
So we have a conceptual path:
Object
↓
+0x00
↓
vptr
↓
vtable
↓
Function Pointer
↓
call
This is one of the most important patterns in Reverse Engineering C++ programs.
How do we find the Virtual Function from this pattern?
Suppose we have this Assembly:
mov rax, [rcx]
mov rax, [rax+0x18]
call rax
We can create this model for now:
Object
|
+0x00 → vptr
|
↓
vtable
|
+0x18 → function pointer
But one important point:
We should not say quickly:
+0x18 = Function X
Because we still do not know exactly what Virtual Function Entry is related to
We should check the vtable itself and its References
Now Inheritance comes in
Suppose we have:
class Animal {
public:
virtual void speak();
};
class Dog : public Animal {
public:
void speak() override;
};
Conceptually:
Animal
↑
|
Dog
Dog
inherits parts of Animal
The Object might look something like this:
Dog Object
+0x00 vptr
+0x08 Animal-related field
+0x0C Dog-related field
This Layout is not exact and is just a conceptual model
Because the actual Layout depends on the Compiler, ABI, Alignment and Inheritance type
But the important point is that:
Base Class
+
Derived Class
can be present in a single Object
We can find the relationship from the Constructor
Suppose we have a Constructor:
mov qword ptr [rcx], offset vtable_Animal
And then in the Constructor we see another class:
call Animal::Animal
mov qword ptr [rcx], offset vtable_Dog
Here we have a very important clue
That is, the Constructor related to Dog first initializes the part related to the Base Class and then the Virtual Dispatch state related to Sets the Derived Class
Conceptually:
Dog Constructor
|
├── Animal Constructor
|
└── Dog-specific initialization
Private Memory و
Image-backed Memory و
VAD
تا اینجا درباره RW → RX صحبت کردیم
این Memory اصلا متعلق به چیه؟
وقتی یک Process رو بررسی میکنید همه Memory هایی که میبینید یک جنس نیستن
برای تحلیل Memory-based Behavior باید بدونیم هر Region چطور ساخته شده و چه رابطهای با فایل روی دیسک داره
Image-backed Memory
وقتی یک PE مثل:
notepad.exe
ntdll.dll
kernel32.dll
داخل Process قرار میگیره بخش هایی از Virtual Address Space به یک Image File روی دیسک مرتبط میشن
بهصورت مفهومی:
PE File
│
▼
Image Mapping
│
▼
Process Virtual Address Space
مثلا یک Executable ممکنه Section هایی مثل این داشته باشه:
notepad.exe
├── .text
├── .rdata
├── .data
└── ...
در Memory این بخشها معمولا به Image مربوط به همان Module مرتبط هستن پس وقتی یک Region از نوع Image باشه یکی از سوالهای مهم اینه:
این Region به کدوم Module روی دیسک مربوطه؟
Private Memory
نوع دیگری از Memory به شکل Private ایجاد میشه
این Memory مستقیما یک Image Mapping از فایل PE مشخص نیست
مثلا Process ممکنه برای موارد مختلف از Private Memory استفاده کنه:
Process
│
├── Image-backed Memory
│
└── Private Memory
├── Heap
├── Runtime Data
├── Temporary Allocations
└── Other Private Allocations
نکته مهم:
Private Memory ≠ Malware
برنامه های کاملا Legitimate هم دائما از Private Memory استفاده میکنن
پس صرفا دیدن
Type = Private
هیچ چیزی رو اثبات نمیکنه
پس چرا Private Memory برای تحلیل امنیتی مهمه؟
چون باید چند ویژگی رو کنار هم بررسی کنیم
مثلا:
Private Memory
+
Executable Permission
+
Unusual Thread
+
Unexpected Call Stack
هر کدوم به تنهایی میتونه کاملا طبیعی باشه
ولی وقتی چند Evidence کنار هم قرار میگیرن ارزش Investigation بیشتر میشه
مثلا یک Region:
Private
RX
به تنهایی نمیگه:
Malicious
اما میتونه یک نقطه مناسب برای بررسی بیشتر باشه
تحلیلگر باید Context رو بررسی کنه
VAD
حالا میرسیم به VAD
Virtual Address Descriptor
VAD
یکی از ساختارهای داخلی
Windows
برای توصیف محدودههای یک Virtual Address Space Process هست
به صورت مفهومی:
Process Virtual Address Space
0x0000
│
├── Region A
├── Region B
├── Region C
├── Region D
└── ...
0xFFFF
VAD ها به Kernel کمک میکنن اطلاعات
مربوط به محدوده های Virtual Memory یک Process رو مدیریت و توصیف کنه
مثلا اینکه یک محدوده کجاست و چه ویژگی هایی داره
Memory Region
فقط یک Address نیست
فرض کنید در Debugger یا Memory Analysis چنین چیزی ببینید:
Address: 0x7...
Size: 0x...
Protection: RX
Type: Private
اگر فقط Address رو ببینید اطلاعات زیادی از دست میدید
باید چند تا سوال بپرسید:
این Region کجاست؟
↓
چه اندازهای داره؟
↓
Protection اون چیه؟
↓
Private
هشت یا Image؟
↓
به چه Mapping یا Allocation ای مربوطه؟
↓
چه Thread هایی به اون دسترسی یا اجرای مرتبط دارن؟
↓
در طول زمان چه اتفاقی برای اون افتاده؟
اینجا Memory Analysis از نگاه کردن به چند Hex Value تبدیل میشه به تحلیل رفتار
VAD در Memory Forensics
در Memory Forensics میتونیم ساختار های Virtual Memory مربوط به Process رو بررسی کنیم
بهصورت مفهومی:
Process
│
├── Image
│ ├── .text
│ └── .data
│
├── DLLs
│
├── Heap
│
└── Private Regions
حالا فرض کنید یک Region پیدا کردیم که نیاز به بررسی بیشتری داره
میتونیم بپرسیم:
Protection چیه؟
↓
Private هست یا Image?
↓
به Module شناختهشده ای مرتبطه؟
↓
محتواش چه شکلیه؟
↓
Thread ی با اون ارتباط داره
↓
Call Stack چه چیزی نشون میده؟
↓
وضعیت Region در طول زمان تغییر کرده؟
نکته مهم اینه که VAD بهتنهایی جواب همه این سؤالها رو نمیده
VAD
یکی از منابع Evidence در تحلیل Memory هست
یک سناریوی تحلیلی
فرض کنید داخل یک Memory Dump این وضعیت رو داریم:
Process: example.exe
Region A
Image
RX
Region B
Private
RW
Region C
Private
RX
Region A خیلی عادی به نظر میرسه
چون به یک Image مربوطه و Permission اون هم برای اجرای Code قابل انتظاره
Image-backed Memory و
VAD
تا اینجا درباره RW → RX صحبت کردیم
این Memory اصلا متعلق به چیه؟
وقتی یک Process رو بررسی میکنید همه Memory هایی که میبینید یک جنس نیستن
برای تحلیل Memory-based Behavior باید بدونیم هر Region چطور ساخته شده و چه رابطهای با فایل روی دیسک داره
Image-backed Memory
وقتی یک PE مثل:
notepad.exe
ntdll.dll
kernel32.dll
داخل Process قرار میگیره بخش هایی از Virtual Address Space به یک Image File روی دیسک مرتبط میشن
بهصورت مفهومی:
PE File
│
▼
Image Mapping
│
▼
Process Virtual Address Space
مثلا یک Executable ممکنه Section هایی مثل این داشته باشه:
notepad.exe
├── .text
├── .rdata
├── .data
└── ...
در Memory این بخشها معمولا به Image مربوط به همان Module مرتبط هستن پس وقتی یک Region از نوع Image باشه یکی از سوالهای مهم اینه:
این Region به کدوم Module روی دیسک مربوطه؟
Private Memory
نوع دیگری از Memory به شکل Private ایجاد میشه
این Memory مستقیما یک Image Mapping از فایل PE مشخص نیست
مثلا Process ممکنه برای موارد مختلف از Private Memory استفاده کنه:
Process
│
├── Image-backed Memory
│
└── Private Memory
├── Heap
├── Runtime Data
├── Temporary Allocations
└── Other Private Allocations
نکته مهم:
Private Memory ≠ Malware
برنامه های کاملا Legitimate هم دائما از Private Memory استفاده میکنن
پس صرفا دیدن
Type = Private
هیچ چیزی رو اثبات نمیکنه
پس چرا Private Memory برای تحلیل امنیتی مهمه؟
چون باید چند ویژگی رو کنار هم بررسی کنیم
مثلا:
Private Memory
+
Executable Permission
+
Unusual Thread
+
Unexpected Call Stack
هر کدوم به تنهایی میتونه کاملا طبیعی باشه
ولی وقتی چند Evidence کنار هم قرار میگیرن ارزش Investigation بیشتر میشه
مثلا یک Region:
Private
RX
`به تنهایی نمیگه:
Malicious
اما میتونه یک نقطه مناسب برای بررسی بیشتر باشه
تحلیلگر باید Context رو بررسی کنه
VAD
حالا میرسیم به VAD
Virtual Address Descriptor
VAD
یکی از ساختارهای داخلی
Windows
برای توصیف محدودههای یک Virtual Address Space Process هست
به صورت مفهومی:
Process Virtual Address Space
0x0000
│
├── Region A
├── Region B
├── Region C
├── Region D
└── ...
0xFFFF
VAD ها به Kernel کمک میکنن اطلاعات
مربوط به محدوده های Virtual Memory یک Process رو مدیریت و توصیف کنه
مثلا اینکه یک محدوده کجاست و چه ویژگی هایی داره
Memory Region
فقط یک Address نیست
فرض کنید در Debugger یا Memory Analysis چنین چیزی ببینید:
Address: 0x7...
Size: 0x...
Protection: RX
Type: Private
اگر فقط Address رو ببینید اطلاعات زیادی از دست میدید
باید چند تا سوال بپرسید:
این Region کجاست؟
↓
چه اندازهای داره؟
↓
Protection اون چیه؟
↓
Private
هشت یا Image؟
↓
به چه Mapping یا Allocation ای مربوطه؟
↓
چه Thread هایی به اون دسترسی یا اجرای مرتبط دارن؟
↓
در طول زمان چه اتفاقی برای اون افتاده؟
اینجا Memory Analysis از نگاه کردن به چند Hex Value تبدیل میشه به تحلیل رفتار
VAD در Memory Forensics
در Memory Forensics میتونیم ساختار های Virtual Memory مربوط به Process رو بررسی کنیم
بهصورت مفهومی:
Process
│
├── Image
│ ├── .text
│ └── .data
│
├── DLLs
│
├── Heap
│
└── Private Regions
حالا فرض کنید یک Region پیدا کردیم که نیاز به بررسی بیشتری داره
میتونیم بپرسیم:
Protection چیه؟
↓
Private هست یا Image?
↓
به Module شناختهشده ای مرتبطه؟
↓
محتواش چه شکلیه؟
↓
Thread ی با اون ارتباط داره
↓
Call Stack چه چیزی نشون میده؟
↓
وضعیت Region در طول زمان تغییر کرده؟
نکته مهم اینه که VAD بهتنهایی جواب همه این سؤالها رو نمیده
VAD
یکی از منابع Evidence در تحلیل Memory هست
یک سناریوی تحلیلی
فرض کنید داخل یک Memory Dump این وضعیت رو داریم:
Process: example.exe
Region A
Image
RX
Region B
Private
RW
Region C
Private
RX
Region A خیلی عادی به نظر میرسه
چون به یک Image مربوطه و Permission اون هم برای اجرای Code قابل انتظاره
Region B
هم میتونه کاملا طبیعی باشه
چون Private RW داخل Process ها بسیار رایجه
اما Region C:
Private
RX
ارزش بررسی بیشتری پیدا میکنه
حالا اطلاعات بیشتری به دست میاریم:
Region C
↓
Private
↓
RX
↓
Thread Start Address → داخل Region
↓
Call Stack → غیرمعمول
اینجا Region C تبدیل میشه به یک Investigation Target
ولی هنوز نمیگیم:
Malware Found
چون ممکنه توضیح Legitimate دیگه ای وجود داشته باشه
مثلا Runtimeها JIT Compilatio، Instrumentation یا بعضی تکنیک های نرمافزاری هم میتونن Memory Executable ایجاد کنن
پس:
Suspicious
≠
Malicious
Permission فقط یک تکیه از پازله
تا اینجا درباره Permission زیاد صحبت کردیم
مثلا:
RW
RX
RWX
ولی یک تحلیل درست فقط Permission رو نگاه نمیکنه
مثلا این دو Region رو مقایسه کنید:
Region A
Image
RX
Known Module
Normal Thread Activity
و:
Region B
Private
RX
Unknown Mapping
Unusual Thread Start
Unexpected Call Stack
هر دو ممکنه Executable باشن
ولی Context کاملا متفاوتی دارن
پس یک مدل بهتر اینه:
Memory Type
+
Protection
+
Content
+
Thread
+
Call Stack
+
Module Relationship
+
Timeline
هرچه Evidence ها به یک داستان مشترک اشاره کنن Confidence تحلیل بالاتر میره
چرا Timeline مهمه؟
فرض کنید یک Region الان این شکلیه:
Private
RX
فقط با دیدن وضعیت فعلی نمیدونیم قبلش چه اتفاقی افتاده
ممکنه اول:
RW
بوده و بعد Permission اون تغییر کرده باشه
یا از اول:
RX
ایجاد شده باشه
یا یک Runtime legitimate اون رو ایجاد کرده باشه
پس اگر Telemetry کافی داشته باشیم
Timeline
میتونه خیلی مهم باشه:
Allocation
↓
Memory Write
↓
Protection Change
↓
Thread Creation
↓
Execution
این زنجیره از یک Snapshot ساده اطلاعات بیشتری میده
از دید Detection
حالا میتونیم چند Evidence رو کنار هم بذاریم:
Process
│
├── Memory Region
│ ├── Private
│ └── Executable
│
├── Thread
│ └── Start Address → Region
│
└── Call Stack
└── Unexpected Frames
هیچکدوم به تنهایی لزوما نشونه حمله نیست
ولی ارتباط بین اونها میتونه برای Detection مهم باشه
این دقیقا یکی از ایدههای اصلی
Behavioral Detection
هست:
یک Event
↓
ممکنه عادی باشه
چند Event مرتبط
↓
Context
↓
Behavior
از دید ردتیمرا
از دید مهاجم هم این قسمت یک درس مهم داره
Memory هم Telemetry
ایجاد میکنه
یعنی این تصور:
No File
=
No Detection
اشتباهه چون ممکنه فایل واضحی روی Disk وجود نداشته باشه ولی وضعیت Process و Memory همچنان Evidence ایجاد کنه
مدل کامل تر:
Disk
+
Process
+
Memory
+
Thread
+
Call Stack
+
Timeline
↓
Behavioral Evidence
پس درک Memory فقط برای کسی که Detection مینویسه مهم نیست
برای Red Team هم مهمه که بدونه کد و Data بعد از ورود به Process چه ردپای قابل مشاهده ای ایجاد میکنن
یک نکته مهم درباره Private و Image
این دو دسته رو بیش از حد ساده نکنیم
در Windows Memory Management نوع یک Region فقط با این جمله تعیین نمیشه که:
Private = چیزی که برنامه خودش ساخته
Image =
چیزی که حتما مستقیم از Disk خونده شده
جزئیات Memory Manager Section Objectها Mappingها و نوع Allocation مهم هستن
پس وقتی در ابزارهای Forensics یا Debugger چیزی مثل:
`
Type = Private
میبینید باید دقیقا بدونید اون ابزار بر اساس چه اطلاعاتی این Classification رو انجام داده
هدف ما در RE و Memory Forensics این نیست که یک Label رو حفظ کنیم
هدف اینه که بدونیم پشت اون Label چه Memory Behavior ای وجود داره
API / Syscall
↓
Memory Allocation
↓
Memory Protection
↓
Memory Type
↓
Thread
↓
Call Stack
↓
VAD
↓
Behavioral Analysis
سه مفهوم رو فعلا خوب نگه دارید:
Image-backed Memory
↓
Memory
مرتبط با Image Mapping
Private Memory
↓
Memory
که به یک Image Mapping معمولی وابسته نیست
VAD
↓
ساختارهای Kernel برای توصیف محدوده های Virtual Address Space
و مهمتر از همه:
Private
≠
Malicious
Executable
≠
Malicious
RW → RX
≠
Malicious
چیزی که مهمه ترکیب Evidence ها و Context هست
@reverseengine
هم میتونه کاملا طبیعی باشه
چون Private RW داخل Process ها بسیار رایجه
اما Region C:
Private
RX
ارزش بررسی بیشتری پیدا میکنه
حالا اطلاعات بیشتری به دست میاریم:
Region C
↓
Private
↓
RX
↓
Thread Start Address → داخل Region
↓
Call Stack → غیرمعمول
اینجا Region C تبدیل میشه به یک Investigation Target
ولی هنوز نمیگیم:
Malware Found
چون ممکنه توضیح Legitimate دیگه ای وجود داشته باشه
مثلا Runtimeها JIT Compilatio، Instrumentation یا بعضی تکنیک های نرمافزاری هم میتونن Memory Executable ایجاد کنن
پس:
Suspicious
≠
Malicious
Permission فقط یک تکیه از پازله
تا اینجا درباره Permission زیاد صحبت کردیم
مثلا:
RW
RX
RWX
ولی یک تحلیل درست فقط Permission رو نگاه نمیکنه
مثلا این دو Region رو مقایسه کنید:
Region A
Image
RX
Known Module
Normal Thread Activity
و:
Region B
Private
RX
Unknown Mapping
Unusual Thread Start
Unexpected Call Stack
هر دو ممکنه Executable باشن
ولی Context کاملا متفاوتی دارن
پس یک مدل بهتر اینه:
Memory Type
+
Protection
+
Content
+
Thread
+
Call Stack
+
Module Relationship
+
Timeline
هرچه Evidence ها به یک داستان مشترک اشاره کنن Confidence تحلیل بالاتر میره
چرا Timeline مهمه؟
فرض کنید یک Region الان این شکلیه:
Private
RX
فقط با دیدن وضعیت فعلی نمیدونیم قبلش چه اتفاقی افتاده
ممکنه اول:
RW
بوده و بعد Permission اون تغییر کرده باشه
یا از اول:
RX
ایجاد شده باشه
یا یک Runtime legitimate اون رو ایجاد کرده باشه
پس اگر Telemetry کافی داشته باشیم
Timeline
میتونه خیلی مهم باشه:
Allocation
↓
Memory Write
↓
Protection Change
↓
Thread Creation
↓
Execution
این زنجیره از یک Snapshot ساده اطلاعات بیشتری میده
از دید Detection
حالا میتونیم چند Evidence رو کنار هم بذاریم:
Process
│
├── Memory Region
│ ├── Private
│ └── Executable
│
├── Thread
│ └── Start Address → Region
│
└── Call Stack
└── Unexpected Frames
هیچکدوم به تنهایی لزوما نشونه حمله نیست
ولی ارتباط بین اونها میتونه برای Detection مهم باشه
این دقیقا یکی از ایدههای اصلی
Behavioral Detection
هست:
یک Event
↓
ممکنه عادی باشه
چند Event مرتبط
↓
Context
↓
Behavior
از دید ردتیمرا
از دید مهاجم هم این قسمت یک درس مهم داره
Memory هم Telemetry
ایجاد میکنه
یعنی این تصور:
No File
=
No Detection
اشتباهه چون ممکنه فایل واضحی روی Disk وجود نداشته باشه ولی وضعیت Process و Memory همچنان Evidence ایجاد کنه
مدل کامل تر:
Disk
+
Process
+
Memory
+
Thread
+
Call Stack
+
Timeline
↓
Behavioral Evidence
پس درک Memory فقط برای کسی که Detection مینویسه مهم نیست
برای Red Team هم مهمه که بدونه کد و Data بعد از ورود به Process چه ردپای قابل مشاهده ای ایجاد میکنن
یک نکته مهم درباره Private و Image
این دو دسته رو بیش از حد ساده نکنیم
در Windows Memory Management نوع یک Region فقط با این جمله تعیین نمیشه که:
Private = چیزی که برنامه خودش ساخته
Image =
چیزی که حتما مستقیم از Disk خونده شده
جزئیات Memory Manager Section Objectها Mappingها و نوع Allocation مهم هستن
پس وقتی در ابزارهای Forensics یا Debugger چیزی مثل:
`
Type = Private
میبینید باید دقیقا بدونید اون ابزار بر اساس چه اطلاعاتی این Classification رو انجام داده
هدف ما در RE و Memory Forensics این نیست که یک Label رو حفظ کنیم
هدف اینه که بدونیم پشت اون Label چه Memory Behavior ای وجود داره
API / Syscall
↓
Memory Allocation
↓
Memory Protection
↓
Memory Type
↓
Thread
↓
Call Stack
↓
VAD
↓
Behavioral Analysis
سه مفهوم رو فعلا خوب نگه دارید:
Image-backed Memory
↓
Memory
مرتبط با Image Mapping
Private Memory
↓
Memory
که به یک Image Mapping معمولی وابسته نیست
VAD
↓
ساختارهای Kernel برای توصیف محدوده های Virtual Address Space
و مهمتر از همه:
Private
≠
Malicious
Executable
≠
Malicious
RW → RX
≠
Malicious
چیزی که مهمه ترکیب Evidence ها و Context هست
@reverseengine
ReverseEngineering
Private Memory و Image-backed Memory و VAD تا اینجا درباره RW → RX صحبت کردیم این Memory اصلا متعلق به چیه؟ وقتی یک Process رو بررسی میکنید همه Memory هایی که میبینید یک جنس نیستن برای تحلیل Memory-based Behavior باید بدونیم هر Region چطور ساخته شده…
Private Memory and
Image-backed Memory and
VAD
So far we have talked about RW → RX
What does this Memory even belong to?
When you examine a process, not all the memory you see is the same
To analyze memory-based behavior, we need to know how each region is built and what its relationship is to the file on disk
Image-backed Memory
When a PE such as:
notepad.exe
ntdll.dll
kernel32.dll
is placed inside the process, parts of the Virtual Address Space are linked to an Image File on disk
Conceptually:
PE File
│
▼
Image Mapping
│
▼
Process Virtual Address Space
For example, an Executable may have sections like this:
notepad.exe
├── .text
├── .rdata
├── .data
└── ...
In memory, these sections are usually linked to the Image related to the same Module, so when a Region is of the Image type, one of the important questions is:
Which Module on disk does this Region relate to?
Private Memory
Another type of memory is created as Private
This memory is not directly mapped to an image from the PE file. For example, a process may use Private Memory for various purposes:
Process
│
├── Image-backed Memory
│
└── Private Memory
├── Heap
├── Runtime Data
├── Temporary Allocations
└── Other Private Allocations
Important note:
Private Memory ≠ Malware
Completely legitimate programs use Private Memory all the time
So just seeing
Type = Private
doesn't prove anything
So why is Private Memory important for security analysis?
Because we need to examine several features together
For example:
Private Memory
+
Executable Permission
+
Unusual Thread
+
Unexpected Call Stack
Each one can be completely normal on its own
But when several Evidences are put together, the Investigation value increases
For example, a Region:
Private
RX`
Alone does not say:
Malicious
But it can be a good point for further investigation
The analyst should examine the Context
VAD
Now we come to VAD
Virtual Address Descriptor
VAD is one of the internal structures
Windows
to describe the limits of a Virtual Address Space Process
Conceptually:
Process Virtual Address Space
0x0000
│
├── Region A
├── Region B
├── Region C
├── Region D
└── ...
0xFFFF
VADs help the Kernel manage and describe information
regarding the Virtual Memory limits of a Process
For example, that a Where is the region and what are its properties
Memory Region
Not just an Address
Suppose you see something like this in Debugger or Memory Analysis:
Address: 0x7...
Size: 0x...
Protection: RX
Type: Private
If you only see the Address, you will miss a lot of information
You should ask a few questions:
Where is this Region?
↓
How big is it?
↓
What is its Protection?
↓
Private
Is it an Octal or an Image?
↓
What Mapping or Allocation does it belong to?
↓
What Threads are accessing or executing it?
↓
What happened to it over time?
Here Memory Analysis goes from looking at a few Hex Values to Behavioral Analysis
VAD in Memory Forensics
In Memory Forensics we can examine the Virtual Memory structures related to the Process
Conceptually:
Process
│
├── Image
│
├── .text
│
└── .data
│
├── DLLs
│
├── Heap
│
└── Private Regions
Now suppose we find a Region that needs further investigation
We can ask:
What is the Protection?
↓
Is it Private or Image?
↓
Is it associated with a known Module?
↓
What does its content look like?
↓
Is there a Thread associated with it
↓
What does the Call Stack show?
↓
Has the Region's state changed over time?
The important point is that VAD alone does not answer all these questions
VAD is one of the sources of evidence in memory analysis
Analysis scenario
Suppose we have this situation in a memory dump:
Process: example.exe
Region A
Image
RX
Region B
Private
RW
Region C
Private
RX
Region A looks very normal
Because it is related to an image and its permission is expected for executing code
Image-backed Memory and
VAD
So far we have talked about RW → RX
What does this Memory even belong to?
When you examine a process, not all the memory you see is the same
To analyze memory-based behavior, we need to know how each region is built and what its relationship is to the file on disk
Image-backed Memory
When a PE such as:
notepad.exe
ntdll.dll
kernel32.dll
is placed inside the process, parts of the Virtual Address Space are linked to an Image File on disk
Conceptually:
PE File
│
▼
Image Mapping
│
▼
Process Virtual Address Space
For example, an Executable may have sections like this:
notepad.exe
├── .text
├── .rdata
├── .data
└── ...
In memory, these sections are usually linked to the Image related to the same Module, so when a Region is of the Image type, one of the important questions is:
Which Module on disk does this Region relate to?
Private Memory
Another type of memory is created as Private
This memory is not directly mapped to an image from the PE file. For example, a process may use Private Memory for various purposes:
Process
│
├── Image-backed Memory
│
└── Private Memory
├── Heap
├── Runtime Data
├── Temporary Allocations
└── Other Private Allocations
Important note:
Private Memory ≠ Malware
Completely legitimate programs use Private Memory all the time
So just seeing
Type = Private
doesn't prove anything
So why is Private Memory important for security analysis?
Because we need to examine several features together
For example:
Private Memory
+
Executable Permission
+
Unusual Thread
+
Unexpected Call Stack
Each one can be completely normal on its own
But when several Evidences are put together, the Investigation value increases
For example, a Region:
Private
RX`
Alone does not say:
Malicious
But it can be a good point for further investigation
The analyst should examine the Context
VAD
Now we come to VAD
Virtual Address Descriptor
VAD is one of the internal structures
Windows
to describe the limits of a Virtual Address Space Process
Conceptually:
Process Virtual Address Space
0x0000
│
├── Region A
├── Region B
├── Region C
├── Region D
└── ...
0xFFFF
VADs help the Kernel manage and describe information
regarding the Virtual Memory limits of a Process
For example, that a Where is the region and what are its properties
Memory Region
Not just an Address
Suppose you see something like this in Debugger or Memory Analysis:
Address: 0x7...
Size: 0x...
Protection: RX
Type: Private
If you only see the Address, you will miss a lot of information
You should ask a few questions:
Where is this Region?
↓
How big is it?
↓
What is its Protection?
↓
Private
Is it an Octal or an Image?
↓
What Mapping or Allocation does it belong to?
↓
What Threads are accessing or executing it?
↓
What happened to it over time?
Here Memory Analysis goes from looking at a few Hex Values to Behavioral Analysis
VAD in Memory Forensics
In Memory Forensics we can examine the Virtual Memory structures related to the Process
Conceptually:
Process
│
├── Image
│
├── .text
│
└── .data
│
├── DLLs
│
├── Heap
│
└── Private Regions
Now suppose we find a Region that needs further investigation
We can ask:
What is the Protection?
↓
Is it Private or Image?
↓
Is it associated with a known Module?
↓
What does its content look like?
↓
Is there a Thread associated with it
↓
What does the Call Stack show?
↓
Has the Region's state changed over time?
The important point is that VAD alone does not answer all these questions
VAD is one of the sources of evidence in memory analysis
Analysis scenario
Suppose we have this situation in a memory dump:
Process: example.exe
Region A
Image
RX
Region B
Private
RW
Region C
Private
RX
Region A looks very normal
Because it is related to an image and its permission is expected for executing code