اگه در زمینه مهندسی معکوس کار کرده باشید، قطعا ابزار معروف dnSpy رو شنیدید و استفاده کردید. این ابزار یه دیباگر و ادیتور اسمبلی های دات نتی هست . بطور کلی میتونید یه باینری دات نتی رو بهش بدید و سورس کدهای اونو مشاهده و ویرایش کنید.
با این تفاسیر، این ابزار نقش مهمی رو در تحلیل بدافزارهای دات نتی میتونه داشته باشه. حالا فرض کنید که یه بدافزاری دات نتی رو دارید تحلیل میکنید که این بدافزار، با سوء استفاده از dnSpy ، سیستم شما رو هم آلوده میکنه.
محققا با استفاده از تکنیک DLL Hijacking تونستن کد دلخواه در این ابزار اجرا کنن که در این پست به بررسی اون پرداختیم. البته اگه گزارش رو کامل بخونید، احتمالا میتونید از این تکنیک در برنامه های دات نتی دیگه هم استفاده کنید.
آسیب پذیری نسخه های 6.1.8 تا 6.4.0 رو تحت تاثیر میزاره و نسخه های 6.4.1 به بالا ، نسخه های اصلاح شده هستن.
نکته: قبل از اینکه ادامه مقاله رو بخونید، حتما نسخه اصلاح شده رو دانلود و نصب کنید. (هکرهای کره شمالی از آنچه فکر میکنید به شما نزدیکترند)
برنامه های Self-Contained :
برنامه نویسی با استفاده از زبانهای دات نتی ، مانند سی شارپ، یه مشکلی که داره اینکه هر باینری که کامپایلر تولید میکنه، نیاز به یه runtime برای اجرا داره.
برنامه نویسا برای اینکه بتونن این مشکل رو حل کنن، از نسخه های پایین دات نت استفاده میکنن تا افراد زیادی اونو نصب شده در سیستم داشته باشن ، یا از نسخه پیش فرض دات نت استفاده کنن یا از نسخه ای که توسط برنامه های دیگه زیاد استفاده میشه، استفاده میکنن . مثلا ویندوز 10 بصورت پیش فرض با نسخه های 4.6 و 4.8.1 منتشر شده، بنابراین طبیعتا این دو نسخه میتونه یه انتخاب خوب برای برنامه نویسا باشه.
با توجه به اینکه در حال نزدیک شدن به انتشار نسخه dotNEt 8 هستیم، استفاده از نسخه های پایینتر ، موجب عدم استفاده از ویژگی ها و بهبودهایی که در نسخه های بالاتر ارائه شدن، میشه. بخصوص نسخه های آخر که با معرفی SPAN<T>
و بهبودهای زیاد که در JIT انجام دادن ، ویژگی های جالبی رو ارائه دادن.
با معرفی نسخه ی dotNET Core 3.1 ، مایکروسافت یه روش جایگزین برای این مورد معرفی کرد. اکنون بجای اینکه انتظار داشته باشید، کاربرا وابستگی های مورد نیاز رو نصب کنن، میتونید برنامه اتون رو بصورت مستقل (self-contained) منتشر کنید. بطور کلی این ویژگی یعنی اینکه، به کامپایلر میگید تا موقع خروجی گرفتن، همه وابستگی های مورد نیاز برای اجرای برنامه اتون، از جمله dotNET runtime ، رو در خروجی اعمال کنه. این قضیه مشکل نصب وابستگی ها در سمت کاربر رو حل و امکان اجرای برنامه در ماشین های مختلف رو فراهم میکنه.
بررسی dnSpy و وابستگی های اون :
برنامه dnSpy از جمله برنامه های دات نتی هست که بصورت self-contained منتشر شده. بنابراین اگه در کنار برنامه، DLLهای زیادی رو که بخشی از dotNET runtime هستن رو ببینیم ، مثله coreclr.dll ، جای تعجب نداره.
dnSpy همچنین به یسری کتابخونه دیگه هم وابستگی داره. از جمله ، dnlib که dnSpy در هسته خودش برای خوندن و نوشتن باینری های دات نتی بهش نیاز داره. خود dnlib به یه DLL دیگه ای بنام Microsoft.DiaSymReader.Native.amd64.dll وابستگی داره. dnlib ازش برای استخراج و خوندن متادیتا از فایلهای PDB استفاده میکنه. فایلهای Program database یا به اختصار PDB از جمله فایلهای مفید در مهندسی معکوس هستن و این امکان رو به محقق میدن تا اطلاعات دیباگ مانند نام متغیرهای محلی و … رو در خصوص باینری دیکامپایل شده استخراج کنن .
خود dnlib با یه نسخه از Microsoft.DiaSymReader.Native.amd64.dll ارائه نمیشه. چون این کتابخونه در سی پلاس نوشته شده و برای پلتفرم ویندوزی هستش. هاردلینک کردن به این کتابخونه، امکان چند پلتفرمی رو از dnlib میگیره. توسعه دهنده به دلیل اینکه ویژگی های ارائه شده توسط اون زیاد توسط dnlib استفاده نمیشه، ترجیح داده بصورت مستقیم بهش ارجاع نده.
با این حال ، dnSpy با نسخه شخصی Microsoft.DiaSymReader.Native.amd64.dll بعنوان بخشی از کتابخونه های self-contained runtime ارائه میشه و dnlib از این DLL استفاده میکنه.
همونطور که احتمالا حدس زدید، مشکل به همین DLL برمیگرده.
یه رویداد لوود DLL عجیب :
Ellie یکی از دوستان محقق ، در بررسی، یسری بدافزار دات نتی در برنامه Procmon ، متوجه رویداد لوود Microsoft.DiaSymReader.Native.amd64.dll از مسیرهای دیگه ای غیر از فولدر برنامه dnSpy شده. محقق اومده این رویداد رو در سیستم خودش باز تولید کرده و متوجه شده که این لوود در مواردی از طریق Windows SDK انجام میشه، نه از نسخه ای که همراه dnSpy ارائه شده :
در ویندوز برای اینکه برنامه از یه DLL استفاده کنه، براساس یه الگوریتم جستجوی خاص، یسری فولدرها و مسیرها برای پیدا کردن و لوود DLL ، بررسی میشه. معمولا این شکلیه که اول فولدر خود برنامه، بعدش دایرکتوری System32 و بعدش دایرکتوریهای تعریف شده در متغیر محیطی PATH بررسی میشن.
این الگوریتم جستجو، پای حمله DLL Hijacking رو وسط میکشه. فرض کنید برنامه یه DLL رو از System32 میخونه، ما اگه یه DLL مشابه نام اون بسازیم و در کنار برنامه بزاریم، برنامه وقتی نیاز به DLL داشته باشه و بخواد اونو لوود کنه، اول میره سراغ دایرکتوری برنامه، و در نتیجه DLL ما بجای DLL واقعی اجرا میشه. بازیگران تهدید با اینجکت کردن کدهای مخرب، مثلا شلکدهای دانلود یه فایل، یا reverse shell ، از این تکنیک بارها استفاده کردن.
dnSpy با توجه به اینکه دارای ویژگی Windows Integration هم هستش، این حمله رو جالبتر میکنه. این ویژگی گزینه ی Open with dnSpy رو به منوهای Windows Explorer اضافه میکنه که کاربر میتونه با کلیک راست روی فایل و انتخاب این گزینه، اونو مستقیم در dnSpy باز کنه.
خیلیا برای استفاده راحت، این ویژگی رو فعال کردن، اما بدتر از اون اینه که اصلا نیاز به فعال کردن این ویژگی هم نیست، چون میشه باینری رو به برنامه کشید و در dnSpy بازش کرد :
در هر دو مورد، Windows Explorer یه پروسس جدیدی از dnSpy با دایرکتوری که باینری توش بوده رو ایجاد میکنه :
این به این معنی هستش که اگه dnSpy رو فریب بدیم که Microsoft.DiaSymReader.Native.amd64.dll رو لوود کنه، در عین حال DLL مخربمون رو در همون دایرکتوری که فایل نمونه برای بررسی در dnSpy قرار باز بشه، قرار بدیم، و کاربر رو فریب بدیم که این فایل رو از دایرکتوری کاری جاری باز کنه (معملا اینجوریه) ، بنابراین dnSpy میاد و DLL مخرب مارو لوود و اجرا میکنه.
محقق با بررسی متوجه شده که این لوود همیشه اتفاق نمی افته بنابراین اومده مشکل رو بررسی کرده و اینکه چیکار کنه که dnSpy همیشه DLL رو از دایرکتوری کاری جاری لوود کنه .
در قدم اول به دلیل اینکه dnlib از این DLL استفاده میکنه ، سراغ اون رفته و متوجه شده که ایراد کاملا از اون نیست. کد زیر این واقعیت رو نشون میده که برای جستجوی DLL ،محدود به دایرکتوری جاری و مسیرهای ایمن از پیش تعریف شده هستش:
1 2 3 4 5 6 7 8 |
static class SymbolReaderWriterFactory { /* ... */ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories | DllImportSearchPath.AssemblyDirectory)] [DllImport("Microsoft.DiaSymReader.Native.amd64.dll", EntryPoint = "CreateSymReader")] static extern void CreateSymReader_x64(ref Guid id, [MarshalAs(UnmanagedType.IUnknown)] out object symReader); /* ... */ |
همچنین dnlib ، تمایل داره که از پیاده سازی کامل مدیریت شده خودش بعنوان یه PDB reader برای استخراج مقادیر زیادی از اطلاعات مورد نیازش استفاده کنه . dnSpy هیچکدوم از گزینه های پیش فرض رو موقع لوود symbolها تغییر نمیده ، بنابراین اگه محدودیت های مسیرهای جستجوی DLLها هم درست تنظیم نشده باشه، dnlib هرگز دستور لوود Microsoft.DiaSymReader.Native.amd64.dll رو وقتی داره symbolها رو واکشی میکنه، نمیگیره.
در ادامه محقق رفته سراغ بررسی CLR . با بررسی سورس کدهایی که مسئول جستجوی مسیرهای DLL هستن، مورد مشکوکی رو مشاهده نکرده.
محقق با بررسی بیشتر متوجه شده که وقتی dnSpy نمیتونه یه باینری رو بصورت کامل دیکامپایل کنه، این لوود DLL از SDK همیشه رخ میده. در چنین موردی ، موتور ILSpy ، یه استثناء ایجاد میکنه و dnSpy اونو میگیره و بصورت کامنت نشونش میده.
با وارد کردن dnSpy به windbg ، میشه منشاء لوود واقعی ماژول رو مشاهده کرد. همونطور که قابل مشاهده هست این مشکل از dnlib نیست و از فراخوانی Exception::get_StackTrace میاد :
dotNET runtime برای کمک به توسعه دهندگان برای رفع باگهاشون، stack trace رو با اطلاعات بیشتری مانند مسیر فایلها و شماره خطوط در صورت امکان ، ارائه میده. برای ارائه این اطلاعات ، dotNET runtime از Debugging Interface Access (DIA) خودش برای خوندن فایلهای PDB باینری که منجر به استثناء شده، استفاده میکنه.
برای ارائه اطلاعات بیشتر ، CLR نمونه ای از ISymUnmanagedBinder رو با فراخوانی FakeCoCreateInstanceEx ، که یه تابع مانند CoCreateInstanceEx هستش اما مسیری DLL رو دریافت میکنه، میگیره. فراخوانی FakeCoCreateInstanceEx رو میتونید ، اینجا مشاهده کنید. قسمتی از کدی که به DLL مورد بحث ما اشاره داره رو میتونید در زیر مشاهده کنید :
1 2 3 4 |
ceeload.h /* ... */ #define NATIVE_SYMBOL_READER_DLL W("Microsoft.DiaSymReader.Native.arm64.dll") /* ... */ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
ceeload.cpp /* ... */ // We're going to be working with Windows PDB format symbols. Attempt to CoCreate the symbol binder. // CoreCLR supports not having a symbol reader installed, so CoCreate searches the PATH env var // and then tries coreclr dll location. // On desktop, the framework installer is supposed to install diasymreader.dll as well // and so this shouldn't happen. hr = FakeCoCreateInstanceEx(CLSID_CorSymBinder_SxS, NATIVE_SYMBOL_READER_DLL, IID_ISymUnmanagedBinder, (void**)&pBinder, NULL); if (FAILED(hr)) { PathString symbolReaderPath; hr = GetClrModuleDirectory(symbolReaderPath); if (FAILED(hr)) { RETURN (NULL); } symbolReaderPath.Append(NATIVE_SYMBOL_READER_DLL); hr = FakeCoCreateInstanceEx(CLSID_CorSymBinder_SxS, symbolReaderPath.GetUnicode(), IID_ISymUnmanagedBinder, (void**)&pBinder, NULL); if (FAILED(hr)) { RETURN (NULL); } } /* ... */ |
همونطور که از کامنتها و کد قابل درکه، CLR ترجیح میده از یه مسیر نسبی برای Microsoft.DiaSymReader.Native.arm64.dll استفاده کنه که باعث میشه برای پیاده سازی DIA ،مسیر جاری (همچنین working directory) رو بررسی کنه. خوشبختانه با Pull Request #87782 ، با حذف فراخوانی اول، این مشکل رو اصلاح کرده و همیشه با استفاده از GetClrModuleDirectory مستقیما سراغ دایرکتوری نصب CLR میره تا DLL رو پیدا کنه. این اصلاحیه از نسخه ی dotNET 6.0.20 ارائه شده و با بروزرسانی ویندوز مشکل حل شده.
اما با توجه به اینکه dnSpy یه برنامه ی self-contained هستش، نسخه ی coreclr.dll هنوز بروز نشده و آسیب پذیر هستش. این یکی از مشکلات برنامه های مستقل هستش، توسعه دهندگان باید مراقب باشن تا باینری هاشون با بروزرسانی های امنیتی ، بروز شده باشه.
پس بطور کلی وقتی یه استثناء رخ میده، یه stack trace درخواست میشه و runtime میاد و Microsoft.DiaSymReader.Native.amd64.dll رو از current working directory لوود میکنه، برخلاف روشی که خود باینری انجام میده.
توسعه اکسپلویت
خب حالا ما روشی رو برای اجرای کد در dnSpy داریم و برای توسعه اکسپلویت، میخواییم کاری کنیم تا ماشین حساب اجرا بشه.
قدمهایی که باید طی کنیم اینا هستن :
- یه DLL با عنوان Microsoft.DiaSymReader.Native.amd64.dll ایجاد کنیم که در DllMain اش، calc.exe رو اجرا کنه.
- یه نمونه ایجاد کنیم که در dnSpy منجر به ایجاد استثناء شده و یه stack trace درخواست کنه و خطا رو پرینت کنه.
- DLL رو در کنار نمونه ساخته شده قرار بدیم.
خب قدم اول که ساده هستش فقط کافیه یه پروژه Cpp DLL در ویژوال استدیو با نام Microsoft.DiaSymReader.Native.amd64.dll ایجاد کنید و با استفاده از WinExec در DllMain اش، برنامه calc.exe رو فراخوانی کنیم :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include "pch.h" BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: WinExec("calc.exe", 0); break; } return TRUE; } |
قدم دوم هم ساده هستش. همونطور که قبلا گفته شد، موتور دیکامپایلر ILSpy وقتی در دیکامپایل یه متد با خطا مواجه میشه، یه پیام کامل از خطا رو نشون میده. خب ما میتونیم یه برنامه Hello World بنویسیم و یه متد ساختگی به کلاس اصلی اون اضافه کنیم و کد رو با برخی داده های ناخواسته جایگزین کنید تا به دستورات CIL معتبری دیکد نشه و در نتیجه ، دیکامپایل نشه و استثناء رو تولید کنه.
1 2 3 4 5 6 7 |
public static class Program { public static void Main() => Console.WriteLine("Hello, world!"); // Replace some opcode bytes of the following method body with some junk. private static void Dummy() => Console.WriteLine("This is never called"); } |
خب قدم سوم هم که این دو تا فایل کنار هم قرار میدید.
نمونه PoC ارائه شده رو میتونید از اینجا دانلود کنید. (در ماشین مجازی یا محیط امن تست کنید)
در ویدیو زیر یه دمو از اجرای این PoC رو مشاهده میکنید.
اقدامات امنیتی :
- هر بروزرسانی در runtime ، نیاز به بروزرسانی برنامه های مستقل داره.
- ماشین های مجازی مانند دات نت ، نوید یه محیط امن رو میدن، در بیشتر موارد حق با اوناست اما مواردی هم هستش که منجر به آسیب پذیری میشه از جمله اجرای کد همونطور که در این پست مشاهده کردیم. بنابراین از یه محیط سندباکس برای اجرای برنامه های ناشناخته استفاده کنید. شما نمیدونید افراد دیگه چه آسیب پذیری هایی رو کشف کردن یا در حال اکسپلویت چه آسیب پذیریهایی هستن.
- در نهایت نسخه جدید dnSpy رو دریافت و استفاده کنید.