Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
MemFiles — مجموعة أدوات CobaltStrike لكتابة الملفات التي يُنتجها Beacon إلى الذاكرة بدلاً من القرص | Kitploit
أدوات/GitHubGitHub/octoberfest7/memfiles
تسريب البياناتما بعد الاستغلالالقيادة والسيطرةالفريق الأحمر
GitHuboctoberfest7/memfiles

MemFiles

مجموعة أدوات CobaltStrike لكتابة الملفات التي يُنتجها Beacon إلى الذاكرة بدلاً من القرص

عرض المستودع
47762منذ 2 سنواتتمت المراجعة من قبل Kitploit

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

MemFiles

إخلاء مسؤولية:

هذا المشروع معقد، وقد يؤدي عدم فهم كيفية عمله واختباره بشكل كافٍ إلى تعطّل Beacons وفقدان الوصول!

أنا أشجعك بشدة على قراءة جميع الوثائق حتى قسم "التفاصيل الفنية، واعتبارات التصميم، والتعليقات"!

مقدمة

MemFiles عبارة عن مجموعة أدوات (toolkit) لـ CobaltStrike تُمكّن المشغلين (Operators) من كتابة الملفات التي تُنتجها عملية Beacon في الذاكرة، بدلاً من كتابتها على قرص النظام المستهدف. تم اختباره بنجاح على Windows 7 و10 و11؛ ويجب أن تعمل إصدارات الخوادم المقابلة دون أي مشاكل. يقتصر MemFiles على Beacons ذات بنية x64.

يحقق ذلك عن طريق ربط (Hooking) عدة NtAPI مختلفة داخل NTDLL.dll وإعادة توجيه الاستدعاءات إلى تلك الواجهات البرمجية إلى دوال تم حقنها في مساحة ذاكرة عملية Beacon.

يفترض MemFiles وجود نسخة نظيفة/غير مربوطة (unhooked) من NTDLL في عملية Beacon. لا توجد أي ضمانات بشأن جدوى MemFiles في عملية Beacon حيث لا تزال خطافات (hooks) EDR موجودة. قم بإصلاح/تحديث NTDLL قبل استخدام MemFiles!

يتم تعريف دليل "خاص" غير موجود ضمن مجموعة أدوات MemFiles؛ أي ملفات تُكتب إلى هذا الدليل الخاص سيتم التقاطها بواسطة MemFiles وكتابتها في الذاكرة حيث يمكن تنزيلها لاحقاً إلى Teamserver.

MemFiles متوافق مع معظم (وليس كل) الأدوات التي تعمل داخل عملية Beacon ويمكن توجيهها لكتابة مخرجاتها إلى دليل محدد. لا يتطلب صلاحيات مرتفعة للعمل.

يتضمن ذلك:
-BOF's
-تجميعات .NET التي تُشغَّل سطرياً (inline) باستخدام شيء مثل inline-executeAssembly
-ملفات PE التي تُشغَّل سطرياً باستخدام شيء مثل Inline-Execute-PE

كل هذه متوافقة لأنها تعمل داخل عملية Beacon، حيث تم ربط NtAPI ذات الصلة.

لا يعمل MemFiles مع أشياء مثل:
-execute-assembly
-shell
-run

لا شيء من هذه متوافق لأنها جميعاً تُنشئ عمليات أخرى لم يتم ربط NtAPI الخاصة بها.

تم اختبار MemFiles بنجاح مع أدوات مثل Rubeus و SharpHound و Procdump و Powershell عندما يتم تشغيلها داخل عملية Beacon.

الإعداد

استنسخ المستودع وعدّل اختيارياً متغير hookdir المعرّف في السطر 56 في كل من /PIC/Source/NtCreateFile.c و /PIC/Source/NtOpenFile.c. هذا المتغير هو الدليل "الخاص" الذي يشير إلى MemFiles بأنه يجب أن يعترض الملف الذي يتم إنشاؤه. يتم تعيين متغير hookdir على "redteam" افتراضياً. تأكد من أن هذا المتغير ليس دليلاً حقيقياً على النظام المستهدف، وأنه نفسه في كلا الملفين!

image

شغّل الأمر 'make all' لتجميع كل من BOF's اللازمة ودوال PIC.

قم بتحميل MemFiles.cna في عميل CobaltStrike. تأكد من أن الدليل الذي يعمل منه CobaltStrike قابل للكتابة من قبل المستخدم الخاص بك؛ يقوم MemFiles بإنشاء ملف نصي هناك (memfiles.txt) لضمان توفر البيانات المطلوبة لعمل MemFiles.

يمكن تكوين MemFiles ليتم تثبيته في كل Beacon جديد يتصل بـ Teamserver؛ ويتم ذلك باستخدام عنصر القائمة MemFiles->Config. افتراضياً، لا يقوم MemFiles بالتثبيت التلقائي في Beacons الجديدة. لاحظ أن هذا إعداد عام؛ إذا كان هناك عميلان متصلان بـ Teamserver وكلاهما يحمّل MemFiles.cna، وإذا قام العميل A بتبديل إعداد "Install on beacon initial"، فسيتم تطبيق التغيير أيضاً على العميل B!

image

الأوامر

يتكون MemFiles من 4 أوامر موجهة للهدف تقوم بتشغيل BOF's وأمر داخلي واحد يتلاعب بهيكل بيانات المشروع.

موجهة للهدف:

  1. meminit
  2. memlist
  3. memfetch
  4. memclean

هيكل البيانات الداخلي:

  1. memtable

meminit

meminit مسؤول عن تثبيت MemFiles في عملية Beacon.

قائمة NtAPI التي يربطها MemFiles هي كما يلي:

  1. NtCreateFile
  2. NtWriteFile
  3. NtClose
  4. NtQueryVolumeInformationFile
  5. NtQueryInformationFile
  6. NtSetInformationFile
  7. NtOpenFile
  8. NtReadFile
  9. NtFlushBuffersFile

ينفذ meminit الإجراءات الرئيسية التالية:

  1. يرسل دالة استبدال مستقلة عن الموقع (position independent) لكل NtAPI مربوط إلى Beacon
  2. ينشئ هيكلاً في ذاكرة Beacon لاحتواء قيم مختلفة مطلوبة بواسطة MemFiles طوال دورة حياته
  3. يثبت عنوان هذا الهيكل في كل واحدة من دوال الاستبدال PIC
  4. يخصص ذاكرة ويحقن كل دالة استبدال PIC في ذاكرة عملية Beacon
  5. ينشئ ترامبولين (trampoline) لكل NtAPI مربوط
  6. يربط كل NtAPI مدرج عن طريق استبدال بعض/كل البايتات، معيداً توجيه التنفيذ إلى دالة الاستبدال PIC.

memlist

يُستخدم memlist لعرض جميع الملفات المخزنة حالياً في الذاكرة بواسطة MemFiles لـ Beacon معين.
image
يتم عرض عدة حقول، وأكثرها صلة وأهمية للمستخدم هي اسم الملف وطول البيانات المخزنة.

memfetch

يُستخدم memfetch لاسترجاع الملفات المخزنة في الذاكرة بواسطة MemFiles لـ Beacon معين.

افتراضياً، سيقوم memfetch باسترجاع أي وجميع الملفات المخزنة بواسطة MemFiles والتي تم إغلاق "مقبضها" (handle). تم اتخاذ هذا القرار التصميمي لتجنب أي مشكلات تتعلق بمحاولة تنزيل ملف لم ينتهِ برنامج/تطبيق من الكتابة إليه بعد.

هذا يعني أنه إذا فشل برنامج/تطبيق في إغلاق المقبض الذي فتحه على الملف، فلن يتم تنزيل الملف بواسطة memfetch.
يمكن التخفيف من ذلك باستخدام وسيط "force" مع memfetch، أي 'memfetch force' لاسترجاع جميع الملفات من الذاكرة بغض النظر عن حالة مقبضها.

الملفات التي يسترجعها memfetch من الذاكرة تُرسل مرة أخرى إلى Teamserver كتنزيل ويمكن مزامنتها من Teamserver إلى العميل عبر تبويب Downloads في CobaltStrike.

بمجرد تنزيل ملف بواسطة Teamserver، يتم مسحه من الذاكرة في عملية Beacon وتتم إزالة إدخاله كما يظهر عبر memlist.

memclean

memclean مسؤول عن التنظيف وإزالة MemFiles من عملية Beacon.

حالة الاستخدام القياسية لـ MemFiles تتضمن تثبيته وتركه مثبتاً طوال عمر Beacon؛ ومع ذلك، إذا أراد المرء استخدام MemFiles بالتزامن مع أداة لالتقاط واسترجاع مخرجات الملفات، ثم إلغاء تثبيت MemFiles بحيث لا تبقى آثاره في الذاكرة، يمكن استخدام memclean لإعادة عملية Beacon إلى حالتها الأصلية قبل تشغيل meminit.

يتضمن ذلك:

  1. إلغاء ربط كل NtAPI مربوط
  2. تصفير وتحرير كل ترامبولين تم إنشاؤه
  3. تصفير وتحرير كل دالة استبدال PIC محقونة
  4. تصفير وتحرير هيكل MemFiles

لاحظ أنه قبل أن ينفذ memclean هذه الإجراءات، سيقوم بتنزيل أي ملفات مخزنة في الذاكرة بواسطة MemFiles قسراً. إذا كان المرء يعتزم استخدام MemFiles مع أداة واحدة ثم إزالته، يمكنه تخطي استخدام memfetch واستخدام memclean فقط لاسترجاع الملفات وإزالة MemFiles من عملية Beacon دفعة واحدة.

memtable

يُستخدم memtable لعرض وتتبع المعلومات المتعلقة بـ Beacons التي تم تثبيت MemFiles فيها حالياً. كما يعرض معلومات التكوين العامة.

كل عميل CobaltStrike لديه memtable الخاص به؛ يبذل MemFiles جهوداً كبيرة لضمان تزامن بياناته بين جميع عملاء CobaltStrike المتصلين بحيث يمكن استخدام MemFiles من قبل جميع المشغلين في جميع Beacons. لمزيد من المعلومات حول هذا، راجع "اعتبارات التصميم والتعليقات".

image

الاستخدام

قم بتهيئة MemFiles في Beacon باستخدام أمر meminit. يمكن تكوين ذلك ليحدث تلقائياً عن طريق تبديل الخيار في قائمة MemFiles->Config.

image

مع تهيئة MemFiles، يمكنك الآن استخدام أدواتك المفضلة لكتابة الملفات في الذاكرة! الطريقة التي تفعل بها ذلك ستعتمد على الأداة المحددة؛ بعضها يسمح لك بتحديد دليل لإخراج ملفات متعددة إليه، بينما يسمح البعض الآخر بتحديد مسار مطلق لملف واحد يتم إنشاؤه بواسطة الأداة. يمكن رؤية بعض الأمثلة أدناه:

SharpHound:

هنا نحدد أن SharpHound يجب أن يخرج جميع الملفات المنتجة إلى دليل c:\redteam\ (دليل MemFiles الخاص بنا) وأنه لا يجب ضغط الملفات؛ لا يدعم MemFiles قراءة البرامج للملفات من الذاكرة، بل كتابتها فقط، لذا فإن وظيفة الضغط في SharpHound لا تعمل.

image

Rubeus:

يتم استخدام أمر "dump" مع Rubeus ونوجهه لإرسال جميع مخرجات وحدة التحكم إلى ملف (يقع في دليلنا الخاص)

image

Powershell:

في هذا المثال، يتم استخدام Inline-Execute-PE لتحميل powershell.exe في عملية Beacon وتشغيل 'Get-ADUser' لاسترجاع قائمة مستخدمي المجال. باستخدام أنبوب (pipe) و 'out-file'، يمكن كتابة البيانات في الذاكرة ثم استرجاعها.

image

عندما تريد استرجاع ملفاتك، قم بتشغيل memfetch:

image

عند الانتهاء من استخدام MemFiles و/أو لا تريد تركه مثبتاً في عملية Beacon، قم بتشغيل memclean:

image

لاحظ أنه في المثال أعلاه، كان هناك ملف لم يتم تنزيله بعد؛ يقوم memclean بتنزيل هذا الملف ومسحه من الذاكرة قبل إلغاء تثبيت MemFiles.

استعلم عن حالة وتكوين MemFiles باستخدام memtable. أثناء العمليات الطويلة، امسح الإدخالات الخاصة بـ Beacons الميتة/القديمة من memtable لتجنب الفوضى.

image

القدرات والقيود

كما تم التأكيد عليه في المقدمة، يتطلب MemFiles نسخة نظيفة من NTDLL في عملية Beacon ليعمل. هذا ضروري لأنه يقرأ البايتات الأصلية في NtFunction وينسخ بعضها إلى الترامبولين، الذي يُستخدم لاحقاً لإكمال الاستدعاءات العادية لـ NtFunction التي لا ينبغي أن يتداخل معها MemFiles. يتم التطرق إلى هذا الموضوع بمزيد من التفصيل في قسم "التفاصيل الفنية، واعتبارات التصميم، والتعليقات".

يقوم MemFiles بتخصيص أولي قدره 1048576 بايت لكل ملف؛ ومع كتابة البيانات إلى الذاكرة، يمكنه وسيقوم بتوسيع هذا التخصيص حسب الحاجة لاستيعاب ملفات أكبر.

يتم استخراج اسم الملف المخزن في هيكل MemFiles من وسيط يتم تمريره إلى دالة الاستبدال NtCreateFile. يقوم MemFiles بذلك بطريقة مبسطة إلى حد ما، عن طريق تحديد موقع الدليل "الخاص" في وسيط مسار الملف، والانتقال إلى نهايته، ثم زيادة المؤشر بمقدار 1 لمراعاة الحرف '\' الذي يفصل بين الدليل "الخاص" واسم الملف. على سبيل المثال، في المسار 'C:\users\tom\redteam\myfile.txt'، يحدد MemFiles موقع 'redteam'، ويراعي حرف الشرطة المائلة للخلف، ويختار 'myfile.txt' كاسم الملف.

لا يهتم MemFiles بأي أدلة سابقة في مسار الملف؛ فالمساران 'C:\redteam\myfile.txt' و 'c:\users\tom\appdata\local\redteam\myfile.txt' هما مساران صالحان بالتساوي من وجهة نظر MemFiles.

نظراً لكيفية تحليل MemFiles لأسماء الملفات من مسارات الملفات، تجدر الإشارة إلى أن MemFiles لا يدعم إنشاء أدلة فرعية؛ هذا يعني أن MemFiles لن يعمل بشكل صحيح مع الأدوات التي تحاول، على سبيل المثال، إنشاء c:\redteam\mynewdir\file1.txt و c:\redteam\mynewdir\file2.txt و c:\redteam\mysecondir\file3.txt، إلخ.

تم تحديد NtAPI التي يربطها MemFiles على أنها تلك المستخدمة بواسطة برامج مختلفة لعمليات الإدخال/الإخراج (I/O). كما ذُكر سابقاً، الأدوات/الإمكانات التي تم اختبارها بنجاح مع MemFiles/هذه المجموعة من NtAPI المربوطة هي: SharpHound و Rubeus و Powershell و Procdump و BOF's والبرامج العامة المكتوبة بلغة C التي تنفذ عمليات كتابة الملفات، وأمر bupload_raw في CobaltStrike (الذي يسمح للمشغل بتحديد موقع الملف البعيد). هناك بالتأكيد أدوات أخرى ستعمل مع MemFiles مباشرة؛ وأخرى ستكون غير متوافقة.

للوهلة الأولى، تبدو عملية إنشاء الملفات في Windows مباشرة: NtCreateFile->NtWriteFile->NtClose. بعد البحث في هذا المشروع وجدت سريعاً أن هناك عدداً من الواجهات البرمجية (API) الأخرى المشاركة، وللمتعة الإضافية تختلف هذه الواجهات البرمجية المشاركة بين البرامج. بعض البرامج كجزء من عملية الإدخال/الإخراج تستدعي Win32 API SetFilePointerEx، والتي بدورها تستدعي NtAPI NtSetInformationFile. البعض الآخر، مثل برامج .NET بما في ذلك SharpHound، ينتهي بهم الأمر باستدعاء NtFlushBuffersFile.

غياب سلسلة واحدة مشتركة من استدعاءات الواجهات البرمجية لإنشاء وكتابة الملفات يفتح مجالاً لعدم التوافق اعتماداً على الأداة التي يُستخدم معها MemFiles. قد يستدعي برنامج NtAPI آخر لم يقم MemFiles بربطه، مرراً المقبض المزيف الذي أنشأه MemFiles في دالة الاستبدال NtCreateFile مما يؤدي إلى خطأ "Invalid Handle" يوقف التنفيذ. تقوم برامج أخرى بتنفيذ إجراءات أكثر تعقيداً لا يستطيع MemFiles انتحالها/استبدالها بشكل كافٍ. أحد حالات عدم التوافق هذه التي تم تحديدها بالفعل هو ADExplorer.exe.

ADExplorer.exe هو ملف ثنائي موقّع من Microsoft يُستخدم لتعداد Active Directory. أثناء وقت التشغيل، يكتب ADExplorer البيانات إلى ملف الإخراج المحدد ثم يعود إليها لاحقاً للإشارة إليها قبل كتابة المخرجات النهائية إلى الملف في نهاية التنفيذ. ولأنه يستخدم ملف الإخراج كنوع من التخزين المؤقت (cache)، فيجب أن يكون قادراً على قراءة البيانات التي كتبها بالفعل إلى الملف، وهناك احتمال ضعيف جداً أن يفعل ذلك بطريقة بسيطة ويمكن التنبؤ بها.

لا يدعم MemFiles حالياً قراءة البرامج أو التطبيقات للملفات الموجودة في الذاكرة، ولكن هذا يجب أن يكون ممكناً مع مزيد من التطوير لدالة NtReadFile المخصصة وإضافة بعض المتغيرات/تتبع البيانات الإضافية إلى هيكل MemFiles.

يمثل ADExplorer تحدياً آخر في حجم الملفات التي ينتجها. في بيئات المؤسسات الكبيرة يمكن أن يتجاوز ملف الإخراج 1 جيجابايت؛ وبينما يجب أن يكون MemFiles قادراً برمجياً على التعامل مع ذلك، فهو بالتأكيد ليس مخصصاً لمثل هذه الحالات.

الأدوات غير المتوافقة

مما لا شك فيه أن المجتمع سيكتشف أدوات لا يعمل معها MemFiles بشكل صحيح؛ أشجعك على فتح Issue يوضح بالتفصيل البرنامج/الأداة غير المتوافقة والظروف التي شغّلتها فيها، أي ما إذا كانت BOF، عبر inline-executeAssembly، أو Inline-Execute-PE، إلخ. حتى أتمكن من معرفة ما إذا كان بإمكاني توسيع MemFiles وجعله يعمل.

