ReverseEngineering
1.32K subscribers
50 photos
11 videos
106 files
888 links
Download Telegram
بخش بیست و دوم بافر اورفلو


Memory Leak
باگی که شاید برنامه رو کرش نکنه ولی دردسر درست میکنه


تا الان با باگ‌ هایی آشنا شدیم که حافظه رو خراب میکردن
ولی این بخش درباره باگیه که معمولا چیزی رو خراب نمیکنه
در عوض باعث میشه برنامه کم‌ کم حافظه بیشتری مصرف کنه
و بعد از مدتی کند بشه یا حتی از کار بیفته



Memory Leak
هر وقت برنامه با malloc حافظه بگیره
باید بعدا با free آزادش کنه
اگر این کار انجام نشه
اون حافظه تا پایان اجرای برنامه اشغال میمونه
به این میگن Memory Leak

یک مثال ساده:
C

#include <stdlib.h>

int main() {

char *buf = malloc(1024);

return 0;
}


مشکل این کجاست اینجا حافظه گرفته شده ولی هیچ وقت آزاد نشده یعنی قبل از خروج برنامه این دستور اجرا نشده

free(buf);



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

مثلا:
C

while (1) {
char *buf = malloc(1024);
}


هر بار 1024 بایت گرفته میشه ولی هیچ وقت آزاد نمیشه بعد از مدتی برنامه مقدار زیادی حافظه مصرف میکنه

موقع مهندسی معکوس دنبال چی بگردیم؟


اگر این الگو رو دیدید

ptr = malloc(...);


بعد مسیر اجرای تابع رو تا اخر دنبال کنید

اگر هیچ جا

free(ptr);


وجود نداشت احتمال Memory Leak هست

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


C

char *buf = malloc(256);

if(error)
return;

free(buf);



اینجا یک مشکل وجود داره اگر شرط error برقرار بشه تابع قبل از رسیدن به free خارج میشه در نتیجه حافظه هیچ وقت آزاد نمیشه


چرا پیدا کردنش سخت‌ تره؟

چون معمولا برنامه کرش نمیکنه خطای واضحی نشون نمیده شاید فقط بعد از چند ساعت یا چند روز اجرا مشخص بشه
برای همین خیلی از Memory Leak ها مدت زیادی مخفی میمونن

ابزارهایی که کمک میکنن
برای پیدا کردن Memory Leak ابزار هایی مثل:

Valgrind
AddressSanitizer
LeakSanitizer


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


هر malloc باید یک free داشته باشه
نبودن free همیشه یعنی احتمال Memory Leak در مهندسی معکوس باید مسیر malloc تا پایان تابع رو دنبال کنیم
خروج زود هنگام از تابع یکی از رایج‌ ترین دلایل Memory Leak هست


تمرین:

یک برنامه ساده که از malloc استفاده میکنه داخل Ghidra یا IDA باز کنید بررسی کنید آیا برای همه مسیرهای اجرای برنامه در نهایت free صدا زده میشه یا نه اگر حتی یک مسیر پیدا کردید که حافظه آزاد نشه اولین Memory Leak خودتون رو پیدا کردید

@reverseengine
ReverseEngineering
بخش بیست و دوم بافر اورفلو Memory Leak باگی که شاید برنامه رو کرش نکنه ولی دردسر درست میکنه تا الان با باگ‌ هایی آشنا شدیم که حافظه رو خراب میکردن ولی این بخش درباره باگیه که معمولا چیزی رو خراب نمیکنه در عوض باعث میشه برنامه کم‌ کم حافظه بیشتری مصرف کنه…
Part 22 Buffer Overflow

Memory Leak A bug that may not crash the program but causes problems

So far we have met with bugs that corrupt memory
But this section is about a bug that usually does not corrupt anything
Instead, it causes the program to gradually consume more memory
And after a while it slows down or even crashes

Memory Leak
Whenever a program allocates memory with malloc
It must later release it with free
If this is not done
That memory remains occupied until the end of the program execution
This is called a Memory Leak

A simple example:

C
#include <stdlib.h>

int main() {

char *buf = malloc(1024);

return 0;
}

What is the problem here?

Here, memory is allocated but never freed, meaning this instruction was not executed before the program exits
free(buf);

What if this happens once? Almost nothing special happens, but if it is inside a loop or a service that is always running, the memory consumption will gradually increase

For example:

C
while (1) {
char *buf = malloc(1024);
}

Each time 1024 bytes are taken but never freed. After a while, the program will consume a lot of memory

What should we look for when reverse engineering?

If you see this pattern
ptr = malloc(...);

Then follow the path of the function execution to the end

If there is no
free(ptr);

anywhere, there is a possibility of a Memory Leak

A more realistic example:

C
char *buf = malloc(256);

if(error)
return;

free(buf);

There is a problem here. If the error condition is met, the function exits before reaching free, as a result, the memory is never freed

Why is it harder to find?

Because the program usually does not crash, it does not show an obvious error, it may only be detected after a few hours or days of execution. That is why many memory leaks remain hidden for a long time. Tools that help to find memory leaks include: Valgrind AddressSanitizer LeakSanitizer These tools show which memory was taken but not freed Every malloc must have a free No free always means there is a possibility of a memory leak In reverse engineering, we must follow the malloc path to the end of the function Early exit from the function is one of the most common causes of memory leaks


Exercise:

Open a simple program that uses malloc in Ghidra or IDA Check whether free is called at the end for all paths of the program execution If you find even one path where the memory is not freed, you have found your first memory leak

@reverseengine
❤4
بخش بیست و سوم بافر اورفلو


Information Leak

یعنی برنامه ناخواسته اطلاعاتی از حافظه رو نمایش بده یا برگردونه که این اطلاعات میتونه شامل
آدرس‌ های حافظه
داده‌ های حساس
رشته‌ های محرمانه
محتوای متغیرها
باشه
یک مثال ساده:

#include <stdio.h>

int main() {

int numbers[5] = {1,2,3,4,5};

printf("%d\n", numbers[10]);

return 0;
}


مشکل این کجاست؟
برنامه داره مقداری خارج از آرایه رو میخونه ممکنه چیزی که چاپ میشه مربوط به یک متغیر دیگه یا بخشی از حافظه باشه
این یعنی اطلاعاتی که نباید دیده بشن نمایش داده شدن
یک مثال دیگه:

char secret[] = "password123";

printf("%s\n", secret);


اگر برنامه به اشتباه آدرس این رشته رو در اختیار کاربر قرار بده
یا مسیر اجرای برنامه طوری باشه که این داده نمایش داده بشه
یک Information Leak رخ داده

چرا برای مهندسی معکوس مهمه؟

فرض کنید یک برنامه ASLR داره
یعنی آدرس‌ های حافظه هر بار تغییر میکنن
اگر یک Information Leak پیدا کنید
ممکنه آدرس یکی از توابع یا کتابخانه‌ها رو به دست بیارید حالا میتونید تحلیل دقیق‌ تری انجام بدید و بفهمید برنامه چطور در حافظه قرار گرفته
به همین دلیل Information Leak خیلی وقت‌ها اولین قدم برای تحلیل آسیب‌پذیری‌ های پیچیده‌ تره

موقع تحلیل باینری دنبال چی بگردیم؟
اگر دیدید برنامه آدرس اشاره‌ گرها رو چاپ میکنه داده‌ ای خارج از محدوده میخونه
پیام‌ های خطای بیش از حد دقیق نمایش میده اطلاعات حافظه رو بدون بررسی برمیگردونه باید بیشتر بررسیش کنید

یک مثال اسمبلی:

lea rdi,[rip+message]
call puts


این خودش مشکلی نداره ولی اگر قبلا puts آدرس یا داده‌ ای از حافظه بدون کنترل آماده شده باشه باید بررسی کنید که آیا اطلاعات حساسی ممکنه نمایش داده بشه یا نه؟


Information Leak
برنامه اطلاعاتی رو که نباید در اختیار کاربر قرار بده نمایش میده این اطلاعات ممکنه برای تحلیل باینری یا پیدا کردن مسیرهای آسیب‌پذیر خیلی ارزشمند باشن برای یک Reverse Engineer پیدا کردن این نشت‌ های اطلاعاتی یکی از مهارت‌ های مهمه

@reverseengine
❤1
ReverseEngineering
بخش بیست و سوم بافر اورفلو Information Leak یعنی برنامه ناخواسته اطلاعاتی از حافظه رو نمایش بده یا برگردونه که این اطلاعات میتونه شامل آدرس‌ های حافظه داده‌ های حساس رشته‌ های محرمانه محتوای متغیرها باشه یک مثال ساده: #include <stdio.h> int main() { …
Part 23 Buffer Overflow


Information Leak

This means that the program unintentionally displays or returns information from memory, which can include
memory addresses
sensitive data
secret strings
variable contents
A simple example:

#include <stdio.h>

int main() {

int numbers[5] = {1,2,3,4,5};

printf("%d\n", numbers[10]);

return 0;
}


What is the problem?

The program is reading a value outside the array. What is printed may be related to another variable or part of memory.

This means that information that should not be seen is displayed.

Another example:

char secret[] = "password123";

printf("%s\n", secret);


If the program mistakenly provides the address of this string to the user
or the program execution path is such that this data is displayed
an Information Leak has occurred

Why is it important for reverse engineering?

Suppose a program has ASLR
that is, the memory addresses change every time
If you find an Information Leak
you may get the address of one of the functions or libraries. Now you can do a more detailed analysis and understand how the program is located in memory
That is why Information Leak is often the first step in analyzing more complex vulnerabilities

What should we look for when analyzing binary?

If you see that the program prints pointer addresses, reads data out of bounds, displays overly detailed error messages, returns memory information without checking, you should investigate further. Here is an assembly example: lea rdi,[rip+message] call puts This is not a problem, but if the address or data from memory has been prepared without checking, you should check whether sensitive information may be displayed. Information Leak The program displays information that should not be made available to the user. This information may be very valuable for binary analysis or finding vulnerable paths. Finding these information leaks is one of the important skills for a reverse engineer.


@reverseengine
بخش بیست و چهارم بافر اورفلو


Fuzzing
شکار باگ بدون اینکه خط به خط کد رو بخونیم


تا اینجا خودمان با تحلیل کد و اسمبلی دنبال باگ می‌گشتیم
ولی اگر برنامه چند میلیون خط کد داشته باشه چی

اینجاست که Fuzzing وارد میشه
Fuzzing
به جای اینکه ما دنبال باگ بگردیم خودش هزار بار یا حتی میلیون‌ ها ورودی مختلف به برنامه میده تا ببیند برنامه کرش میکنه یا نه
Fuzzing یعنی چی
به زبان ساده
یک ابزار به صورت خودکار ورودی‌ های مختلف تولید میکنه و به برنامه میده
اگر برنامه
کرش کنه
هنگ کنه
رفتار غیرعادی داشته باشه
ابزار اون ورودی رو ذخیره میکنه تا بعدا بررسی کنیم

یک مثال ساده:

فرض کنید برنامه فقط یک رشته از کاربر بگیره

#include <stdio.h>

int main() {

char input[64];

fgets(input,sizeof(input),stdin);

printf("%s",input);

return 0;
}


یک Fuzzer ممکنه این ورودی‌ ها رو امتحان کنه

AAAA

AAAAAAAAAAAAAAAAAAAAAAAAAAAA

123456789

!@#$%^&*

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA



چرا Fuzzing مهمه؟

چون انسان نمیتونه میلیون‌ ها ورودی رو امتحان کنه
ولی Fuzzer این کار رو در مدت کوتاهی انجام میده
به همین دلیل خیلی از آسیب‌پذیری‌ های معروف دنیا اولین بار با Fuzzing پیدا شدن

انواع Fuzzing:

Dumb Fuzzing
ساده‌ترین حالت
فقط داده‌ های تصادفی به برنامه میده
هیچ اطلاعی از ساختار برنامه نداره

Smart Fuzzing
ساختار ورودی رو میشناسه

مثلا اگر برنامه فایل PNG باز میکنه
ورودی‌هایی شبیه PNG تولید میکنه
در نتیجه شانس پیدا کردن باگ بیشتر میشه

Coverage Guided Fuzzing
این روش خیلی محبوبه
ابزار بررسی میکنه هر ورودی باعث اجرای کدوم قسمت‌ های برنامه شده
اگر ورودی جدید مسیر جدیدی از کد رو اجرا کنه
همون مسیر رو بیشتر بررسی میکنه
به همین دلیل خیلی سریع‌ تر از روش‌ های ساده باگ پیدا میکنه

ابزارهای معروف

چند ابزار معروف که تقریباً هر Reverse Engineer باید اسمشون رو بدونه

AFL++
libFuzzer
Honggfuzz


این ابزار ها سال‌ هاست برای پیدا کردن باگ‌های حافظه استفاده میشن

موقع مهندسی معکوس چرا مهمه؟

فرض کنید یک باینری دارید و هیچ سورسی از اون موجود نیست

میتونید اون رو Fuzz کنید
اگر کرش کرد
همن ورودی رو داخل GDB یا IDA بررسی کنید

و قدم به قدم علت کرش رو پیدا میکنید
به همین دلیل Fuzzing و Reverse Engineering مکمل هم هستن


Fuzzing
یعنی به جای اینکه خودتون حدس بزنید چه ورودی باعث باگ میشه یک ابزار هزار بار یا میلیون‌ها ورودی مختلف رو امتحان میکنه هر جا برنامه رفتار غیرعادی داشت
همان نقطه تبدیل به هدف تحلیل مهندسی معکوس میشه

تمرین:

یک برنامه ساده که از ورودی کاربر استفاده میکنه بنویسید بعد فکر کنید اگر قرار بود یک Fuzzer برای اون بنویسید چه نوع ورودی‌ هایی رو امتحان میکردید

@reverseengine
ReverseEngineering
بخش بیست و چهارم بافر اورفلو Fuzzing شکار باگ بدون اینکه خط به خط کد رو بخونیم تا اینجا خودمان با تحلیل کد و اسمبلی دنبال باگ می‌گشتیم ولی اگر برنامه چند میلیون خط کد داشته باشه چی اینجاست که Fuzzing وارد میشه Fuzzing به جای اینکه ما دنبال باگ بگردیم…
Part 24 Buffer Overflow


Fuzzing
Bug hunting without reading the code line by line

So far we have been looking for bugs ourselves by analyzing the code and assembly

But what if the program has several million lines of code

This is where Fuzzing comes in

Fuzzing
Instead of us looking for bugs, it gives the program thousands or even millions of different inputs to see if the program crashes or not

What does Fuzzing mean

In simple terms
A tool automatically generates different inputs and gives them to the program

If the program

Crash
Hangs

Or behaves abnormally
The tool saves that input for later review

A simple example:

Suppose the program only takes a string from the user

#include <stdio.h>

int main() {

char input[64];

fgets(input,sizeof(input),stdin);

printf("%s",input);

return 0;

}


A Fuzzer might try these inputs

AAAA

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

123456789

!@#$%^&*

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA


Why is Fuzzing Important?

Because humans cannot try millions of inputs
But Fuzzer does this in a short time
That is why many of the world's famous vulnerabilities were first found with Fuzzing

Types of Fuzzing:

Dumb Fuzzing
The simplest
Only gives random data to the program
It has no information about the structure of the program

Smart Fuzzing
It knows the structure of the input

For example, if the program opens a PNG file
It produces inputs similar to PNG
As a result, the chances of finding a bug increase

Coverage Guided Fuzzing
This method is very popular
The tool checks which parts of the program each input causes to be executed
If the new input executes a new path of code
It checks the same path more
That is why it finds bugs much faster than simple methods

Famous tools

A few famous tools that almost every Reverse Engineer should know their names

AFL++
libFuzzer
Honggfuzz


These tools have been used for years to find Memory bugs are used

Why is it important when reverse engineering?

Suppose you have a binary and no source is available

You can fuzz it

If it crashes

Examine the same input in GDB or IDA

And you will find the cause of the crash step by step

That is why Fuzzing and Reverse Engineering are complementary

Fuzzing

Instead of guessing what input causes the bug, a tool tries thousands or millions of different inputs. Wherever the program behaves abnormally

That point becomes the target of reverse engineering analysis

Exercise:

Write a simple program that uses user input. Then think about what kind of inputs you would try if you were to write a fuzzer for it

@reverseengine
بخش بیست و پنجم بافر اورفلو

Address Sanitizer
یا ASan شکار باگ‌ های حافظه


تا اینجا یاد گرفتیم با Fuzzing میشه ورودی‌ های زیادی به برنامه داد و کرش‌ ها رو پیدا کرد

نکته مهم
وقتی برنامه کرش کرد از کجا بفهمیم دقیقا چه اتفاقی افتاده

اینجاست که AddressSanitizer یا ASan وارد میشه
ASan
یک ابزار برای پیدا کردن خطا های مربوط به حافظه در زمان اجرای برنامه است

ASan
چیکار میکنه
برنامه رو با یک سری بررسی‌ های اضافه اجرا میکنه

