en
Feedback

Don't get caught by a cheater! Telemetrio finds and tags such channels 👉 If you want to see the tag, subscribe

ReverseEngineering

ReverseEngineering

Open in Telegram
1 442
Subscribers
No data24 hours
+1087 days
+11030 days
Posts Archive
Fuzzing means first understand where the input comes from then understand how it is processed inside the program then do Fuzzing on that path This is much better than just generating a bunch of random data without knowing the program and hoping that something will explode Because the operating system and programs are unfortunately not designed to cooperate with our wishes When a Crash is found Now suppose the Fuzzer gives us a result Crash found input_0421.dat The work is not over here Incidentally, the interesting part has just begun We run the same Input again with the Debugger Input ↓ Program ↓ Crash ↓ Debugger ↓ Call Stack ↓ Faulting Instruction Now we want to find out Where exactly the Crash occurred Which instruction caused it What state did the registers have What was the memory like What path does the Call Stack show And most importantly What was the Root Cause For example, we might first just see the program crash on a specific instruction But this still does not mean that the same Instruction is the root cause of the bug It is possible that corrupt data was entered into the program a few steps earlier and its effect has only shown itself here. So the analysis path can look like this: Crash ↓ Faulting Instruction ↓ Register State ↓ Memory State ↓ Call Stack ↓ Data Flow ↓ Root Cause This is exactly where Fuzzing and Reverse Engineering come together Fuzzer says This Input caused the Crash Reverse Engineer says Well, now let's see why When we don't have Source Code First, we don't go to Fuzzing blindly First, we examine the Binary Then we look for Input Then we find the path of Input movement inside the program, then we understand the Input Format structure as much as we can Then we find the appropriate Seed Then we put the Fuzzer on that path And if a Crash is found We go back to Debugger and Reverse Engineering The whole path can be seen like this Binary ↓ Reverse Engineering ↓ Input Discovery ↓ Input Analysis ↓ Seed ↓ Fuzzing ↓ Crash ↓ Debugger ↓ Root Cause Analysis The main point of this post is that when you don't have the source code it doesn't mean you can't do fuzzing you just need to understand the binary before fuzzing Reverse Engineering helps you understand where the data comes from where it is parsed what parts of the input are important and what paths it takes in the program and this information can greatly improve the quality of fuzzing @reverseengine

What we get from Assembly is not always 100% certain The compiler may have optimized the code The structure may be more complex There may be several different paths to parse the input So we need to check our hypotheses by actually running the program and debugging A very important point In Binary Only Fuzzing, there are usually three We have a very valuable stage Input Discovery ↓ Program Analysis

Binary Only Fuzzing When we don't have source code So far, we have mostly seen Fuzzing on programs whose source code we have But in Reverse Engineering and Fuzzing, there are many times when we only have an executable file For example program.exe or program We don't have the source Or proper documentation Or we don't even know exactly what's going on inside the program This is where we need to first examine the program To understand where it gets its input To understand how that input moves inside the program Then prepare the same path for Fuzzing Step One Opening the Binary First, we open the program in tools like Ghidra or IDA Our goal is not to understand the entire program from the beginning We are not going to sit down and read millions of instructions one by one First, we look for places where the program deals with external data For example, in Linux and various programs, we may encounter things like read recv fread fgets scanf In Windows also has things like ReadFile recv fgets can be a good clue But here's an important point Just seeing the name of a function doesn't mean that the main input to the program is there Data may be passed through several functions We may have a Wrapper There may be a separate Parser So we need to follow the data path Step 2 Follow the data path Suppose we get something like this in the Decompiler buffer = malloc(256); read(fd, buffer, size); process_data(buffer); Here we have a very good path External Input ↓ read ↓ buffer ↓ process_data That is, external data entered the program It is placed in a Buffer Then it goes into a function that probably processes that data. Now we not only know that the program has input, but we also have a clue where the input goes after it enters the program Step 3 Find out what the input looks like This part is very important Because the Fuzzer cannot just throw a bunch of random data into the program and expect the program to happily process it all The program usually expects the input to have a specific structure For example, the program may read a file program sample.dat Or take input from stdin program < sample.dat Or even input from the network Client ↓ Network ↓ recv ↓ Parser So we need to understand Where the input comes from What format it is Where it is parsed And what parts of the program work with it. This information is very important for Fuzzing because the more Understand the input structure Fuzzer can also generate more useful inputs Step 4 Creating a Seed Now suppose we understand that the program reads a file with its own format For example: sample.dat First we find a healthy and small file We call this Seed Then the Fuzzer modifies this Seed Seed ↓ Mutation ↓ Program ↓ New Path or Crash For example, a part of the file changes Then the program runs Fuzzer checks whether the program has entered a new path or not Or whether it has crashed Why is this important This is exactly where Fuzzing comes in handy Fuzzer Alone can't always figure out what the input structure is But we can find clues by examining the Assembly and Decompiler For example, suppose we see in the Assembly: cmp byte ptr [rax], 48h jne invalid Here we see the program is checking a specific Byte That is, it is probably A specific part of the input is important to the program We will see it later in another place cmp dword ptr [rax+4], 100h Here the program is checking a value at Offset 4 Now if we put a few of these checks together We can gradually guess what the input structure is For example, we might get something like this: Offset 0 ↓ Magic Offset 4 ↓ Size Offset 8 ↓ Flags Offset 12 ↓ Data That is, without having the Source Code, we can gradually deduce the Input Format structure from the Binary behavior Of course, one important point

