ReverseEngineering
1.33K subscribers
50 photos
11 videos
112 files
916 links
Download Telegram
CPU
↓
I/O
↓
CPU
↓
I/O
↓
CPU

Or the execution time of the program depends on user input

So it is not always possible to know in advance exactly how much CPU each job will need

This is where Scheduling becomes much more complex than a simple queue

9. Why is this important to us?

At first you might think what does this have to do with Reverse Engineering

But when a program has multiple threads, the actual execution of the program does not depend only on the order of the instructions in the binary

For example:

Thread A
↓
Running
↓
Preempted

Simultaneously:

Thread B
↓
Running
↓
Blocked

Then again:

Thread A
↓
Running

That is, the execution of the program can be switched between different threads

This becomes more important when we are dealing with things like:

Multi-threading

Race Condition

Deadlock

Timing Bug

Synchronization

For example, a bug may only occur when two threads reach a specific part of the program almost simultaneously

So we should not just ask:

What does this instruction do

We should ask:

When did this thread run? What thread ran before it? Why did this thread stop and then start again?

This view will be very important later for analyzing concurrency-related bugs

For now, learn these three metrics well:

Turnaround Time

Arrival → Completion

The total time the Job spent in the system from entry to completion

Waiting Time

The waiting time for CPU

How much time the Process spent in the Ready Queue waiting for the CPU

Response Time

Arrival → First Run

How long it took for the Process to get the CPU for the first time.

In summary:

Arrival
│
├─── Waiting ─────┐
│ │
▼ ▼
First Run Running
│ │
│ │
└── Response │
│
▼
Completion

Turnaround = Arrival → Completion

So when we want to compare two scheduling algorithms, we don't just say:

Which one is faster?

We need to see:

How long did we wait?

How fast did we get the first response?

How long did it take to complete the task?

These are the metrics that help us understand how well a Scheduler really performs

@reverseengine
قسمت سی و سوم بافر اورفلو


Taint Analysis on Binary

در Assembly از کجا بفهمیم داده کجا میره

میخوایم Taint Analysis رو روی باینری اجرا کنیم
اینجا دیگه خبری از اسم متغیر های مثل input و buffer نیست
باید با Registerها و ادرس‌های حافظه کار کنیم


پیدا کردن Source
اول باید بفهمیم داده از کجا وارد برنامه شده مثلا داخل یک برنامه لینوکسی ممکنه به این برسیم

call read

یا

call recv

فرض کنید قبلش اینو داریم

lea rsi,[rbp-40h]
mov edx,40h
call read

اینجا میتونیم مسیر ساده‌ای داشته باشیم

External Input
↓
read()
↓
[rbp-40h]

پس داده ورودی داخل [rbp-40h] قرار میگیره
این نقطه میتونه Source ما باشه


دنبال کردن داده

حالا چند دستور پایین‌ تر میبینیم

mov rax,[rbp-40h]
mov rcx,rax

پس مقدار از حافظه اومده داخل RAX
بعد وارد RCX شده
مسیر فعلا اینه

[rbp-40h]
↓
RAX
↓
RCX


دنبال کردن انتقال داده

فرض کنید بعدش اینو ببینیم

mov rdx,rcx
call process_data

حالا

[rbp-40h]
↓
RAX
↓
RCX
↓
RDX
↓
process_data()

یعنی داده‌ای که از بیرون اومده وارد process_data شده
حالا باید داخل اون تابع ادامه مسیر رو دنبال کنیم



رسیدن به Sink
فرض کنید داخل تابع به این برسیم

mov [rdi],rax

اینجا یک Memory Write داریم
پس مسیر کلی ما میشه

Input
↓
read()
↓
Stack / Memory
↓
Register
↓
Function
↓
Memory Write

این نقطه میتونه Sink موردنظر ما باشه
البته رسیدن داده به یک Write به تنهایی به معنی وجود آسیب‌ پذیری نیست
باید اندازه و محدوده دسترسی هم بررسی بشه

Taint
کردن Register ها

فرض کنید

mov rax,[rbp-40h]
و [rbp-40h] شامل داده خارجی باشه
پس
RAX = Tainted
بعد
mov rcx,rax
حالا
RCX = Tainted
بعد
add rcx,8

مقدار RCX تغییر کرده ولی همچنان به داده اولیه وابسته است
پس مسیر Taint قطع نشده
اما یک نکته مهم
همه دستورها باعث انتقال ساده Taint نمیشن

مثلا:
cmp rax,10h
اینجا داده مستقیما به Register دیگری منتقل نشده
ولی مقدار RAX روی تصمیم برنامه تاثیر گذاشته

مثلا:

cmp rax,10h
ja invalid

پس اینجا یک Control Dependency داریم یعنی داده خارجی روی مسیر اجرای برنامه اثر گذاشته

یک مثال کامل تر:
lea rsi,[rbp-40h]
mov edx,40h
call read

movzx eax,byte ptr [rbp-40h]

cmp eax,20h
ja invalid

mov edi,eax
call process_data

مسیر رو مرحله به مرحله ببینید

read()
↓
[rbp-40h]
↓
EAX
↓
cmp
↓
Control Flow
↓
process_data()

اینجا مقدار ورودی هم روی داده تاثیر گذاشته
هم روی مسیر اجرای برنامه

در Ghidra چه کار میکنیم
وقتی به یک مقدار مشکوک رسیدیم
اول References اون مقدار رو بررسی میکنیم بعد استفاده‌های بعدی رو پیدا میکنیم

مثلا:
Source
↓
Reference
↓
Register
↓
Memory Access
↓
Function Call

بعد همین کار رو برای تابع بعدی ادامه میدیم تا مسیر کامل مشخص بشه یک نکته مهم لازم نیست همیشه کل Taint Analysis رو دستی انجام بدید
در برنامه‌ های بزرگ این کار خیلی سخت میشه برای همین ابزارهای مختلفی وجود دارن که میتونن این کار رو تا حدی خودکار کنن

مثلا:
angr
Triton
DynamoRIO
Intel Pin

هرکدوم روش و کاربرد خودشون رو دارن

