قذيفة نووية على ICMP

قذيفة نووية على ICMP

TL؛ DR: أنا أكتب وحدة kernel التي ستقرأ الأوامر من حمولة ICMP وتنفذها على الخادم حتى لو تعطل SSH الخاص بك. بالنسبة للأشخاص الذين نفاد صبرهم، كل التعليمات البرمجية موجودة جيثب.

الحذر! مبرمجو C ذوو الخبرة يخاطرون بالبكاء من الدم! وربما أكون مخطئا في المصطلحات، ولكن أي انتقاد هو موضع ترحيب. هذا المنشور مخصص لأولئك الذين لديهم فكرة تقريبية جدًا عن برمجة C ويريدون إلقاء نظرة على الدواخل الداخلية لنظام Linux.

في التعليقات على حسابي الأول مقالة ذكرت SoftEther VPN، والتي يمكنها محاكاة بعض البروتوكولات "العادية"، وخاصة HTTPS وICMP وحتى DNS. أستطيع أن أتخيل أن أولهما فقط يعمل، لأنني على دراية بـ HTTP(S)، وكان علي أن أتعلم الاتصال النفقي عبر ICMP وDNS.

قذيفة نووية على ICMP

نعم، تعلمت في عام 2020 أنه يمكنك إدراج حمولة عشوائية في حزم ICMP. لكن أن تأتي متأخرا أفضل من ألا تأتي أبدا! وبما أنه يمكن فعل شيء حيال ذلك، فلا بد من القيام به. نظرًا لأنني غالبًا ما أستخدم سطر الأوامر في حياتي اليومية، بما في ذلك عبر SSH، فقد خطرت في ذهني أولاً فكرة غلاف ICMP. ومن أجل تجميع لعبة Bullshield Bingo كاملة، قررت أن أكتبها كوحدة Linux بلغة ليس لدي سوى فكرة تقريبية عنها. لن تكون هذه الصدفة مرئية في قائمة العمليات، يمكنك تحميلها في النواة ولن تكون موجودة في نظام الملفات، ولن ترى أي شيء مشبوه في قائمة منافذ الاستماع. من حيث إمكانياته، فهو عبارة عن برنامج rootkit كامل، لكنني آمل تحسينه واستخدامه كواجهة أخير عندما يكون متوسط ​​التحميل مرتفعًا جدًا بحيث لا يمكن تسجيل الدخول عبر SSH وتنفيذه على الأقل echo i > /proc/sysrq-triggerلاستعادة الوصول دون إعادة التشغيل.

نحن نأخذ محرر نصوص، ومهارات البرمجة الأساسية في Python وC، وGoogle و افتراضي والتي لا تمانع في وضعها تحت السكين إذا انكسر كل شيء (اختياري - VirtualBox المحلي/KVM/إلخ) ودعنا نذهب!

جزء العميل

بدا لي أنه بالنسبة لجزء العميل، كان علي أن أكتب نصًا يتكون من حوالي 80 سطرًا، ولكن كان هناك أشخاص طيبون فعلوا ذلك من أجلي كل العمل. تبين أن الكود بسيط بشكل غير متوقع، حيث أنه يتلاءم مع 10 أسطر مهمة:

import sys
from scapy.all import sr1, IP, ICMP

if len(sys.argv) < 3:
    print('Usage: {} IP "command"'.format(sys.argv[0]))
    exit(0)

p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
    p.show()

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

تتطلب النواة امتيازات لصياغة الحزم، لذلك يجب تشغيل البرنامج النصي كمستخدم متميز. لا تنس منح أذونات التنفيذ وتثبيت scapy نفسه. لدى دبيان حزمة تسمى python3-scapy. الآن يمكنك التحقق من كيفية عمل كل شيء.

تشغيل وإخراج الأمر
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Hello, world!"
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
options
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hello, world!

وهذا ما يبدو عليه في الشم
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
Frame 1: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0xd603 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1] [Response time: 19.094 ms] Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

^C2 packets captured

لا تتغير الحمولة في حزمة الاستجابة.

وحدة النواة

لبناء جهاز دبيان الظاهري ستحتاج على الأقل make и linux-headers-amd64، والباقي سيأتي في شكل تبعيات. لن أقدم الكود بأكمله في المقالة، يمكنك استنساخه على Github.

إعداد هوك

