ReverseEngineering
قسمت سی و چهارم بافر اورفلو Symbolic Execution از ورودی واقعی تا ورودی نماد ها تا اینجا یاد گرفتیم چطور داده رو داخل برنامه دنبال کنیم و بفهمیم چطور به یک Crash میرسه حالا میخوایم یک قدم جلوتر بریم فرض کنید یک برنامه شرطی داره که فقط وقتی ورودی مقدار خاصی…
This is crucial for binary analysis, as processors operate on values of specific sizes—such as 8, 32, or 64 bits—and their behavior is always... It is not like mathematical integers.
The connection between Symbolic Execution and Reverse Engineering
Suppose you have a binary containing the following logic:
C++
int validate(int value)
{
if (value < 100)
return 0;
if (value > 200)
return 0;
if (value == 150)
return 1;
The connection between Symbolic Execution and Reverse Engineering
Suppose you have a binary containing the following logic:
C++
int validate(int value)
{
if (value < 100)
return 0;
if (value > 200)
return 0;
if (value == 150)
return 1;
C
return 0;
}
Standard analysis reveals that the program has several conditions.
However, Symbolic Execution can extract the conditions necessary to reach a successful execution path:
value >= 100
AND
value <= 200
AND
value == 150
Consequently,
value = 150
is a valid solution.
This capability has significant applications in reverse engineering:
Analyzing validation logic
Finding the inputs required to reach a specific path
Examining complex conditions within the binary
Analyzing paths that lead to a crash
Determining which constraints make a path reachable or unreachable
Of course, in a real binary, conditions might depend on multiple functions or be influenced by memory and runtime state.
In such scenarios, constructing an accurate model of the program becomes more difficult.
The difference between Symbolic and Concrete Execution
Make sure to understand this distinction well.
Concrete Execution
The program runs with a concrete (actual) value.
x = 15
In this case, program processing is based on the actual value of 15.
Symbolic Execution
Instead of a concrete value, we use a symbolic value.
x = X
When conditions are encountered, constraints are generated for X.
A simple comparison
An important point
Symbolic Execution
does not guarantee that all program paths will be examined.
Loops and a large number of branches can lead to a "path explosion." 😁
Complex calculations and interactions with the operating system or external libraries can also complicate the analysis.
So, even Symbolic Execution has its own limitations.
Connection to Fuzzing
This is where Symbolic Execution demonstrates one of its most interesting applications.
Suppose a fuzzer has generated an input and executed the program:
Input
↓
Program
↓
Branch A
↓
Branch B ↓
Crash
Now, let's examine a new execution path—one that wasn't reached because a specific condition wasn't met.
For instance, the program contains a condition like this:
JavaScript
if (x == 1337)
dangerous_function(); If our current input is:
x = 10
The program does not enter
Symbolic Execution
can extract the branch condition:
X == 1337
Then, the solver suggests a valid value:
X = 1337
This value can be used as a new input in the testing process.
So, the overall workflow looks like this:
Fuzzer
↓
Input
↓
Program Execution
↓
Uncovered Branch
↓
Path Constraint
↓
SMT Solver
↓
New Input
↓
Fuzzing Again
This concept is also utilized in methods like Concolic Execution.
In Concolic Execution, the program runs with concrete inputs while values and conditions are simultaneously tracked symbolically; thus, concrete execution and symbolic analysis are combined.
Practical Tools
To gain hands-on experience with this topic, a few tools are worth exploring:
angr
A binary analysis framework featuring capabilities such as Symbolic Execution and path analysis.
Official repository:
https://github.com/angr/angr
Triton
A framework for Dynamic Binary Analysis that offers capabilities like Taint Analysis and Symbolic Execution.
Official repository:
https://github.com/JonathanSalwan/Triton
Z3
An SMT solver capable of solving symbolic constraints.
Official repository:
https://github.com/Z3Prover/z3
These tools do not perform exactly the same function:
angr
Is primarily used for constructing and analyzing symbolic paths within binaries.
Triton
Provides capabilities for dynamic binary analysis and symbolic computation.
Z3
Is a constraint solver; while it is not a standalone, all-in-one tool for analyzing binary execution paths, it is a crucial component of such systems. It’s not possible [to do it that way].
The general workflow
of Symbolic Execution looks like this:
Symbolic Input
↓
Program Analysis
↓
Branch Conditions
↓
Path Constraints
↓
SMT Solver
↓
Possible Input
return 0;
}
Standard analysis reveals that the program has several conditions.
However, Symbolic Execution can extract the conditions necessary to reach a successful execution path:
value >= 100
AND
value <= 200
AND
value == 150
Consequently,
value = 150
is a valid solution.
This capability has significant applications in reverse engineering:
Analyzing validation logic
Finding the inputs required to reach a specific path
Examining complex conditions within the binary
Analyzing paths that lead to a crash
Determining which constraints make a path reachable or unreachable
Of course, in a real binary, conditions might depend on multiple functions or be influenced by memory and runtime state.
In such scenarios, constructing an accurate model of the program becomes more difficult.
The difference between Symbolic and Concrete Execution
Make sure to understand this distinction well.
Concrete Execution
The program runs with a concrete (actual) value.
x = 15
In this case, program processing is based on the actual value of 15.
Symbolic Execution
Instead of a concrete value, we use a symbolic value.
x = X
When conditions are encountered, constraints are generated for X.
A simple comparison
An important point
Symbolic Execution
does not guarantee that all program paths will be examined.
Loops and a large number of branches can lead to a "path explosion." 😁
Complex calculations and interactions with the operating system or external libraries can also complicate the analysis.
So, even Symbolic Execution has its own limitations.
Connection to Fuzzing
This is where Symbolic Execution demonstrates one of its most interesting applications.
Suppose a fuzzer has generated an input and executed the program:
Input
↓
Program
↓
Branch A
↓
Branch B ↓
Crash
Now, let's examine a new execution path—one that wasn't reached because a specific condition wasn't met.
For instance, the program contains a condition like this:
JavaScript
if (x == 1337)
dangerous_function(); If our current input is:
x = 10
The program does not enter
dangerous_function.Symbolic Execution
can extract the branch condition:
X == 1337
Then, the solver suggests a valid value:
X = 1337
This value can be used as a new input in the testing process.
So, the overall workflow looks like this:
Fuzzer
↓
Input
↓
Program Execution
↓
Uncovered Branch
↓
Path Constraint
↓
SMT Solver
↓
New Input
↓
Fuzzing Again
This concept is also utilized in methods like Concolic Execution.
In Concolic Execution, the program runs with concrete inputs while values and conditions are simultaneously tracked symbolically; thus, concrete execution and symbolic analysis are combined.
Practical Tools
To gain hands-on experience with this topic, a few tools are worth exploring:
angr
A binary analysis framework featuring capabilities such as Symbolic Execution and path analysis.
Official repository:
https://github.com/angr/angr
Triton
A framework for Dynamic Binary Analysis that offers capabilities like Taint Analysis and Symbolic Execution.
Official repository:
https://github.com/JonathanSalwan/Triton
Z3
An SMT solver capable of solving symbolic constraints.
Official repository:
https://github.com/Z3Prover/z3
These tools do not perform exactly the same function:
angr
Is primarily used for constructing and analyzing symbolic paths within binaries.
Triton
Provides capabilities for dynamic binary analysis and symbolic computation.
Z3
Is a constraint solver; while it is not a standalone, all-in-one tool for analyzing binary execution paths, it is a crucial component of such systems. It’s not possible [to do it that way].
The general workflow
of Symbolic Execution looks like this:
Symbolic Input
↓
Program Analysis
↓
Branch Conditions
↓
Path Constraints
↓
SMT Solver
↓
Possible Input
GitHub
GitHub - angr/angr: A powerful and user-friendly binary analysis platform!
A powerful and user-friendly binary analysis platform! - angr/angr
In previous sections, we learned where data enters the program and how it moves through registers and memory.
Now, we have learned how to analyze program execution conditions to determine which inputs can reach a specific path.
When we combine these concepts:
Fuzzing
↓
Interesting Input
↓
Dynamic Taint Analysis
↓
Data Flow
↓
Symbolic Execution
↓
Path Constraints
↓
New Input
↓
Crash Analysis
↓
Root Cause
We no longer limit ourselves to simply running the program and observing crashes; we can trace the data path, understand the necessary conditions, and generate more targeted inputs to explore new paths.
@reverseengine
Now, we have learned how to analyze program execution conditions to determine which inputs can reach a specific path.
When we combine these concepts:
Fuzzing
↓
Interesting Input
↓
Dynamic Taint Analysis
↓
Data Flow
↓
Symbolic Execution
↓
Path Constraints
↓
New Input
↓
Crash Analysis
↓
Root Cause
We no longer limit ourselves to simply running the program and observing crashes; we can trace the data path, understand the necessary conditions, and generate more targeted inputs to explore new paths.
@reverseengine
Memory Aliasing بین توابع
تا اینجا دیدیم که چطور ممکنه دو تا Pointer به یه آدرس از حافظه اشاره کنن حالا میخوایم یه حالت مهمتر رو بررسی کنیم
فرض کنید یه تابع داریم که یه مقدار رو توی حافظه تغییر میده و یه تابع دیگه همون مقدار رو از حافظه میخونه
نکته اینجاست که ممکنه این دو تا تابع از Pointer های متفاوتی استفاده کنن ولی هر دو به یه آدرس مشترک دسترسی داشته باشن
این موضوع توی مهندسی معکوس خیلی مهمه، چون ممکنه با بررسی هر تابع بهتنهایی متوجه ارتباط بین اونها نشیم
یه مثال ساده با
C
void set_value(int *p)
{
*p = 100;
}
int get_value(int *q)
{
return *q;
}
int main()
{
int value = 10;
set_value(&value);
int result = get_value(&value);
return result;
}
اینجا چه اتفاقی میفته؟
اول مقدار
بعد آدرس
تابع
بعد همون آدرس به تابع
در نهایت
نکته اصلی اینه که Pointerهای
پس ارتباط بین این دو تابع از طریق حافظه شکل گرفته، نه از طریق مقدار برگشتی یکی از اونها
حالا این موضوع توی اسمبلی چه شکلیه؟
فرض کنید تابع
set_value:
mov dword ptr [rdi], 100
ret
این مثال بر اساس قرارداد فراخوانی System V AMD64 نوشته شده که معمولا توی برنامههای 64 بیتی لینوکس استفاده میشه
طبق این قرار داد آرگومان اول تابع داخل رجیستر
حالا این دستور رو بررسی کنیم
mov dword ptr [rdi], 100
این دستور عدد 100 رو در آدرسی مینویسه که داخل
دقت کنید که مقدار
آدرس رو نگه میداره و عبارت
حالا تابع
get_value:
mov eax, dword ptr [rdi]
ret
اینجا مقدار چهار بایتی موجود در آدرس
حالا توی مهندسی معکوس چطور پیداش کنیم؟
وقتی داری یه فایل باینری رو توی Ghidra یا Disassembler بررسی میکنید این مراحل رو دنبال کنید
مرحله اول:
آرگومانهای تابع رو بررسی کنید
ببینید تابع چه Pointer هایی دریافت میکنه و از اونها برای خوندن یا نوشتن حافظه استفاده میکنه یا نه
مرحله دوم:
دنبال دستورهای نوشتن حافظه بگردید
مثلا دستور زیر رو در نظر بگیرید
mov dword ptr [rdi], 100
این دستور نشون میده تابع از آدرسی که داخل
مرحله سوم:
تابعهای دیگه رو بررسی کنید
ببینید تابع های دیگه همون آدرس یا Pointer مرتبطی رو دریافت میکنن و بعدا از اون حافظه مقداری میخونن یا نه
فقط دنبال اسم یکسان متغیرها یا رجیسترها نباشید ممکنه آدرس از طریق رجیستر دیگهای منتقل بشه داخل ساختار ذخیره بشه یا با چند مرحله محاسبه به دست بیاد
مرحله چهارم:
ارتباط بین تابع ها رو دنبال کنید
فقط به اسم تابع یا مقدار برگشتی اون توجه نکنیدممکنه یه تابع حافظه رو تغییر بده و هیچ مقداری برنگردونه ولی اثر تغییرش چند مرحله بعد داخل تابع دیگهای دیده بشه
برای همین باید مسیر انتقال Pointer ها و خوندن و نوشتن حافظه رو دنبال کنید
یه نکته مهم درباره Must Alias و May Alias
همیشه نمیتونیم از روی شباهت آرگومان ها مطمئن بشیم که دو Pointer به یه آدرس اشاره میکنن
اینجا دو مفهوم مهم داریم
یعنی طبق اطلاعات و شرایطی که بررسی کردیم دو Pointer حتما به یه محل حافظه اشاره میکنن
یعنی ممکنه دو Pointer به یه محل حافظه اشاره کنن ولی اطلاعات موجود برای اثبات قطعی این موضوع کافی نیست
این تفاوت توی تحلیل برنامه اهمیت زیادی داره اگه دو Pointer رو اشتباهی یکی فرض کنیم ممکنه ترتیب تغییرات حافظه و در نتیجه مسیر واقعی اجرای برنامه رو اشتباه تحلیل کنیم
تمرین:
کد زیر رو بررسی کنید
C
void change(int *p)
{
*p = 50;
}
void read_value(int *q)
{
int x = *q;
}
int main()
{
int a = 10;
int b = 20;
تا اینجا دیدیم که چطور ممکنه دو تا Pointer به یه آدرس از حافظه اشاره کنن حالا میخوایم یه حالت مهمتر رو بررسی کنیم
فرض کنید یه تابع داریم که یه مقدار رو توی حافظه تغییر میده و یه تابع دیگه همون مقدار رو از حافظه میخونه
نکته اینجاست که ممکنه این دو تا تابع از Pointer های متفاوتی استفاده کنن ولی هر دو به یه آدرس مشترک دسترسی داشته باشن
این موضوع توی مهندسی معکوس خیلی مهمه، چون ممکنه با بررسی هر تابع بهتنهایی متوجه ارتباط بین اونها نشیم
یه مثال ساده با
C
void set_value(int *p)
{
*p = 100;
}
int get_value(int *q)
{
return *q;
}
int main()
{
int value = 10;
set_value(&value);
int result = get_value(&value);
return result;
}
اینجا چه اتفاقی میفته؟
اول مقدار
value برابر 10 هستبعد آدرس
value به تابع set_value داده میشهتابع
set_value مقدار موجود در اون آدرس رو به 100 تغییر میدهبعد همون آدرس به تابع
get_value داده میشه و مقدار جدید از حافظه خونده میشهدر نهایت
result برابر 100 میشهنکته اصلی اینه که Pointerهای
p و q اسم و متغیرهای متفاوتی دارن اما در این مثال هر دو به همون خانه حافظه دسترسی دارنپس ارتباط بین این دو تابع از طریق حافظه شکل گرفته، نه از طریق مقدار برگشتی یکی از اونها
حالا این موضوع توی اسمبلی چه شکلیه؟
فرض کنید تابع
set_value به این شکل کامپایل شدهset_value:
mov dword ptr [rdi], 100
ret
این مثال بر اساس قرارداد فراخوانی System V AMD64 نوشته شده که معمولا توی برنامههای 64 بیتی لینوکس استفاده میشه
طبق این قرار داد آرگومان اول تابع داخل رجیستر
RDI قرار میگیرهحالا این دستور رو بررسی کنیم
mov dword ptr [rdi], 100
این دستور عدد 100 رو در آدرسی مینویسه که داخل
RDI قرار دارهدقت کنید که مقدار
RDI با مقداری که داخل حافظه ذخیره شده فرق دارهRDIآدرس رو نگه میداره و عبارت
[rdi] به داده ای اشاره میکنه که در اون آدرس قرار دارهحالا تابع
get_value رو ببینیدget_value:
mov eax, dword ptr [rdi]
ret
اینجا مقدار چهار بایتی موجود در آدرس
RDI خونده میشه و داخل EAX قرار میگیره از اونجایی که مقدار برگشتی تابع از نوع int هست این مقدار در رجیستر EAX برگردونده میشه پس یه تابع توی حافظه مینویسه و تابع دیگه از همون حافظه میخونه البته به شرطی که آدرسی که به هر دو تابع داده شده به همون محل حافظه اشاره کنهحالا توی مهندسی معکوس چطور پیداش کنیم؟
وقتی داری یه فایل باینری رو توی Ghidra یا Disassembler بررسی میکنید این مراحل رو دنبال کنید
مرحله اول:
آرگومانهای تابع رو بررسی کنید
ببینید تابع چه Pointer هایی دریافت میکنه و از اونها برای خوندن یا نوشتن حافظه استفاده میکنه یا نه
مرحله دوم:
دنبال دستورهای نوشتن حافظه بگردید
مثلا دستور زیر رو در نظر بگیرید
mov dword ptr [rdi], 100
این دستور نشون میده تابع از آدرسی که داخل
RDI قرار داره برای نوشتن داده استفاده میکنهمرحله سوم:
تابعهای دیگه رو بررسی کنید
ببینید تابع های دیگه همون آدرس یا Pointer مرتبطی رو دریافت میکنن و بعدا از اون حافظه مقداری میخونن یا نه
فقط دنبال اسم یکسان متغیرها یا رجیسترها نباشید ممکنه آدرس از طریق رجیستر دیگهای منتقل بشه داخل ساختار ذخیره بشه یا با چند مرحله محاسبه به دست بیاد
مرحله چهارم:
ارتباط بین تابع ها رو دنبال کنید
فقط به اسم تابع یا مقدار برگشتی اون توجه نکنیدممکنه یه تابع حافظه رو تغییر بده و هیچ مقداری برنگردونه ولی اثر تغییرش چند مرحله بعد داخل تابع دیگهای دیده بشه
برای همین باید مسیر انتقال Pointer ها و خوندن و نوشتن حافظه رو دنبال کنید
یه نکته مهم درباره Must Alias و May Alias
همیشه نمیتونیم از روی شباهت آرگومان ها مطمئن بشیم که دو Pointer به یه آدرس اشاره میکنن
اینجا دو مفهوم مهم داریم
Must Aliasیعنی طبق اطلاعات و شرایطی که بررسی کردیم دو Pointer حتما به یه محل حافظه اشاره میکنن
May Aliasیعنی ممکنه دو Pointer به یه محل حافظه اشاره کنن ولی اطلاعات موجود برای اثبات قطعی این موضوع کافی نیست
این تفاوت توی تحلیل برنامه اهمیت زیادی داره اگه دو Pointer رو اشتباهی یکی فرض کنیم ممکنه ترتیب تغییرات حافظه و در نتیجه مسیر واقعی اجرای برنامه رو اشتباه تحلیل کنیم
تمرین:
کد زیر رو بررسی کنید
C
void change(int *p)
{
*p = 50;
}
void read_value(int *q)
{
int x = *q;
}
int main()
{
int a = 10;
int b = 20;
C
change(&a);
read_value(&b);
read_value(&a);
}
مشخص کنید کدوم فراخوانی مقدار تغییر کرده توسط
به این نکته هم توجه کن که تابع
نکتهای که باید از این قسمت یادتون بمونه
برای تحلیل جریان داده همیشه لازم نیست دو تابع مستقیما با هم ارتباط داشته باشن
بعضی وقتا یه تابع مقداری رو داخل حافظه تغییر میده و تابع دیگهای که هیچ ارتباط مستقیمی با اون نداره از طریق یه Pointer به همون محل دسترسی پیدا میکنه
در مهندسی معکوس دنبال کردن همین ارتباط های غیرمستقیم میتونه رفتار واقعی برنامه رو مشخص کنه
@reverseengine
change(&a);
read_value(&b);
read_value(&a);
}
مشخص کنید کدوم فراخوانی مقدار تغییر کرده توسط
change رو میخونه و چرابه این نکته هم توجه کن که تابع
read_value مقدار رو میخونه اما اون رو برنمیگردونه و در جای دیگهای هم ذخیره نمیکنهنکتهای که باید از این قسمت یادتون بمونه
برای تحلیل جریان داده همیشه لازم نیست دو تابع مستقیما با هم ارتباط داشته باشن
بعضی وقتا یه تابع مقداری رو داخل حافظه تغییر میده و تابع دیگهای که هیچ ارتباط مستقیمی با اون نداره از طریق یه Pointer به همون محل دسترسی پیدا میکنه
در مهندسی معکوس دنبال کردن همین ارتباط های غیرمستقیم میتونه رفتار واقعی برنامه رو مشخص کنه
@reverseengine
ReverseEngineering
Memory Aliasing بین توابع تا اینجا دیدیم که چطور ممکنه دو تا Pointer به یه آدرس از حافظه اشاره کنن حالا میخوایم یه حالت مهمتر رو بررسی کنیم فرض کنید یه تابع داریم که یه مقدار رو توی حافظه تغییر میده و یه تابع دیگه همون مقدار رو از حافظه میخونه نکته اینجاست…
Memory Aliasing Between Functions
So far we have seen how two pointers can point to the same memory address. Now we want to examine a more important case.
Suppose we have a function that modifies a value in memory and another function reads the same value from memory.
The point here is that these two functions may use different pointers, but both access the same address.
This is very important in reverse engineering, because we may not notice the relationship between them by examining each function alone.
A simple example with
C
void set_value(int *p)
{
*p = 100;
}
int get_value(int *q)
{
return *q;
}
int main()
{
int value = 10;
set_value(&value);
int result = get_value(&value);
return result;
}
What is happening here?
First, the value is 10
Then the address of value is given to the set_value function
The set_value function changes the value at that address to 100
Then the same address is given to the get_value function and the new value is read from memory
Finally, the result is 100
The main point is that the pointers p and q have different names and variables, but in this example they both access the same memory location
So the connection between these two functions is formed through memory, not through the return value of one of them
Now, how does this look in assembly?
Suppose the set_value function is compiled like this
set_value:
mov dword ptr [rdi], 100
ret
This example is written according to the System V AMD64 calling convention, which is commonly used in 64-bit Linux programs
According to this convention, the first argument of the function is placed in the RDI register
Now let's examine this instruction
mov dword ptr [rdi], 100
This instruction writes the number 100 to the address located in RDI
Note that the value of RDI is different from the value stored in memory
RDI
holds the address and the expression [rdi] refers to the data located at that address
Now look at the get_value function
get_value:
mov eax, dword ptr [rdi]
ret
Here the four-byte value located at the address RDI is read and placed in EAX. Since the return value of the function is of type int, this value is stored in the register EAX is returned, so one function writes to memory and another function reads from the same memory, provided that the address given to both functions points to the same memory location.
Now how do we find it in reverse engineering?
When you are examining a binary file in Ghidra or Disassembler, follow these steps
Step 1:
Examine the function arguments
See what pointers the function receives and whether it uses them to read or write memory
Step 2:
Look for memory write instructions
For example, consider the following command
mov dword ptr [rdi], 100
This command shows that the function uses the address in RDI to write data
Step 3:
Examine other functions
See if other functions receive the same address or associated pointer and later read something from that memory
Don't just look for identical variable or register names - the address may be passed through another register, stored in a structure, or obtained with several calculation steps
Step 4:
Look for relationships between functions
Don't just look at the function name or its return value - a function may change memory It does not return any value, but the effect of its change can be seen in another function a few steps later.
Therefore, you must follow the path of transferring pointers and reading and writing memory. An important point about Must Alias and May Alias.
We cannot always be sure that two pointers point to the same address based on the similarity of the arguments. Here we have two important concepts.
Must Alias means that according to the information and conditions we have examined, two pointers must point to the same memory location.
May Alias means that two pointers may point to the same memory location, but the available information is not enough to prove this definitively. This difference is very important in program analysis.
So far we have seen how two pointers can point to the same memory address. Now we want to examine a more important case.
Suppose we have a function that modifies a value in memory and another function reads the same value from memory.
The point here is that these two functions may use different pointers, but both access the same address.
This is very important in reverse engineering, because we may not notice the relationship between them by examining each function alone.
A simple example with
C
void set_value(int *p)
{
*p = 100;
}
int get_value(int *q)
{
return *q;
}
int main()
{
int value = 10;
set_value(&value);
int result = get_value(&value);
return result;
}
What is happening here?
First, the value is 10
Then the address of value is given to the set_value function
The set_value function changes the value at that address to 100
Then the same address is given to the get_value function and the new value is read from memory
Finally, the result is 100
The main point is that the pointers p and q have different names and variables, but in this example they both access the same memory location
So the connection between these two functions is formed through memory, not through the return value of one of them
Now, how does this look in assembly?
Suppose the set_value function is compiled like this
set_value:
mov dword ptr [rdi], 100
ret
This example is written according to the System V AMD64 calling convention, which is commonly used in 64-bit Linux programs
According to this convention, the first argument of the function is placed in the RDI register
Now let's examine this instruction
mov dword ptr [rdi], 100
This instruction writes the number 100 to the address located in RDI
Note that the value of RDI is different from the value stored in memory
RDI
holds the address and the expression [rdi] refers to the data located at that address
Now look at the get_value function
get_value:
mov eax, dword ptr [rdi]
ret
Here the four-byte value located at the address RDI is read and placed in EAX. Since the return value of the function is of type int, this value is stored in the register EAX is returned, so one function writes to memory and another function reads from the same memory, provided that the address given to both functions points to the same memory location.
Now how do we find it in reverse engineering?
When you are examining a binary file in Ghidra or Disassembler, follow these steps
Step 1:
Examine the function arguments
See what pointers the function receives and whether it uses them to read or write memory
Step 2:
Look for memory write instructions
For example, consider the following command
mov dword ptr [rdi], 100
This command shows that the function uses the address in RDI to write data
Step 3:
Examine other functions
See if other functions receive the same address or associated pointer and later read something from that memory
Don't just look for identical variable or register names - the address may be passed through another register, stored in a structure, or obtained with several calculation steps
Step 4:
Look for relationships between functions
Don't just look at the function name or its return value - a function may change memory It does not return any value, but the effect of its change can be seen in another function a few steps later.
Therefore, you must follow the path of transferring pointers and reading and writing memory. An important point about Must Alias and May Alias.
We cannot always be sure that two pointers point to the same address based on the similarity of the arguments. Here we have two important concepts.
Must Alias means that according to the information and conditions we have examined, two pointers must point to the same memory location.
May Alias means that two pointers may point to the same memory location, but the available information is not enough to prove this definitively. This difference is very important in program analysis.
ReverseEngineering
Memory Aliasing بین توابع تا اینجا دیدیم که چطور ممکنه دو تا Pointer به یه آدرس از حافظه اشاره کنن حالا میخوایم یه حالت مهمتر رو بررسی کنیم فرض کنید یه تابع داریم که یه مقدار رو توی حافظه تغییر میده و یه تابع دیگه همون مقدار رو از حافظه میخونه نکته اینجاست…
If we mistakenly assume that two pointers are the same, we may analyze the order of memory changes and, as a result, the actual path of program execution incorrectly. Exercise: Examine the following code.
C
void change(int *p) { *p = 50
; }
void read_value(int *q)
{ int x = *q;
int main() int a = 10;
int b = 20;
C
void change(int *p) { *p = 50
; }
void read_value(int *q)
{ int x = *q;
int main() int a = 10;
int b = 20;
C
change(&a);
read_value(&b);
read_value(&a);
}
Determine which function call reads the value modified by
change and why.
Note that the read_value
function reads the value but does not return it, nor does it store it elsewhere.
A key takeaway from this section:
When analyzing data flow, it is not always necessary for two functions to interact directly.
Sometimes, one function modifies a value in memory, and another function—with no direct connection to the first—accesses that same location via a pointer.
In reverse engineering, tracing these very indirect connections can reveal the program's actual behavior.
@reverseengine
change(&a);
read_value(&b);
read_value(&a);
}
Determine which function call reads the value modified by
change and why.
Note that the read_value
function reads the value but does not return it, nor does it store it elsewhere.
A key takeaway from this section:
When analyzing data flow, it is not always necessary for two functions to interact directly.
Sometimes, one function modifies a value in memory, and another function—with no direct connection to the first—accesses that same location via a pointer.
In reverse engineering, tracing these very indirect connections can reveal the program's actual behavior.
@reverseengine
Turnaround Time
از زمانی که یک Job وارد سیستم میشه تا زمانی که کاملا تموم بشه چقدر زمان گذشته؟
فرمول:
Turnaround Time = Completion Time - Arrival Time
مثلا:
Arrival Time = 0
Completion = 10
پس:
Turnaround Time = 10
یعنی Process از اول تا اخر 10 واحد زمانی داخل سیستم بوده
@reverseengine
از زمانی که یک Job وارد سیستم میشه تا زمانی که کاملا تموم بشه چقدر زمان گذشته؟
فرمول:
Turnaround Time = Completion Time - Arrival Time
مثلا:
Arrival Time = 0
Completion = 10
پس:
Turnaround Time = 10
یعنی Process از اول تا اخر 10 واحد زمانی داخل سیستم بوده
@reverseengine
ReverseEngineering
Turnaround Time از زمانی که یک Job وارد سیستم میشه تا زمانی که کاملا تموم بشه چقدر زمان گذشته؟ فرمول: Turnaround Time = Completion Time - Arrival Time مثلا: Arrival Time = 0 Completion = 10 پس: Turnaround Time = 10 یعنی Process از اول تا اخر 10…
Turnaround Time
How long does it take for a job to complete from the time it is entered into the system until it is completely finished?
Formula:
Turnaround Time = Completion Time - Arrival Time
For example:
Arrival Time = 0
Completion = 10
So:
Turnaround Time = 10
This means that the process has been in the system for 10 time units from start to finish
@reverseengine
How long does it take for a job to complete from the time it is entered into the system until it is completely finished?
Formula:
Turnaround Time = Completion Time - Arrival Time
For example:
Arrival Time = 0
Completion = 10
So:
Turnaround Time = 10
This means that the process has been in the system for 10 time units from start to finish
@reverseengine
Waiting Time
Process
چه مقدار از زمان رو فقط منتظر CPU بوده؟
این میشه:
Waiting Time
مثلا Process وارد سیستم میشه:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
تمام زمانی که Process در صف آماده اجرا منتظر CPU بوده در Waiting Time حساب میشه
Waiting Time =
زمان انتظار برای CPU
@reverseengine
Process
چه مقدار از زمان رو فقط منتظر CPU بوده؟
این میشه:
Waiting Time
مثلا Process وارد سیستم میشه:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
تمام زمانی که Process در صف آماده اجرا منتظر CPU بوده در Waiting Time حساب میشه
Waiting Time =
زمان انتظار برای CPU
@reverseengine
ReverseEngineering
Waiting Time Process چه مقدار از زمان رو فقط منتظر CPU بوده؟ این میشه: Waiting Time مثلا Process وارد سیستم میشه: Arrival ↓ Waiting ↓ Running ↓ Waiting ↓ Running ↓ Finished تمام زمانی که Process در صف آماده اجرا منتظر CPU بوده در Waiting Time…
Waiting Time
Process How much time did it spend just waiting for the CPU?
This is:
Waiting Time
For example, a process enters the system:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
The entire time the process spent waiting for the CPU in the ready queue counts as Waiting Time.
Waiting Time = Time spent waiting for the CPU
@reverseengine
Process How much time did it spend just waiting for the CPU?
This is:
Waiting Time
For example, a process enters the system:
Arrival
↓
Waiting
↓
Running
↓
Waiting
↓
Running
↓
Finished
The entire time the process spent waiting for the CPU in the ready queue counts as Waiting Time.
Waiting Time = Time spent waiting for the CPU
@reverseengine
PE
در حافظه و تفاوتش با فایل روی دیسک
تا اینجا درباره ی
حالا میخوایم ببینیم وقتی یک فایل اجرایی ویندوز وارد حافظه میشه چه اتفاقی براش میوفته و چرا نسخهی داخل Memory معمولا با نسخهی روی دیسک یکی نیست
اول ببینیم PE چیه
فرمت اجرایی اصلی بسیاری از برنامههای ویندوز PE یا Portable Executable هست
فایلهایی مثل اینها رو در نظر بگیرید:
program.exe
library.dll
ساختار کلی یک فایل PE تقریبا به این شکله:
PE File
│
├── DOS Header
├── PE Signature
├── COFF File Header
├── Optional Header
├── Section Headers
│
├── .text
├── .rdata
├── .data
├── .pdata
└── Other Sections
همه ی فایلها لزوما تمام این Section ها رو ندارن و ترتیب و جزئیاتشون هم ممکنه متفاوت باشه
هر بخش اطلاعات مشخصی داره و Windows از ساختار PE برای شناخت و بارگذاری فایل اجرایی استفاده میکنه
وقتی PE وارد Memory میشه چه اتفاقی میوفته؟
فرض کنید این فایل روی دیسک وجود داره:
C:\Program Files\App\program.exe
وقتی Windows اون رو اجرا میکنه Loader سیستم عامل فرایند اپلود رو انجام میده
نمای ساده :
PE File on Disk
|
v
Windows Loader
|
v
Virtual Address Space
|
v
Program Execution
اما نکته اینجاست که Windows معمولا فایل رو به شکل یک تیکه ی خام و بدون تغییر داخل Memory کپی نمیکنه
Loader باید ساختارهای فایل رو پردازش کنه و بخشهای مختلف رو در جای مناسب قرار بده
تفاوت Raw Offset و RVA
اینجا به یکی از مفاهیم پایهای میرسیم
Raw Offset
آدرسیه که برای پیدا کردن اطلاعات داخل خود فایل روی دیسک استفاده میشه
مثلا:
File Offset = 0x600
یعنی اطلاعات موردنظر از Offset برابر با
RVA
مخفف این عبارته:
RVA = Relative Virtual Address
یعنی آدرس یک بخش نسبت به
مثلا:
ImageBase = 0x140000000
RVA = 0x2000
در این مثال:
VA = ImageBase + RVA
پس:
VA = 0x140002000
این محاسبه آدرس مجازی موردنظر رو به دست میاره به شرطی که از
واقعی Image بارگذاری شده استفاده کنیم
تفاوت اصلی
Raw Offset
|
+--> موقعیت در فایل روی دیسک
RVA
|
+--> موقعیت نسبت به
ImageBase
در فضای آدرس مجازی
Raw Offset و RVA
لزوما برابر نیستن
برای تبدیل بین اونها باید اطلاعات Section مربوطه مثل
رو هم در نظر بگیریم
ضمن اینکه همه ی آدرس های مجازی
لزوما معادل یک بخش از دادههای موجود در فایل نیستن برای مثال ممکنه یک بخش در حافظه بزرگتر از دادهی خام متناظر با خودش باشه
ImageBase
هر Image در حالت اپلود شده یک Base Address داره که بهش
برای مثال:
Preferred ImageBase:
0x140000000
این آدرس محل ترجیحی تعریف شده برای بارگذاری Image در فایل PE هست
ولی Windows همیشه مجبور نیست Image رو دقیقا همونجا قرار بده
چرا؟
چون ممکنه اون محدوده از فضای آدرس مجازی در دسترس نباشه یا شرایط دیگهای وجود داشته باشه
در این شرایط Image ممکنه در آدرس دیگری بارگذاری بشه
اینجا میریم سراغ مفهوم بعدی
Base Relocation
فرض کنید برنامه انتظار داره Image در این آدرس قرار بگیره:
Preferred Base = 0x140000000
اما در عمل در آدرس دیگه ای اپلود میشه:
Actual Base = 0x180000000
اختلاف این دو آدرس:
Delta = Actual Base - Preferred Base
در صورتی که Image به Relocation نیاز داشته باشه و اطلاعات لازم موجود باشه Loader از اطلاعات Relocation برای اصلاح آدرسهای وابسته به محل بارگذاری استفاده میکنه
به این فرایند Base Relocation میگیم
نکتهی مهم اینه که تمام آدرس های داخل فایل نیازمند Relocation نیستن فقط مواردی که طبق ساختار PE به محل بارگذاری وابسته هستن و اطلاعات Relocation مناسب براشون وجود داره باید درست بشن
Import Table
چه کاری انجام میده؟
برنامهها معمولا از توابع کتابخانههای دیگه استفاده میکنن
برای مثال یک برنامه ممکنه به توابعی از این Moduleها نیاز داشته باشه:
KERNEL32.dll
USER32.dll
ADVAPI32.dll
اطلاعات مربوط به وابستگیهای خارجی در ساختارهای Import فایل PE قرار میگیره
Loader
این وابستگی ها رو پردازش میکنه و آدرس توابع موردنیاز رو در ساختار های مرتبط قرار میده
در حافظه و تفاوتش با فایل روی دیسک
تا اینجا درباره ی
Private Memory Image-backed Memory و VAD صحبت کردیمحالا میخوایم ببینیم وقتی یک فایل اجرایی ویندوز وارد حافظه میشه چه اتفاقی براش میوفته و چرا نسخهی داخل Memory معمولا با نسخهی روی دیسک یکی نیست
اول ببینیم PE چیه
فرمت اجرایی اصلی بسیاری از برنامههای ویندوز PE یا Portable Executable هست
فایلهایی مثل اینها رو در نظر بگیرید:
program.exe
library.dll
ساختار کلی یک فایل PE تقریبا به این شکله:
PE File
│
├── DOS Header
├── PE Signature
├── COFF File Header
├── Optional Header
├── Section Headers
│
├── .text
├── .rdata
├── .data
├── .pdata
└── Other Sections
همه ی فایلها لزوما تمام این Section ها رو ندارن و ترتیب و جزئیاتشون هم ممکنه متفاوت باشه
هر بخش اطلاعات مشخصی داره و Windows از ساختار PE برای شناخت و بارگذاری فایل اجرایی استفاده میکنه
وقتی PE وارد Memory میشه چه اتفاقی میوفته؟
فرض کنید این فایل روی دیسک وجود داره:
C:\Program Files\App\program.exe
وقتی Windows اون رو اجرا میکنه Loader سیستم عامل فرایند اپلود رو انجام میده
نمای ساده :
PE File on Disk
|
v
Windows Loader
|
v
Virtual Address Space
|
v
Program Execution
اما نکته اینجاست که Windows معمولا فایل رو به شکل یک تیکه ی خام و بدون تغییر داخل Memory کپی نمیکنه
Loader باید ساختارهای فایل رو پردازش کنه و بخشهای مختلف رو در جای مناسب قرار بده
تفاوت Raw Offset و RVA
اینجا به یکی از مفاهیم پایهای میرسیم
Raw Offset
آدرسیه که برای پیدا کردن اطلاعات داخل خود فایل روی دیسک استفاده میشه
مثلا:
File Offset = 0x600
یعنی اطلاعات موردنظر از Offset برابر با
0x600 در فایل شروع میشهRVA
مخفف این عبارته:
RVA = Relative Virtual Address
یعنی آدرس یک بخش نسبت به
ImageBase در فضای آدرس مجازیمثلا:
ImageBase = 0x140000000
RVA = 0x2000
در این مثال:
VA = ImageBase + RVA
پس:
VA = 0x140002000
این محاسبه آدرس مجازی موردنظر رو به دست میاره به شرطی که از
ImageBaseواقعی Image بارگذاری شده استفاده کنیم
تفاوت اصلی
Raw Offset
|
+--> موقعیت در فایل روی دیسک
RVA
|
+--> موقعیت نسبت به
ImageBase
در فضای آدرس مجازی
Raw Offset و RVA
لزوما برابر نیستن
برای تبدیل بین اونها باید اطلاعات Section مربوطه مثل
PointerToRawData و VirtualAddressرو هم در نظر بگیریم
ضمن اینکه همه ی آدرس های مجازی
لزوما معادل یک بخش از دادههای موجود در فایل نیستن برای مثال ممکنه یک بخش در حافظه بزرگتر از دادهی خام متناظر با خودش باشه
ImageBase
هر Image در حالت اپلود شده یک Base Address داره که بهش
ImageBase میگیمبرای مثال:
Preferred ImageBase:
0x140000000
این آدرس محل ترجیحی تعریف شده برای بارگذاری Image در فایل PE هست
ولی Windows همیشه مجبور نیست Image رو دقیقا همونجا قرار بده
چرا؟
چون ممکنه اون محدوده از فضای آدرس مجازی در دسترس نباشه یا شرایط دیگهای وجود داشته باشه
در این شرایط Image ممکنه در آدرس دیگری بارگذاری بشه
اینجا میریم سراغ مفهوم بعدی
Base Relocation
فرض کنید برنامه انتظار داره Image در این آدرس قرار بگیره:
Preferred Base = 0x140000000
اما در عمل در آدرس دیگه ای اپلود میشه:
Actual Base = 0x180000000
اختلاف این دو آدرس:
Delta = Actual Base - Preferred Base
در صورتی که Image به Relocation نیاز داشته باشه و اطلاعات لازم موجود باشه Loader از اطلاعات Relocation برای اصلاح آدرسهای وابسته به محل بارگذاری استفاده میکنه
به این فرایند Base Relocation میگیم
نکتهی مهم اینه که تمام آدرس های داخل فایل نیازمند Relocation نیستن فقط مواردی که طبق ساختار PE به محل بارگذاری وابسته هستن و اطلاعات Relocation مناسب براشون وجود داره باید درست بشن
Import Table
چه کاری انجام میده؟
برنامهها معمولا از توابع کتابخانههای دیگه استفاده میکنن
برای مثال یک برنامه ممکنه به توابعی از این Moduleها نیاز داشته باشه:
KERNEL32.dll
USER32.dll
ADVAPI32.dll
اطلاعات مربوط به وابستگیهای خارجی در ساختارهای Import فایل PE قرار میگیره
Loader
این وابستگی ها رو پردازش میکنه و آدرس توابع موردنیاز رو در ساختار های مرتبط قرار میده
❤1
نمای ساده :
Program.exe
|
v
Import Table
|
v
Required DLLs
|
v
Resolve Functions
|
v
Import Address Table (IAT)
جدول
به همین دلیل محتوای بعضی قسمت های PE در Memory میتونه با مقادیر اولیه ی متناظر در فایل روی دیسک متفاوت باشه
پس چرا PE روی دیسک و Memory ممکنه فرق داشته باشن؟
چند دلیل مهم وجود داره:
Relocation:
بعضی آدرسهای وابسته به محل بارگذاری اصلاح میشن
Import Resolution:
آدرس توابع Importشده در فرایند بارگذاری مشخص میشه
Memory Layout:
ساختار Section ها در حافظه لزوما مثل چیدمان خام فایل نیست
Runtime Changes:
خود برنامه ممکنه هنگام اجرا بعضی داده های حافظه رو تغییر بده
Zero Initialization:
بعضی قسمتها ممکنه در حافظه فضای اختصاص یافته داشته باشن ولی دادهی متناظری در فایل نداشته باشن و با صفر مقداردهی بشن
پس اگر یک تحلیلگر بخواد فایل روی دیسک رو با Memory مقایسه کنه نباید انتظار داشته باشه تمام Byte ها دقیقا یکی باشن
این موضوع در Memory Forensics چه کاربردی داره؟
فرض کنی در حال بررسی یک Memory Dump هستید
یک بخش پیدا میکنید که محتوای اون شبیه ساختار یک PE به نظر میرسه
چند سوال از خودتون میپرسید:
Header معتبره؟
Section
ها قابل شناسایی هستن؟
آدرسهای موجود با ساختار Image سازگارن؟
Memory
به یک Module شناخت هشده مربوطه؟
تفاوت های فایل و Memory قابل توضیح هستن؟
نکتهی مهم اینه که پیدا کردن امضای
برای تحلیل درست باید مجموعه ای از شواهد رو بررسی کرد
همچنین یک PE ممکنه به دلیل نحوه ی اپلود تغییرات زمان اجرا یا حتی ناقص بودن دادههای Memory Dump بهسادگی قابل بازیابی نباشه
ارتباط این با EDR
EDR
میتونه از اطلاعات مربوط به Module ها حافظه و رفتار Process برای بررسی فعالیت های مشکوک استفاده کنه
برای مثال ممکنه این اطلاعات رو کنار هم قرار بدیم:
Process Information
+
Loaded Modules
+
Memory Regions
+
Thread Activity
+
Call Stack
+
Execution Timeline
اگر اطلاعات Memory با وضعیت مورد انتظار یک برنامه سازگار نباشه ممکنه نیاز به بررسی بیشتری وجود داشته باشه
ولی باید دقت کنیم که:
تفاوت Memory با فایل روی دیسک بهتنهایی نشونهی قطعی دستکاری مخرب نیست
بعضی تفاوت ها نتیجهی طبیعی اپلود و اجرای برنامه هستن
تا اینجا با چند مفهوم مهم آشنا شدیم:
این مفاهیم برای ما داخل بحث تحلیل حافظه و بررسی رفتار برنامه ها خیلی خیلی مهمن
@reverseengine
Program.exe
|
v
Import Table
|
v
Required DLLs
|
v
Resolve Functions
|
v
Import Address Table (IAT)
جدول
IAT شامل آدرس هایی میشه که برای دسترسی به توابع Import شده استفاده میشنبه همین دلیل محتوای بعضی قسمت های PE در Memory میتونه با مقادیر اولیه ی متناظر در فایل روی دیسک متفاوت باشه
پس چرا PE روی دیسک و Memory ممکنه فرق داشته باشن؟
چند دلیل مهم وجود داره:
Relocation:
بعضی آدرسهای وابسته به محل بارگذاری اصلاح میشن
Import Resolution:
آدرس توابع Importشده در فرایند بارگذاری مشخص میشه
Memory Layout:
ساختار Section ها در حافظه لزوما مثل چیدمان خام فایل نیست
Runtime Changes:
خود برنامه ممکنه هنگام اجرا بعضی داده های حافظه رو تغییر بده
Zero Initialization:
بعضی قسمتها ممکنه در حافظه فضای اختصاص یافته داشته باشن ولی دادهی متناظری در فایل نداشته باشن و با صفر مقداردهی بشن
پس اگر یک تحلیلگر بخواد فایل روی دیسک رو با Memory مقایسه کنه نباید انتظار داشته باشه تمام Byte ها دقیقا یکی باشن
این موضوع در Memory Forensics چه کاربردی داره؟
فرض کنی در حال بررسی یک Memory Dump هستید
یک بخش پیدا میکنید که محتوای اون شبیه ساختار یک PE به نظر میرسه
چند سوال از خودتون میپرسید:
Header معتبره؟
Section
ها قابل شناسایی هستن؟
آدرسهای موجود با ساختار Image سازگارن؟
Memory
به یک Module شناخت هشده مربوطه؟
تفاوت های فایل و Memory قابل توضیح هستن؟
نکتهی مهم اینه که پیدا کردن امضای
MZ بهتنهایی اثبات نمیکنه که یک PE معتبر یا قابل اجرا در اون ناحیه وجود دارهبرای تحلیل درست باید مجموعه ای از شواهد رو بررسی کرد
همچنین یک PE ممکنه به دلیل نحوه ی اپلود تغییرات زمان اجرا یا حتی ناقص بودن دادههای Memory Dump بهسادگی قابل بازیابی نباشه
ارتباط این با EDR
EDR
میتونه از اطلاعات مربوط به Module ها حافظه و رفتار Process برای بررسی فعالیت های مشکوک استفاده کنه
برای مثال ممکنه این اطلاعات رو کنار هم قرار بدیم:
Process Information
+
Loaded Modules
+
Memory Regions
+
Thread Activity
+
Call Stack
+
Execution Timeline
اگر اطلاعات Memory با وضعیت مورد انتظار یک برنامه سازگار نباشه ممکنه نیاز به بررسی بیشتری وجود داشته باشه
ولی باید دقت کنیم که:
تفاوت Memory با فایل روی دیسک بهتنهایی نشونهی قطعی دستکاری مخرب نیست
بعضی تفاوت ها نتیجهی طبیعی اپلود و اجرای برنامه هستن
تا اینجا با چند مفهوم مهم آشنا شدیم:
این مفاهیم برای ما داخل بحث تحلیل حافظه و بررسی رفتار برنامه ها خیلی خیلی مهمن
@reverseengine
ReverseEngineering
PE در حافظه و تفاوتش با فایل روی دیسک تا اینجا درباره ی Private Memory Image-backed Memory و VAD صحبت کردیم حالا میخوایم ببینیم وقتی یک فایل اجرایی ویندوز وارد حافظه میشه چه اتفاقی براش میوفته و چرا نسخهی داخل Memory معمولا با نسخهی روی دیسک یکی نیست…
PE
In memory and its difference from a file on disk
So far we have talked about Private Memory Image-backed Memory and VAD
Now we want to see what happens to a Windows executable file when it enters memory and why the version in memory is usually not the same as the version on disk
First, let's see what PE is
The main executable format of many Windows programs is PE or Portable Executable
Consider files like these:
program.exe
library.dll
The general structure of a PE file is approximately as follows:
PE File
│
├── DOS Header
├── PE Signature
├── COFF File Header
├── Optional Header
├── Section Headers
│
├── .text
├── .rdata
├── .data
├── .pdata
└── Other Sections
Not all files necessarily have all of these sections, and their order and details may also vary
Each section has specific information, and Windows uses the PE structure to recognize and load the executable file. Uses
What happens when PE enters Memory?
Suppose this file is on disk:
C:\Program Files\App\program.exe
When Windows runs it, the operating system Loader performs the upload process
Simple view:
PE File on Disk
|
v
Windows Loader
|
v
Virtual Address Space
|
v
Program Execution
But the point is that Windows usually does not copy the file into memory as a raw, unmodified piece
The Loader must process the file structures and place the different parts in the appropriate places
The difference between Raw Offset and RVA
Here we come to one of the basic concepts
Raw Offset
Is the address used to find the information inside the file itself on the disk
For example:
File Offset = 0x600
That is, the desired information starts from Offset equal to 0x600 in the file
RVA
is the abbreviation for this phrase:
RVA = Relative Virtual Address
That is, the address of a section relative to ImageBase in the virtual address space
For example:
ImageBase = 0x140000000
RVA = 0x2000
In this example:
VA = ImageBase + RVA
So:
VA = 0x140002000
This calculation calculates the desired virtual address It is obtained provided that we use the actual
ImageBase
of the loaded Image
The main difference
Raw Offset
|
+--> Location in the file on disk
RVA
|
+--> Location relative to
ImageBase
in the virtual address space
Raw Offset and RVA are not necessarily equal
To convert between them, we must also consider the relevant Section information such as
PointerToRawData and VirtualAddress
In addition, not all virtual addresses
necessarily correspond to a section of the data in the file, for example, a section in memory may be larger than its corresponding raw data
ImageBase
Each Image in the uploaded state has a Base Address, which we call ImageBase
For example:
Preferred ImageBase:
0x140000000
This address is the preferred location defined for loading the Image in the PE file
But Windows does not always have to place the Image exactly there
Why?
Because that range of virtual address space may not be available or there may be other conditions
In this situation, the Image may be loaded to another address
Here we go to the next concept
Base Relocation
Suppose the program expects the Image to be located at this address:
Preferred Base = 0x140000000
But in practice it is uploaded to another address:
Actual Base = 0x180000000
The difference between these two addresses:
Delta = Actual Base - Preferred Base
If the Image needs Relocation and the necessary information is available, the Loader uses the Relocation information to correct the addresses dependent on the loading location
We call this process Base Relocation
The important point is that not all addresses in the file require Relocation, only those that are dependent on the loading location according to the PE structure and have appropriate Relocation information must be corrected
What does Import Table
do?
Programs often use functions from other libraries
For example, a program might need functions from these modules:
KERNEL32.dll
USER32.dll
ADVAPI32.dll
Information about external dependencies is placed in the Import structures of the PE file
In memory and its difference from a file on disk
So far we have talked about Private Memory Image-backed Memory and VAD
Now we want to see what happens to a Windows executable file when it enters memory and why the version in memory is usually not the same as the version on disk
First, let's see what PE is
The main executable format of many Windows programs is PE or Portable Executable
Consider files like these:
program.exe
library.dll
The general structure of a PE file is approximately as follows:
PE File
│
├── DOS Header
├── PE Signature
├── COFF File Header
├── Optional Header
├── Section Headers
│
├── .text
├── .rdata
├── .data
├── .pdata
└── Other Sections
Not all files necessarily have all of these sections, and their order and details may also vary
Each section has specific information, and Windows uses the PE structure to recognize and load the executable file. Uses
What happens when PE enters Memory?
Suppose this file is on disk:
C:\Program Files\App\program.exe
When Windows runs it, the operating system Loader performs the upload process
Simple view:
PE File on Disk
|
v
Windows Loader
|
v
Virtual Address Space
|
v
Program Execution
But the point is that Windows usually does not copy the file into memory as a raw, unmodified piece
The Loader must process the file structures and place the different parts in the appropriate places
The difference between Raw Offset and RVA
Here we come to one of the basic concepts
Raw Offset
Is the address used to find the information inside the file itself on the disk
For example:
File Offset = 0x600
That is, the desired information starts from Offset equal to 0x600 in the file
RVA
is the abbreviation for this phrase:
RVA = Relative Virtual Address
That is, the address of a section relative to ImageBase in the virtual address space
For example:
ImageBase = 0x140000000
RVA = 0x2000
In this example:
VA = ImageBase + RVA
So:
VA = 0x140002000
This calculation calculates the desired virtual address It is obtained provided that we use the actual
ImageBase
of the loaded Image
The main difference
Raw Offset
|
+--> Location in the file on disk
RVA
|
+--> Location relative to
ImageBase
in the virtual address space
Raw Offset and RVA are not necessarily equal
To convert between them, we must also consider the relevant Section information such as
PointerToRawData and VirtualAddress
In addition, not all virtual addresses
necessarily correspond to a section of the data in the file, for example, a section in memory may be larger than its corresponding raw data
ImageBase
Each Image in the uploaded state has a Base Address, which we call ImageBase
For example:
Preferred ImageBase:
0x140000000
This address is the preferred location defined for loading the Image in the PE file
But Windows does not always have to place the Image exactly there
Why?
Because that range of virtual address space may not be available or there may be other conditions
In this situation, the Image may be loaded to another address
Here we go to the next concept
Base Relocation
Suppose the program expects the Image to be located at this address:
Preferred Base = 0x140000000
But in practice it is uploaded to another address:
Actual Base = 0x180000000
The difference between these two addresses:
Delta = Actual Base - Preferred Base
If the Image needs Relocation and the necessary information is available, the Loader uses the Relocation information to correct the addresses dependent on the loading location
We call this process Base Relocation
The important point is that not all addresses in the file require Relocation, only those that are dependent on the loading location according to the PE structure and have appropriate Relocation information must be corrected
What does Import Table
do?
Programs often use functions from other libraries
For example, a program might need functions from these modules:
KERNEL32.dll
USER32.dll
ADVAPI32.dll
Information about external dependencies is placed in the Import structures of the PE file
ReverseEngineering
PE در حافظه و تفاوتش با فایل روی دیسک تا اینجا درباره ی Private Memory Image-backed Memory و VAD صحبت کردیم حالا میخوایم ببینیم وقتی یک فایل اجرایی ویندوز وارد حافظه میشه چه اتفاقی براش میوفته و چرا نسخهی داخل Memory معمولا با نسخهی روی دیسک یکی نیست…
The Loader
processes these dependencies and places the addresses of the required functions in the associated structures
processes these dependencies and places the addresses of the required functions in the associated structures
Simple view:
Program.exe
|
v
Import Table
|
v
Required DLLs
|
v
Resolve Functions
|
v
Import Address Table (IAT)
The IAT table contains the addresses used to access imported functions
That is why the contents of some PE parts in Memory can differ from the corresponding initial values in the file on disk
So why might the PE on disk and the memory be different?
There are several important reasons:
Relocation:
Some addresses are modified depending on the loading location
Import Resolution:
The address of imported functions is determined during the loading process
Memory Layout:
The structure of sections in memory is not necessarily the same as the raw layout of the file
Runtime Changes:
The program itself may change some memory data during execution
Zero Initialization:
Some sections may have allocated space in memory but have no corresponding data in the file and are initialized to zero
So if an analyst wants to compare a file on disk with Memory, he should not expect all the bytes to be exactly the same
How is this useful in Memory Forensics?
Suppose you are examining a Memory Dump
and you find a section whose content looks like the structure of a PE
A few questions to ask yourself:
Is the header valid?
Are the sections recognizable?
Are the addresses consistent with the Image structure?
Memory
Related to a known module?
Can the differences between file and memory be explained?
It is important to note that finding an MZ signature alone does not prove that a valid or executable PE exists in that area. A complete set of evidence must be examined for proper analysis. A PE may also not be easily recoverable due to the way it was uploaded, runtime changes, or even incomplete memory dump data.
How this relates to EDR. EDR can use information about memory modules and process behavior to investigate suspicious activity.
For example, we might put this information together:
Process Information
+ Loaded Modules + Memory Regions + Thread Activity
+ Call Stack + Execution Timeline
If the memory information is inconsistent with the expected state of a program, further investigation may be warranted.
However, it is important to note that: A difference in memory from a file on disk alone is not a definitive indication of malicious manipulation.
Some differences are a natural consequence of program upload and execution.
So far, we have been familiar with some important concepts. We are:
These concepts are very, very important to us in the discussion of memory analysis and program behavior analysis
@reverseengine
Program.exe
|
v
Import Table
|
v
Required DLLs
|
v
Resolve Functions
|
v
Import Address Table (IAT)
The IAT table contains the addresses used to access imported functions
That is why the contents of some PE parts in Memory can differ from the corresponding initial values in the file on disk
So why might the PE on disk and the memory be different?
There are several important reasons:
Relocation:
Some addresses are modified depending on the loading location
Import Resolution:
The address of imported functions is determined during the loading process
Memory Layout:
The structure of sections in memory is not necessarily the same as the raw layout of the file
Runtime Changes:
The program itself may change some memory data during execution
Zero Initialization:
Some sections may have allocated space in memory but have no corresponding data in the file and are initialized to zero
So if an analyst wants to compare a file on disk with Memory, he should not expect all the bytes to be exactly the same
How is this useful in Memory Forensics?
Suppose you are examining a Memory Dump
and you find a section whose content looks like the structure of a PE
A few questions to ask yourself:
Is the header valid?
Are the sections recognizable?
Are the addresses consistent with the Image structure?
Memory
Related to a known module?
Can the differences between file and memory be explained?
It is important to note that finding an MZ signature alone does not prove that a valid or executable PE exists in that area. A complete set of evidence must be examined for proper analysis. A PE may also not be easily recoverable due to the way it was uploaded, runtime changes, or even incomplete memory dump data.
How this relates to EDR. EDR can use information about memory modules and process behavior to investigate suspicious activity.
For example, we might put this information together:
Process Information
+ Loaded Modules + Memory Regions + Thread Activity
+ Call Stack + Execution Timeline
If the memory information is inconsistent with the expected state of a program, further investigation may be warranted.
However, it is important to note that: A difference in memory from a file on disk alone is not a definitive indication of malicious manipulation.
Some differences are a natural consequence of program upload and execution.
So far, we have been familiar with some important concepts. We are:
These concepts are very, very important to us in the discussion of memory analysis and program behavior analysis
@reverseengine
🔥1
Struct Reconstruction
وقتی یه فایل باینری رو مهندسی معکوس میکنی خیلی وقت ها سورس کد اصلی رو ندارید یعنی نمیدونید برنامه نویس چه ساختار هایی تعریف کرده و هر کدوم چه فیلدهایی داشتن
اینجا باید از روی نحوه دسترسی برنامه به حافظه ساختار داده ها رو دوباره بازسازی کنید به این کار میگیم Struct Reconstruction
فرض کنید برنامه اصلی همچین ساختاری داشته باشه
C
struct User {
int id;
int age;
char active;
};
این ساختار سه تا فیلد داره یکی برای شناسه کاربر یکی برای سن و یکی هم برای وضعیت فعال بودن.
ولی وقتی فایل کامپایل شده رو بررسی میکنی ممکنه اصلا اسم
حالا ببین چطور میتونیم ساختار رو از روی همین دستورها تشخیص بدیم
پیدا کردن آدرس فیلد ها
فرض کنید توی اسمبلی به دستورهای زیر رسیدید
Asm
mov eax, dword ptr [rcx]
mov edx, dword ptr [rcx+4]
movzx eax, byte ptr [rcx+8]
بیایم یکی یکی بررسیشون کنیم
Asm
mov eax, dword ptr [rcx]
چهار بایت از آدرسی که داخل RCX قرار داره میخونه
Asm
mov edx, dword ptr [rcx+4]
چهار بایت از چهار بایت بعدی میخونه
Asm
movzx eax, byte ptr [rcx+8]
یک بایت از هشت بایت بعد از آدرس اولیه میخونه
حالا اطلاعاتی که داریم این شکلیه
آدرس پایه + 0 چهار بایت
آدرس پایه + 4 چهار بایت
آدرس پایه + 8 یک بایت
این الگو میتونه نشون بده که برنامه داره به سه فیلد مختلف از یه ساختار دسترسی پیدا میکنه
تشخیص اندازه و نوع فیلد ها
از روی اندازه دسترسی ها میتونیم حدس بزنیم هر فیلد چه نوعی داره
دسترسی چهار بایتی ممکنه مربوط به
دسترسی یک بایتی ممکنه مربوط به
اما حواستون باشه که اندازه به تنهایی نوع دقیق داده رو مشخص نمیکنه مثلا یه مقدار چهار بایتی میتونه بخشی از یه عدد بزرگتر هم باشه
برای تشخیص بهتر باید ببینید مقدار کجا استفاده میشه
اگه مقدار با دستورهای محاسباتی عددی استفاده بشه ممکنه یه عدد معمولی باشه اگه با دستور های مربوط به عملیات ممیز شناور کار کنه احتمال
بازسازی ساختار
با توجه به دسترسی هایی که دیدیم میتونیم یه ساختار احتمالی بسازیم
C
struct Unknown {
int field_0;
int field_4;
char field_8;
};
اسم فیلد ها رو فعلا بر اساس آفست حافظه گذاشتیم چون هنوز کاربرد واقعی اونها رو نمیدونیم
بعد از بررسی بخش های بیشتری از برنامه ممکنه بفهمیم
سم گذاری بر اساس معنی داده ها انجام میشه نه صرفا ظاهر اسمبلی
بررسی نوشتن روی فیلدها
فقط دستور های خوندن رو بررسی نکنید نوشتن روی حافظه هم اطلاعات مهمی بهمون میده
مثلا:
Asm
mov dword ptr [rcx], 123
mov dword ptr [rcx+4], 25
mov byte ptr [rcx+8], 1
این دستورها به ترتیب روی آفست های صفر چهار و هشت مینویسن
اگه جاهای مختلف برنامه ببینید که همین آفست ها برای خوندن و نوشتن استفاده میشن احتمال اینکه با فیلد های یه ساختار طرف باشی بیشتر میشه
البته هنوز باید بررسی کنید که RCX واقعا آدرس پایه یک ساختار مشترکه یا فقط در اون لحظه به یه بخش از حافظه اشاره میکنه
پیدا کردن اندازه واقعی ساختار
اینجا یه نکته مهم وجود داره
بزرگ ترین آفستی که پیدا میکنید لزوما اندازه کامل ساختار نیست
مثلا اگه آخرین فیلد از آفست هشت شروع بشه و یک بایت اندازه داشته باشه حداقل فضای مورد نیاز برای این فیلدها 9 بایته اما اندازه نهایی ساختار ممکنه به خاطر قوانین همترازی حافظه یا همون Alignment بزرگتر باشه
برای مثال کامپایلر ممکنه ساختار رو به 12 بایت گرد کنه تا همترازی مورد نیاز رعایت بشه
همچنین ممکنه ساختار فیلد های دیگهای داشته باشه که هنوز در مسیرهای بررسی شده بهشون دسترسی پیدا نکردیم
پس اندازه ساختار رو باید با بررسی تخصیص حافظه نحوه استفاده از اشارهگرها و تمام آفست های قابل مشاهده تخمین بزنید
این کار توی Ghidra چطور انجام میشه؟
وقتی ساختار احتمالی رو پیدا کردید میتونید از بخش Data Type Manager یه ساختار جدید بسازی و فیلدهاش رو تعریف کنید
بعد در صورت مشخص بودن نوع متغیر و آدرس پایه میتونید نوع ساختار رو به متغیر مربوط اختصاص بدید تا به جای آفستهای خام دسترسیها خواناتر بشن
مثلا بهجای اینکه فقط ببینید برنامه از
وقتی یه فایل باینری رو مهندسی معکوس میکنی خیلی وقت ها سورس کد اصلی رو ندارید یعنی نمیدونید برنامه نویس چه ساختار هایی تعریف کرده و هر کدوم چه فیلدهایی داشتن
اینجا باید از روی نحوه دسترسی برنامه به حافظه ساختار داده ها رو دوباره بازسازی کنید به این کار میگیم Struct Reconstruction
فرض کنید برنامه اصلی همچین ساختاری داشته باشه
C
struct User {
int id;
int age;
char active;
};
این ساختار سه تا فیلد داره یکی برای شناسه کاربر یکی برای سن و یکی هم برای وضعیت فعال بودن.
ولی وقتی فایل کامپایل شده رو بررسی میکنی ممکنه اصلا اسم
User یا فیلد های id و age رو نبینید فقط دستور های اسمبلی رو دارید که با آدرس های حافظه کار میکننحالا ببین چطور میتونیم ساختار رو از روی همین دستورها تشخیص بدیم
پیدا کردن آدرس فیلد ها
فرض کنید توی اسمبلی به دستورهای زیر رسیدید
Asm
mov eax, dword ptr [rcx]
mov edx, dword ptr [rcx+4]
movzx eax, byte ptr [rcx+8]
بیایم یکی یکی بررسیشون کنیم
Asm
mov eax, dword ptr [rcx]
چهار بایت از آدرسی که داخل RCX قرار داره میخونه
Asm
mov edx, dword ptr [rcx+4]
چهار بایت از چهار بایت بعدی میخونه
Asm
movzx eax, byte ptr [rcx+8]
یک بایت از هشت بایت بعد از آدرس اولیه میخونه
حالا اطلاعاتی که داریم این شکلیه
`textآدرس پایه + 0 چهار بایت
آدرس پایه + 4 چهار بایت
آدرس پایه + 8 یک بایت
این الگو میتونه نشون بده که برنامه داره به سه فیلد مختلف از یه ساختار دسترسی پیدا میکنه
تشخیص اندازه و نوع فیلد ها
از روی اندازه دسترسی ها میتونیم حدس بزنیم هر فیلد چه نوعی داره
دسترسی چهار بایتی ممکنه مربوط به
int یا float باشهدسترسی یک بایتی ممکنه مربوط به
char یا یک مقدار بولی باشهاما حواستون باشه که اندازه به تنهایی نوع دقیق داده رو مشخص نمیکنه مثلا یه مقدار چهار بایتی میتونه بخشی از یه عدد بزرگتر هم باشه
برای تشخیص بهتر باید ببینید مقدار کجا استفاده میشه
اگه مقدار با دستورهای محاسباتی عددی استفاده بشه ممکنه یه عدد معمولی باشه اگه با دستور های مربوط به عملیات ممیز شناور کار کنه احتمال
float بودنش بیشترهبازسازی ساختار
با توجه به دسترسی هایی که دیدیم میتونیم یه ساختار احتمالی بسازیم
C
struct Unknown {
int field_0;
int field_4;
char field_8;
};
اسم فیلد ها رو فعلا بر اساس آفست حافظه گذاشتیم چون هنوز کاربرد واقعی اونها رو نمیدونیم
بعد از بررسی بخش های بیشتری از برنامه ممکنه بفهمیم
field_0 شناسه کاربره field_4 سن کاربره و field_8 وضعیت فعال بودنشهسم گذاری بر اساس معنی داده ها انجام میشه نه صرفا ظاهر اسمبلی
بررسی نوشتن روی فیلدها
فقط دستور های خوندن رو بررسی نکنید نوشتن روی حافظه هم اطلاعات مهمی بهمون میده
مثلا:
Asm
mov dword ptr [rcx], 123
mov dword ptr [rcx+4], 25
mov byte ptr [rcx+8], 1
این دستورها به ترتیب روی آفست های صفر چهار و هشت مینویسن
اگه جاهای مختلف برنامه ببینید که همین آفست ها برای خوندن و نوشتن استفاده میشن احتمال اینکه با فیلد های یه ساختار طرف باشی بیشتر میشه
البته هنوز باید بررسی کنید که RCX واقعا آدرس پایه یک ساختار مشترکه یا فقط در اون لحظه به یه بخش از حافظه اشاره میکنه
پیدا کردن اندازه واقعی ساختار
اینجا یه نکته مهم وجود داره
بزرگ ترین آفستی که پیدا میکنید لزوما اندازه کامل ساختار نیست
مثلا اگه آخرین فیلد از آفست هشت شروع بشه و یک بایت اندازه داشته باشه حداقل فضای مورد نیاز برای این فیلدها 9 بایته اما اندازه نهایی ساختار ممکنه به خاطر قوانین همترازی حافظه یا همون Alignment بزرگتر باشه
برای مثال کامپایلر ممکنه ساختار رو به 12 بایت گرد کنه تا همترازی مورد نیاز رعایت بشه
همچنین ممکنه ساختار فیلد های دیگهای داشته باشه که هنوز در مسیرهای بررسی شده بهشون دسترسی پیدا نکردیم
پس اندازه ساختار رو باید با بررسی تخصیص حافظه نحوه استفاده از اشارهگرها و تمام آفست های قابل مشاهده تخمین بزنید
این کار توی Ghidra چطور انجام میشه؟
وقتی ساختار احتمالی رو پیدا کردید میتونید از بخش Data Type Manager یه ساختار جدید بسازی و فیلدهاش رو تعریف کنید
بعد در صورت مشخص بودن نوع متغیر و آدرس پایه میتونید نوع ساختار رو به متغیر مربوط اختصاص بدید تا به جای آفستهای خام دسترسیها خواناتر بشن
مثلا بهجای اینکه فقط ببینید برنامه از
[rcx+4] میخونه بتونید تشخیص بدید که داره فیلد دوم ساختار رو میخونهاین کار تحلیل توابع بعدی رو هم ساده تر میکنه چون لازم نیست هر بار از اول محاسبه کنید هر آفست به کدوم بخش حافظه اشاره داره
تمرین:
فرض کنید توی یه فایل باینری این دستورها رو پیدا کردید
Asm
mov eax, dword ptr [rbx]
cmp eax, 0
mov ecx, dword ptr [rbx+8]
mov byte ptr [rbx+12], 1
با توجه به این دستورها مشخص کنید
چه آفست هایی از حافظه استفاده شدن و اندازه هر دسترسی چقدره
چه نوع فیلد هایی ممکنه در این آفست ها وجود داشته باشن
چرا هنوز نمیتونیم با اطمینان بگیم ساختار اصلی دقیقا چه فیلدهایی داشته
نکته اصلی این قسمت اینه که توی مهندسی معکوس لازم نیست از همون اول ساختار اصلی رو بدونید با جمع کردن شواهد از آفستها اندازه دسترسی ها و نحوه استفاده از دادهها میتونید قدم به قدم یه مدل قابل قبول از ساختار حافظه بسازید
@reverseengine
تمرین:
فرض کنید توی یه فایل باینری این دستورها رو پیدا کردید
Asm
mov eax, dword ptr [rbx]
cmp eax, 0
mov ecx, dword ptr [rbx+8]
mov byte ptr [rbx+12], 1
با توجه به این دستورها مشخص کنید
چه آفست هایی از حافظه استفاده شدن و اندازه هر دسترسی چقدره
چه نوع فیلد هایی ممکنه در این آفست ها وجود داشته باشن
چرا هنوز نمیتونیم با اطمینان بگیم ساختار اصلی دقیقا چه فیلدهایی داشته
نکته اصلی این قسمت اینه که توی مهندسی معکوس لازم نیست از همون اول ساختار اصلی رو بدونید با جمع کردن شواهد از آفستها اندازه دسترسی ها و نحوه استفاده از دادهها میتونید قدم به قدم یه مدل قابل قبول از ساختار حافظه بسازید
@reverseengine
Struct Reconstruction
When you reverse engineer a binary file, you often don't have the original source code, meaning you don't know what structures the programmer defined and what fields each one has.
Here you have to reconstruct the data structure based on how the program accesses memory. We call this Struct Reconstruction.
Assume the original program has a structure like this:
C
struct User {
int id;
int age;
char active;
};
This structure has three fields, one for the user ID, one for age, and one for the active status.
But when you examine the compiled file, you may not see the User name or the id and age fields at all. You only have assembly instructions that work with memory addresses.
Now let's see how we can identify the structure from these instructions.
Finding the field addresses.
Suppose you come across the following instructions in assembly.
Asm
mov eax, dword ptr [rcx]
mov edx, dword ptr [rcx+4]
movzx eax, byte ptr [rcx+8]
Let's examine them one by one.
Asm
mov eax, dword ptr [rcx]
Reads four bytes from the address in RCX.
Asm
mov edx, dword ptr [rcx+4]
Reads four bytes from the next four bytes.
Asm
movzx eax, byte ptr [rcx+8]
Reads one byte from the eight bytes after the initial address.
Now, the information that We have this form
Text
Base address + 0 four bytes
Base address + 4 four bytes
Base address + 8 one byte
This pattern can show that the program is accessing three different fields of a structure
Determining the size and type of fields
Based on the size of the accesses, we can guess what type each field has
A four-byte access may be related to int or float
A one-byte access may be related to char or a boolean value
But be aware that size alone does not determine the exact type of data, for example, a four-byte value can also be part of a larger number
To better identify, you need to see where the value is used
If the value is used with numerical calculation instructions, it may be a regular number. If it works with instructions related to floating-point operations, it is more likely to be a float
Reconstructing the structure
Given the accesses we saw, we can create a possible structure
C
struct Unknown {
int field_0;
int field_4;
char field_8;
};
We named the fields based on their memory offsets for now because we don't know their actual use yet
After examining more parts of the program, we might find out that field_0 is the user ID, field_4 is the user's age, and field_8 is the activation status
The labeling is done based on the meaning of the data, not just the appearance of the assembly
Checking for writing to fields
Don't just check the read instructions, writing to memory also gives us important information
For example:
Asm
mov dword ptr [rcx], 123
mov dword ptr [rcx+4], 25
mov byte ptr [rcx+8], 1
These instructions write to offsets zero, four, and eight, respectively
If you see the same offsets used for reading and writing in different parts of the program, the likelihood that you are dealing with fields of a structure increases
Of course, you still need to check whether RCX is really the base address of a shared structure or just a point to a part of memory at that moment
Finding the actual size of the structure
There is an important point here
The largest offset you find is not necessarily the full size of the structure
For example, if the last field starts at offset eight and is one byte in size, the minimum space required for these fields is 9 bytes, but the final size of the structure may be larger due to memory alignment rules.
For example, the compiler may round the structure to 12 bytes to meet the required alignment.
The structure may also have other fields that we have not yet accessed in the paths examined.
So you should estimate the size of the structure by examining the memory allocation, how pointers are used, and all visible offsets.
How is this done in Ghidra?
Once you have found a possible structure, you can create a new structure from the Data Type Manager and define its fields.
When you reverse engineer a binary file, you often don't have the original source code, meaning you don't know what structures the programmer defined and what fields each one has.
Here you have to reconstruct the data structure based on how the program accesses memory. We call this Struct Reconstruction.
Assume the original program has a structure like this:
C
struct User {
int id;
int age;
char active;
};
This structure has three fields, one for the user ID, one for age, and one for the active status.
But when you examine the compiled file, you may not see the User name or the id and age fields at all. You only have assembly instructions that work with memory addresses.
Now let's see how we can identify the structure from these instructions.
Finding the field addresses.
Suppose you come across the following instructions in assembly.
Asm
mov eax, dword ptr [rcx]
mov edx, dword ptr [rcx+4]
movzx eax, byte ptr [rcx+8]
Let's examine them one by one.
Asm
mov eax, dword ptr [rcx]
Reads four bytes from the address in RCX.
Asm
mov edx, dword ptr [rcx+4]
Reads four bytes from the next four bytes.
Asm
movzx eax, byte ptr [rcx+8]
Reads one byte from the eight bytes after the initial address.
Now, the information that We have this form
Text
Base address + 0 four bytes
Base address + 4 four bytes
Base address + 8 one byte
This pattern can show that the program is accessing three different fields of a structure
Determining the size and type of fields
Based on the size of the accesses, we can guess what type each field has
A four-byte access may be related to int or float
A one-byte access may be related to char or a boolean value
But be aware that size alone does not determine the exact type of data, for example, a four-byte value can also be part of a larger number
To better identify, you need to see where the value is used
If the value is used with numerical calculation instructions, it may be a regular number. If it works with instructions related to floating-point operations, it is more likely to be a float
Reconstructing the structure
Given the accesses we saw, we can create a possible structure
C
struct Unknown {
int field_0;
int field_4;
char field_8;
};
We named the fields based on their memory offsets for now because we don't know their actual use yet
After examining more parts of the program, we might find out that field_0 is the user ID, field_4 is the user's age, and field_8 is the activation status
The labeling is done based on the meaning of the data, not just the appearance of the assembly
Checking for writing to fields
Don't just check the read instructions, writing to memory also gives us important information
For example:
Asm
mov dword ptr [rcx], 123
mov dword ptr [rcx+4], 25
mov byte ptr [rcx+8], 1
These instructions write to offsets zero, four, and eight, respectively
If you see the same offsets used for reading and writing in different parts of the program, the likelihood that you are dealing with fields of a structure increases
Of course, you still need to check whether RCX is really the base address of a shared structure or just a point to a part of memory at that moment
Finding the actual size of the structure
There is an important point here
The largest offset you find is not necessarily the full size of the structure
For example, if the last field starts at offset eight and is one byte in size, the minimum space required for these fields is 9 bytes, but the final size of the structure may be larger due to memory alignment rules.
For example, the compiler may round the structure to 12 bytes to meet the required alignment.
The structure may also have other fields that we have not yet accessed in the paths examined.
So you should estimate the size of the structure by examining the memory allocation, how pointers are used, and all visible offsets.
How is this done in Ghidra?
Once you have found a possible structure, you can create a new structure from the Data Type Manager and define its fields.
Then, if the variable type and base address are known, you can assign the structure type to the relevant variable so that accesses are more readable instead of raw offsets.
For example, instead of just seeing that the program reads from [rcx+4], you can recognize that it is reading the second field of the structure.
This also simplifies the analysis of subsequent functions because you do not have to calculate from the beginning which memory section each offset refers to each time.
Exercise:
Suppose you find these instructions in a binary file.
Asm
mov eax, dword ptr [rbx]
cmp eax, 0
mov ecx, dword ptr [rbx+8]
mov byte ptr [rbx+12], 1
Based on these instructions, determine
what memory offsets are used and what is the size of each access.
What types of fields are possible in this Offsets exist
Why can't we still say with certainty what fields the original structure has?
The main point of this section is that in reverse engineering you don't need to know the original structure from the start. By gathering evidence from offsets, access sizes, and how data is used, you can build a plausible model of the memory structure step by step
@reverseengine
For example, instead of just seeing that the program reads from [rcx+4], you can recognize that it is reading the second field of the structure.
This also simplifies the analysis of subsequent functions because you do not have to calculate from the beginning which memory section each offset refers to each time.
Exercise:
Suppose you find these instructions in a binary file.
Asm
mov eax, dword ptr [rbx]
cmp eax, 0
mov ecx, dword ptr [rbx+8]
mov byte ptr [rbx+12], 1
Based on these instructions, determine
what memory offsets are used and what is the size of each access.
What types of fields are possible in this Offsets exist
Why can't we still say with certainty what fields the original structure has?
The main point of this section is that in reverse engineering you don't need to know the original structure from the start. By gathering evidence from offsets, access sizes, and how data is used, you can build a plausible model of the memory structure step by step
@reverseengine