Fuzzing یعنی اول بفهمید ورودی از کجا میاد بعد بفهمید چطور داخل برنامه پردازش میشه بعد Fuzzing رو روی همون مسیر انجام بده این خیلی بهتر از اینه که بدون شناخت برنامه فقط یه عالمه داده تصادفی تولید کنیم و امیدوار باشیم چیزی منفجر بشه چون سیستم عامل و برنامه‌ها متاسفانه برای همکاری با آرزوهای ما طراحی نشدن وقتی Crash پیدا شد حالا فرض کنید Fuzzer یه نتیجه بهمون داده Crash found input_0421.dat اینجا کار تموم نشده اتفاقا تازه بخش جالب ماجرا شروع شده همون Input رو دوباره با Debugger اجرا میکنیم Input ↓ Program ↓ Crash ↓ Debugger ↓ Call Stack ↓ Faulting Instruction حالا میخوایم بفهمیم دقیقا کجا Crash اتفاق افتاده کدوم Instruction باعثش شده Registerها چه وضعیتی داشتن Memory چه شکلی بوده Call Stack چه مسیری رو نشون میده و مهم‌تر از همه Root Cause چی بوده مثلا ممکنه اول فقط ببینیم برنامه روی یه Instruction خاص Crash کرده ولی این هنوز به معنی این نیست که همون Instruction ریشه اصلی باگه ممکنه چند مرحله قبل‌تر داده خراب وارد برنامه شده باشه و اثرش تازه اینجا خودش رو نشون داده باشه پس مسیر تحلیل میتونه این شکلی بشه: Crash ↓ Faulting Instruction ↓ Register State ↓ Memory State ↓ Call Stack ↓ Data Flow ↓ Root Cause این دقیقا همون جاییه که Fuzzing و Reverse Engineering دست همدیگه رو میگیرن Fuzzer میگه این Input باعث Crash شد Reverse Engineer میگه خب حالا ببینیم چرا وقتی Source Code نداریم اول نمیریم سراغ Fuzzing کورکورانه اول Binary رو بررسی میکنیم بعد دنبال Input میگردیم بعد مسیر حرکت Input داخل برنامه رو پیدا میکنیم بعد تا جایی که میتونیم ساختار Input Format رو میفهمیم بعد Seed مناسب پیدا میکنیم بعد Fuzzer رو روی اون مسیر قرار میدیم و اگر Crash پیدا شد برمیگردیم سراغ Debugger و Reverse Engineering کل مسیر رو میشه اینطوری دید Binary ↓ Reverse Engineering ↓ Input Discovery ↓ Input Analysis ↓ Seed ↓ Fuzzing ↓ Crash ↓ Debugger ↓ Root Cause Analysis نکته اصلی این پست اینه وقتی Source Code ندارید به این معنی نیست که نمیتونید Fuzzing انجام بدی فقط باید قبل از Fuzzing خود Binary رو بشناسید Reverse Engineering اینجا کمک میکنه بفهمی داده از کجا وارد میشه کجا Parse میشه چه بخش‌هایی از Input مهم هستن و چه مسیرهایی داخل برنامه طی میشن و همین اطلاعات میتونه کیفیت Fuzzing رو خیلی بهتر کنه @reverseengine

