Encrypted C2 Loader Explained: ربط PCAP والذاكرة مع stage مخفي

🇺🇸EN🇸🇦AR

التحدي ده بدأ بملفين فقط:

  • capture.pcapng
  • DbgInfo.DMP

ماكانش فيه سياق إضافي، فكان لازم يبدأ التحقيق مباشرة من الملفين.

الخطوة العملية كانت تقسيم المشكلة لنصين.

الـ PCAP يوضح سلوك الـ implant على الشبكة، والـ minidump يوضح آثار runtime في الذاكرة. الاتنين معًا يقدموا منظورين مستقلين لنفس الحادثة.

الكتابة دي ماشية على نفس الخط من الـ initial triage لحد تحليل الـ binary في النهاية. فيها C2 traffic، واسترجاع AES key، واستخراج screenshot، والـ executable المسترجع، ثم reverse engineering اللي ربط كل السلسلة.


الـ Initial Triage

بدأت بتأكيد الملفين المتاحين:

ls -lh capture.pcapng DbgInfo.DMP

وده طلع لي بالضبط اللي كنت متوقعه، ولا حاجة زيادة:

  • capture.pcapng
  • DbgInfo.DMP

وده كان بداية واضحة وحددت نطاق التحقيق مباشرة.

الـ plan من هنا كان بسيط كفاية:

  • أستخدم الـ PCAP علشان أعيد بناء C2 behavior
  • أستخدم الـ minidump علشان أتأكد إيه اللي كان موجود في الذاكرة
  • أستخدم الاتنين علشان أسترجع screenshot والـ uploaded binary
  • بعدها أفتح الـ binary وأشوف هو مخبي إيه

الخطة كانت بسيطة على الورق، لكن التنفيذ احتاج متابعة دقيقة.


فحص الـ PCAP

بدأت بـ packet capture لأن network traffic غالبًا يكشف شكل الهجوم بسرعة: البروتوكول، النمط، وتسلسل المهام.

أول محاولة كانت بدائية:

tshark -r capture.pcapng -q -z io,phs

وده فشل فورًا بـ Exit code 127، يعني tshark ماكانش متثبت في البيئة.

بعد التثبيت، أعدت نفس الفحوصات.

أول ما tshark بقى موجود، طلبت الـ protocol breakdown والـ HTTP view. وده كان أول شكل مفيد للـ traffic.

الـ capture كان مركز حوالين HTTP. الـ flow أظهر implant بيعمل check-in على internal service على port واحد، وبعدها يستقبل tasks ويبعت results. الـ endpoints كانت بتضم login و ping و query requests، وده متوافق مع شكل C2 API.

كمان صدّرت HTTP objects علشان أشتغل على كل artifact بشكل مباشر:

tshark -r capture.pcapng --export-objects http,http_objects

وده طلع لي مجموعة objects صغيرة، ومعاهم object أكبر شويّة لفت نظري فورًا. الـ small objects كانوا هم المهمين في البداية. واحد منهم كان فيه login response، والـ response ده كان شايل implant ID و XOR-obfuscated AES key blob.

وده كان أول clue حقيقي. مش الفلاج، ولا screenshot، ولا الـ binary. لكنه كان كفاية يثبت إن البيانات مش plain text.


key exchange هو الجزء اللي خلّى باقي الـ traffic مقروء

في المرحلة دي الـ PCAP كان بيبدو كأنه web service طبيعي، ثم يبدأ بإظهار طبقات التعمية الفعلية.

الـ login response كان فيه JSON صغير فيه implant ID و key field. الـ key ماكانش usable مباشرة. كان base64، والبايتات اللي جواه كانت XOR-obfuscated قبل ما AES يدخل أصلاً على الخط.

وده معناه إن لازم أفك الـ pre-crypto layer الأول، وبعدها أختبر هل الناتج فعلًا بيفك بيانات مفيدة.

الـ helper script الخاص بالـ implant family كان المكان المنطقي أبدأ منه. نزلته، فتحته، وشفت هو متوقع إيه علشان يعمل key recovery. بعدها ثبتّ الـ crypto dependency المطلوبة.

cd /tmp/nimplanted
cat NimPlanted.py | head -100
pip3 install pycryptodome

ده خلّى شكل الـ recovery أوضح فورًا. الـ helper كان مبني على نفس الفكرة اللي أنا كنت مستنتجها من الـ capture، وده طمّنّي شوية. معناه إنّي ماكنتش بألف شكل المشكلة من دماغي.

المنطق الفعلي للاسترجاع كان:

  • reverse XOR obfuscation على الـ key blob
  • اختبار candidate keys بطول 16 حرفًا/رقمًا
  • استخدام AES-CTR مع الـ decoded key material
  • التوقف أول ما sample مفكوك يطلع printable JSON

ما بدأتش brute-force على كل المساحة مباشرة. اختبرت range أصغر الأول لتأكيد المنهج قبل التوسيع.

الاختبار الصغير رجّع key حقيقي و decrypted sample حقيقي. الـ sample ده فك إلى JSON task قصير شبه check-in عادي، وده كان إثبات كفاية إن الـ key صح.

أول ما ده حصل، باقي الـ traffic بطل يبقى غامض.


فك كل المحادثة خلّى الـ capture transcript

بعد ما الـ key اتأكدت، بقيت أمشي على الـ exported HTTP objects وأفك encrypted JSON fields واحد واحد.

الـ pattern كان نفسه كل مرة:

  • base64 decode للـ field
  • فصل الـ IV
  • استخدام AES-CTR بالـ key المسترجع
  • decode للـ plaintext كـ JSON أو text

وده حوّل الـ PCAP من "كمية packets" إلى transcript حقيقي.

الـ decrypted traffic أظهر implant registration وتسلسل tasks. الـ operator بدأ بفحوصات هوية وبيئة أساسية قبل الانتقال لجمع artifacts.

الـ tasks كانت من النوع ده:

  • whoami
  • pwd
  • shell ipconfig
  • ps
  • getAv
  • env
  • ping إلى private IP
  • screenshot
  • upload لـ binary
  • shell whoami /all

التسلسل ده مهم لأنه بيوضح انتقال الـ operator من فحص الهوية والبيئة إلى فحص أدوات الحماية ثم جمع artifacts من الجهاز.

وكمان الـ registration record قال لي نوع الـ host ده إيه. مش هسرد كل تفاصيل الميتاداتا، لكن المهم إن الـ PCAP والـ minidump كانوا واضحين جدًا إنهم بيتكلموا عن نفس Windows host ونفس process family. وده cross-check مهم لأنه يثبت إن الاتنين مرتبطين بنفس الـ host ونفس العملية.

الـ capture كان بدأ يوريني outline للهجوم. الـ screenshot والـ binary كانوا هيكملوا التفاصيل.


استخراج الـ screenshot

الـ screenshot task كان الحاجة المنطقية اللي أدوّر عليها بعدها. الـ operator كان واضح إنه طلب من الـ implant يلقط الشاشة، فده معناه إن النتيجة المفروض تكون موجودة في الـ decrypted traffic.

الـ result object كان متعدد الطبقات: base64 ثم gzip ثم PNG، لكن الشكل ماكانش واضح من أول محاولة.

أول محاولة مني كانت سطحية. حاولت أفك gzip blob اللي كنت فاكره هو الصحيح، وطلع الخطأ ده:

gzip.BadGzipFile: Not a gzipped file (b'H4')

وده كان معناه إني كنت بمرر طبقة base64 إلى gzip بدل payload المضغوط.

فوقفت ورجعت decode بالشكل الصحيح.

الـ workflow اللي اشتغل كان:

  1. decrypt للـ task result باستخدام AES key
  2. parse للـ JSON wrapper
  3. base64 decode للـ result field
  4. base64 decode تاني
  5. gzip decompress للـ final blob
  6. حفظ الناتج كـ PNG

