Kernschale über ICMP

Kernschale über ICMP

TL;DR: Ich schreibe ein Kernel-Modul, das ICMP-Payload-Befehle liest und diese auf dem Server ausführt, selbst wenn SSH ausgefallen ist. Für die Ungeduldigen ist der gesamte Code auf github.

Vorsicht! Erfahrene C-Programmierer könnten blutige Tränen vergießen! Ich könnte in der Terminologie falsch liegen, aber jede Kritik ist willkommen. Der Beitrag richtet sich an diejenigen, die eine vage Vorstellung vom Programmieren in C haben und einen Blick in die Tiefen von Linux werfen möchten.

In den Kommentaren zu meinem ersten Artikel wurde SoftEther VPN erwähnt, das in der Lage ist, sich als einige "gewöhnliche" Protokolle auszugeben, insbesondere HTTPS, ICMP und sogar DNS. Ich kann mir nur die Funktionsweise des ersten vorstellen, da ich gut mit HTTP(S) vertraut bin, während ich das Tunneln über ICMP und DNS lernen musste.

Kernschale über ICMP

Ja, 2020 habe ich erfahren, dass man beliebigen Payload in ICMP-Pakete einfügen kann. Besser spät als nie! Und wenn man damit etwas anfangen kann, sollte man es auch tun. Da ich im Alltag meistens die Befehlszeile benutze, auch über SSH, kam mir die Idee eines ICMP-Shells als erstes. Um das volle Bullshit-Bingo zu vervollständigen, entschloss ich mich, es als Linux-Modul in einer Sprache zu schreiben, von der ich nur eine vage Vorstellung habe. Eine solche Shell wird nicht in der Prozessliste sichtbar sein, kann ins Kernel geladen werden und wird nicht im Dateisystem liegen, sodass ihr nichts Verdächtiges in der Liste der offenen Ports sehen werdet. Funktional gesehen ist es ein vollständiges Rootkit, aber ich hoffe, es weiterzuentwickeln und als letzte Hoffnung zu verwenden, wenn die Last zu hoch ist, um per SSH reinzukommen und mindestens echo i > /proc/sysrq-trigger, um den Zugang ohne Neustart wiederherzustellen.

Nehmen wir einen Texteditor, grundlegende Programmierkenntnisse in Python und C, Google und eine virtuelle Maschine , die ich ungern opfern würde, wenn alles kaputtgeht (optional - lokaler VirtualBox/KVM/etc.) und los geht's!

Der Clientteil

Ich dachte, dass ich für den Client-Teil ein Skript von etwa 80 Zeilen schreiben müsste, aber es fanden sich freundliche Menschen, die die gesamte Arbeit für mich erledigten. die gesamte Arbeit. Der Code stellte sich als unerwartet einfach heraus und passt in 10 bedeutende Zeilen:

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()

Das Skript akzeptiert zwei Argumente, die Adresse und den Payload. Vor der Sendung wird der Payload mit dem Schlüssel run:, wir benötigen ihn, um Pakete mit zufälliger Nutzlast auszuschließen.

Der Kernel benötigt Privilegien, um Pakete zu erstellen, daher muss das Skript mit Superuser-Rechten ausgeführt werden. Vergessen Sie nicht, die Ausführungsrechte zu setzen und scapy zu installieren. In Debian gibt es ein Paket namens python3-scapy. Jetzt können wir überprüfen, wie das alles funktioniert.

Ausführung und Ausgabe des Befehls
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Hallo, Welt!"
Emission beginnen:
.Fertig, 1 Pakete gesendet.
*
2 Pakete empfangen, 1 Antworten erhalten, verbleibende 0 Pakete
###[ 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
optionen
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hallo, Welt!

