ReverseEngineering
1.32K subscribers
50 photos
11 videos
106 files
888 links
Download Telegram
این یکی از مهم‌ترین بخش‌ های Heap هست اگر این مفهوم رو خوب یاد بگیرید خیلی از رفتارهای malloc() و free() براتون قابل پیش‌ بینی میشه

Bin چیه و Allocator حافظه‌های آزاد
رو چطور مدیریت میکنه؟

در پست قبل گفتیم وقتی free() رو صدا میزنیم معمولا حافظه فورا به سیستم‌ عامل برنمیگرده

سوال اینجاست:

پس این حافظه کجا میره؟

اینجاست که مفهوم Bin وارد میشه

Bin یعنی چی؟

به زبان ساده Bin مثل یک انبار یا لیست انتظار برای Chunk های آزاده
وقتی یک Chunk آزاد میشه Allocator اون رو دور نمیندازه بلکه داخل یکی از Bin ها قرار میده تا اگر بعدا برنامه دوباره به حافظه‌ ای با اندازه مشابه نیاز داشت همون Chunk رو دوباره استفاده کنه

این کار باعث میشه:

سرعت malloc() بیشتر بشه
تعداد درخواست‌ ها از سیستم‌ عامل کمتر بشه

مصرف حافظه بهینه‌ تر بشه
چرا چند نوع Bin وجود داره؟
همه‌ی Chunk ها اندازه یکسانی ندارن
مثلا ممکنه یک برنامه هم 32 بایت حافظه بخاد هم 512 بایت و هم چند کیلوبایت
اگر همه‌ی Chunk ها داخل یک لیست قرار بگیرن پیدا کردن Chunk مناسب زمان زیادی میبره به همین دلیل Allocator اونا رو بر اساس اندازه دسته‌ بندی میکنه

انواع Bin در glibc

در نسخه‌های امروزی glibc معمولاً با این Binها رو به‌ رو میشیم:
🔹 Tcache
جدیدترین و سریع‌ترین بخش
هر Thread یک Tcache مخصوص خودش داره وقتی Chunkهای کوچیک آزاد میشن معمولا اول وارد Tcache میشن تا اگر دوباره همون اندازه نیاز شد خیلی سریع استفاده بشن
هدف اصلی Tcache افزایش سرعت برنامه هست

🔹 Fast Bin
برای Chunkهای کوچیک استفاده میشه
ویژگی مهم Fast Bin اینه که هنگام آزاد شدن Chunkها عملیات ادغام (Coalescing) بلافاصله انجام نمیشه
این موضوع سرعت رو بالا میبره اما مدیریت حافظه رو کمی پیچیده‌ تر میکنه


🔹 Unsorted Bin
وقتی بعضی Chunk ها آزاد میشن اول وارد Unsorted Bin میشن
بعدا Allocator تصمیم میگیره اونها رو به Bin مناسب منتقل کنه
میشه گفت Unsorted Bin یک محل موقت برای Chunk های آزاده


🔹 Small Bin
برای Chunk هایی با اندازه مشخص و نسبتا کوچیک استفاده میشه
چون اندازه‌ ها مشخصن پیدا کردن Chunk مناسب سریع انجام میشه

🔹 Large Bin
برای Chunk های بزرگ‌ تر استفاده میشه
اینجا Allocator باید دقت بیشتری داشته باشه تا بهترین Chunk رو برای درخواست جدید انتخاب کنه

چرا شناخت Bin ها مهمه؟
چون رفتار malloc() و free() کاملا به همین ساختارها وابسته ست
وقتی یک برنامه حافظه درخواست میکنه Allocator اول بررسی میکنه که آیا داخل Bin ها Chunk مناسبی وجود داره یا نه
اگر پیدا بشه همون رو برمیگردونه
اگر پیدا نشه از Heap یا در بعضی شرایط از سیستم‌ عامل حافظه جدید میگیره

آیا همیشه از سیستم‌ عامل حافظه جدید گرفته میشه؟ خیر

