Here RBX may be a Pointer to an Object that is given as an Argument to process_user
Calling Convention
is also very important
For example, in System V x86-64, the initial arguments usually pass through these registers:
RDI
RSI
RDX
RCX
R8
R9
But in Windows x64, the first four arguments usually pass through:
RCX
RDX
R8
R9
So, for example, in a Windows binary:
mov eax, [rcx+0x8]
It can be an important clue that RCX is a Pointer to an Object or Struct input to the function
Of course, we still need to check the Context of the function
Guess its type based on how the Field is used
Suppose we have:
mov eax, [rcx+0x4]
cmp eax, 10
This can create this possibility:
+0x4 = integer-like field
Now:
mov rax, [rcx+0x8]
mov rdx, [rax]
Here the probability of +0x8 being a Pointer is more
Or:
mov rax, [rcx+0x10]
call rax
Here the probability of +0x10 being a Function Pointer is raised
Or:
mov rax, [rcx+0x18]
lea rdx, [rax+...]
It is possible that +0x18 is a Pointer to a Buffer or another Object
But an important point:
None of these alone are definitive proof
We must also examine the instructions around the Cross-References, other functions, and the Data Flow path
Nested Structs
A Struct
can be placed inside another Struct
For example:
struct Address {
int city;
int zip;
};
struct User {
int id;
struct Address address;
};
Now we may see in Binary:
mov eax, [rcx+0x8]
And elsewhere:
mov eax, [rcx+0xC]
If the Context is appropriate, we may conclude that:
User
|
+0x00 id
|
+0x08 address
|
+0x00 city
+0x04 zip
That is:
User + 0x08 = Address
User + 0x0C = Address.zip
In Source Code we may see very easily:
user->address.zip
But in Binary such a name usually does not exist.
What we see might just be:
mov eax, [rcx+0xC]
So we have to rebuild the logical structure of the program based on the offsets and how the data is used
Don't forget about Padding and Alignment
A common mistake is to think that Struct fields are always exactly one after the other
For example:
struct Example {
char flag;
void *ptr;
};
On a typical 64-bit ABI Layout might look something like this:
+0x00 flag 1 byte
+0x01 padding
+0x02 padding
+0x03 padding
+0x04 padding
+0x05 padding
+0x06 padding
+0x07 padding
+0x08 ptr 8 bytes
Why?
Because Pointer usually needs its own alignment
So we should not think:
char
pointer
That means Pointer must start at +0x01
Compiler may put padding between Fields
So when you reconstruct Offsets always consider these two things:
Alignment
Padding
Arrays are a very good clue
Suppose we see this in Binary:
mov eax, [rcx+rax*4]
Here we are almost seeing this Address Calculation model:
base + index * 4
This can show that we are dealing with an Array of 4-byte elements
For example:
Element 0 → +0x00
Element 1 → +0x04
Element 2 → +0x08
Element 3 → +0x0C
Now if we see:
mov rax, [rcx+rax*8]
Each Element is probably eight bytes apart
But if Let's see:
mov eax, [rcx+rax*0x20]
We can guess that the distance between the start of the Elements is approximately:
0x20 bytes
.
We call this distance:
Stride
That is, the distance from the start of one Element to the start of the next Element
For example:
Element 0
|
| 0x20
v
Element 1
|
| 0x20
v
Element 2
This is very useful when reverse engineering unknown structures
A more real-world example
Suppose we find this assembly:
mov eax, [rcx+0x4]
test eax, eax
je short loc_140001250
mov rax, [rcx+0x8]
mov rdx, [rax]
call rdx
mov rax, [rcx+0x10]
mov eax, [rax+0x4]
Now what can we understand without the source code?
First:
mov eax, [rcx+0x4]
That is, a 32-bit value is read from Offset 0x4
Then:
test eax, eax
And the result is used for a condition
So we can guess:
+0x4 = integer-like / flag-like field
Next:
mov rax, [rcx+0x8]
A 64-bit value is read from +0x8
Next:
mov rdx, [rax]
That is, the value read is used as an Address
And then:
call rdx
This Address is used for a Call
Post #1847
72