So sieht es im Sniffer aus
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Wird als Benutzer "root" und Gruppe "root" ausgeführt. Dies könnte gefährlich sein.
Aufnahme auf 'wlp1s0'
Frame 1: 59 Bytes im Netz (472 Bits), 59 Bytes erfasst (472 Bits) auf dem Interface wlp1s0, ID 0
Internetprotokoll Version 4, Quelle: 192.168.0.240, Ziel: 45.11.26.232
Internet Control Message Protocol
Typ: 8 (Echo (Ping)-Anfrage)
Code: 0
Prüfziffer: 0xd603 [korrekt]
[Prüfzifferstatus: Gut]
Identifikator (BE): 0 (0x0000)
Identifikator (LE): 0 (0x0000)
Sequenznummer (BE): 0 (0x0000)
Sequenznummer (LE): 0 (0x0000)
Daten (17 Bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hallo, Welt
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Länge: 17]

Frame 2: 59 Bytes im Netz (472 Bits), 59 Bytes erfasst (472 Bits) an der Schnittstelle wlp1s0, ID 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Typ: 0 (Echo (Ping)-Antwort)
Code: 0
Prüfziffer: 0xde03 [korrekt]
[Prüfzifferstatus: Gut]
Identifikator (BE): 0 (0x0000)
Identifikator (LE): 0 (0x0000)
Sequenznummer (BE): 0 (0x0000)
Sequenznummer (LE): 0 (0x0000)
[Anforderungsrahmen: 1]
[Antwortzeit: 19.094 ms]
Daten (17 Bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hallo, Welt
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Länge: 17]

^C2 Pakete erfasst

Die Nutzlast im Antwortpaket bleibt unverändert.

Kernel-Modul

Für die Erstellung in einer virtuellen Maschine mit Debian wird mindestens benötigt make und linux-headers-amd64, alles andere wird als Abhängigkeiten geladen. Ich werde den gesamten Code in diesem Artikel nicht angeben, Sie können ihn auf GitHub klonen.

Hook-Konfiguration

Zunächst benötigen wir zwei Funktionen, um das Modul zu laden und um es zu entladen. Die Funktion zum Entladen ist optional, aber dann rmmod lässt sich nicht ausführen, das Modul wird nur beim Ausschalten entladen.

#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);

Was hier passiert:

  1. Zwei Header-Dateien werden für die Manipulation des Moduls und des Netfilter geladen.
  2. Alle Operationen erfolgen über den Netfilter, wo Hooks eingestellt werden können. Dazu muss eine Struktur deklariert werden, in der der Hook konfiguriert wird. Am wichtigsten ist es, die Funktion anzugeben, die als Hook ausgeführt wird: nfho.hook = icmp_cmd_executor; Zu der Funktion werde ich noch kommen.
    Dann habe ich den Moment der Paketverarbeitung festgelegt: NF_INET_PRE_ROUTING gibt an, das Paket zu verarbeiten, sobald es im Kernel erscheint. Man kann verwenden NF_INET_POST_ROUTING um das Paket beim Verlassen des Kernels zu verarbeiten.
    Ich hänge den Filter an IPv4: nfho.pf = PF_INET;.
    Ich weise meinem Hook die höchste Priorität zu: nfho.priority = NF_IP_PRI_FIRST;
    Und registriere die Datensatzstruktur als Hook: nf_register_net_hook(&init_net, &nfho);
  3. In der abschließenden Funktion wird der Hook entfernt.
  4. Die Lizenz ist ausdrücklich angegeben, damit der Compiler nicht meckert.
  5. Funktionen module_init() und module_exit() definieren andere Funktionen als die initialisierende und abschließende Funktion des Moduls.

Extraktion der Nutzlast

Jetzt muss der Payload extrahiert werden, das erwies sich als die schwierigste Aufgabe. Der Kern hat keine integrierten Funktionen zur Arbeit mit dem Payload, man kann nur die Header höherstufiger Protokolle parsen.

#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;
}

