بخش بیست و دوم بافر اورفلو
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
اینجا بافر فقط 8 بایت جا داره
ولی رشته خیلی بزرگ تره
کامپایل با ASan
shell
حالا اجراش میکنیم
shell
./file21_demo
ASan
متوجه میشه که برنامه خارج از محدوده
و یک گزارش خطا نمایش میده
نکته مهم گزارش 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 بررسی میکنه
تمرین این قسمت:
همین
بعد گزارش ASan رو نگاه کنید و سه چیز رو پیدا کنید
نوع خطا
تابعی که خطا داخلش اتفاق افتاده
خط کدی که باعث خطا شده
@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
برنامه رو زیر نظر میگیره و دسترسی های حافظه رو بررسی میکنه
مثلا میتونه مواردی مثل اینا رو پیدا کنه
یک مثال ساده:
فایل
C
اینجا فقط برای 4 عدد حافظه گرفتیم
ولی داریم عضو شماره 5 رو مینویسیم
پس یک Out of Bounds Write داریم
کامپایل
shell
بعد با Valgrind اجراش میکنیم
shell
Valgrind
گزارش میده که برنامه یک دسترسی غیرمجاز به حافظه داشته
قسمت مهمش برای Reverse Engineering
فرض کنید یک برنامه پیچیده دارید
برنامه کرش میکنه ولی هنوز نمیدونید مشکل دقیقا کجاست
Valgrind
میتونه Stack Trace و اطلاعات مربوط به دسترسی اشتباه رو نشون بده
بعد
میتونید همون تابع رو داخل Ghidra یا IDA باز کنید و Assembly اون قسمت رو بررسی کنید
یعنی دوباره این مسیر رو داریم
برنامه
در مقابل 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
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
Valgrind
↓
Memory Error
↓
Stack Trace
↓
Ghidra / IDA
↓
Assembly Analysis
در مقابل 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 1
↓
Check 2
↓
Hidden Function
سعی میکنه ورودی مسیر رو تغییر بده تا 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
↓
junk ^ 123
↓
X
و هیچ مسیری به
در نتیجه وقتی هدفمون
هست
داخل Backward Slice مربوط به
اینجاست که میبینیم Slicing چرا مفیده
به جای اینکه تمام دستورهای تابع رو هم وزن ببینیم
فقط دستورهایی رو نگه میداریم که به داده موردنظر ما وابستگی دارن
همین کار رو روی Assembly انجام بدیم
فرض میکنیم کامپایلر تابع رو تقریبا به این شکل تبدیل کرده
mov eax, edi
add eax, 5
imul eax, 3
sub eax, 7
ret
حالا از پایین به بالا نگاه میکنیم
دستور
داخل این مثال مقدار خروجی داخل
پس میریم دستور قبل
sub eax, 7
یعنی مقدار
قبل از اون
imul eax, 3
باز هم همون مقدار
بعد
add eax, 5
باز هم
و در اخر
mov eax, 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
میتونیم این مسیر رو ذهنی تبدیل کنیم به
با 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
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