في البداية، نحتاج إلى وظيفتين لتحميل الوحدة وتفريغها. وظيفة التفريغ غير مطلوبة، ولكن بعد ذلك rmmod لن يعمل لن يتم تفريغ الوحدة إلا عند إيقاف تشغيلها.

#include <linux/module.h>
#include <linux/netfilter_ipv4.h>

static struct nf_hook_ops nfho;

static int __init startup(void)
{
  nfho.hook = icmp_cmd_executor;
  nfho.hooknum = NF_INET_PRE_ROUTING;
  nfho.pf = PF_INET;
  nfho.priority = NF_IP_PRI_FIRST;
  nf_register_net_hook(&init_net, &nfho);
  return 0;
}

static void __exit cleanup(void)
{
  nf_unregister_net_hook(&init_net, &nfho);
}

MODULE_LICENSE("GPL");
module_init(startup);
module_exit(cleanup);

ما الذي يحدث هنا:

  1. يتم سحب ملفين رأسيين لمعالجة الوحدة نفسها ومرشح netfilter.
  2. تتم جميع العمليات من خلال netfilter، يمكنك وضع الخطافات فيه. للقيام بذلك، تحتاج إلى إعلان البنية التي سيتم تكوين الخطاف فيها. أهم شيء هو تحديد الوظيفة التي سيتم تنفيذها كخطاف: nfho.hook = icmp_cmd_executor; سأصل إلى الوظيفة نفسها لاحقًا.
    ثم قمت بتعيين وقت المعالجة للحزمة: NF_INET_PRE_ROUTING يحدد معالجة الحزمة عند ظهورها لأول مرة في النواة. ممكن استخدامه NF_INET_POST_ROUTING لمعالجة الحزمة عند خروجها من النواة.
    لقد قمت بتعيين الفلتر على IPv4: nfho.pf = PF_INET;.
    أعطي خطافتي الأولوية القصوى: nfho.priority = NF_IP_PRI_FIRST;
    وقمت بتسجيل بنية البيانات باعتبارها الخطاف الفعلي: nf_register_net_hook(&init_net, &nfho);
  3. الوظيفة النهائية تزيل الخطاف.
  4. تمت الإشارة إلى الترخيص بوضوح حتى لا يشتكي المترجم.
  5. وظائف module_init() и module_exit() قم بتعيين وظائف أخرى لتهيئة الوحدة وإنهائها.

استرداد الحمولة

الآن نحن بحاجة إلى استخراج الحمولة، وتبين أن هذه هي المهمة الأكثر صعوبة. لا تحتوي النواة على وظائف مدمجة للعمل مع الحمولات، بل يمكنك فقط تحليل رؤوس البروتوكولات ذات المستوى الأعلى.

#include <linux/ip.h>
#include <linux/icmp.h>

#define MAX_CMD_LEN 1976

char cmd_string[MAX_CMD_LEN];

struct work_struct my_work;

DECLARE_WORK(my_work, work_handler);

static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  struct iphdr *iph;
  struct icmphdr *icmph;

  unsigned char *user_data;
  unsigned char *tail;
  unsigned char *i;
  int j = 0;

  iph = ip_hdr(skb);
  icmph = icmp_hdr(skb);

  if (iph->protocol != IPPROTO_ICMP) {
    return NF_ACCEPT;
  }
  if (icmph->type != ICMP_ECHO) {
    return NF_ACCEPT;
  }

  user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
  tail = skb_tail_pointer(skb);

  j = 0;
  for (i = user_data; i != tail; ++i) {
    char c = *(char *)i;

    cmd_string[j] = c;

    j++;

    if (c == '')
      break;

    if (j == MAX_CMD_LEN) {
      cmd_string[j] = '';
      break;
    }

  }

  if (strncmp(cmd_string, "run:", 4) != 0) {
    return NF_ACCEPT;
  } else {
    for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {
      cmd_string[j] = cmd_string[j+4];
      if (cmd_string[j] == '')
	break;
    }
  }

  schedule_work(&my_work);

  return NF_ACCEPT;
}

