
إطار تطوير شيلكود وضع المستخدم لنظام ويندوز (WUMSDF)
v1.1مشروع SILVERPICK هو إطار عمل تطوير شيل كود في وضع المستخدم لنظام ويندوز (WUMSDF)، والغرض الوحيد منه هو تمكين مطوّري القدرات من بناء كتل كود مستقل عن العنوان (PIC) لنظام Windows بمعمارية x64 باستخدام C/C++ بطريقة سهلة، وذلك لتقليل تكاليف التطوير لمثل هذا المسعى.
إنه مشتق من مشروع WILDBEAST، وبالتالي يعتمد على:
Visual Studio Code كمحرر أكوادMinGW-w64 كسلسلة أدوات الترجمةGNU Make كنظام بناءيمكنك العثور على تعليمات الإعداد من هنا: GCC-Clang-Setup-Windows
يُرجى ملاحظة أن هذا المشروع يستخدم MSYS2.
كتابة الشيل كود بلغات برمجة عالية المستوى ليس بالأمر الجديد، فقد نُشرت عدد لا يحصى من التدوينات والأوراق البحثية حول هذا الموضوع منذ عام 2010. إذن، ما الجديد في SILVERPICK؟
حسنًا، يسعدني أنك سألت.
SILVERPICK يملك حقيبة صغيرة لطيفة من الحيل المخبأة في جعبته، لكن الأهم من ذلك كله، أن هذه هي رؤيتي الشخصية للموضوع.
لذا، دون مزيد من اللغط، أقدم لكم حيلتي الأولى.
منذ أن روّج مات غريبر (Matt Graeber) لكتابة الشيل كود بلغة C، كان معظم الناس يستخدمون كعب محاذاة المكدس ذا الـ 16 بايت المكتوب بلغة Assembly.
ورغم أن هذا ليس مشكلة، وبما أننا لسنا IKEA، فإن لغة التجميع لا ينبغي أن تكون ضرورية، وهي في الحقيقة ليست كذلك.
توجد سمة دالة في GCC من شأنها إصدار كعب محاذاة المكدس نيابةً عنك.
تعرّف على سمة الدالة force_align_arg_pointer في صورة ماكرو مفيد يُدعى ALIGN_STACK، والذي يولّد لغة التجميع التالية:
Disassembly of section .init:
<PicEntry>:
push rbp
mov rbp,rsp
and rsp,0xfffffffffffffff0
sub rsp,0x20
call <PicEntry+0x11> IMAGE_REL_AMD64_REL32 .text$payload
leave
ret
ما هو قسم .init، تتساءل؟ حسنًا، هذا يقودنا بشكل جميل إلى حيلتي الثانية.
ربما يكون مات غريبر قد روّج لكتابة الشيل كود بلغة C في وقت ما، لكن في الحقيقة، كان بول أونغور (Paul Ungur) هو من أحيا هذا الفن الأسود مع مشروع Stardust.
الآن، يستخدم Stardust سكربت ربط خاص بأدوات Binutils للتحكم في وضع الدوال والبيانات داخل قسم PE المناسب بالترتيب الصحيح. هذه التقنية نفسها مشتقة من عمل أوستن هدسون (Austin Hudson)، وكثير من الناس يستخدمون نسخة مختلفة من سكربتات الربط الخاصة به.
في حين أن سكربتات الربط رائعة لترتيب الأقسام في الرابط، إلا أنها غير ضرورية إذا كان كل ما تحتاجه هو وضع دالة معينة في بداية قسم الكود.
هنا تأتي سمة الدالة section مع اسم قسم خاص يُدعى .init، والذي يخبر الرابط بأن الدالة تحتوي على كود تهيئة زمن التشغيل الذي يسبق main()، ويجب أن تكون أولًا في ترتيب الربط.
من أجل هذا الغرض، تم إنشاء ماكرو CODE_BEGIN.
أما بالنسبة لحيلتي الثالثة، فأقدم لكم ماكرو STACK_STRING.
في C، يمكنك إنشاء سلسلة نصية على المكدس (سلسلة تُبنى ديناميكيًا على المكدس) عن طريق تعريف حرفية السلسلة كمصفوفة من أحرف ANSI:
char charrHelloKitty[] = { 'H', 'e', 'l', 'l', 'o', 'K', 'i', 't', 't', 'y', '\0' };
في C++، يمكنك إنشاء سلسلة نصية على المكدس بمجرد تعريف مصفوفة char على أنها constexpr:
constexpr char charrHelloKitty[]{ "HelloKitty" };
ومع ذلك، فإن كلتا التقنيتين تصبحان عديمتي الفائدة في مواجهة تحسينات المترجم إذا كانت الحروف النصية كبيرة بما يكفي، على عكس حلّنا الذي سيعمل بغض النظر عن طول السلسلة ومستوى تحسينات المترجم، وذلك بفضل خدعة برمجة قوالب C++ الذكية التي تفضلنا بها جان بولو (Can Bölük).
استخدام هذا الماكرو سهل للغاية:
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
سيولّد هذا لغة التجميع التالية:
mov [rsp+58h+var_23], 61h ; 'a'
mov [rsp+58h+var_22], 6Eh ; 'n'
mov [rsp+58h+var_21], 20h ; ' '
mov [rsp+58h+var_20], 65h ; 'e'
mov [rsp+58h+var_1F], 78h ; 'x'
mov [rsp+58h+var_1E], 74h ; 't'
mov [rsp+58h+var_1D], 72h ; 'r'
mov [rsp+58h+var_1C], 61h ; 'a'
mov [rsp+58h+var_1B], 20h ; ' '
mov [rsp+58h+var_1A], 6Ch ; 'l'
mov [rsp+58h+var_19], 6Fh ; 'o'
mov [rsp+58h+var_18], 6Eh ; 'n'
mov [rsp+58h+var_17], 67h ; 'g'
mov [rsp+58h+var_16], 20h ; ' '
mov [rsp+58h+var_15], 68h ; 'h'
mov [rsp+58h+var_14], 65h ; 'e'
mov [rsp+58h+var_13], 6Ch ; 'l'
mov [rsp+58h+var_12], 6Ch ; 'l'
mov [rsp+58h+var_11], 6Fh ; 'o'
mov [rsp+58h+var_10], 20h ; ' '
mov [rsp+58h+var_2F], 0
mov [rsp+58h+var_F], 77h ; 'w'
mov [rsp+58h+var_E], 6Fh ; 'o'
mov [rsp+58h+var_D], 72h ; 'r'
mov [rsp+58h+var_C], 6Ch ; 'l'
mov [rsp+58h+var_B], 64h ; 'd'
mov [rsp+58h+var_A], 21h ; '!'
mov [rsp+58h+var_33], 44h ; 'D'
mov [rsp+58h+var_32], 65h ; 'e'
mov [rsp+58h+var_31], 6Dh ; 'm'
mov [rsp+58h+var_30], 6Fh ; 'o'
وبالحديث عن C++، أقدم لكم في حيلتي الرابعة التجزئة النصية في زمن الترجمة (compile-time string hashing).
ورغم أن هذا ليس مفهومًا جديدًا، فإن SILVERPICK يقدم بعض التحسينات على التنفيذات العامة الموجودة.
أولًا، نستخدم المتغير 64-بت من دالة التجزئة غير المشفرة الشهيرة FNV-1a لتقليل احتمالية نجاح هجوم تصادم التجزئة.
ثانيًا، نستخدم معاملًا معدلًا لدالة التجزئة للدفاع ضد عمليات البحث في جداول التجزئة المحسوبة مسبقًا، مثل HashDB. والأهم من ذلك، أن هذا لا يغير خصائص دالة التجزئة.
لتجزئة سلسلة قصيرة في زمن التشغيل، ما عليك سوى استخدام ماكرو HASH_STRING_RUN_TIME.
لتجزئة حرفية نصية قصيرة في زمن الترجمة، ما عليك سوى استخدام ماكرو HASH_STRING_COMPILE_TIME. يتم ضمان التقييم في زمن الترجمة فقط عبر consteval.
اتضح أنه يمكنك تنفيذ عدد لا بأس به من دوال مكتبة وقت التشغيل للغة C (CRT) باستخدام تعليمات السلاسل في معمارية x86. لذا، بالطبع، كان عليّ تنفيذها باستخدام مزيج من الدوال الجوهرية للمترجم (compiler intrinsics) ولغة التجميع المضمّنة (inline assembly).
هل تريد استخدام دالة msvcrt!memset في كودك؟ استخدم بدلًا منها ماكرو ZERO_MEMORY، والذي يستخدم تعليمة rep stosb الصادرة عبر دالة جوهرية للمترجم.
ماذا عن دالة msvcrt!memcpy أو دالة msvcrt!memmove، تسأل؟ تعرّف على ماكرو COPY_MEMORY كبديل، والذي يستخدم تعليمة rep movsb الصادرة عبر دالة جوهرية للمترجم.
ولكن ماذا عن بديل لدالة msvcrt!memcmp؟ اتضح أنه لا توجد بالضبط دالة جوهرية للمترجم متاحة لإصدار تعليمة repe cmpsb. لذا، نكتب دالة compare_memory باستخدام لغة التجميع المضمّنة بدلًا من ذلك.
أخيرًا، إذا كنت تبحث عن بديل لدالة msvcrt!memchr، تعرّف على دالة scan_memory، والتي تستخدم مرة أخرى لغة التجميع المضمّنة نظرًا لعدم وجود دالة جوهرية للمترجم متاحة لإصدار تعليمة repne scasb.
أوه، وهل نسيت أن أذكر أنه يمكنك كتابة نسختك الأكثر أمانًا من دالة msvcrt!strlen باستخدام روتين scan_memory على النحو التالي:
DWORD_PTR dwptrExportNameLength = std::min(BIT_CAST(DWORD_PTR, scan_memory(strExportName, 0x00, MAX_EXPORTED_SYMBOL_NAME_LEN)) - BIT_CAST(DWORD_PTR, strExportName), MAX_EXPORTED_SYMBOL_NAME_LEN);
يُرجى ملاحظة أن هذه التنفيذات قد لا تنتج الكود الأكثر أداءً، اعتمادًا على البنية الدقيقة لوحدة المعالجة المركزية CPU المستهدفة. ومع ذلك، فهي مضمونة لإتمام المهمة.
هل تهتم بمزيد من الحيل الساحرة؟
هناك الكثير من الماكرو الصغيرة الأخرى في Common.h والتي وُجدت لتجريد بعض التعقيدات المتعلقة بترويض المترجم.
يتم توفير تنفيذ خالٍ من الاعتماديات لدالة GetModuleHandle في UserModuleBase.cpp. ولتبسيط سهولة الاستخدام، تم إنشاء ماكرو مفيد باسم GET_USER_MODULE_BASE.
وبالمثل، يتم توفير تنفيذ خالٍ من الاعتماديات لدالة GetProcAddress في PEParse.cpp، وهو ملفوف في ماكرو عملي باسم مناسب هو GET_EXPORTED_SYMBOL_ADDRESS. علاوة على ذلك، تم توفير ماكروين إضافيين للمساعدة في الربط الديناميكي في زمن التشغيل - INITIALIZE_FUNCTION_POINTER لتعريف وتهيئة مؤشر دالة إلى 0، وRESOLVE_FUNCTION_POINTER لحل مؤشر الدالة المذكور.
تكامل Visual Studio Code مدمج في المشروع حتى يتمكن المطورون من استخدام اختصار لوحة المفاتيح Ctrl+Shift+B لعملية بناء خالية من المتاعب.
تكامل GitHub Actions مدمج أيضًا في المشروع لتمكين بناءات CI.
يتباهى المشروع إلى حد ما بهيكله المنظم جيدًا، بالإضافة إلى الكود المعلق عليه جيدًا والنظيف نسبيًا.
أخيرًا، ألقِ نظرة على Makefile الخاص بالمشروع، والذي يحتوي على أفضل مجموعة مختارة من أعلام المترجم والرابط التي ستولّد كودًا صغيرًا وآمنًا وودودًا لـ OPSEC. وفي الوقت نفسه، ستوفر السجلات المفصلة وملف خريطة الرابط المُولَّد رؤية واضحة لعملية البناء لتسهيل فهم أعمق لسلسلة الأدوات. بالإضافة إلى ذلك، تنتج كل وحدة ترجمة أيضًا ملف تفكيك، وغالبًا ما يجعلك تفكيكه تقول أشياء مثل "ماذا فعل المترجم الآن؟"، وما إلى ذلك.
إذا كنت مقتنعًا بهذا الإطار، فسيصف هذا القسم كيفية استخدامه.
فيما يلي مقتطف مأخوذ من PicMain.cpp:
/// @brief PIC start function
/// @param None
/// @return None
EXTERN_C NO_INLINE VOID __stdcall payload(
VOID
) {
// Init local variables
PVOID pKernel32 = nullptr;
INITIALIZE_FUNCTION_POINTER(LoadLibraryA);
HMODULE hUser32 = nullptr;
STACK_STRING(sstrUser32, "user32.dll");
INITIALIZE_FUNCTION_POINTER(MessageBoxA);
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
// Get the image base address of kernel32.dll
pKernel32 = GET_USER_MODULE_BASE("kernel32.dll");
if (pKernel32 == nullptr)
goto cleanup;
// Resolve kernel32!LoadLibraryA
RESOLVE_FUNCTION_POINTER(pKernel32, LoadLibraryA);
if (LoadLibraryA == nullptr)
goto cleanup;
// Load User32.dll into the process VAS
hUser32 = LoadLibraryA(sstrUser32.data());
if (hUser32 == nullptr)
goto cleanup;
// Resolve user32!MessageBoxA
RESOLVE_FUNCTION_POINTER(hUser32, MessageBoxA);
if (MessageBoxA == nullptr)
goto cleanup;
// Display a message box
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
// Cleanup
cleanup:
return;
}
يبدو الأمر سهلًا بما فيه الكفاية، أليس كذلك؟
عند كتابة PIC بلغة C/C++ باستخدام إطار SILVERPICK، عليك أن تراعي القواعد التالية:
payload كما تتعامل مع دالة main في البرنامج التقليدي، أي باعتبارها نقطة الدخول (الشبه فعلية).Windows API أو Native API إلا عبر الربط الديناميكي في زمن التشغيل بعد التأكد من أن النموذج الأولي للدالة متاح في ملف الرأس المقابل.يحتوي هذا القسم على قائمة غير شاملة بالتحسينات المخطط لها والتي سيتم دمجها في مشروع مستقبلي.
Clang/LLVM.GS.تصفية عناوين التصدير (EAF).فيما يلي قائمة بالمراجع مرتبة ترتيبًا زمنيًا، والتي كانت ذات قيمة لا تُقدَّر بثمن بالنسبة لي أثناء بحثي، واستُخدمت على نطاق واسع كمصدر إلهام لهذا المشروع:
Writing Shellcode with a C Compiler بقلم Nick Harbour (2010)
Shellcode with a C-compiler بقلم Didier Stevens (2010)
Writing Optimized Windows Shellcode in C بقلم Matt Graeber (2013)
Shellcode the better way, or how to just use your compiler بقلم Justin Fisher (2016)
ShellcodeStdio بقلم Jack Ullrich (2016)
Shellcode: A Windows PIC using RSA-2048 key exchange, AES-256, SHA-3 بقلم Odzhan (2016)
Writing Optimized Windows Shellcode بقلم Dimitri Fourny (2017)
Writing and Compiling Shellcode in C بقلم Aleksandra Doniec و Mantvydas Baranauskas (2021)
Creating Shellcode from any Code Using Visual Studio and C++ بقلم Hamid Memar (2021)
Writing Optimized Windows Shellcode in C بقلم Philip Woldhek (2021)
From C, with inline assembly, to shellcode بقلم Steve Salinas (2023)
How To Craft Your Own Windows x86/64 Shellcode with Visual Studio بقلم Yazid Benjamaa (2023)
Modern implant design: position independent malware development بقلم Paul Ungur (2024)
ما أثار دهشتي هو تصنيف VirTool:Win64/Silepesz.A من قبل Microsoft Defender.
هذه هي البايتات التي تغطيها التوقيعات:
[+] Target file size: 2560 bytes
[+] Analyzing...
[!] Identified end of bad bytes at offset 0x4CB
000003CB 44 24 2F 32 C6 44 24 30 2E C6 44 24 31 64 C6 44 D$/2�D$0.�D$1d�D
000003DB 24 32 6C C6 44 24 33 6C C6 44 24 35 61 C6 44 24 $2l�D$3l�D$5a�D$
000003EB 36 6E C6 44 24 37 20 C6 44 24 38 65 C6 44 24 39 6n�D$7 �D$8e�D$9
000003FB 78 C6 44 24 3A 74 C6 44 24 3B 72 C6 44 24 3C 61 x�D$:t�D$;r�D$<a
0000040B C6 44 24 3D 20 C6 44 24 3E 6C C6 44 24 3F 6F C6 �D$= �D$>l�D$?o�
0000041B 44 24 40 6E C6 44 24 41 67 C6 44 24 42 20 C6 44 D$@n�D$Ag�D$B �D
0000042B 24 43 68 C6 44 24 44 65 C6 44 24 45 6C C6 44 24 $Ch�D$De�D$El�D$
0000043B 46 6C C6 44 24 47 6F C6 44 24 48 20 C6 44 24 29 Fl�D$Go�D$H �D$)
0000044B 00 C6 44 24 49 77 C6 44 24 4A 6F C6 44 24 4B 72 .�D$Iw�D$Jo�D$Kr
0000045B C6 44 24 4C 6C C6 44 24 4D 64 C6 44 24 4E 21 C6 �D$Ll�D$Md�D$N!�
0000046B 44 24 25 44 C6 44 24 26 65 C6 44 24 27 6D C6 44 D$%D�D$&e�D$'m�D
0000047B 24 28 6F E8 55 00 00 00 48 85 C0 74 4B 48 BA 58 $(o�U...H.AtKH�X
0000048B D0 CC C6 F8 E7 BF 0A 48 89 C1 E8 86 FD FF FF 48 DI�o��.H.A�.y��H
0000049B 85 C0 74 34 48 8D 4C 24 2A FF D0 48 85 C0 74 28 .At4H.L$*�DH.At(
000004AB 48 BA D9 92 FB 55 9A AC 70 E0 48 89 C1 E8 63 FD H�U.�U.�p�H.A�cy
000004BB FF FF 48 85 C0 74 11 48 8D 54 24 35 45 31 C9 4C ��H.At.H.T$5E1�L
يُترجم هذا مرة أخرى إلى الكود المصدري التالي:
pKernel32 = GET_USER_MODULE_BASE("kernel32.dll");
if (pKernel32 == nullptr)
goto cleanup;
RESOLVE_FUNCTION_POINTER(pKernel32, LoadLibraryA); // mov rdx, 0x0ABFE7F8C6CCD058 (FNV-1a hash of "LoadLibraryA" with modified offset basis)
if (LoadLibraryA == nullptr)
goto cleanup;
hUser32 = LoadLibraryA(sstrUser32.data());
if (hUser32 == nullptr)
goto cleanup;
RESOLVE_FUNCTION_POINTER(hUser32, MessageBoxA); // mov rdx, 0xE070AC9A55FB92D9 (FNV-1a hash of "MessageBoxA" with modified offset basis)
if (MessageBoxA == nullptr)
goto cleanup;
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK); // arg setup only
غني عن القول، إن هذا اكتشاف هش للغاية، ولن يكتشف سوى نموذج الكود المحدد الموضح في المثال. ومع ذلك، فإنه يسلط الضوء على أهمية استخدام تجزئات API متعددة الأشكال (polymorphic).
From C to shellcode (simple way) بقلم Print3M (2024)
relocatable بقلم Tijme Gommers (2025)
PIC Development Crash Course بقلم Raphael Mudge (2025)
scfw بقلم Petr Beneš (2026)