النقطة الفارقة كانت وجود second base64 layer. أول ما اتفك صح، ظهر gzip header وتحول الـ payload إلى صورة فعلية.

حفظت screenshot من الـ PCAP باسم ss_pcap.png وراجعته بصريًا. جودة OCR كانت ضعيفة في البداية، وده متوقع لأن النص داخل لقطات C2 بيكون غالبًا مضغوط ومشوّه.

جربت tesseract رغم كده:

tesseract ss_pcap.png stdout --psm 6

وده ماطلعليش النص المطلوب مباشرة. قصّيت الجزء المهم من الصورة، حاولت تاني، وكمان فحصت الصورة بـ zsteg علشان أستبعد وجود محتوى مخفي واضح.

المهم هنا ماكانش OCR output نفسه. المهم إن الصورة كانت واضح فيها أول fragment ظاهر من الفلاج داخل editor window. المشكلة الأساسية كانت جودة الصورة والـ rendering، وده صعّب قراءة الـ fragment مباشرة.

فاخدت screenshot بجدية، خلّيته readable، واسترجعت أول جزء من الفلاج من الصورة نفسها.

الخطوة دي كانت مهمة لأنها قدمت دليل مباشر وقابل للتحقق على أول fragment من الفلاج.


الـ minidump أكد إن الصورة ماكنتش artifact لمرة واحدة

بعد ما جبت screenshot من الـ PCAP، حبيت أعرف إذا كان نفس artifact البصري ده موجود في الذاكرة كمان. ده هيقوّي القضية بشكل واضح. لو الـ screenshot ظهر في الـ minidump، يبقى أنا مش بتعامل مع network artifact عابر. أنا ببص على trace حيّ من runtime لنفس الحاجة.

بدأت parser بسيط، وهو على طول خلاني أدفع ثمن التفاؤل.

pip3 install minidump -q
python3 -c '
from minidump.minidumpfile import MinidumpFile
mf = MinidumpFile.parse("DbgInfo.DMP")
print(mf.get_streams())
'

وده فشل بـ AttributeError لأن الـ method اللي استخدمتها ماكانتِش موجودة في نسخة المكتبة المستخدمة. راجعت الـ object وعدلت طريقة التحليل.

فوقفت خطوة لورا، بصيت على الـ object بـ dir()، وراجعت system info مباشرة. وده طلع لي الجزء المهم أولًا: الـ dump كان جاي من Windows 11 22H2، build 22621. وده كان متوافق مع باقي الـ runtime evidence وادّى أساس أوضح لتحليل الـ memory dump.

بعدها دوّرت في الـ dump على الـ marker الواضح لـ gzip-wrapped base64 blob، وهو prefix H4sI. وده كان الـ clue اللي محتاجه.

الـ minidump فيه screenshot artifact كمان، لكن المرة دي كان أكبر وموجود كـ embedded base64 gzip blob. فكّيته، وفكّيت الضغط، وحفظته كصورة screenshot منفصلة.

وده كان مفيد لسببين.

أولًا، أثبت إن الـ screenshot ماكانش مجرد network artifact. كان موجود في الذاكرة أيضًا.

ثانيًا، النسخة الأعلى دقة أكدت إن حالة الـ desktop كانت متوافقة مع نسخة الـ PCAP. نفس البيئة، نفس عائلة الـ artifact، ونفس الـ session. وده وفر cross-check مستقل بين أدلة الشبكة والذاكرة.

ففي المرحلة دي كان عندي:

  • screenshot من الـ PCAP
  • screenshot من الـ minidump
  • أول fragment من الفلاج ظاهر في الصورة
  • تأكيد إن الـ screenshot artifact موجود في runtime memory

وده كان كفاية أتحرك من غير تخمين.


الـ large upload object طلع هو الـ executable المسترجع