مثلا میتونه مواردی مثل اینا رو پیدا کنه:

Stack Buffer Overflow
Heap Buffer Overflow
Use After Free
Out of Bounds Access

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

یک مثال ساده:

فایل
C
#include <stdio.h>
#include <string.h>

int main()
{
char buf[8];

strcpy(buf,"AAAAAAAAAAAAAAAA");

printf("%s\n",buf);

return 0;
}



اینجا بافر فقط 8 بایت جا داره
ولی رشته خیلی بزرگ‌ تره

کامپایل با ASan
shell

gcc -g -fsanitize=address file21_demo.c -o file21_demo



حالا اجراش میکنیم
shell

./file21_demo

ASan
متوجه میشه که برنامه خارج از محدوده buf نوشته
و یک گزارش خطا نمایش میده

نکته مهم گزارش ASan

یکی از جذاب‌ ترین قسمت‌ های ASan اینه که معمولا اطلاعات مفیدی درباره خطا میده
مثلا مشخص میکنه

ERROR: AddressSanitizer
stack-buffer-overflow

بعد پایین‌ تر معمولا Stack Trace رو هم میبینید یعنی میفهمید خطا از چه تابعی و در چه خطی ایجاد شده

چرا برای Reverse Engineering مهمه؟

فرض کنید سورس کد ندارید یا برنامه خیلی پیچیده است اگر نسخه‌ای از برنامه رو با Instrumentation مناسب داشته باشید

ASan
میتونه بهتون کمک کنه بفهمید یک ورودی خاص دقیقا چه نوع Memory Bug ایجاد کرده
بعد همون نقطه رو میبرید داخل Ghidra یا IDA و از روی Assembly بررسیش کنید

یعنی مسیرمون میشه

Fuzzing
↓
Crash
↓
ASan
↓
محل خطا
↓
Ghidra / IDA
↓
تحلیل Assembly

این ترکیب برای پیدا کردن و تحلیل Memory Bug خیلی قدرتمنده

یک نکته مهم

ASan
خودش جلوی همه آسیب‌پذیری‌ ها رو نمیگیره کار اصلیش اینه که در محیط توسعه و تست

خطا های حافظه رو سریع‌ تر پیدا کنیم
برای همین معمولا کنار Fuzzing استفاده میشه


Fuzzer
برنامه با این ورودی خراب شد

ASan
کمک میکنه بفهمید
دقیقا چه نوع خطای حافظه‌ ای اتفاق افتاده و کجا

بعد Reverse Engineer میاد همون

قسمت رو با Ghidra یا IDA بررسی میکنه

تمرین این قسمت:

همین file21_demo.c رو با ASan کامپایل کنید
بعد گزارش ASan رو نگاه کنید و سه چیز رو پیدا کنید

نوع خطا

تابعی که خطا داخلش اتفاق افتاده

خط کدی که باعث خطا شده

@reverseengine
ReverseEngineering
بخش بیست و پنجم بافر اورفلو Address Sanitizer یا ASan شکار باگ‌ های حافظه تا اینجا یاد گرفتیم با Fuzzing میشه ورودی‌ های زیادی به برنامه داد و کرش‌ ها رو پیدا کرد نکته مهم وقتی برنامه کرش کرد از کجا بفهمیم دقیقا چه اتفاقی افتاده اینجاست که AddressSanitizer…
Part 25 Buffer Overflow


Address Sanitizer
or ASan Hunting for Memory Bugs

So far, we have learned that with Fuzzing, you can give a lot of input to the program and find crashes

Important point
When the program crashes, how do we find out exactly what happened

This is where AddressSanitizer or ASan comes in

ASan
is a tool for finding memory-related errors during program execution

What ASan does
It runs the program with a series of additional checks

For example, it can find things like:

Stack Buffer Overflow

Heap Buffer Overflow

Use After Free

Out of Bounds Access

That is, instead of just saying that the program crashed, you can find out where in the program the problem occurred

A simple example:

C

#include <stdio.h>

#include <string.h>

int main()

{
char buf[8];

strcpy(buf,"AAAAAAAAAAAAAA");

printf("%s\n",buf);

return 0;
}


Here the buffer is only 8 bytes

But the string is much larger

Compile with ASan

shell

gcc -g -fsanitize=address file21_demo.c -o file21_demo

Now we run it
shell

./file21_demo

ASan
notices that the program wrote outside the buf range

And displays an error report

Important point of ASan report

One of the most interesting parts of ASan is that it usually gives useful information about the error

For example, it specifies

ERROR: AddressSanitizer

stack-buffer-overflow

Then below you usually see the Stack Trace, which means you understand which function and line the error occurred

Why is it important for Reverse Engineering?

Suppose you don't have the source code or the program is very complex. If you have a version of the program with proper instrumentation,

ASan
can help you find out exactly what kind of memory bug a particular input caused.

Then you take that point into Ghidra or IDA and examine it from the assembly.

That is, our path will be

Fuzzing
↓
Crash
↓
ASan
↓
Error location
↓
Ghidra / IDA
↓
Assembly analysis

This combination is very powerful for finding and analyzing memory bugs.

An important point

ASan
itself does not prevent all vulnerabilities. Its main job is to find memory errors faster in the development and test environment.

That is why it is usually used together with Fuzzing.

Fuzzer
The program crashed with this input.

ASan
helps you find out exactly what kind of memory error occurred and where.

Then the reverse engineer comes and examines the same part with Ghidra or IDA.

Exercise of this part:

This is Compile file21_demo.c with ASan

Then look at the ASan report and find three things

Type of error

Function where the error occurred

Line of code that caused the error

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