بخش بیست و نهم بافر اورفلو
Binary Only Fuzzing وقتی سورس کد نداریم تا اینجا Fuzzing رو بیشتر روی برنامه‌هایی دیدیم که سورس کدشون دستمون بود ولی توی Reverse Engineering و Fuzzing یه عالمه وقت پیش میاد که فقط یه فایل اجرایی داریم مثلا program.exe یا program نه سورس داریم نه مستندات درست و حسابی نه حتی دقیقا میدونیم داخل برنامه چه خبره اینجاست که باید اول برنامه رو بررسی کنیم بفهمیم ورودی رو از کجا میگیره بفهمیم اون ورودی چطوری داخل برنامه حرکت میکنه بعد همون مسیر رو برای Fuzzing آماده کنیم مرحله اول باز کردن Binary اول برنامه رو داخل ابزارهایی مثل Ghidra یا IDA باز میکنیم هدفمون این نیست که از همون اول کل برنامه رو بفهمیم قرار نیست بشینیم میلیون‌ها Instruction رو یکی یکی بخونیم اول دنبال جاهایی میگردیم که برنامه با داده خارجی سر و کار داره مثلا توی Linux و برنامه‌های مختلف ممکنه با چیزهایی مثل این برخورد کنیم read recv fread fgets scanf توی Windows هم چیزهایی مثل ReadFile recv fgets میتونن سرنخ خوبی باشن ولی اینجا یه نکته مهم داریم صرفا دیدن اسم یک تابع به این معنی نیست که همونجا ورودی اصلی برنامه قرار داره ممکنه داده از چندتا تابع رد بشه ممکنه Wrapper داشته باشیم ممکنه Parser جداگانه وجود داشته باشه پس باید مسیر داده رو دنبال کنیم مرحله دوم دنبال کردن مسیر ورود داده فرض کنید داخل Decompiler به چیزی شبیه این رسیدیم buffer = malloc(256); read(fd, buffer, size); process_data(buffer); اینجا یه مسیر خیلی خوب داریم External Input ↓ read ↓ buffer ↓ process_data یعنی داده خارجی وارد برنامه شده داخل یک Buffer قرار گرفته بعد رفته داخل تابعی که احتمالا اون داده رو پردازش میکنه حالا دیگه فقط نمیدونیم برنامه ورودی داره بلکه یه سرنخ داریم که ورودی بعد از وارد شدن به برنامه کجا میره مرحله سوم بفهمیم ورودی چه شکلیه این قسمت خیلی مهمه چون Fuzzer نمیتونه همینطوری یه مشت داده تصادفی بریزه توی برنامه و انتظار داشته باشه برنامه هم با خوشحالی همه رو پردازش کنه برنامه معمولا انتظار داره ورودی یه ساختار خاص داشته باشه مثلا ممکنه برنامه یک فایل بخونه program sample.dat یا ورودی رو از stdin بگیره program < sample.dat یا اصلا ورودی از شبکه بیاد Client ↓ Network ↓ recv ↓ Parser پس باید بفهمیم ورودی از کجا میاد چه فرمتی داره کجا Parse میشه و چه قسمت‌هایی از برنامه با اون کار میکنن این اطلاعات برای Fuzzing خیلی مهمن چون هرچی بیشتر ساختار ورودی رو بفهمیم Fuzzer هم میتونه ورودی‌های مفیدتری تولید کنه مرحله چهارم ساختن Seed حالا فرض کنید فهمیدیم برنامه یک فایل با فرمت خاص خودش رو میخونه مثلا: sample.dat اول یه فایل سالم و کوچک پیدا میکنیم به این میگیم Seed بعد Fuzzer همین Seed رو تغییر میده Seed ↓ Mutation ↓ Program ↓ New Path یا Crash` مثلا یه بخش از فایل تغییر میکنه بعد برنامه اجرا میشه Fuzzer بررسی میکنه آیا برنامه وارد مسیر جدیدی شده یا نه یا مثلا Crash کرده یا نه چرا اینجا مهمه اینجا دقیقا جاییه که این به کمک Fuzzing میاد Fuzzer به تنهایی همیشه نمیتونه بفهمه ساختار ورودی چیه ولی ما میتونیم با بررسی Assembly و Decompiler سرنخ‌هایی پیدا کنیم مثلا فرض کن توی Assembly میبینیم: cmp byte ptr [rax], 48h jne invalid اینجا میبینیم برنامه داره یک Byte مشخص رو بررسی میکنه یعنی احتمالا یه بخش خاص از ورودی برای برنامه اهمیت داره بعد یه جای دیگه میبینیم cmp dword ptr [rax+4], 100h اینجا برنامه داره مقداری رو در Offset چهار بررسی میکنه حالا اگه چندتا از این Checkها رو کنار هم بذاریم کم کم میتونیم حدس بزنیم ورودی چه ساختاری داره مثلا ممکنه به چیزی شبیه این برسیم: Offset 0 ↓ Magic Offset 4 ↓ Size Offset 8 ↓ Flags Offset 12 ↓ Data یعنی بدون داشتن Source Code هم میتونیم از روی رفتار Binary کم کم ساختار Input Format رو دربیاریم البته یه نکته مهم این چیزی که از Assembly درمیاریم همیشه صددرصد قطعی نیست ممکنه Compiler کد رو Optimize کرده باشه ممکنه ساختار پیچیده‌تر باشه ممکنه چندتا مسیر مختلف برای Parse کردن ورودی وجود داشته باشه پس باید فرضیه‌هامون رو با اجرای واقعی برنامه و Debugging بررسی کنیم یک نکته خیلی مهم در Binary Only Fuzzing معمولا سه مرحله خیلی ارزشمند داریم Input Discovery ↓ Program Analysis

Which process did this? Which thread did it? Which memory region was used? What was the call stack like? When did this happen? What happened before and after? This is exactly one of the important differences between Signature Detection and Behavioral Detection. Signature Detection is more about looking for a specific pattern. For example: This Hash This String This Pattern This Signature While Behavioral Detection is more about looking for the relationship between events. That is, this Process This Thread This Memory This Stack and this Behavior Do they create an unusual pattern when put together? This is also very important from the Red Team's perspective. Suppose you change a specific Event to be less visible. You might think, "Well, that's it." But no, because EDR may not just look at the Event itself. It may look at the relationship between several different layers. For example: Process + Memory + Thread + Call Stack + Network + Timing ↓ Behavior Pattern That is, even if a particular event is less visible The relationship between other events may still create a certain pattern So when you study EDR Evasion Don't just memorize the names of the techniques For example, you don't know what a particular technique is It is more important to understand Which Visibility Layer exactly does this technique affect For example, you should be able to ask yourself Where does EDR see this from From Process From Thread From Memory From API From Call Stack From Kernel From Network Or a combination of several In this way, you gradually move away from memorizing techniques And you start to understand what the security system really sees Now let's put the whole discussion of this post together API ↓ Call Stack ↓ Thread ↓ Memory ↓ Process ↓ Network ↓ Behavior The more Context we have of an Event, the more accurate the behavior analysis can be, so an API alone may not give us much information But when we say This API is used by this Thread Inside this Process From this Memory Region With this Call Stack At this Time And with this Network Activity Now we have a much more complete picture And this is exactly what makes Behavioral Detection Analysis so much more interesting than just looking at a simple Signature The Call Stack is not just a simple list of Functions But a clue to the path that Execution took to get to the current point So when a critical operation happens Don't just ask What was executed Ask a more important question Where did it get here And when you put this question aside from Process and Thread and Memory and Network You are starting to get closer to the view that modern EDRs use for behavior analysis @reverseengine

Call Stack Analysis EDR Call Stack Suppose we have a program main() ↓ Function_A() ↓ Function_B() ↓ Function_C()` That is, main has called Function_A Function_A also called Function_B Function_B also called Function_C Now when Function_C is executing We can have a picture of the call path that led the program to this point In very simple language We can ask Where did this Function get here from A simple picture of the Call Stack ┌─ Stack Of course, in a real system, the issue is a bit more complicated than this simple picture Because the Stack really doesn't just hold the names of the Functions, various information such as Stack Frame, Return Address, Local Data, and other things can be in the Stack For now, to get the concept right This simple picture is enough Why is the Call Stack important for EDR Suppose a sensitive API is executed The API itself alone does not always provide us with complete information about what path led to this point This is where the Call Stack can provide more Context For example: Sensitive Operation ↑ Function_B ↑ Function_A ↑ Application Now EDR can think about What path did this operation take? Does this path fit with the normal behavior of this program For example, suppose we have a Backup software Backup.exe ↓ BackupEngine.dll ↓ Memory Manager This path can be completely normal Because the Backup program is doing its job Now imagine another situation Do Unknown Memory ↓ Unusual Execution ↓ Sensitive Operation Here the story gets a bit more suspicious Because the path of the operation execution is different from what we expect But a very important point The unusualness of the Stack alone does not mean that an attack has occurred It may have a completely legitimate reason So a professional EDR will not come to issue a ruling just by seeing a strange Stack Stack Walking Now suppose a security tool wants to check the Stack of a Thread It can very simply check such a path Thread ↓ Stack ↓ Return Addresses ↓ Loaded Modules ↓ Memory Regions That is, the tool checks the addresses in the Thread execution path Then it asks Where does this Address belong to For example, we may see something like this 0x7FF... → known DLL 0x7FF... → known DLL 0x1A3... → private executable memory The first two may be completely normal But the last one may need more investigation Okay Why Because now we are seeing one of the execution path addresses to a private and executable Memory Region Again This alone is not proof of an attack It is just a Signal Here is an important point about Stack Stack is not an absolute truth That is, you cannot just look at the Stack and say Then something bad must have happened Real programs can have very complex Stacks For example: JIT Different Runtimes Debugger Instrumentation Exception Handling Thread Pool All of these can cause unusual paths in the Stack So professional Detection usually does not rely on just one Signal It may put several things together For example: Process Thread Memory Call Stack Network Timing And other information Now we come to the more interesting part Stack, Memory and Thread So far we have talked about these separately But in the real world, these are not separate An EDR can see this information together Process │ ┌─� ▼ ▼ Memory Thread │ │ │ ▼ │ Call Stack │ │ └──────┬────────┘ ▼ Behavior In other words, instead of just saying, "This API is dangerous," you can ask a much bigger question.