الـ traffic كان فيه كمان object كبير متuploaded، وده الـ decrypted C2 transcript خلّاه مستحيل يتجاهل. الـ upload ده كان الحاجة اللي فتحتها بعد كده.

الـ encrypted object فك إلى compressed data. بعد فك الضغط، البايتات تحولت إلى Windows PE file. وده كان الـ recovered executable، وسميته rev.exe للتوضيح.

الـ chain هنا كان بسيط لو بصيت له بعدين:

  • AES-decrypt للـ upload object
  • decompress للـ result
  • التأكد إن الناتج يبدأ بـ MZ
  • حفظه كـ PE file

وده عطاني الـ binary اللي محتاجه لجزء reverse engineering من التحقيق.

هنا الـ PCAP بقى مش مجرد network evidence، بقى مصدر stage تاني. الـ operator push file على الجهاز، والـ capture حفظه. لا تخمين، لا assumptions، بس executable حقيقي ينفع يكون جزء من session.

الـ binary ده كان الجسر بين جانب الشبكة وجانب الكود في التحقيق. وهو الجزء اللي حوّل القضية من forensic case إلى reverse engineering case.

ومن هنا بدأت مرحلة تحليل الـ binary.


التحليل الأولي للـ PE

أول pass على الـ executable المسترجع كان triage المعتاد.

file rev.exe
sha256sum rev.exe
objdump -h rev.exe
objdump -p rev.exe | grep -A300 'The Import Tables'
strings -a -n 5 rev.exe > strings.txt
strings -el -n 5 rev.exe > strings_unicode.txt

الملف كان PE32+ x64 console executable مع ست sections. مفيش في ده حاجة exotic لوحدها. والـ imports كمان ماكانوش واضحين بشكل كافٍ من أول نظرة.

بعدها الـ string scan والـ import table بدأوا يضيّقوا الدائرة.

الـ binary كان بيستورد شوية Windows APIs عامة، لكن اللي يهم فعلًا كان:

  • VirtualProtect
  • GetProcAddress
  • LoadLibraryExW
  • IsDebuggerPresent

التركيبة دي إشارة معروفة لسلوك loader: تغيير صلاحيات الذاكرة، حل API بشكل ديناميكي، وفحوصات debugger.

وكان فيه كمان runtime و console APIs عامة، مع exception-handling و thread-related calls. المهم هنا كان اللي مش موجود. ماكانش فيه network-stack import واضح في الجدول. وده رجّح احتمال إن الـ binary إما ما بيتعاملش مع الشبكة مباشرة، أو بيحل الـ APIs المهمة بشكل dynamic وقت التشغيل.

وده كان النوع التاني.


تحليل نقطة دخول الـ loader

حمّلت الـ executable في radare2 مع تطبيق relocations وعملت analysis.

r2 -e bin.relocs.apply=true -A rev.exe

الـ entry point ماكانش الـ payload. كان wrapper صغير بينادي function تانية وبعدها يقفز.

وده نوع الحاجات اللي بتلاحظها لما الـ binary بيحاول يخلي أول شوية instructions يبانوا أقل أهمية مما هم عليه. الشغل الحقيقي كان أعمق جوه.

فتحت main والـ pattern بقى أوضح.

الـ function عملت ثلاث حاجات مهمة:

  1. عملت check على timing condition
  2. نادت VirtualProtect على memory region ثابتة
  3. شغّلت decryption loop على 687 bytes وبعدها نادت على نفس المنطقة

العنوان المهم كان هو اللي فضل يظهر حوالين decryption logic. الحجم كان 0x2af، يعني 687 bytes. والرقم ده كان مهم لأنه طابق بالضبط الـ data buffer اللي استخرجته بعد كده.

الـ decryption loop كان الـ clue الكبير. ماكانش XOR عشوائي. كان باين عليه stream cipher setup، مع 256-byte state array، swaps متكررة، و keystream XOR على الـ buffer. RC4 كان fit واضح.

في المرحلة دي بقى واضح إن الـ binary عبارة عن loader بيحتوي على encrypted stage.