در بیشتر مواقع Allocator سعی میکنه از حافظه‌ هایی که قبلا آزاد شدن استفاده کنه
به همین دلیل ممکن است دو بار malloc() پشت سر هم همون آدرسی رو برگردونه که قبلا با free() آزاد شده بود این رفتار کاملا طبیعیه و برای افزایش کارایی انجام میشه


Bin
ها محل نگهداری Chunk های آزادن Allocator با استفاده از اونها تلاش میکنه بدون درخواست مداوم حافظه از سیستم‌ عامل حافظه‌ های آزاد شده رو دوباره استفاده کنه همین طراحی باعث شده Heap هم سریع‌ تر باشد و هم پیچیده‌ تر از Stack

@reverseengine
ReverseEngineering
این یکی از مهم‌ترین بخش‌ های Heap هست اگر این مفهوم رو خوب یاد بگیرید خیلی از رفتارهای malloc() و free() براتون قابل پیش‌ بینی میشه Bin چیه و Allocator حافظه‌های آزاد رو چطور مدیریت میکنه؟ در پست قبل گفتیم وقتی free() رو صدا میزنیم معمولا حافظه فورا به…
This is one of the most important parts of the Heap. If you learn this concept well, many of the behaviors of malloc() and free() will be predictable for you.

What is Bin and how does Allocator manage freed memory?

In the previous post, we said that when we call free(), the memory is usually not returned to the operating system immediately.

The question is:

So where does this memory go?

This is where the concept of Bin comes in.

What does Bin mean?

In simple terms, Bin is like a warehouse or waiting list for freed Chunks.

When a Chunk is freed, the Allocator does not throw it away, but places it in one of the Bins so that if the program needs memory of the same size again later, it can reuse the same Chunk.

This will:

Increase the speed of malloc().

Reduce the number of requests from the operating system.

Optimize memory usage.

Why are there several types of Bins?

Not all chunks are the same size
For example, a program may need 32 bytes of memory, 512 bytes, or a few kilobytes
If all the chunks are in a list, finding the right chunk will take a long time, so the Allocator sorts them by size

Types of Bins in glibc

In today's versions of glibc, we usually encounter these bins:

🔹 Tcache
The newest and fastest part
Each thread has its own Tcache. When small chunks are freed, they usually enter Tcache first so that if the same size is needed again, they can be used very quickly
The main purpose of Tcache is to increase the speed of the program

🔹 Fast Bin
It is used for small chunks
The important feature of Fast Bin is that when the chunks are freed, the coalescing operation is not performed immediately
This increases speed, but makes memory management a little more complicated

🔹 Unsorted Bin
When some chunks are freed, they enter Tcache first Unsorted Bin
Later, the Allocator decides to move them to the appropriate Bin
You can say that the Unsorted Bin is a temporary place for free Chunks

🔹 Small Bin
Used for Chunks of a specific and relatively small size
Since the sizes are specific, finding the appropriate Chunk is done quickly

🔹 Large Bin
Used for larger Chunks
Here the Allocator must be more careful to choose the best Chunk for the new request

Why is it important to know the Bins?

Because the behavior of malloc() and free() is completely dependent on these structures
When a program requests memory, the Allocator first checks whether there is a suitable Chunk in the Bins or not
If it is found, it returns it
If not found, it gets new memory from the Heap or in some cases from the operating system

Is new memory always taken from the operating system? No

In most cases, the Allocator tries to use previously freed memory

That is why malloc() may return the same address twice in a row that was previously freed with free(). This behavior is completely normal and is done to increase performance

Bins are where freed chunks are stored. The Allocator uses them to try to reuse freed memory without constantly requesting memory from the operating system. This design makes the Heap both faster and more complex than the Stack

@reverseengine
IDA pro 9.4

Linux + Windows + Mac + ARM

download