چه Process ی این کار رو انجام داد کدوم Thread انجامش داد از چه Memory Regionی استفاده شد Call Stack چه شکلی بود این اتفاق چه زمانی افتاد قبل و بعدش چه اتفاقاتی افتاد این دقیقا یکی از تفاوت‌های مهم بین Signature Detection و Behavioral Detection رو نشون میده Signature Detection بیشتر دنبال یه الگوی مشخص میگرده مثلا: این Hash این String این Pattern این Signature ولی Behavioral Detection بیشتر دنبال رابطه بین اتفاقاته یعنی این Process این Thread این Memory این Stack و این رفتار ایا وقتی کنار هم قرار میگیرن یه الگوی غیرعادی ایجاد میکنن از دید Red Team هم این موضوع خیلی مهمه فرض کنید یه Event خاص رو طوری تغییر بدید که کمتر قابل مشاهده باشه ممکنه فکر کنید خب دیگه تموم شد ولی نه چون EDR ممکنه فقط خود Event رو نگاه نکنه ممکنه رابطه بین چندتا لایه مختلف رو بررسی کنه مثلا: Process + Memory + Thread + Call Stack + Network + Timing ↓ Behavior Pattern یعنی حتی اگر یه Event خاص کمتر دیده بشه رابطه بین Eventهای دیگه هنوز ممکنه یه الگوی مشخص ایجاد کنه پس وقتی درباره EDR Evasion مطالعه میکنید فقط اسم تکنیک‌ها رو حفظ نکند مثلا ندونید فقط فلان تکنیک چیه مهم‌تر اینه که بفهمید این تکنیک دقیقا کدوم Visibility Layer رو تحت تأثیر قرار میده مثلا باید بتونید از خودتون بپرسید EDR این اتفاق رو از کجا میبینه از Process از Thread از Memory از API از Call Stack از Kernel از Network یا ترکیبی از چندتا اینجوری کم کم از حفظ کردن تکنیک‌ ها فاصله میگیری و شروع میکنید به فهمیدن اینکه سیستم امنیتی واقعا چه چیزی رو میبینه حالا کل بحث این پست رو کنار هم بذاریم API ↓ Call Stack ↓ Thread ↓ Memory ↓ Process ↓ Network ↓ Behavior` هرچی Context بیشتری از یه Event داشته باشیم تحلیل رفتار هم میتونه دقیق‌تر بشه پس یه API به تنهایی شاید اطلاعات زیادی بهمون نده ولی وقتی بگیم این API توسط این Thread داخل این Process از این Memory Region با این Call Stack در این زمان و همراه با این Network Activity اتفاق افتاده حالا تصویر خیلی کامل‌تری داریم و این دقیقا همون چیزیه که باعث میشه تحلیل Behavioral Detection خیلی جالب‌تر از فقط نگاه کردن به یه Signature ساده باشه Call Stack فقط یه لیست ساده از Function ها نیست بلکه یه سرنخ از مسیریه که Execution طی کرده تا به نقطه فعلی برسه پس وقتی یه عملیات حساس اتفاق میفته فقط نپرسید چی اجرا شد یه سوال مهم‌تر هم بپرسد از کجا به اینجا رسید و وقتی این سوال رو کنار Process و Thread و Memory و Network بذارید کم کم به دیدی نزدیک میشید که EDR های مدرن برای تحلیل رفتار استفاده میکنن @reverseengine

Call Stack Analysis EDR Call Stack فرض کنید یه برنامه داریم main() ↓ Function_A() ↓ Function_B() ↓ Function_C()` یعنی main اومده Function_A رو صدا زده Function_A هم Function_B رو صدا زده Function_B هم Function_C رو صدا زده حالا وقتی Function_C داره اجرا میشه میشه یه تصویر از مسیر فراخوانی‌هایی که باعث رسیدن برنامه به این نقطه شدن داشت به زبان خیلی ساده میتونیم پرسیم این Function از کجا به اینجا رسید یه تصویر ساده از Call Stack ┌──────────────────┐ │ Function_C │ ← الان اینجاییم ├──────────────────┤ │ Function_B │ ├──────────────────┤ │ Function_A │ ├──────────────────┤ │ main() │ └──────────────────┘ به این زنجیره میگیم Call Stack البته در سیستم واقعی موضوع کمی پیچیده‌ تر از این تصویر ساده ست چون Stack واقعا فقط اسم Function ها رو نگه نمیداره اطلاعات مختلفی مثل Stack Frame و Return Address و Local Data و موارد دیگه میتونن در Stack قرار داشته باشن فعلا برای اینکه مفهوم رو درست بگیریم همین تصویر ساده کافیه چرا Call Stack برای EDR مهمه فرض کنید یه API حساس اجرا شده خود API به تنهایی همیشه اطلاعات کاملی درباره اینکه چه مسیری باعث رسیدن به اینجا شده در اختیار ما نمیذاره اینجاست که Call Stack میتونه Context بیشتری بده مثلا: Sensitive Operation ↑ Function_B ↑ Function_A ↑ Application حالا EDR میتونه به این فکر کنه این عملیات از چه مسیری به وجود اومده ایا این مسیر با رفتار معمول این برنامه جور درمیاد مثلا فرض کنی یه نرم افزار Backup داریم Backup.exe ↓ BackupEngine.dll ↓ Memory Manager این مسیر کاملا میتونه طبیعی باشه چون برنامه Backup داره کار مربوط به خودش رو انجام میده حالا یه حالت دیگه رو تصور کنید Unknown Memory ↓ Unusual Execution ↓ Sensitive Operation اینجا داستان یه مقدار مشکوک‌ تر میشه چون مسیر اجرای عملیات با چیزی که انتظار داریم فرق داره ولی یه نکته خیلی مهم غیرعادی بودن Stack به تنهایی یعنی حمله اتفاق افتاده نیست ممکنه دلیل کاملا قانونی داشته باشه پس EDR حرفه‌ای فقط با دیدن یه Stack عجیب نمیاد حکم صادر کنه Stack Walking حالا فرض کنید یه ابزار امنیتی میخواد Stack یه Thread رو بررسی کنه به صورت خیلی ساده میتونه چنین مسیری رو بررسی کنه Thread ↓ Stack ↓ Return Addresses ↓ Loaded Modules ↓ Memory Regions یعنی ابزار میاد ادرس‌ هایی که توی مسیر اجرای Thread وجود دارن رو بررسی میکنه بعد میپرسه این Address متعلق به کجاست مثلا ممکنه چیزی شبیه این ببینیم 0x7FF... → known DLL 0x7FF... → known DLL 0x1A3... → private executable memory دو تای اول ممکنه کاملا عادی باشن ولی مورد آخر میتونه نیاز به بررسی بیشتری داشته باشه چرا چون حالا داریم میبینیم یکی از ادرس‌ های مسیر اجرا به یه Memory Region خصوصی و قابل اجرا مربوطه باز هم این به تنهایی اثبات حمله نیست فقط یه Signal محسوب میشه اینجا یه نکته مهم درباره Stack داریم Stack حقیقت مطلق نیست یعنی نمیشه فقط Stack رو دید و گفت پس حتما اتفاق بدی افتاده برنامه‌های واقعی میتونن Stack های خیلی پیچیده‌ای داشته باشن مثلا: JIT Runtimeهای مختلف Debugger Instrumentation Exception Handling Thread Pool همه اینا میتونن باعث ایجاد مسیرهای غیرمعمول در Stack بشن پس Detection حرفه‌ای معمولا فقط به یه Signal تکیه نمیکنه ممکنه چندتا چیز رو کنار هم بذاره مثلا: Process Thread Memory Call Stack Network Timing و اطلاعات دیگه حالا میرسیم به بخش جالب‌تر Stack و Memory و Thread تا اینجا درباره اینا جدا جدا صحبت کردیم ولی توی دنیای واقعی اینا از هم جدا نیستن یه EDR میتونه این اطلاعات رو کنار هم ببینه Process │ ┌──────┴──────┐ ▼ ▼ Memory Thread │ │ │ ▼ │ Call Stack │ │ └──────┬──────┘ ▼ Behavior یعنی به جای اینکه فقط بگه این API خطرناکه میتونه سوال خیلی بزرگتری بپرسه