وده وصف أحسن بكتير تقدر تقولّه.


استخراج الـ encrypted stage

بعد ما فهمت إن الكود بيفك ويشغّل buffer في الذاكرة، استخرجت بالضبط الـ region دي من الـ executable.

r2 -q -c 'p8 687 @ 0x1400014b0' rev.exe | tr -d '\n' | xxd -r -p > stage.enc
wc -c stage.enc
xxd -l 64 stage.enc

الحجم طابق تمامًا: الكود أشار إلى 687 bytes، والـ buffer المستخرج كان 687 bytes. ده أكد إن المنطقة المستخرجة هي نفسها الـ buffer المرتبط بالـ decryption logic.

الـ raw bytes ماكانوش readable code لسه، وده كان بالضبط اللي متوقعه. ماكانوش قابلين للقراءة ككود بشكل مباشر، وده كان متوافق مع وجود decryption routine داخل الـ loader.

فبنيت decryption routine في Python من جديد، واستخدمت static key string اللي في الـ binary، وشغلت الـ buffer من خلالها.

الـ output كان recognizably x64 shellcode فورًا.

أول bytes كانت بالشكل ده:

fc 48 83 e4 f0 e8 ...

وده متوافق مع بداية شائعة في Windows shellcode قبل الدخول في مرحلة dynamic API resolution. في النقطة دي بقى واضح إن الـ buffer هو الـ executable stage المقصود.

السلوك الناتج كان متوافقًا مع الـ loader logic اللي ظهر في الـ disassembly.

فك stage ثانية في الذاكرة، غيّر memory protection، وشغّل الناتج.

وده نمط loader الكلاسيكي: إخفاء المرحلة الفعلية حتى وقت التشغيل ثم القفز إلى الذاكرة المفكوكة.


تحليل الـ shellcode المفكوك

في المرحلة دي كان دور الـ loader واضح. الـ encrypted 687-byte region اتستخرجت، وRC4 اتعاد بناؤه، والـ decrypted bytes اتأكد إنها x64 shellcode.

بعدها جبت كل الـ strings المقروءة من الـ shellcode.

strings -a -n 4 stage.dec

الـ output ده احتوى strings مهمة، منها wininet، والـ private IP الظاهر في C2 workflow، ومسار فيه slashes شكله request أو resource name. ده ربط الـ shellcode مباشرة بسلوك الشبكة في الـ PCAP.

لكن كان فيه حاجة تانية هناك. String مقروءة ماكانتوش مجرد network hint، ولا config stub، لكن الجزء المفقود الثاني من الفلاج.

الجزء الثاني كان موجودًا بوضوح داخل الـ decrypted output. بعد تأكيده، السلسلة بقت واضحة: screenshot أعطى الجزء الأول، وshellcode أعطى الجزء الثاني.

وده كان النقطة اللي بطلت فيها أتعامل مع الـ shellcode كـ "output interesting"، وبدأت أتعامل معاه كأنه الإجابة.


التحقق من مسار الـ screenshot

حتى بعد ما مسار الـ shellcode بقى واضح، استمريت في فحص minidump screenshot. نسخة الذاكرة كانت أعلى دقة، فكان منطقي أتحقق إذا كانت تحمل artifacts إضافية.

الـ dump كان بالفعل مديّني أسباب أخلي بالي هناك:

  • artifact screenshot.png كان موجود في الذاكرة
  • أبعاد الـ screenshot كانت 2560x1440
  • الـ desktop كان فيه PDF icon باسم EULA ، وده بدا كإشارة تستحق الفحص

فكملت أجرب على الصورة برضه.

convert screenshot.png -resize 50% -contrast-stretch 0x5% -colorspace Gray ocr_prep.png
tesseract ocr_prep.png stdout --psm 6

الـ output ماكانش مفيد.

جربت كمان عدة crop وthreshold passes إضافية.