tree "/home/data/0x01/IDA/IDA pro 9.4/"
/home/reeves/data/soft/IDA/IDA pro 9.4/
├── ida-pro_94_armlinux.run
├── ida-pro_94_armmac.app.zip
├── ida-pro_94_armwin.exe
├── ida-pro_94_x64linux.run
├── ida-pro_94_x64mac.app.zip
├── ida-pro_94_x64win.exe
├── kg_patch
│   ├── idapro.hexlic
│   ├── keygen.js
│   ├── README
│   ├── x64linux
│   │   ├── libida32.so
│   │   └── libida.so
│   └── x64win
│   ├── ida32.dll
│   └── ida.dll
└── misc
├── hexlicsrv94_x64linux.run
├── hexvault94_x64linux.run
├── idapin94.zip
└── lumina94_x64linux.run

5 directories, 17 files


@reverseengine
🔥3
Control Flow Flattening
وقتی مسیر اجرای برنامه عمدا به‌ هم ریخته میشه

یکی از رایج‌ترین تکنیک‌هایی که هم توی محافظ‌ های نرم‌ افزاری و هم توی بعضی بدافزار ها دیده میشه Control Flow Flattening هست

هدفش فقط یک چیزه

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

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

if (x > 10)
A();
else
B();

C();

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

یا وارد A() میشه یا B() و بعد C() اجرا میشه

حالا همین منطق رو با Control Flow Flattening مبهم میکنیم

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

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

مثلا چیزی شبیه این:

while (1)
{
switch(state)
{
case 0:
...
state = 3;
break;

case 3:
...
state = 7;
break;

case 7:
...
return;
}
}


اینجا دیگه ترتیب واقعی اجرای برنامه مشخص نیست

همه چیز داخل یک حلقه و یک switch بزرگ مخفی شده

به همین خاطر وقتی فایل رو داخل IDA یا Ghidra باز میکنید نمودار Control Flow خیلی شلوغ و عجیب به نظر میرسه

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

یکی از نشونه‌های Control Flow Flattening همین حلقه مرکزیه

تقریبا تمام بلوک‌ های برنامه بعد از اجرا دوباره به همون حلقه برمیگردن و منتظر تصمیم بعدی میمونن

کاری که تحلیلگر انجام میده اینه که به جای دنبال کردن ظاهر کد تغییرات متغیر state رو دنبال میکنه

چون این متغیر مشخص میکنه بعد از هر مرحله برنامه واقعا قراره کجا بره

وقتی مسیر تغییر state رو روی کاغذ رسم کنید کم‌ کم مسیر واقعی اجرای برنامه دوباره مشخص میشه

در واقع هدف تحلیلگر اینه که این ساختار مبهم رو دوباره به همون if و while و for های ساده اولیه تبدیل کنه

به این کار Deflattening میگن

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

تمرین:

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

ابتدا مقداردهی اولیه

بعد انجام یک محاسبه

در اخر چاپ نتیجه

حالا بدون استفاده از if یا اجرای مستقیم این سه مرحله رو فقط با یک متغیر state و یک switch اجرا کنید

بعد سعی کنید مسیر اجرای برنامه رو فقط با دنبال کردن تغییرات state روی کاغذ رسم کنید

وقتی این تمرین رو انجام بدید مفهوم Control Flow Flattening رو خیلی بهتر درک میکنید
❤2
ReverseEngineering
Control Flow Flattening وقتی مسیر اجرای برنامه عمدا به‌ هم ریخته میشه یکی از رایج‌ترین تکنیک‌هایی که هم توی محافظ‌ های نرم‌ افزاری و هم توی بعضی بدافزار ها دیده میشه Control Flow Flattening هست هدفش فقط یک چیزه اینکه نفهمید برنامه از کجا شروع شده و قراره…
Control Flow Flattening
When the program execution path is deliberately messed up

One of the most common techniques seen in both software protectors and some malware is Control Flow Flattening

Its goal is only one thing

To make it difficult to understand where the program started and where it is going to go

Suppose the original code is this:

if (x > 10)
A();
else
B();

C();


The execution path of this code is very clear

Either it enters A() or B() and then C() is executed

Now we obfuscate this logic with Control Flow Flattening

Instead of the program execution proceeding straight ahead, everything enters a big loop

Inside that loop, only one variable specifies what the next step is

For example, something like this:

while (1)
{
switch(state)
{
case 0:
...
state = 3;
break;

case 3:
...
state = 7;
break;

Case 7:
...
return;
}
}