Valgrind
چیه و چه فرقی با ASan داره
توی قسمت قبل با ASan آشنا شدیم
اینجا میخوایم بریم سراغ Valgrind و ببینیم چطور میتونه Memory Bugها رو پیدا کنه
بعد هم خیلی ساده ASan و Valgrind رو با هم مقایسه میکنیم


Valgrind
برنامه رو زیر نظر میگیره و دسترسی‌ های حافظه رو بررسی میکنه
مثلا میتونه مواردی مثل اینا رو پیدا کنه

Invalid Read
Invalid Write
Use After Free
Memory Leak
استفاده نادرست از حافظه

یک مثال ساده:
فایل
C
#include <stdio.h>
#include <stdlib.h>

int main()
{
int *data = malloc(4 * sizeof(int));

data[5] = 100;

free(data);

return 0;
}



اینجا فقط برای 4 عدد حافظه گرفتیم
ولی داریم عضو شماره 5 رو مینویسیم
پس یک Out of Bounds Write داریم

کامپایل
shell
gcc -g file22_demo.c -o file22_demo



بعد با Valgrind اجراش میکنیم
shell
valgrind ./file22_demo


Valgrind
گزارش میده که برنامه یک دسترسی غیرمجاز به حافظه داشته

قسمت مهمش برای Reverse Engineering

فرض کنید یک برنامه پیچیده دارید
برنامه کرش میکنه ولی هنوز نمیدونید مشکل دقیقا کجاست

Valgrind
میتونه Stack Trace و اطلاعات مربوط به دسترسی اشتباه رو نشون بده
بعد

میتونید همون تابع رو داخل Ghidra یا IDA باز کنید و Assembly اون قسمت رو بررسی کنید

یعنی دوباره این مسیر رو داریم

برنامه
↓
Valgrind
↓
Memory Error
↓
Stack Trace
↓
Ghidra / IDA
↓
Assembly Analysis
Valgrind
در مقابل ASan
خیلی ساده بخوایم بگیم
ASan
معمولا سریع‌ تره و برای Fuzzing و تست‌های مداوم خیلی کاربردیه
Valgrind
نیازی به کامپایل با ASan نداره و ابزارهای مختلفی برای تحلیل رفتار برنامه در اختیارمون میذاره البته Valgrind معمولا سربار اجرایی بیشتری داره

یک نکته مهم:

Valgrind
و ASan جای Reverse Engineering رو نمیگیرن

اونا فقط کمک میکنن سریع‌ تر بفهمیم
کجا باید دنبال مشکل بگردیم
بعد کار اصلی ما شروع میشه
یعنی رفتن داخل Ghidra یا IDA و فهمیدن اینکه چرا این Memory Bug اتفاق افتاده


تا اینجا سه ابزار مهم رو داریم
Fuzzer
↓
Crash پیدا می‌کنه

ASan / Valgrind
↓
Memory Bug رو تحلیل میکنن

Ghidra / IDA
↓
علت Bug رو از روی Binary بررسی میکنیم این دقیقا همون ترکیبیه که یک Reverse Engineer برای تحلیل Memory Bug ها باید کم‌ کم بهش مسلط بشه

@reverseengine
❤2
Part 26 Buffer Overflow



What is Valgrind and how is it different from ASan?

We met ASan in the previous section.

Here we want to go to Valgrind and see how it can find Memory Bugs.

Then we will compare ASan and Valgrind very simply.

Valgrind
monitors the program and checks memory accesses.

For example, it can find things like these:

Invalid Read

Invalid Write

Use After Free

Memory Leak

Incorrect memory usage


A simple example:

C file

#include <stdio.h>

#include <stdlib.h>

int main()

{
int *data = malloc(4 * sizeof(int));

data[5] = 100;

free(data);

return 0;
}


Here we only got 4 memory slots

But we are writing member number 5

So we have an Out of Bounds Write

Compile
shell

gcc -g file22_demo.c -o file22_demo


Then we run it with Valgrind
shell
valgrind ./file22_demo


Valgrind
reports that the program has an illegal memory access

The important part is for Reverse Engineering

Suppose you have a complex program

The program crashes but you still don't know exactly where the problem is

Valgrind
can show Stack Trace and information about the incorrect access

Then

You can open the same function in Ghidra or IDA and check the Assembly of that part

That means we have this path again

Program
↓
Valgrind
↓
Memory Error
↓
Stack Trace
↓
Ghidra / IDA
↓
Assembly Analysis


Valgrind vs. ASan
To put it simply
ASan
is usually faster and for Fuzzing and continuous testing are very useful
Valgrind
Does not require compilation with ASan and provides us with various tools to analyze the behavior of the program, of course Valgrind usually has more execution overhead
An important point:
Valgrind
and ASan do not replace Reverse Engineering
They only help us understand faster
Where to look for the problem
Then our main work begins
That is, going into Ghidra or IDA and understanding why this Memory Bug occurred
So far we have three important
tools

Fuzzer
↓
Finds a crash
ASan / Valgrind
↓
Analyzes the Memory Bug

Ghidra / IDA
↓


We examine the cause of the Bug from the Binary This is exactly the combination that a Reverse Engineer should gradually master to analyze Memory Bugs

@reverseengine
بخش بیست و هفتم بافر اورفلو


libFuzzer و Coverage Guided


Fuzzing
تا اینجا فهمیدیم Fuzzing یعنی دادن تعداد زیادی ورودی مختلف به برنامه و منتظر موندن تا یک جایی خرابکاری کنه😁
ولی Fuzzer های جدید فقط ورودی رندوم تولید نمیکنن بعضی از اونها بررسی میکنن هر ورودی برنامه رو از چه مسیر هایی عبور داده و همین باعث میشه کم کم ورودی‌ های جالب‌ تر تولید کنه

Coverage Guided یعنی چی

فرض کن یک برنامه این شکلیه