For example, the Address Space of a Process is no longer used by that Process File Descriptors are also closed during the termination of the Process But this does not mean that the Resource itself is necessarily destroyed at that moment For example, a File may be open by another Process So the File Descriptor is not the same as the Resource itself We will see this issue much more later in Linux Internals An interesting point about return and exit For example, if we write:
int main() { return 0; }
When main ends, the program ends normally But if we write exit(0); Here, the program termination request is given directly That's why in simple programs, the result of both may seem almost the same But in terms of the program execution path, they are not exactly the same We also have another interesting difference exit is not the same as exit_ In C, the exit function can perform some User Space cleanups before the program ends For example, it can execute Handlers registered with atexit Or flush the stdio buffers But exit_ does not perform these User Space cleanups and directly terminates the Process This difference becomes very important when we get into the discussion of fork, buffering, and system calls Another point is that a Process does not end only with exit A Process may also terminate due to a Signal So the overall story could be like this Process │ ┌───────────────┐ │ │ exit() Signal │ │ └─� ▼ Termination │ ▼ Exit Information │ ▼ Zombie │ wait() │ ▼ Reaped Now let's move on to Reverse Engineering Suppose you are examining a Binary In the Trace you see something like this happen fork() ↓ exec() ↓ Program Execution ↓ exit() Now you can have a mental picture of what happened New Process created ↓ Child started executing ↓ New Program executed ↓ Program did its job ↓ Process terminated If you then see Parent waiting Child │ ▼ exit() │ ▼ Zombie │ ▼ Parent │ wait() │ ▼ Exit Status │ ▼ Reaping You can understand that Parent was probably waiting for the result of Child execution This is where the things we have learned so far are starting to come in handy for Reverse Engineering Now a picture Have more complete than what we have learned so far Parent Process │ fork() │ ▼ Child Process │ exec() │ ▼ Program │ ▼ Execution │ exit() │ ▼ Termination │ ▼ Zombie │ wait() │ ▼ Reaping exit Very simply, Process says I am done After that Process ↓ Termination ↓ Exit Status ↓ Parent gets the status with wait ↓ Reaping ↓ Zombie dies So far we have four very important concepts of Process API fork() ↓ Creating Child Process exec() ↓ Executing a new program inside a Process wait() ↓ Waiting or getting the status of Child exit() ↓ Completing the execution of Process Learn these four concepts well Because when you later enter Linux Internals and System Calls And Reverse Engineering ELF binaries, we will come across the same concepts again and again Just one thing to always remember We are following the Unix and Linux model here The internal details of Process, PCB and Process Descriptor may differ in different operating systems For now, we will make this model our main model @reverseengine