Here the actual order of program execution is no longer clear

Everything is hidden inside a loop and a big switch

That's why when you open the file in IDA or Ghidra, the Control Flow diagram looks very busy and strange

Instead of seeing a few simple paths, you see dozens of different paths that all return to the same point

One of the signs of Control Flow Flattening is this central loop

Almost all blocks of the program return to the same loop after execution and wait for the next decision

What the analyzer does is that instead of following the appearance of the code, it follows the changes of the state variable

Because this variable determines where the program is really going to go after each step

When you draw the path of state changes on paper, the actual path of program execution gradually becomes clear again

In fact, the analyzer's goal is to convert this ambiguous structure back to the same simple if, while, and for statements

This is called Deflating

That is, returning the program execution path to the state Natural

Exercise:

Write a very simple program that has three steps

First initialize

Then perform a calculation

Finally print the result

Now, without using if or direct execution, execute these three steps with just a state variable and a switch

Then try to draw the execution path of the program on paper just by following the state changes

When you do this exercise, you will understand the concept of Control Flow Flattening much better

@reverseengine
❤3
Hook Detection
و مفهوم EDR Hooking

یکی از پایه‌ ای ترین چیز هایی که برای فهم بایپس EDR باید بدونید Hook هست وقتی یک EDR بخواد رفتار یک برنامه رو زیر نظر بگیره یکی از مکانیزم‌ هایی که ممکنه استفاده کنه اینه که مسیر اجرای بعضی Function ها رو تحت نظر بگیره

مثلا به‌صورت مفهومی:

Application
↓
Windows API
↓
EDR Monitoring
↓
Native API
↓
Kernel

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

Inline Hook

در Inline Hook ابتدای یک Function تغییر میکنه تا اجرای برنامه به مسیر دیگه ای هدایت بشه:

Original:

Function


├── Instruction 1
├── Instruction 2
├── Instruction 3

Hooked:

Function

│
└── Jump ──────► Monitoring Code
│
▼
Original Code

به همین دلیل وقتی درباره تکنیک‌ هایی مثل Unhooking, Direct Syscall و Indirect Syscall میخونید باید اول دقیقا بفهمید Hook چه مشکلی ایجاد میکنه

نکته مهم

Hook
شدن به معنای اینه که EDR تمام سیستم رو از طریق همین روش میبینه نیست

EDR
های مدرن فقط به یک Hook وابسته نیستن و میتونن از چندین منبع telemetry استفاده کنن

برای همین این زنجیره رو باید اینطوری ببینید:

API Hook
+
ETW
+
Kernel Telemetry
+
Process Behavior
+
Memory Analysis
+
Network Telemetry

و این دقیقا دلیل پیچیده شدن بحث AV/EDR Evasion در سال‌ های اخیر شده

@reverseengine


Hook Detection And the Concept of EDR Hooking

One of the most basic things you need to know to understand EDR bypass is Hook. When an EDR wants to monitor the behavior of an application, one of the mechanisms it may use is to monitor the execution path of some Function

For example, conceptually:

Application
↓
Windows API
↓
EDR Monitoring
↓
Native API
↓
Kernel

That is, the application thinks it has called a normal function, but before reaching the main path, a monitoring layer can check the activity

Inline Hook

In an Inline Hook, the beginning of a Function is changed to redirect the execution of the application to another path:

Original:

Function

├── Instruction 1
├── Instruction 2
├── Instruction 3

Hooked:

Function

│
└── Jump ──────► Monitoring Code
│
▼
Original Code

That is why when you read about techniques such as Unhooking, Direct Syscall and Indirect Syscall, you should First, understand exactly what the problem is with Hooks

Important point

Being Hooked
does not mean that EDR sees the entire system in this way

Modern EDRs do not rely on just one Hook and can use multiple telemetry sources

So you should think of this chain as:

API Hook
+
ETW
+
Kernel Telemetry
+
Process Behavior
+
Memory Analysis
+
Network Telemetry

And this is exactly why the AV/EDR Evasion discussion has become so complicated in recent years

@reverseengine
Opaque Predicate
شرط‌ هایی که ظاهرشون گمراه‌ کننده ست

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

Opaque Predicate
یعنی برنامه یک شرط میسازه که از ظاهرش مشخص نیست نتیجه همیشه چی میشه ولی سازنده از قبل میدونه نتیجه چیه

مثلا از نظر منطقی ممکنه چنین چیزی داشته باشیم:

int x = 10;

if ((x * x) % 2 == 0)
{
A();
}
else
{
B();
}


اگه مقدار x همیشه 10 باشه شرط همیشه درست میشه ولی وقتی همین منطق به اسمبلی تبدیل و با محاسبات اضافه قاطی بشه تشخیصش سخت تر میشه
مثلا ممکنه به جای یک شرط ساده چند عملیات پشت سر هم ببینید:

محاسبه یک مقدار

XOR

ADD

CMP

JZ

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

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

فرض کنید به چنین ساختاری رسیدید:

شرط:
/ \ مسیرA
/ \ مسیر B
\ / ادامه


اولین سوال این نیست که کدوم مسیر بهتر به نظر میرسه

سوال اینه:

مسیر B اصلا قابل رسیدنه؟

اگر جواب نه باشه احتمالا با یک Opaque Predicate یا یک شرط غیرقابل‌ وقوع طرف هستیم

یکی از کاربردهای این تکنیک شلوغ کردن Control Flow Graph هست

تحلیلگر وقتی نمودار رو باز میکنه تعداد زیادی مسیر میبینه

اما بخشی از این مسیرها فقط برای گمراه کردن تحلیل ایجاد شدن

نکته مهم اینه که Opaque Predicate همیشه به معنی یک شرط ثابت مثل true یا false نیست

بعضی وقتا نتیجه شرط به یک invariant وابسته ست

یعنی مقداری که با وجود تغییرات مختلف برنامه یک ویژگی مشخص رو حفظ میکنه

برای همین شما باید منطق ریاضی و رفتار داده‌ ها رو هم بررسی کنید

تمرین

این شرط رو بررسی کنید:

int x = 10;

if ((x * 2) % 2 == 0)
{
printf("A");
}
else
{
printf("B");
}


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

این دقیقا همون ذهنیتی هست که برای تحلیل Obfuscation لازم داریم

@reverseengine
ReverseEngineering
Opaque Predicate شرط‌ هایی که ظاهرشون گمراه‌ کننده ست حالا یکی از تکنیک‌ هایی رو که میخایم بررسی کنیم که معمولا کنار Obfuscation استفاده میشه Opaque Predicate یعنی برنامه یک شرط میسازه که از ظاهرش مشخص نیست نتیجه همیشه چی میشه ولی سازنده از قبل میدونه نتیجه…
Opaque Predicate

Conditions that are deceptive in appearance

Now we will examine one of the techniques that is usually used alongside Obfuscation

Opaque Predicate
That is, the program creates a condition that is not always clear from its appearance what the result will be, but the constructor already knows what the result will be

For example, logically we might have something like this:

int x = 10;

if ((x * x) % 2 == 0)
{
A();
}
else
{
B();
}


If the value of x is always 10, the condition will always be true, but when this logic is converted to assembly and mixed with additional calculations, it becomes more difficult to detect. For example, instead of a simple condition, you may see several operations in a row:

Calculate a value

XOR

ADD

CMP

JZ


Decompiler
may also make a very complex condition out of this operation. This is where the analyst should not just trust the appearance of the condition

He should understand whether both paths are actually feasible or whether one of the paths has never actually happened.

Suppose you get this structure:

Condition:

/ \ PathA
/ \ PathB
\ / continue

The first question is not which path looks better

The question is:

Is path B even feasible?

If the answer is no, we are probably dealing with an Opaque Predicate or an unfulfilled condition

One of the uses of this technique is to clutter the Control Flow Graph

When the analyst opens the graph, he sees a lot of paths

But some of these paths are created just to mislead the analysis

The important point is that Opaque Predicate does not always mean a fixed condition such as true or false

Sometimes the result of the condition depends on an invariant

That is, a value that maintains a certain property despite various program changes