Input
↓
Check 1
↓
Check 2
↓
Hidden Function
اگر ورودی اول فقط به Check 1 برسه

سعی میکنه ورودی مسیر رو تغییر بده تا Fuzzer جدیدی باز بشه
مثلا به Check 2 برسه
بعد دوباره از همون ورودی استفاده میکنه و تغییرات بیشتری میده

هدف اینه که قسمت‌ های بیشتری از برنامه اجرا بشن چون خب ظاهرا ما تصمیم گرفتیم برای پیدا کردن باگ باید به همه جای برنامه سرک بکشیم 😅

libFuzzer
چیکار میکنه
libFuzzer
یک موتور Fuzzing برای برنامه‌های C و ++C است که با LLVM و Clang کار میکنه ما یک تابع مشخص به اون میدیم
بعد خودش بار ها و بار ها اون تابع رو با ورودی‌ های مختلف اجرا میکنه
هر ورودی که باعث رسیدن به مسیر جدیدی بشه ارزشمند تر میشه

تابع اصلی Fuzzing

معمولا چیزی شبیه این داریم:
C
#include <stdint.h>
#include <stddef.h>

int LLVMFuzzerTestOneInput(
const uint8_t *data,
size_t size
)
{
return 0;
}



توضیح کد زیر
این تابع هدف Fuzzer هست
هر بار libFuzzer یک ورودی جدید تولید میکنه محتوای ورودی داخل data قرار میگیره و اندازه اون داخل size قرار میگیره

یک مثال ساده:
C
#include <stdint.h>
#include <stddef.h>
#include <string.h>

int LLVMFuzzerTestOneInput(
const uint8_t *data,
size_t size
)
{
if (size >= 5)
{
if (memcmp(data, "HELLO", 5) == 0)
{
volatile int x = 1;
(void)x;
}
}

return 0;
}




اینجا چه اتفاقی میوفته
Fuzzer
ورودی‌ های مختلف رو امتحان میکنه

مثلا

AAAAA


بعد

HELAA


بعد شاید

HELLO


وقتی ورودی به HELLO برسه
یک مسیر جدید از برنامه اجرا میشه
Coverage Guided Fuzzing
این مسیر جدید رو تشخیص میده
و اون ورودی رو نگه میداره تا از اون برای پیدا کردن مسیرهای بعدی استفاده کنه

کامپایل با Clang
shell
clang -g -fsanitize=fuzzer,address file23_fuzz.c -o file23_fuzz



اینجا دو چیز با هم فعال شده

libFuzzer
+
AddressSanitizer


libFuzzer
ورودی تولید میکنه
ASan
مراقب خطا های حافظه هست
این ترکیب برای پیدا کردن Memory Bug خیلی قدرتمنده

اجرای Fuzzer
shell
./file23_fuzz


بعد برنامه شروع میکنه به تولید و تغییر ورودی‌ ها
اگر ورودی باعث کرش بشه معمولا همون ورودی ذخیره میشه

تا بتونیم بعدا دوباره بررسیش کنیم

چرا برای ما مهمه؟

فرض کنید یک برنامه پیچیده دارید
ولی دقیقا نمیدونید چه ورودی باعث رسیدن به یک تابع حساس میشه

Fuzzer
میتونه با امتحان کردن ورودی‌ های مختلف مسیر های جدید رو پیدا کنه

بعد شما میتونید همون مسیر ها رو داخل Ghidra یا IDA بررسی کنید

یعنی:

Fuzzing
↓
New Code Path
↓
Crash یا Behavior
↓
Ghidra / IDA
↓
Assembly Analysis


Coverage Guided Fuzzing
فقط دنبال کرش نیست
دنبال مسیر های جدید هم هست
هر مسیر جدید یعنی بخش جدیدی از برنامه که ارزش بررسی داره و وقتی libFuzzer رو با ASan ترکیب میکنیم هم میتونیم ورودی‌ های هوشمندانه‌ تر تولید کنیم هم Memory Bug ها رو سریع‌ تر تشخیص بدیم

تمرین:

تابع بالا رو کمی تغییر بدید و یک شرط جدید برای یک ورودی خاص اضافه کنید بعد فکر کنید Fuzzer چطور باید قدم به قدم ورودی رو تغییر بده تا به اون مسیر جدید برسه

@reverseengine
❤1
ReverseEngineering
بخش بیست و هفتم بافر اورفلو libFuzzer و Coverage Guided Fuzzing تا اینجا فهمیدیم Fuzzing یعنی دادن تعداد زیادی ورودی مختلف به برنامه و منتظر موندن تا یک جایی خرابکاری کنه😁 ولی Fuzzer های جدید فقط ورودی رندوم تولید نمیکنن بعضی از اونها بررسی میکنن هر ورودی…
Part 27 Buffer Overflow


libFuzzer and Coverage Guided

Fuzzing

So far, we have understood that Fuzzing means giving a lot of different inputs to the program and waiting for it to mess up somewhere😁
But new Fuzzers don't just generate random inputs. Some of them check what paths each input has taken in the program, which makes it gradually generate more interesting inputs

What does Coverage Guided mean

Suppose a program looks like this

Input
↓
Check 1
↓
Check 2
↓
Hidden Function


If the first input only reaches Check 1

It tries to change the input path so that a new Fuzzer opens

For example,

it reaches Check 2

Then it uses the same input again and makes more changes

The goal is to run more parts of the program because apparently we decided to go everywhere in the program to find the bug 😅

libFuzzer
What does libFuzzer do

A Fuzzing Engine for C Programs And it's C++ that works with LLVM and Clang. We give it a specific function.

Then it runs that function over and over again with different inputs.
Each input that leads to a new path becomes more valuable.

The main Fuzzing function

Usually we have something like this:

C
#include <stdint.h>
#include <stddef.h>