When a process terminates, the kernel cleans up resources that are no longer needed

exit What happens when a Process finishes its work So far we have been introduced to fork, exec, wait and even Zombie Process Now suppose we have a Process that has done its work and has nothing more to do For example, we have a very simple program:
int main() { printf("Hello"); return 0; }
When main finishes, the program must also tell the operating system that My work is finished This is where the concept of exit comes into play What exactly does exit do To put it simply, exit tells the operating system that this Process has nothing more to do and wants to finish its execution For example: exit(0); That number 0 usually means that the program has completed successfully That is exit(0) ↓ Successful execution But if we give a non-zero value, it usually means that another situation has occurred For example, an error Of course, the exact meaning of that number is determined by the program itself So the Kernel is not supposed to understand what exactly the number 1 means This number is more of an information that the Process gives to the Parent What happens after exit Here we have an important point When the Process reaches exit, it is no longer supposed to continue the normal program commands That means the program execution is finished But the Kernel still has to do a few things For example, the resources that the Process used must be managed Such as: Memory File Descriptor I O resources And information about the Process itself But a very important point Not everything is finished right away This is where the Zombie Process enters the story again Suppose we have this situation Parent │ └──── Child │ ▼ exit() │ ▼ Child execution is finished │ ▼ Zombie Child is no longer running CPU does not give it CPU Time to run the program But some information about its termination is still kept by the Kernel Why Because Parent needs to be able to understand how Child terminated For example, what was its Exit Status Then Parent comes and waits Child │ ▼ exit() │ ▼ Zombie │ ▼ Parent calls wait() │ ▼ Exit Status is received │ ▼ Reaping │ ▼ Zombie is destroyed We call this last step Reaping That is, Parent gets the termination status of Child and Kernel no longer needs the remaining information about that Child So remember one very important point Termination is not the same as Reaping Termination That is, Process is no longer running Reaping That is, Parent gets the termination status of Process and the remaining information about That Process is collected from Process Management Structures This difference is exactly why Zombie Process exists What is Exit Status When a Process terminates, it can have a value as its exit status For example
exit(0);
Exit Status = 0
or
exit(1);
That is
Exit Status = 1
In Unix and Linux, it is usually 0, meaning everything is fine And non-zero values ​​are usually used for other states Parent can get this value with wait or waitpid Now let's put the whole story together So far, we have learned some important parts of the Process cycle Parent │ fork() │ ▼ Child │ exec() │ ▼ New Program │ exit() │ ▼ Zombie │ wait() │ ▼ Reaped That is, first Parent creates a Child Then Child can execute a new Program with exec Program does its job Then exits Process is no longer running It may become Zombie for a short time Then Parent gets its state with wait And finally Child Reaps Now an important question Does exit mean that Process is deleted from everywhere at once? No This is one of those places where the operating system is a little more complicated than it seems at first glance When a process terminates it no longer runs but the kernel may retain a limited amount of information about it The most important reason is that the parent needs to be able to understand the termination status of the child so don't mix the two Termination ↓ Process no longer runs Reaping ↓ Parent gets the state of the process ↓ Remaining information is collected Another point about resources