Was passiert:

  1. Ich musste zusätzliche Header-Dateien einbinden, diesmal zur Manipulation mit IP- und ICMP-Headern.
  2. Ich setze die maximale Länge der Zeichenkette: #define MAX_CMD_LEN 1976. Warum genau so? Weil der Compiler sich bei einer größeren beschwert! Mir wurde bereits geraten, mich mit dem Stack und dem Heap zu befassen, das werde ich irgendwann tun und vielleicht sogar den Code anpassen. Sofort lege ich eine Zeichenkette fest, in der der Befehl gespeichert wird: char cmd_string[MAX_CMD_LEN];. Sie sollte in allen Funktionen sichtbar sein, darüber werde ich im Punkt 9 ausführlicher berichten.
  3. Jetzt muss die Struktur initialisiert werden (struct work_struct my_work;) und mit einer weiteren Funktion verbunden werden (DECLARE_WORK(my_work, work_handler);). Warum das nötig ist, werde ich ebenfalls im neunten Punkt erklären.
  4. Jetzt deklariere ich die Funktion, die der Hook sein wird. Der Typ und die entgegengenommenen Argumente werden vom Netfilter diktiert, uns interessiert nur skb. Dies ist der Socket-Puffer, eine fundamentale Datenstruktur, die alle verfügbaren Informationen über das Paket enthält.
  5. Für die Funktionsweise werden zwei Strukturen und mehrere Variablen benötigt, darunter zwei Iteratoren.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Jetzt kann mit der Logik begonnen werden. Für den Betrieb des Moduls sind keine Pakete außer ICMP Echo erforderlich, daher parsen wir den Puffer mit integrierten Funktionen und werfen alle nicht ICMP- und nicht Echo-Pakete weg. Rückgabe NF_ACCEPT bedeutet die Annahme des Pakets, aber Sie können die Pakete auch abweisen, indem Sie 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;
      }

    Ich habe nicht überprüft, was passiert, wenn die IP-Header nicht überprüft werden. Mein minimales Wissen über C sagt mir: Ohne zusätzliche Überprüfungen passiert mit Sicherheit etwas Schreckliches. Ich wäre froh, wenn Sie mich davon überzeugen könnten!

  7. Jetzt, da das Paket genau vom richtigen Typ ist, können die Daten extrahiert werden. Ohne eine integrierte Funktion muss zuerst der Zeiger auf den Anfang des Payloads erhalten werden. Dies geschieht auf indirekte Weise, indem man den Zeiger auf den Anfang des ICMP-Headers nimmt und ihn um die Größe dieses Headers verschiebt. Für alles wird die Struktur verwendet icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Das Ende des Headers muss mit dem Ende der Nutzlast in skbübereinstimmen, daher erhalten wir es mit nuklearen Mitteln aus der entsprechenden Struktur: tail = skb_tail_pointer(skb);.

    Kernschale über ICMP

    Das Bild wurde entführt von hier, Sie können mehr über den Socket-Puffer lesen.

  8. Nachdem wir die Zeiger auf den Anfang und das Ende erhalten haben, können wir die Daten in die Zeichenkette kopieren cmd_string, überprüfen, ob sie ein Präfix enthält run: und entweder das Paket wegwerfen, falls es fehlt, oder die Zeichenkette erneut überschreiben, indem wir dieses Präfix entfernen.
  9. Nun, alles klar, jetzt können wir einen weiteren Handler aufrufen: schedule_work(&my_work);. Da wir bei einem solchen Aufruf keinen Parameter übergeben können, muss die Befehlszeile global sein. schedule_work() stellt die Funktion, die mit der übergebenen Struktur verknüpft ist, in die allgemeine Warteschlange des Task-Schedulers und beendet sich, sodass wir nicht auf den Abschluss des Befehls warten müssen. Das ist nötig, da der Hook sehr schnell sein muss. Andernfalls wird bei Ihnen, je nach Wahl, entweder nichts gestartet oder Sie erhalten einen Kernel-Panic. Zögern ist der Tod!
  10. Alles klar, wir können das Paket mit dem entsprechenden Rückgabewert annehmen.

Der Programmaufruf im Userspace