@reverseengine
ReverseEngineering
قسمت سی و سوم بافر اورفلو Taint Analysis on Binary در Assembly از کجا بفهمیم داده کجا میره میخوایم Taint Analysis رو روی باینری اجرا کنیم اینجا دیگه خبری از اسم متغیر های مثل input و buffer نیست باید با Registerها و ادرس‌های حافظه کار کنیم پیدا کردن…
Part 33 Buffer Overflow


Taint Analysis on Binary

In Assembly, how do we know where the data goes

We want to run Taint Analysis on binary
Here, there is no more mention of variable names like input and buffer

We have to work with registers and memory addresses

Finding the Source
First, we need to find out where the data entered the program from, for example, in a Linux program, we might get this

call read

or

call recv

Suppose we have this before

lea rsi,[rbp-40h]

mov edx,40h
call read

Here we can have a simple path

External Input
↓
read()
↓
[rbp-40h]

So the input data is placed in [rbp-40h]

This point can be our Source

Following the data

Now we see a few commands below

mov rax,[rbp-40h]

mov rcx,rax

So the value came from memory Inside RAX
Then entered RCX
The current path is

[rbp-40h]
↓
RAX
↓
RCX

Following data transfer

Suppose we see this later

mov rdx,rcx
call process_data

Now

[rbp-40h]
↓
RAX
↓
RCX
↓
RDX
↓
process_data()

That is, the data that came from outside entered process_data
Now we have to continue the path inside that function

Reaching Sink
Suppose we reach this inside the function

mov [rdi],rax

Here we have a Memory Write
So our general path becomes

Input
↓
read()
↓
Stack / Memory
↓
Register
↓
Function
↓
Memory Write

This point can be our desired Sink
Of course, reaching a Write alone does not mean there is a vulnerability
The size and access range should also be checked

Taint
Registers

Assuming

mov rax,[rbp-40h]

and [rbp-40h] contains external data

so
RAX = Tainted

then
mov rcx,rax

now
RCX = Tainted

then
add rcx,8

The RCX value has changed but is still dependent on the original data

so the Taint path is not broken

but one important point

not all instructions cause a simple Taint transfer

for example:

cmp rax,10h

here the data is not directly transferred to another register

but the RAX value affects the program's decision

for example:

cmp rax,10h
ja invalid

so here we have a Control Dependency, that is, the external data affects the program's execution path

A more complete example:

lea rsi,[rbp-40h]

mov edx,40h
call read

movzx eax,byte ptr [rbp-40h]

cmp eax,20h
ja invalid

mov edi,eax
call process_data

See the path step by step

read()
↓
[rbp-40h]
↓
EAX
↓
cmp
↓
Control Flow
↓
process_data()

Here the input value affects both the data and the execution path

What do we do in Ghidra
When we reach a suspicious value
First we check the References of that value, then we find the next uses

For example:
Source
↓
Reference
↓
Register
↓
Memory Access
↓
Function Call

Then we continue the same for the next function to determine the full path. One important point is that you don't always have to do the entire Taint Analysis manually
In large programs, this can be very difficult, so there are various tools that can automate this task to some extent

For example:

angr
Triton
DynamoRIO
Intel Pin

Each has its own method and application

@reverseengine
Memory Allocation
و تغییر RW → RX

تا اینجا درباره‌ی Call Stack گفتیم حالا یک لایه پایین‌ تر میریم سراغ خود حافظه

یکی از چیزهایی که EDR ها در تحلیل رفتار Processها زیر نظر میگیرن

Permission
حافظه
و تغییراتشه

اصلا RW و RX یعنی چی؟

هر بخش از حافظه میتونه Permission های مختلفی داشته باشه:

R = Read
W = Write
X = Execute

مثلا:

RW

یعنی:

Read + Write

برنامه میتونه اطلاعات داخل این قسمت رو بخونه و تغییر بده ولی قرار نیست از همون قسمت کد اجرا کنه

یا:

RX

یعنی:

Read + Execute

میتونه اطلاعات رو بخونه و از اون قسمت دستورهای CPU رو اجرا کنه ولی نباید بتونه محتوای اون صفحه رو تغییر بده

به‌صورت مفهومی ممکنه چنین زنجیره‌ای ببینیم:

Memory Allocation
↓
RW
↓
Write Data
↓
Permission Change
↓
RX
↓
Execution

یعنی یک بخش از حافظه اول قابل نوشتن بوده و بعد Permission اون به حالتی تغییر کرده که قابلیت Execute داشته باشه

همین تغییر به‌تنهایی به معنی Malware بودن نیست مثلا بعضی نرم‌افزارهای کاملا legitimate هم به دلایل مختلف کد تولید یا کامپایل میکنن

JIT
یکی از مثال‌های مهمه

بعضی Runtime ها از تکنولوژی‌ به اسم:

JIT = Just-In-Time Compilation

استفاده میکنن اینجا برنامه ممکنه در زمان اجرا بخشی از کد رو تولید کنه و برای اجرای اون نیاز به حافظه‌ی مناسب داشته باشه

پس:

RW → RX

میتونه در یک نرم‌ افزار سالم هم اتفاق بیفته

بنابراین یک EDR خیلی خوب نباید بگه:

RW → RX = Malware

چون این مدل منطق تقریبا همونقدر درسته که بگیم هر کسی هودی پوشید هکره 😂

چیزی که مهمه Context هست

EDR
معمولا قط یک Event رو نگاه نمیکنه
مثلا فرض کنید چنین زنجیره‌ای وجود داشته باشه:

Process
↓
Memory Allocation
↓
Write
↓
Protection Change
↓
RX Memory
↓
Thread Activity
↓
Call Stack
↓
Network Activity

هرکدوم به‌ تنهایی ممکنه کاملا عادی باشه
ولی وقتی چند رفتار غیرمعمول کنار هم قرار میگیرن ارزش تحلیلی بیشتری پیدا میکنن

به همین ددلیل Detection مدرن بیشتر شبیه اینه:

Event
+
Context
+
Process History
+
Memory Information
+
Thread Information
+
Call Stack


RWX
بعضی وقتا ممکنه Permission یک بخش به شکل زیر باشه:

RWX

یعنی:

Read
Write
Execute

در این حالت همون Memory Region هم قابل تغییر و هم قابل اجراست

