From Chrome renderer code exec to kernel with MSG_OOB
https://projectzero.google/2025/08/from-chrome-renderer-code-exec-to-kernel.html
@reverseengine
https://projectzero.google/2025/08/from-chrome-renderer-code-exec-to-kernel.html
@reverseengine
projectzero.google
From Chrome renderer code exec to kernel with MSG_OOB
IntroductionIn early June, I was reviewing a new Linux kernel feature when I learned about the MS...
State divergence enables unauthorized access
https://blog.trailofbits.com/2026/08/25/state-divergence-enables-unauthorized-access
@reverseengine
https://blog.trailofbits.com/2026/08/25/state-divergence-enables-unauthorized-access
@reverseengine
The Trail of Bits Blog
State divergence enables unauthorized access
We found and reported a bug in Provenance Blockchain, a public proof-of-stake chain built on Cosmos SDK, that lets any user grant themselves admin control over marker accounts without holding a single token.
From P-Code to GNN: extract binary code semantics
https://blog.quarkslab.com/from-p-code-to-gnn-extract-binary-code-semantics.html
@reverseengine
https://blog.quarkslab.com/from-p-code-to-gnn-extract-binary-code-semantics.html
@reverseengine
Quarkslab
From P-Code to GNN: extract binary code semantics - Quarkslab's blog
pcode_graph is a Python library, published by Quarkslab, suitable to build semantic graphs from binary code. We present how to use it to detect function similarities in binaries.
Kernel Shield: Reversing the NSecKrnl (Malops.io) Rootkit Driver
https://medium.com/@sushant.m.mane/kernel-shield-reversing-the-nseckrnl-malops-io-rootkit-driver-fb4af6bcb19c
https://medium.com/@sushant.m.mane/kernel-shield-reversing-the-nseckrnl-malops-io-rootkit-driver-fb4af6bcb19c
Medium
Kernel Shield: Reversing the NSecKrnl (Malops.io) Rootkit Driver
Challenge URL: https://malops.io/challenges/kernel-shield
Reverse Engineering a 0day used Against EDRs
https://medium.com/@jehadbudagga/reverse-engineering-a-0day-used-against-crowdstrike-edr-a5ea1fbe3fd4
https://medium.com/@jehadbudagga/reverse-engineering-a-0day-used-against-crowdstrike-edr-a5ea1fbe3fd4
Medium
Reverse Engineering a 0day used Against EDRs
Hello again…
😁3
Forwarded 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
⚔️ 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
DarkBit
The Art of DLL Hijacking
An in-depth exploration of DLL Hijacking and related techniques, covering how DLL loading works, how vulnerable applications can be identified, and the differences between classic DLL Hijacking, DLL Sideloading, DLL Proxying, and Phantom DLL Hijacking.
DarkBit
📌 مقاله جدید منتشر شد ⚔️ The Art of DLL Hijacking در این مقاله به بررسی عمیق مکانیزمهای DLL Hijacking در ویندوز پرداختهام؛ از مبانی نحوه بارگذاری DLLها و DLL Search Order Hijacking تا تکنیکهای پیشرفتهتر مانند: ◾️ DLL Search Order Hijacking ◾️ DLL…
📌 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
⚔️ 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
DarkBit
The Art of DLL Hijacking
An in-depth exploration of DLL Hijacking and related techniques, covering how DLL loading works, how vulnerable applications can be identified, and the differences between classic DLL Hijacking, DLL Sideloading, DLL Proxying, and Phantom DLL Hijacking.
❤3
exit
وقتی یک Process کارش تموم میشه چه اتفاقی میوفته
تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم
حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره
مثلا یه برنامه خیلی ساده داریم:
وقتی main تموم میشه برنامه هم باید به سیستم عامل بفهمونه که
کار من تموم شده
اینجاست که مفهوم exit وارد داستان میشه
exit دقیقا چیکار میکنه
خیلی ساده بخوایم بگیم
exit
به سیستم عامل میگه این Process دیگه کاری نداره و میخواد اجرای خودش رو تموم کنه
مثلا:
اون عدد 0 معمولا یعنی برنامه با موفقیت تموم شده
یعنی
exit(0)
↓
اجرای موفق
ولی اگه مقدار غیر صفر بدیم معمولا یعنی یه وضعیت دیگه اتفاق افتاده
مثلا یه خطا
البته معنی دقیق اون عدد رو خود برنامه مشخص میکنه
پس قرار نیست Kernel بفهمه عدد 1 دقیقا یعنی چه خطایی
این عدد بیشتر یه اطلاعاتیه که Process به Parent میده
بعد از exit چی میشه
اینجا یه نکته مهم داریم
وقتی Process به exit میرسه دیگه قرار نیست دستورهای معمول برنامه رو ادامه بده
یعنی اجرای برنامه تموم شده
ولی Kernel هنوز باید چندتا کار انجام بده
مثلا منابعی که Process استفاده میکرد باید مدیریت بشن
مثل:
منابع مربوط به 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 تموم میشه میتونه یه مقدار به عنوان وضعیت پایان خودش داشته باشه
مثلا
یعنی
یا
یعنی
توی 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 میکنه
وقتی یک 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 میکنه
مثلا Address Space مربوط به Process دیگه مورد استفاده اون Process نیست
File Descriptor
های باز هم در جریان پایان Process بسته میشن
ولی این به معنی این نیست که خود Resource حتماً همون لحظه نابود میشه
مثلا ممکنه یه File توسط Process دیگه هم باز باشه
پس File Descriptor با خود Resource یکی نیست
این موضوع رو بعدا توی Linux Internals خیلی بیشتر میبینیم
یه نکته جالب درباره return و exit
مثلا اگه بنویسیم:
وقتی 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
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
ReverseEngineering
exit وقتی یک Process کارش تموم میشه چه اتفاقی میوفته تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره مثلا یه برنامه خیلی ساده داریم: int main() { …
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:
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
or
That is
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
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
ReverseEngineering
exit وقتی یک Process کارش تموم میشه چه اتفاقی میوفته تا اینجا با fork و exec و wait و حتی Zombie Process آشنا شدیم حالا فرض کنید یه Process داریم که کارش رو انجام داده و دیگه کاری برای انجام دادن نداره مثلا یه برنامه خیلی ساده داریم: int main() { …
When a process terminates, the kernel cleans up resources that are no longer needed
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:
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
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
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 خطرناکه
میتونه سوال خیلی بزرگتری بپرسه
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 خطرناکه
میتونه سوال خیلی بزرگتری بپرسه
چه 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
کدوم 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
ReverseEngineering
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…
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.
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.
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
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
بخش بیست و نهم بافر اورفلو
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
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
یعنی اول بفهمید
ورودی از کجا میاد
بعد بفهمید
چطور داخل برنامه پردازش میشه
بعد 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