For this reason, you should also examine the mathematical logic and data behavior

Exercise:

Examine this condition:

int x = 10;

if ((x * 2) % 2 == 0)

{
printf("A");
}
else

{
printf("B");
}


Determine which path can be executed without executing the program. Then try the same idea with a more complex condition and see if you can mathematically determine which branch is practically infeasible.

This is exactly the mindset we need for Obfuscation Analysis.

@reverseengine
Tcache

در پست قبل گفتیم که glibc برای مدیریت Chunk های آزاد از ساختار های مختلفی مثل Bin ها استفاده میکنه اما یک ساختار مهم وجود داره که باید جداگانه بشناسیم:

Tcache
اصلا چرا ساخته شد؟
برای پیدا کردن و استفاده مجدد از Chunk های آزاد Allocator بیشتر به ساختار های عمومی Heap متکی بود
مشکل اینجا بود که در برنامه‌های چند نخی چند Thread میتونستن همزمان بخوان از همین ساختار ها استفاده کنن

پس:

مدیریت حافظه میتونست هزینه و پیچیدگی بیشتری داشته باشه


glibc
برای سریع‌ تر کردن تخصیص حافظه Thread Cache یا همون Tcache رو معرفی کرد

اسم Tcache از کجا میاد؟

Tcache Thread Local Cache
هست

یعنی هر Thread یک Cache مخصوص خودش داره

در نتیجه وقتی یک Thread حافظه‌ ای رو آزاد میکنه در بسیاری از موارد میتونه اون Chunk رو در Cache خودش نگه داره
بعد اگر همون

Thread


دوباره حافظه‌ ای با اندازه مناسب بخواد Allocator میتونه خیلی سریع از Tcache استفاده کنه

ساختارش چطوریه؟

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

Thread
│
└── Tcache
├── Chunk
├── Chunk
├── Chunk

Tcache
برای اندازه‌های مختلف مجموعه‌ ای از لیست‌های آزاد داره

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

size A → chunk → chunk → chunk

size B → chunk → chunk

size C → chunk → chunk → chunk

یعنی Chunk ها بر اساس اندازه در دسته‌ های مختلف قرار میگیرن

وقتی malloc اجرا میشه چه اتفاقی میوفته؟

فرض کنید برنامه درخواست حافظه میکنه:

malloc(size);

Allocator
اول بررسی میکنه آیا Chunk مناسبی در مسیر های سریع خودش وجود داره یا نه
اگر Chunk مناسب در Tcache موجود باشه میتونه همون رو در اختیار برنامه قرار بده در این حالت لازم نیست هر بار سراغ ساختار های پیچیده‌تر Heap بره

نتیجه؟

سرعت بیشتر

وقتی free اجرا میشه چی؟
برعکس همین اتفاق هم ممکنه رخ بده
وقتی برنامه مینویسه:

free(ptr);

Chunk
آزاد شده میتونه در Tcache قرار بگیره تا بعدا دوباره مورد استفاده قرار بگیره

پس:

malloc()
↓
استفاده از Chunk
↓
free()
↓
Tcache
↓
malloc()
↓
استفاده مجدد

این چرخه یکی از پایه‌های مهم رفتار Heap در glibc محسوب میشه

چرا Tcache برای ما در Binary Exploitation مهمه؟

اینجا قضیه جالب میشه
هر چیزی که حافظه رو سریع‌ تر مدیریت کنه در صورت وجود یک Memory Corruption Bug میتونه رفتار متفاوتی نسبت به ساختارهای قدیمی‌ تر Heap داشته باشه

به همین دلیل وقتی یک Heap Bug رو بررسی میکنیم باید بدونیم:

Chunk
موردنظر الان کجاست؟


ممکنه:
داخل Tcache باشه
داخل یکی از Binهای دیگه باشه
هنوز در اختیار برنامه باشه
یا بخشی از Heap بزرگ‌تر شده باشه
پس بدون شناخت Tcache تحلیل Heap مدرن ناقص میمونه