مؤشرات الاختراق (IOC's) و AV/EDR

مؤشرات الاختراق المرتبطة بـ MemFiles تشمل على سبيل المثال لا الحصر:

تخصيص الذاكرة باستخدام VirtualAlloc
كتابة البيانات باستخدام WriteProcessMemory
تغيير حمايات الذاكرة على الذاكرة المخصصة بين RW و RX
استبدال الذاكرة داخل NTDLL.dll

AV/EDR

لم يتم تطوير MemFiles ضد EDR حقيقي أو اختباره ضده؛ Microsoft Defender هو ما كان متاحاً. ومع ذلك، أود أن أقول إن أي أداة/برنامج يشغله Beacon لإنتاج ملف من المرجح أن يتم التنبيه عليه أكثر من قيام MemFiles بالتقاط أو تخزين هذا الملف في الذاكرة. إن استبدال الذاكرة في NTDLL/ربط NtAPI يبدو لي شيئاً قد تعترض عليه بعض المنتجات، لكن ليس لدي أي دليل يدعم ذلك. بالنسبة للاستدعاءات إلى NtFunctions المربوطة التي لا تتعلق بملفات يلتقطها/يفترض أن يلتقطها MemFiles، لا يزال استدعاء النظام (syscall) يصدر من داخل مساحة عنوان NTDLL.dll، حيث تكتشف المنتجات الأمنية وتنبه على استدعاءات النظام الصادرة من خارج هذه المنطقة.

تجدر الإشارة إلى أن الملفات المخزنة في الذاكرة بواسطة MemFiles ليست مرمّزة أو مشفرة؛ يمكن إضافة هذه الميزة إذا تم تحديد حالة استخدام/مثيل حقيقي حيث يقوم AV/EDR بالتنبيه على ملف منتج في الذاكرة.

التفاصيل الفنية، واعتبارات التصميم، والتعليقات

تم تقديمي لأول مرة لمفهوم نظام الملفات في الذاكرة من خلال محاضرة في مؤتمر قبل عدة أشهر في KFiveFour's Tradecraftcon، حيث عرض متحدث (@DexterGerig) نموذج إثبات مفهوم (POC) أنشأ نظام ملفات في الذاكرة باستخدام نموذج خادم-عميل (Client-server). تمت تغطية نصف الوظائف التي تصورتها لمثل هذا المشروع في إصداراتي الرئيسية الأخيرة، Inline-Execute-PE. النصف الآخر، فكرة القدرة على التقاط الملفات التي تنتجها الأدوات وتخزينها في الذاكرة بدلاً من القرص، لم يتحقق بواسطة ذلك المشروع وبقيت إمكانية مرغوبة للغاية لأسباب واضحة.

كان MemFiles مهمة صعبة بشكل لا يصدق بالنسبة لي، حيث أنني قبل هذا المشروع قضيت وقتاً قليلاً جداً في استخدام مصحح الأخطاء (debugger)، ولم يكن لدي فهم للتجميع (assembly)، ولم أفهم ربط الواجهات البرمجية (API hooking). واجهت عدة عوائق استمرت من 10 إلى 20 ساعة خلال هذا المشروع، وتمكنت ولحسن الحظ من تجاوزها بفضل المثابرة. ورغم أن هذه ليست على الأرجح الطريقة الأكثر كفاءة للقيام بذلك، إلا أنني اكتسبت إلماماً كبيراً بمصححات الأخطاء وفهماً أكبر لكيفية عمل أجهزة الكمبيوتر داخلياً عندما يتعلق الأمر بالتجميع والسجلات (registers) والمكدس (stack) واصطلاحات الاستدعاء.

فيما يلي غوص تقني عميق في بعض التفاصيل الفنية الأكثر أهمية واعتبارات التصميم التي دخلت في MemFiles.

الملفات في Windows، وكيف يعمل MemFiles

يبدأ إنشاء الملفات في Windows بـ NtCreateFile، الذي يُعطى له مسار الملف المطلوب، وفي المقابل يقوم Windows بإنشاء ملف في ذلك الموقع ويوفر مقبضاً له. يُستخدم المقبض الذي تم إرجاعه في جميع الاستدعاءات اللاحقة التي تتضمن الملف، على سبيل المثال لـ NtWriteFile و NtClose.

عند التفكير في كيفية فصل الاستدعاءات إلى كل هذه الواجهات البرمجية بين تلك التي نريد اعتراضها والعبث بها وتلك التي نريد تركها، استقررت على البحث عن كلمة مفتاحية في استدعاء NtCreateFile. تم تحقيق ذلك عن طريق تحديد دليل فريد غير موجود كجزء من مسار الملف في استدعاء NtCreateFile. عندما يعيد الرابط (hook) توجيه التنفيذ إلى دالة الاستبدال NtCreateFile، يتم فحص مسار الملف الممرر كوسيط إلى NtCreateFile للتأكد من وجود تلك "الكلمة المفتاحية" الفريدة؛ وإذا حددها، يعرف MemFiles أن استدعاء NtCreateFile هذا يتعلق بملف يجب وضعه في الذاكرة بدلاً من القرص. عندما يحدث ذلك، يقوم MemFiles بتهيئة عدة متغيرات ويخصص أول 1 ميجابايت من الذاكرة لاستخدام الملف، ولكن الأهم من ذلك أنه يربط مقبضاً مزيفاً باسم الملف المحدد في هيكل MemFiles، ويعيد هذا المقبض المزيف إلى المتصل.

بالنسبة لجميع NtAPI الأخرى التي يربطها MemFiles، تنظر دوال NtFunction البديلة المقابلة إلى المقبض الممرر كوسيط لمعرفة ما إذا كان موجوداً في هيكل MemFiles؛ إذا كان المقبض موجوداً في هيكل MemFiles (المقابض المزيفة التي ينتجها MemFiles مزيفة بما يكفي بحيث لا ينبغي أن تتداخل أبداً مع مقبض حقيقي)، يحدد MemFiles هذا الاستدعاء باعتباره استدعاءً يتعلق بملف في الذاكرة ويتصرف وفقاً لذلك.### نظرية الربط والدوال البديلة لكي يتمكن MemFiles من كتابة ملفات إلى الذاكرة كان مقدرًا لها أن تُكتب على القرص، فإنه يحتاج إلى اعتراض استدعاءات بعض واجهات API التي تقوم بها البرامج عند محاولتها إنشاء ملف. لقد وُجد ربط API منذ زمن طويل، ويُستخدم بنشاط في العديد من منتجات EDR كجزء أساسي من وظائفها؛ حيث يتم إعادة توجيه استدعاءات بعض واجهات API إلى مساحة عنوان EDR، حيث يتم تحليل استدعاء API والمتغيرات التي تم تمريرها إليه. وإذا قرر EDR أن الاستدعاء خبيث، على سبيل المثال جزء من أداة هجوم أو سلسلة قتل، فسيمنع اكتمال الاستدعاء ويُطلق تنبيهًا. وإذا قرر EDR أن الاستدعاء حميد، فسيُصحح التنفيذ ويعيده إلى النقطة التي تم إعادة التوجيه منها ويسمح لاستدعاء API بأن يكتمل كما كان مقصودًا في الأصل. يمكن إيجاد تشبيه مبسط في القول إنك ترسل رسالة إلى صديق، ولكن قبل أن يستلمها صديقك يفتحها طرف ثالث ويقرأها، ثم يقرر ما إذا كان هناك شيء غير قانوني في الرسالة، وفي هذه الحالة لن يستلم صديقك الرسالة أبدًا ويتم إبلاغ الشرطة.

MemFiles يتبع النظرية نفسها، دون التنبيهات (أو تدخل الشرطة النظري). يتم عادةً تنفيذ ربط API على أدنى مستوى ممكن في مساحة المستخدم؛ دوال NtFunctions داخل NTDLL.dll. دعونا نلقي نظرة على NtCreateFile قبل أن يتم أي ربط:

صورة

جميع دوال NtFunctions متطابقة، باستثناء رقم syscall الذي يساوي 55 في هذا المثال. يتغير رقم syscall بين دوال NtFunctions، وتجدر الإشارة أيضًا إلى أن هذا الرقم يمكن أن يتغير بين إصدارات Windows؛ فرقم syscall الخاص بـ NtCreateFile على نظام التشغيل هذا (Windows 11) هو 55، ومع ذلك قد يكون مختلفًا على Windows 10 (وبالتأكيد على Windows 7).

من الجدير ملاحظة تعليمتي TEST و JNE. هاتان التعليمتان موجودتان لتحديد ما إذا كانت دالة NtFunction يجب أن تستخدم تعليمة syscall العادية أم تعليمة INT 2E القديمة. سأقتبس من منشور klezvirus SysWhispers is dead, long live SysWhispers!:

الجزء المثير للاهتمام الآن، تتحقق الدالة مما إذا كانت SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) مضبوطة على 1. SharedUserData هو رمز يشير إلى بنية وضع النواة KUSER_SHARED_DATA.

تُعرّف بنية KUSER_SHARED_DATA مساحة ذاكرة ثابتة (أو محددة مسبقًا) تُستخدم لمشاركة المعلومات مع برامج وضع المستخدم. تم ذلك بالطبع لجعل بعض معلومات النظام العامة جاهزة للاستهلاك بواسطة كود مساحة المستخدم دون الحاجة إلى التبديل في كل مرة بين تنفيذ وضع المستخدم ووضع النواة.

تمثل القيمة عند الفهرس 0x308 تعليمة syscall، وهي مدعومة في جميع إصدارات Windows بدءًا من 1511. كما قد تتخيل، في جميع إصدارات Windows قبل 1511، كانت الطريقة القياسية لتنفيذ syscall هي استدعاء المقاطعة int 2Eh.

...

إذا كنت تسأل نفسك لماذا لا تزال int 2Eh موجودة هنا، حتى مع أن Windows الآن أحدث بكثير من الإصدار 1511، فذلك لأن هذه التعليمات لا تزال مستخدمة. في الواقع، عند تمكين HVCI (Hypervisor-protected Code Integrity)، يتم ضبط SharedUserData[0x308] على 0، ويتم استخدام int 2Eh بدلاً من تعليمة syscall. يتم ذلك غالبًا لأسباب تتعلق بالأداء، نظرًا للطريقة التي تتم بها عملية الانتقال من Ring3 إلى Ring0 باستخدام إحدى التعليمتين أو الأخرى.

طلبت مزيدًا من التوضيح حول هذا الموضوع على تويتر، فردّ @yarden_shafir بالآتي:

صورة

باختصار، قد تكون كل تعليمة في دالة NtFunction ضرورية في مرحلة ما (ربما باستثناء NOP متعدد البايت الموجود في النهاية والذي لا يبدو أنه قابل للوصول)، وإذا قمنا بالكتابة فوق تعليمات في دالة NtFunction، فيتعين علينا التأكد من حفظها وتنفيذها في مرحلة ما قبل إصدار syscall النهائي (أو INT 2E حسب الحالة).

من الجدير بالذكر أنه قبل إصدار syscall، يتم نقل رقم syscall إلى RAX (كما يظهر كـ EAX في لقطة الشاشة). ولأننا لا نرى RAX يُدفع إلى المكدس قبل ذلك، فقد افترضت (ربما بسذاجة) أن القيمة الموجودة في RAX قبل نقل رقم syscall إليه ليست مهمة أو مطلوبة لاحقًا بعد إصدار syscall. وهذا خبر جيد، لأنه يعني أنه يمكننا استخدام مسجل RAX بحرية طالما نضمن أنه يحتوي على رقم syscall قبل إصدار syscall.