از دید امنیتی این وضعیت معمولا حساس‌ تره چون اصل مهمی به نام:

W^X

وجود داره

یعنی:

Writable XOR Executable


به زبان ساده:

یک Memory Region بهتره همزمان هم Writable و هم Executable نباشه


البته باز هم:

RWX ≠ Malware

و:

RW → RX ≠ Malware

این‌ها فقط Signal هستن نه کار پایانی

DEP
یکی از مکانیزم‌ های مهم Windows:

Data Execution Prevention

هدفش اینه که بخش‌هایی از Memory که نباید کد اجرا کنن قابل Execute نباشن

مثلا اگر یک بخش فقط:

RW

باشه انتظار نداریم CPU از اونجا مثل یک Code Region معمولی دستور اجرا کنه
پس Permission های Memory بخشی از مدل امنیتی سیستم‌ عامل هستن نه صرفا یک عدد داخل Debugger

EDR
دقیقا به چه چیزهایی توجه میکنه؟
برای تحلیل یک Memory Region ممکنه اطلاعاتی مثل اینها اهمیت داشته باشه:


Process
├── Memory Type
├── Protection
├── Allocation History
├── Thread
├── Thread Start Address
├── Call Stack
├── Loaded Modules
├── Parent Process
└── Process Behavior

مثلا این دو وضعیت از نظر تحلیلی یکی نیستن:

Application
↓
JIT Activity
↓
RW → RX

در مقایسه با:

Unknown Process
↓
Private Memory
↓
Write
↓
RW → RX
↓
Suspicious Thread Activity

EDR
دقیقا به همین تفاوت Context اهمیت میده


نکته مهم
وقتی داخل Debugger یا Memory Viewer یک Region میبینید فقط به Address نگاه نکنید

این موارد رو هم بررسی کنید:

Address
Size
Protection
Type
State
Backing
Owner Process
Thread Relation

مثلا:

0x000001A...
Size: 0x5000
Protection: RX
Type: ...
State: ...

این اطلاعات کمک میکنن بفهمید با چه نوع Memory Region طرف هستید


پس فعلا این مدل ذهنی رو داشته باشید:

Memory
│
├── R
├── W
└── X

و یک تغییر Permission به‌ تنهایی چیزی رو ثابت نمیکنه
چیزی که برای تحلیل مهم‌ تره اینه:

Memory
+
Permission Change
+
Thread
+
Call Stack
+
Process Context
+
Behavior

@reverseengine
ReverseEngineering
Memory Allocation و تغییر RW → RX تا اینجا درباره‌ی Call Stack گفتیم حالا یک لایه پایین‌ تر میریم سراغ خود حافظه یکی از چیزهایی که EDR ها در تحلیل رفتار Processها زیر نظر میگیرن Permission حافظه و تغییراتشه اصلا RW و RX یعنی چی؟ هر بخش از حافظه میتونه…
Memory Allocation and RW → RX Transitions

We have discussed the call stack; now, let’s go a layer deeper and look at memory itself.

One of the things EDRs monitor when analyzing process behavior is memory permissions and their changes.

What exactly do RW and RX mean? Each memory segment can have different permissions:

R = Read
W = Write
X = Execute

For example:

RW

Means:

Read + Write

The program can read and modify the data in this section but is not intended to execute code from it.

Or:

RX

Means:

Read + Execute

It can read data and execute CPU instructions from that section but should not be able to modify the page's content.

Conceptually, we might see a sequence like this:

Memory Allocation
↓
RW
↓
Write Data
↓
Permission Change
↓
RX
↓
Execution

This means a memory segment was initially writable, and then its permission was changed to allow execution.

This change alone does not imply the presence of malware; for instance, some entirely legitimate software applications generate or compile code for various reasons.

JIT
is a key example.

Some runtimes utilize a technology called:

JIT = Just-In-Time Compilation

Here, the program might generate a portion of code during runtime and require suitable memory to execute it.

So:

RW → RX

can occur even in legitimate software.

Therefore, a high-quality EDR should not simply conclude:

RW → RX = Malware

Because that logic is about as accurate as saying anyone wearing a hoodie is a hacker 😂

What matters is the context.

An EDR
usually doesn't look at just a single event.
For example, imagine a sequence like this:

Process
↓
Memory Allocation
↓
Write
↓
Protection Change
↓
RX Memory
↓
Thread Activity
↓
Call Stack
↓
Network Activity

Each step might be perfectly normal on its own,
but when several unusual behaviors... ...they acquire greater analytical value.

For this reason, modern detection looks more like this:

Event
+
Context
+
Process History
+
Memory Information
+
Thread Information
+
Call Stack

RWX
Sometimes, a memory segment's permissions might look like this:

RWX

Meaning:

Read
Write
Execute

In this case, the memory region is both modifiable and executable.

From a security perspective, this situation is usually more critical because of an important principle known as:

W^X

Meaning:

Writable XOR Executable

Simply put:

It is best for a memory region not to be both Writable and Executable at the same time.

Of course:

RWX ≠ Malware

And:

RW → RX ≠ Malware

These are merely signals, not a definitive conclusion.

DEP
One of Windows' key mechanisms:

Data Execution Prevention

Its goal is to ensure that memory segments not intended to run code cannot be executed.

For instance, if a segment is only:

RW

...we do not expect the CPU to execute instructions from it as it would a standard code region.
Thus, memory permissions are part of the operating system's security model, not just a value within a debugger.

EDR
What exactly does it focus on? When analyzing a memory region, information such as the following can be significant:

Process
├── Memory Type
├── Protection
├── Allocation History
├── Thread
├── Thread Start Address
├── Call Stack
├── Loaded Modules
├── Parent Process
└── Process Behavior

For instance, from an analytical standpoint, these two scenarios are not the same:

Application
↓
JIT Activity
↓
RW → RX

Compared to:

Unknown Process
↓
Private Memory
↓
Write
↓
RW → RX
↓
Suspicious Thread Activity

EDR systems focus precisely on this contextual difference.

Important Note:
When viewing a memory region in a debugger or memory viewer, do not look solely at the address.

Examine these attributes as well:

Address
Size
Protection
Type
State
Backing
Owner Process
Thread Relation

For example:

0x000001A...
Size: 0x5000
Protection: RX
Type: ...
State: ...

This information helps you understand the nature of the memory region you are dealing with.

So, for now, keep this mental model in mind:
What is more important for analysis is:

Memory
+
Permission Change
+
Thread
+
Call Stack
+
Process Context
+
Behavior

@reverseengine
Struct Layout
و Offset Analysis در Reverse Engineering

وقتی Source Code رو داشته باشیم فهمیدن Struct خیلی راحت‌ تره

مثلا:

struct User {
int id;
int age;
char *name;
};

اینجا دقیقا میدونیم هر Field چیه و کجا قرار گرفته.

ولی توی Reverse Engineering معمولا خبری از این کد نیست

کامپایلر Struct رو تبدیل کرده به دستورهای Machine Code و Assembly و چیزی که ما میبینیم ممکنه فقط این باشه:

mov eax, [rdi+0x4]
mov rax, [rdi+0x8]

اینجا دیگه کامپایلر نمیاد بهمون بگه:

+0x04 = age
+0x08 = name

پس باید خودمون از روی نحوه استفاده از Memory بفهمیم هر Offset احتمالا مربوط به چه Field ی بوده

این کار یکی از بخش‌های مهم برای بازسازی ساختارهای ناشناخته است

Offset
دقیقا چیه؟

فرض کنید آدرس شروع یک Object این باشه:

0x1000

حالا توی Assembly داریم:

mov eax, [rdi+0x4]

اگر RDI برابر 0x1000 باشه CPU میره سراغ:

0x1000 + 0x4 = 0x1004

یعنی:

Object Base
|
v
0x1000
+0x00
+0x04 <-- accessed
+0x08
+0x10

اون +0x4 همون Offset هست

پس وقتی توی RE میبینید:

[Register + Offset]

یکی از اولین سوال‌ها اینه:

این Register به کجا اشاره میکنه؟
این Offset چه Field ی رو نشون میده؟

در خیلی از مواقع چیزی که داریم دنبالش میگردیم همینه:

Base Address + Offset
|
v
Field


حالا یک Struct ساده رو ببینیم

مثلا:

struct User {
int id;
int age;
char *name;
};

در یک محیط 64 بیتی معمولی که int چهار بایت و Pointer هشت بایت باشه Layout مفهومی میتونه این شکلی باشه:

Offset Size Field

+0x00 4 bytes id
+0x04 4 bytes age
+0x08 8 bytes name

پس وقتی Compiler می‌خواد age رو بخونه:

mov eax, [rdi+0x4]

و برای خوندن name:

mov rax, [rdi+0x8]

ما از روی همین Offsetها میتونیم کم‌ کم Layout Struct رو بازسازی کنیم

البته این Layout به ABI و Alignment هم وابسته است پس همیشه نباید یک Layout رو برای همه Binary ها قطعی فرض کنیم



Read و Write
رو از هم تشخیص بده

این دستور:

mov eax, [rdi+0x4]

یعنی مقدار موجود در Object + 0x4 خونده شده

پس:

read field at +0x4

ولی این:

mov [rdi+0x4], eax

یعنی مقدار جدیدی در Object + 0x4 نوشته شده

پس:

write field at +0x4

این تفاوت توی RE خیلی مهمه

مثلا اگر در چند جای برنامه ببینیم:

mov eax, [rdi+0x4]

و جای دیگه:

add dword ptr [rdi+0x4], 1

می‌تونیم حدس بزنیم که +0x4 احتمالا یک مقدار عددی یا چیزی با رفتار عددی هست

چرا؟

چون برنامه داره مستقیما روی اون Memory به شکل یک مقدار 32 بیتی عملیات انجام میده


اندازه Memory Access هم سرنخ میده

مثلا:

mov eax, [rdi+0x4]

اینجا داریم یک مقدار 4 بایتی میخونیم.

در مقابل:

mov rax, [rdi+0x8]

یک مقدار 8 بایتی خونده میشه

در یک Binary 64 بیتی این مقدار میتونه یک Pointer یا یک مقدار 64 بیتی باشه

حالا اگر بعدش ببینیم:

mov rax, [rdi+0x8]
mov rcx, [rax]

اینجا یک سرنخ مهم داریم

مقداری که از:

Object + 0x8

خوندیم بعدا به عنوان Address استفاده شده

پس احتمال Pointer بودنش بیشتر میشه:

+0x8 = pointer ?

ولی فعلا علامت سؤال رو نگه دارید 😁

در Reverse Engineering بهتره با یک Instruction سریع نتیجه قطعی نگیریم

Assembly
به تنهایی خیلی چیزها رو نمیگه باید Context رو هم ببینیم



دنبال Offsetهای تکراری بگرد

یکی از روش‌های خیلی خوب اینه که یک Offset رو در چند تابع مختلف بررسی کنیم

مثلا:

Function A:
mov eax, [rdi+0x4]

و:

Function B:
cmp dword ptr [rdi+0x4], 0

و:

Function C:
add dword ptr [rdi+0x4], 1

اگر RDI در این توابع واقعا به Object مشابهی اشاره کنه احتمال زیادی وجود داره که:

+0x4

یک Field مشترک از همون Struct باشه

حالا مثلا میبینیم:

mov rax, [rdi+0x8]

و جای دیگه:

call [rdi+0x8]

اینجا باید بیشتر بررسی کنیم

ممکنه +0x8 یک Pointer باشه

ممکنه Function Pointer باشه

یا ممکنه بخشی از یک Object Layout خاص باشه

پس فقط از روی Offset اسم‌گذاری نکنید

رفتار اون Field رو هم بررسی کنید



Base Pointer
رو درست پیدا کنید

قبل از اینکه بریم سراغ Offset ها باید بفهمیم Register پایه واقعا به چی اشاره میکنه

مثلا:

mov eax, [rdi+0x4]

ممکنه RDI به یک Struct اشاره کنه

ولی صرفا چون اسمش RDI هست نمیتونیم چنین نتیجه‌ای بگیریم

باید مسیر مقدار رو دنبال کنیم

مثلا:

mov rdi, rbx
call process_user
اینجا ممکنه RBX یک Pointer به Object باشه که به عنوان Argument به process_user داده شده

Calling Convention
هم خیلی مهمه

مثلاً در System V x86-64 معمولا Argumentهای ابتدایی از این Register ها عبور میکنن:

RDI
RSI
RDX
RCX
R8
R9

ولی در Windows x64 چهار Argument اول معمولا از:

RCX
RDX
R8
R9

عبور میکنن

پس مثلا توی یک Binary ویندوزی:

mov eax, [rcx+0x8]

میتونه سرنخ مهمی باشه که RCX یک Pointer به Object یا Struct ورودی تابع باشه

البته باز هم باید Context تابع رو بررسی کنیم



از روی نحوه استفاده از Field نوعش رو حدس بزنید

فرض کنید داریم:

mov eax, [rcx+0x4]
cmp eax, 10

این میتونه این احتمال رو ایجاد کنه:

+0x4 = integer-like field

حالا:

mov rax, [rcx+0x8]
mov rdx, [rax]

اینجا احتمال Pointer بودن +0x8 بیشتره

یا:

mov rax, [rcx+0x10]
call rax

اینجا احتمال Function Pointer بودن مقدار +0x10 مطرح میشه

یا:

mov rax, [rcx+0x18]
lea rdx, [rax+...]

ممکنه +0x18 یک Pointer به Buffer یا Object دیگه باشه

ولی یک نکته مهم:

هیچکدوم از اینها به تنهایی اثبات قطعی نیستن

باید Instruction های اطراف Cross-Reference ها تابع‌ های دیگه و مسیر Data Flow رو هم بررسی کنیم



Nested Struct ها


Struct
میتونه داخل Struct دیگه قرار بگیره

مثلا:

struct Address {
int city;
int zip;
};

struct User {
int id;
struct Address address;
};

حالا ممکنه توی Binary ببینیم:

mov eax, [rcx+0x8]

و جای دیگه:

mov eax, [rcx+0xC]

اگر Context مناسب باشه ممکنه به این نتیجه برسیم که:

User
|
+0x00 id
|
+0x08 address
|
+0x00 city
+0x04 zip

یعنی:

User + 0x08 = Address
User + 0x0C = Address.zip

در Source Code ممکنه خیلی راحت ببینیم:

user->address.zip

ولی در Binary معمولا چنین اسمی وجود نداره.

چیزی که میبینیم ممکنه فقط این باشه:

mov eax, [rcx+0xC]

پس باید از روی Offset ها و نحوه استفاده از داده ساختار منطقی برنامه رو دوباره بسازیم

Padding و Alignment
رو فراموش نکنید

یکی از اشتباهات رایج اینه که فکر کنیم Field های Struct همیشه دقیقا پشت سر هم قرار میگیرن

مثلا:

struct Example {
char flag;
void *ptr;
};

ممکنه روی یک ABI معمول 64 بیتی Layout چیزی شبیه این باشه:

+0x00 flag 1 byte
+0x01 padding
+0x02 padding
+0x03 padding
+0x04 padding
+0x05 padding
+0x06 padding
+0x07 padding
+0x08 ptr 8 bytes

چرا؟

چون Pointer معمولا نیاز به Alignment مناسب خودش داره

پس نباید فکر کنیم:

char
pointer

یعنی Pointer حتما از +0x01 شروع میشه

ممکنه Compiler بین Field ها Padding قرار بده

بنابراین وقتی Offset ها رو بازسازی میکنید همیشه این دو مورد رو هم در نظر بگیرید:

Alignment
Padding



Array
ها یک سرنخ خیلی خوب هستن

فرض کنید توی Binary اینو ببینیم:

mov eax, [rcx+rax*4]

اینجا تقریبا داریم این مدل Address Calculation رو میبینیم:

base + index * 4

این میتونه نشون بده که با Array ای از عناصر 4 بایتی طرفیم

مثلا:

Element 0 → +0x00
Element 1 → +0x04
Element 2 → +0x08
Element 3 → +0x0C

حالا اگر ببینیم:

mov rax, [rcx+rax*8]

احتمالا هر Element هشت بایت فاصله داره

اما اگر ببینیم:

mov eax, [rcx+rax*0x20]

می‌تونیم حدس بزنیم فاصله بین شروع Elementها حدودا:

0x20 bytes

هست.

به این فاصله میگیم:

Stride

یعنی فاصله شروع یک Element تا شروع Element بعدی

مثلا:

Element 0
|
| 0x20
v
Element 1
|
| 0x20
v
Element 2

این موضوع موقع Reverse Engineering ساختارهای ناشناخته خیلی به درد میخوره


یک مثال واقعی‌ تر

فرض کنید این 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]

حالا بدون Source Code چی میتونیم بفهمیم؟

اول:

mov eax, [rcx+0x4]

یعنی یک مقدار 32 بیتی از Offset 0x4 خونده شده

بعد:

test eax, eax

و نتیجه برای یک شرط استفاده شده

پس میتونیم حدس بزنیم:

+0x4 = integer-like / flag-like field

بعد:

mov rax, [rcx+0x8]

یک مقدار 64 بیتی از +0x8 خونده شده

بعد:

mov rdx, [rax]

یعنی مقدار خونده‌شده به عنوان Address استفاده شده

و بعد:

call rdx

این Address برای یک Call استفاده شده

پس احتمال Pointer بودن Field در +0x8 بیشتر میشه و حتی ممکنه Pointer به چیزی باشه که از طریق اون یک Function Pointer یا جدول Functionها قابل دسترسیه

بعد میرسیم به:
mov rax, [rcx+0x10]
mov eax, [rax+0x4]

اینجا مقدار +0x10 هم به عنوان Pointer استفاده شده و بعد از Object مقصد Offset +0x4 خونده شده

پس احتمالاً +0x10 یک Pointer به یک ساختار یا Object دیگه است

در نتیجه فعلا میتونیم یک مدل اولیه داشته باشیم:

Object
|
+0x00 unknown
+0x04 integer / flag
+0x08 pointer-like
+0x10 pointer-like

ولی این هنوز Struct نهایی نیست

این فقط یک Hypothesis هست

باید با Instructionهای بیشتر Cross-Referenceها و توابع دیگه بررسیش کنیم


حالا بریم سراغ
Ghidra

وقتی Offsetهای مختلف رو پیدا کردیم میتونیم توی Ghidra یک Struct بسازیم و Field ها رو بهش اضافه کنیم

مثلا اول کار ممکنه فقط این اطلاعات رو داشته باشیم:

User

0x00 unknown
0x04 field_4
0x08 field_8
0x10 field_10

بعد از بررسی بیشتر متوجه بشیم:

User

0x00 unknown
0x04 id
0x08 name
0x10 address

یعنی چیزی که اول فقط این بود:

field_8

کم‌ کم تبدیل میشه به:

name

و این دقیقا یکی از بخش‌های جذاب Reverse Engineering هست

چون هدف فقط این نیست که بفهمیم:

mov rax, [rcx+0x8]

چه کاری انجام میده

هدف اینه که از بین همین Instruction ها دوباره یک مدل منطقی از برنامه بسازیم

یعنی از:

[rcx+0x8]

برسیم به چیزی مثل:

user->name




برای بازسازی یک Struct ناشناخته این مسیر رو توی ذهنتون داشته باشید:

Base Pointer رو پیدا کنید

Offset های دسترسی رو جمع کنید

Read و Write رو تشخیص بدید

اندازه Memory Access رو بررسی کنید

نحوه استفاده از مقدار رو بررسی کنید

Offset
های تکراری رو در توابع مختلف پیدا کنید

Pointer
ها رو از Integer ها تفکیک کنید

Nested Structure ها رو بررسی کنید

Padding و Alignment رو در نظر بگیرید

10. Arrayها و Strideها رو بررسی کن
11. یک Hypothesis بساز
12. با Cross-Reference و تحلیل بیشتر تأیید یا اصلاحش کن

در Source Code ممکنه خیلی راحت داشته باشیم:

user->name

ولی در Binary ممکنه فقط این رو ببینیم:

mov rax, [rcx+0x8]

کار Reverse Engineer اینه که از همین اطلاعات کم‌کم بفهمه:

RCX
↓
User Object
↓
+0x8
↓
name

یعنی از چندتا Offset و Instruction پراکنده دوباره ساختار منطقی برنامه رو سرهم کنیم

@reverseengine
ReverseEngineering
Struct Layout و Offset Analysis در Reverse Engineering وقتی Source Code رو داشته باشیم فهمیدن Struct خیلی راحت‌ تره مثلا: struct User { int id; int age; char *name; }; اینجا دقیقا میدونیم هر Field چیه و کجا قرار گرفته. ولی توی Reverse Engineering…
Struct Layout
and Offset Analysis in Reverse Engineering

It is much easier to understand Struct when we have the Source Code

For example:

struct User {
int id;
int age;
char *name;
};

Here we know exactly what each Field is and where it is located.

But in Reverse Engineering, this code is usually not mentioned

The compiler converts Struct into Machine Code and Assembly instructions and what we see may be just this:

mov eax, [rdi+0x4]

mov rax, [rdi+0x8]

Here, the compiler no longer tells us:

+0x04 = age

+0x08 = name

So we have to figure out what Field each Offset probably belongs to based on how Memory is used

This is one of the important parts of reconstructing unknown structures

What exactly is Offset?

Suppose the starting address of an Object is:

0x1000

Now in Assembly we have:

mov eax, [rdi+0x4]

If RDI is 0x1000, the CPU goes to:

0x1000 + 0x4 = 0x1004

That is:

Object Base
|
v
0x1000
+0x00
+0x04 <-- accessed
+0x08
+0x10

That +0x4 is the Offset

So when you see in RE:

[Register + Offset]

One of the first questions is:

Where does this Register point to?

What Field does this Offset indicate?

In many cases, what we are looking for is this:

Base Address + Offset
|
v
Field

Now let's look at a simple Struct

For example:

struct User {
int id;
int age;
char *name;
};

In a typical 64-bit environment where int is four bytes and pointer is eight bytes, the conceptual layout can be like this:

Offset Size Field

+0x00 4 bytes id

+0x04 4 bytes age

+0x08 8 bytes name

So when the Compiler wants to read age:

mov eax, [rdi+0x4]

And to read name:

mov rax, [rdi+0x8]

We can gradually rebuild the Layout Struct based on these Offsets

Of course, this Layout also depends on ABI and Alignment, so we should not always assume a definitive Layout for all Binaries

Distinguish Read and Write

This command:

mov eax, [rdi+0x4]

means the value in Object + 0x4 is read

So:

read field at +0x4

But this:

mov [rdi+0x4], eax

means a new value in Object + 0x4 is written

So:

write field at +0x4

This difference is very important in RE

For example, if we see in several places of the program:

mov eax, [rdi+0x4]

And in another place:

add dword ptr [rdi+0x4], 1

We can guess that +0x4 is probably a numeric value or something with numeric behavior

Why?

Because the program is directly operating on that Memory as a 32-bit value

The size of the Memory Access also gives a clue

For example:

mov eax, [rdi+0x4]

Here we are reading a 4-byte value.

In contrast:

mov rax, [rdi+0x8]

An 8-byte value is read

In a 64-bit binary this value can be a Pointer or a 64-bit value

Now if we see later:

mov rax, [rdi+0x8]

mov rcx, [rax]

Here we have an important clue

The value we read from:

Object + 0x8

was later used as an Address

So it is more likely to be a Pointer:

+0x8 = pointer ?

But keep the question mark for now 😁

In Reverse Engineering, it is better not to draw a definitive conclusion with a quick instruction

Assembly
alone does not tell much, we need to see the Context

Look for duplicate Offsets

One of the very good methods is to check an Offset in several different functions

For example:

Function A:
mov eax, [rdi+0x4]

And:

Function B:
cmp dword ptr [rdi+0x4], 0

And:

Function C:
add dword ptr [rdi+0x4], 1

If RDI in these functions really points to the same Object, there is a high probability that:

+0x4

is a common Field of the same Struct

Now, for example, we see:

mov rax, [rdi+0x8]

And elsewhere:

call [rdi+0x8]

Here we need to check more

Maybe +0x8 is a Pointer Okay

It could be a Function Pointer

Or it could be part of a specific Object Layout

So don't just name it by Offset

Check the behavior of that Field too

Find the Base Pointer

Before we go into Offsets, we need to understand what the Base Register actually points to

For example:

mov eax, [rdi+0x4]

RDI could be pointing to a Struct

But just because it's called RDI, we can't conclude that

We need to follow the value path

For example:

mov rdi, rbx
call process_user
❤1
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
So the probability of the Field at +0x8 being a Pointer increases and it may even be a Pointer to something through which a Function Pointer or a Function table can be accessed

Then we get to:
mov rax, [rcx+0x10]
mov eax, [rax+0x4]

Here the value +0x10 is also used as a Pointer and after the destination Object Offset +0x4 is read

So +0x10 is probably a Pointer to another structure or Object

So for now we can have a basic model:

Object
|
+0x00 unknown
+0x04 integer / flag
+0x08 pointer-like
+0x10 pointer-like

But this is not the final Struct yet

This is just a Hypothesis

We need to check it with more instructions, Cross-References and other functions

Now let's move on to
Ghidra

When we find the different Offsets, we can create a Struct in Ghidra and add Fields to it

For example, at first we may only have this information:

User

0x00 unknown
0x04 field_4
0x08 field_8
0x10 field_10

After further investigation, we will find out:

User

0x00 unknown
0x04 id
0x08 name
0x10 address

That is, what was initially just:

field_8

slowly becomes:

name

And this is exactly one of the interesting parts of Reverse Engineering

Because The goal is not just to understand:

mov rax, [rcx+0x8]

what it does

The goal is to rebuild a logical model of the program from these instructions

That is, from:

[rcx+0x8]

to get to something like:

user->name

To reconstruct an unknown struct, keep this path in mind:

Find the Base Pointer

Collect the access offsets

Distinguish Read and Write

Check the size of the Memory Access

Check how the value is used

Find duplicate offsets in different functions

Separate Pointers from Integers

Check Nested Structures

Consider Padding and Alignment

10. Check Arrays and Strides

11. Make a Hypothesis

12. Confirm or modify it with Cross-Reference and further analysis

In the Source Code, we may very easily have:

user->name

But In Binary we may only see this:

mov rax, [rcx+0x8]

The job of the Reverse Engineer is to understand from this information:

RCX
↓
User Object
↓
+0x8
↓
name

That is, we reassemble the logical structure of the program from a few scattered Offsets and Instructions

@reverseengine
باز سازی Inheritance و Multiple Inheritance در Assembly

تا اینجا یاد گرفتیم چطور از روی Memory Access ها و Offset ها یک Struct ناشناخته رو بازسازی کنیم

ولی وقتی با برنامه‌ های + +C طرف میشیم قضیه یک مقدار پیچیده‌ تر میشه

چون ممکنه یک Object فقط چند Field ساده نداشته باشه

ممکنه:

Class
↓
Inheritance
↓
Virtual Functions
↓
vptr
↓
vtable

داشته باشیم

اینجاست که باید از روی Assembly بفهمیم:

این Object چه Class ای هست؟
از چه Class ای ارثبری کرده؟

Virtual Function
ها کجا قرار دارن؟
و Offset های مختلف مربوط به کدوم بخش Object هستن؟



Virtual Function
رو بفهمیم

فرض کنید Source Code این شکلی باشه:

class Animal {
public:
virtual void speak();
};

و:

class Dog : public Animal {
public:
void speak() override;
};



چون "speak" یک Virtual Function هست Compiler معمولا برای این Object یک Pointer به جدول Virtual Function ها در نظر میگیره

به این Pointer معمولا میگیم:

vptr

و جدول:

vtable

به شکل مفهومی:

Object
│
├── vptr
│ │
│ ▼
│ vtable
│ │
│ ├── speak()
│ ├── ...
│
└── Other Fields

یعنی Object معمولا یک Pointer داره که به اطلاعات Virtual Functionهای مربوط به نوع Dynamic Object اشاره میکنه

البته جزئیات دقیق Layout به Compiler و ABI وابسته است.



داخل Assembly دنبال چه چیزی بگردیم؟

فرض کنید داخل Constructor چنین چیزی ببینیم:

mov qword ptr [rcx], offset vtable_Dog

اینجا یک سرنخ خیلی مهم داریم

یعنی مقدار ابتدای Object تنظیم شده:

Object + 0x00
↓
vptr
↓
vtable_Dog

پس میتونیم فعلا بگیم:

Dog Object

+0x00 vptr
+0x08 unknown
+0x10 unknown

هنوز نمیدونیم Field های بعدی چی هستن

ولی حداقل یک چیز مهم فهمیدیم:

+0x00
احتمالا vptr است



vtable
دقیقا چیه؟

خیلی ساده اگر بخوایم نگاهش کنیم:

vptr
│
▼
vtable
│
├── Function Pointer
├── Function Pointer
├── Function Pointer
└──

هر Entry در vtable معمولا یک Pointer به یک Function یا بخشی از مکانیزم Virtual Dispatch هست

مثلا فرض کنید Assembly این شکلی باشه:

mov rax, [rcx]
mov rax, [rax+0x10]
call rax


بیایم مرحله‌ به‌ مرحله نگاه کنیم

اول:

mov rax, [rcx

]

یعنی:

RCX
↓
Object
↓
+0x00
↓
vptr

بعد:

mov rax, [rax+0x10

]

یعنی از جدول مربوط به vptr یک Entry در Offset مشخص خونده شده

بعد:

call rax

یعنی اون مقدار برای Call استفاده شده

پس یک مسیر مفهومی داریم:

Object
↓
+0x00
↓
vptr
↓
vtable
↓
Function Pointer
↓
call

این یکی از الگوهای خیلی مهم در Reverse Engineering برنامه‌های C++ هست.



از روی این الگو چطور Virtual Function رو پیدا کنیم؟

فرض کنید این Assembly رو داریم:

mov rax, [rcx]
mov rax, [rax+0x18]
call rax


میتونیم فعلا این مدل رو بسازیم:

Object
|
+0x00 → vptr
|
↓
vtable
|
+0x18 → function pointer

اما یک نکته مهم:

نباید سریع بگیم:

+0x18 = Function X

چون هنوز نمی‌دونیم Entry دقیقا مربوط به چه Virtual Function ای هست

باید خود vtable و Reference های مربوط به اون رو بررسی کنیم



حالا Inheritance وارد میشه

فرض کنید داریم:

class Animal {
public:
virtual void speak();
};

class Dog : public Animal {
public:
void speak() override;
};


از نظر مفهومی:

Animal
↑
|
Dog

Dog
بخش‌هایی از Animal رو به ارث میبره

ممکنه Object چیزی شبیه این داشته باشه:

Dog Object

+0x00 vptr
+0x08 Animal-related field
+0x0C Dog-related field

این Layout دقیق نیست و صرفا یک مدل مفهومیه

چون Layout واقعی به Compiler، ABI، Alignment و نوع Inheritance بستگی داره

ولی نکته مهم اینه که:

Base Class
+
Derived Class

میتونن در یک Object واحد حضور داشته باشن



از روی Constructor می‌تونیم رابطه رو پیدا کنیم

فرض کن یک Constructor داریم:

mov qword ptr [rcx], offset vtable_Animal


و بعد در Constructor کلاس دیگه ای میبینیم:

call Animal::Animal
mov qword ptr [rcx], offset vtable_Dog

اینجا یک سرنخ خیلی مهم داریم

یعنی Constructor مربوط به Dog ابتدا بخش مربوط به Base Class رو مقدار دهی میکنه و بعد وضعیت Virtual Dispatch مربوط به Derived Class رو تنظیم میکنه

به شکل مفهومی:

Dog Constructor
|
├── Animal Constructor
|
└── Dog-specific initialization

این یکی از چیزهایی هست که میتونه به ما کمک کنه رابطه بین Class ها رو پیدا کنیم

@reverseengine
ReverseEngineering
باز سازی Inheritance و Multiple Inheritance در Assembly تا اینجا یاد گرفتیم چطور از روی Memory Access ها و Offset ها یک Struct ناشناخته رو بازسازی کنیم ولی وقتی با برنامه‌ های + +C طرف میشیم قضیه یک مقدار پیچیده‌ تر میشه چون ممکنه یک Object فقط چند Field…
Reconstructing Inheritance and Multiple Inheritance in Assembly

So far we have learned how to reconstruct an unknown Struct from Memory Accesses and Offsets

But when we deal with C++ programs, the matter becomes a bit more complicated

Because an Object may not have just a few simple fields

We may have:

Class
↓
Inheritance
↓
Virtual Functions
↓
vptr
↓
vtable

This is where we need to understand from the Assembly:

What Class is this Object?

What Class does it inherit from?

Where are the Virtual Functions

And which part of the Object do the different Offsets belong to?

Understand Virtual Functions

Suppose the Source Code is like this:

class Animal {
public:
virtual void speak();

};

And:

class Dog : public Animal {
public:
void speak() override;
};

Because "speak" is a Virtual Function, the Compiler usually considers a Pointer to the Virtual Function Table for this Object

We usually call this Pointer:

vptr

and the table:

vtable

Conceptually:

Object
│
├── vptr
│ │
│ ▼
│ vtable
│ │
│ ├── speak()
│ ├── ...
│
└── Other Fields

That is, the Object usually has a Pointer that points to the Virtual Function information related to the Dynamic Object type

Of course, the exact details of the Layout depend on the Compiler and ABI.

What should we look for inside the Assembly?

Suppose we see something like this inside the Constructor:

mov qword ptr [rcx], offset vtable_Dog

Here we have a very important clue

That is, the initial value of Object is set:

Object + 0x00
↓
vptr
↓
vtable_Dog

So we can say for now:

Dog Object

+0x00 vptr
+0x08 unknown
+0x10 unknown

We still don't know what the next fields are

But at least we know one important thing:

+0x00
is probably vptr

What exactly is vtable?

Very simply, if we want to look at it:

vptr
│
▼
vtable
│
├── Function Pointer
├── Function Pointer
├── Function Pointer
└──

Each entry in the vtable is usually a pointer to a function or part of the Virtual Dispatch mechanism

For example, suppose the assembly looks like this:

mov rax, [rcx]

mov rax, [rax+0x10]
call rax


Let's look at it step by step

First:

mov rax, [rcx
]

That is:

RCX
↓
Object
↓
+0x00
↓
vptr

Then:

mov rax, [rax+0x10
]


That is, an entry at a specific offset is read from the vptr table

Then:

call rax

That is, that value is used for Call

So we have a conceptual path:

Object
↓
+0x00
↓
vptr
↓
vtable
↓
Function Pointer
↓
call

This is one of the most important patterns in Reverse Engineering C++ programs.

How do we find the Virtual Function from this pattern?

Suppose we have this Assembly:

mov rax, [rcx]

mov rax, [rax+0x18]
call rax


We can create this model for now:

Object
|
+0x00 → vptr
|
↓
vtable
|
+0x18 → function pointer

But one important point:

We should not say quickly:

+0x18 = Function X

Because we still do not know exactly what Virtual Function Entry is related to

We should check the vtable itself and its References

Now Inheritance comes in

Suppose we have:

class Animal {
public:
virtual void speak();
};

class Dog : public Animal {
public:
void speak() override;
};

Conceptually:

Animal
↑
|
Dog

Dog
inherits parts of Animal

The Object might look something like this:

Dog Object

+0x00 vptr
+0x08 Animal-related field
+0x0C Dog-related field

This Layout is not exact and is just a conceptual model

Because the actual Layout depends on the Compiler, ABI, Alignment and Inheritance type

But the important point is that:

Base Class
+
Derived Class

can be present in a single Object

We can find the relationship from the Constructor

Suppose we have a Constructor:

mov qword ptr [rcx], offset vtable_Animal

And then in the Constructor we see another class:

call Animal::Animal
mov qword ptr [rcx], offset vtable_Dog

Here we have a very important clue

That is, the Constructor related to Dog first initializes the part related to the Base Class and then the Virtual Dispatch state related to Sets the Derived Class

Conceptually:

Dog Constructor
|
├── Animal Constructor
|
└── Dog-specific initialization