Diese Funktion ist die verständlichste. Ihr Name wurde in DECLARE_WORK(), der Typ und die akzeptierten Argumente sind nicht interessant. Wir nehmen die Befehlszeile und übergeben sie vollständig an die Shell. Lassen Sie sie mit dem Parsen, dem Suchen nach Binärdateien und allem anderen umgehen.

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. Wir setzen die Argumente in ein Array von Zeichenfolgen argv[]. Ich nehme an, dass alle wissen, dass Programme tatsächlich genau so ausgeführt werden, und nicht als durchgehende Zeichenfolge mit Leerzeichen.
  2. Wir setzen die Umgebungsvariablen. Ich habe nur PATH mit einer minimalen Menge von Pfaden eingefügt, in der Annahme, dass bei allen bereits zusammengeführt ist /bin c /usr/bin und /sbin c /usr/sbin. Andere Pfade sind in der Praxis ziemlich selten von Bedeutung.
  3. Fertig, führen wir es aus! Die Kernfunktion call_usermodehelper() nimmt einen Pfad zur Binärdatei, ein Array von Argumenten und ein Array von Umgebungsvariablen entgegen. Hier nehme ich ebenfalls an, dass alle den Sinn der Übergabe des Pfades zur ausführbaren Datei als separatem Argument verstehen, aber Sie können fragen. Das letzte Argument gibt an, ob auf das Ende des Prozesses gewartet werden soll (UMH_WAIT_PROC), ob der Prozess gestartet werden soll (UMH_WAIT_EXEC) oder ob überhaupt nicht gewartet werden soll (UMH_NO_WAIT). Es gibt auch UMH_KILLABLE, ich habe mich nicht weiter damit beschäftigt.

Bau

Die Erstellung von Kernelmodulen erfolgt über das Kernel-make-Framework. Es wird aufgerufen make innerhalb eines speziellen Verzeichnisses, das an die Kernelversion gebunden ist (definiert hier: KERNELDIR:=/lib/modules/$(shell uname -r)/build), und der Standort des Moduls wird durch eine Variable in den Argumenten übergeben. In den Zielen icmpshell.ko und clean wird dieses Framework vollständig verwendet. In M obj-m wird die Objektdatei angegeben, die in ein Modul umgewandelt wird. Die Syntax, die umwandelt main.o icmpshell.o in icmpshell-objs = main.o () erscheint mir nicht sehr logisch, aber lassen wir es so.obj-m = icmpshell.o

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

all: icmpshell.ko
) erscheint mir nicht sehr logisch, aber lassen wir es so.

icmpshell.ko: main.c

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

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

. Laden: makeinsmod icmpshell.ko . Fertig, Sie können es überprüfen:sudo ./send.py 45.11.26.232 "date > /tmp/test" . Wenn auf Ihrem Computer eine Datei erscheintund darin das Datum der Anfrage sendet, haben Sie alles richtig gemacht und ich habe alles richtig gemacht. /tmp/test Meine erste Erfahrung in der Kernel-Entwicklung war viel einfacher, als ich erwartet hatte. Selbst ohne Erfahrung in C und nur mit den Hinweisen des Compilers und den Google-Suchresultaten konnte ich ein funktionierendes Modul schreiben und fühlte mich wie ein Kernel-Hacker und gleichzeitig wie ein Script-Kiddy. Außerdem bin ich in den Channel Kernel Newbies gegangen, wo man mir geraten hat,

Fazit

statt des Aufrufs schedule_work() innerhalb des Hooks zu verwenden und mich zurecht gewarnt hat, berechtigt, Scam zu vermuten. Ein paar Hundert Zeilen Code kosteten mir etwa eine Woche Entwicklung in meiner Freizeit. Eine erfolgreiche Erfahrung, die meinen persönlichen Mythos über die unüberwindbare Komplexität der Systementwicklung zerstört hat. call_usermodehelper() Wenn jemand bereit ist, eine Code-Überprüfung auf GitHub durchzuführen, wäre ich dankbar. Ich bin mir fast sicher, dass ich viele dumme Fehler gemacht habe, insbesondere im Umgang mit Strings.

🥇Kernel-Shell über ICMP | ProHoster

Kernschale über ICMP

Quelle: habr.com

60GB SSD 8Gb DDR4