من أجل إعادة توجيه التنفيذ إلى الكود المخصص لدينا / دالة NtFunction البديلة، سنقوم بالكتابة فوق جزء من NtAPI الأصلية، ونقل عنوان دالة NtFunction البديلة إلى RAX ثم استخدام تعليمة JMP للانتقال إلى ذلك الكود:

صورة

يلزم 12 بايت لتعليمتي MOV و JMP؛ ولأننا نشوّه تعليمات أخرى عن طريق الكتابة فوق أول 12 بايت من NtAPI، فقد تم استبدال تلك التعليمات بتعليمات NOP من أجل الحفاظ على التباعد والمحاذاة الصحيحة لـ NtAPI.

عندما يستدعي البرنامج الآن NtCreateFile، سينتقل التنفيذ إلى دالة NtFunction البديلة لدينا.

الكود المخصص ودوال NtFunctions البديلة

يختلف MemFiles عن طريقة تنفيذ EDR للربط من حيث المكان الذي يتم فيه توجيه الدوال البديلة التي ربطت واجهات API للإقامة فيه. يقوم العديد من منتجات EDR بتحميل DLL خاص بها داخل العملية. يتم إعادة توجيه واجهات API المرتبطة إلى مساحة العنوان الخاصة بتلك الـ DLL المحمّلة حيث يمكن إجراء التحليل. وبما أن الهدف الأساسي من هذا المشروع كان تجنب إسقاط ملفات على القرص، فإن وضع DLL على القرص وجعل عملية Beacon لدينا تقوم بتحميله من أجل الوصول إلى دوال NtFunctions البديلة بدا وكأنه مسار سيئ. هناك POC عمره عدة سنوات متاح يسمح بتحميل ملفات DLL من الذاكرة، وهي استراتيجية قابلة للتطبيق لاحتياجاتنا، لكن المشروع غير مُصان ويبدو أن هناك عدة مشكلات فيه. بالإضافة إلى ذلك، فهو يتكون من 1200 سطر من الكود وسيكون من الصعب تحويله إلى تنسيق BOF.

كملاحظة جانبية سريعة، لا يمكن لدوال NtFunctions البديلة أن تقيم داخل BOF؛ فـ CobaltStrike يقوم بتحميل BOFs وتشغيلها ثم يمسحها من ذاكرة العملية عند الانتهاء منها. وبما أننا بحاجة إلى دالة (أو دوال) دائمة في الذاكرة يمكن استدعاؤها كلما استدعت العملية إحدى واجهات NtAPI المرتبطة، فإن BOFs لن تنجح.

الحل الذي توصّلت إليه كان الكود المستقل عن العنوان (PIC). كما يوحي الاسم، وعلى النقيض من الملفات التنفيذية العادية التي تتطلب تحميلها في مكان معين/مع أجزاء منها في علاقة معينة مع أجزاء أخرى، يمكن وضع وتشغيل كود PIC في أي مكان في الذاكرة. وهذا يفتح الباب أمام كتابة دوال NtFunctions البديلة كملفات تنفيذية PIC، وحقنها في عملية Beacon، وجعل خطافاتنا تعيد توجيه التنفيذ إليها عند إجراء استدعاءات لواجهات NTAPI المرتبطة لدينا.

قالب دوال NtFunctions بنمط PIC يأتي من مشروع Cracked5pider ShellcodeTemplate.

أحد الانحرافات الملحوظة عن المشروع الأساسي هو أن المشروع الأساسي مصمم حول إنشاء ملف exe كامل بنمط PIC؛ أي أنه يتضمن كود ASM لحفظ مؤشر المكدس، وإنشاء مساحة على المكدس، واستدعاء دالة NtFunction البديلة المعينة الموجودة داخل exe، ثم استعادة مؤشر المكدس بعد عودة تلك الدالة. تشكّل تعليمة call التي يصدرها exe بنمط PIC مشكلة، لأنها تدفع عنوان العودة الخاص بمكان إجراء الاستدعاء (في كود ASM الخاص بـ exe بنمط PIC) إلى المكدس، مما سيؤدي إلى أن تعليمة ret التي يتم مواجهتها لاحقًا تعيد التنفيذ إلى exe بنمط PIC، بدلاً من إعادته إلى المتصل الأصلي بـ NtAPI.

للتخفيف من هذه المشكلة، تم تعديل ملف ASM في مشروع ShellcodeTemplate لإزالة كود ASM المتعلق بإعداد المكدس، واستدعاء الدالة، ثم استعادة مؤشر المكدس بعد انتهاء الدالة من التنفيذ. والنتيجة هي أن الخطاف الموضوع في NtAPI ينقل التنفيذ الآن مباشرة إلى دالة NtFunction البديلة، مع بقاء المكدس والمسجلات على نفس الحالة التي كانت عليها عند استدعاء البرنامج لـ NtAPI الأصلية (مع استثناء RAX الذي يُستخدم لتعليمة JMP الخاصة بنا).

Original ShellcodeTemplate ASM:
صورة

MemFiles ASM:
صورة

كل NtAPI مربوطة لها دالة NtFunction بنمط PIC خاصة بها تحتوي على المنطق المطلوب إما:

أ. تنفيذ إجراءات خاصة بـ MemFiles مثل إنشاء مقبض مزيف، أو كتابة بيانات في الذاكرة، أو تعديل متغيرات في بنية MemFiles، إلخ
أو
ب. توجيه التنفيذ إلى ترامبولين يعيد استدعاء API إلى مساره الصحيح ويعيده إلى NTDLL حيث يمكن إصدار syscall

بعض دوال NtFunctions البديلة أكثر تعقيدًا من غيرها؛ عندما يكون استدعاء NtAPI مربوطة متعلقًا بـ MemFiles، فإن بعضها، مثل NtCreateFile و NtQueryVolumeInformationFile، تعدّل متغيرات تم تمريرها كوسائط إلى NtAPI وفقًا لتوثيق MSDN ونتائج الاختبارات وبعض التخمين/الفطرة السليمة. بينما يقوم البعض الآخر، مثل NtClose و NtReadFile، ببساطة بإرجاع STATUS_SUCCESS إلى المتصل الأصلي لتجنب خطأ "Invalid Handle" الحتمي الذي قد ينشأ خلاف ذلك عند تمرير مقبض مزيف أنشأه MemFiles.

عندما لا يكون استدعاء NtAPI مربوطة متعلقًا بـ MemFiles، نحتاج إلى توجيه التنفيذ إلى ترامبولين لإعادة الأمور إلى مسارها الصحيح:

صورة

الترامبولينات

الترامبولين مسؤول عن تنفيذ أي/جميع التعليمات التي لم يتم تنفيذها في NtAPI الأصلية بسبب ربط تلك الواجهة؛ ويشمل ذلك أي تعليمات تمت الكتابة فوقها جزئيًا أو كليًا بواسطة الخطاف الأصلي. يمكن أن يؤدي ربط واجهات API بسرعة إلى مشكلات لا نملك فيها مساحة كافية لتنفيذ جميع التعليمات التي نحتاج إليها. يمكن أن تساعد الترامبولينات في تخفيف هذه المشكلة أيضًا، حيث يمكننا تنفيذ أي عدد من الإجراءات لإعداد مسجلاتنا و/أو مكدسنا قبل القفز مرة أخرى إلى NtAPI الأصلية. يمكن رؤية ترامبولين NtCreateFile أدناه:

صورة

