[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
Post #1769
50