پیشگفتار
تزریق DLL یا همان DLL Injection در واقع فرآیندی است که طی آن، یک قطعه کد مورد نظر خودمان را در یک پروسس شناخته شدهی در حال اجرا قرار میدهیم، تا بدینوسیله کد یا به عبارت دیگر ابزارهایمان را با استفاده از آن اجرا کنیم. به این ترتیب اگر این فرآیند با موفقیت انجام شود، میتوان راهکارهای دفاعی مانند آنتیویروس را دور زد. از آنجاییکه DLLها به طور پیشفرض در زمان اجرا توسط پروسسها بارگذاری میشوند، اغلب قطعه کدی که قصد اجرای آن را داریم نیز به صورت یک DLL به پروسس مورد نظر میدهیم. در این روش، توجه به این نکته مهم است که برای دسترسی به حافظهی یک برنامه، نیاز به سطح دسترسی مناسبی در سامانهی هدف خواهیم داشت. در این مقاله یکی از روشهای کلاسیک تزریق DLL به یک پروسس را شرح دادهایم.
بررسی اجمالی
ویژگی Windows API تعدادی از توابع مختلف را در اختیار ما قرار میدهد تا با اهدافی مانند ایرادیابی و غیره بتوانیم به برنامههای دیگر متصل شده و تغییراتی در آنها ایجاد کنیم. در این مقاله روشی را توضیح میدهیم که با استفاده از این توابع، تزریق DLL در چهار گام زیر را امکانپذیر میکند:
- اتصال به پروسس هدف
- اختصاص حافظه در این پروسس
- کپی کردن DLL یا آدرس آن در حافظهی پروسس و تعیین آدرسهای مناسب برای آن
- قرار دادن برنامه یا پروسس در مسیری که منجر به اجرای DLL شود
هر کدام از این گامها میتواند توسط یک یا چند تکنیک برنامهنویسی مختلف اجرا شود که بهطور خلاصه در شکل زیر نمایش داده شده است. با توجه به مزایا و معایبی که هر یک از این روشها دارند، مهم است که جزییات عملکرد هر تکنیک را بدانید.
جزییات مربوط به هر یک از این گامها در بخشهای بعد شرح داده شده است.
نقطهی شروع اجرا
انتخابهای مختلفی برای دستور دادن به پروسس هدف جهت اجرای DLL تزریق شده داریم، به عنوان مثال میتوان به توابعی مانند ()CreateRemoteThread()، NtCreateThreadEx و غیره اشاره کرد. البته ما نمیتوانیم نام DLL خود را به عنوان ورودی به این توابع بدهیم و به جای آن باید از آدرس حافظهی مربوط به آنها برای اجرا کردنشان استفاده کنیم. همانگونه که ذکر شد ابتدافضایی در حافظهی پروسس هدف اختصاص داده و سپس DLL را در آنجا قرار میدهیم و به این ترتیب آن را به عنوان نقطهی شروع اجرا، آماده میکنیم. در حقیقت ما دو مدل نقطهی شروع میتوانیم داشته باشیم، روش اول استفاده از تابع () LoadLibraryA و روش دیگر، پریدن به DllMain میباشد که در ادامه هر دوی این تکنیکها به صورت خلاصه شرح داده شده است.
استفاده از تابع () LoadLibraryA
یکی از توابع kernel32.dll است که برای بارگذاری DLLها، فایلهای اجرایی و دیگر کتابخانههای پشتیبانی شده در زمان اجرا کاربرد دارد. این تابع تنها یک نام فایل را به عنوان ورودی دریافت میکند. به عبارت دیگر کافیست تا تنها مقداری حافظه برای آدرسی که به DLL ما اشاره میکند اختصاص دهیم و نقطهی شروع اجرای خود را بر روی آدرس () LoadLibraryA تنظیم کنیم. یعنی آدرس حافظه را به آن ارایه دهیم بهگونهای که این مسیر به عنوان یک پارامتر ورودی در نظر گرفته میشود.
بزرگترین نقطهضعف استفاده از تابع () LoadLibraryA این است که DLL بارگذاری شده را با برنامهی مورد نظر ثبت میکند و بنابراین DLL ما به راحتی قابل ردیابی و شناسایی است. یک نکته مهم دیگر در خصوص این روش که میتواند کمی آزار دهنده باشد این است که اگر یک DLL قبل از آن یکبار با () LoadLibraryA بارگذاری شده باشد، دیگر آن را اجرا نخواهد کرد. البته میتوان این مشکل را برطرف کرد اما نیاز به برنامهنویسی بیشتری دارد.
پرش به DllMain
یک تکنیک جایگزین برای () LoadLibraryA، بارگذاری کامل DLL در حافظه و پس از آن تعیین یک مقدار Offset برای نقطهی ورود DLL (DLL’s Entry Point) است. با استفاده از این روش میتوانید از ثبت شدن DLL با برنامه جلوگیری کرده و علاوه برای اجرای بیسروصدا (Stealthy)، آن DLL را هر چندبار که نیاز بود به صورت مکرر در پروسس هدف تزریق کنید.
گام اول: اتصال به پروسس
در گام اول به یک Handle نیاز داریم تا توسط آن بتوانیم با پروسس مورد نظر تعامل داشته باشیم. برای این منظور از تابع ()OpenProcess استفاده خواهیم کرد. همچنین به سطح مناسبی از دسترسی در سیستمعامل نیاز خواهیم دشات تا بتوانیم وظایف نمایش داده شده در شکل زیر را اجرا کنیم. حق دسترسیهای مشخصی که درخواست میکنیم با توجه به نسخهی ویندوز میتواند متفاوت باشد اما در حالت کلی، موارد زیر بر روی اغلب نسخهها کار میکند:
hHandle = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | |
گام دوم: تخصیص حافظه
در حالت کلی پیش از اینکه بتوانیم چیزی را در یک پروسس تزریق کنیم، به فضایی نیاز خواهیم داشت تا کد مورد نظر مان را در آنجا قرار دهیم. برای این منظور از تابع ()VirtualAllocEx بهره میبریم.
تابع () VirtualAllocEx، مقدار حافظهی مورد نیاز را به عنوان یکی از پارامترهای خود میگیرد. اگر از تابع () LoadLibraryA استفاده کنیم، مقداری فضا برای آدرس کامل DLL مورد نظر اختصاص خواهیم داد. اما اگر از تکنیک پرش به DllMain بهره ببریم، باید فضایی برای تمام محتوای DLL تخصیص دهیم. در ادامه هریک از این دو روش شرح داده شده است.
مسیر DLL
در صورت انتخاب روش اختصاص فضا برای مسیر کامل DLL، به برنامهنویسی کمتری نیاز است اما نه به میزان چشمگیر، همچنین لازم است تا از تابع () LoadLibraryA استفاده کنیم که یکسری مشکلات را به دنبال خواهد داشت (در بخشهای قبل در این مورد توضیح داده شد)، ابته استفاده از این روش بسیار متداول است. برای اجرای این روش ابتدا نیاز است تا با استفاده از تابع () VirtualAllocEx، مقدارحافظهی کافی برای پشتیبانی از رشتهای که شامل آدرس DLL میباشد را اختصاص دهید:
GetFullPathName(TEXT(“somedll.dll”), BUFSIZE, dllPathAddr = VirtualAllocEx(hHandle, 0, |
DLL کامل
انتخاب روش اختصاص فضا برای تمام DLL، کمی به برنامهنویسی بیشتری نیاز دارد اما نسبت به روش قبل، قابل اعتمادتر بوده و همچنین دیگر نیازی به استفاده از تابع () LoadLibraryA نخواهد بود.
برای این منظور ابتدا با استفاده از تابع () CreateFileA یک Handle به DLL مورد نظر باز نموده سپس اندازهی آن را با تابع () GetFileSize مشخص و نتیجه را به تابع ()VirtualAllocEx ارسال کنید:
GetFullPathName(TEXT(“somedll.dll”), BUFSIZE, hFile = CreateFileA( dllPath, GENERIC_READ, dllFileLength = GetFileSize( hFile, NULL ); remoteDllAddr = VirtualAllocEx( hProcess, NULL, |
گام سوم: کپی کردن DLL یا تعیین آدرسها
در این مرحله نوبت آن رسیده است که DLL خود (محتوا یا آدرس) را در فضای حافظهی پروسس هدف کپی کنیم.
در واقع اکنون که بخشی از فضای حافظهی پروسس هدف را تخصیص دادهایم، میتوانیم محتوای DLL یا آدرس آن (بسته به تکنیکی که انتخاب کردهایم) را در پروسس کپی کنیم. همانگونه که در بخشهای بعد شرح داده شده است، برای اینکار از تابع () WriteProcessMemory بهره خواهیم برد.
مسیر DLL
در این تکنیک میتوانیم به صورت زیر، مسیر DLL خود را در پروسس کپی کنیم:
WriteProcessMemory(hHandle, dllPathAddr, |
کپی کردن کامل DLL
در این روش، نیاز است تا ابتدا DLL مورد نظر خود را در حافظه خوانده و سپس آن را در پروسس هدف کپی کنیم. با استفاده از کد زیر میتوان این فرآیند را اجرا کرد.
lpBuffer = HeapAlloc( GetProcessHeap(), 0, ReadFile( hFile, lpBuffer, WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, |
تعیین نقطهی شروع اجرا
اغلب توابع اجرایی برای شروع کار خود یک آدرس حافظه را دریافت میکنند، در اینجا نیز لازم است تا این مقدار را برای اجرای DLL خود مشخص کنیم. در بخشهای بعد، شیوهی تعیین این آدرس برای هر دو تکنیک مذکور، شرح داده شده است.
مسیر DLL و () LoadLibraryA
نیاز است تا حافظهی پروسس مورد نظر خود را برای یافتن آدرس شروع () LoadLibraryA جستجو کنیم، سپس آن را به همراه آدرس حافظهی مسیر DLL به عنوان پارامتر به تابع اجرایی خود ارایه دهیم. برای دریافت آدرس () LoadLibraryA مطابق خط زیر میتوانیم از توابع () GetModuleHandle و () GetProcAddress استفاده کنیم.
loadLibAddr = GetProcAddress(GetModuleHandle(TEXT(“kernel32.dll”)), “LoadLibraryA”); |
تکنیک استفاده از DLL کامل و پرش به DllMain
با کپی کردن DLL کامل در حافظه، میتوانیم از ثبت شدن DLL خود با پروسس هدف جلوگیری کرده و تزریق قابل اطمینانتری را انجام دهیم. یک قسمت به نسبت دشوار این روش، بهدست آوردن نقطهی ورود به DLL زمانیکه در حافظه بارگذاری شده، میباشد. خوشبختانه تابع () LoadRemoteLibraryR که Stephen Fewer در تکنیک ReflectiveDLLInjection خود از آن استفاده کرده، اینکار را بهطور کامل برای ما انجام میدهد. البته باید در نظر داشت که در اینصورت، روش اجرایی ما تنها به تابع () CreateRemoteThread محدود خواهد شد. بنابراین به جای آن، ما از () GetReflectiveLoaderOffset برای Offset خود در حافظهی پروسسهای مورد نظر استفاده میکنیم و سپس از این Offset به اضافهی آدرس پایهی حافظه در پروسس قربانی هدف که DLL خود را در آن نوشتهایم، به عنوان نقطهی شروع اجرا استفاده خواهیم کرد.
در اینجا ذکر این نکته ضروری است که DLL ما باید با Includeها و Optionهای مورد نیاز کامپایل شده باشد بهگونهای که بتواند خود را با تکنیک ReflectiveDLLInjection هماهنگ کند. شیوهی پیادهسازی این تکنیک در شکل زیر نمایش داده شده است.
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpWriteBuff); |
گام چهارم: اجرای DLL
تا اینجا ما DLL خود را در حافظه قرار داده و آدرس حافظه برای شروع اجرای فرآیند را میدانیم. بنابراین تنها کافیست تا به پروسس هدف بگوییم تا آن را اجرا کند. روشهای مختلفی برای این کار وجود دارد که در ادامه برخی از مهمترین آنها را شرح دادهایم.
استفاده از تابع () CreateRemoteThread
تابع () CreateRemoteThread شناختهشدهترین و پرکاربردترین روش برای این کار به حساب میآید. این روش بسیار قابل اعتماد است، اما شاید شما به دلایلی مانند دور زدن راهکارهای شناسایی در سامانهی هدف یا تغییراتی که مایکروسافت برای جلوگیری از اجرای () CreateRemoteThread انجام داده، قصد داشته باشید تا از روش دیگری بهره ببرید.
از آنجایی که () CreateRemoteThread یک روش تثبیتشده است، شما انعطافپذیری بالایی در شیوهی استفاده از آن خواهید داشت. برای مثال، میتوانید از زبان پایتون برای تزریق DLL در ویندوز بهره ببرید.
rThread = CreateRemoteThread(hTargetProcHandle, NULL, 0, lpStartExecAddr, lpExecParam, 0, NULL); |
استفاده از تابع () NtCreateThreadEx
() NtCreateThreadEx یکی از توابع مستند نشدهی ntdll.dll است. ایراد استفاده از توابع مستند نشده، این است که بنابر تصمیم شرکت مایکروسافت، امکان دارد در هر زمانی حذف یا تغییر داده شوند. فراخوانی تابع () NtCreateThreadEx کمی پیچیدهتر از تابع قبلی است. بهگونهای که ما به یک ساختار خاص برای انتقال داده به آن و یک ساختار دیگر برای دریافت داده از آن نیاز خواهیم داشت. یک نمونه از پیادهسازی آن در شکل زیر نمایش داده شده است:
struct NtCreateThreadExBuffer { typedef NTSTATUS (WINAPI *LPFUN_NtCreateThreadEx) ( HANDLE bCreateRemoteThread(HANDLE hHandle, LPVOID loadLibAddr, LPVOID dllPathAddr) { HANDLE hRemoteThread = NULL; LPVOID ntCreateThreadExAddr = NULL; ntCreateThreadExAddr = GetProcAddress(GetModuleHandle(TEXT(“ntdll.dll”)), “NtCreateThreadEx”); if( ntCreateThreadExAddr ) { ntbuffer.Size = sizeof(struct NtCreateThreadExBuffer); LPFUN_NtCreateThreadEx funNtCreateThreadEx = (LPFUN_NtCreateThreadEx)ntCreateThreadExAddr; &hRemoteThread, if (hRemoteThread == NULL) { |
حال همانگونه که در شکل زیر قابل مشاهده است، با روشی بسیار شبیه به تابع () CreateRemoteThread، میتوانیم آن را فراخوانی کنیم:
rThread = bCreateRemoteThread(hTargetProcHandle, lpStartExecAddr, lpExecParam); |
تعلیق، تزریق و بازیابی
تعلیق، تزریق و بازیابی، عباراتی غیر رسمی است، برای توصیف فرآیند «تزریق به پروسس هدف با اتصال به آن، تعلیق پروسس و تمام رشتههای (Threads) مربوط به آن، هدف گرفتن یک رشتهی خاص، ذخیرهی رجیسترهای فعلی، تغییر اشارهگر دستورالعمل (Instruction Pointer) جهت اشاره به نقطهی شروع اجرایی شما و در نهایت بازیابی رشته (Thread)». این فرآیند یک روش غیر رسمی است اما با ضریب اطمینان خوبی کار میکند و نیازی به فراخوانی توابع اضافی دیگر ندارد.
پیادهسازی این روش کمی دشوار است، اما در صورت علاقهمندی به کسب اطلاعات بیشتر میتوانید به این مقاله رجوع کنید. در شکل زیر نمونهای از پیادهسازی آن نمایش داده شده است:
VOID suspendInjectResume(HANDLE hHandle, LPVOID loadLibAddr, LPVOID dllPathAddr) { This is a mixture from the following sites: https://syprog.blogspot.com/2012/05/createremotethread-bypass-windows.html */ HANDLE hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 ); PVOID scAddr; int i; unsigned char sc[] = { te.dwSize = sizeof(THREADENTRY32); sc[3] = ((unsigned int) dllPathAddr & 0xFF); sc[8] = ((unsigned int) loadLibAddr & 0xFF); // Suspend Threads |
شناسایی تهدیدات مبتنی بر تزریق DLL
یکی از روشهای شناسایی این نوع تهدیدات، شناسایی پروسسهای غیرعادی یا ناشناختهای است که به عنوان یکی از اپلیکیشنهای شما، سطح دسترسی Aniministrator دارد. همچنین استفاده از راهکارهای بهروز مانند آنتیویروس، EDR و Threat Intelligence میتواند در شناسایی بسیاری از این تهدیدات به سازمان شما کمک کند. در صورت شناسایی یک مهاجم که به زیرساخت سازمان نفوذ کرده، نیاز است تا یک متخصص شکار تهدیدات سایبری یا یک متخصص فارنزیک، سامانههای شما را با تکنیکها و ابزارهایی که برای اینکار وجود دارند بررسی و تحلیل کند.