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 abstürzt. Für die Ungeduldigsten: Der gesamte Code ist vorhanden github.

Vorsicht! Erfahrene C-Programmierer laufen Gefahr, in blutige Tränen auszubrechen! Vielleicht irre ich mich sogar in der Terminologie, aber jede Kritik ist willkommen. Der Beitrag richtet sich an diejenigen, die eine sehr grobe Vorstellung von der C-Programmierung haben und einen Blick in das Innere von Linux werfen möchten.

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

Atombombe über ICMP

Ja, im Jahr 2020 habe ich erfahren, dass man eine beliebige Nutzlast in ICMP-Pakete einfügen kann. Aber besser spät als nie! Und da man etwas dagegen tun kann, muss man es auch tun. Da ich in meinem täglichen Leben am häufigsten die Befehlszeile verwende, auch über SSH, kam mir zuerst die Idee einer ICMP-Shell in den Sinn. Und um ein komplettes Bullshield-Bingo zusammenzustellen, habe ich beschlossen, es als Linux-Modul in einer Sprache zu schreiben, von der ich nur eine grobe Vorstellung habe. Eine solche Shell wird in der Liste der Prozesse nicht sichtbar sein, Sie können sie in den Kernel laden und sie befindet sich nicht im Dateisystem, Sie werden in der Liste der Überwachungsports nichts Verdächtiges sehen. Was seine Fähigkeiten angeht, ist dies ein vollwertiges Rootkit, aber ich hoffe, es zu verbessern und es als letzten Ausweg zu verwenden, wenn der Lastdurchschnitt zu hoch ist, um sich über SSH anzumelden und zumindest auszuführen echo i > /proc/sysrq-triggerum den Zugriff ohne Neustart wiederherzustellen.

Wir benötigen einen Texteditor, grundlegende Programmierkenntnisse in Python und C, Google und virtuell die Sie gerne unters Messer legen, wenn alles kaputt geht (optional – lokale VirtualBox/KVM/usw.) und los geht’s!

Kundenteil

Mir kam es so vor, als müsste ich für den Kundenteil ein Skript mit etwa 80 Zeilen schreiben, aber es gab nette Leute, die das für mich erledigten all die Arbeit. Der Code erwies sich als unerwartet einfach und passte in 10 wichtige 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 benötigt 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älligen Nutzlasten auszuschließen.

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

Ausführen und Ausgeben 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 im 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: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

Die Nutzlast im Antwortpaket ändert sich nicht.

Kernelmodul

Um eine virtuelle Debian-Maschine einzubauen, benötigen Sie mindestens make и linux-headers-amd64, der Rest wird in Form von Abhängigkeiten kommen. Ich werde in dem Artikel nicht den gesamten Code bereitstellen; Sie können ihn auf Github klonen.

Hook-Setup

Zunächst benötigen wir zwei Funktionen, um das Modul zu laden und zu entladen. Die Funktion zum Entladen ist dann aber nicht erforderlich rmmod es wird nicht funktionieren; das Modul wird nur entladen, wenn es ausgeschaltet ist.

#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 denn hier los:

  1. Es werden zwei Header-Dateien abgerufen, um das Modul selbst und den Netzfilter zu manipulieren.
  2. Alle Vorgänge durchlaufen 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 werden soll: nfho.hook = icmp_cmd_executor; Zur Funktion selbst komme ich später.
    Dann stelle ich die Bearbeitungszeit für das Paket ein: NF_INET_PRE_ROUTING Gibt an, dass das 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 habe den Filter auf IPv4 eingestellt: nfho.pf = PF_INET;.
    Ich gebe meinem Haken die höchste Priorität: nfho.priority = NF_IP_PRI_FIRST;
    Und ich registriere die Datenstruktur als eigentlichen Hook: nf_register_net_hook(&init_net, &nfho);
  3. Die letzte Funktion entfernt den Hook.
  4. Damit sich der Compiler nicht beschwert, ist die Lizenz deutlich gekennzeichnet.
  5. Funktionen module_init() и module_exit() Legen Sie andere Funktionen fest, um das Modul zu initialisieren und zu beenden.