ماذا يحدث:

  1. اضطررت إلى تضمين ملفات رأس إضافية، هذه المرة لمعالجة رؤوس IP وICMP.
  2. قمت بتعيين الحد الأقصى لطول الخط: #define MAX_CMD_LEN 1976. لماذا هذا بالضبط؟ لأن المترجم يشكو من ذلك! لقد اقترحوا علي بالفعل أنني بحاجة إلى فهم المكدس والكومة، وفي يوم من الأيام سأفعل ذلك بالتأكيد وربما أصحح الكود. قمت على الفور بتعيين السطر الذي سيحتوي على الأمر: char cmd_string[MAX_CMD_LEN];. وينبغي أن يكون مرئيا في جميع الوظائف، وسأتحدث عن هذا بمزيد من التفصيل في الفقرة 9.
  3. الآن نحن بحاجة إلى التهيئة (struct work_struct my_work;) هيكلتها وربطها بوظيفة أخرى (DECLARE_WORK(my_work, work_handler);). سأتحدث أيضًا عن سبب ضرورة ذلك في الفقرة التاسعة.
  4. الآن أعلن عن وظيفة ستكون بمثابة خطاف. يتم تحديد النوع والوسائط المقبولة بواسطة netfilter، الذي يهمنا فقط skb. هذا عبارة عن مخزن مؤقت للمأخذ، وهو عبارة عن بنية بيانات أساسية تحتوي على كافة المعلومات المتوفرة حول الحزمة.
  5. لكي تعمل الدالة، ستحتاج إلى بنيتين والعديد من المتغيرات، بما في ذلك مكرران.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. يمكننا أن نبدأ بالمنطق. لكي تعمل الوحدة، ليست هناك حاجة إلى حزم أخرى غير ICMP Echo، لذلك نقوم بتحليل المخزن المؤقت باستخدام وظائف مدمجة ونتخلص من جميع الحزم غير ICMP وغير Echo. يعود NF_ACCEPT يعني قبول الطرد، ولكن يمكنك أيضًا إسقاط الطرود عن طريق إرجاعها NF_DROP.
      iph = ip_hdr(skb);
      icmph = icmp_hdr(skb);
    
      if (iph->protocol != IPPROTO_ICMP) {
        return NF_ACCEPT;
      }
      if (icmph->type != ICMP_ECHO) {
        return NF_ACCEPT;
      }

    لم أختبر ما سيحدث دون التحقق من رؤوس IP. يخبرني الحد الأدنى من معرفتي بلغة C أنه بدون فحوصات إضافية، لا بد أن يحدث شيء فظيع. سأكون سعيدًا إذا أثنتني عن هذا!

  7. الآن بعد أن أصبحت الحزمة من النوع الدقيق الذي تحتاجه، يمكنك استخراج البيانات. بدون وظيفة مدمجة، عليك أولاً الحصول على مؤشر لبداية الحمولة. يتم ذلك في مكان واحد، تحتاج إلى أخذ المؤشر إلى بداية رأس ICMP ونقله إلى حجم هذا الرأس. كل شيء يستخدم الهيكل icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    يجب أن تتطابق نهاية الرأس مع نهاية الحمولة skbلذلك نحصل عليها باستخدام الوسائل النووية من البنية المقابلة: tail = skb_tail_pointer(skb);.

    قذيفة نووية على ICMP

    لقد سرقت الصورة من هنا، يمكنك قراءة المزيد عن المخزن المؤقت للمأخذ.

  8. بمجرد حصولك على مؤشرات إلى البداية والنهاية، يمكنك نسخ البيانات إلى سلسلة cmd_string، تحقق من وجود البادئة run: وإما أن تتخلص من الحزمة إذا كانت مفقودة، أو تعيد كتابة السطر مرة أخرى، مع إزالة هذه البادئة.
  9. هذا كل شيء، الآن يمكنك الاتصال بمعالج آخر: schedule_work(&my_work);. نظرًا لأنه لن يكون من الممكن تمرير معلمة لمثل هذه المكالمة، فيجب أن يكون السطر الذي يحتوي على الأمر عالميًا. schedule_work() سيضع الوظيفة المرتبطة بالبنية التي تم تمريرها في قائمة الانتظار العامة لبرنامج جدولة المهام ويكتمل، مما يسمح لك بعدم الانتظار حتى يكتمل الأمر. يعد ذلك ضروريًا لأن الخطاف يجب أن يكون سريعًا جدًا. وإلا فإن خيارك هو ألا يبدأ أي شيء وإلا ستصاب بذعر النواة. التأخير مثل الموت!
  10. هذا كل شيء، يمكنك قبول الطرد مع الإرجاع المقابل.

استدعاء برنامج في مساحة المستخدمين