مثلا Address Space مربوط به Process دیگه مورد استفاده اون Process نیست File Descriptor های باز هم در جریان پایان Process بسته میشن ولی این به معنی این نیست که خود Resource حتماً همون لحظه نابود میشه مثلا ممکنه یه File توسط Process دیگه هم باز باشه پس File Descriptor با خود Resource یکی نیست این موضوع رو بعدا توی Linux Internals خیلی بیشتر میبینیم یه نکته جالب درباره return و exit مثلا اگه بنویسیم:
int main() { return 0; }
وقتی main تموم بشه برنامه هم به شکل عادی به پایان میرسه اما اگه بنویسیم exit(0); اینجا مستقیما درخواست پایان برنامه داده شده برای همین توی برنامه های ساده ممکنه نتیجه هر دو تقریبا یکی به نظر برسه ولی از نظر مسیر اجرای برنامه دقیقا یکی نیستن یه تفاوت جالب دیگه هم داریم exit با exit_ یکی نیست توی C تابع exit قبل از پایان برنامه میتونه بعضی Cleanupهای مربوط به User Space رو انجام بده مثلا Handlerهایی که با atexit ثبت شدن رو اجرا کنه یا Bufferهای stdio رو Flush کنه ولی exit_ این Cleanup های User Space رو انجام نمیده و مستقیم تر Process رو terminate میکنه این تفاوت وقتی وارد بحث fork و Buffering و System Call بشیم خیلی مهم میشه یه نکته دیگه هم اینه که Process فقط با exit تموم نمیشه ممکنه Process در اثر Signal هم terminate بشه پس داستان کلی میتونه این شکلی باشه Process │ ┌─────────┴─────────┐ │ │ exit() Signal │ │ └─────────┬─────────┘ ▼ Termination │ ▼ Exit Information │ ▼ Zombie │ wait() │ ▼ Reaped حالا بریم سمت Reverse Engineering فرض کنید داری یه Binary رو بررسی میکنید توی Trace میبینید یه اتفاقاتی شبیه این افتاده fork() ↓ exec() ↓ Program Execution ↓ exit() حالا دیگه میتونید یه تصویر ذهنی از چیزی که اتفاق افتاده داشته باشید Process جدید ساخته شد ↓ Child شروع به اجرا کرد ↓ Program جدید اجرا شد ↓ Program کار خودش رو انجام داد ↓ Process تموم شد اگه بعدش Parent رو ببینی که wait میکنه Child │ ▼ exit() │ ▼ Zombie │ ▼ Parent │ wait() │ ▼ Exit Status │ ▼ Reaping میتونید بفهمید که Parent احتمالا منتظر نتیجه اجرای Child بوده اینجاست که چیزایی که تا الان یاد گرفتیم کم کم دارن به درد Reverse Engineering میخورن حالا یه تصویر کامل تر از چیزایی که تا اینجا یاد گرفتیم داشته باشید Parent Process │ fork() │ ▼ Child Process │ exec() │ ▼ Program │ ▼ Execution │ exit() │ ▼ Termination │ ▼ Zombie │ wait() │ ▼ Reaping exit خیلی ساده یعنی Process میگه کار من تموم شد بعد از اون Process ↓ Termination ↓ Exit Status ↓ Parent با wait وضعیت رو میگیره ↓ Reaping ↓ Zombie از بین میره پس تا اینجا چهار مفهوم خیلی مهم Process API رو داریم fork() ↓ ساخت Child Process exec() ↓ اجرای Program جدید داخل Process wait() ↓ منتظر موندن یا گرفتن وضعیت Child exit() ↓ تموم کردن اجرای Process این چهار مفهوم رو خوب یاد بگیرید چون وقتی بعدا وارد Linux Internals و System Call ها و Reverse Engineering باینری های ELF بشیم دوباره بارها به همین مفاهیم برمیخوریم فقط یه نکته رو همیشه یادتون باشه ما اینجا داریم مدل Unix و Linux رو دنبال میکنیم جزئیات داخلی Process و PCB و Process Descriptor ممکنه توی سیستم عامل های مختلف فرق داشته باشهن فعلا همین مدل رو مدل اصلیمون قرار میدیم @reverseengine