int LLVMFuzzerTestOneInput(
const uint8_t *data,
size_t size
)
{
return 0;
}


Explanation of the code below
This function is the target of the Fuzzer
Every time libFuzzer generates a new input, the content of the input is placed in data and its size is placed in size

A simple example:

C
#include <stdint.h>
#include <stddef.h>
#include <string.h>

int LLVMFuzzerTestOneInput(
const uint8_t *data,
size_t size
)
{
if (size >= 5)
{
if (memcmp(data, "HELLO", 5) == 0)
{
volatile int x = 1;
(void)x;
}
}

return 0;
}


What happens here
The Fuzzer
trys different inputs

For example

AAAA


then

HELAA


then maybe

HELLO


When the input reaches HELLO
a new path is executed from the program

Coverage Guided Fuzzing
detects this new path

and stores that input to use for finding subsequent paths

Compile with Clang:

shell
clang -g -fsanitize=fuzzer,address file23_fuzz.c -o file23_fuzz


Here two things are enabled together

libFuzzer
+
AddressSanitizer


libFuzzer
generates input

ASan
watches for memory errors

This combination is very powerful for finding memory bugs

Run Fuzzer

shell
./file23_fuzz


Then the program starts generating and modifying inputs

If the input causes a crash, usually the same input is saved

so that we can check it again later Let's do

Why is it important to us?

Suppose you have a complex program

But you don't know exactly what input will cause a critical function to be reached

Fuzzer
can find new paths by trying different inputs

Then you can examine those paths in Ghidra or IDA

That is:

Fuzzing
↓
New Code Path
↓
Crash or Behavior
↓
Ghidra / IDA
↓
Assembly Analysis


Coverage Guided Fuzzing
It doesn't just look for crashes
It also looks for new paths
Each new path is a new part of the program that is worth examining and when we combine libFuzzer with ASan we can both generate smarter inputs and detect memory bugs faster

Exercise:

Change the above function a little and add a new condition for a specific input then think about how the Fuzzer should change the input step by step to reach that new path

@reverseengine
❤1
بخش بیست و هشتم بافر اورفلو


AFL++ و Instrumentation

چطور Fuzzer میفهمه داخل برنامه چه خبره

توی بخش قبل با libFuzzer و Coverage Guided Fuzzing آشنا شدیم
حالا میریم سراغ AFL++

AFL++
یکی از معروف ترین ابزارهای Fuzzing هست که مخصوصا برای تست برنامه های C و ++C و Binary ها خیلی استفاده میشه
میخوایم بفهمیم AFL++ چطور متوجه میشه یه ورودی برنامه رو وارد یه مسیر جدید کرده

AFL++
فقط ورودی تصادفی نمیفرسته
فرض کن یه برنامه داریم که مسیرهای مختلفی داره

Input
│
▼
┌─────────┐
│ Check A │
└────┬────┘
│
┌────┴────┐
▼ ▼
Path 1 Path 2
│
▼
┌─────────┐
│ Check B │
└────┬────┘
│
┌────┴────┐
▼ ▼
Path 3 Path 4

اگر AFL++ یه ورودی بفرسته و برنامه وارد Path 1 بشه
اون مسیر ثبت میشه
بعد AFL++ ورودی رو تغییر میده و دوباره امتحانش میکنه
اگر ورودی جدید باعث بشه برنامه وارد Path 2 بشه

AFL++
متوجه میشه یه مسیر جدید پیدا شده
همین ورودی جدید ارزشمند میشه و نگهش میداره
Instrumentation
یعنی چی
اینجا میرسیم به بخش مهم

Instrumentation
یعنی اضافه کردن یه سری مکانیزم به برنامه تا بتونیم بفهمیم موقع اجرا چه اتفاقی داخلش افتاده

مثلا AFL++ میتونه با Instrumentation اطلاعاتی درباره مسیر اجرای برنامه جمع کنه

به زبون ساده:

قبل از Instrumentation

Input
│
▼
Program
│
▼
Result

بعد از Instrumentation

Input
│
▼
Program
│
▼
Path Tracking
│
▼
Result

یعنی برنامه همچنان کار خودش رو انجام میده ولی حالا یه نفر هم داره بررسی میکنه برنامه از چه مسیرهایی رد شده
اسم این کار رو گذاشتن Instrumentation تا قضیه یکم علمی تر به نظر بیاد

یه مثال ساده:

فرض کنید این برنامه رو داریم
C
#include <stdio.h>
#include <string.h>

int main(void)
{
char input[32];

if (!fgets(input, sizeof(input), stdin))
return 0;

if (strncmp(input, "HELLO", 5) == 0)
{
puts("First check passed");

if (input[5] == '!')
{
puts("Second check passed");
}
}

return 0;
}



AFL++
اینجا دنبال چیه
اول ممکنه ورودی های ساده رو امتحان کنه

AAAA

این ورودی فقط یه مسیر معمولی رو اجرا میکنه
بعد AFL++ شروع میکنه ورودی رو تغییر دادن
اگر به این برسه

HELLO

یه شاخه جدید اجرا میشه
پس AFL++ متوجه میشه این ورودی جالبه
بعد همین ورودی رو بیشتر تغییر میده
مثلا:


HELLO!

حالا شرط دوم هم رد شده
پس یه مسیر جدید دیگه پیدا شده
به صورت ساده میتونیم این روند رو اینطوری ببینیم

AAAA
│
▼
مسیر معمولی
│
▼
AFL++ تغییر میده
│
▼
HELLO
│
▼
مسیر جدید
│
▼
AFL++ دوباره تغییر میده
│
▼
HELLO!
│
▼
مسیر جدیدتر

Seed یا Corpus
یعنی چی

AFL++
معمولا با یه سری ورودی اولیه شروع میکنه به این ورودی های اولیه میگیم Seed
مثلا یه فایل ساده