هذه الوظيفة هي الأكثر مفهومة. وقد ورد اسمها في DECLARE_WORK()، النوع والوسائط المقبولة ليست مثيرة للاهتمام. نأخذ السطر الذي يحتوي على الأمر ونمرره بالكامل إلى الصدفة. دعه يتعامل مع التحليل والبحث عن الثنائيات وكل شيء آخر.

static void work_handler(struct work_struct * work)
{
  static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};
  static char *envp[] = {"PATH=/bin:/sbin", NULL};

  call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}

  1. قم بتعيين الوسائط إلى مجموعة من السلاسل argv[]. سأفترض أن الجميع يعلم أن البرامج يتم تنفيذها بهذه الطريقة، وليس كخط متصل بمسافات.
  2. تعيين متغيرات البيئة. لقد قمت بإدراج PATH فقط مع الحد الأدنى من المسارات، على أمل أن تكون جميعها مدمجة بالفعل /bin с /usr/bin и /sbin с /usr/sbin. نادراً ما تكون المسارات الأخرى ذات أهمية في الممارسة العملية.
  3. تم، دعونا نفعل ذلك! وظيفة النواة call_usermodehelper() يقبل الدخول. المسار إلى الثنائي، مجموعة من الوسائط، مجموعة من متغيرات البيئة. وهنا أفترض أيضًا أن الجميع يفهم معنى تمرير المسار إلى الملف القابل للتنفيذ كوسيطة منفصلة، ​​ولكن يمكنك أن تسأل. تحدد الوسيطة الأخيرة ما إذا كان يجب الانتظار حتى تكتمل العملية (UMH_WAIT_PROC)،بدء العملية (UMH_WAIT_EXEC) أو لا تنتظر على الإطلاق (UMH_NO_WAIT). هل هناك المزيد UMH_KILLABLE، لم أطلع على الأمر.

جمعية

يتم تنفيذ تجميع وحدات النواة من خلال إطار عمل النواة. مُسَمًّى make داخل دليل خاص مرتبط بإصدار النواة (محدد هنا: KERNELDIR:=/lib/modules/$(shell uname -r)/build)، ويتم تمرير موقع الوحدة إلى المتغير M في الحجج. يستخدم icmpshell.ko والأهداف النظيفة هذا الإطار بالكامل. في obj-m يشير إلى ملف الكائن الذي سيتم تحويله إلى وحدة نمطية. بناء الجملة الذي يعيد main.o в icmpshell.o (icmpshell-objs = main.o) لا يبدو منطقيًا جدًا بالنسبة لي، ولكن فليكن.

KERNELDIR:=/lib/modules/$(shell uname -r)/build

obj-m = icmpshell.o
icmpshell-objs = main.o

all: icmpshell.ko

icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules

clean:
make -C $(KERNELDIR) M=$(PWD) clean

نجمعها: make. تحميل: insmod icmpshell.ko. تم، يمكنك التحقق من: sudo ./send.py 45.11.26.232 "date > /tmp/test". إذا كان لديك ملف على جهازك /tmp/test ويحتوي على تاريخ إرسال الطلب، مما يعني أنك فعلت كل شيء بشكل صحيح وأنا فعلت كل شيء بشكل صحيح.

اختتام

كانت تجربتي الأولى مع التطوير النووي أسهل بكثير مما توقعت. حتى بدون خبرة في التطوير في لغة C، مع التركيز على تلميحات المترجم ونتائج Google، تمكنت من كتابة وحدة عمل وأشعر وكأنني متسلل نواة، وفي نفس الوقت طفل نصي. بالإضافة إلى ذلك، ذهبت إلى قناة Kernel Newbies، حيث قيل لي أن أستخدمها schedule_work() بدلا من الاتصال call_usermodehelper() داخل الخطاف نفسه وفضحه، للاشتباه بحق في عملية احتيال. كلفني مائة سطر من التعليمات البرمجية حوالي أسبوع من التطوير في وقت فراغي. تجربة ناجحة دمرت أسطورتي الشخصية حول التعقيد الهائل لتطوير النظام.

إذا وافق شخص ما على إجراء مراجعة التعليمات البرمجية على جيثب، سأكون ممتنًا. أنا متأكد من أنني ارتكبت الكثير من الأخطاء الغبية، خاصة عند العمل مع الخيوط.

قذيفة نووية على ICMP

المصدر: www.habr.com

إضافة تعليق