Abrufen 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 Payloads; Sie können nur 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. Ich musste zusätzliche Header-Dateien einbinden, dieses Mal um IP- und ICMP-Header zu manipulieren.
  2. Ich stelle die maximale Zeilenlänge ein: #define MAX_CMD_LEN 1976. Warum genau das? Weil sich der Compiler darüber beschwert! Sie haben mir bereits vorgeschlagen, dass ich den Stapel und den Heap verstehen muss. Eines Tages werde ich das auf jeden Fall tun und vielleicht sogar den Code korrigieren. Ich habe sofort die Zeile festgelegt, die den Befehl enthalten soll: char cmd_string[MAX_CMD_LEN];. Es sollte in allen Funktionen sichtbar sein; darauf werde ich in Absatz 9 genauer eingehen.
  3. Jetzt müssen wir initialisieren (struct work_struct my_work;) strukturieren und mit einer anderen Funktion verbinden (DECLARE_WORK(my_work, work_handler);). Ich werde im neunten Absatz auch darüber sprechen, warum dies notwendig ist.
  4. Jetzt deklariere ich eine Funktion, die ein Hook sein wird. Der Typ und die akzeptierten Argumente werden vom Netzfilter vorgegeben, uns interessiert nur skb. Dabei handelt es sich um einen Socket-Puffer, eine grundlegende Datenstruktur, die alle verfügbaren Informationen zu einem Paket enthält.
  5. Damit die Funktion funktioniert, 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 beginnen. Damit das Modul funktioniert, sind keine anderen Pakete als ICMP-Echo erforderlich. Daher analysieren wir den Puffer mithilfe integrierter Funktionen und verwerfen alle Nicht-ICMP- und Nicht-Echo-Pakete. Zurückkehren NF_ACCEPT bedeutet die Annahme des Pakets, Sie können das Paket aber auch durch Rücksendung aufgeben 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 passieren wird, ohne die IP-Header zu überprüfen. Meine minimalen C-Kenntnisse sagen mir, dass ohne zusätzliche Überprüfungen zwangsläufig etwas Schreckliches passieren wird. Ich würde mich freuen, wenn Sie mich davon abbringen würden!

  7. Da das Paket nun genau dem Typ entspricht, den Sie benötigen, können Sie die Daten extrahieren. Ohne eine eingebaute Funktion müssen Sie zunächst einen Zeiger auf den Anfang der Nutzlast erhalten. Dies geschieht an einer Stelle. Sie müssen den Zeiger auf den Anfang des ICMP-Headers setzen und ihn auf 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 in übereinstimmen skb, deshalb erhalten wir es mit nuklearen Mitteln aus der entsprechenden Struktur: tail = skb_tail_pointer(skb);.

    Atombombe über ICMP

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

  8. Sobald Sie Zeiger auf den Anfang und das Ende haben, können Sie die Daten in einen String kopieren cmd_string, prüfen Sie, ob ein Präfix vorhanden ist run: und verwerfen Sie entweder das Paket, wenn es fehlt, oder schreiben Sie die Zeile erneut und entfernen Sie dieses Präfix.
  9. Das war's, jetzt können Sie einen anderen Handler aufrufen: schedule_work(&my_work);. Da bei einem solchen Aufruf kein Parameter übergeben werden kann, 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 schließt sie ab, sodass Sie nicht auf den Abschluss des Befehls warten müssen. Dies ist notwendig, da der Haken sehr schnell sein muss. Andernfalls haben Sie die Wahl, dass nichts startet oder Sie eine Kernel-Panik bekommen. Verzögerung ist wie der Tod!
  10. Das war's, Sie können das Paket mit einer entsprechenden Rücksendung annehmen.

Aufruf eines Programms im Userspace

Diese Funktion ist am verständlichsten. Sein Name wurde angegeben DECLARE_WORK(), der Typ und die akzeptierten Argumente sind nicht interessant. Wir nehmen die Zeile mit dem Befehl und übergeben ihn vollständig an die Shell. Lassen Sie ihn sich um das Parsen, die Suche nach Binärdateien und alles andere kümmern.

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 die Argumente auf ein Array von Zeichenfolgen fest argv[]. Ich gehe davon aus, dass jeder weiß, dass Programme tatsächlich auf diese Weise ausgeführt werden und nicht als fortlaufende Zeile mit Leerzeichen.
  2. Umgebungsvariablen festlegen. Ich habe nur PATH mit einem Minimum an Pfaden eingefügt, in der Hoffnung, dass sie alle bereits kombiniert wurden /bin с /usr/bin и /sbin с /usr/sbin. Andere Wege spielen in der Praxis selten eine Rolle.
  3. Fertig, lasst es uns tun! Kernelfunktion call_usermodehelper() nimmt den Eintritt an. 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 fragen. Das letzte Argument gibt an, ob auf den Abschluss des Prozesses gewartet werden soll (UMH_WAIT_PROC), Prozessstart (UMH_WAIT_EXEC) oder gar nicht warten (UMH_NO_WAIT). Gibt es noch mehr? UMH_KILLABLE, ich habe nicht nachgeschaut.

Montage

Die Assemblierung der Kernelmodule erfolgt über das Kernel-Make-Framework. Angerufen make in einem speziellen Verzeichnis, das an die Kernel-Version gebunden ist (hier definiert: KERNELDIR:=/lib/modules/$(shell uname -r)/build), und der Speicherort des Moduls wird an die Variable übergeben M in den Argumenten. Die Ziele icmpshell.ko und clean nutzen 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) sieht für mich nicht sehr logisch aus, aber sei es so.

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

Wir sammeln: make. Wird geladen: insmod icmpshell.ko. Fertig, Sie können Folgendes ü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, an dem die Anfrage gesendet wurde, was bedeutet, dass Sie alles richtig gemacht haben und ich alles richtig gemacht habe.

Abschluss

Meine ersten Erfahrungen mit der nuklearen Entwicklung waren viel einfacher als ich erwartet hatte. Selbst ohne Erfahrung in der Entwicklung in C, wobei ich mich auf Compiler-Hinweise und Google-Ergebnisse konzentrierte, war ich in der Lage, ein funktionierendes Modul zu schreiben und mich wie ein Kernel-Hacker und gleichzeitig wie ein Skript-Kind zu fühlen. Außerdem bin ich zum Kernel Newbies-Kanal gegangen, wo mir gesagt wurde, dass ich ihn verwenden soll schedule_work() statt anzurufen call_usermodehelper() in den Haken selbst und beschämte ihn, da er zu Recht einen Betrug vermutete. Hundert Zeilen Code kosteten mich in meiner Freizeit etwa eine Woche Entwicklungszeit. Eine erfolgreiche Erfahrung, die meinen persönlichen Mythos über die überwältigende Komplexität der Systementwicklung zerstört hat.

Wenn jemand einer Codeüberprüfung auf Github zustimmt, bin ich dankbar. Ich bin mir ziemlich sicher, dass ich viele dumme Fehler gemacht habe, besonders bei der Arbeit mit Saiten.

Atombombe über ICMP

Source: habr.com

Kommentar hinzufügen