Πυρηνικό κέλυφος πάνω από το ICMP

Πυρηνικό κέλυφος πάνω από το ICMP

TL? DR: Γράφω μια λειτουργική μονάδα πυρήνα που θα διαβάζει εντολές από το ωφέλιμο φορτίο του ICMP και θα τις εκτελεί στον διακομιστή ακόμα κι αν το SSH σας διακοπεί. Για τους πιο ανυπόμονους, όλος ο κώδικας είναι GitHub.

Προσοχή! Οι έμπειροι προγραμματιστές C κινδυνεύουν να ξεσπάσουν σε δάκρυα αίματος! Μπορεί ακόμη και να κάνω λάθος στην ορολογία, αλλά κάθε κριτική είναι ευπρόσδεκτη. Η ανάρτηση προορίζεται για όσους έχουν μια πολύ πρόχειρη ιδέα του προγραμματισμού C και θέλουν να εξετάσουν το εσωτερικό του Linux.

Στα σχόλια στο πρώτο μου άρθρο ανέφερε το SoftEther VPN, το οποίο μπορεί να μιμηθεί ορισμένα «κανονικά» πρωτόκολλα, ιδίως HTTPS, ICMP και ακόμη και DNS. Μπορώ να φανταστώ ότι μόνο το πρώτο από αυτά λειτουργεί, καθώς είμαι πολύ εξοικειωμένος με το HTTP(S) και έπρεπε να μάθω τη δημιουργία σήραγγας μέσω ICMP και DNS.

Πυρηνικό κέλυφος πάνω από το ICMP

Ναι, το 2020 έμαθα ότι μπορείτε να εισάγετε ένα αυθαίρετο ωφέλιμο φορτίο σε πακέτα ICMP. Αλλά κάλλιο αργά παρά ποτέ! Και αφού κάτι μπορεί να γίνει γι' αυτό, τότε πρέπει να γίνει. Δεδομένου ότι στην καθημερινή μου ζωή χρησιμοποιώ συχνότερα τη γραμμή εντολών, συμπεριλαμβανομένου του SSH, η ιδέα ενός κελύφους ICMP ήρθε στο μυαλό μου πρώτα. Και για να συναρμολογήσω ένα πλήρες μπίνγκο bullshield, αποφάσισα να το γράψω ως λειτουργική μονάδα 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. Το Debian έχει ένα πακέτο που ονομάζεται 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!

Έτσι φαίνεται στο sniffer
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

Το ωφέλιμο φορτίο στο πακέτο απόκρισης δεν αλλάζει.

Μονάδα πυρήνα

Για να δημιουργήσετε μια εικονική μηχανή Debian θα χρειαστείτε τουλάχιστον 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. Δύο αρχεία κεφαλίδας τραβιούνται για να χειριστείτε την ίδια τη μονάδα και το φίλτρο δικτύου.
  2. Όλες οι λειτουργίες περνούν από ένα φίλτρο δικτύου, μπορείτε να ρυθμίσετε άγκιστρα σε αυτό. Για να το κάνετε αυτό, πρέπει να δηλώσετε τη δομή στην οποία θα διαμορφωθεί το άγκιστρο. Το πιο σημαντικό πράγμα είναι να καθορίσετε τη συνάρτηση που θα εκτελεστεί ως άγκιστρο: 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. Τώρα δηλώνω μια συνάρτηση, η οποία θα είναι ένα άγκιστρο. Ο τύπος και τα αποδεκτά επιχειρήματα υπαγορεύονται από το φίλτρο δικτύου, μας ενδιαφέρει μόνο skb. Αυτό είναι ένα buffer υποδοχής, μια θεμελιώδης δομή δεδομένων που περιέχει όλες τις διαθέσιμες πληροφορίες για ένα πακέτο.
  5. Για να λειτουργήσει η συνάρτηση, θα χρειαστείτε δύο δομές και πολλές μεταβλητές, συμπεριλαμβανομένων δύο επαναλήψεων.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Μπορούμε να ξεκινήσουμε με τη λογική. Για να λειτουργήσει η λειτουργική μονάδα, δεν χρειάζονται άλλα πακέτα εκτός από το ICMP Echo, επομένως αναλύουμε το buffer χρησιμοποιώντας ενσωματωμένες λειτουργίες και πετάμε όλα τα πακέτα που δεν είναι 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

    Η εικόνα κλάπηκε ως εκ τούτου,, μπορείτε να διαβάσετε περισσότερα για το buffer υποδοχής.

  8. Αφού έχετε δείκτες προς την αρχή και το τέλος, μπορείτε να αντιγράψετε τα δεδομένα σε μια συμβολοσειρά cmd_string, ελέγξτε το για την παρουσία προθέματος run: και, είτε απορρίψτε το πακέτο εάν λείπει, είτε ξαναγράψτε τη γραμμή, αφαιρώντας αυτό το πρόθεμα.
  9. Αυτό είναι όλο, τώρα μπορείτε να καλέσετε έναν άλλο χειριστή: schedule_work(&my_work);. Δεδομένου ότι δεν θα είναι δυνατό να μεταβιβαστεί μια παράμετρος σε μια τέτοια κλήση, η γραμμή με την εντολή πρέπει να είναι καθολική. schedule_work() θα τοποθετήσει τη συνάρτηση που σχετίζεται με τη δομή που πέρασε στη γενική ουρά του προγραμματιστή εργασιών και θα ολοκληρωθεί, επιτρέποντάς σας να μην περιμένετε να ολοκληρωθεί η εντολή. Αυτό είναι απαραίτητο γιατί το άγκιστρο πρέπει να είναι πολύ γρήγορο. Διαφορετικά, η επιλογή σας είναι ότι δεν θα ξεκινήσει τίποτα ή θα σας πιάσει πανικός πυρήνα. Η καθυστέρηση είναι σαν τον θάνατο!
  10. Αυτό ήταν, μπορείτε να αποδεχτείτε το πακέτο με αντίστοιχη επιστροφή.

Κλήση προγράμματος στο userspace

Αυτή η λειτουργία είναι η πιο κατανοητή. Το όνομά του δόθηκε 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() μέσα στο ίδιο το γάντζο και τον ντρόπιασε, υποπτευόμενος δικαίως μια απάτη. Εκατό γραμμές κώδικα μου κόστισαν περίπου μια εβδομάδα ανάπτυξης στον ελεύθερο χρόνο μου. Μια επιτυχημένη εμπειρία που κατέστρεψε τον προσωπικό μου μύθο σχετικά με τη συντριπτική πολυπλοκότητα της ανάπτυξης του συστήματος.

Εάν κάποιος συμφωνήσει να κάνει έναν έλεγχο κώδικα στο Github, θα είμαι ευγνώμων. Είμαι σίγουρος ότι έκανα πολλά ανόητα λάθη, ειδικά όταν δουλεύω με χορδές.

Πυρηνικό κέλυφος πάνω από το ICMP

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο