اگه شما به حوزه ی کشف آسیب پذیری و توسعه ی اکسپلویت علاقمند باشید یا حملات خاص سایبری رو رصد کنید، احتمالا مشاهده کردید که اغلب آموزش ها و نمونه های واقعی، روی آسیب پذیری های خرابی حافظه (Memory Corruption) تمرکز دارن.
بصورت کلی خرابی حافظه، نوعی آسیب پذیری امنیتی هستش که در اون، حافظه به شکل غیرمنتظره تغییر پیدا میکنه که این تغییر منجر به اجرای کد دلخواه، لیک شدن حافظه و … میشه.
آسیب پذیری هایی مانند Buffer Overflow، Stack Overflow، Heap Overflow، Use-After-Free و Integer Overflow جزء خرابی حافظه محسوب میشن.
در این پست بررسی کردیم که چرا توسعه دهندگان اکسپلویت، بیشتر ترجیح میدن برن سمت آسیب پذیری های خرابی حافظه و این نوع آسیب پذیری هارو با آسیب پذیری های منطقی و ایمنی حافظه، مقایسه کردیم. این بررسی رو از دید sha1lan انجام دادیم.
اگه به این موضوع و حوزه علاقمند هستید، این مقالات هم میتونه براتون جالب باشه:
- رودمپ یادگیری رایگان Binary Exploitation از دید DayZeroSec (نسخه 2024)
- اهمیت یادگیری زبان C برای برنامه نویسان
چرا اغلب اکسپلویتهایی که در دنیای واقعی، کاربران نهایی رو هدف قرار میدن، از خرابی حافظه استفاده میکنن؟
تقریبا تا سال 2021 (آخرین سالی که دسترسی به آمار وجود داره (1)(2)(3)(4) )، آسیب پذیری های خرابی حافظه، حدود 60 تا 70 درصد آسیب پذیری ها در محیط واقعی رو پوشش میدادن. البته، در اکسپلویتهایی که شناسایی شدن، سوگیری نمونهبرداری وجود داره و همچنین بیشتر نرمافزارهای امروزی، هنوز با زبانهای ناامن حافظه نوشته میشن، بنابراین طبیعیه که بیشتر باگها به دلیل ناامنی حافظه باشن.
با این حال، نویسنده معتقده که تکنیکهای خرابی حافظه حتی پس از اینکه انتقال به زبانهای امن حافظه، اشکالات ناامنی حافظه رو نسبت به اشکالات منطقی نادرتر میکنه، اما همچنان روی اکسپلویتهای دنیای واقعی که هدف اونا پلتفرمها و محصولات کاربران نهایی هستش، تسلط خواهند داشت. نویسنده انتظار نداره که MTE این رو تغییر بده: این فقط باعث میشه که باگهای خوب خرابی حافظه حتی نادرتر و اکسپلویت اونا سختتر بشه.
دلیل اینکه نویسنده معتقده خرابی حافظه همچنان محبوب خواهد بود اینه که، خرابی حافظه تقریباً همیشه سادهترین راه برای مهاجم هستش تا سیستم هدف رو مجبور کنه، کاری که میخواد رو انجام بده، بخصوص هنگام اکسپلویت یک دستگاه کاربر نهایی. زنجیرههای اکسپلویت مدرن معمولی شامل چندین مرحله هستن که زمینههای اجرای مجزایی رو طی میکنن، و مهاجم اغلب باید در هر مرحله چیزی در حد اجرای کد دلخواه رو بدست بیاره تا بتونه آسیبپذیری مرحله بعدی رو اکسپلویت کنه. خرابی حافظه معمولاً سادهترین راه برای دستیابی به چنین چیزی هستش.
برخی از باگهای منطقی بسیار خوب مثلاً ()exec ، میتونن کد یا یک اسکریپت ارائه شده توسط مهاجم رو تحت یک مفسر به اندازه کافی قدرتمند، اجرا کنن تا باگ مرحله ی بعدی در زنجیره رو اکسپلویت کنن. سایر باگهای منطقی اگه با یک فرم خارجی ناامن، مانند JIT، جفت بشن میتونن اثرات خرابی حافظه رو تولید کنن. با این حال، بسیار رایجه که اکسپلویت یک باگ منطقی در یک برنامه آسیبپذیر همچنان توسط محدودیتهای اعمال شده در سورس کد برنامه و زبان برنامهنویسی محدود بشه. این دنیایی کاملاً متفاوت از اونچه که خرابی حافظه ارائه میده هستش، جایی که بخش زیادی از باگها، میتونن باعث بشن برنامه آسیبپذیر محدودیتهای برنامه و زبان رو دور بزنه و فقط توسط محدودیتهایی که در CPU اعمال شده، محدود بشن.
خرابی حافظه در مقابل ناامنی حافظه:
نویسنده بین خرابی حافظه و ناامنی حافظه (Memory Unsafety) تمایز قائله. آسیب پذیری های ناامنی حافظه منجر به تخریب حافظه میشن. اما تخریب حافظه یک اثر هستش نه یک علت اصلی. بعضی از باگهای منطقی هم وقتی با ناامنی حافظه ترکیب میشن، مانند JIT، جداول صفحه و یا سخت افزار، منجر به خرابی حافظه میشن.
نمونه ای از این تمایز رو میشه در باگ WebAssembly type confusion آقای Manfred Paul در مسابقات Pwn2Own 2024 مشاهده کرد. در اصل مشکل یک باگ منطقی هستش: کد سعی میکنه تعداد انواع در یک ماژول WebAssembly رو محدود کنه، چون انواع در ایندکسهای بالا به انواع داخلی (built-in) و خاص اشاره میکنن. عدم محدود کردن تعداد انواع تعریف شده توسط کاربر به اونا اجازه میده تا انواع داخلی رو جایگزین کنن. Type Confusion نتیجه ی حذف بررسی نوع در کد تولید شده هستش. این مورد ممکنه در Rust هم اتفاق بیافته.
نویسنده بطور خاص استدلال میکنه که خرابی حافظه، نه باگهای ناامنی حافظه، همچنان پایه ی اصلی اکسپلویت خواهند بود. با حرکت جهان به سمت زبانهای ایمن، توزیع آسیبپذیریها تغییر خواهد کرد، بنابراین مهاجمان ممکنه به روشهای دیگه ای برای باز کردن قفل خرابی حافظه روی بیارن. به عنوان مثال، در کرنل، ما شاهد تغییر کوچکی از use-after-free به سمت باگهای منطقی در مدیریت جدول صفحه هستیم.
Weird machines و مولفه ی بهم متصل قوی:
در Limiting weird machines، آقای Thomas Dullien (Halvar Flake) بینش بسیار مفیدی در مورد اینکه چرا خرابی حافظه چنین اهمیتی داره رو ایجاد میکنه. ما میتونیم یک برنامه رو به عنوان یک ماشین Intended Finite State Machine (IFSM) مدل سازی کنیم که برای شبیهسازی ماشین حالت، روی یک CPU قابل اجرای پیچیده با n بیت حافظه، کامپایل شده.
منظور از IFSM، عبارتی هستش که برای توصیف یک سیستم ساده استفاده میشه. این سیستم در هر لحظه میتونه در یکی از حالتهای محدود قرار بگیره و با دریافت ورودی، از یک حالت به حالت دیگه تغییر کنه. مثلاً یک چراغ راهنمایی رو در نظر بگیرید. این چراغ فقط سه حالت داره: قرمز، زرد و سبز. با گذشت زمان یا با دریافت سیگنال، از یک حالت به حالت دیگه تغییر میکنه.
فضای حالت رو میشه به عنوان 2^n گره دید، که هر کدوم یک بیت-بردار ممکن، برای حافظه سیستم رو نشون میدن. هر دستور CPU لبهای رو از هر گره به حالت جدید سیستم پس از اجرای اون دستور معرفی میکنه. اجرای یک برنامه بعدش یک مسیر رو در فضای حالت 2^n دنبال میکنه.
برای ساده تر شدن متن بالا:
- فضای حالت: کل حالات ممکن یک سیستم رو “فضای حالت” میگن. به عبارت سادهتر، تمام حالتهایی که یک سیستم میتونه در اون قرار بگیره.
- 2^n گره: این بخش به تعداد حالتهای ممکن اشاره داره. “n” تعداد بیتهای حافظه سیستم هستش. هر بیت میتونه دو مقدار داشته باشه: 0 یا 1. بنابراین، با n بیت، 2 به توان n حالت مختلف ممکن هستش. هر یک از این حالتها رو یک “گره” در نظر میگیرن.
- بیت-بردار: یک بیت-بردار مجموعهای از صفرها و یکهاست که حالت حافظه سیستم رو در یک لحظه مشخص نشون میده. هر بیت در این بردار به یک بیت در حافظه سیستم اشاره داره.
- لبه: اگه دو گره داشته باشیم، این گره ها بهم وصل بشن، اون خط اتصالی رو لبه میگیم.
مثال: فرض کنید سیستم ما فقط 3 بیت حافظه داره. در این صورت، فضای حالت ما 2^3 = 8 حالت مختلف خواهد داشت. هر حالت رو میتونیم با یک بیت-بردار 3 تایی نشون داد. مثلاً:
- 000
- 001
- 010
- 011
- 100
- 101
- 110
- 111
هر یک از این بیت-بردارها یک حالت ممکن برای حافظه سیستم رو نشون میده.
پس بصورت کلی، میشه یک برنامه رو به عنوان یک ماشین IFSM تصور کرد. این ماشین حالتها و انتقال بین اونارو تعریف میکنه. بعدش، این ماشین حالت روی یک پردازنده واقعی اجرا میشه. هر حالت ممکن، برای برنامه یک گره در فضای حالت نامیده میشه و هر دستور CPU باعث تغییر از یک گره به گره دیگه میشه. بنابراین، اجرای یک برنامه در واقع یک مسیر در فضای حالت هستش.
در حالیکه کل مجموعه بسیار شگفتانگیز هستش، بینش اصلی Halvar که در بخش بعدی ازش استفاده خواهیم کرد، به شرح زیر:
هنگامیکه یک آسیبپذیری در برنامه فعال میشه، دستور CPU سیستم رو به یک “حالت عجیب” انتقال میده که حالت مشابهی در IFSM نداره، و از اونجایی که کد برنامه همچنان اجرا میشه و سعی میکنه IFSM رو ، روی CPU شبیهسازی کنه، ما در واقع شروع به حرکت در “لبههای تصادفی” در فضای حالت کامل میکنیم. اگه لبههای تصادفی کافی در این گراف وجود داشته باشن، یک مؤلفه بزرگ متصل به هم قوی پدید میاد. و از اونجاییکه بیشتر اون 2^n حالت از طریق این لبههای تصادفی در این مؤلفه متصل به هم قوی گنجونده شدن، این بدین معناست که مسیری از اکثر حالتهای عجیب به حالت های عجیب دیگه وجود داره، از جمله برخی حالتهایی که نشون دهنده دستیابی مهاجم به هدفش هستش. به زبان ساده، مهاجم میتونه از باگ برای دستیابی به هر هدفی که میخواد استفاده کنه.
به عنوان مثال، یک حمله Zero-Click روی یک موبایل رو تصور کنید که در اون، مهاجم میخواد پیامهای متنی مخربی ارسال کنه که منجر به آپلود تمام عکسهای کاربر به سرور مهاجم میشه. مهاجم میتونه یک پیام متنی ارسال کنه که یک Out-Of-Bounds Heap Write رو فعال کنه، و در نتیجه برنامه پیامرسان وارد یک حالت عجیب میشه. با ادامه اجرای کد برنامه، تمام انتقالهای بعدی در فضای حالت عجیب رخ میدن، که در واقع حرکت در لبههای تصادفی برای تغییر حافظه برنامه هستش. مشاهده Halvar اینه که فضای حالت عجیب احتمالاً به شدت به هم متصل هستن، بنابراین احتمالاً مسیری از اون حالت خرابی حافظه به حالتی که دستگاه در حال آپلود عکسها هستش، وجود داره. این حالت نهایی در واقع روی CPU وجود داره: برای برخی از حالتهای n بیتی با دقت انتخاب شده حافظه، CPU یک زنجیره ROP رو اجرا میکنه تا دقیقاً همین کار رو انجام بده.
به همین دلیله که خرابی حافظه بسیار قدرتمند هستش: وقتی به مؤلفه متصل به هم قوی میرسید، تقریباً هر حالت حافظه n بیتی قابل دسترس میشه. و برای یک IFSM به اندازه کافی پیچیده، CPU توسط دادههای موجود در حافظه قابل برنامه نویسی هستش، که به این معنیه که برنامه آسیبپذیر میتونه اساساً هر کاری رو انجام بده که CPU اجازه میده.
باگهای منطقی ایمنی حافظه متفاوت هستن:
این آنالیز برای باگهای منطقی ایمنی حافظه جواب نمیده. نویسنده اینجا وارد ریاضیات نمیشه و تنها از تجربیات برای استدلال استفاده میکنه. استدلال Halvar در بالا یک فرض ضمنی داره که حالت عجیب ناشی از (یا منجر به) خرابی حافظه هستش و CPU در توانایی خود برای دستکاری فضای حالت (یعنی حافظه) کاملاً بدون محدودیت هستش، مانند یک CPU فیزیکی که اسمبلی arm64 رو اجرا میکنه.
برای مشاهده اینکه استدلال برای باگهای منطقی ایمنی حافظه شکست میخوره، تلاش کنید به حالتی برسید که تمام حافظه با 0x41414141 پر شده باشه. این حالت قطعاً در مؤلفه متصل به هم قوی، برای یک باگ خرابی حافظه وجود داره: فقط برنامه رو وادار کنید تا به memset با آرگومانهای مناسب بپره. اما اکنون تصور کنید که یک باگ منطقی در یک برنامه امن حافظه رو اکسپلویت میکنید. داشتن تمام حافظه تنظیم شده روی 0x41 با هیچ حالت معتبر و امن حافظه برای چنین برنامهای، باگ یا بدون باگ، مطابقت نداره. این بدین معنی که، حالتی که تمام حافظه در اون روی 0x41 تنظیم شده، در مؤلفه متصل به هم قوی برای یک باگ منطقی ایمنی حافظه وجود نداره.
نویسنده گمان میکنه، دلیلی که استدلال مؤلفه متصل به هم قوی تحت ایمنی حافظه شکست میخوره، شامل فرض تصادف برای لبهها در فضای حالت عجیب هستش. این فرض احتمالاً برای باگهای خرابی حافظه به اندازه کافی خوب عمل میکنه، اما تحت محدودیتهای ایمنی حافظه نه. یعنی انتقالهای انجام شده توسط شبیهسازی IFSM روی یک CPU فیزیکی، پس از ورود به یک حالت عجیب، مشروط به این محدودیت که برنامه از نظر حافظه ایمن باقی میمونه در فضای کامل حافظه به اندازه کافی “تصادفی” نیستن تا مولفه ی قوی متصل، ظاهر بشه.
این تجربه شهودی نویسندگان اکسپلویت رو پشتیبانی میکنه که جستجوی خرابی حافظه سادهترین راه برای وادار کردن یک برنامه به انجام کارهایی هستش که بسیار خارج از عملکرد طبیعی اونه، در حالیکه باگهای منطقی تمایل دارن قابلیتهای سفارشی و محدودی رو ارائه بدن.
برای کار کردن استدلال ماشین عجیب بالا برای باگهای منطقی ایمنی حافظه، چگونه باید اونارو تغییر بدیم؟ بطور کلی باید یک CPU و فضای حالتی رو انتخاب کنید که ویژگیهای حفظ شده توسط مجموعه باگهای مورد نظر رو رعایت کنن. برای یک برنامه ایمن حافظه پایتونی، این ممکنه به معنای انتخاب فضای حالت برای انتساب گراف های شی پایتون به متغیرهای برنامه و “CPU” به عنوان یک ماشین انتزاعی برای زیرمجموعه امن زبان پایتون باشه. بعبارت دیگه، انتقال الزام ایمنی حافظه از لبهها به مجموعه دستورالعملها، ممکنه به لبهها کمک کنه تا دوباره “به اندازه کافی تصادفی” به نظر برسن تا در مورد نوعی مؤلفه متصل به هم قوی برای باگهای منطقی، بشه صحبت کرد.
توسعه دهندگان اکسپلویت هم توسعه دهنده ی نرم افزار هستن:
همه اینا در عمل به چه معناست؟
بطور خلاصه، خرابی حافظه خاص هستش و امید نویسند اینه که تونسته باشه کمی شهود، برای افرادی که تجربه اکسپلویت باینری رو ندارن انتقال داده باشه. مهاجمان وقتی نیاز دارن یک برنامه آسیبپذیر رو وادار کنن تا کاری بطور دلخواه انجام بده، مانند انتقال به مرحله بعدی یک زنجیره اکسپلویت، خرابی حافظه رو ترجیح میدن. باگهای منطقی جایگزین قابل استفاده در اینجا نیستن.
از دیدگاه عملی، توسعه دهندگان اکسپلویت، توسعهدهندگان نرمافزار هستن که به دنبال اصول طراحی در اکسپلویتها هستن که توسعهدهندگان نرمافزار در برنامههای معمولی میخوان. هنگامی که یک برنامه رو اکسپلویت میکنید تا یک weird machine ایجاد کنید و بعدش که اونو برنامه نویسی میکنید، شروع به ساخت انتزاعات میکنید. شما شروع به ساخت APIهایی مانند خواندن/نوشتن دلخواه میکنید که به شما اجازه میده جزئیات و محدودیتهای باگ اکسپلویت شده رو حذف کنید. این انتزاع به نوبه خود امکان ماژولار بودن رو فراهم میکنه، بنابراین میتونید یک اکسپلویت رو با یکی دیگه جایگزین کنید. ناگهان، فراتر از انتزاع خوندن/نوشتن، تمام باگهای خرابی حافظه کم و بیش یکسان به نظر میرسن. بیشتر باگهای منطقی اینجوری کار نمیکنن.
بنابراین، نویسنده معتقده که مهاجمان همچنان روی باگهای خرابی حافظه تمرکز میکنن، حتی اگه استفاده از اونا نادرتر و سختتر بشه، چون اونا چیزی رو ارائه میدن که یافتن اونا در جاهای دیگه بسیار سخت هستش.