یک نکته مهم
Tcache
خودش یک آسیب‌پذیری نیست
اتفاقا برای بهبود عملکرد Allocator طراحی شده
اما وقتی یک برنامه دچار باگ‌هایی مثل:

Use-After-Free
یا
Double Free

میشه رفتار Tcache میتونه روی نحوه‌ ی بروز و تحلیل مشکل تاثیر بذاره به همین دلیل در مباحث Heap Exploitation اسم Tcache رو زیاد میبینیم


Tcache

یک Thread Local Cache در glibc که برای سریع‌ تر کردن مدیریت Chunk های کوچیک طراحی شده هر Thread میتونه Chunk های آزاد شده رو داخل Cache خودش نگه داره و در درخواست‌ های بعدی دوباره از اونها استفاده کنه
برای فهم Heap مدرن شناخت Tcache خیلی مهمه چون رفتار malloc() و free() در حضور Tcache با مدل‌ های قدیمی‌ تر Heap تفاوت‌ هایی داره
@reverseengine
ReverseEngineering
Tcache در پست قبل گفتیم که glibc برای مدیریت Chunk های آزاد از ساختار های مختلفی مثل Bin ها استفاده میکنه اما یک ساختار مهم وجود داره که باید جداگانه بشناسیم: Tcache اصلا چرا ساخته شد؟ برای پیدا کردن و استفاده مجدد از Chunk های آزاد Allocator بیشتر به…
Tcache In the previous post, we said that glibc uses various structures such as Bins to manage free Chunks, but there is an important structure that we need to know separately:

Why was Tcache created at all?

Allocator relied more on general Heap structures to find and reuse free Chunks

The problem was that in multi-threaded programs, multiple threads could read from the same structures at the same time

So:

Memory management could be more expensive and complex

glibc introduced Thread Cache, or Tcache, to speed up memory allocation

Where does the name Tcache come from?

Tcache is Thread Local Cache

That is, each Thread has its own Cache

As a result, when a Thread frees memory, in many cases it can keep that Chunk in its Cache

Then, if the same

Thread

requests memory of the appropriate size again, the Allocator can use Tcache very quickly

What is its structure?

If we imagine it very simply:

Thread
│
└── Tcache
├── Chunk
├── Chunk
├── Chunk

Tcache
has a set of free lists for different sizes

For example, you can imagine:

size A → chunk → chunk → chunk

size B → chunk → chunk

size C → chunk → chunk → chunk → chunk

That is, the Chunks are placed in different categories based on size

What happens when malloc is executed?

Suppose the program requests memory:

malloc(size);


Allocator
First, it checks whether there is a suitable Chunk in its fast paths or not. If a suitable Chunk is available in Tcache, it can make it available to the program. In this case, it does not need to resort to more complex Heap structures every time.

Result?

Higher speed

What happens when free is executed?
The opposite can also happen
When the program writes:

free(ptr);

The freed chunk can be placed in Tcache to be reused later

Then:

malloc()
↓
Using Chunk
↓
free()
↓
Tcache
↓
malloc()
↓
Reuse


This cycle is one of the important foundations of the Heap behavior in glibc

Why is Tcache important to us in Binary Exploitation?

Here's where it gets interesting
Anything that manages memory faster can behave differently than older Heap structures if there is a Memory Corruption Bug

That's why when we investigate a Heap Bug we need to know:

Where is the Chunk
in question now?

It may be:

Inside Tcache
Inside one of the other bins
Still in the hands of the program
Or part of the Heap has grown bigger
So without knowing Tcache, the analysis of the modern Heap remains incomplete

An important point

Tcache
Itself is not a vulnerability
It is designed to improve the performance of the Allocator
However, when a program has bugs such as:

Use-After-Free
Or
Double Free

The behavior of Tcache can affect the way the problem occurs and is analyzed, which is why we often see the name Tcache in Heap Exploitation discussions

Tcache

A Thread Local Cache in glibc that is designed to speed up the management of small Chunks. Each Thread can keep the freed Chunks in its own Cache and reuse them in subsequent requests
To understand the modern Heap, it is very important to understand Tcache because the behavior of malloc() and free() in the presence of Tcache is different from older models of Heap

@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