والأكثر وضوحًا، يمكن رؤية التعليمات الثلاث التي تمت الكتابة فوقها بواسطة خطافنا الأولي على أنها التعليمات الثلاث الأولى في الترامبولين:

MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1

كما ذُكر سابقًا، المتطلب الكبير في كل هذا التلاعب هو أن رقم syscall (55 في المثال أعلاه) يجب أن يكون موجودًا في RAX(EAX) قبل إصدار syscall. ومع ذلك، لدينا مشكلة تتمثل في أننا ما زلنا بحاجة إلى استخدام تعليمة JMP لتوجيه التنفيذ مرة أخرى إلى NtAPI الأصلية. وبينما قد يكون هناك مسجل آخر لا يحمل معلومات مهمة ويمكن الاستفادة منه للقيام بذلك، لم أجد خيارًا آمنًا باستمرار بالنظر إلى عدد واجهات NtAPI التي نقوم بربطها، وكل منها قد يستخدم المسجلات بشكل مختلف. الخيار الآمن هو الاستمرار في استخدام RAX؛ يمكننا القيام بذلك عن طريق دفع رقم syscall المخزن في RAX إلى المكدس. يمكننا بعد ذلك نقل العنوان الذي نرغب في القفز إليه داخل NtAPI الأصلية إلى RAX واستخدام تعليمة JMP للعودة إلى NTDLL:

صورة

يتضمن الخطاف المثبت في NtAPI تعليمة POP RAX، وهذا هو المكان الذي نقفز إليه باستخدام الترامبولين الخاص بنا. تنفيذ هذه التعليمات يعيد رقم syscall إلى RAX من أعلى المكدس ويُعدّنا لإصدار syscall. لاحظ أن تعليمة JNE من NtAPI الأصلية غير المربوطة لا تزال موجودة هنا؛ تعليمة TEST المقابلة، التي تضبط علم ZF وتحدد ما إذا كان سيتم تنفيذ JNE أم لا (والذي سيقفز بنا فوق syscall إلى INT 2E)، تم تنفيذها في الترامبولين. ترتيب الأمور بهذه الطريقة يتيح لنا ربط NtAPI بنجاح وإعادة توجيه التنفيذ إلى دالة NtFunction البديلة بنمط PIC، بالإضافة إلى ضمان عدم تخطي أي شيء أو فقدان أي وظيفة نتيجة للربط.

مشكلة العربة قبل الحصان

لتلخيص ما تم تناوله حتى الآن بإيجاز، عند تهيئة MemFiles يتم إنشاء بنية في ذاكرة عملية Beacon تحتوي على معلومات مهمة لوظائف Memfiles. يتم الرجوع إلى المعلومات الموجودة في هذه البنية باستمرار طوال دورة حياة MemFiles، بما في ذلك كل دالة من دوال NtFunction البديلة بنمط PIC وكذلك BOFs المستخدمة للاستعلام عن الملفات المخزنة في الذاكرة بواسطة MemFiles وجلبها. وتحقيقًا لهذه الغاية، عند إنشاء البنية، يتم إرسال عنوان الذاكرة الذي تقيم فيه البنية مرة أخرى إلى Teamserver:

صورة

بعد تخزين هذا العنوان في memtable، تقوم أوامر MemFiles اللاحقة (memlist, memfetch, memclean) بإرسال هذا العنوان كوسيطة إلى BOF بحيث يمكن تحديد موقع البنية والرجوع إليها. لكن كيف تقوم دوال NtFunction بنمط PIC بتحديد موقع البنية؟

المشكلة البارزة هي أن دوال NtFunction بنمط PIC تتطلب عنوان ذاكرة بنية MemFiles، لكنها تكون مترجمة بالفعل بحلول الوقت الذي يتم فيه إنشاء البنية. عالج تنفيذ مبكر لـ MemFiles هذه المشكلة عن طريق تقسيم تهيئة MemFiles إلى BOFs منفصلتين. الأولى كانت تنشئ البنية وترسل العنوان مرة أخرى إلى Teamserver، الذي كان عبر بعض سحر Aggressor script يقوم باستخراج العنوان، وإدراجه في ملف الكود المصدري لكل NtFunction، ثم يعيد ترجمتها إلى دالة NtFunction النهائية بنمط PIC. ثم تقوم الـ BOF الثانية بنقل دوال NtFunctions الجاهزة بنمط PIC وتنفيذ عملية الحقن والربط الفعلية لواجهات NtAPI.

إلى جانب كونه حلاً قبيحًا ويستغرق وقتًا إضافيًا، يمكن أن تنشأ مشكلات حقيقية في الحالة التي تحاول فيها عدة Beacons تهيئة MemFiles في الوقت نفسه. إذا اتصلت Beacon 2 بعنوان بنية MemFiles الخاص بها بينما تكون Beacon 1 في عملية دمج وإعادة ترجمة ملفات الكود المصدري لدوال NtFunction، فقد تصبح الأمور فوضوية.

الحل الأنيق لهذه المشكلة يتضمن إجراء تصحيح ثنائي (binary patch) على دالة NtFunction بنمط PIC، حيث يتم دمج عنوان بنية MemFiles في دالة NtFunction المترجمة ويكون قابلاً للوصول إليه أثناء وقت التشغيل. لتسهيل ذلك، يتم كتابة سلسلة نصية مؤقتة (placeholder) في كل دالة NtFunction:

صورة

يمكن رؤية هذا المتغير في الكود المترجم باستخدام أداة مثل xxd:

صورة

عند تشغيل أمر meminit، يتم إرسال كل دالة من دوال NtFunctions بنمط PIC إلى Beacon إلى جانب BOF الخاص بـ InstallHooks. هذا الـ BOF مسؤول عن إنشاء بنية MemFiles؛ وبعد أن يقوم بذلك، يستدعي دالة patchAddr على كل واحدة من دوال NtFunctions بنمط PIC. تكون patchAddr مسؤولة عن تحديد موقع سلسلة حروف A واستبدالها بالتمثيل النصي لعنوان البنية. وبالفعل، باستخدام عنوان الذاكرة من لقطة الشاشة السابقة، يصبح متغير pFileInfoStr الآن كما يلي:

char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );

يمكن بعد ذلك تحويل هذا التمثيل النصي إلى القيمة السداسية العشرية الفعلية، وهي عنوان الذاكرة الخاص بنا. تقوم BOFs بذلك ببساطة تامة باستخدام واجهة API _strtoi64:

صورة

بالطبع لا يمكن أن تكون الأمور بهذه السهولة بالنسبة لدوال NtFunctions بنمط PIC.

يوضح هذا المقتطف القصير من لغة التجميع نهاية إحدى دوال NtFunctions بنمط PIC. لاحظ تعليمة JMP RAX، وهي دالة NtFunction بنمط PIC التي تستدعي الترامبولين (بمعنى أن هذا استدعاء لم يقم MemFiles بالتلاعب به/التدخل فيه):

صورة

لأسباب غير معروفة، عندما حاولت استخدام _strtoi64 (أو أي من واجهات API المشابهة لها مثل stroull أو atoll)، تحولت تعليمة JMP RAX هذه إلى تعليمة CALL RAX. أنا متأكد من وجود سبب وجيه لذلك يتعلق بمستوى عميق من "كيف تعمل الحواسيب"، لكن هذا التغيير الذي يبدو غير مهم يكسر عددًا لا بأس به من الأشياء. بعد قضاء أكثر من 15 ساعة في محاولة إيجاد طريقة لتحويل التمثيل النصي لعنوان بنية MemFiles إلى القيمة السداسية العشرية الفعلية حتى أتمكن من استخدام القيم المخزنة بداخلها، صادفت منشور StackOverflow هذا حيث قدم أحد المعلقين روتينًا مخصصًا مصممًا لوحدات التحكم الدقيقة لتحويل سلسلة نصية إلى uint32؛ ولحسن الحظ، عمل أيضًا مع uint64 دون تعديل، والأهم من ذلك أنه حافظ على تعليمة JMP RAX لاحقًا في دالة NtFunction بنمط PIC دون تحويلها إلى CALL. المقتطف النهائي:

صورة

NtAPI القديمة مقابل NtAPI الجديدة

من أجل البساطة لم يُذكر هذا في وقت سابق، لكن تنسيق واجهات NtAPI قد تغير على مر السنين. الإصدارات الأقدم، مثل تلك المستخدمة في Windows 7، أقصر بكثير من نظيراتها الحديثة، إذ يبلغ طولها 16 بايت فقط بدلاً من 32:

صورة

يتطلب هذا تغييرات في كيفية ربط MemFiles لواجهات NtAPI وكذلك في كيفية بناء الترامبولين. لكي يتمكن MemFiles من تحديد إصدار واجهات NtAPI التي يتعامل معها، يقوم BOF الخاص بـ InstallHooks أولاً بتحديد عنوان NtAPI المعنية ثم يقرأ 32 بايت من ذلك الموقع. يؤدي التنسيق المختلف إلى وجود تعليمة syscall في أماكن مختلفة بين إصدارات NtAPI؛ ومن خلال التحقق من وجود أو عدم وجود syscall عند إزاحة بايت معينة، يتمكن MemFiles من تحديد ما إذا كان يتعامل مع تنفيذ NtAPI الحديث أو القديم والتصرف وفقًا لذلك:

صورة

بعد الربط، تبدو واجهة NtCreateFile القديمة بهذا الشكل:

صورة

والترامبولين المستخدم لإعداد المسجلات ثم القفز مرة أخرى إلى NtCreateFile:

صورة

بشكل عام، التقنية هي نفسها إلى حد كبير، لكن الأمور أكثر إحكامًا بكثير ودون مساحة كبيرة للمناورة. ومن الجدير بالذكر أنه من أجل جعل كل شيء يتناسب ويعمل بشكل صحيح، كان لا بد من نقل تعليمة syscall داخل NtCreateFile؛ فهي لا تزال موجودة في مساحة ذاكرة واجهة NtCreateFile داخل NTDLL، لكنها انتقلت من البايتين التاسع والعاشر من الواجهة إلى البايتين الرابع عشر والخامس عشر من الواجهة، حيث تم التضحية بـ NOP متعدد البايت من أجل حشر كل شيء بشكل صحيح.

العثور على واجهات NtAPI المتعلقة بالإدخال/الإخراج

كانت بعض واجهات API المتعلقة بعمليات الإدخال/الإخراج للملفات بديهية وبالتالي سهلة التحديد والربط؛ بينما كانت أخرى أكثر مراوغة بكثير، وتطلبت ساعات وساعات قضيتها في WinDbg و x64dbg وأنا أتنقل عبر لغة التجميع محاولاً تحديد واجهات API التي يتم استدعاؤها. لا بد أن هناك طريقة أكثر كفاءة لإنجاز المهمة، لكنني كنت أتعلم أثناء التقدم.كإضافة سريعة، رأيت أن أفصّل العملية التي، كما اتضح لاحقًا، كان يجب أن تكون سريعة جدًا، لتحديد NtAPI الذي كان يمنع SharpHound من العمل لمدة 20 ساعة. وبما أنه برنامج .NET، فقد أخرج SharpHound مكدس استدعاءات قبيحًا جدًا يتعلق كله بحقيقة أنه قرر أن "المقبض غير صالح". وبينما تعتبر .NET لغة صديقة للمبرمجين، فإن معظم (أو كل؟) الوظائف تُترجم وتمر في النهاية عبر Win32 API (ونتيجة لذلك عبر NtAPI) حيث يمكننا مراقبتها واعتراضها مثل أي شيء آخر.

sharphounderror

خطأ "المقبض غير صالح" كان دليلًا قاطعًا على وجود NtAPI يستخدمه SharpHound لم أكن أعترض عليه (وليس أن واحدة من دوال NtFunctions الموجودة في PIC الخاص بي لا تعمل بشكل صحيح)، لذا شرعت في محاولة معرفة ما هو. كانت التقنية التي استخدمتها مع الأدوات الأخرى حتى هذه النقطة هي أولًا تعيين نقطة توقف على NtCreateFile وتحديد الاستدعاء الذي يتعلق بدليلي "الخاص". ومن هناك كنت أتنقل عبر البرنامج خطوة بخطوة (عادةً عدة مرات لأنني كنت أضيع)، وأرى ما هي الدوال التي يستدعيها البرنامج بعد ذلك. باتباع هذه المنهجية اكتشفت أن NtQueryVolumeInformationFile وNtQueryInformationFile وNtSetInformationFile كانت تُستدعى وتحتاج إلى اعتراض.

يضيف SharpHound بعض المنعطفات الإضافية كونه ينفذ العديد من مهامه بشكل غير متزامن. هذا يجعل تتبع الخطوات الخطية التي يتخذها ملف واحد عبر استدعاءات API أكثر صعوبة بكثير لأنه توجد عدة ملفات تمر عبر العملية في الوقت نفسه. إضافة إلى ذلك، وجدت من خلال التنقل خطوة بخطوة بعد استدعاء NtCreateFile أن الخيط الذي تم فيه استدعاء NtCreateFile ينتهي في النهاية؛ استدعاءات NtWriteFile اللاحقة (وأي استدعاءات API أخرى غير معروفة هي موضوع هذا البحث) تحدث في خيط مختلف، مما يزيد من إرباك عملية البحث.

بما أنني لم أتعامل مع .NET إلا لفترة وجيزة، لم أكن بارعًا في تحليل مكدسات الاستدعاءات الناتجة عن الأخطاء، بل وأكثر صعوبة تلك التي تزداد قبحًا بفضل استخدام async. على مدار مشكلة العشرين ساعة، وفي غياب أي تقدم باستخدام استراتيجيتي السابقة، كنت أعود إليها مرارًا وتكرارًا وأفهمها شيئًا فشيئًا. في الجزء الثالث تقريبًا من الأعلى كما تفصل بينه أسطر "--- End of stack trace..."، يمكن رؤية سطر يقرأ "at Sharphound.Writers.JsonDataWriter...". أعطاني هذا مكانًا نسبيًا للبدء في كود SharpHound الفعلي، وهو مفتوح المصدر ومتاح على Github. كما يوحي الاسم، كانت دالة SharpHound تتعامل مع كتابة مخرجات JSON إلى ملف؛ كنت أعرف مسبقًا أن بياناتي لا تُكتب إلى الملف بنجاح، لذا لم يكن ذلك جديدًا. بتتبع مكدس الاستدعاءات مستوى واحدًا للأعلى، كان السطر التالي ذو الصلة هو "at System.IO.Streamwriter.". أخبرتني البادئة System.IO أن هذه دالة متأصلة في .NET، وليست دالة خاصة بـ SharpHound. بالنظر إلى الجزء العلوي جدًا من مكدس الاستدعاءات، كان السطر الذي لفت انتباهي هو "at System.IO.FileStream.FlushOSBuffer()". قررت البحث في Google عن FlushOSBuffer ومعرفة ما يمكنني العثور عليه.

قادني ذلك إلى وثائق Microsoft الخاصة بـ .NET لـ filestream.cs. وهناك وجدت تعريف FlushOSBuffer:

image

