Tracking data throughout the program
So far, we have learned how to trace data paths using Forward Slicing and Backward Slicing.
Now, let's take it a step further.
Taint Analysis
This involves marking a specific piece of data and then observing where it travels within the program and what it affects.
Suppose the program receives an input:
C
int input = get_input();
We consider
input to be "Tainted"—meaning, for the time being, we assume:
input = TAINTED
Now, if this value enters a calculation:
int x = input + 10;
Then:
input
↓
x
x is also considered Tainted.If we subsequently have:
C
int y = x * 5;
The path becomes:
input
↓
x
↓
y
Thus,
y is also influenced by the initial input.Now, if we have:
C
int result = y ^ 0x55;
The path becomes:
input
↓
x
↓
y
↓
result
And if, finally, we have:
if (result == 100)
We realize that the initial input has influenced this condition.
This is one of the key applications of Taint Analysis.
A simple example:
C
int check(int input)
{
int x = input ^ 0x55;
int y = x + 10;
int temp = 500;
temp *= 20;
if (y == 100)
return 1;
return 0; }
We mark the input as "Tainted."
So, its path becomes:
input
↓
x
↓
y
↓
comparison
However, this part:
temp
↓
temp * 20
...is not connected to the final condition.
Therefore, the taint is not propagated to it;
meaning
temp remains "Clean."Taint in Assembly:
When we delve into reverse engineering, we usually don't have a variable explicitly named
input.We might see something like this:
mov eax, [rbp-20h]
xor eax, 55h
add eax, 10
cmp eax, 100
Let's assume:
[rbp-20h] = input
So, the data path becomes:
[rbp-20h]
↓
EAX
↓
XOR 55h
↓
ADD 10
↓
CMP 100
Here, we can see that the input enters the calculations directly and eventually reaches the comparison.
Taint analysis isn't limited to user input;
any data can serve as a taint starting point.
For example:
PowerShell
File Data
Network Data
Configuration
Function Argument
Registry Value
Environment Variable
For instance, if a function takes this:
C
void process(char *data)
We can designate
data as the starting pointand then trace which functions this data enters as the program proceeds.
The path might look like this:
Input
↓
Parser
↓
Transformation
↓
Function A
↓
Function B
↓
Comparison
Or even:
Input
↓
Buffer
↓
Memory
↓
Function
↓
Output
An important point:
Taint analysis
does not necessarily imply that the exact initial value remains unchanged;
the value might be modified multiple times.
For example:
Input
↓
XOR
↓
ADD
↓
SHIFT
↓
SUB
↓
Comparison
What we track is whether the new value still depends on the initial data.
In other words, the core question is:
Is this value influenced by the tainted data? ...is situated...
It is not the case that...
Is the value exactly the same as the initial value?
We call the transfer of a "taint" from one value to another "Taint Propagation."
For example:
a = input;
b = a + 5;
c = b ^ 0x44;
The path looks like this:
input
↓
a
↓
b
↓
c
This means the taint moves along with the data.
However, if we have:
C++
int x = 100;
int y = x + 20;
Since
x did not come from input:x = CLEAN
y = CLEAN
So, the taint does not enter this path.
Now, for a more realistic example: suppose we have a complex function:
Function
│
├── Input
├── Calculation A
├── Junk Code
├── Function B
├── Calculation C
├── Comparison
└── Return
If we mark the input as "tainted," analysis might lead us to this conclusion:
Input
↓
Calculation A
↓
Function B
↓
Calculation C
↓
Comparison
...and the "Junk Code" section might not be part of the taint path at all.
This is precisely where we see why Taint Analysis is so powerful when combined with Slicing.
Slicing tells us:
What elements are related to this specific point?
Taint Analysis tells us:
Where this specific piece of data went and what it affected.
The two concepts are closely related but not exactly the same.
Exercise:
Examine this function: