ReverseEngineering
1.32K subscribers
50 photos
11 videos
106 files
888 links
Download Telegram
elf.pdf
337.1 KB
Tool Interface Standard (TIS)
Executable and Linking Format (ELF)
Specification

Version 1.2

@reverseengine
_x86_software_reverse_engineering_cracking_and_counter_measures.pdf
5.6 MB
x86 Software ReverseEngineering, Cracking,
and Counter-Measures

@reverseengine
Attacking_Software_Reverse_Engineering.pdf
432.6 KB
Automa'cally A,acking So2ware Reverse Engineering AI Agents

@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
بخش سی ام بافر اورفلو


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
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
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 وارد محاسبه 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]

حالا باید ببینیم [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
[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
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 خیلی مهمه

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 ها خیلی به کارتون میاد
System Call
رابط کنترل‌شده‌ ای که یک برنامه در User Mode از طریق اون درخواست‌ هایی رو از Kernel میخاد

مهم‌ترین نکته:

User Mode
│
│ System Call
↓
Kernel Mode
│
↓
Kernel performs operation
│
↓
User Mode

و این دقیقا یکی از پایه‌ های مهم درک OS Internals و Reverse Engineering هست

@reverseengine
ReverseEngineering
System Call برنامه چجور با Kernel حرف میزنه؟ تا اینجا با چیزهایی مثل: lua fork() exec() wait() exit() کار کردیم اما یک سوال مهم وجود داره این توابع چطور میتونن از Kernel درخواست انجام یک کار بکنن؟ مثلا برنامه‌ای که داخل User Space اجرا میشه چطور میتونه:…
System Call
How does a program talk to the Kernel?

So far we have worked with things like:
lua
fork()
exec()
wait()
exit()

But there is an important question

How can these functions request the Kernel to do something?

For example, how can a program running in User Space:

Open a file?

Read data?

Create a new process?

Get memory?

Process
terminate itself?

Original answer:
System Call

User Mode and Kernel Mode

The CPU
usually executes the program at a limited access level, which we call:
User Mode


The program in this mode cannot directly do anything it wants with the system

For example, it cannot directly manipulate:
Hardware
Kernel Memory
Page Tables
Device Control

Because if any program had such access, a malicious program could destroy the entire system😁
And people have already found enough other ways to do this

Kernel Mode

The Kernel is executed at a higher access level:

Kernel Mode

In this mode, the Kernel can access sensitive system resources
So the overall structure is approximately as follows:

User Space
│
│ System Call
↓
Kernel Space
│
↓
Hardware / Resources

The program does not enter the Kernel directly
But uses a controlled gateway, this gateway is the System Call

A simple example
Suppose the program wants to Read a file
At the program level we might have something like this:

read(fd, buffer, size);

The program requests:

I want to read a certain amount of data from this File Descriptor

At the end of the request, it should reach the Kernel

The Kernel checks:

Who is this Process?

Does it have access?

Is the fd
valid?

Is the buffer address valid?

How much data should be read?

Then it performs the operation and returns the result

General System Call Path

In simple terms:

Program
│
↓
Library / API Wrapper
│
↓
System Call Mechanism
│
↓
CPU → Kernel Mode
│
↓
Kernel
│
↓
Operation
│
↓
Return Value
│
↓
User Mode

That is, the program enters the Kernel from a specific path to perform sensitive operations

System Call is not the same as Function Call

Let's not confuse the two

A normal Function Call, for example:

result = add(a, b);

Usually it is done in the same execution space of the program

But System Call creates a crossing of the User and Kernel access boundaries

So:

Function Call
↓
Same execution space

System Call
↓
User Mode
↓
Kernel Mode
↓
Return to User Mode

This difference is very important in Reverse Engineering

Fork() and wait()
also use the same path

For example, in a C program we write:

fork();

But this does not mean a simple Function like:

C
int add(int a, int b)

In Unix-like systems this request must finally reach the Kernel so that the Kernel can create a new Process

The same idea exists for operations like:
shell

read
write
open
mmap
fork
exec
wait

Of course, one important point

What we see in the code is not necessarily the raw System Call itself, it may be a Library Wrapper that finally performs the appropriate System Call

How does System Call
reach the Kernel?

In modern architectures, the CPU has a special mechanism for this transfer. For example, in x86-64 Linux, the command:

syscall

is used to enter the System Call path. Conceptually:

User Code
↓
Arguments are prepared
↓
System Call Number
↓
syscall
↓
CPU
↓
Kernel Entry
↓
Kernel Handler

The Kernel
checks and processes the request after it has entered.

Why is it important to us?
Suppose you see this inside a Binary:

syscall

You are no longer dealing with just a regular assembly instruction

This instruction can indicate that the program passes through:

User Space
↓
Kernel Space

On the other hand, if you see APIs like:

CreateFile
ReadFile
VirtualAlloc
CreateProcess


in the program code, you should know that these APIs ultimately depend on Kernel mechanisms to perform a large number of system operations, so in RE you should be able to see this chain in your mind:

Application
↓
Library / API
↓
System Call
↓
Kernel
↓
Resource / Hardware

This perspective will come in handy later when analyzing Processes, Memory, File System, Threads, and even more complex behavior of Binary.
👍1