Defeating AI-Assisted Reverse Engineering (or at Least Trying To)
https://blog.quarkslab.com/defeating-ai-assisted-reverse-engineering-or-at-least-trying-to.html
@reverseengine
https://blog.quarkslab.com/defeating-ai-assisted-reverse-engineering-or-at-least-trying-to.html
@reverseengine
Quarkslab
Defeating AI-Assisted Reverse Engineering (or at Least Trying To) - Quarkslab's blog
Is LLM-assisted reverse engineering making obfuscation pointless? We spent a couple of weeks trying to find out, by handing sandboxed agents a series of progressively hardened AArch64 binaries and one prompt: recover the hidden strings inside. This post walks…
Vtable
چیه و چرا داخل ++C اهمیت داره؟
پست قبل دربارهی Function Pointer صحبت کردیم و دیدیم که برنامه میتونه آدرس یک تابع رو نگه داره و بعدا از طریق اون تابعی رو اجرا کنه
حالا در ++C یک مفهوم مشابه ولی پیچیده تر داریم:
Vtable
یا:
Virtual Table
این موضوع برای فهم ساختار داخلی Object های C++ خیلی مهمه
Virtual Function
در ++C ممکنه یک کلاس تابعی داشته باشه که با کلمهی virtual تعریف شده:
class Animal {
public:
virtual void speak() {
std::cout << "Animal";
}
};
حالا یک کلاس دیگه از اون ارثبری میکنه:
class Dog : public Animal {
public:
void speak() override {
std::cout << "Dog";
}
};
اینجا نکته اینه که اگر با یک Pointer از نوع Animal به یک Object از نوع Dog اشاره کنیم برنامه باید بدونه در نهایت کدوم نسخه از ()speak اجرا میشه
یعنی:
Animal Pointer
│
▼
Dog Object
│
▼
Dog::speak()
برای حل این مسئله معمولا از مکانیزمی مثل Vtable استفاده میکنیم
Vtable
رو میتونیم یک جدول در نظر بگیریم که شامل آدرس توابع Virtual یک کلاس هست
به شکل ساده:
Vtable
+----------------------+
| &function_1 |
+----------------------+
| &function_2 |
+----------------------+
| &function_3 |
+----------------------+
هر Object از کلاسی که Virtual Function دارده معمولا به این جدول مرتبطه
برای این ارتباط کامپایلر معمولا چیزی شبیه یک Pointer داخلی به نام:
vptr
داخل Object قرار میده
پس یک Object ممکنه به شکل مفهومی این طوری باشه:
Object
+----------------------+
| vptr --------------+----------+
+----------------------+ |
| Data | ▼
| Data | Vtable
+----------------------+ +------------------+
| &Virtual Function|
+------------------+
| &Virtual Function|
+------------------+
vptr به Vtable
مربوط به نوع واقعی Object اشاره میکنه
وقتی Virtual Function صدا زده میشه چه اتفاقی میوفته؟
فرض کنید این کد رو داریم:
Animal *animal = new Dog();
animal->speak();
برنامه نمیتونه فقط بر اساس نوع Pointer تصمیم بگیره چون نوع واقعی Object ممکنه Dog باشه
پس بهصورت مفهومی این مسیر رو طی میکنه:
animal
│
▼
Object
│
▼
vptr
│
▼
Vtable
│
▼
آدرس speak()
│
▼
اجرای تابع
یعنی برنامه از اطلاعات داخلی Object کمک میگیره تا بفهمه کدوم تابع باید اجرا بشه
چرا Vtable برای Binary Analysis مهمه؟
وقتی یک برنامهی ++C رو Reverse میکنیم Vtable ها میتونن اطلاعات مفیدی درباره ی ساختار برنامه به ما بدن
مثلا با پیدا کردن یک Vtable ممکنه بتونیم حدس بزنیم:
یک Class چه Virtual Function هایی داره
چه Class هایی به هم مرتبطن
ساختار Object تقریبا چجوریه
کدوم توابع به یک Class مربوط میشن
برای همین Vtable ها در Reverse برنامههای ++C اهمیت زیادی دارن
بعضی وقتا اسم توابع از بین رفته و Symbol ها هم وجود ندارن چون انسان ها ظاهرا تصمیم گرفتن برنامهها رو بدون برچسب ول بکنن در این شرایط ساختار هایی مثل Vtable میتونن سرنخ مهمی باشن
ارتباط Vtable با Memory Corruption
نکتهی مهم اینجاست که vptr خودش یک Pointer هست
یعنی Object ممکنه چیزی شبیه این داشته باشه:
Object
+------------------+
| vptr | ← Pointer
+------------------+
| member_1 |
+------------------+
| member_2 |
+------------------+
اگر یک آسیب پذیری حافظه باعث خراب شدن دادههای Object بشه در تحلیل باید بررسی کنیم:
کدوم قسمتهای Object تحت تاثیر قرار گرفتن؟
اگر یک Pointer مهم در ساختار Object قرار داشته باشه تغییر دادن اون میتونه رفتار بعدی برنامه رو تغییر بده
اما باز هم باید تفاوت مهمی رو یادمون باشه:
خراب شدن یک Object لزوما به معنی کنترل اجرای برنامه نیست
باید بررسی کنیم برنامه بعدا با دادهی خراب شده چکاری انجام میده و چه مکانیزم های محافظتی وجود داره
Vtable با Function Pointer
چه فرقی داره؟
از نظر مفهوم هر دو به آدرس توابع مربوطن
اما تفاوت دارن
Function Pointer
یک متغیر مستقیما آدرس یک تابع رو نگه میداره:
Function Pointer
│
▼
Address of Code
Vtable
چیه و چرا داخل ++C اهمیت داره؟
پست قبل دربارهی Function Pointer صحبت کردیم و دیدیم که برنامه میتونه آدرس یک تابع رو نگه داره و بعدا از طریق اون تابعی رو اجرا کنه
حالا در ++C یک مفهوم مشابه ولی پیچیده تر داریم:
Vtable
یا:
Virtual Table
این موضوع برای فهم ساختار داخلی Object های C++ خیلی مهمه
Virtual Function
در ++C ممکنه یک کلاس تابعی داشته باشه که با کلمهی virtual تعریف شده:
class Animal {
public:
virtual void speak() {
std::cout << "Animal";
}
};
حالا یک کلاس دیگه از اون ارثبری میکنه:
class Dog : public Animal {
public:
void speak() override {
std::cout << "Dog";
}
};
اینجا نکته اینه که اگر با یک Pointer از نوع Animal به یک Object از نوع Dog اشاره کنیم برنامه باید بدونه در نهایت کدوم نسخه از ()speak اجرا میشه
یعنی:
Animal Pointer
│
▼
Dog Object
│
▼
Dog::speak()
برای حل این مسئله معمولا از مکانیزمی مثل Vtable استفاده میکنیم
Vtable
رو میتونیم یک جدول در نظر بگیریم که شامل آدرس توابع Virtual یک کلاس هست
به شکل ساده:
Vtable
+----------------------+
| &function_1 |
+----------------------+
| &function_2 |
+----------------------+
| &function_3 |
+----------------------+
هر Object از کلاسی که Virtual Function دارده معمولا به این جدول مرتبطه
برای این ارتباط کامپایلر معمولا چیزی شبیه یک Pointer داخلی به نام:
vptr
داخل Object قرار میده
پس یک Object ممکنه به شکل مفهومی این طوری باشه:
Object
+----------------------+
| vptr --------------+----------+
+----------------------+ |
| Data | ▼
| Data | Vtable
+----------------------+ +------------------+
| &Virtual Function|
+------------------+
| &Virtual Function|
+------------------+
vptr به Vtable
مربوط به نوع واقعی Object اشاره میکنه
وقتی Virtual Function صدا زده میشه چه اتفاقی میوفته؟
فرض کنید این کد رو داریم:
Animal *animal = new Dog();
animal->speak();
برنامه نمیتونه فقط بر اساس نوع Pointer تصمیم بگیره چون نوع واقعی Object ممکنه Dog باشه
پس بهصورت مفهومی این مسیر رو طی میکنه:
animal
│
▼
Object
│
▼
vptr
│
▼
Vtable
│
▼
آدرس speak()
│
▼
اجرای تابع
یعنی برنامه از اطلاعات داخلی Object کمک میگیره تا بفهمه کدوم تابع باید اجرا بشه
چرا Vtable برای Binary Analysis مهمه؟
وقتی یک برنامهی ++C رو Reverse میکنیم Vtable ها میتونن اطلاعات مفیدی درباره ی ساختار برنامه به ما بدن
مثلا با پیدا کردن یک Vtable ممکنه بتونیم حدس بزنیم:
یک Class چه Virtual Function هایی داره
چه Class هایی به هم مرتبطن
ساختار Object تقریبا چجوریه
کدوم توابع به یک Class مربوط میشن
برای همین Vtable ها در Reverse برنامههای ++C اهمیت زیادی دارن
بعضی وقتا اسم توابع از بین رفته و Symbol ها هم وجود ندارن چون انسان ها ظاهرا تصمیم گرفتن برنامهها رو بدون برچسب ول بکنن در این شرایط ساختار هایی مثل Vtable میتونن سرنخ مهمی باشن
ارتباط Vtable با Memory Corruption
نکتهی مهم اینجاست که vptr خودش یک Pointer هست
یعنی Object ممکنه چیزی شبیه این داشته باشه:
Object
+------------------+
| vptr | ← Pointer
+------------------+
| member_1 |
+------------------+
| member_2 |
+------------------+
اگر یک آسیب پذیری حافظه باعث خراب شدن دادههای Object بشه در تحلیل باید بررسی کنیم:
کدوم قسمتهای Object تحت تاثیر قرار گرفتن؟
اگر یک Pointer مهم در ساختار Object قرار داشته باشه تغییر دادن اون میتونه رفتار بعدی برنامه رو تغییر بده
اما باز هم باید تفاوت مهمی رو یادمون باشه:
خراب شدن یک Object لزوما به معنی کنترل اجرای برنامه نیست
باید بررسی کنیم برنامه بعدا با دادهی خراب شده چکاری انجام میده و چه مکانیزم های محافظتی وجود داره
Vtable با Function Pointer
چه فرقی داره؟
از نظر مفهوم هر دو به آدرس توابع مربوطن
اما تفاوت دارن
Function Pointer
یک متغیر مستقیما آدرس یک تابع رو نگه میداره:
Function Pointer
│
▼
Address of Code
Vtable
❤2
یک Object معمولا اول به یک جدول اشاره میکنه و اون جدول شامل آدرس توابع هست:
Object
│
▼
vptr
│
▼
Vtable
│
├── Function A
├── Function B
└── Function C
بنابراین Vtable یک سطح ساختارمند تر برای مدیریت توابع Virtual فراهم میکنه
Vtable
دقیقاً یک استاندارد رسمی ++C هست؟
یک نکته ی مهم:
خود مفهوم Vtable به شکل دقیق در استاندارد C++ تعریف نشده
Vtable
در واقع یک روش رایج برای پیاده سازی Polymorphism توسط کامپایلر هاست
یعنی ممکنه جزئیات پیادهسازی بین:
GCC
Clang
MSVC
متفاوت باشه
اما ایدهی کلی یعنی استفاده از اطلاعاتی برای پیدا کردن تابع درست در زمان اجرا در عمل بسیار رایجه
برای همین هنگام Reverse باید همیشه به ABI و کامپایلری که برنامه با اون ساخته شده توجه کنیم
Vtable
معمولا جدولی شامل آدرس Virtual Function های یک کلاس در ++C هست Object هایی که از Virtual Function استفاده میکنن اغلب یک Pointer داخلی مثل vptr دارن که اونها رو به Vtable مربوط میکنه موقع اجرای یک Virtual Function برنامه از این ساختار برای پیدا کردن تابع مناسب استفاده میکنه
شناخت Vtable هم در Reverse کردن برنامه های ++C مهمه و هم برای درک اینکه Object ها در حافظه چجوری سازماندهی میشن
@reverseengine
Object
│
▼
vptr
│
▼
Vtable
│
├── Function A
├── Function B
└── Function C
بنابراین Vtable یک سطح ساختارمند تر برای مدیریت توابع Virtual فراهم میکنه
Vtable
دقیقاً یک استاندارد رسمی ++C هست؟
یک نکته ی مهم:
خود مفهوم Vtable به شکل دقیق در استاندارد C++ تعریف نشده
Vtable
در واقع یک روش رایج برای پیاده سازی Polymorphism توسط کامپایلر هاست
یعنی ممکنه جزئیات پیادهسازی بین:
GCC
Clang
MSVC
متفاوت باشه
اما ایدهی کلی یعنی استفاده از اطلاعاتی برای پیدا کردن تابع درست در زمان اجرا در عمل بسیار رایجه
برای همین هنگام Reverse باید همیشه به ABI و کامپایلری که برنامه با اون ساخته شده توجه کنیم
Vtable
معمولا جدولی شامل آدرس Virtual Function های یک کلاس در ++C هست Object هایی که از Virtual Function استفاده میکنن اغلب یک Pointer داخلی مثل vptr دارن که اونها رو به Vtable مربوط میکنه موقع اجرای یک Virtual Function برنامه از این ساختار برای پیدا کردن تابع مناسب استفاده میکنه
شناخت Vtable هم در Reverse کردن برنامه های ++C مهمه و هم برای درک اینکه Object ها در حافظه چجوری سازماندهی میشن
@reverseengine
❤2
ReverseEngineering
Vtable چیه و چرا داخل ++C اهمیت داره؟ پست قبل دربارهی Function Pointer صحبت کردیم و دیدیم که برنامه میتونه آدرس یک تابع رو نگه داره و بعدا از طریق اون تابعی رو اجرا کنه حالا در ++C یک مفهوم مشابه ولی پیچیده تر داریم: Vtable یا: Virtual Table این…
Vtable
What is it and why is it important in C++?
In the previous post, we talked about Function Pointer and saw that the program can hold the address of a function and later execute a function through it
Now in C++ we have a similar but more complex concept:
Vtable
Or:
Virtual Table
This is very important to understand the internal structure of C++ Objects
Virtual Function
In C++, a class may have a function that is defined with the virtual keyword:
class Animal {
public:
virtual void speak() {
std::cout << "Animal";
}
};
Now another class inherits from it:
class Dog : public Animal {
public:
void speak() override {
std::cout << "Dog";
}
};
The point here is that if we point to an Object of type Dog with a Pointer of type Animal, the program needs to know which version of speak() will be executed in the end
That is:
Animal Pointer
│
▼
Dog Object
│
▼
Dog::speak()
To solve this problem, we usually use a mechanism like Vtable
We can consider Vtable
as a table that contains the addresses of Virtual functions of a class
Simply:
Vtable
+--------------------+
| &function_1 |
+-----------------------+
| &function_2 |
+-----------------------+
| &function_3 |
+-----------------------+
Every Object of a class that has a Virtual Function is usually linked to this table
For this connection, the compiler usually puts something like an internal Pointer called:
vptr
inside the Object
So an Object may conceptually look like this:
Object
+-----------------------+
| vptr --------------+------+
+----------------------+ |
| Data | ▼
| Data | Vtable
+---------+ +------------------+
| &Virtual Function|
+------------------+
| &Virtual Function|
+------------------+
vptr points to the Vtable
of the actual type of Object
What happens when a Virtual Function is called?
Suppose we have this code:
Animal *animal = new Dog();
animal->speak();
The program cannot decide based on the Pointer type alone because the actual type of Object may be Dog
So conceptually it goes like this:
animal
│
▼
Object
│
▼
vptr
│
▼
Vtable
│
▼
speak() address
│
▼
function execution
That is, the program uses the internal information of the Object to understand which function to execute
Why is the Vtable important for Binary Analysis?
When we reverse a C++ program, Vtables can give us useful information about the program structure.
For example, by finding a Vtable, we may be able to guess:
What virtual functions a class has
What classes are related to each other
What is the approximate structure of an object
Which functions are related to a class
That is why Vtables are so important in reversing C++ programs
Sometimes the function names are missing and the symbols are missing because humans apparently decided to leave programs unlabeled. In these situations, structures like Vtables can be important clues
The relationship of Vtables to Memory Corruption
The important point here is that vptr itself is a Pointer
That is, an Object might look something like this:
Object
+------------------+
| vptr | ← Pointer
+------------------+
| member_1 |
+------------------+
| member_2 |
+------------------+
If a memory vulnerability causes Object data corruption, in the analysis we need to consider:
Which parts of the Object are affected?
If there is an important Pointer in the Object structure, changing it can change the subsequent behavior of the program
But we still need to remember an important difference:
Corruption of an Object does not necessarily mean control of program execution
We need to consider what the program does with the corrupted data later and what protection mechanisms are in place
What is the difference between a Vtable and a Function Pointer?
Conceptually, both are related to the address of functions
But they are different
Function Pointer
A variable directly holds the address of a function:
Function Pointer
│
▼
Address of Code
Vtable
What is it and why is it important in C++?
In the previous post, we talked about Function Pointer and saw that the program can hold the address of a function and later execute a function through it
Now in C++ we have a similar but more complex concept:
Vtable
Or:
Virtual Table
This is very important to understand the internal structure of C++ Objects
Virtual Function
In C++, a class may have a function that is defined with the virtual keyword:
class Animal {
public:
virtual void speak() {
std::cout << "Animal";
}
};
Now another class inherits from it:
class Dog : public Animal {
public:
void speak() override {
std::cout << "Dog";
}
};
The point here is that if we point to an Object of type Dog with a Pointer of type Animal, the program needs to know which version of speak() will be executed in the end
That is:
Animal Pointer
│
▼
Dog Object
│
▼
Dog::speak()
To solve this problem, we usually use a mechanism like Vtable
We can consider Vtable
as a table that contains the addresses of Virtual functions of a class
Simply:
Vtable
+--------------------+
| &function_1 |
+-----------------------+
| &function_2 |
+-----------------------+
| &function_3 |
+-----------------------+
Every Object of a class that has a Virtual Function is usually linked to this table
For this connection, the compiler usually puts something like an internal Pointer called:
vptr
inside the Object
So an Object may conceptually look like this:
Object
+-----------------------+
| vptr --------------+------+
+----------------------+ |
| Data | ▼
| Data | Vtable
+---------+ +------------------+
| &Virtual Function|
+------------------+
| &Virtual Function|
+------------------+
vptr points to the Vtable
of the actual type of Object
What happens when a Virtual Function is called?
Suppose we have this code:
Animal *animal = new Dog();
animal->speak();
The program cannot decide based on the Pointer type alone because the actual type of Object may be Dog
So conceptually it goes like this:
animal
│
▼
Object
│
▼
vptr
│
▼
Vtable
│
▼
speak() address
│
▼
function execution
That is, the program uses the internal information of the Object to understand which function to execute
Why is the Vtable important for Binary Analysis?
When we reverse a C++ program, Vtables can give us useful information about the program structure.
For example, by finding a Vtable, we may be able to guess:
What virtual functions a class has
What classes are related to each other
What is the approximate structure of an object
Which functions are related to a class
That is why Vtables are so important in reversing C++ programs
Sometimes the function names are missing and the symbols are missing because humans apparently decided to leave programs unlabeled. In these situations, structures like Vtables can be important clues
The relationship of Vtables to Memory Corruption
The important point here is that vptr itself is a Pointer
That is, an Object might look something like this:
Object
+------------------+
| vptr | ← Pointer
+------------------+
| member_1 |
+------------------+
| member_2 |
+------------------+
If a memory vulnerability causes Object data corruption, in the analysis we need to consider:
Which parts of the Object are affected?
If there is an important Pointer in the Object structure, changing it can change the subsequent behavior of the program
But we still need to remember an important difference:
Corruption of an Object does not necessarily mean control of program execution
We need to consider what the program does with the corrupted data later and what protection mechanisms are in place
What is the difference between a Vtable and a Function Pointer?
Conceptually, both are related to the address of functions
But they are different
Function Pointer
A variable directly holds the address of a function:
Function Pointer
│
▼
Address of Code
Vtable
❤1
An Object usually first points to a table, and that table contains the addresses of functions:
Object
│
▼
vptr
│
▼
Vtable
│
├── Function A
├── Function B
└── Function C
So Vtable provides a more structured level for managing Virtual functions
Vtable
Is it really an official C++ standard?
An important note:
The concept of Vtable itself is not precisely defined in the C++ standard
Vtable
is actually a common way for compilers to implement Polymorphism
That is, the implementation details may differ between:
GCC
Clang
MSVC
But the general idea of using information to find the right function at runtime is very common in practice
That is why when reversing, we should always pay attention to the ABI and the compiler with which the program was built
Vtable
Usually a table containing the addresses of Virtual Functions of a class in C++ Objects that use Virtual Functions often have an internal Pointer such as vptr that relates them to the Vtable. When executing a Virtual Function, the program uses this structure to find the appropriate function
Understanding Vtable is important both in reversing C++ programs and in understanding how Objects are organized in memory
@reverseengine
Object
│
▼
vptr
│
▼
Vtable
│
├── Function A
├── Function B
└── Function C
So Vtable provides a more structured level for managing Virtual functions
Vtable
Is it really an official C++ standard?
An important note:
The concept of Vtable itself is not precisely defined in the C++ standard
Vtable
is actually a common way for compilers to implement Polymorphism
That is, the implementation details may differ between:
GCC
Clang
MSVC
But the general idea of using information to find the right function at runtime is very common in practice
That is why when reversing, we should always pay attention to the ABI and the compiler with which the program was built
Vtable
Usually a table containing the addresses of Virtual Functions of a class in C++ Objects that use Virtual Functions often have an internal Pointer such as vptr that relates them to the Vtable. When executing a Virtual Function, the program uses this structure to find the appropriate function
Understanding Vtable is important both in reversing C++ programs and in understanding how Objects are organized in memory
@reverseengine
❤1
Data Flow Analysis
دنبال کردن مسیر واقعی داده
تا اینجا بیشتر تمرکزمون روی این بود که برنامه چه دستور هایی اجرا میکنه
ولی از اینجا به بعد یک سؤال مهمتر میپرسیم:
داده از کجا میاد و آخرش کجا میره؟
این دقیقا همون چیزیه که بهش Data Flow Analysis میگیم
فرض کنید این کد رو داریم:
C++
اگر فقط به ترتیب دستورها نگاه کنیم میگیم:
اول جمع انجام میشه
بعد ضرب انجام میشه
بعد نتیجه برمیگرده
ولی در Data Flow Analysis این شکلی نگاه میکنیم:
یعنی مسیر خود داده رو دنبال میکنیم
حالا فرض کنید وسط برنامه این دستورها هم وجود داشته باشن:
C++
اگر
اینجا Data Flow Analysis خیلی کمک میکنه Junk Code رو از منطق واقعی جدا کنیم
وقتی داخل IDA یا Ghidra یک تابع پیچیده میبینیم لازم نیست از اول تا آخر همه چیز رو حفظ کنیم
یک مقدار مهم رو انتخاب کنید و دنبالش کنید
مثلا اگر ورودی تابع داخل یک رجیستر وارد شده ببینید:
کجا کپی میشه
کجا تغییر میکنه
داخل حافظه ذخیره میشه یا نه
به تابع دیگه ای میره یا نه
و در اخر روی چه چیزی تأثیر میذاره
مثلا در اسمبلی ممکنه چنین چیزی ببینید:
Asm
اگر فرض کنیم
نکته مهم اینه که اسم رجیستر به معنی مسیر داده نیست ممکنه یک مقدار از
اگر فقط اسم رجیسترها رو نگاه کنید خیلی زود گم میشید😅
باید خود مقدار رو دنبال کنید
به این مفهوم بعصی وقتا Data Provenance هم میگیم
یعنی بفهمید منشا یک مقدار کجاست
مثلا یک مقدار ممکنه از اینجا اومده باشه:
این مدل نگاه مخصوصا موقع تحلیل برنامه های Obfuscate شده خیلی مهمه
چون Obfuscation ممکنه مسیر اجرای برنامه رو شلوغ کنه ولی داده هنوز باید از یک جایی وارد بشه و به یک جایی برسه
تمرین:
این تابع رو بررسی کنید:
C
روی کاغذ مسیر
بعد مشخص کنید
هدف این تمرین اینه که کم کم وقتی یک تابع رو باز میکنید فقط دستورها رو نبینید
داده رو ببینید که داره بین رجیسترها حافظه و توابع حرکت میکنه
@reverseengine
دنبال کردن مسیر واقعی داده
تا اینجا بیشتر تمرکزمون روی این بود که برنامه چه دستور هایی اجرا میکنه
ولی از اینجا به بعد یک سؤال مهمتر میپرسیم:
داده از کجا میاد و آخرش کجا میره؟
این دقیقا همون چیزیه که بهش Data Flow Analysis میگیم
فرض کنید این کد رو داریم:
C++
int calculate(int a, int b)
{
int x = a + b;
int y = x * 2;
return y;
}
اگر فقط به ترتیب دستورها نگاه کنیم میگیم:
اول جمع انجام میشه
بعد ضرب انجام میشه
بعد نتیجه برمیگرده
ولی در Data Flow Analysis این شکلی نگاه میکنیم:
a ─┐
├── ADD ──> x ──> MUL ──> y ──> Return
b ─┘
یعنی مسیر خود داده رو دنبال میکنیم
حالا فرض کنید وسط برنامه این دستورها هم وجود داشته باشن:
C++
int temp = 500;
temp ^= 123;
temp += 20;
اگر
temp هیچوقت روی y یا خروجی تابع تأثیر نذاره توی مسیر اصلی داده قرار نمیگیرهاینجا Data Flow Analysis خیلی کمک میکنه Junk Code رو از منطق واقعی جدا کنیم
وقتی داخل IDA یا Ghidra یک تابع پیچیده میبینیم لازم نیست از اول تا آخر همه چیز رو حفظ کنیم
یک مقدار مهم رو انتخاب کنید و دنبالش کنید
مثلا اگر ورودی تابع داخل یک رجیستر وارد شده ببینید:
کجا کپی میشه
کجا تغییر میکنه
داخل حافظه ذخیره میشه یا نه
به تابع دیگه ای میره یا نه
و در اخر روی چه چیزی تأثیر میذاره
مثلا در اسمبلی ممکنه چنین چیزی ببینید:
Asm
mov eax, edi
add eax, esi
imul eax, 2
اگر فرض کنیم
EDI و ESI ورودی هستن مسیر داده این شکلیه:EDI ─┐
├──> EAX ──> ADD ──> IMUL
ESI ─┘
نکته مهم اینه که اسم رجیستر به معنی مسیر داده نیست ممکنه یک مقدار از
RAX وارد RBX بشه بعد داخل حافظه ذخیره بشه و دوباره داخل RCX برگردهاگر فقط اسم رجیسترها رو نگاه کنید خیلی زود گم میشید😅
باید خود مقدار رو دنبال کنید
به این مفهوم بعصی وقتا Data Provenance هم میگیم
یعنی بفهمید منشا یک مقدار کجاست
مثلا یک مقدار ممکنه از اینجا اومده باشه:
User Input
↓
Buffer
↓
Function A
↓
Function B
↓
Comparison
این مدل نگاه مخصوصا موقع تحلیل برنامه های Obfuscate شده خیلی مهمه
چون Obfuscation ممکنه مسیر اجرای برنامه رو شلوغ کنه ولی داده هنوز باید از یک جایی وارد بشه و به یک جایی برسه
تمرین:
این تابع رو بررسی کنید:
C
int process(int a)
{
int x = a;
x = x ^ 0x55;
x = x + 10;
int junk = 100;
junk *= 5;
return x;
}
روی کاغذ مسیر
a تا خروجی رو بکشیدبعد مشخص کنید
junk وارد مسیر اصلی داده میشه یا نههدف این تمرین اینه که کم کم وقتی یک تابع رو باز میکنید فقط دستورها رو نبینید
داده رو ببینید که داره بین رجیسترها حافظه و توابع حرکت میکنه
@reverseengine
❤2
ReverseEngineering
Data Flow Analysis دنبال کردن مسیر واقعی داده تا اینجا بیشتر تمرکزمون روی این بود که برنامه چه دستور هایی اجرا میکنه ولی از اینجا به بعد یک سؤال مهمتر میپرسیم: داده از کجا میاد و آخرش کجا میره؟ این دقیقا همون چیزیه که بهش Data Flow Analysis میگیم فرض…
Data Flow Analysis Following the actual data path
So far, we have been mostly focused on what instructions the program executes
But from here on, we ask a more important question:
Where does the data come from and where does it end up?
This is exactly what we call Data Flow Analysis
Suppose we have this code:
C++
If we just look at the order of the instructions, we would say:
First the addition is done
Then the multiplication is done
Then the result is returned
But in Data Flow Analysis, we look at it like this:
That is, we follow the path of the data itself
Now suppose there are these instructions in the middle of the program:
C++
If temp never affects y or the output of the function, it will not be in the main data path.
Here Data Flow Analysis helps a lot to separate Junk Code from the real logic.
When we see a complex function in IDA or Ghidra, we do not need to memorize everything from beginning to end.
Choose an important value and follow it.
For example, if the input of the function is entered into a register, see:
Where is it copied?
Where is it changed?
Is it stored in memory or not?
Is it passed to another function or not?
And what does it affect in the end?
For example, in assembly, you might see something like this:
Asm
If we assume that EDI and ESI are inputs, the data path looks like this:
The important thing is that the name of the register does not mean the data path. A value from RAX may be entered into RBX, then stored in memory, and then again into RCX. Back
If you just look at the register names, you'll get lost very quickly😅
You have to follow the value itself
Sometimes we also call this Data Provenance
That is, find out where a value comes from
For example, a value might come from:
This approach is especially important when analyzing obfuscated programs
Because obfuscation may clutter the program's execution path, but the data still has to come from somewhere and go somewhere
Exercise:
Examine this function:
C
Draw a path from a to the output on paper
Then determine whether junk enters the main data path
The goal of this exercise is to gradually see not just the instructions when you open a function
See the data moving between registers, memory, and functions
@reverseengine
So far, we have been mostly focused on what instructions the program executes
But from here on, we ask a more important question:
Where does the data come from and where does it end up?
This is exactly what we call Data Flow Analysis
Suppose we have this code:
C++
int calculate(int a, int b)
{
int x = a + b;
int y = x * 2;
return y;
}
If we just look at the order of the instructions, we would say:
First the addition is done
Then the multiplication is done
Then the result is returned
But in Data Flow Analysis, we look at it like this:
a ─┐
├── ADD ──> x ──> MUL ──> y ──> Return
b ─┘
That is, we follow the path of the data itself
Now suppose there are these instructions in the middle of the program:
C++
int temp = 500;
temp ^= 123;
temp += 20;
If temp never affects y or the output of the function, it will not be in the main data path.
Here Data Flow Analysis helps a lot to separate Junk Code from the real logic.
When we see a complex function in IDA or Ghidra, we do not need to memorize everything from beginning to end.
Choose an important value and follow it.
For example, if the input of the function is entered into a register, see:
Where is it copied?
Where is it changed?
Is it stored in memory or not?
Is it passed to another function or not?
And what does it affect in the end?
For example, in assembly, you might see something like this:
Asm
mov eax, edi
add eax, esi
imul eax, 2
If we assume that EDI and ESI are inputs, the data path looks like this:
EDI ─┐
├──> EAX ──> ADD ──> IMUL
ESI ─┘
The important thing is that the name of the register does not mean the data path. A value from RAX may be entered into RBX, then stored in memory, and then again into RCX. Back
If you just look at the register names, you'll get lost very quickly😅
You have to follow the value itself
Sometimes we also call this Data Provenance
That is, find out where a value comes from
For example, a value might come from:
User Input
↓
Buffer
↓
Function A
↓
Function B
↓
Comparison
This approach is especially important when analyzing obfuscated programs
Because obfuscation may clutter the program's execution path, but the data still has to come from somewhere and go somewhere
Exercise:
Examine this function:
C
int process(int a)
{
int x = a;
x = x ^ 0x55;
x = x + 10;
int junk = 100;
junk *= 5;
return x;
}
Draw a path from a to the output on paper
Then determine whether junk enters the main data path
The goal of this exercise is to gradually see not just the instructions when you open a function
See the data moving between registers, memory, and functions
@reverseengine
❤1
Thread Analysis و Start Address
بعضی وقتا خود Process چیز خاصی نشون نمیده ولی Thread هاش داستان اصلی رو لو میدن
تا اینجا دیدیم که EDR فقط اسم فایل یا Process رو نگاه نمیکنه
یکی از چیزایی که عمیق تر بررسی میشه Thread های داخل Process هستن
هر Process میتونه چند تا Thread داشته باشه
هر Thread هم مسیر اجرای خودش رو داره
برای همین وقتی EDR یا یک تحلیلگر میخواد بفهمه داخل یک Process دقیقا چه خبره بررسی Thread ها میتونه اطلاعات مهمی بهش بده
Start Address
خیلی ساده هر Thread برای اجرا باید از یک نقطه شروع کنه
به اون نقطه میگن Start Address
یعنی EDR میتونه بررسی کنه که Thread از کجای حافظه شروع به اجرا کرده
مثلا ممکنه Start Address داخل یک DLL شناخته شده باشه
یا ممکنه داخل یک قسمت ناشناس از حافظه باشه
اگر داخل یک DLL معمولی باشه باز هم باید Context بررسی بشه
اگر داخل یک قسمت ناشناس از حافظه باشه ممکنه مشکوک تر به نظر برسه
ولی اینجا یک نکته خیلی مهم وجود داره
هر Memory Region ناشناسی لزوما مخرب نیست
بعضی برنامه ها خودشون موقع اجرا کد تولید میکنن یا حافظه رو به شکل خاصی مدیریت میکنن
مثلا JIT Compiler ها مرورگرها و بعضی Runtime ها ممکنه رفتارهایی داشته باشن که از بیرون عجیب به نظر برسه
چون مشخصا انسان ها یک False Positive ساده رو نمیپذیرن و ترجیح میدن سیستم امنیتی برنامه سالمشون رو هم گاهی بکشه😁
EDR
فقط Start Address رو نگاه نمیکنه
در تحلیل واقعی معمولا چند تا چیز با هم بررسی میشه
اول اینکه Thread چه زمانی ساخته شده
مثلا یک برنامه اجرا میشه
چند دقیقه بعد یک اتفاق غیرعادی در حافظه میفته
بلافاصله بعدش یک Thread جدید ساخته میشه
اینجا زمان بندی اتفاق ها مهمه
چون یک Event به تنهایی ممکنه چیز خاصی نباشه ولی وقتی چند Event پشت سر هم اتفاق میفتن داستان فرق میکنه
دوم اینکه چه کسی باعث ایجاد Thread شده
مثلا یک Process با Process دیگه ارتباط برقرار میکنه
بعد داخل اون Process یک Thread جدید ظاهر میشه
همین ارتباط بین دو Process میتونه برای Detection خیلی مهم باشه
بعضی وقت ها خود Thread چیز عجیبی نیست ولی اتفاقاتی که قبل از ایجادش افتاده مشکوک هستن
سوم اینکه Thread دقیقاً از کجا اجرا میشه
EDR
میتونه Memory Region اطراف Start Address رو بررسی کنه
مثلا این حافظه ممکنه مربوط به یک Module باشه
ممکنه Private Memory باشه
ممکنه مربوط به یک Image باشه
یا یک Memory Region دیگه باشه
مثلا اگر Thread از یک Private Executable Region شروع به اجرا کنه ممکنه نیاز به بررسی بیشتری داشته باشه
مخصوصاً اگر قبل از اون یک اتفاق غیرعادی روی همون قسمت از حافظه افتاده باشه
چهارم Call Stack هست
Call Stack
میتونه تا حدی نشون بده Thread از چه مسیر و چه Function هایی به وضعیت الانش رسیده
یعنی EDR فقط نقطه فعلی Thread رو نمیبینه و ممکنه مسیر رسیدن به اون نقطه رو هم بررسی کنه
اگر مسیر اجرا با رفتار معمول اون برنامه جور درنیاد میتونه یک نشونه مشکوک باشه
ولی باز هم یک Call Stack عجیب به تنهایی یعنی برنامه مخرب نیست
ابزارهای Debugging
Instrumentation
و Runtime های مختلف هم میتونن ساختارهایی ایجاد کنن که معمولی به نظر نرسن
مدل فکری Detection
فرض کنید فقط این اتفاق دیده بشه
یک Thread جدید ساخته شده
به تنهایی اطلاعات زیادی بهمون نمیده
ولی حالا فرض کنید این اتفاق ها پشت سر هم افتاده
Process A اجرا میشه
بعد با Process B ارتباط برقرار میکنه
داخل Process B یک اتفاق مربوط به حافظه رخ میده
بعد یک قسمت قابل اجرا در حافظه ظاهر میشه
بعد یک Thread شروع به اجرا میکنه
بعد فعالیت شبکه شروع میشه
اینجا قضیه فرق میکنه
چون EDR فقط یک اتفاق رو نمیبینه
داره رابطه بین اتفاق ها رو میبینه
و این نکته اصلیه
EDR
دنبال یک Event جادویی نیست
دنبال ارتباط بین Event هاست
Thread
ها چرا برای تحلیلگر مهمن
چون میتونن بین تغییراتی که در حافظه اتفاق افتاده و اجرای واقعی ارتباط ایجاد کنن
مثلا ممکنه یک قسمت از حافظه تغییر کرده باشه ولی هنوز مشخص نباشه این تغییر واقعاً استفاده شده یا نه
اما اگر بعدش یک مسیر اجرایی جدید ظاهر بشه و یک Thread از اون مسیر شروع به کار کنه قضیه برای تحلیلگر معنی بیشتری پیدا میکنه
برای تحلیل رفتار یک Process باید این موارد رو کنار هم دید
هیچ کدوم از اینا به تنهایی کافی نیست
ولی وقتی همه کنار هم قرار میگیرن Detection قوی تر میشه
بعضی وقتا خود Process چیز خاصی نشون نمیده ولی Thread هاش داستان اصلی رو لو میدن
تا اینجا دیدیم که EDR فقط اسم فایل یا Process رو نگاه نمیکنه
یکی از چیزایی که عمیق تر بررسی میشه Thread های داخل Process هستن
هر Process میتونه چند تا Thread داشته باشه
هر Thread هم مسیر اجرای خودش رو داره
برای همین وقتی EDR یا یک تحلیلگر میخواد بفهمه داخل یک Process دقیقا چه خبره بررسی Thread ها میتونه اطلاعات مهمی بهش بده
Start Address
خیلی ساده هر Thread برای اجرا باید از یک نقطه شروع کنه
به اون نقطه میگن Start Address
یعنی EDR میتونه بررسی کنه که Thread از کجای حافظه شروع به اجرا کرده
مثلا ممکنه Start Address داخل یک DLL شناخته شده باشه
یا ممکنه داخل یک قسمت ناشناس از حافظه باشه
اگر داخل یک DLL معمولی باشه باز هم باید Context بررسی بشه
اگر داخل یک قسمت ناشناس از حافظه باشه ممکنه مشکوک تر به نظر برسه
ولی اینجا یک نکته خیلی مهم وجود داره
هر Memory Region ناشناسی لزوما مخرب نیست
بعضی برنامه ها خودشون موقع اجرا کد تولید میکنن یا حافظه رو به شکل خاصی مدیریت میکنن
مثلا JIT Compiler ها مرورگرها و بعضی Runtime ها ممکنه رفتارهایی داشته باشن که از بیرون عجیب به نظر برسه
چون مشخصا انسان ها یک False Positive ساده رو نمیپذیرن و ترجیح میدن سیستم امنیتی برنامه سالمشون رو هم گاهی بکشه😁
EDR
فقط Start Address رو نگاه نمیکنه
در تحلیل واقعی معمولا چند تا چیز با هم بررسی میشه
اول اینکه Thread چه زمانی ساخته شده
مثلا یک برنامه اجرا میشه
چند دقیقه بعد یک اتفاق غیرعادی در حافظه میفته
بلافاصله بعدش یک Thread جدید ساخته میشه
اینجا زمان بندی اتفاق ها مهمه
چون یک Event به تنهایی ممکنه چیز خاصی نباشه ولی وقتی چند Event پشت سر هم اتفاق میفتن داستان فرق میکنه
دوم اینکه چه کسی باعث ایجاد Thread شده
مثلا یک Process با Process دیگه ارتباط برقرار میکنه
بعد داخل اون Process یک Thread جدید ظاهر میشه
همین ارتباط بین دو Process میتونه برای Detection خیلی مهم باشه
بعضی وقت ها خود Thread چیز عجیبی نیست ولی اتفاقاتی که قبل از ایجادش افتاده مشکوک هستن
سوم اینکه Thread دقیقاً از کجا اجرا میشه
EDR
میتونه Memory Region اطراف Start Address رو بررسی کنه
مثلا این حافظه ممکنه مربوط به یک Module باشه
ممکنه Private Memory باشه
ممکنه مربوط به یک Image باشه
یا یک Memory Region دیگه باشه
مثلا اگر Thread از یک Private Executable Region شروع به اجرا کنه ممکنه نیاز به بررسی بیشتری داشته باشه
مخصوصاً اگر قبل از اون یک اتفاق غیرعادی روی همون قسمت از حافظه افتاده باشه
چهارم Call Stack هست
Call Stack
میتونه تا حدی نشون بده Thread از چه مسیر و چه Function هایی به وضعیت الانش رسیده
یعنی EDR فقط نقطه فعلی Thread رو نمیبینه و ممکنه مسیر رسیدن به اون نقطه رو هم بررسی کنه
اگر مسیر اجرا با رفتار معمول اون برنامه جور درنیاد میتونه یک نشونه مشکوک باشه
ولی باز هم یک Call Stack عجیب به تنهایی یعنی برنامه مخرب نیست
ابزارهای Debugging
Instrumentation
و Runtime های مختلف هم میتونن ساختارهایی ایجاد کنن که معمولی به نظر نرسن
مدل فکری Detection
فرض کنید فقط این اتفاق دیده بشه
یک Thread جدید ساخته شده
به تنهایی اطلاعات زیادی بهمون نمیده
ولی حالا فرض کنید این اتفاق ها پشت سر هم افتاده
Process A اجرا میشه
بعد با Process B ارتباط برقرار میکنه
داخل Process B یک اتفاق مربوط به حافظه رخ میده
بعد یک قسمت قابل اجرا در حافظه ظاهر میشه
بعد یک Thread شروع به اجرا میکنه
بعد فعالیت شبکه شروع میشه
اینجا قضیه فرق میکنه
چون EDR فقط یک اتفاق رو نمیبینه
داره رابطه بین اتفاق ها رو میبینه
و این نکته اصلیه
EDR
دنبال یک Event جادویی نیست
دنبال ارتباط بین Event هاست
Thread
ها چرا برای تحلیلگر مهمن
چون میتونن بین تغییراتی که در حافظه اتفاق افتاده و اجرای واقعی ارتباط ایجاد کنن
مثلا ممکنه یک قسمت از حافظه تغییر کرده باشه ولی هنوز مشخص نباشه این تغییر واقعاً استفاده شده یا نه
اما اگر بعدش یک مسیر اجرایی جدید ظاهر بشه و یک Thread از اون مسیر شروع به کار کنه قضیه برای تحلیلگر معنی بیشتری پیدا میکنه
برای تحلیل رفتار یک Process باید این موارد رو کنار هم دید
Process Tree
Cross Process Access
Memory Regions
Memory Permission Changes
Thread Start Address
Call Stack
Timeline
هیچ کدوم از اینا به تنهایی کافی نیست
ولی وقتی همه کنار هم قرار میگیرن Detection قوی تر میشه
Telemetry
Context
Timeline
Correlation
همه اینا وقتی با هم بررسی بشن Detection دقیق تری میدن
و دقیقا به همین دلیله که بحث AV و EDR Evasion فقط عوض کردن یک API نیست
سیستم های دفاعی جدید سعی میکنن اثر و نتیجه عملیات رو ببینن
حتی اگر مسیر انجام اون عملیات تغییر کرده باشه
@reverseengine
ReverseEngineering
Thread Analysis و Start Address بعضی وقتا خود Process چیز خاصی نشون نمیده ولی Thread هاش داستان اصلی رو لو میدن تا اینجا دیدیم که EDR فقط اسم فایل یا Process رو نگاه نمیکنه یکی از چیزایی که عمیق تر بررسی میشه Thread های داخل Process هستن هر Process میتونه…
Thread Analysis and Start Address
Sometimes the Process itself doesn't show anything special, but its Threads reveal the real story
So far we have seen that EDR doesn't just look at the file name or Process
One of the things that is examined more deeply is the Threads inside the Process
Each Process can have several Threads
Each Thread has its own execution path
Therefore, when EDR or an analyst wants to understand what exactly is going on inside a Process, examining the Threads can give him important information
Start Address
Very simply, each Thread must start from a point to execute
That point is called the Start Address
That is, EDR can check where in memory the Thread started executing
For example, the Start Address may be inside a known DLL
Or it may be inside an unknown part of memory
If it is inside a regular DLL, the Context must still be checked
If it is inside an unknown part of memory, it may look more suspicious
But there is a very important point here
Every unknown Memory Region is not necessarily malicious
Some programs generate code themselves when they run or map memory in a special way They manage
For example, JIT Compilers, browsers, and some runtimes may have behaviors that may seem strange from the outside
Because humans obviously don't accept a simple False Positive and prefer the security system to kill their healthy program sometim😁
EDR
doesn't just look at the Start Address
In real analysis, several things are usually checked together
First, when the Thread was created
For example, a program is executed
A few minutes later, an unusual event occurs in memory
Immediately after that, a new Thread is created
The timing of events is important here
Because an Event alone may not be anything special, but when several Events occur in succession, the story is different
Second, who caused the Thread to be created
For example, a Process communicates with another Process
Then a new Thread appears inside that Process
This connection between the two Processes can be very important for Detection
Sometimes the Thread itself is not strange, but the events that occurred before its creation are suspicious
Third, where exactly the Thread is executed
EDR
can look at the Memory Region around the Start Check the address
For example, this memory may be related to a Module
It may be Private Memory
It may be related to an Image
Or another Memory Region
For example, if a Thread starts executing from a Private Executable Region, it may need further investigation
Especially if an unusual event has occurred on the same part of memory before that
The fourth is the Call Stack
The Call Stack
Can show to some extent the path and functions through which the Thread reached its current state
That is, EDR does not only see the current point of the Thread and may also examine the path to reach that point
If the execution path does not match the usual behavior of that program, it can be a suspicious sign
But still, a strange Call Stack alone means that the program is not malicious
Debugging
Instrumentation
And various runtimes can also create structures that do not seem normal
Detection mental model
Suppose this event is only seen
A new Thread is created
It does not give us much information on its own
But now suppose these events happened one after the other
Process A is executed
Then It communicates with Process B
A memory event occurs inside Process B
Then an executable appears in memory
Then a Thread starts executing
Then network activity starts
Here the situation is different
Because EDR does not see just one event
It sees the relationship between events
And this is the main point
EDR
is not looking for a magic Event
It looks for the relationship between Events
Why are Threads important to the analyst
Because they can establish a connection between the changes that occurred in memory and the actual execution
Sometimes the Process itself doesn't show anything special, but its Threads reveal the real story
So far we have seen that EDR doesn't just look at the file name or Process
One of the things that is examined more deeply is the Threads inside the Process
Each Process can have several Threads
Each Thread has its own execution path
Therefore, when EDR or an analyst wants to understand what exactly is going on inside a Process, examining the Threads can give him important information
Start Address
Very simply, each Thread must start from a point to execute
That point is called the Start Address
That is, EDR can check where in memory the Thread started executing
For example, the Start Address may be inside a known DLL
Or it may be inside an unknown part of memory
If it is inside a regular DLL, the Context must still be checked
If it is inside an unknown part of memory, it may look more suspicious
But there is a very important point here
Every unknown Memory Region is not necessarily malicious
Some programs generate code themselves when they run or map memory in a special way They manage
For example, JIT Compilers, browsers, and some runtimes may have behaviors that may seem strange from the outside
Because humans obviously don't accept a simple False Positive and prefer the security system to kill their healthy program sometim😁
EDR
doesn't just look at the Start Address
In real analysis, several things are usually checked together
First, when the Thread was created
For example, a program is executed
A few minutes later, an unusual event occurs in memory
Immediately after that, a new Thread is created
The timing of events is important here
Because an Event alone may not be anything special, but when several Events occur in succession, the story is different
Second, who caused the Thread to be created
For example, a Process communicates with another Process
Then a new Thread appears inside that Process
This connection between the two Processes can be very important for Detection
Sometimes the Thread itself is not strange, but the events that occurred before its creation are suspicious
Third, where exactly the Thread is executed
EDR
can look at the Memory Region around the Start Check the address
For example, this memory may be related to a Module
It may be Private Memory
It may be related to an Image
Or another Memory Region
For example, if a Thread starts executing from a Private Executable Region, it may need further investigation
Especially if an unusual event has occurred on the same part of memory before that
The fourth is the Call Stack
The Call Stack
Can show to some extent the path and functions through which the Thread reached its current state
That is, EDR does not only see the current point of the Thread and may also examine the path to reach that point
If the execution path does not match the usual behavior of that program, it can be a suspicious sign
But still, a strange Call Stack alone means that the program is not malicious
Debugging
Instrumentation
And various runtimes can also create structures that do not seem normal
Detection mental model
Suppose this event is only seen
A new Thread is created
It does not give us much information on its own
But now suppose these events happened one after the other
Process A is executed
Then It communicates with Process B
A memory event occurs inside Process B
Then an executable appears in memory
Then a Thread starts executing
Then network activity starts
Here the situation is different
Because EDR does not see just one event
It sees the relationship between events
And this is the main point
EDR
is not looking for a magic Event
It looks for the relationship between Events
Why are Threads important to the analyst
Because they can establish a connection between the changes that occurred in memory and the actual execution
ReverseEngineering
Thread Analysis و Start Address بعضی وقتا خود Process چیز خاصی نشون نمیده ولی Thread هاش داستان اصلی رو لو میدن تا اینجا دیدیم که EDR فقط اسم فایل یا Process رو نگاه نمیکنه یکی از چیزایی که عمیق تر بررسی میشه Thread های داخل Process هستن هر Process میتونه…
For example, a part of memory may have changed but it is not yet clear whether this change was actually used or not
But if a new execution path appears later and a Thread starts running from that path, the case becomes more meaningful for the analyst
To analyze the behavior of a Process, you need to look at these things together
None of these are enough on their own
But when they are all put together, the detection is stronger
But if a new execution path appears later and a Thread starts running from that path, the case becomes more meaningful for the analyst
To analyze the behavior of a Process, you need to look at these things together
Process Tree
Cross Process Access
Memory Regions
Memory Permission Changes
Thread Start Address
Call Stack
Timeline
None of these are enough on their own
But when they are all put together, the detection is stronger
❤1
Telemetry
Context
Timeline
Correlation
All of these, when considered together, provide more accurate detection
And that's exactly why AV and EDR Evasion is not just about changing an API
New defense systems try to see the effect and outcome of an operation
Even if the path to that operation has changed
@reverseengine
❤1
Copy on Write یا COW
سیستم عامل چطور بدون کپی کردن همه چیز fork میسازه
توی پست های قبلی گفتیم وقتی fork اجرا میشه یک Child Process ساخته میشه
در نگاه اول شاید فکر کنیم سیستم عامل این کارو میکنه
Parent Process
│
▼
کپی کامل حافظه
│
▼
Child Process
یعنی کل حافظه Parent رو برمیداره و دوباره برای Child کپی میکنه
ولی این کار یه مشکل بزرگ داره
فرض کن Parent چند گیگابایت حافظه استفاده کرده
اگه سیستم عامل هر بار که fork اجرا میشه کل حافظه رو کپی کنه
زمان زیادی مصرف میشه
RAM زیادی مصرف میشه
CPU
هم بیخودی درگیر کپی کردن اطلاعات میشه
ولی این بار سیستم عامل یه کم عاقل تر عمل میکنه
راه حل Copy on Write یا COW
سیستم عامل میگه
فعلا چیزی رو کپی نکنید
بعد از fork در ابتدا Parent و Child میتونن از همون Page های حافظه استفاده کنن
به صورت ساده
Physical Memory
│
┌─────────┴─────────┐
│ │
Parent Child
│ │
└─────────┬─────────┘
│
Shared Page
تا وقتی هیچکدوم چیزی رو تغییر نداده باشن لازم نیست یه نسخه جدا ساخته بشه
حالا اگه یکی از اونها بخواد چیزی رو تغییر بده چی میشه
فرض کنید Parent و Child اول کار این مقدار رو دارن
int x = 10
حالا Child میخواد این کارو انجام بده
x = 20
اینجا سیستم عامل متوجه میشه Child میخواد Page مشترک رو تغییر بده
پس تقریبا این اتفاق میفته
قبل از تغییر
Parent
│
▼
┌──────────────┐
│ Page x = 10 │
└──────────────┘
▲
│
Child
یعنی فعلا هر دو به همون Page دسترسی دارن
حالا Child میخواد روی اون Page چیزی بنویسه
سیستم عامل کل حافظه رو کپی نمیکنه
فقط همون Page ای که لازم شده رو کپی میکنه
بعد نتیجه تقریبا این شکلی میشه
Physical Memory
┌──────────────────────┐
│ Page x = 10 │
│ Parent │
└──────────────────────┘
┌──────────────────────┐
│ Page x = 20 │
│ Child │
└──────────────────────┘
پس Parent همچنان x برابر 10 داره
و Child نسخه خودش رو داره که x برابر 20 شده
به همین دلیل اسمش شده Copy on Write
یعنی وقتی نیاز به نوشتن پیش اومد کپی کن
چرا این روش خیلی مهمه
چون توی خیلی از برنامه ها Child بلافاصله بعد از fork میره سراغ exec
یعنی
fork
│
▼
Child
│
▼
exec
یادتون هست exec چیکار میکرد
برنامه فعلی رو با یه برنامه جدید جایگزین میکرد پس اگه سیستم عامل قبل از exec کل حافظه Parent رو کپی میکرد ممکن بود مقدار زیادی از اون حافظه اصلا هیچ وقت استفاده نشه
با COW اتفاق به شکل ساده اینطوریه
fork
│
▼
فعلا حافظه کامل کپی نمیشه
│
▼
Child اجرا میشه
│
▼
exec
│
▼
Program جدید جایگزین میشه
در نتیجه مقدار زیادی زمان و حافظه ذخیره میشه
پشت صحنه چه اتفاقی میفته
بعد از fork سیستم عامل میتونه Page های Parent و Child رو به صورت Shared نگه داره
ولی این Page ها رو برای نوشتن محافظت میکنه
حالا اگه یکی از Process ها بخواد روی همچین Page ای بنویسه
Write Attempt
│
▼
CPU detects protected write
│
▼
Page Fault
│
▼
Kernel handles it
│
▼
Copy the Page
│
▼
Give the writer its own Page
│
▼
Continue execution
یعنی Page Fault همیشه به معنی خراب شدن برنامه نیست
گاهی Page Fault کاملا یه اتفاق طبیعی توی مدیریت حافظه است
این موضوع وقتی برسیم به Virtual Memory و Paging خیلی بیشتر به کارمون میاد
یه نکته مهم
فرض کنید یه برنامه توی Parent یه متغیر رو تغییر میده و انتظار دارید Child هم مقدار جدید رو ببینه
اگه Parent و Child دو Process مستقل باشن معمولا این انتظار درست نیست
بعد از اینکه یکی از Process ها به خاطر COW اون Page رو تغییر بده هر کدوم نسخه خودشون رو دارن
یعنی
Parent Memory
│
▼
Separate Page
Child Memory
│
▼
Separate Page
پس:
Parent Memory ≠ Child Memory
این موضوع برای تحلیل برنامه های چندپردازه ای مهمه
مثلا موقع Debugging ممکنه Parent و Child اول کار رفتار مشابهی داشته باشن
ولی بعد از اینکه یکی از اونها حافظه خودش رو تغییر داد مسیرشون از هم جدا میشه
COW
رو با Shared Memory اشتباه نگیرید
این دوتا شبیه هم به نظر میان ولی یکی نیستن
در COW
اول
│
▼
سیستم عامل چطور بدون کپی کردن همه چیز fork میسازه
توی پست های قبلی گفتیم وقتی fork اجرا میشه یک Child Process ساخته میشه
در نگاه اول شاید فکر کنیم سیستم عامل این کارو میکنه
Parent Process
│
▼
کپی کامل حافظه
│
▼
Child Process
یعنی کل حافظه Parent رو برمیداره و دوباره برای Child کپی میکنه
ولی این کار یه مشکل بزرگ داره
فرض کن Parent چند گیگابایت حافظه استفاده کرده
اگه سیستم عامل هر بار که fork اجرا میشه کل حافظه رو کپی کنه
زمان زیادی مصرف میشه
RAM زیادی مصرف میشه
CPU
هم بیخودی درگیر کپی کردن اطلاعات میشه
ولی این بار سیستم عامل یه کم عاقل تر عمل میکنه
راه حل Copy on Write یا COW
سیستم عامل میگه
فعلا چیزی رو کپی نکنید
بعد از fork در ابتدا Parent و Child میتونن از همون Page های حافظه استفاده کنن
به صورت ساده
Physical Memory
│
┌─────────┴─────────┐
│ │
Parent Child
│ │
└─────────┬─────────┘
│
Shared Page
تا وقتی هیچکدوم چیزی رو تغییر نداده باشن لازم نیست یه نسخه جدا ساخته بشه
حالا اگه یکی از اونها بخواد چیزی رو تغییر بده چی میشه
فرض کنید Parent و Child اول کار این مقدار رو دارن
int x = 10
حالا Child میخواد این کارو انجام بده
x = 20
اینجا سیستم عامل متوجه میشه Child میخواد Page مشترک رو تغییر بده
پس تقریبا این اتفاق میفته
قبل از تغییر
Parent
│
▼
┌──────────────┐
│ Page x = 10 │
└──────────────┘
▲
│
Child
یعنی فعلا هر دو به همون Page دسترسی دارن
حالا Child میخواد روی اون Page چیزی بنویسه
سیستم عامل کل حافظه رو کپی نمیکنه
فقط همون Page ای که لازم شده رو کپی میکنه
بعد نتیجه تقریبا این شکلی میشه
Physical Memory
┌──────────────────────┐
│ Page x = 10 │
│ Parent │
└──────────────────────┘
┌──────────────────────┐
│ Page x = 20 │
│ Child │
└──────────────────────┘
پس Parent همچنان x برابر 10 داره
و Child نسخه خودش رو داره که x برابر 20 شده
به همین دلیل اسمش شده Copy on Write
یعنی وقتی نیاز به نوشتن پیش اومد کپی کن
چرا این روش خیلی مهمه
چون توی خیلی از برنامه ها Child بلافاصله بعد از fork میره سراغ exec
یعنی
fork
│
▼
Child
│
▼
exec
یادتون هست exec چیکار میکرد
برنامه فعلی رو با یه برنامه جدید جایگزین میکرد پس اگه سیستم عامل قبل از exec کل حافظه Parent رو کپی میکرد ممکن بود مقدار زیادی از اون حافظه اصلا هیچ وقت استفاده نشه
با COW اتفاق به شکل ساده اینطوریه
fork
│
▼
فعلا حافظه کامل کپی نمیشه
│
▼
Child اجرا میشه
│
▼
exec
│
▼
Program جدید جایگزین میشه
در نتیجه مقدار زیادی زمان و حافظه ذخیره میشه
پشت صحنه چه اتفاقی میفته
بعد از fork سیستم عامل میتونه Page های Parent و Child رو به صورت Shared نگه داره
ولی این Page ها رو برای نوشتن محافظت میکنه
حالا اگه یکی از Process ها بخواد روی همچین Page ای بنویسه
Write Attempt
│
▼
CPU detects protected write
│
▼
Page Fault
│
▼
Kernel handles it
│
▼
Copy the Page
│
▼
Give the writer its own Page
│
▼
Continue execution
یعنی Page Fault همیشه به معنی خراب شدن برنامه نیست
گاهی Page Fault کاملا یه اتفاق طبیعی توی مدیریت حافظه است
این موضوع وقتی برسیم به Virtual Memory و Paging خیلی بیشتر به کارمون میاد
یه نکته مهم
فرض کنید یه برنامه توی Parent یه متغیر رو تغییر میده و انتظار دارید Child هم مقدار جدید رو ببینه
اگه Parent و Child دو Process مستقل باشن معمولا این انتظار درست نیست
بعد از اینکه یکی از Process ها به خاطر COW اون Page رو تغییر بده هر کدوم نسخه خودشون رو دارن
یعنی
Parent Memory
│
▼
Separate Page
Child Memory
│
▼
Separate Page
پس:
Parent Memory ≠ Child Memory
این موضوع برای تحلیل برنامه های چندپردازه ای مهمه
مثلا موقع Debugging ممکنه Parent و Child اول کار رفتار مشابهی داشته باشن
ولی بعد از اینکه یکی از اونها حافظه خودش رو تغییر داد مسیرشون از هم جدا میشه
COW
رو با Shared Memory اشتباه نگیرید
این دوتا شبیه هم به نظر میان ولی یکی نیستن
در COW
اول
│
▼
❤1
داده ممکنه مشترک باشه
│
▼
یکی میخواد بنویسه
│
▼
نسخه جدا ساخته میشه
ولی در Shared Memory
Parent
│
▼
Shared Memory
▲
│
Child
اگه Parent اطلاعات رو تغییر بده Child میتونه تغییر رو ببینه
پس هدف این دوتا فرق داره
هدف COW اینه که fork بهینه تر انجام بشه و سیستم عامل مجبور نشه بی دلیل کل حافظه رو کپی کنه
هدف Shared Memory اینه که چند Process بتونن واقعا از یه فضای حافظه مشترک استفاده کنن
Copy on Write یا COW
بعد از fork لازم نیست کل حافظه فورا کپی بشه
Parent و Child
میتونن اول از Page های فیزیکی مشترک استفاده کنن
Page
ها برای جلوگیری از نوشتن مستقیم محافظت میشن
وقتی یکی از Process ها بخواد چیزی رو تغییر بده Page Fault اتفاق میفته
Kernel
این اتفاق رو مدیریت میکنه
یه نسخه جدا از اون Page ساخته میشه
Process
ی که اطلاعات رو تغییر داده از نسخه خودش استفاده میکنه
در نتیجه RAM و CPU کمتری مصرف میشه
اگه بخوایم خیلی ساده توی یه جمله بگیم
Copy on Write
یعنی سیستم عامل تا وقتی مجبور نشده چیزی رو کپی نمیکنه و این یکی از مثال های خوبیه که نشون میده Process و Memory چقدر به هم وابسته هستن
@reverseengine
│
▼
یکی میخواد بنویسه
│
▼
نسخه جدا ساخته میشه
ولی در Shared Memory
Parent
│
▼
Shared Memory
▲
│
Child
اگه Parent اطلاعات رو تغییر بده Child میتونه تغییر رو ببینه
پس هدف این دوتا فرق داره
هدف COW اینه که fork بهینه تر انجام بشه و سیستم عامل مجبور نشه بی دلیل کل حافظه رو کپی کنه
هدف Shared Memory اینه که چند Process بتونن واقعا از یه فضای حافظه مشترک استفاده کنن
Copy on Write یا COW
بعد از fork لازم نیست کل حافظه فورا کپی بشه
Parent و Child
میتونن اول از Page های فیزیکی مشترک استفاده کنن
Page
ها برای جلوگیری از نوشتن مستقیم محافظت میشن
وقتی یکی از Process ها بخواد چیزی رو تغییر بده Page Fault اتفاق میفته
Kernel
این اتفاق رو مدیریت میکنه
یه نسخه جدا از اون Page ساخته میشه
Process
ی که اطلاعات رو تغییر داده از نسخه خودش استفاده میکنه
در نتیجه RAM و CPU کمتری مصرف میشه
اگه بخوایم خیلی ساده توی یه جمله بگیم
Copy on Write
یعنی سیستم عامل تا وقتی مجبور نشده چیزی رو کپی نمیکنه و این یکی از مثال های خوبیه که نشون میده Process و Memory چقدر به هم وابسته هستن
@reverseengine
❤1
ReverseEngineering
Copy on Write یا COW سیستم عامل چطور بدون کپی کردن همه چیز fork میسازه توی پست های قبلی گفتیم وقتی fork اجرا میشه یک Child Process ساخته میشه در نگاه اول شاید فکر کنیم سیستم عامل این کارو میکنه Parent Process │ ▼ کپی کامل حافظه │ ▼ Child Process یعنی کل…
Copy on Write or COW
How does the operating system create a fork without copying everything
In previous posts, we said that when a fork is executed, a Child Process is created
At first glance, we might think that the operating system does this
Parent Process
│
▼
Copying the entire memory
│
▼
Child Process
That is, it takes the entire memory of the Parent and copies it again for the Child
But this has a big problem
Suppose how many gigabytes of memory the Parent has used
If the operating system copies the entire memory every time a fork is executed
It takes a lot of time
It uses a lot of RAM
The CPU
also gets unnecessarily busy copying information
But this time the operating system acts a little more wisely
Copy on Write or COW solution
The operating system says
Don't copy anything for now
After the fork, at first the Parent and Child can use the same memory pages
Simply
Physical Memory
│
┌───────────────┐
│ │
Parent Child
│ │
└─�
│
Shared Page
As long as neither of them has changed anything, there is no need to create a separate copy
Now what happens if one of them wants to change something
Suppose Parent and Child initially have this value
int x = 10
Now Child wants to do this
x = 20
Here the operating system understands that Child wants to change the shared Page
So this is what happens
Before changing
Parent
│
▼
┌──────────────┐
│ Page x = 10 │
└───────────┘
▲
│
Child
That is, both of them have access to the same Page for now
Now Child wants to write something to that Page
The operating system does not copy the entire memory
It copies only the Page that is needed
Then the result will be something like this
Physical Memory
┌─
┌───────────────────────────────────
│ Page x = 20 │
│ Child │
└──────────────────────────────────────────� This is how
fork
│
▼
Currently, the entire memory is not copied
│
▼
Child is executed
│
▼
exec
│
▼
New program is replaced
As a result, a lot of time and memory is saved
What happens behind the scenes
After fork, the operating system can keep the Parent and Child Pages as Shared
But it protects these Pages for writing
Now if one of the Processes wants to write to such a Page
Write Attempt
│
▼
CPU detects protected write
│
▼
Page Fault
│
▼
Kernel handles it
│
▼
Copy the Page
│
▼
Give the writer its own Page
│
▼
Continue execution
That is, Page Fault does not always mean that the program is broken
Sometimes Page Fault is a completely normal occurrence in memory management
This will be much more useful when we get to Virtual Memory and Paging
An important point
Suppose a program changes a variable in Parent and you expect Child to see the new value too
If Parent and Child are two independent processes. Usually this expectation is not correct. After one of the processes changes that page due to COW, each has its own version. That is, Parent Memory│
▼
Separate Page
Child Memory│
▼
Separate Page
So:
Parent Memory ≠ Child Memory
This is important for analyzing multiprocess programs. For example, when debugging, Parent and Child may initially behave similarly. But after one of them changes its memory, their paths diverge. Don't confuse COW with Shared Memory. These two look similar, but they are not the same. In COW, the data may be shared. One wants to write. Separate versions are created. But in Shared Memory,
Parent│
▼
Shared Memory▲
│
Child
If Parent changes the data, Child can see the change. So the purpose of these two is different. The purpose of COW is to make fork more efficient. And the operating system does not have to copy the entire memory for no reason
The purpose of Shared Memory is that several processes can actually use a shared memory space
Copy on Write or COW
After fork, the entire memory does not have to be copied immediately
Parent and Child
can use shared physical pages first
Pages
are protected to prevent direct writing
When one of the processes tries to change something, a Page Fault occurs
Kernel
handles this event
A separate copy of that page is created
The process
that has changed the information uses its own copy
How does the operating system create a fork without copying everything
In previous posts, we said that when a fork is executed, a Child Process is created
At first glance, we might think that the operating system does this
Parent Process
│
▼
Copying the entire memory
│
▼
Child Process
That is, it takes the entire memory of the Parent and copies it again for the Child
But this has a big problem
Suppose how many gigabytes of memory the Parent has used
If the operating system copies the entire memory every time a fork is executed
It takes a lot of time
It uses a lot of RAM
The CPU
also gets unnecessarily busy copying information
But this time the operating system acts a little more wisely
Copy on Write or COW solution
The operating system says
Don't copy anything for now
After the fork, at first the Parent and Child can use the same memory pages
Simply
Physical Memory
│
┌───────────────┐
│ │
Parent Child
│ │
└─�
│
Shared Page
As long as neither of them has changed anything, there is no need to create a separate copy
Now what happens if one of them wants to change something
Suppose Parent and Child initially have this value
int x = 10
Now Child wants to do this
x = 20
Here the operating system understands that Child wants to change the shared Page
So this is what happens
Before changing
Parent
│
▼
┌──────────────┐
│ Page x = 10 │
└───────────┘
▲
│
Child
That is, both of them have access to the same Page for now
Now Child wants to write something to that Page
The operating system does not copy the entire memory
It copies only the Page that is needed
Then the result will be something like this
Physical Memory
┌─
┌───────────────────────────────────
│ Page x = 20 │
│ Child │
└──────────────────────────────────────────� This is how
fork
│
▼
Currently, the entire memory is not copied
│
▼
Child is executed
│
▼
exec
│
▼
New program is replaced
As a result, a lot of time and memory is saved
What happens behind the scenes
After fork, the operating system can keep the Parent and Child Pages as Shared
But it protects these Pages for writing
Now if one of the Processes wants to write to such a Page
Write Attempt
│
▼
CPU detects protected write
│
▼
Page Fault
│
▼
Kernel handles it
│
▼
Copy the Page
│
▼
Give the writer its own Page
│
▼
Continue execution
That is, Page Fault does not always mean that the program is broken
Sometimes Page Fault is a completely normal occurrence in memory management
This will be much more useful when we get to Virtual Memory and Paging
An important point
Suppose a program changes a variable in Parent and you expect Child to see the new value too
If Parent and Child are two independent processes. Usually this expectation is not correct. After one of the processes changes that page due to COW, each has its own version. That is, Parent Memory│
▼
Separate Page
Child Memory│
▼
Separate Page
So:
Parent Memory ≠ Child Memory
This is important for analyzing multiprocess programs. For example, when debugging, Parent and Child may initially behave similarly. But after one of them changes its memory, their paths diverge. Don't confuse COW with Shared Memory. These two look similar, but they are not the same. In COW, the data may be shared. One wants to write. Separate versions are created. But in Shared Memory,
Parent│
▼
Shared Memory▲
│
Child
If Parent changes the data, Child can see the change. So the purpose of these two is different. The purpose of COW is to make fork more efficient. And the operating system does not have to copy the entire memory for no reason
The purpose of Shared Memory is that several processes can actually use a shared memory space
Copy on Write or COW
After fork, the entire memory does not have to be copied immediately
Parent and Child
can use shared physical pages first
Pages
are protected to prevent direct writing
When one of the processes tries to change something, a Page Fault occurs
Kernel
handles this event
A separate copy of that page is created
The process
that has changed the information uses its own copy
❤1
ReverseEngineering
Copy on Write یا COW سیستم عامل چطور بدون کپی کردن همه چیز fork میسازه توی پست های قبلی گفتیم وقتی fork اجرا میشه یک Child Process ساخته میشه در نگاه اول شاید فکر کنیم سیستم عامل این کارو میکنه Parent Process │ ▼ کپی کامل حافظه │ ▼ Child Process یعنی کل…
As a result, less RAM and CPU are consumed
To put it very simply in a sentence
Copy on Write
means that the operating system does not copy anything until it is forced to, and this is one of the good examples that shows how interdependent Process and Memory are
@reverseengine
To put it very simply in a sentence
Copy on Write
means that the operating system does not copy anything until it is forced to, and this is one of the good examples that shows how interdependent Process and Memory are
@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
برای این مرحله معمولا میریم سراغ ابزارهایی مثل
پس مسیر کلی کار میتونه این شکلی باشه
Seed Input
│
▼
AFL++
│
▼
New Paths
│
▼
Interesting Input
│
▼
Crash
│
▼
GDB
│
▼
Root Cause Analysis
یعنی Fuzzer یه ورودی جالب پیدا میکنه
بعد ممکنه همین ورودی باعث Crash بشه
از اینجا به بعد ما وارد ماجرا میشیم و باید بفهمیم علت واقعی مشکل چی بوده
AFL++
با فرستادن ورودی های مختلف فقط دنبال Crash نیست مسیرهای جدید برنامه رو هم دنبال میکنه
Instrumentation
کمک میکنه بفهمیم یه ورودی چه بخش هایی از برنامه رو اجرا کرده
هر مسیر جدید میتونه یه راه جدید برای رسیدن به یه باگ پنهان باشه
و وقتی Crash پیدا شد
کار Fuzzer تموم میشه
و نوبت ما میرسه که علت واقعی مشکل رو پیدا کنیم
تمرین:
همین برنامه رو با یه شرط سوم تغییر بدید
مثلا بعد از !HELLO یه شرط جدید اضافه کنید بعد روی کاغذ فکر کنید AFL++ چه ورودی هایی ممکنه به ترتیب پیدا کنه تا هر سه مسیر برنامه رو پوشش بده
@reverseengine
GDB
Ghidra
IDA
ASan
پس مسیر کلی کار میتونه این شکلی باشه
Seed Input
│
▼
AFL++
│
▼
New Paths
│
▼
Interesting Input
│
▼
Crash
│
▼
GDB
│
▼
Root Cause Analysis
یعنی Fuzzer یه ورودی جالب پیدا میکنه
بعد ممکنه همین ورودی باعث Crash بشه
از اینجا به بعد ما وارد ماجرا میشیم و باید بفهمیم علت واقعی مشکل چی بوده
AFL++
با فرستادن ورودی های مختلف فقط دنبال Crash نیست مسیرهای جدید برنامه رو هم دنبال میکنه
Instrumentation
کمک میکنه بفهمیم یه ورودی چه بخش هایی از برنامه رو اجرا کرده
هر مسیر جدید میتونه یه راه جدید برای رسیدن به یه باگ پنهان باشه
و وقتی Crash پیدا شد
کار Fuzzer تموم میشه
و نوبت ما میرسه که علت واقعی مشکل رو پیدا کنیم
تمرین:
همین برنامه رو با یه شرط سوم تغییر بدید
مثلا بعد از !HELLO یه شرط جدید اضافه کنید بعد روی کاغذ فکر کنید AFL++ چه ورودی هایی ممکنه به ترتیب پیدا کنه تا هر سه مسیر برنامه رو پوشش بده
@reverseengine