يبدو أنها تستدعي Win32 API وهو FlushFileBuffers. تعريف Win32Native.FlushFileBuffers يمكن العثور عليه بالنظر إلى وثائق Win32Native:

image

أي شخص عمل مع .NET باستخدام P/Invoke سيتعرف على هذا التنسيق. أصبح لدي الآن Win32 API أعرف أن System.IO.Filestream.FlushOSBuffer()، دالة .NET التي تمثل مشكلتي، تستدعيه. إعداد نقطة توقف على KERNEL32!FlushFileBuffers وتشغيل SharpHound أكد ذلك، وبالتنقل خطوة بخطوة رأيت بسرعة أن FlushFileBuffers تستدعي في الخلفية NtFlushBuffersFile. اعتراض هذا الـ API خفف المشكلات التي كان يواجهها SharpHound ومكّنه من العمل بنجاح، وكتابة ملفات مخرجاته إلى الذاكرة.

تنزيل الملفات من الذاكرة

جزء حاسم من هذا المشروع هو القدرة على تنزيل الملفات فعليًا إلى CobaltStrike Teamserver بمجرد وجودها في الذاكرة. ومن المتوقع أن أمر التنزيل العادي في CobaltStrike لن يعمل مع مسار ملف غير موجود فعلًا. بمعرفتي الحالية، يكمن الحل غالبًا في تطوير كود PIC البديل لـ NtReadFile لتسهيل قراءة الملفات الموجودة في الذاكرة بدلًا من كتابتها فقط. ولأنني لم أمتلك تلك المعرفة مسبقًا، كانت القدرة على استرجاع الملفات فعلًا عائقًا كبيرًا.

بالصدفة صادفت BOF كتبه EspressoCake يحتوي على دالة لفتت انتباهي:

image

بالنظر إلى الكود، يبدو أنه يستخدم خيار CALLBACK غير موثق من Beacon:

image

تمكّن هذه الدالة BOF من بدء تنزيل ملف إلى Teamserver من الهدف، وليس من العميل. هذه الإمكانية (التي علمت لاحقًا أنها كانت جهدًا مشتركًا لعدة أشخاص آخرين منهم @Cr0Eax و@EthicalChaos و@anthemtotheego) أزالت العائق الكبير الذي كان قائمًا سابقًا، إذ أصبح لدي الآن طريقة لبدء نقل ملف من النظام الهدف للملف الموجود في الذاكرة. شكر كبير لجميع المشاركين في مقتطف الكود هذا الذي أتوقع أن يكون مفيدًا في المستقبل أيضًا.

بنية بيانات MemFiles

كان أحد الجوانب الصعبة في هذا المشروع هو ضمان توفر وظيفة MemFiles لجميع عملاء CobaltStrike المتصلين بـ Team Server. تُخزَّن بيانات MemFiles في هياكل تنشئها MemFiles.cna، والتي يجب تحميلها في كل عميل يرغب في استخدام الأداة؛ ونتيجة لذلك، تعيش هذه الهياكل داخل كل عميل، وليس على Team Server. إذا كانت هذه البيانات تعيش في موقع مركزي واحد (TS)، لكان استرجاعها من كل عميل أمرًا بسيطًا وكان هذا الأمر برمته لن يكون مشكلة؛ ولو قرر فريق CobaltStrike دمج إمكانية مثل MemFiles رسميًا في CobaltStrike، فأنا واثق أن هذا هو الاتجاه الذي سيسلكونه. لكن بما أنها إضافة مجتمعية، نكتفي بما لدينا.

هناك بضعة سيناريوهات مختلفة يجب أن نقلق بشأنها عندما يتعلق الأمر بضمان أن كل عميل CobaltStrike لديه أحدث البيانات الدقيقة المتعلقة بحالة MemFiles داخل Beacons والإعدادات:

عملاء جدد يتصلون بـ TS ويحتاجون إلى memtable الحالي
حالات يتصل فيها عميل واحد فقط بـ TS ويعيد تشغيل CobaltStrike (وبالتالي يفقد memtable المخزن في ذاكرة العميل)
قيام العميل A بتغيير بيانات MemFiles يجب توصيلها إلى العميل B

تم اتباع نهج متعدد الجوانب لمعالجة هذه السيناريوهات. للتعامل مع الحالة التي يكون فيها عميل CobaltStrike واحد فقط متصلًا بـ TS (وبالتالي هو الكيان الوحيد الذي لديه بيانات memtable)، يقوم العميل في كل مرة يعدّل فيها memtable (meminit, memclean) بكتابة محتويات memtable الخاصة به إلى ملف نصي محلي موجود في دليل CobaltStrike. إذا خرج العميل/أعاد التشغيل، أو عند إعادة تحميل MemFiles.cna، سيحاول أولًا القراءة من ملف memtable.txt المحلي لملء memtable في الذاكرة.

عندما يكون عدة عملاء متصلين بـ TS وينضم عميل جديد (وفقًا لسجل الأحداث)، يقوم كل عميل بجلب قائمة بجميع المستخدمين المتصلين بـ TS ويرتبها أبجديًا. يتم اختيار العميل الأول في تلك القائمة باعتباره عميل "البث"، وبعد الانتظار لمدة 5 ثوانٍ (للسماح للعميل الجديد بالتهيئة وقراءة ملف memtable.txt المحلي الخاص به) سيرسل رسائل (إجراءات) في سجل الأحداث لكل إدخال في memtable الخاص به. جميع العملاء (باستثناء عميل البث) سيقرؤون هذه الرسائل ويحدّثون memtable الخاصة بهم بمعلومات البث؛ ويشمل ذلك تحديث الإدخالات الموجودة بالإضافة إلى إضافة أي إدخالات إضافية لا تحتوي عليها memtable الخاصة بهم.

العمليات العادية التي تتضمن MemFiles تعتمد أيضًا على إرسال رسائل في سجل الأحداث. عندما يقوم العميل A بتشغيل meminit، يتم بث رسالة تحتوي على جميع معلومات memtable ذات الصلة؛ جميع العملاء يحدّثون memtable الخاصة بهم عن طريق تحليل رسائل سجل الأحداث المبثوثة باستخدام خطاف "on Event_Action". تُجرى أيضًا تغييرات على بيانات MemFiles عندما ينتهي meminit من تنفيذ BOF الخاص به؛ يتم إبلاغ هذه التغييرات بواسطة Beacon (مثلًا، بعد تشغيل meminit، يستدعي Beacon مرة أخرى بموقع الذاكرة لهيكل pMemAddrs)، وبالتالي تكون مرئية لجميع العملاء المتصلين، الذين يحدّثون memtable الخاصة بهم باستخدام خطاف "on Beacon_Output".

هذه الجهود المنفصلة مجتمعة تجعل MemFiles قادرًا على مزامنة البيانات الحرجة بين عدة عملاء بكفاءة وموثوقية.

الشكر والتقدير

لم يكن هذا المشروع ممكنًا لولا مساهمات الأفراد والمشاريع التالية:

  1. x64-NTAPI-inline-hook بواسطة globalpolicy
  2. x64 Function Hooking by Example بواسطة Kyle Halladay
  3. ShellcodeTemplate بواسطة Cracked5pider AKA @C5pider
  4. @ilove2pwn_، بوساطة Cracked5pider
  5. DLL-Exports-Extraction-BOF بواسطة EspressoCake AKA @the_bit_diddler
  6. @anthemtotheego، بوساطة EspressoCake
  7. @Cr0Eax، بوساطة @anthemtotheego
  8. @EthicalChaos، بوساطة @anthemtotheego
  9. SysWhispers is dead, long live SysWhispers! بواسطة KlezVirus
  10. @yarden_shafir
  11. @DexterGerig
تنزيل الأداة