
TL; DR : Ich schreibe ein Kernelmodul, das Befehle von der ICMP-Nutzlast liest und sie auf dem Server ausführt, selbst wenn Ihr SSH abstürzt. Für die Ungeduldigsten ist der gesamte Code auf Github .
Vorsicht! Erfahrene C-Programmierer laufen Gefahr, in Tränen blutiger Tränen auszubrechen! Ich kann mich auch in der Terminologie irren, aber jede Kritik ist willkommen. Der Beitrag richtet sich an diejenigen, die die gröbste Vorstellung von C-Programmierung haben und sich mit den Interna von Linux befassen möchten.
In den Kommentaren zu meinem ersten Artikelerwähnte SoftEther VPN, das einige "normale" Protokolle imitieren kann, insbesondere HTTPS, ICMP und sogar DNS. Ich kann mir nur die Arbeit der ersten vorstellen, da ich mit HTTP (S) gut vertraut bin und das Tunneln über ICMP und DNS lernen musste.
Ja, ich habe im Jahr 2020 erfahren, dass Sie eine beliebige Nutzlast in ICMP-Pakete einfügen können. Aber besser spät als nie! Und da Sie etwas dagegen tun können, müssen Sie es tun. Da ich in meinem Alltag am häufigsten die Befehlszeile benutze, auch über SSH, kam mir zuerst die Idee einer ICMP-Shell in den Sinn. Und um ein komplettes Bullshit-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 ist in der Liste der Prozesse nicht sichtbar, Sie können sie in den Kernel laden und sie liegt nicht im Dateisystem. In der Liste der Überwachungsports wird nichts Verdächtiges angezeigt. In Bezug auf seine Funktionen ist dies ein vollwertiges Rootkit, aber ich hoffe, es zu ändern und als Shell des letzten Auswegs zu verwenden, wenn der Lastdurchschnitt zu hoch ist, um sich über SSH anzumelden und mindestens auszuführen
echo i > /proc/sysrq-triggerum den Zugriff ohne Neustart wiederherzustellen.
Wir nehmen einen Texteditor, grundlegende Programmierkenntnisse in Python und C, Google und eine virtuelle Maschine , die Sie gerne unter die Lupe nehmen, wenn alles kaputt geht (optional - lokale VirtualBox / KVM / etc) und los geht's!
Client-Teil
Es schien mir, dass ich für die Client-Seite ein Skript für 80 Zeilen schreiben müsste, aber es gab nette Leute, die die ganze Arbeit für mich erledigten . Der Code erwies sich als überraschend einfach und passt 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 akzeptiert zwei Argumente, eine Adresse und eine Nutzlast. Vor dem Senden wird der Nutzlast ein Schlüssel vorangestellt
run:. Wir benötigen ihn, um Pakete mit einer zufälligen Nutzlast auszuschließen.
Der Kernel benötigt Berechtigungen, um Pakete zu erstellen, daher muss das Skript mit Superuser-Rechten ausgeführt werden. Vergessen Sie nicht, die Ausführungsberechtigung 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 eines 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 Schnüffler 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 Nutzdaten im Antwortpaket ändern sich nicht.
Kernelmodul
Um eine virtuelle Maschine mit Debian einzubauen, benötigen Sie mindestens
makeund linux-headers-amd64der Rest wird als Abhängigkeiten verschärft. Ich werde nicht den gesamten Code im Artikel angeben, Sie können ihn auf Github klonen.
Haken einrichten
Erstens benötigen wir zwei Funktionen, um das Modul zu laden und zu entladen. Die Funktion zum Entladen ist nicht erforderlich, funktioniert dann aber
rmmodnicht. Das Modul wird erst 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 hier los:
- Es werden zwei Header-Dateien eingezogen, um das Modul selbst und den Netzfilter zu bearbeiten.
- , . , . — , :
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); - .
- , .
-
module_init()module_exit().
Jetzt müssen wir die Nutzlast extrahieren, was sich als die schwierigste Aufgabe herausstellte. Der Kernel verfügt nicht über integrierte Funktionen für die Arbeit mit Nutzdaten. Sie können nur Header von Protokollen höherer Ebenen 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 == '\0')
break;
if (j == MAX_CMD_LEN) {
cmd_string[j] = '\0';
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] == '\0')
break;
}
}
schedule_work(&my_work);
return NF_ACCEPT;
}
Was ist los:
- Ich musste zusätzliche Header-Dateien einfügen, diesmal um IP- und ICMP-Header zu bearbeiten.
- Gibt die maximale Länge einer Zeichenfolge an :
#define MAX_CMD_LEN 1976. Warum genau das? Weil der Compiler auf den Großen schwört! Sie sagten mir bereits, dass ich mich mit dem Stapel und dem Haufen befassen muss, eines Tages werde ich dies definitiv tun und vielleicht sogar den Code korrigieren. Legt sofort eine Zeichenfolge fest, auf der das Team basiert :char cmd_string[MAX_CMD_LEN];. Es sollte in allen Funktionen sichtbar sein, darauf werde ich in Absatz 9 näher eingehen. - (
struct work_struct my_work;) (DECLARE_WORK(my_work, work_handler);). , , . - , . ,
skb. , , . - , , .
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - . ICMP Echo, 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 : - . , ! - , , . . , ICMP .
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
skb, :tail = skb_tail_pointer(skb);.
, . - ,
cmd_string,run:, , , . - , :
schedule_work(&my_work);. , .schedule_work(), . . , , kernel panic. ! - , .
Diese Funktion ist am einfachsten. Sein Name wurde in angegeben
DECLARE_WORK(), der Typ und die akzeptierten Argumente sind nicht interessant. Wir nehmen die Kommandozeile und übergeben sie vollständig an die Shell. Lassen Sie ihn sich mit dem Parsen, der Suche nach Binärdateien und allem anderen selbst befassen.
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 auf ein Array von Strings
argv[]. Ich gehe davon aus, dass jeder weiß, dass Programme tatsächlich auf diese Weise ausgeführt werden und keine durchgezogene Linie mit Leerzeichen. - Umgebungsvariablen einstellen. Ich habe nur PATH mit einem minimalen Satz von Pfaden eingefügt, in der Erwartung, dass alle bereits
/binmit/usr/binund/sbinmit kombiniert wurden/usr/sbin. Andere Wege spielen in der Praxis selten eine Rolle. - , !
call_usermodehelper(). , , . , , . , (UMH_WAIT_PROC), (UMH_WAIT_EXEC) (UMH_NO_WAIT).UMH_KILLABLE, .
Das Erstellen von Kernelmodulen erfolgt über ein Kernel-Make-Framework. Es wird
makein einem speziellen Verzeichnis aufgerufen , das mit der Kernelversion verknüpft ist (hier definiert :) KERNELDIR:=/lib/modules/$(shell uname -r)/build, und der Speicherort des Moduls wird Min den Argumenten an die Variable übergeben . Die Ziele icmpshell.ko und clean verwenden dieses Framework vollständig. In obj-mgibt die Objektdatei an, die in ein Modul konvertiert wird. Die Syntax, die er main.oin icmpshell.o( icmpshell-objs = main.o) entfernt, sieht für mich nicht sehr logisch aus, aber lassen Sie es so sein.
Putten : . Laden : . Fertig, können Sie überprüfen : . Wenn eine Datei auf Ihrem Computer angezeigt wird und das Datum enthält, an dem die Anforderung gesendet wurde, haben Sie alles richtig gemacht, und ich habe alles richtig gemacht.
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
makeinsmod icmpshell.kosudo ./send.py 45.11.26.232 "date > /tmp/test"/tmp/test
Fazit
Meine ersten Erfahrungen mit Nukleartechnik waren viel einfacher als ich erwartet hatte. Auch ohne Erfahrung in der C-Entwicklung, die sich auf Compiler-Hinweise und Google-Ausgabe konzentrierte, konnte ich ein funktionierendes Modul schreiben und mich wie ein Kernel-Hacker und gleichzeitig wie ein Script-Kiddie fühlen. Außerdem ging ich zum Kernel Newbies-Kanal, wo sie mir sagten, ich solle ihn verwenden,
schedule_work()anstatt call_usermodehelper()den Haken selbst anzurufen, und beschämte mich , weil ich zu Recht einen Betrug vermutete. Hundert Codezeilen haben mich in meiner Freizeit ungefähr eine Woche Entwicklungszeit gekostet. Eine erfolgreiche Erfahrung, die meinen persönlichen Mythos über die überwältigende Komplexität der Systementwicklung zerstörte.
Wenn jemand einer Codeüberprüfung auf Github zustimmt, wäre ich dankbar. Ich bin mir ziemlich sicher, dass ich viele dumme Fehler gemacht habe, besonders im Umgang mit Saiten.
