ReverseEngineering
Backward Slicing یعنی از نتیجه برگردید به عقب تا اینجا در Data Flow Analysis یاد گرفتیم داده از کجا وارد میشه و چه مسیری رو طی میکنه Backward Slicing دقیقا برعکسشه اینجا از یک نقطه مهم شروع میکنیم و برمیگردیم عقب تا بفهمیم چه چیزهایی باعث به وجود اومدن…
Backward Slicing
It means Work backwards from the result
So far in Data Flow Analysis we have learned where the data comes from and what path it takes
Backward Slicing
is exactly the opposite
Here we start from a significant point and work backwards to understand what caused that result
For example, suppose we have this code:
C++
If our goal is to return y, we start at y
We ask:
Where did y
come from?
Answer:
y = x * 2
So now we need to figure out where x came from
x = a + b
So a and b affect the output
Now if we look at junk we see that it has no path to y or output
So we can ignore it completely for now in this analysis
The Backward Slice path looks like this:
Return y
↑
y = x * 2
↑
x = a + b
↑ ↑
a b
This method is especially useful when you are dealing with a very large and busy function
Because you do not have to analyze everything from the beginning to the end of the function. First, you specify what you are looking for
For example:
The result of a condition
The output value
An important address
A value written to memory
Or an important function
Then you start from that point and go back
Suppose you see this in the disassembler:
The important question is not where jne goes
First, you should Ask:
Where did the EAX value come from before this comparison?
There may have been a few statements:
call check_value
Now you need to go back and see what check_value returns
Or maybe EAX came from a long calculation
In this case, Backward Slicing helps to check only the statements that actually affect the final value of EAX
This is very important in obfuscated programs because there may be a hundred extra statements around an important condition
But maybe only ten statements actually affect the result of that condition. The goal of Backward Slicing is to find exactly that statement
An important point is that you should not only look for direct dependencies
For example:
A affects B
B affects C
C affects the result
So A is also part of the Slice even if it is not directly connected to the result
Exercise:
Check this function:
C
Start from return result and work backwards to determine which variables really affect the output
Then draw the path on paper
input
↓
a
↓
b
↓
result
↓
return
temp
Check it out, you'll see that there are a few operations that are performed but have no effect on the output, and this is exactly where Backward Slicing helps us get straight to the point in the middle of a lot of code
@reverseengine
It means Work backwards from the result
So far in Data Flow Analysis we have learned where the data comes from and what path it takes
Backward Slicing
is exactly the opposite
Here we start from a significant point and work backwards to understand what caused that result
For example, suppose we have this code:
C++
int process(int a, int b)
{
int x = a + b;
int y = x * 2;
int junk = 500;
junk ^= 123;
return y;
}
If our goal is to return y, we start at y
We ask:
Where did y
come from?
Answer:
y = x * 2
So now we need to figure out where x came from
x = a + b
So a and b affect the output
Now if we look at junk we see that it has no path to y or output
So we can ignore it completely for now in this analysis
The Backward Slice path looks like this:
Return y
↑
y = x * 2
↑
x = a + b
↑ ↑
a b
This method is especially useful when you are dealing with a very large and busy function
Because you do not have to analyze everything from the beginning to the end of the function. First, you specify what you are looking for
For example:
The result of a condition
The output value
An important address
A value written to memory
Or an important function
Then you start from that point and go back
Suppose you see this in the disassembler:
cmp eax, 1
jne failed
The important question is not where jne goes
First, you should Ask:
Where did the EAX value come from before this comparison?
There may have been a few statements:
call check_value
Now you need to go back and see what check_value returns
Or maybe EAX came from a long calculation
In this case, Backward Slicing helps to check only the statements that actually affect the final value of EAX
This is very important in obfuscated programs because there may be a hundred extra statements around an important condition
But maybe only ten statements actually affect the result of that condition. The goal of Backward Slicing is to find exactly that statement
An important point is that you should not only look for direct dependencies
For example:
A affects B
B affects C
C affects the result
So A is also part of the Slice even if it is not directly connected to the result
Exercise:
Check this function:
C
int verify(int input)
{
int a = input * 2;
int b = a + 15;
int temp = 100;
temp += 50;
int result = b ^ 0x33;
return result;
}
Start from return result and work backwards to determine which variables really affect the output
Then draw the path on paper
input
↓
a
↓
b
↓
result
↓
return
temp
Check it out, you'll see that there are a few operations that are performed but have no effect on the output, and this is exactly where Backward Slicing helps us get straight to the point in the middle of a lot of code
@reverseengine
Static Analysis of GLOBAL GROUP Ransomware: From Encrypted Config to Panic Mode
https://blog.ghozt.ninja/2026/01/07/aware
@reverseengine
https://blog.ghozt.ninja/2026/01/07/aware
@reverseengine
/home/ghozt
Static Analysis of GLOBAL GROUP Ransomware: From Encrypted Config to Panic Mode
Analysis Date: January 7, 2026Methodology: Static analysis onlySample: f6f7a37b49310287a253dbdf81e22f0593f44111215ca9308e46d2c68516196f TL;DRIn-depth analysis of GLOBAL GROUP ransomware (RaaS), revea
ReverseEngineering
Part 28 Buffer Overflow AFL++ and Instrumentation How does a Fuzzer understand what is going on inside a program In the previous section, we learned about libFuzzer and Coverage Guided Fuzzing Now let's move on to AFL++ AFL++ is one of the most famous…
/home/ghozt
AFL - Introduction
This is a simple walkthrough to install, setup and run AFL against an open source dumb C program. What is AFL American fuzzy lop is a security-oriented fuzzer that employs a novel type of compile-time
elf.pdf
337.1 KB
Tool Interface Standard (TIS)
Executable and Linking Format (ELF)
Specification
Version 1.2
@reverseengine
Executable and Linking Format (ELF)
Specification
Version 1.2
@reverseengine
Packt.Mobile.App.Reverse.Engineering.pdf
17.3 MB
Mobile App Reverse Engineering: Get started with discovering, analyzing, and exploring the internals of Android and iOS apps
@reverseengine
@reverseengine
RECON 2026 - Deobfuscation in tne Age of Agentic Reverse Engineering
https://www.youtube.com/watch?v=3-gJ6EUFoKM
@reverseengine
https://www.youtube.com/watch?v=3-gJ6EUFoKM
@reverseengine
YouTube
RECON 2026 - Deobfuscation in the Age of Agentic Reverse Engineering
Presented by by Tim Blazytko and Nicolò Altamura
Large language models first entered reverse engineering as helpful assistants: improving decompiler output, suggesting names, answering questions about code, and generating small analysis scripts. Useful…
Large language models first entered reverse engineering as helpful assistants: improving decompiler output, suggesting names, answering questions about code, and generating small analysis scripts. Useful…
بخش سی ام بافر اورفلو
Crash Triage
تا اینجا یاد گرفتیم با Fuzzing کلی Crash پیدا کنیم
ولی اینجا یه مشکل خیلی بزرگ داریم
Fuzzer
ممکنه برای فقط یک باگ صدها یا حتی هزاران Crash مختلف تولید کنه
پس بعد از Fuzzing باید بشینیم بفهمیم
کدوم Crash واقعا مهمه
کدوم Crash ها در اصل مربوط به یه باگ مشترکن
Crash
دقیقا کجای برنامه اتفاق افتاده
و مهمتر از همه
علت اصلی Crash چی بوده
به این مرحله میگیم Crash Triage
یعنی مرتب کردن و بررسی Crash ها تا بفهمیم کدوم ها واقعا ارزش بررسی دارن
اولین چیزی که بررسی میکنیم
فرض کنید Fuzzer با این ورودی ها باعث Crash شده
AAAA
BBBBBBBB
test123
AAAAAAAAAAAAAAAA
hello_world
نباید سریع نتیجه بگیریم که اینجا 5 تا باگ داریم
ممکنه هر پنج ورودی در اخر به یک دستور مشخص از برنامه برسن
مثلا
0x401234: mov byte ptr [rax], dl
و همین دستور باعث Crash بشه
پس چیزی که برای ما مهمه فقط فایل ورودی نیست
باید ببینیم Crash کجا اتفاق افتاده و تحت چه شرایطی اتفاق افتاده
یعنی
Input
↓
Program
↓
Fault
↓
Where did it happen
↓
Why did it happen
Faulting Instruction
یکی از مهم ترین چیزایی که موقع بررسی Crash باید پیدا کنیم
"Faulting Instruction" هست
یعنی دستوری که CPU موقع اتفاق افتادن Exception یا Fault در حال اجرای اون بوده
مثلا
Program received signal SIGSEGV
RIP = 0x401234
0x401234:
mov byte ptr [rax], dl
اینجا فقط اینکه برنامه "SIGSEGV" داده برامون کافی نیست
باید بپرسیم
RAX
چه مقداری داشته
چرا این مقدار نامعتبر بوده
این مقدار از کجا اومده
مثلا ممکنه "RAX" به یه آدرس نامعتبر اشاره کنه
پس به جای اینکه فقط بگیم
Program crashed
باید بریم یه مرحله عمیقتر
Crash
↓
Faulting Instruction
↓
Invalid Register / Memory
↓
Why did this happen
Crash خودش علت نیست
Crash نتیجه یک اتفاق قبلیه
Stack Trace
بعد میریم سراغ "Stack Trace"
مثلا
#0 0x401234 in process_data()
#1 0x401567 in handle_input()
#2 0x401789 in main()
یعنی مسیر رسیدن برنامه به Crash تقریبا این بوده
main()
↓
handle_input()
↓
process_data()
↓
CRASH
این اطلاعات برای ما خیلی مهمه
چون حالا میدونیم باید اول کدوم قسمت برنامه رو بررسی کنیم
مثلا میتونیم داخل IDA یا Ghidra بریم سراغ "process_data()" و ببینیم دقیقا قبل از Crash چه اتفاقی افتاده
Crash Deduplication
حالا فرض کنید Fuzzer هزار تا Crash به ما داده
اگر برای هر Crash اطلاعاتی مثل این داشته باشیم
Signal
Faulting Address
Faulting Instruction
Stack Trace
میتونیم Crashهای شبیه به هم رو کنار هم قرار بدیم
مثلا
Crash 1 → process_data → 0x401234
Crash 2 → process_data → 0x401234
Crash 3 → process_data → 0x401234
Crash 4 → parse_packet → 0x402010
احتمالا Crash های اول تا سوم یه خانواده هستن
Crash 1
Crash 2
Crash 3
↓
Crash Family A
ولی Crash چهارم مسیر متفاوتی داره
Crash 4
↓
Crash Family B
البته یه نکته مهم اینجاست
برابر بودن "Faulting Address" به تنهایی ثابت نمیکنه که دو Crash حتما یک باگ هستن
ممکنه چند مسیر متفاوت به یک دستور برسن
پس برای Deduplication معمولا چند تا نشونه رو کنار هم بررسی میکنیم
مثل
Faulting Instruction
Call Stack
Registers
Memory State
Crash Type
Input Behavior
هدف اینه که Crashهای تکراری رو از Crashهای واقعاً متفاوت جدا کنیم
Sanitizerها اینجا خیلی کمک میکنن
مثلاً اگه برنامه با "AddressSanitizer" یا همون "ASan" اجرا شده باشه
به جای اینکه فقط یه پیام ساده مثل این ببینیم
Segmentation fault
ممکنه اطلاعات خیلی بیشتری داشته باشیم
مثلا
ERROR: AddressSanitizer:
heap-buffer-overflow
READ of size 4
#0 process_data
#1 handle_input
#2 main
ASan
میتونه اطلاعات بیشتری درباره نوع خطا و محل دسترسی غیرمجاز بده و در بعضی خطاها اطلاعات مربوط به allocation و محل access رو هم گزارش کنه
در نتیجه مسیر بررسی میتونه از این
Crash
برسه به
Crash
↓
Error Type
↓
Invalid Access
↓
Stack Trace
↓
Suspicious Function
↓
Root Cause
یعنی به جای اینکه فقط بدونیم برنامه از بین رفته
میفهمیم تقریبا چه اتفاقی باعث از بین رفتت برنامه شده
یک مثال ساده
این برنامه رو ببینید
#include <stdio.h>
void process(int index) {
int data[4] = {10, 20, 30, 40};
printf("%d\n", data[index]);
}
int main(void) {
process(10);
return 0;
}
اینجا آرایه فقط 4 عضو داره
data[0]
data[1]
data[2]
data[3]
ولی برنامه میخواد اینو بخونه
data[10]
پس برنامه داره خارج از محدوده آرایه به حافظه دسترسی پیدا میکنه
Valid
data[0] ─┐
data[1] │
data[2] │
data[3] ─┘
Invalid
data[10] → خارج از محدوده
به این حالت میگیم "Out-of-Bounds Read"
یعنی برنامه داره از جایی خارج از محدودهای که برای آرایه در نظر گرفته شده داده میخونه
حالا از دید Reverse Engineer
فرض کنید Crash داریم و Stack Trace اینو نشون میده
#0 process
#1 main
پس میریم داخل "process"
void process(int index) {
int data[4];
printf("%d\n", data[index]);
}
اینجا سریع دنبال الگوهایی مثل این میگردیم
array[index]
بعد باید بفهمیم "index" از کجا اومده
index
↓
Where did it come from
↓
Was it validated
↓
Was the upper bound checked
مثلا اگر قبل از دسترسی چنین بررسی ای وجود داشته باشه
if (index < 0 || index >= 4)
return;
برنامه قبل از دسترسی نامعتبر جلوی این حالت رو میگیره
ولی اگر چنین بررسیای وجود نداشته باشه
index
↓
array[index]
↓
No Bounds Check
↓
Out-of-Bounds Access
احتمال وجود باگ خیلی بیشتر میشه
هدف اصلی Crash Triage
در اخر میخوایم هزاران Crash رو تبدیل کنیم به چند مورد محدود و قابل بررسی
مثلا
1000 Crash
↓
250 Crash Groups
↓
20 Unique Crash
↓
7 Root Cause
↓
7 Bug
یعنی Fuzzer کارش اینه که کلی ورودی مختلف رو امتحان کنه و رفتارهای غیرعادی رو پیدا کنه
ولی بعدش باید بفهمیم این رفتارها دقیقا چی هستن
اینجاست که ما وارد داستان میشیم
Fuzzer
↓
Crash
↓
Crash Triage
↓
Crash Deduplication
↓
Root Cause Analysis
در واقع Fuzzer میگه
یه جای برنامه یه اتفاق عجیب افتاد
ما باید بفهمهیم
کجا
چرا
و دقیقا چه چیزی باعثش شد
این دقیقا جاییه که Fuzzing و ما به هم وصل میشیم
@reverseengine
data[0] ─┐
data[1] │
data[2] │
data[3] ─┘
Invalid
data[10] → خارج از محدوده
به این حالت میگیم "Out-of-Bounds Read"
یعنی برنامه داره از جایی خارج از محدودهای که برای آرایه در نظر گرفته شده داده میخونه
حالا از دید Reverse Engineer
فرض کنید Crash داریم و Stack Trace اینو نشون میده
#0 process
#1 main
پس میریم داخل "process"
void process(int index) {
int data[4];
printf("%d\n", data[index]);
}
اینجا سریع دنبال الگوهایی مثل این میگردیم
array[index]
بعد باید بفهمیم "index" از کجا اومده
index
↓
Where did it come from
↓
Was it validated
↓
Was the upper bound checked
مثلا اگر قبل از دسترسی چنین بررسی ای وجود داشته باشه
if (index < 0 || index >= 4)
return;
برنامه قبل از دسترسی نامعتبر جلوی این حالت رو میگیره
ولی اگر چنین بررسیای وجود نداشته باشه
index
↓
array[index]
↓
No Bounds Check
↓
Out-of-Bounds Access
احتمال وجود باگ خیلی بیشتر میشه
هدف اصلی Crash Triage
در اخر میخوایم هزاران Crash رو تبدیل کنیم به چند مورد محدود و قابل بررسی
مثلا
1000 Crash
↓
250 Crash Groups
↓
20 Unique Crash
↓
7 Root Cause
↓
7 Bug
یعنی Fuzzer کارش اینه که کلی ورودی مختلف رو امتحان کنه و رفتارهای غیرعادی رو پیدا کنه
ولی بعدش باید بفهمیم این رفتارها دقیقا چی هستن
اینجاست که ما وارد داستان میشیم
Fuzzer
↓
Crash
↓
Crash Triage
↓
Crash Deduplication
↓
Root Cause Analysis
در واقع Fuzzer میگه
یه جای برنامه یه اتفاق عجیب افتاد
ما باید بفهمهیم
کجا
چرا
و دقیقا چه چیزی باعثش شد
این دقیقا جاییه که Fuzzing و ما به هم وصل میشیم
@reverseengine
ReverseEngineering
بخش سی ام بافر اورفلو Crash Triage تا اینجا یاد گرفتیم با Fuzzing کلی Crash پیدا کنیم ولی اینجا یه مشکل خیلی بزرگ داریم Fuzzer ممکنه برای فقط یک باگ صدها یا حتی هزاران Crash مختلف تولید کنه پس بعد از Fuzzing باید بشینیم بفهمیم کدوم Crash واقعا مهمه…
Part 30 Buffer Overflow
Crash Triage
So far we have learned to find a general crash with Fuzzing
But here we have a very big problem
Fuzzer
may generate hundreds or even thousands of different crashes for just one bug
So after Fuzzing we need to figure out
Which crash is really important
Which crashes are actually related to a common bug
Where exactly in the program did the crash occur
And most importantly
What was the root cause of the crash
We call this stage Crash Triage
That is, sorting and examining the crashes to find out which ones are really worth investigating
The first thing we examine
Suppose the Fuzzer caused a crash with these inputs
AAAA
BBBBBBBB
test123
AAAAAAAAAAAAAA
hello_world
We should not quickly conclude that we have 5 bugs here
It is possible that all five inputs ultimately lead to a specific command in the program Get
For example
0x401234: mov byte ptr [rax], dl
And this command causes a Crash
So what is important to us is not just the input file
We need to see where the Crash occurred and under what conditions
That is
Input
↓
Program
↓
Fault
↓
Where did it happen
↓
Why did it happen
2 Faulting Instruction
One of the most important things to find when investigating a Crash is
"Faulting Instruction"
That is, the instruction that the CPU was executing when the Exception or Fault occurred
For example
Program received signal SIGSEGV
RIP = 0x401234
0x401234:
mov byte ptr [rax], dl
Here, just knowing that the program gave us "SIGSEGV" is not enough
We need to ask
What value did RAX have? Why was this value invalid? Where did this value come from?
For example "RAX" may point to an invalid address
So instead of just saying
Program crashed
we need to go a step deeper
Crash
↓
Faulting Instruction
↓
Invalid Register / Memory
↓
Why did this happen
Crash itself is not the cause
Crash is the result of a previous event
3 Stack Trace
Then we go to the "Stack Trace"
For example
#0 0x401234 in process_data()
#1 0x401567 in handle_input()
#2 0x401789 in main()
That means the path the program took to Crash was roughly
main()
↓
handle_input()
↓
process_data()
↓
CRASH
This information is very important for us
Because now we know which part of the program we should check first
For example, we can go to "process_data()" in IDA or Ghidra and see exactly what happened before the Crash
Crash Deduplication
Now suppose the Fuzzer gives us a thousand crashes
If we have information like this for each crash
Signal
Faulting Address
Faulting Instruction
Stack Trace
We can put similar crashes together
For example
Crash 1 → process_data → 0x401234
Crash 2 → process_data → 0x401234
Crash 3 → process_data → 0x401234
Crash 4 → parse_packet → 0x402010
The first to third crashes are probably in the same family
Crash 1
Crash 2
Crash 3
↓
Crash Family A
But the fourth crash has a different path
Crash 4
↓
Crash Family B
Of course, there is an important point here
The equality of the "Faulting Address" alone does not prove that two crashes are necessarily the same bug
There may be several different paths to the same instruction Get
So for Deduplication we usually check several indicators together
For example
Faulting Instruction
Call Stack
Registers
Memory State
Crash Type
Input Behavior
The goal is to separate duplicate crashes from truly different crashes
Sanitizers are very helpful here
For example, if the program was run with "AddressSanitizer" or "ASan"
Instead of just seeing a simple message like this
Segmentation fault
we may have much more information
For example
ERROR: AddressSanitizer:
heap-buffer-overflow
READ of size 4
#0 process_data
#1 handle_input
#2 main
ASan
can give more information about the type of error and the location of the unauthorized access and in some errors it also reports information about the allocation and access location
As a result, the inspection path can go from this
Crash
to
Crash
↓
Error Type
↓
Invalid Access
↓
Stack Trace
↓
Suspicious Function
↓
Root Cause
Instead of just knowing that the program crashed
Valid
data[0] ─┐
data[1] │
data[2] │
data[3] ─┘
Invalid
data[10] → Out of bounds
We call this situation "out of bounds read"
That is, the program is reading from outside the range intended for the array
Now from the perspective of a reverse engineer
Suppose we have a Crash and the Stack Trace shows this
Process number 0
Original number 1
So we go into "process"
Empty process (int index) {
int data[4];
printf("%d\n", data[index]);
}
Here we quickly look for patterns like this
Array[index]
Next we need to figure out where "index" came from
Index
↓
Where did it come from
↓
Confirmed
↓
Was the upper bound checked?
For example, if there is access to such a check
if (index < 0 || index >= 4)
return;
The program will prevent this situation before accessing invalid
But if there are no such checks
Index
↓
Array[index]
↓
Check without restrictions
↓
Access out of bounds
The probability of a bug is much higher
The main goal of Crash Triage
In the end, we want to reduce thousands of crashes to a few limited and checkable ones
For example
1000 crashes
↓
250 crash groups
↓
20 unique crashes
↓
7 root causes
↓
7 bugs
That is, the job of the Fuzzer is to try a lot of different inputs and find unusual behaviors
But then we need to understand what exactly these behaviors are
This is where we get into the story
Fuzzer
↓
Crash
↓
Crash triage
↓
Crash non-recurrence
↓
Root cause analysis
In essence, the Fuzzer says
Something strange happened somewhere in the program
We need to understand
Where
Why
And exactly what caused it This is exactly where Fuzzing and we connect
@reverseengine
data[0] ─┐
data[1] │
data[2] │
data[3] ─┘
Invalid
data[10] → Out of bounds
We call this situation "out of bounds read"
That is, the program is reading from outside the range intended for the array
Now from the perspective of a reverse engineer
Suppose we have a Crash and the Stack Trace shows this
Process number 0
Original number 1
So we go into "process"
Empty process (int index) {
int data[4];
printf("%d\n", data[index]);
}
Here we quickly look for patterns like this
Array[index]
Next we need to figure out where "index" came from
Index
↓
Where did it come from
↓
Confirmed
↓
Was the upper bound checked?
For example, if there is access to such a check
if (index < 0 || index >= 4)
return;
The program will prevent this situation before accessing invalid
But if there are no such checks
Index
↓
Array[index]
↓
Check without restrictions
↓
Access out of bounds
The probability of a bug is much higher
The main goal of Crash Triage
In the end, we want to reduce thousands of crashes to a few limited and checkable ones
For example
1000 crashes
↓
250 crash groups
↓
20 unique crashes
↓
7 root causes
↓
7 bugs
That is, the job of the Fuzzer is to try a lot of different inputs and find unusual behaviors
But then we need to understand what exactly these behaviors are
This is where we get into the story
Fuzzer
↓
Crash
↓
Crash triage
↓
Crash non-recurrence
↓
Root cause analysis
In essence, the Fuzzer says
Something strange happened somewhere in the program
We need to understand
Where
Why
And exactly what caused it This is exactly where Fuzzing and we connect
@reverseengine
Program Slicing
با Ghidra
تا اینجا Backward Slicing و Forward Slicing رو یاد گرفتیم
حالا وقتشه همین مفاهیم رو روی یه باینری واقعی اجرا کنیم
هدف اینه که وقتی یه تابع شلوغ و پر از متغیر و دستور دیدیم لازم نباشه کل تابع رو زیر و رو کنیم
فقط مسیر دادهای که برامون مهمه رو جدا میکنیم و همون رو دنبال میکنیم
یه برنامه ساده برای آزمایش
مثلا این کد رو داریم
C
#include <stdio.h>
int check(int input) {
int a = input + 5;
int b = a * 3;
int junk = 900;
junk ^= 123;
int result = b - 7;
return result;
}
int main() {
int x = 10;
printf("%d\n", check(x));
return 0;
}
اگه input برابر 10 باشه محاسبات این شکلی پیش میرن
input = 10
↓
a = 10 + 5 = 15
↓
b = 15 × 3 = 45
↓
result = 45 - 7 = 38
در نهایت return مقدار 38 رو برمیگردونه
ولی این وسط یه متغیر دیگه هم داریم
junk = 900
junk ^= 123
این مقدار هیچ تاثیری روی خروجی نداره
و دقیقا همین چیزیه که میخوایم با Program Slicing پیدا کنیم
باینری رو وارد Ghidra میکنیم
اول برنامه رو کامپایل میکنیم و فایل اجرایی رو داخل Ghidra باز میکنیم
بعد میریم سراغ تابع
check
اینجا دو بخش برای ما خیلی مهم هستن
Decompiler
Function Graph
Decompiler
کمک میکنه منطق کلی تابع رو راحت تر بفهمیم
Function Graph
هم کمک میکنه مسیرهای مختلف اجرای تابع و ارتباط بین Basic Blockها رو ببینیم
ولی حواسمون باشه
Decompiler
خودش حقیقت مطلق نیست
چیزی که میبینیم بازسازی Ghidra از باینریه
برای تحلیل جدی باید در صورت نیاز برگردیم به Assembly و خود Data Flow رو بررسی کنیم
اول Return Value رو پیدا میکنیم
فرض میکنیم Decompiler چیزی شبیه این نشون بده
C
int check(int input)
{
int a;
int b;
int junk;
int result;
a = input + 5;
b = a * 3;
junk = 900;
junk = junk ^ 123;
result = b - 7;
return result;
}
حالا سوال اصلی اینه
چه چیزهایی روی return تاثیر گذاشتن
اینجا از Backward Slicing شروع میکنیم
از Return به عقب برمیگردیم
آخرین قسمت اینه
lua
return result;
پس اولین چیزی که باید بررسی کنیم result هست
result اینجا ساخته شده
result = b - 7;
پس وابستگی ما فعلا اینه
return
↑
result
↑
b
حالا میپرسیم b از کجا اومده
b = a * 3;
پس
return
↑
result
↑
b
↑
a
حالا a
a = input + 5;
در نتیجه میرسیم به
return
↑
result
↑
b
↑
a
↑
input
اگه محاسبات رو هم داخل مسیر بذاریم
input
↓
+5
↓
×3
↓
-7
↓
return
پس Slice مربوط به خروجی این برنامه تقریبا همین مسیره
حالا Junk Code رو بررسی کنیم
این قسمت رو داریم
C
int junk = 900;
junk ^= 123;
ولی هیچ جا نتیجه
پس مسیرش این شکلیه
junk
↓
junk ^ 123
↓
X
و هیچ مسیری به
در نتیجه وقتی هدفمون
هست
داخل Backward Slice مربوط به
اینجاست که میبینیم Slicing چرا مفیده
به جای اینکه تمام دستورهای تابع رو هم وزن ببینیم
فقط دستورهایی رو نگه میداریم که به داده موردنظر ما وابستگی دارن
همین کار رو روی Assembly انجام بدیم
فرض میکنیم کامپایلر تابع رو تقریبا به این شکل تبدیل کرده
mov eax, edi
add eax, 5
imul eax, 3
sub eax, 7
ret
حالا از پایین به بالا نگاه میکنیم
دستور
داخل این مثال مقدار خروجی داخل
پس میریم دستور قبل
sub eax, 7
یعنی مقدار
قبل از اون
imul eax, 3
باز هم همون مقدار
بعد
add eax, 5
باز هم
و در اخر
mov eax, edi
اینجا مشخص میشه مقدار اولیه از
پس Slice ما در Assembly میشه
EDI
↓
EAX
↓
EAX + 5
↓
EAX × 3
↓
EAX - 7
↓
RET
اینجا دیگه داریم Data Flow واقعی رو در سطح Instruction دنبال میکنیم
یه نکته خیلی مهم
در باینری واقعی معمولا دیگه خبری از اسمهایی مثل اینا نیست
input
result
a
b
ممکنه فقط چیزایی مثل این ببینیم
RAX
RBX
RCX
RDX
[rbp-0x20]
[rbp-0x18]
اینجاست که کار ما شروع میشه
باید خودمون رابطه بین این داده ها رو بازسازی کنیم
مثلا
mov eax, [rbp-20h]
add eax, 5
imul eax, 3
mov [rbp-18h], eax
میتونیم این مسیر رو ذهنی تبدیل کنیم به
با Ghidra
تا اینجا Backward Slicing و Forward Slicing رو یاد گرفتیم
حالا وقتشه همین مفاهیم رو روی یه باینری واقعی اجرا کنیم
هدف اینه که وقتی یه تابع شلوغ و پر از متغیر و دستور دیدیم لازم نباشه کل تابع رو زیر و رو کنیم
فقط مسیر دادهای که برامون مهمه رو جدا میکنیم و همون رو دنبال میکنیم
یه برنامه ساده برای آزمایش
مثلا این کد رو داریم
C
#include <stdio.h>
int check(int input) {
int a = input + 5;
int b = a * 3;
int junk = 900;
junk ^= 123;
int result = b - 7;
return result;
}
int main() {
int x = 10;
printf("%d\n", check(x));
return 0;
}
`اگه input برابر 10 باشه محاسبات این شکلی پیش میرن
input = 10
↓
a = 10 + 5 = 15
↓
b = 15 × 3 = 45
↓
result = 45 - 7 = 38
در نهایت return مقدار 38 رو برمیگردونه
ولی این وسط یه متغیر دیگه هم داریم
junk = 900
junk ^= 123
این مقدار هیچ تاثیری روی خروجی نداره
و دقیقا همین چیزیه که میخوایم با Program Slicing پیدا کنیم
باینری رو وارد Ghidra میکنیم
اول برنامه رو کامپایل میکنیم و فایل اجرایی رو داخل Ghidra باز میکنیم
بعد میریم سراغ تابع
check
اینجا دو بخش برای ما خیلی مهم هستن
Decompiler
Function Graph
Decompiler
کمک میکنه منطق کلی تابع رو راحت تر بفهمیم
Function Graph
هم کمک میکنه مسیرهای مختلف اجرای تابع و ارتباط بین Basic Blockها رو ببینیم
ولی حواسمون باشه
Decompiler
خودش حقیقت مطلق نیست
چیزی که میبینیم بازسازی Ghidra از باینریه
برای تحلیل جدی باید در صورت نیاز برگردیم به Assembly و خود Data Flow رو بررسی کنیم
اول Return Value رو پیدا میکنیم
فرض میکنیم Decompiler چیزی شبیه این نشون بده
C
int check(int input)
{
int a;
int b;
int junk;
int result;
a = input + 5;
b = a * 3;
junk = 900;
junk = junk ^ 123;
result = b - 7;
return result;
}
حالا سوال اصلی اینه
چه چیزهایی روی return تاثیر گذاشتن
اینجا از Backward Slicing شروع میکنیم
از Return به عقب برمیگردیم
آخرین قسمت اینه
lua
return result;
پس اولین چیزی که باید بررسی کنیم result هست
result اینجا ساخته شده
result = b - 7;
پس وابستگی ما فعلا اینه
return
↑
result
↑
b
حالا میپرسیم b از کجا اومده
b = a * 3;
پس
return
↑
result
↑
b
↑
a
حالا a
a = input + 5;
در نتیجه میرسیم به
return
↑
result
↑
b
↑
a
↑
input
اگه محاسبات رو هم داخل مسیر بذاریم
input
↓
+5
↓
×3
↓
-7
↓
return
پس Slice مربوط به خروجی این برنامه تقریبا همین مسیره
حالا Junk Code رو بررسی کنیم
این قسمت رو داریم
C
int junk = 900;
junk ^= 123;
ولی هیچ جا نتیجه
junk وارد محاسبه result نشدهپس مسیرش این شکلیه
junk
↓
junk ^ 123
↓
X
و هیچ مسیری به
result ندارهدر نتیجه وقتی هدفمون
returnهست
junk داخل Backward Slice مربوط به
return قرار نمیگیرهاینجاست که میبینیم Slicing چرا مفیده
به جای اینکه تمام دستورهای تابع رو هم وزن ببینیم
فقط دستورهایی رو نگه میداریم که به داده موردنظر ما وابستگی دارن
همین کار رو روی Assembly انجام بدیم
فرض میکنیم کامپایلر تابع رو تقریبا به این شکل تبدیل کرده
mov eax, edi
add eax, 5
imul eax, 3
sub eax, 7
ret
حالا از پایین به بالا نگاه میکنیم
دستور
ret مقدار برگشتی تابع رو برمیگردونهداخل این مثال مقدار خروجی داخل
EAX قرار دارهپس میریم دستور قبل
sub eax, 7
یعنی مقدار
EAX هنوز روی خروجی تاثیر دارهقبل از اون
imul eax, 3
باز هم همون مقدار
EAX رو داریم که وارد محاسبه بعدی میشهبعد
add eax, 5
باز هم
EAX بخشی از مسیر داده استو در اخر
mov eax, edi
اینجا مشخص میشه مقدار اولیه از
EDI وارد این مسیر شدهپس Slice ما در Assembly میشه
EDI
↓
EAX
↓
EAX + 5
↓
EAX × 3
↓
EAX - 7
↓
RET
اینجا دیگه داریم Data Flow واقعی رو در سطح Instruction دنبال میکنیم
یه نکته خیلی مهم
در باینری واقعی معمولا دیگه خبری از اسمهایی مثل اینا نیست
input
result
a
b
ممکنه فقط چیزایی مثل این ببینیم
RAX
RBX
RCX
RDX
[rbp-0x20]
[rbp-0x18]
اینجاست که کار ما شروع میشه
باید خودمون رابطه بین این داده ها رو بازسازی کنیم
مثلا
mov eax, [rbp-20h]
add eax, 5
imul eax, 3
mov [rbp-18h], eax
میتونیم این مسیر رو ذهنی تبدیل کنیم به
[rbp-20h]
↓
+5
↓
×3
↓
[rbp-18h]
حالا باید ببینیم
اگر در اخر به چیزی شبیه این برسیم
mov eax, [rbp-18h]
ret
میفهمیم این مقدار بخشی از مسیر دادهای هست که به خروجی تابع میرسه
Forward و Backward
رو با هم ترکیب کنیم
اینجا کار جالبتر میشه
فرض میکنیم ورودی تابع در
از ورودی شروع میکنیم
Forward Slicing
EDI
↓
EAX
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
حالا از طرف دیگه از
Backward Slicing
RETURN
↑
SUB
↑
IMUL
↑
ADD
↑
EAX
↑
EDI
اگر این دو تحلیل به یه مسیر مشترک برسن
اطمینان بیشتری داریم که مسیر اصلی Data Flow رو درست پیدا کردیم
EDI
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
داخل Ghidra به چی نگاه کنیم
وقتی دارید Program Slicing رو روی یه تابع واقعی انجام میدید چند چیز خیلی مهمن
Decompiler
Function Graph
References
Register Usage
Memory References
Variable Definitions
Variable Uses
ولی دو سوال از همه مهمترن
این مقدار کجا ساخته میشه
و
این مقدار بعدا کجا استفاده میشه
این دو سوال ساده به نظر میرسن ولی پایه خیلی از تحلیلهای جدی Reverse Engineering هستن
هر بار یه Register یا Memory Location دیدید باید همین رابطه رو دنبال کنید
Definition
↓
Value
↓
Use
↓
New Value
↓
Next Use
یه مثال کمی واقعیتر
فرض کنیم این تابع رو داریم
C++
int verify(int input) {
int x = input ^ 0x55;
int y = x + 0x1234;
int noise = 777;
noise *= 4;
int z = y ^ 0xAA;
return z == 0x5678;
}
اینجا هدف ما فهمیدن شرط اخره
lua
return z == 0x5678;
پس از
اول
بعد
بعد
و در اخر
مسیر میشه
return
↑
comparison
↑
z
↑
y
↑
x
↑
input
اگر محاسبات رو هم اضافه کنیم
input
↓
XOR 0x55
↓
+ 0x1234
↓
XOR 0xAA
↓
compare 0x5678
↓
return
ولی این قسمت
noise = 777
noise *= 4
هیچ ارتباطی با
پس برای تحلیل این شرط فعلا میتونه خارج از Slice ما قرار بگیره
چرا این موضوع برای ما مهمه
چون توی برنامه واقعی ممکنه یه تابع صدها یا حتی هزاران Instruction داشته باشه
ولی چیزی که واقعا دنبالش هستیم ممکنه فقط این باشه
Input
↓
Transformation
↓
Comparison
↓
Decision
Slicing
کمک میکنه همین مسیر رو از بین حجم زیادی از کد جدا کنیم
مخصوصا وقتی با چیزهایی مثل این روبهرو میشیم
Obfuscation
Junk Code
Function Complexity
Chain of Calculations
Multiple Conditions
ممکنه ظاهر تابع خیلی شلوغ باشه
ولی وقتی Data Flow رو دنبال کنیم بفهمیم فقط چند دستور واقعا روی نتیجهای که برامون مهمه تاثیر دارن
تمرین:
این تابع رو بررسی کنید
int verify(int input) {
int a = input ^ 0x31;
int junk1 = 1234;
junk1 += 55;
int b = a * 7;
int junk2 = 999;
junk2 ^= 0x44;
int c = b - 12;
return c == 0x500;
}
دو تا Slice استخراج کنید
اول Backward Slice از
یعنی از اینجا شروع کنید
return
↑
?
↑
?
↑
?
↑
input
بعد Forward Slice رو از
input
↓
?
↓
?
↓
?
↓
comparison
↓
return
در اخر باید بتونید مسیر اصلی رو تقریبا به این شکل دربیارید
input
↓
XOR 0x31
↓
× 7
↓
- 12
↓
comparison 0x500
↓
return
و این دو قسمت
junk1
junk2
نباید وارد Slice مربوط به این خروجی بشن چون روی نتیجه نهایی تاثیری ندارن
حالا یه مرحله مهمتر هم وجود داره
همین تحلیل رو روی Assembly انجام بدید اونجا دیگه خبری از اسمهای تمیز و قشنگ
باید خودتون Register ها و Memory Location ها رو دنبال کنید و Data Flow رو از روی Instruction ها بازسازی کنید
اینجاست که تفاوت بین فقط خوندن Decompiler و واقعا تحلیل کردن باینری کم کم مشخص میشه
@reverseengine
↓
+5
↓
×3
↓
[rbp-18h]
حالا باید ببینیم
[rbp-18h] بعدا کجا استفاده میشهاگر در اخر به چیزی شبیه این برسیم
mov eax, [rbp-18h]
ret
میفهمیم این مقدار بخشی از مسیر دادهای هست که به خروجی تابع میرسه
Forward و Backward
رو با هم ترکیب کنیم
اینجا کار جالبتر میشه
فرض میکنیم ورودی تابع در
EDI قرار دارهاز ورودی شروع میکنیم
Forward Slicing
EDI
↓
EAX
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
حالا از طرف دیگه از
RETURN برمیگردیم عقبBackward Slicing
RETURN
↑
SUB
↑
IMUL
↑
ADD
↑
EAX
↑
EDI
اگر این دو تحلیل به یه مسیر مشترک برسن
اطمینان بیشتری داریم که مسیر اصلی Data Flow رو درست پیدا کردیم
EDI
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
داخل Ghidra به چی نگاه کنیم
وقتی دارید Program Slicing رو روی یه تابع واقعی انجام میدید چند چیز خیلی مهمن
Decompiler
Function Graph
References
Register Usage
Memory References
Variable Definitions
Variable Uses
ولی دو سوال از همه مهمترن
این مقدار کجا ساخته میشه
و
این مقدار بعدا کجا استفاده میشه
این دو سوال ساده به نظر میرسن ولی پایه خیلی از تحلیلهای جدی Reverse Engineering هستن
هر بار یه Register یا Memory Location دیدید باید همین رابطه رو دنبال کنید
Definition
↓
Value
↓
Use
↓
New Value
↓
Next Use
یه مثال کمی واقعیتر
فرض کنیم این تابع رو داریم
C++
int verify(int input) {
int x = input ^ 0x55;
int y = x + 0x1234;
int noise = 777;
noise *= 4;
int z = y ^ 0xAA;
return z == 0x5678;
}
اینجا هدف ما فهمیدن شرط اخره
lua
return z == 0x5678;
پس از
return به عقب برمیگردیماول
zبعد
yبعد
xو در اخر
inputمسیر میشه
return
↑
comparison
↑
z
↑
y
↑
x
↑
input
اگر محاسبات رو هم اضافه کنیم
input
↓
XOR 0x55
↓
+ 0x1234
↓
XOR 0xAA
↓
compare 0x5678
↓
return
ولی این قسمت
noise = 777
noise *= 4
هیچ ارتباطی با
z و شرط نهایی ندارهپس برای تحلیل این شرط فعلا میتونه خارج از Slice ما قرار بگیره
چرا این موضوع برای ما مهمه
چون توی برنامه واقعی ممکنه یه تابع صدها یا حتی هزاران Instruction داشته باشه
ولی چیزی که واقعا دنبالش هستیم ممکنه فقط این باشه
Input
↓
Transformation
↓
Comparison
↓
Decision
Slicing
کمک میکنه همین مسیر رو از بین حجم زیادی از کد جدا کنیم
مخصوصا وقتی با چیزهایی مثل این روبهرو میشیم
Obfuscation
Junk Code
Function Complexity
Chain of Calculations
Multiple Conditions
ممکنه ظاهر تابع خیلی شلوغ باشه
ولی وقتی Data Flow رو دنبال کنیم بفهمیم فقط چند دستور واقعا روی نتیجهای که برامون مهمه تاثیر دارن
تمرین:
این تابع رو بررسی کنید
int verify(int input) {
int a = input ^ 0x31;
int junk1 = 1234;
junk1 += 55;
int b = a * 7;
int junk2 = 999;
junk2 ^= 0x44;
int c = b - 12;
return c == 0x500;
}
دو تا Slice استخراج کنید
اول Backward Slice از
returnیعنی از اینجا شروع کنید
return
↑
?
↑
?
↑
?
↑
input
بعد Forward Slice رو از
input پیدا کنیدinput
↓
?
↓
?
↓
?
↓
comparison
↓
return
در اخر باید بتونید مسیر اصلی رو تقریبا به این شکل دربیارید
input
↓
XOR 0x31
↓
× 7
↓
- 12
↓
comparison 0x500
↓
return
و این دو قسمت
junk1
junk2
نباید وارد Slice مربوط به این خروجی بشن چون روی نتیجه نهایی تاثیری ندارن
حالا یه مرحله مهمتر هم وجود داره
همین تحلیل رو روی Assembly انجام بدید اونجا دیگه خبری از اسمهای تمیز و قشنگ
input و result نیستباید خودتون Register ها و Memory Location ها رو دنبال کنید و Data Flow رو از روی Instruction ها بازسازی کنید
اینجاست که تفاوت بین فقط خوندن Decompiler و واقعا تحلیل کردن باینری کم کم مشخص میشه
@reverseengine
ReverseEngineering
Program Slicing با Ghidra تا اینجا Backward Slicing و Forward Slicing رو یاد گرفتیم حالا وقتشه همین مفاهیم رو روی یه باینری واقعی اجرا کنیم هدف اینه که وقتی یه تابع شلوغ و پر از متغیر و دستور دیدیم لازم نباشه کل تابع رو زیر و رو کنیم فقط مسیر دادهای که…
Program Slicing
With Ghidra
So far we have learned Backward Slicing and Forward Slicing
Now it is time to implement these concepts on a real binary
The goal is that when we see a busy function full of variables and instructions, we do not need to go through the entire function
We just isolate the data path that is important to us and follow that
A simple program to test
For example, we have this code
C
#include <stdio.h>
int check(int input) {
int a = input + 5;
int b = a * 3;
int junk = 900;
junk ^= 123;
int result = b - 7;
return result;
}
int main() {
int x = 10;
printf("%d\n", check(x));
return 0;
}
`
If input is 10, the calculations go like this
input = 10
↓
a = 10 + 5 = 15
↓
b = 15 × 3 = 45
↓
result = 45 - 7 = 38
Finally, return returns the value 38
But we also have another variable in the middle
junk = 900
junk ^= 123
This value has no effect on the output
And this is exactly what we want to find with Program Slicing
We import the binary into Ghidra
First, we compile the program and open the executable file in Ghidra
Then we go to the function
check
Here, two parts are very important for us
Decompiler
Function Graph
Decompiler
Helps us to understand the general logic of the function more easily
Function Graph
also helps to see the different paths of function execution and the relationship between Basic Blocks
But be careful
Decompiler
Itself is the absolute truth No
What we see is Ghidra reconstruction from binary
For serious analysis, we need to go back to Assembly and examine the Data Flow itself
First we find the Return Value
Suppose the Decompiler shows something like this
C
int check(int input)
{
int a;
int b;
int junk;
int result;
a = input + 5;
b = a * 3;
junk = 900;
junk = junk ^ 123;
result = b - 7;
return result;
}
Now the main question is
What things affect return
Here we start with Backward Slicing
We go back from Return
The last part is
lua
return result;
So the first thing we need to check is result
result is created here
result = b - 7;
So our dependency is now
return
↑
result
↑
b
Now we ask where b came from
b = a * 3;
So
return
↑
result
↑
b
↑
a
Now a
a = input + 5;
As a result, we get
return
↑
result
↑
b
↑
a
↑
input
If we also put the calculations in the path
input
↓
+5
↓
×3
↓
-7
↓
return
So the Slice related to the output of this program is almost the same path
Now let's examine the Junk Code
We have this part
C
int junk = 900;
junk ^= 123;
But nowhere is the result of junk included in the result calculation
So its path is like this
junk
↓
junk ^ 123
↓
X
And there is no path to result
As a result, when our target
is
return
junk
it is not included in the Backward Slice related to return
This is where we see why Slicing is useful
Instead of considering all the function instructions as equal
We only keep the instructions that depend on the data we want
Let's do the same thing in Assembly
Assuming the compiler has converted the function to something like this
mov eax, edi
add eax, 5
imul eax, 3
sub eax, 7
ret
Now we look from the bottom up
The ret instruction returns the return value of the function
In this example, the output value is in EAX
So we go to the previous instruction
sub eax, 7
That is, the EAX value still affects the output
Before that
imul eax, 3
Again, the same value We have EAX which goes into the next calculation
add eax, 5
Again EAX is part of the data path
And finally
mov eax, edi
Here it is clear that the initial value of EDI has entered this path
So our Slice in Assembly becomes
EDI
↓
EAX
↓
EAX + 5
↓
EAX × 3
↓
EAX - 7
↓
RET
Here we are following the real Data Flow at the Instruction level
A very important point
In real binary, there is usually no more names like
input
result
a
b
You may only see things like
RAX
RBX
RCX
RDX
[rbp-0x20]
[rbp-0x18]
This is where our work begins
We have to reconstruct the relationship between these data ourselves
For example
mov eax, [rbp-20h]
add eax, 5
imul eax, 3
mov [rbp-18h], eax
We can mentally convert this path to
With Ghidra
So far we have learned Backward Slicing and Forward Slicing
Now it is time to implement these concepts on a real binary
The goal is that when we see a busy function full of variables and instructions, we do not need to go through the entire function
We just isolate the data path that is important to us and follow that
A simple program to test
For example, we have this code
C
#include <stdio.h>
int check(int input) {
int a = input + 5;
int b = a * 3;
int junk = 900;
junk ^= 123;
int result = b - 7;
return result;
}
int main() {
int x = 10;
printf("%d\n", check(x));
return 0;
}
`
If input is 10, the calculations go like this
input = 10
↓
a = 10 + 5 = 15
↓
b = 15 × 3 = 45
↓
result = 45 - 7 = 38
Finally, return returns the value 38
But we also have another variable in the middle
junk = 900
junk ^= 123
This value has no effect on the output
And this is exactly what we want to find with Program Slicing
We import the binary into Ghidra
First, we compile the program and open the executable file in Ghidra
Then we go to the function
check
Here, two parts are very important for us
Decompiler
Function Graph
Decompiler
Helps us to understand the general logic of the function more easily
Function Graph
also helps to see the different paths of function execution and the relationship between Basic Blocks
But be careful
Decompiler
Itself is the absolute truth No
What we see is Ghidra reconstruction from binary
For serious analysis, we need to go back to Assembly and examine the Data Flow itself
First we find the Return Value
Suppose the Decompiler shows something like this
C
int check(int input)
{
int a;
int b;
int junk;
int result;
a = input + 5;
b = a * 3;
junk = 900;
junk = junk ^ 123;
result = b - 7;
return result;
}
Now the main question is
What things affect return
Here we start with Backward Slicing
We go back from Return
The last part is
lua
return result;
So the first thing we need to check is result
result is created here
result = b - 7;
So our dependency is now
return
↑
result
↑
b
Now we ask where b came from
b = a * 3;
So
return
↑
result
↑
b
↑
a
Now a
a = input + 5;
As a result, we get
return
↑
result
↑
b
↑
a
↑
input
If we also put the calculations in the path
input
↓
+5
↓
×3
↓
-7
↓
return
So the Slice related to the output of this program is almost the same path
Now let's examine the Junk Code
We have this part
C
int junk = 900;
junk ^= 123;
But nowhere is the result of junk included in the result calculation
So its path is like this
junk
↓
junk ^ 123
↓
X
And there is no path to result
As a result, when our target
is
return
junk
it is not included in the Backward Slice related to return
This is where we see why Slicing is useful
Instead of considering all the function instructions as equal
We only keep the instructions that depend on the data we want
Let's do the same thing in Assembly
Assuming the compiler has converted the function to something like this
mov eax, edi
add eax, 5
imul eax, 3
sub eax, 7
ret
Now we look from the bottom up
The ret instruction returns the return value of the function
In this example, the output value is in EAX
So we go to the previous instruction
sub eax, 7
That is, the EAX value still affects the output
Before that
imul eax, 3
Again, the same value We have EAX which goes into the next calculation
add eax, 5
Again EAX is part of the data path
And finally
mov eax, edi
Here it is clear that the initial value of EDI has entered this path
So our Slice in Assembly becomes
EDI
↓
EAX
↓
EAX + 5
↓
EAX × 3
↓
EAX - 7
↓
RET
Here we are following the real Data Flow at the Instruction level
A very important point
In real binary, there is usually no more names like
input
result
a
b
You may only see things like
RAX
RBX
RCX
RDX
[rbp-0x20]
[rbp-0x18]
This is where our work begins
We have to reconstruct the relationship between these data ourselves
For example
mov eax, [rbp-20h]
add eax, 5
imul eax, 3
mov [rbp-18h], eax
We can mentally convert this path to
[rbp-20h]
↓
+5
↓
×3
↓
[rbp-18h]
Now we need to see where [rbp-18h] is used later
If we end up with something like this
mov eax, [rbp-18h]
ret
We understand that this value is part of the data path that leads to the function output
Combine Forward and Backward
Here's where things get more interesting
Assume the function input is in EDI
We start from the input
Forward Slicing
EDI
↓
EAX
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
Now on the other hand we go back from RETURN
Backward Slicing
RETURN
↑
SUB
↑
IMUL
↑
ADD
↑
EAX
↑
EDI
If these two analyses reach a common path
We are more confident that we found the main Data Flow path correctly
EDI
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
What to look for in Ghidra
When you are doing Program Slicing on a real function, there are a few things that are very important
Decompiler
Function Graph
References
Register Usage
Memory References
Variable Definitions
Variable Uses
But the two most important questions are
Where is this value created
And
Where is this value used next
These two questions seem simple, but they are the basis of a lot of serious Reverse Engineering analysis
Every time you see a Register or Memory Location, you should follow this relationship
Definition
↓
Value
↓
Use
↓
New Value
↓
Next Use
A slightly more realistic example
Let's say we have this function
C++
int verify(int input) {
int x = input ^ 0x55;
int y = x + 0x1234;
int noise = 777;
noise *= 4;
int z = y ^ 0xAA;
return z == 0x5678;
}
Here our goal is to understand the last condition
lua
return z == 0x5678;
After return, we go back
first z
then y
then x
and finally input
the path
return
↑
comparison
↑
z
↑
y
↑
x
↑
input
If we add the calculations
input
↓
XOR 0x55
↓
+ 0x1234
↓
XOR 0xAA
↓
compare 0x5678
↓
return
But this part
noise = 777
noise *= 4
has nothing to do with z and the final condition
So for the analysis of this condition, it can be outside our Slice for now
Why is this important to us
Because in a real program, a function may have hundreds or even thousands of instructions
But what we are really looking for may be just this
Input
↓
Transformation
↓
Comparison
↓
Decision
Slicing
It helps to separate this path from a large amount of code
Especially when dealing with things like this We will
Obfuscation
Junk Code
Function Complexity
Chain of Calculations
Multiple Conditions
The function may look very busy
But when we follow the Data Flow, we will find that only a few instructions actually affect the result that matters to us
Exercise:
Examine this function
int verify(int input) {
int a = input ^ 0x31;
int junk1 = 1234;
junk1 += 55;
int b = a * 7;
int junk2 = 999;
junk2 ^= 0x44;
int c = b - 12;
return c == 0x500;
}
Extract two slices
First, the Backward Slice from return
That is, start here
return
↑
?
↑
?
↑
?
↑
input
Then find the Forward Slice from input
input
↓
?
↓
?
↓
?
↓
comparison
↓
return
In the end, you should be able to get the main path to look something like this
input
↓
XOR 0x31
↓
× 7
↓
- 12
↓
comparison 0x500
↓
return
And these two parts
junk1
junk2
should not go into the Slice for this output because they have no effect on the final result
Now there is a more important step
Do the same analysis on Assembly, there is no more clean and beautiful input and result names
You have to follow the Registers and Memory Locations yourself and reconstruct the Data Flow from the Instructions
This is where the difference between just reading the Decompiler and really analyzing the binary becomes clear
@reverseengine
↓
+5
↓
×3
↓
[rbp-18h]
Now we need to see where [rbp-18h] is used later
If we end up with something like this
mov eax, [rbp-18h]
ret
We understand that this value is part of the data path that leads to the function output
Combine Forward and Backward
Here's where things get more interesting
Assume the function input is in EDI
We start from the input
Forward Slicing
EDI
↓
EAX
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
Now on the other hand we go back from RETURN
Backward Slicing
RETURN
↑
SUB
↑
IMUL
↑
ADD
↑
EAX
↑
EDI
If these two analyses reach a common path
We are more confident that we found the main Data Flow path correctly
EDI
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
What to look for in Ghidra
When you are doing Program Slicing on a real function, there are a few things that are very important
Decompiler
Function Graph
References
Register Usage
Memory References
Variable Definitions
Variable Uses
But the two most important questions are
Where is this value created
And
Where is this value used next
These two questions seem simple, but they are the basis of a lot of serious Reverse Engineering analysis
Every time you see a Register or Memory Location, you should follow this relationship
Definition
↓
Value
↓
Use
↓
New Value
↓
Next Use
A slightly more realistic example
Let's say we have this function
C++
int verify(int input) {
int x = input ^ 0x55;
int y = x + 0x1234;
int noise = 777;
noise *= 4;
int z = y ^ 0xAA;
return z == 0x5678;
}
Here our goal is to understand the last condition
lua
return z == 0x5678;
After return, we go back
first z
then y
then x
and finally input
the path
return
↑
comparison
↑
z
↑
y
↑
x
↑
input
If we add the calculations
input
↓
XOR 0x55
↓
+ 0x1234
↓
XOR 0xAA
↓
compare 0x5678
↓
return
But this part
noise = 777
noise *= 4
has nothing to do with z and the final condition
So for the analysis of this condition, it can be outside our Slice for now
Why is this important to us
Because in a real program, a function may have hundreds or even thousands of instructions
But what we are really looking for may be just this
Input
↓
Transformation
↓
Comparison
↓
Decision
Slicing
It helps to separate this path from a large amount of code
Especially when dealing with things like this We will
Obfuscation
Junk Code
Function Complexity
Chain of Calculations
Multiple Conditions
The function may look very busy
But when we follow the Data Flow, we will find that only a few instructions actually affect the result that matters to us
Exercise:
Examine this function
int verify(int input) {
int a = input ^ 0x31;
int junk1 = 1234;
junk1 += 55;
int b = a * 7;
int junk2 = 999;
junk2 ^= 0x44;
int c = b - 12;
return c == 0x500;
}
Extract two slices
First, the Backward Slice from return
That is, start here
return
↑
?
↑
?
↑
?
↑
input
Then find the Forward Slice from input
input
↓
?
↓
?
↓
?
↓
comparison
↓
return
In the end, you should be able to get the main path to look something like this
input
↓
XOR 0x31
↓
× 7
↓
- 12
↓
comparison 0x500
↓
return
And these two parts
junk1
junk2
should not go into the Slice for this output because they have no effect on the final result
Now there is a more important step
Do the same analysis on Assembly, there is no more clean and beautiful input and result names
You have to follow the Registers and Memory Locations yourself and reconstruct the Data Flow from the Instructions
This is where the difference between just reading the Decompiler and really analyzing the binary becomes clear
@reverseengine
System Call
برنامه چجور با Kernel حرف میزنه؟
تا اینجا با چیزهایی مثل:
lua
fork()
exec()
wait()
exit()
کار کردیم
اما یک سوال مهم وجود داره
این توابع چطور میتونن از Kernel درخواست انجام یک کار بکنن؟
مثلا برنامهای که داخل User Space اجرا میشه چطور میتونه:
فایل باز کنه؟
داده بخوانه؟
Process
جدید بسازه؟
حافظه بگیره؟
Process
خودش رو تموم کنه؟
جواب اصلی:
System Call
User Mode و Kernel Mode
CPU
معمولا اجرای برنامه رو در سطح دسترسی محدودی انجام میدهد که به اون:
User Mode
میگیم
برنامه در این حالت نمیتونه مستقیما هر کاری که دلش خواست با سیستم انجام بده
مثلا نمیتونه مستقیما:
Hardware
Kernel Memory
Page Tables
Device Control
رو دستکاری کنه
چون اگر هر برنامهای چنین دسترسیای داشت یک برنامه خرابکار میتونست کل سیستم رو به فنا بده😁
و ادم ها قبلا به اندازه کافی راه های دیگه ای برای این کار پیدا کردن
Kernel Mode
Kernel
در سطح دسترسی بالاتری اجرا میشه:
Kernel Mode
در این حالت Kernel میتونه به منابع حساس سیستم دسترسی داشته باشه
پس ساختار کلی تقریبا این شکلیه:
User Space
│
│ System Call
↓
Kernel Space
│
↓
Hardware / Resources
برنامه مستقیم وارد Kernel نمیشه
بلکه از یک دروازه کنترل شده استفاده میکنه این دروازه همون System Call هست
یک مثال ساده
فرض کنیم برنامه میخاد از یک فایل بخونه
در سطح برنامه ممکنه چیزی شبیه این داشته باشیم:
read(fd, buffer, size);
برنامه درخواست میکنه:
من میخاهم از این File Descriptor
به اندازه مشخصی داده بخونم
در اخر درخواست باید به Kernel برسه
Kernel
بررسی میکنه:
این Process چه کسیه؟
اجازه دسترسی داره؟
fd
معتبره؟
آدرس buffer معتبره؟
چه مقدار داده باید خونده بشه؟
بعد عملیات رو انجام میده و نتیجه رو برمیگردونه
مسیر کلی System Call
به شکل ساده:
Program
│
↓
Library / API Wrapper
│
↓
System Call Mechanism
│
↓
CPU → Kernel Mode
│
↓
Kernel
│
↓
Operation
│
↓
Return Value
│
↓
User Mode
یعنی برنامه برای انجام عملیات حساس از یک مسیر مشخص وارد Kernel میشه
System Call با Function Call
یکی نیست
این دو تا رو قاطی نکنیم
یک Function Call معمولی مثلا:
result = add(a, b);
معمولا در همون فضای اجرای برنامه انجام میشه
اما System Call یک عبور از مرز دسترسی User و Kernel ایجاد میکنه
پس:
Function Call
↓
همون فضای اجرا
System Call
↓
User Mode
↓
Kernel Mode
↓
برگشت به User Mode
این تفاوت در Reverse Engineering خیلی مهمه
هم از همین مسیر استفاده میکنن
مثلا در برنامه C مینویسیم:
fork();
اما این به معنی یک Function ساده مثل:
C
int add(int a, int b)
نیست
در سیستمهای Unix-like این درخواست در اخر باید به Kernel برسه تا Kernel بتونه Process جدید ایجاد کنه
همین ایده برای عملیات هایی مثل:
shell
read
write
open
mmap
fork
exec
wait
هم وجود داره
البته یک نکته مهم
چیزی که در کد میبینیم لزوما خود System Call خام نیست ممکنه یک Library Wrapper باشه که در اخر System Call مناسب رو انجام میده
System Call
چطور به Kernel میرسه؟
در معماریهای مدرن CPU مکانیزم مخصوصی برای این انتقال داره
مثلا در x86-64 لینوکس دستور:
syscall
برای ورود به مسیر System Call استفاده میشه
به صورت مفهومی:
User Code
↓
Arguments آماده میشن
↓
System Call Number
↓
syscall
↓
CPU
↓
Kernel Entry
↓
Kernel Handler
Kernel
بعد از ورود درخواست رو بررسی و پردازش میکنه
برای ما چرا مهمه؟
فرض کنید داخل یک Binary این رو ببینید:
syscall
دیگه فقط با یک دستور اسمبلی معمولی طرف نیستید
این دستور میتونه نشون دهنده عبور برنامه از:
User Space
↓
Kernel Space
باشه
از طرف دیگه اگر داخل کد برنامه API هایی مثل:
CreateFile
ReadFile
VirtualAlloc
CreateProcess
رو ببینید باید بدونید این API ها در اخر برای انجام تعداد زیادی از عملیات های سیستمی به مکانیزمهای Kernel وابسته هستن پس در RE باید بتونید این زنجیره رو در ذهنتون ببینید:
Application
↓
Library / API
↓
System Call
↓
Kernel
↓
Resource / Hardware
این دیدگاه بعدا هنگام تحلیل Process ها Memory File System Thread ها و حتی رفتارهای پیچیدهتر Binary ها خیلی به کارتون میاد
برنامه چجور با Kernel حرف میزنه؟
تا اینجا با چیزهایی مثل:
lua
fork()
exec()
wait()
exit()
کار کردیم
اما یک سوال مهم وجود داره
این توابع چطور میتونن از Kernel درخواست انجام یک کار بکنن؟
مثلا برنامهای که داخل User Space اجرا میشه چطور میتونه:
فایل باز کنه؟
داده بخوانه؟
Process
جدید بسازه؟
حافظه بگیره؟
Process
خودش رو تموم کنه؟
جواب اصلی:
System Call
User Mode و Kernel Mode
CPU
معمولا اجرای برنامه رو در سطح دسترسی محدودی انجام میدهد که به اون:
User Mode
میگیم
برنامه در این حالت نمیتونه مستقیما هر کاری که دلش خواست با سیستم انجام بده
مثلا نمیتونه مستقیما:
Hardware
Kernel Memory
Page Tables
Device Control
رو دستکاری کنه
چون اگر هر برنامهای چنین دسترسیای داشت یک برنامه خرابکار میتونست کل سیستم رو به فنا بده😁
و ادم ها قبلا به اندازه کافی راه های دیگه ای برای این کار پیدا کردن
Kernel Mode
Kernel
در سطح دسترسی بالاتری اجرا میشه:
Kernel Mode
در این حالت Kernel میتونه به منابع حساس سیستم دسترسی داشته باشه
پس ساختار کلی تقریبا این شکلیه:
User Space
│
│ System Call
↓
Kernel Space
│
↓
Hardware / Resources
برنامه مستقیم وارد Kernel نمیشه
بلکه از یک دروازه کنترل شده استفاده میکنه این دروازه همون System Call هست
یک مثال ساده
فرض کنیم برنامه میخاد از یک فایل بخونه
در سطح برنامه ممکنه چیزی شبیه این داشته باشیم:
read(fd, buffer, size);
برنامه درخواست میکنه:
من میخاهم از این File Descriptor
به اندازه مشخصی داده بخونم
در اخر درخواست باید به Kernel برسه
Kernel
بررسی میکنه:
این Process چه کسیه؟
اجازه دسترسی داره؟
fd
معتبره؟
آدرس buffer معتبره؟
چه مقدار داده باید خونده بشه؟
بعد عملیات رو انجام میده و نتیجه رو برمیگردونه
مسیر کلی System Call
به شکل ساده:
Program
│
↓
Library / API Wrapper
│
↓
System Call Mechanism
│
↓
CPU → Kernel Mode
│
↓
Kernel
│
↓
Operation
│
↓
Return Value
│
↓
User Mode
یعنی برنامه برای انجام عملیات حساس از یک مسیر مشخص وارد Kernel میشه
System Call با Function Call
یکی نیست
این دو تا رو قاطی نکنیم
یک Function Call معمولی مثلا:
result = add(a, b);
معمولا در همون فضای اجرای برنامه انجام میشه
اما System Call یک عبور از مرز دسترسی User و Kernel ایجاد میکنه
پس:
Function Call
↓
همون فضای اجرا
System Call
↓
User Mode
↓
Kernel Mode
↓
برگشت به User Mode
این تفاوت در Reverse Engineering خیلی مهمه
fork() و wait() هم از همین مسیر استفاده میکنن
مثلا در برنامه C مینویسیم:
fork();
اما این به معنی یک Function ساده مثل:
C
int add(int a, int b)
نیست
در سیستمهای Unix-like این درخواست در اخر باید به Kernel برسه تا Kernel بتونه Process جدید ایجاد کنه
همین ایده برای عملیات هایی مثل:
shell
read
write
open
mmap
fork
exec
wait
هم وجود داره
البته یک نکته مهم
چیزی که در کد میبینیم لزوما خود System Call خام نیست ممکنه یک Library Wrapper باشه که در اخر System Call مناسب رو انجام میده
System Call
چطور به Kernel میرسه؟
در معماریهای مدرن CPU مکانیزم مخصوصی برای این انتقال داره
مثلا در x86-64 لینوکس دستور:
syscall
برای ورود به مسیر System Call استفاده میشه
به صورت مفهومی:
User Code
↓
Arguments آماده میشن
↓
System Call Number
↓
syscall
↓
CPU
↓
Kernel Entry
↓
Kernel Handler
Kernel
بعد از ورود درخواست رو بررسی و پردازش میکنه
برای ما چرا مهمه؟
فرض کنید داخل یک Binary این رو ببینید:
syscall
دیگه فقط با یک دستور اسمبلی معمولی طرف نیستید
این دستور میتونه نشون دهنده عبور برنامه از:
User Space
↓
Kernel Space
باشه
از طرف دیگه اگر داخل کد برنامه API هایی مثل:
CreateFile
ReadFile
VirtualAlloc
CreateProcess
`رو ببینید باید بدونید این API ها در اخر برای انجام تعداد زیادی از عملیات های سیستمی به مکانیزمهای Kernel وابسته هستن پس در RE باید بتونید این زنجیره رو در ذهنتون ببینید:
Application
↓
Library / API
↓
System Call
↓
Kernel
↓
Resource / Hardware
این دیدگاه بعدا هنگام تحلیل Process ها Memory File System Thread ها و حتی رفتارهای پیچیدهتر Binary ها خیلی به کارتون میاد