convert ss_pcap.png -colorspace Gray -negate -threshold 50% bin.png
tesseract bin.png stdout --psm 6

وده طلع نص أقل قابلية للاستخدام.

كمان جربت الفحوصات السهلة الواضحة على screenshot الـ dump.

steghide extract -sf screenshot.png -p ""
exiftool screenshot.png

وده مرّ بالمنظر اللي تتوقعه لما package مش موجود أو الملف نفسه مش مخبّي حاجة. steghide ماكانش متاح، وexiftool ماكانش موجود، والـ metadata نفسها كانت شبه فارغة لما شيكت عليها بـ Pillow.

python3 -c '
from PIL import Image
img = Image.open("screenshot.png")
print(img.info)
print(img.size)
'

وده أكد إن الملف صورة عادية، من غير محتوى إضافي ظهر من خلال الفحوصات دي.

كمان دوّرت في الـ dump مباشرة على strings بشكل flag.

strings DbgInfo.DMP | grep -oE 'HTB\{[^}]+\}'
strings DbgInfo.DMP | grep -oE 'flag\{[^}]+\}'
strings DbgInfo.DMP | grep -i htb | head

ده ماطلعليش الـ flag النهائي. ظهر noisy data وembedded base64 artifact، لكن مافيش fragment مباشر من الجزء المتبقي.

فـ screenshot بتاع الـ dump فضل دليلًا داعمًا، لكنه ماكانش مصدر الـ fragment الأخير.

الـ fragment الأخير اتاخد من الـ decrypted shellcode.


ليه الأدلة كانت كافية

في النقطة دي الـ chain كان بقى نظيف جدًا.

الـ screenshot من الـ PCAP جاب أول fragment، والـ decrypted shellcode جاب fragment التاني. Screenshot الـ minidump كان runtime artifact مهم لدعم العلاقة بين أدلة الشبكة والذاكرة، لكنه ماكانش المصدر المباشر للـ fragment الأخير.

أول ما الـ shellcode evidence اتوافق مع باقي الأدلة، بقى الـ fragment الأخير مدعوم بشكل كافٍ.

الـ fragment التاني جه مباشرة من الـ decrypted stage اللي الـ loader حطه في الذاكرة وشغله، وده كان متوافق مع أدلة الشبكة والـ executable والـ runtime memory.

وده قفل مسار التحقيق لأن الأدلة اتفقت بين الـ network capture والـ binary والـ memory dump.


التجميع النهائي

فالتحقيق انتهى والـ fragmentين جايين من طبقتين مختلفتين من نفس الحادثة:

  • أول fragment من الـ screenshot
  • تاني fragment من الـ decrypted shellcode

وده السبب في ربط الـ screenshot والـ dump والـ binary في نفس المسار: كلهم أجزاء من نفس الـ chain، والـ flag ما اكتملش إلا بربط الأدلة بدل تحليل كل artifact بمعزل عن الباقي.

النتيجة النهائية كانت الـ flag الكامل متجمع من الـ fragmentين، ومدعوم بالـ artifacts نفسها.


الخلاصة

كان التعامل مع الـ solve كسلسلة transformations أوضح من البحث عن flag string منفصل.

الـ PCAP بقى decrypted JSON.

الـ JSON بقى screenshot و binary upload.

الـ binary بقى PE executable.

الـ PE بقى encrypted 687-byte stage.

الـ stage بقى shellcode.

والـ shellcode سلّم fragment الناقص.

الـ dead ends في البداية كانت مهمة لأنها منعت استنتاجات مبكرة وخلّت التحقيق يتقدم خطوة بخطوة.

كل layer كان يبدو في البداية كأنه problem مستقل: C2 traffic، memory noise، executable عام، وshellcode bytes.

عمليًا، كل ده كان أجزاء من نفس السلسلة.

اللحظة اللي loader فيها بيفك stage في الذاكرة ويقفز عليها، الـ artifact بيتحوّل من ملف ساكن إلى سلوك قابل للإثبات.