TL;DR: Ich schreibe ein Kernel-Modul, das Befehle aus dem ICMP-Payload lesen und diese auf dem Server ausführen kann, selbst wenn Ihr SSH abgestürzt ist. Für die Ungeduldigen ist der gesamte Code auf .
Vorsicht! Erfahrene C-Programmierer riskieren, in blutigen Tränen zu zerfließen! Ich könnte mich in der Terminologie irren, aber jede Kritik ist willkommen. Dieser Beitrag richtet sich an diejenigen, die nur 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 wurde SoftEther VPN erwähnt, das in der Lage ist, sich als einige "gewöhnliche" Protokolle, insbesondere HTTPS, ICMP und sogar DNS, zu tarnen. Ich kann mir nur die Funktionsweise des ersten vorstellen, da ich mit HTTP(S) gut vertraut bin, während ich das Tunnel über ICMP und DNS erst lernen musste.

Ja, ich habe 2020 erfahren, dass man beliebige Payloads in ICMP-Pakete einfügen kann. Besser spät als nie! Und wenn man damit etwas machen kann, dann sollte man es auch tun. Da ich in meinem Alltag am häufigsten die Eingabeaufforderung, auch über SSH, benutze, kam mir die Idee eines ICMP-Shells als Erstes in den Sinn. Und um das volle Bullshit-Bingo zu erreichen, beschloss ich, es als Linux-Modul in einer Programmiersprache zu schreiben, von der ich nur eine ungefähre Vorstellung habe. Dieses Shell wird nicht in der Prozessliste sichtbar sein, man kann es in den Kernel laden, und es wird nicht im Dateisystem liegen, sodass Sie nichts Verdächtiges in der Liste der offenen Ports sehen werden. In seinen Möglichkeiten ist es ein vollständiger Rootkit, aber ich hoffe, es weiterzuentwickeln und als letzte Rettungsshell zu verwenden, wenn die Last zu hoch ist, um sich per SSH einzuloggen und zumindest echo i > /proc/sysrq-trigger, um den Zugang ohne Neustart wiederherzustellen.
Wir nehmen einen Texteditor, grundlegende Programmierkenntnisse in Python und C, Google und etwas, das man opfern kann, falls alles kaputtgeht (optional: lokaler VirtualBox/KVM/etc.) und los geht's!
Client-Seite
Ich dachte, dass ich ein Skript mit etwa 80 Zeilen für den Client-Bereich schreiben müsste, aber es fanden sich freundliche Leute, die die Arbeit für mich erledigten. . Der Code stellte sich überraschend 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 das Payload. Vor dem Senden wird das Payload mit einem Schlüssel präfixiert run:, den wir benötigen, um Pakete mit einem zufälligen Payload 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 vergeben und scapy selbst zu installieren. In Debian gibt es ein Paket namens python3-scapy. Jetzt kann man ü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 starten:
.Übertragung von 1 Paket abgeschlossen.
*
2 Pakete empfangen, 1 Antwort erhalten, 0 Pakete verbleiben.
###[ 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 und host 45.11.26.232"
Wird als Benutzer "root" und Gruppe "root" ausgeführt. Dies könnte gefährlich sein.
Erfassung auf 'wlp1s0'
Frame 1: 59 Bytes auf dem Draht (472 Bits), 59 Bytes erfasst (472 Bits) auf dem Interface wlp1s0, ID 0
Internetprotokoll Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Typ: 8 (Echo (ping) Anfrage)
Code: 0
Prüfziffer: 0xd603 [korrekt]
[Prüfziffer Status: Gut]
Identifier (BE): 0 (0x0000)
Identifier (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:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Länge: 17]
Frame 2: 59 Bytes im Kabel (472 Bits), 59 Bytes erfasst (472 Bits) am Interface 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
Checksum: 0xde03 [korrekt]
[Prüfziffer Status: Gut]
Identifier (BE): 0 (0x0000)
Identifier (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:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Länge: 17]
^C2 Pakete erfasst
Das Payload im Antwortpaket ändert sich nicht.
Kernelmodul
Für den Aufbau in einer virtuellen Maschine mit Debian werden mindestens make und linux-headers-amd64, die restlichen Abhängigkeiten werden automatisch gezogen. Den vollständigen Code werde ich nicht in den Artikel einfügen, Sie können ihn jedoch auf GitHub klonen.
Hook-Einrichtung
Zunächst benötigen wir zwei Funktionen, um das Modul zu laden und um es zu entladen. Die Funktion zum Entladen ist nicht obligatorisch, aber dann ist es auch nicht möglich, rmmod auszuführen, das Modul wird nur beim Herunterfahren 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:
- Es werden zwei Header-Dateien für die Manipulation des Moduls und des Netfilters geladen.
- Alle Operationen laufen über den Netfilter, dort können Hooks festgelegt werden. Dazu muss eine Struktur deklariert werden, in der der Hook konfiguriert wird. Das Wichtigste ist, die Funktion anzugeben, die als Hook ausgeführt werden soll:
nfho.hook = icmp_cmd_executor;Ich werde noch zur Funktion selbst kommen.
Dann habe ich den Bearbeitungspunkt des Pakets festgelegt:NF_INET_PRE_ROUTINGzeigt an, das Paket zu bearbeiten, sobald es im Kernel ankommt. Man kann auchNF_INET_POST_ROUTINGverwenden, um das Paket beim Verlassen des Kernels zu bearbeiten.
Ich lege einen Filter für IPv4 fest:nfho.pf = PF_INET;.
Setze meinem Hook die höchste Priorität:nfho.priority = NF_IP_PRI_FIRST;
Und ich registriere die Datenstruktur als Hook:nf_register_net_hook(&init_net, &nfho); - Im Abschlussfunktionen wird der Hook entfernt.
- Die Lizenz ist ausdrücklich angegeben, damit der Compiler keine Fehler ausgibt.
- Funktionen
module_init()undmodule_exit()definieren andere Funktionen als initialisierende und beendende Funktionen des Moduls.
Payload extrahieren
Jetzt muss ich den Payload extrahieren, das stellte sich als die schwierigste Aufgabe heraus. Im Kernel gibt es keine eingebauten Funktionen zur Verarbeitung von Payloads, 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:
- Ich musste zusätzliche Header-Dateien einbinden, diesmal für die Manipulation von IP- und ICMP-Headern.
- Ich setze die maximale Länge der Zeichenkette:
#define MAX_CMD_LEN 1976. Warum gerade so? Weil der Compiler sich bei größeren Beschwert! Mir wurde bereits gesagt, dass ich mich mit dem Stack und dem Heap auseinandersetzen muss, das werde ich irgendwann sicherlich tun und vielleicht sogar den Code verbessern. Ich setze sofort die Zeichenkette, in der der Befehl liegen wird:char cmd_string[MAX_CMD_LEN];. Sie sollte in allen Funktionen sichtbar sein, dazu werde ich in Punkt 9 ausführlicher berichten. - Jetzt muss ich initialisieren (
struct work_struct my_work;) die Struktur und mit einer weiteren Funktion verknüpfen (DECLARE_WORK(my_work, work_handler);). Darüber, warum das nötig ist, werde ich auch im neunten Punkt sprechen. - Ich erkläre jetzt die Funktion, die als Hook dient. Der Typ und die akzeptierten Argumente werden durch den Netfilter diktiert, uns interessiert nur
skb. Das ist der Socket-Puffer, eine grundlegende Datenstruktur, die alle verfügbaren Informationen über das Paket enthält. - Für die Funktion 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; - Nun können wir mit der Logik beginnen. Für das Modul sind keine Pakete außer ICMP Echo erforderlich, deshalb parsen wir den Puffer mit integrierten Funktionen und werfen alle nicht-ICMP- und nicht-Echo-Pakete weg. Die Rückgabe
NF_ACCEPTbedeutet, das Paket anzunehmen, aber Sie können die Pakete auch verwerfen undNF_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 ohne die Überprüfung der IP-Header passieren wird. Mein minimales Wissen über C sagt mir: Ohne zusätzliche Überprüfungen wird definitiv etwas Schreckliches passieren. Ich wäre dankbar, wenn Sie mich davon überzeugen könnten!
- Jetzt, da das Paket den richtigen Typ hat, können die Daten extrahiert werden. Ohne die eingebaute Funktion müssen wir zuerst einen Zeiger auf den Anfang des Payloads erhalten. Dies erfolgt an einer bestimmten Stelle, indem wir den Zeiger auf den Anfang des ICMP-Headers nehmen und ihn um die Größe dieses Headers verschieben. Für alles wird eine Struktur verwendet.
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
Das Ende des Headers muss mit dem Ende der Nutzlast in übereinstimmenskb, daher erhalten wir es mit Kernelfunktionen aus der entsprechenden Struktur:tail = skb_tail_pointer(skb);.
Das Bild wurde entführt , Sie können mehr über den Socket-Puffer lesen. - Nachdem wir die Zeiger auf den Anfang und das Ende erhalten haben, können die Daten in die Zeichenfolge kopiert werden
cmd_string, sie auf das Vorhandensein eines Präfix zu überprüfenrun:und entweder das Paket verwerfen, wenn es nicht vorhanden ist, oder die Zeichenfolge erneut überschreiben, um dieses Präfix zu entfernen. - Nun, jetzt können wir einen weiteren Handler aufrufen:
schedule_work(&my_work);. Da wir in einem solchen Aufruf keinen Parameter übergeben können, sollte die Zeichenfolge mit dem Befehl global sein.schedule_work()Die Funktion wird mit der übergebenen Struktur in die gemeinsame Warteschlange des Task-Schedulers eingefügt und beendet sich, sodass man nicht auf den Abschluss des Befehls warten muss. Das ist notwendig, weil der Hook sehr schnell sein muss. Andernfalls wird entweder nichts gestartet oder es kommt zu einem Kernel-Panic. Zögern ist der Tod! - Alles klar, das Paket kann mit der entsprechenden Antwort akzeptiert werden.
Aufruf des Programms im Userspace
Diese Funktion ist die verständlichste. Ihr Name wurde festgelegt in DECLARE_WORK(), der Typ und die akzeptierten Argumente sind nicht relevant. Wir nehmen die Zeile mit dem Befehl und übergeben sie vollständig an die Shell. Lassen Sie ihn selbst das Parsen, die Suche nach Binaries und alles andere übernehmen.
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);
}- Wir setzen die Argumente in ein Array von Strings
argv[]. Ich nehme an, dass alle wissen, dass Programme tatsächlich so ausgeführt werden und nicht als durchgehender String mit Leerzeichen. - Wir setzen die Umgebungsvariablen. Ich habe nur PATH mit einer minimalen Anzahl von Pfaden eingefügt, in der Annahme, dass bei allen bereits vereinigt sind
/binmit/usr/binund/sbinmit/usr/sbin. Andere Pfade spielen in der Praxis ziemlich selten eine Rolle. - Bereit, ausführen! Kernel-Funktion
call_usermodehelper()nimmt einen Pfad zur Binärdatei, ein Array von Argumenten und ein Array von Umgebungsvariablen als Eingabe. Ich gehe auch davon aus, dass alle den Sinn verstehen, den Pfad zur ausführbaren Datei als separates Argument zu übergeben, aber das können Sie gerne fragen. Das letzte Argument gibt an, ob auf den Abschluss des Prozesses gewartet werden soll (UMH_WAIT_PROC), um den Prozess zu starten (UMH_WAIT_EXEC) oder gar nicht zu warten (UMH_NO_WAIT). Es gibt auch nochUMH_KILLABLE, mit dem habe ich mich nicht weiter beschäftigt.
Zusammenbau
Das Kompilieren von Kernelmodulen erfolgt über das Kernel-Make-Framework. Es wird make in einem speziellen Verzeichnis aufgerufen, das an die Kernelversion gebunden ist (hier definiert: KERNELDIR:=/lib/modules/$(shell uname -r)/build), und der Speicherort des Moduls wird durch eine Variable M in den Argumenten übergeben. In den Zielen icmpshell.ko und clean wird dieses Framework vollständig verwendet. In obj-m wird die Objektdatei angegeben, die in ein Modul umgewandelt werden soll. Die Syntax, die main.o in in (icmpshell.oicmpshell-objs = main.o
KERNELDIR:=/lib/modules/$(shell uname -r)/build
) für mich sieht nicht sehr logisch aus, aber lassen wir es so.
icmpshell.o
obj-m = icmpshell.o
all: icmpshell.ko
icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean makeWir bauen: . Wir laden:insmod icmpshell.ko . Fertig, jetzt können Sie überprüfen:sudo ./send.py 45.11.26.232 "date > /tmp/test" /tmp/test . Wenn auf Ihrem Rechner eine Datei erstellt wurde
Fazit
Meine erste Erfahrung in der Kernel-Entwicklung stellte sich als viel einfacher heraus, als ich erwartet hatte. Selbst ohne Erfahrung in C, indem ich mich an die Hinweise des Compilers und an die Google-Ausgabe hielt, konnte ich ein funktionierendes Modul schreiben und mich wie ein Kernel-Hacker fühlen, gleichzeitig auch wie ein Script-Kiddie. Außerdem bin ich auf den Kanal Kernel Newbies gegangen, wo man mir geraten hat, schedule_work() anstatt einen Aufruf call_usermodehelper() innerhalb des Hooks selbst zu verwenden, und jemand hat mich zurecht gewarnt und zu Recht Scam vermutet. Hundert Zeilen Code haben mich etwa eine Woche Entwicklung in meiner Freizeit gekostet. Eine gelungene Erfahrung, die meinen persönlichen Mythos über die unerträglich komplexe Systementwicklung zerstört hat.
Wenn jemand bereit ist, ein Code-Review auf GitHub durchzuführen, wäre ich dankbar. Ich bin mir fast sicher, dass ich viele dumme Fehler gemacht habe, insbesondere bei der Arbeit mit Zeichenfolgen.
Quelle: habr.com