project
├── input
│ └── seed1
└── output

داخل seed1 میتونه فقط این باشه

AAAA

بعد AFL++ همین ورودی رو بارها تغییر میده
حذف میکنه
اضافه میکنه
بایت ها رو تغییر میده
و بررسی میکنه کدوم تغییر باعث شده یه مسیر جدید پیدا بشه
ورودی هایی که ارزش داشته باشن میتونن در Corpus قرار بگیرن

Corpus
یعنی مجموعه ای از ورودی های جالب که Fuzzer میتونه از اونها برای ادامه Fuzzing استفاده کنه
پس میتونیم این روند رو اینطوری تصور کنیم

Seed
│
▼
Mutation
│
├── Input A ──► مسیر جدید نیست
│
├── Input B ──► مسیر جدید
│ │
│ ▼
│ Corpus
│
└── Input C ──► مسیر جدیدتر
│
▼
Corpus

یک مثال از ساختار فایل ها
فرض کنید این پوشه رو داریم

project
├── input
│ └── seed1
└── output

داخل seed1 میتونه فقط این باشه

AAAA

بعد AFL++ با همین ورودی شروع میکنه و به مرور ورودی های جدید تولید میکنه
یک نکته مهم
وقتی AFL++ یه Crash پیدا میکنه
کار تموم نشده تازه قسمت جذاب ماجرا شروع میشه

باید بفهمیم
چه ورودی باعث Crash شده

Crash
دقیقا کجا اتفاق افتاده
چه تابعی درگیر بوده
آیا مشکل واقعا یه Memory Bug هست یا نه
❤1
ReverseEngineering
بخش بیست و هشتم بافر اورفلو AFL++ و Instrumentation چطور Fuzzer میفهمه داخل برنامه چه خبره توی بخش قبل با libFuzzer و Coverage Guided Fuzzing آشنا شدیم حالا میریم سراغ AFL++ AFL++ یکی از معروف ترین ابزارهای Fuzzing هست که مخصوصا برای تست برنامه های C…
Part 28 Buffer Overflow


AFL++ and Instrumentation

How does a Fuzzer understand what is going on inside a program

In the previous section, we learned about libFuzzer and Coverage Guided Fuzzing

Now let's move on to AFL++

AFL++
is one of the most famous fuzzing tools, which is especially used for testing C, C++, and binary programs. We want to understand how AFL++ understands that a program input has entered a new path

AFL++
does not just send random input
Suppose we have a program that has different paths

Input
│
▼
┌────────┐
│ Check A │
└────┬────┘
│
┌─────┴───┐
▼ ▼
Path 1 Path 2
│
▼
┌───────┐
│ Check B │
└──────┬───┘
│
┌──────┐
│ ┌──────┐
▼ ▼
Path 3 Path 4

If AFL++ sends an input and the program enters Path 1

That path is recorded

Then AFL++ changes the input and tries it again

If the new input causes the program to enter Path 2

AFL++
notices that a new path has been found

This new input becomes valuable and keeps it

What does Instrumentation mean

Here we come to the important part

Instrumentation
means adding a series of mechanisms to the program so that we can understand what happened inside it during execution

For example, AFL++ can collect information about the path of the program execution with Instrumentation

In simple terms:

Before Instrumentation

Input
│
▼
Program
│
▼
Result

After Instrumentation

Input
│
▼
Program
│
▼
Path Tracking
│
▼
Result

That means the program is still doing its job, but now someone is also checking which paths the program has taken
I named this task Instrumentation to make it seem a little more scientific

An example Simple:

Suppose we have this program

C
#include <stdio.h>
#include <string.h>

int main(void)
{
char input[32];

if (!fgets(input, sizeof(input), stdin))
return 0;

if (strncmp(input, "HELLO", 5) == 0)
{
puts("First check passed");

if (input[5] == '!')
{
puts("Second check passed");
}
}

return 0;
}


What is AFL++ looking for here?
First it might try simple inputs

AAAA

This input just executes a normal path
Then AFL++ starts modifying the input
If it reaches

HELLO

a new branch is executed
So AFL++ realizes that this input is interesting
Then it modifies this input further
For example:

HELLO!

Now the second condition is also met
So another new path has been found
Simply we can see this process like this

AAAA
│
▼
Normal path
│
▼
AFL++ changes
│
▼
HELLO
│
▼
New path
│
▼
AFL++ changes again
│
▼
HELLO!
│
▼
Newer path

Seed or Corpus
What does it mean

AFL++
Usually starts with a series of initial inputs, we call these initial inputs Seed
For example, a simple file

project
├── input
│ └── seed1
└── output

Inside seed1 can be just this

AAAA

Then AFL++ changes this input repeatedly
deletes
adds
changes bytes
and checks which change caused a new path to be found
Inputs that are worth it can be placed in Corpus

Corpus
means a set of interesting inputs that the Fuzzer can use to continue Fuzzing
So we can imagine this process like this

Seed
│
▼
Mutation
│
├── Input A ──► Not a new path
│
├── Input B ──► New path
│ │
│ ▼
│ Corpus
│
└── Input C ──► Path Newer
│
▼
Corpus

An example of file structure
Suppose we have this folder

project
├── input
│ └── seed1
└── output

Inside seed1 can be just this

AAAA

Then AFL++ starts with this input and gradually generates new inputs
An important point
When AFL++ finds a Crash
The work is not over yet, the interesting part begins
We need to find out
What input caused the Crash
Where exactly did the Crash happen
What function was involved
Is the problem really a Memory Bug or not
بخش سی ام بافر اورفلو


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]

پس برنامه داره خارج از محدوده آرایه به حافظه دسترسی پیدا میکنه
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

میتونیم این مسیر رو ذهنی تبدیل کنیم به
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