TL; DRJag skriver en kärnmodul som läser kommandon från ICMP-nyttolasten och kör dem på servern även om din SSH är nere. För de mest otåliga är hela koden påslagen. .
Varning! Erfarna C-programmerare riskerar att gråta blodiga tårar! Jag kan ha fel även i terminologin, men all kritik är välkommen. Inlägget är avsett för dem som har den minsta aning om C-programmering och vill undersöka Linux interna funktioner.
I kommentarerna till min första nämnde SoftEther VPN, som kan härma vissa "vanliga" protokoll, särskilt HTTPS, ICMP och till och med DNS. Jag kan bara föreställa mig att det första av dem fungerar, eftersom jag är väl bekant med HTTP(S), och tunnling över ICMP och DNS var tvungen att studeras.

Ja, jag lärde mig 2020 att man kan infoga godtycklig nyttolast i ICMP-paket. Men bättre sent än aldrig! Och om man kan göra något åt det, då måste man göra något. Eftersom jag i mitt dagliga liv oftast använder kommandoraden, inklusive via SSH, kom idén om ett ICMP-shell först till mig. Och för att bygga ett komplett bullshield-bingo bestämde jag mig för att skriva det som en Linux-modul i ett språk som jag bara har en grov uppfattning om. Ett sådant skal kommer inte att synas i listan över processer, du kan ladda det i kärnan och det kommer inte att ligga på filsystemet, du kommer inte att se något misstänkt i listan över lyssningsportar. När det gäller dess funktioner är detta ett fullfjädrat rootkit, men jag hoppas kunna förbättra det och använda det som ett skal i sista hand när Load Average är för högt för att logga in via SSH och utföra minst echo i > /proc/sysrq-triggerför att återställa åtkomst utan att starta om.
Vi tar en textredigerare, grundläggande programmeringskunskaper i Python och C, Google och som du inte har något emot att kasta under kniven om allt går sönder (valfritt - lokal VirtualBox/KVM/etc.) och så kör vi!
Klientsidan
Det verkade som att jag för klientdelen skulle behöva skriva ett manus på ungefär 80 rader, men det fanns vänliga människor som gjorde det åt mig. Koden visade sig vara oväntat enkel, den får plats i 10 viktiga rader:
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() Skriptet tar två argument, en adress och en nyttolast. Nyttelasten får en nyckel innan den skickas. run:, vi behöver den för att exkludera paket med slumpmässig nyttolast.
Kärnan kräver behörighet för att skapa paket, så skriptet måste köras med superanvändarrättigheter. Glöm inte att ge det exekveringsrättigheter och installera scapy självt. Debian har ett paket som heter python3-scapyNu kan du kolla hur allt fungerar.
Start och utdata av kommandot
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!
Så här ser det ut i en sniffer
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 spring:Hej världen
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Längd: 17]
Ram 2: 59 byte på kabel (472 bitar), 59 byte infångade (472 bitar) på gränssnittet wlp1s0, id 0
Internetprotokoll version 4, källa: 45.11.26.232, destination: 192.168.0.240
Protokoll för meddelanden om kontroll av Internet
Typ: 0 (Eko (ping) svar)
Kod: 0
Kontrollsumma: 0xde03 [korrekt] [Kontrollsummans status: Bra] Identifierare (BE): 0 (0x0000)
Identifierare (LE): 0 (0x0000)
Sekvensnummer (BE): 0 (0x0000)
Sekvensnummer (LE): 0 (0x0000)
[Begäranram: 1] [Svarstid: 19.094 ms] Data (17 byte)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 spring:Hej världen
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Längd: 17]
^C2-paket infångade
Nyttelasten i svarspaketet ändras inte.
Kärnmodul
För att bygga en virtuell maskin med Debian behöver du minst make и linux-headers-amd64, resten kommer att hämtas som beroenden. Jag kommer inte att tillhandahålla hela koden i artikeln, du kan klona den på GitHub.
Att sätta upp en krok
Först behöver vi två funktioner för att ladda modulen och för att avlasta den. Avlastningsfunktionen krävs inte, men sedan rmmod Det kommer inte att fungera, modulen kommer bara att laddas ur när den stängs av.
#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);Vad händer här:
- Två headerfiler hämtas för manipulationer med själva modulen och med netfilter.
- Alla operationer går igenom ett nätfilter, där du kan sätta hooks. För att göra detta måste du deklarera strukturen i vilken hooken ska konfigureras. Det viktigaste är att ange vilken funktion som ska exekveras som en hook:
nfho.hook = icmp_cmd_executor;Jag återkommer till själva funktionen senare.
Sedan bestämde jag tidpunkten för bearbetning av paketet:NF_INET_PRE_ROUTINGanger att ett paket ska bearbetas när det först visas i kärnan. Du kan användaNF_INET_POST_ROUTINGför att bearbeta paketet när det lämnar kärnan.
Jag hänger ett filter på IPv4:nfho.pf = PF_INET;.
Jag prioriterar min krok högst:nfho.priority = NF_IP_PRI_FIRST;
Och jag registrerar datastrukturen som själva kroken:nf_register_net_hook(&init_net, &nfho); - I den sista funktionen tas kroken bort.
- Licensen är tydligt angiven så att kompilatorn inte klagar.
- funktioner
module_init()иmodule_exit()definiera andra funktioner som initialiserings- och avslutningsfunktioner för modulen.
Utvinning av nyttolasten
Nu behöver vi extrahera nyttolasten, vilket visade sig vara den svåraste uppgiften. Kärnan har inga inbyggda funktioner för att arbeta med nyttolasten, man kan bara analysera rubrikerna för protokoll på högre nivå.
#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;
}Vad händer:
- Var tvungen att inkludera ytterligare headerfiler, den här gången för manipulation av IP- och ICMP-headers.
- Jag ställde in den maximala längden på linjen:
#define MAX_CMD_LEN 1976. Varför just den här? För att kompilatorn klagar på den stora! Jag har redan fått höra att jag måste hantera stacken och heapen, någon dag kommer jag definitivt att göra detta och kanske till och med korrigera koden. Jag satte omedelbart raden där kommandot ska finnas:char cmd_string[MAX_CMD_LEN];Det borde synas i alla funktioner, jag berättar mer om detta i punkt 9. - Nu behöver vi initialisera (
struct work_struct my_work;) struktur och länka den till en annan funktion (DECLARE_WORK(my_work, work_handler);). Jag kommer också att berätta varför detta är nödvändigt i punkt nio. - Nu deklarerar jag en funktion som ska vara kroken. Typen och argumenten den accepterar dikteras av netfilter, vi är bara intresserade av
skbDetta är socketbufferten, en grundläggande datastruktur som innehåller all tillgänglig information om ett paket. - För att köra funktionen behöver du två strukturer och flera variabler, inklusive två iteratorer.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - Vi kan gå vidare till logiken. Modulen behöver inga andra paket än ICMP Echo för att fungera, så vi analyserar bufferten med hjälp av inbyggda funktioner och kastar ut alla icke-ICMP- och icke-Echo-paket. Return
NF_ACCEPTinnebär att acceptera paketet, men du kan också lämna paketen genom att returnera demNF_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; }Jag har inte testat vad som händer utan att kontrollera IP-rubrikerna. Mina minimala kunskaper i C säger mig att utan ytterligare kontroller kommer något hemskt definitivt att hända. Jag skulle vara tacksam om du kunde avfärda mig från det!
- Nu när paketet är av exakt rätt typ kan vi extrahera data. Utan en inbyggd funktion måste vi först hämta en pekare till början av nyttolasten. Detta görs genom ett ställe, vi behöver ta en pekare till början av ICMP-headern och flytta den med storleken på denna header. Strukturen används för allt
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
Änden på rubriken måste matcha änden på nyttolasten iskb, därför erhåller vi den med kärnkraft från motsvarande struktur:tail = skb_tail_pointer(skb);.
Bilden blev stulen , kan du läsa mer om socketbufferten. - Efter att ha mottagit start- och slutpekarna kan vi kopiera data till strängen
cmd_string, kontrollera om det finns ett prefixrun:och antingen kasta paketet om det saknas, eller skriv om raden igen och ta bort prefixet. - Nå, nu kan vi anropa en annan hanterare:
schedule_work(&my_work);Eftersom det inte är möjligt att skicka en parameter till ett sådant anrop måste raden med kommandot vara global.schedule_work()placerar funktionen som är associerad med den skickade strukturen i den allmänna kön i aktivitetsschemaläggaren och kommer att slutföras, vilket gör att du inte behöver vänta på att kommandot ska slutföras. Detta är nödvändigt eftersom hooken måste vara mycket snabb. Annars har du valet att inte köra någonting eller så får du kernelpanik. Prokrastinering är som döden! - Det var det, du kan acceptera paketet med motsvarande retur.
Anropa ett program i användarutrymmet
Denna funktion är den mest förståeliga. Dess namn gavs i DECLARE_WORK(), typen och de mottagna argumenten är inte intressanta. Vi tar raden med kommandot och skickar den helt till skalet. Låter det sköta parsning, sökning efter binärfiler och allt annat självt.
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);
}- Ange argument till en strängmatris
argv[]Jag antar att alla vet att program faktiskt exekveras på det här sättet, och inte som en kontinuerlig linje med mellanslag. - Ställ in miljövariabler. Jag infogade endast PATH med en minimal uppsättning sökvägar, i förväntan om att alla redan har dem kombinerade
/binс/usr/binи/sbinс/usr/sbinAndra vägar är sällan av praktisk betydelse. - Klart, nu kör vi! Kärnfunktion
call_usermodehelper()tar som indata. sökväg till binärfilen, array av argument, array av miljövariabler. Här antar jag också att alla förstår innebörden av att skicka sökvägen till den körbara filen som ett separat argument, men man kan fråga. Det sista argumentet anger om man ska vänta på att processen ska slutföras (UMH_WAIT_PROC), starta processen (UMH_WAIT_EXEC) eller vänta inte alls (UMH_NO_WAIT). Det finns merUMH_KILLABLE, Jag brydde mig inte om att undersöka det.
aggregatet
Sammansättningen av kärnmoduler utförs via kernel make-ramverket. Det kallas make inuti en speciell katalog kopplad till kärnversionen (definierad här: KERNELDIR:=/lib/modules/$(shell uname -r)/build), och modulens plats skickas till en variabel M i argumenten. icmpshell.ko- och clean-målen använder detta ramverk helt och hållet. obj-m anger objektfilen som ska konverteras till en modul. Syntaxen som konverterar main.o в icmpshell.o (icmpshell-objs = main.o) verkar inte särskilt logiskt för mig, men låt det vara så.
KERNELDIR:=/lib/modules/$(shell uname -r)/build
obj-m = icmpshell.o
icmpshell-objs = main.o
allt: icmpshell.ko
icmpshell.ko:main.c
skapa -C $(KERNELDIR) M=$(PWD) moduler
rena:
gör -C $(KERNELDIR) M=$(PWD) ren
Vi samlar in: makeLaddar: insmod icmpshell.koKlart, du kan kontrollera: sudo ./send.py 45.11.26.232 "date > /tmp/test"Om du har en fil på din dator /tmp/test och den innehåller datumet för avsändandet av begäran, vilket betyder att du gjorde allt korrekt och jag gjorde allt korrekt.
Slutsats
Min första erfarenhet av kärnutveckling visade sig vara mycket enklare än jag förväntade mig. Även utan någon erfarenhet av C-utveckling, kunde jag med hjälp av kompilatorprompter och Google-resultat skriva en fungerande modul och känna mig som en kärnhacker, och samtidigt en script kiddie. Dessutom gick jag till Kernel Newbies-kanalen, där de föreslog att jag skulle använda schedule_work() istället för att ringa call_usermodehelper() inuti själva kroken och skammas, med rätta misstänkt en bluff. Hundra rader kod kostade mig ungefär en veckas utveckling på min fritid. En lyckad upplevelse som krossade min personliga myt om den överväldigande komplexiteten i systemutveckling.
Om någon skulle vara villig att göra en kodgranskning på GitHub skulle jag uppskatta det. Jag är ganska säker på att jag gjorde många dumma misstag, särskilt när jag arbetade med strängar.
Källa: will.com