exit وقتی یک Process کارش تموم میشه چه اتفاقی میوفته تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره مثلا یه برنامه خیلی ساده داریم:
int main() { printf("Hello"); return 0; }
وقتی main تموم میشه برنامه هم باید به سیستم عامل بفهمونه که کار من تموم شده اینجاست که مفهوم exit وارد داستان میشه exit دقیقا چیکار میکنه خیلی ساده بخوایم بگیم exit به سیستم عامل میگه این Process دیگه کاری نداره و میخواد اجرای خودش رو تموم کنه مثلا:
exit(0);
اون عدد 0 معمولا یعنی برنامه با موفقیت تموم شده یعنی exit(0) ↓ اجرای موفق ولی اگه مقدار غیر صفر بدیم معمولا یعنی یه وضعیت دیگه اتفاق افتاده مثلا یه خطا البته معنی دقیق اون عدد رو خود برنامه مشخص میکنه پس قرار نیست Kernel بفهمه عدد 1 دقیقا یعنی چه خطایی این عدد بیشتر یه اطلاعاتیه که Process به Parent میده بعد از exit چی میشه اینجا یه نکته مهم داریم وقتی Process به exit میرسه دیگه قرار نیست دستورهای معمول برنامه رو ادامه بده یعنی اجرای برنامه تموم شده ولی Kernel هنوز باید چندتا کار انجام بده مثلا منابعی که Process استفاده میکرد باید مدیریت بشن مثل:
Memory File Descriptor
منابع مربوط به I O و اطلاعات مربوط به خود Process ولی یه نکته خیلی مهم همه چیز همون لحظه کامل تموم نمیشه این همون جاییه که Zombie Process دوباره وارد داستان میشه فرض کنید این وضعیت رو داریم Parent │ └──── Child │ ▼ exit() │ ▼ اجرای Child تموم شد │ ▼ Zombie Child دیگه اجرا نمیشه CPU هم دیگه بهش برای اجرای برنامه CPU Time نمیده ولی یه مقدار اطلاعات مربوط به پایانش هنوز توسط Kernel نگه داشته میشه چرا چون Parent باید بتونه بفهمه Child چطوری تموم شده مثلا Exit Status اون چی بوده بعد Parent میاد و wait میکنه Child │ ▼ exit() │ ▼ Zombie │ ▼ Parent calls wait() │ ▼ Exit Status دریافت میشه │ ▼ Reaping │ ▼ Zombie از بین میره به این مرحله آخر میگیم Reaping یعنی Parent وضعیت پایان Child رو میگیره و Kernel دیگه اطلاعات باقی مونده مربوط به اون Child رو لازم نداره پس یه نکته خیلی مهم رو یادتون باشه Termination با Reaping یکی نیست Termination یعنی Process دیگه اجرا نمیشه Reaping یعنی Parent وضعیت پایان Process رو دریافت میکنه و اطلاعات باقی مونده مربوط به اون Process از ساختارهای مدیریت Process جمع میشه این تفاوت دقیقاً دلیل وجود Zombie Process هست Exit Status چیه وقتی یه Process تموم میشه میتونه یه مقدار به عنوان وضعیت پایان خودش داشته باشه مثلا
exit(0);
یعنی
Exit Status = 0
یا
exit(1);
یعنی
Exit Status = 1
توی Unix و Linux معمولا 0 یعنی همه چیز خوب تموم شده و مقدارهای غیر صفر معمولا برای وضعیت‌های دیگه استفاده میشن Parent میتونه این مقدار رو با wait یا waitpid دریافت کنه حالا کل داستان رو کنار هم بذاریم تا الان چندتا تکه مهم از چرخه Process رو یاد گرفتیم Parent │ fork() │ ▼ Child │ exec() │ ▼ New Program │ exit() │ ▼ Zombie │ wait() │ ▼ Reaped یعنی اول Parent یه Child میسازه بعد Child میتونه با exec یه Program جدید رو اجرا کنه Program کارش رو انجام میده بعد exit میکنه Process دیگه اجرا نمیشه ممکنه برای مدت کوتاهی Zombie بشه بعد Parent با wait وضعیتش رو میگیره و در نهایت Child Reap میشه حالا یه سوال مهم آیا exit یعنی Process همون لحظه از همه جا پاک میشه؟ نه اینجا یکی از اون جاهاییه که سیستم عامل یه کم از چیزی که در نگاه اول به نظر میاد پیچیده تره وقتی Process terminate میشه دیگه اجرا نمیشه ولی Kernel ممکنه یه مقدار اطلاعات محدود از اون رو نگه داره مهمترین دلیلش هم اینه که Parent باید بتونه وضعیت پایان Child رو بفهمه پس این دوتا رو قاطی نکنید Termination ↓ Process دیگه اجرا نمیشه Reaping ↓ Parent وضعیت Process رو میگیره ↓ اطلاعات باقی مونده جمع میشه یه نکته دیگه درباره منابع وقتی Process تموم میشه Kernel منابعی که دیگه لازم نیستن رو Cleanup میکنه

📌 New article released ⚔️ The Art of DLL Hijacking In this article, I have taken an in-depth look at the mechanisms of DLL Hijacking in Windows; from the basics of how DLLs are loaded and DLL Search Order Hijacking to more advanced techniques such as: ◾️ DLL Search Order Hijacking ◾️ DLL Substitution (Replacement) ◾️ DLL Side-Loading ◾️ DLL Proxying ◾️ Phantom DLL Hijacking In this article, we will examine how the Windows Loader resolves and loads DLLs, how suitable conditions for DLL Hijacking are identified, and why some legitimate programs can become part of the loading chain. A practical approach to analyzing and identifying Phantom DLLs is also presented. Read the article: https://darkbitx.github.io/posts/the-art-of-dll-hijacking ✍️ Author: Darkbit | DarkBit 💬 Forum 📣 DarkBit #CyberSecurity #WindowsInternals #Persistence #Maldev #RedTeam

Repost from DarkBit
📌 مقاله جدید منتشر شد ⚔️ The Art of DLL Hijacking در این مقاله به بررسی عمیق مکانیزم‌های DLL Hijacking در ویندوز پرداخته‌ام؛ از مبانی نحوه بارگذاری DLLها و DLL Search Order Hijacking تا تکنیک‌های پیشرفته‌تر مانند: ◾️ DLL Search Order Hijacking ◾️ DLL Substitution (Replacement) ◾️ DLL Side-Loading ◾️ DLL Proxying ◾️ Phantom DLL Hijacking در این مقاله بررسی می‌کنیم که Windows Loader چگونه DLLها را resolve و بارگذاری می‌کند، چگونه شرایط مناسب برای DLL Hijacking شناسایی می‌شوند، و چرا برخی برنامه‌های معتبر می‌توانند به بخشی از زنجیره بارگذاری تبدیل شوند. همچنین یک رویکرد عملی برای تحلیل و شناسایی Phantom DLLها ارائه شده است. مطالعه مقاله: https://darkbitx.github.io/posts/the-art-of-dll-hijacking/ ✍️ نویسنده: دارک‌بیت | DarkBit 💬 Forum 📣 DarkBit #CyberSecurity #WindowsInternals #Persistence #Maldev #RedTeam

Static Devirtualization of Tencent VM https://aftermathlabs.net/blog/31/07/2026 @reverseengine