Nuklearna školjka iznad ICMP-a

Nuklearna školjka iznad ICMP-a

TL; DR: Pišem modul kernela koji će čitati naredbe iz ICMP korisnog opterećenja i izvršavati ih na serveru čak i ako se vaš SSH sruši. Za one najnestrpljivije, sva šifra je GitHub.

Oprez! Iskusni C programeri rizikuju da briznu u krvave suze! Možda čak i griješim u terminologiji, ali svaka kritika je dobrodošla. Objava je namijenjena onima koji imaju vrlo grubu ideju o C programiranju i žele pogledati unutrašnjost Linuxa.

U komentarima na moju prvu članak spomenuo SoftEther VPN, koji može oponašati neke "obične" protokole, posebno HTTPS, ICMP, pa čak i DNS. Mogu da zamislim da samo prvi od njih radi, pošto sam veoma upoznat sa HTTP(S), a morao sam da naučim tuneliranje preko ICMP-a i DNS-a.

Nuklearna školjka iznad ICMP-a

Da, 2020. godine sam naučio da možete umetnuti proizvoljan teret u ICMP pakete. Ali bolje ikad nego nikad! A pošto se nešto može učiniti po tom pitanju, onda to treba i učiniti. Budući da u svakodnevnom životu najčešće koristim komandnu liniju, uključujući i preko SSH-a, prvo mi je pala na pamet ideja o ICMP shell-u. A da bih sastavio kompletan bullshield bingo, odlučio sam da ga napišem kao Linux modul na jeziku o kojem imam samo grubu ideju. Takva ljuska neće biti vidljiva na listi procesa, možete je učitati u kernel i neće biti u sistemu datoteka, nećete vidjeti ništa sumnjivo na listi portova za slušanje. Što se tiče njegovih mogućnosti, ovo je punopravni rootkit, ali nadam se da ću ga poboljšati i koristiti ga kao posljednju ljusku kada je prosjek opterećenja previsok da bi se mogao prijaviti preko SSH-a i izvršiti barem echo i > /proc/sysrq-triggerda vratite pristup bez ponovnog pokretanja.

Uzimamo uređivač teksta, osnovne vještine programiranja u Python-u i C-u, Google-u i virtuelno koju nemate ništa protiv staviti pod nož ako se sve pokvari (opcionalno - lokalni VirtualBox/KVM/itd) i idemo!

Na strani klijenta

Činilo mi se da ću za klijentski deo morati da napišem scenario sa oko 80 redova, ali bilo je ljubaznih ljudi koji su to uradili za mene sav posao. Kod se pokazao neočekivano jednostavnim, uklapajući se u 10 značajnih linija:

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

Skripta uzima dva argumenta, adresu i korisni teret. Prije slanja, korisnom učitavanju prethodi ključ run:, trebat će nam da isključimo pakete sa nasumičnim učitavanjem.

Kernel zahtijeva privilegije za izradu paketa, tako da će skripta morati biti pokrenuta kao superkorisnik. Ne zaboravite dati dozvole za izvršavanje i sam instalirati scapy. Debian ima paket pod nazivom python3-scapy. Sada možete provjeriti kako sve funkcionira.

Pokretanje i izlaz naredbe
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!

Ovako to izgleda u njuškalu
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

Korisno opterećenje u paketu odgovora se ne mijenja.

Kernel modul

Za ugradnju Debian virtualne mašine trebat će vam barem make и linux-headers-amd64, ostalo će doći u obliku zavisnosti. Neću dati cijeli kod u članku; možete ga klonirati na Githubu.

Postavljanje kuke

Za početak, potrebne su nam dvije funkcije kako bismo učitali modul i istovarili ga. Funkcija za istovar nije potrebna, ali onda rmmod neće raditi; modul će se isprazniti samo kada se isključi.

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

sta se desava ovde:

  1. Dva zaglavlja fajla se uvlače da bi se manipulisalo samim modulom i netfilterom.
  2. Sve operacije prolaze kroz netfilter, u njega možete postaviti kuke. Da biste to učinili, morate deklarirati strukturu u kojoj će kuka biti konfigurirana. Najvažnije je navesti funkciju koja će se izvršavati kao kuka: nfho.hook = icmp_cmd_executor; Kasnije ću doći do same funkcije.
    Zatim postavljam vrijeme obrade za paket: NF_INET_PRE_ROUTING zadaje obradu paketa kada se prvi put pojavi u kernelu. Može biti korišteno NF_INET_POST_ROUTING za obradu paketa pri izlasku iz kernela.
    Postavio sam filter na IPv4: nfho.pf = PF_INET;.
    Svojoj udici dajem najveći prioritet: nfho.priority = NF_IP_PRI_FIRST;
    I registrujem strukturu podataka kao stvarnu kuku: nf_register_net_hook(&init_net, &nfho);
  3. Konačna funkcija uklanja kuku.
  4. Licenca je jasno naznačena tako da se kompajler ne žali.
  5. Funkcije module_init() и module_exit() postavite druge funkcije za inicijalizaciju i završetak modula.

Preuzimanje korisnog tereta

Sada moramo izvući teret, ispostavilo se da je to najteži zadatak. Kernel nema ugrađene funkcije za rad sa korisnim opterećenjem; možete analizirati samo zaglavlja protokola višeg nivoa.

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

Šta se dešava:

  1. Morao sam uključiti dodatne datoteke zaglavlja, ovaj put da bih manipulirao IP i ICMP zaglavljima.
  2. Postavio sam maksimalnu dužinu linije: #define MAX_CMD_LEN 1976. Zašto baš ovo? Zato što se kompajler žali na to! Već su mi sugerirali da moram razumjeti stack i hrpu, jednog dana ću to sigurno učiniti i možda čak ispraviti kod. Odmah sam postavio red koji će sadržavati naredbu: char cmd_string[MAX_CMD_LEN];. Trebalo bi biti vidljivo u svim funkcijama; o tome ću detaljnije govoriti u paragrafu 9.
  3. Sada moramo inicijalizirati (struct work_struct my_work;) strukturu i povežite je s drugom funkcijom (DECLARE_WORK(my_work, work_handler);). O tome zašto je to potrebno govoriću iu devetom paragrafu.
  4. Sada deklariram funkciju, koja će biti kuka. Tip i prihvaćene argumente diktira netfilter, samo nas zanima skb. Ovo je bafer utičnice, osnovna struktura podataka koja sadrži sve dostupne informacije o paketu.
  5. Da bi funkcija radila, trebat će vam dvije strukture i nekoliko varijabli, uključujući dva iteratora.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Možemo početi sa logikom. Da bi modul radio, nisu potrebni nikakvi paketi osim ICMP Echo, tako da analiziramo bafer koristeći ugrađene funkcije i izbacujemo sve ne-ICMP i ne-Echo pakete. Povratak NF_ACCEPT znači prihvatanje paketa, ali možete i odbaciti pakete vraćanjem 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;
      }

    Nisam testirao šta će se dogoditi bez provjere IP zaglavlja. Moje minimalno poznavanje C govori mi da će se bez dodatnih provjera nešto strašno dogoditi. Biće mi drago ako me razuveriš od ovoga!

  7. Sada kada je paket tačnog tipa koji vam je potreban, možete izdvojiti podatke. Bez ugrađene funkcije, prvo morate dobiti pokazivač na početak korisnog opterećenja. Ovo se radi na jednom mjestu, potrebno je da uzmete pokazivač na početak ICMP zaglavlja i premjestite ga na veličinu ovog zaglavlja. Sve koristi strukturu icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Kraj zaglavlja mora odgovarati kraju korisnog opterećenja u skb, stoga ga dobijamo nuklearnim sredstvima iz odgovarajuće strukture: tail = skb_tail_pointer(skb);.

    Nuklearna školjka iznad ICMP-a

    Slika je ukradena odavde, možete pročitati više o baferu utičnice.

  8. Kada imate pokazivače na početak i kraj, možete kopirati podatke u niz cmd_string, provjerite ima li prefiksa run: i, ili odbacite paket ako nedostaje, ili prepišite red ponovo, uklanjajući ovaj prefiks.
  9. To je to, sada možete pozvati drugog rukovaoca: schedule_work(&my_work);. Pošto neće biti moguće proslediti parametar takvom pozivu, linija sa naredbom mora biti globalna. schedule_work() stavit će funkciju povezanu s proslijeđenom strukturom u opći red rasporeda zadataka i završiti, omogućavajući vam da ne čekate da se naredba završi. Ovo je neophodno jer udica mora biti veoma brza. U suprotnom, vaš izbor je da ništa neće početi ili ćete dobiti kernel paniku. Kašnjenje je kao smrt!
  10. To je to, možete prihvatiti paket sa odgovarajućim povratom.

Pozivanje programa u korisničkom prostoru

Ova funkcija je najrazumljivija. Njegovo ime je dato u DECLARE_WORK(), tip i prihvaćeni argumenti nisu zanimljivi. Uzimamo liniju sa naredbom i u potpunosti je prosljeđujemo ljusci. Neka se bavi raščlanjivanjem, traženjem binarnih datoteka i svim 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. Postavite argumente na niz stringova argv[]. Pretpostavljam da svi znaju da se programi zapravo izvode na ovaj način, a ne kao neprekidni red sa razmacima.
  2. Postavite varijable okruženja. Ubacio sam samo PATH sa minimalnim skupom putanja, nadajući se da su sve već kombinovane /bin с /usr/bin и /sbin с /usr/sbin. Drugi putevi su rijetko važni u praksi.
  3. Gotovo, uradimo to! Funkcija kernela call_usermodehelper() prihvata ulazak. put do binarnog, niz argumenata, niz varijabli okruženja. Ovdje također pretpostavljam da svi razumiju značenje prosljeđivanja putanje do izvršne datoteke kao zasebnog argumenta, ali možete pitati. Posljednji argument određuje da li treba čekati da se proces završi (UMH_WAIT_PROC), početak procesa (UMH_WAIT_EXEC) ili uopće ne čekati (UMH_NO_WAIT). Ima li još UMH_KILLABLE, nisam to gledao.

Montaža

Sastavljanje modula kernela se izvodi kroz make-framework kernela. Called make unutar posebnog direktorija vezanog za verziju kernela (definirano ovdje: KERNELDIR:=/lib/modules/$(shell uname -r)/build), a lokacija modula se prosljeđuje varijabli M u argumentima. icmpshell.ko i čisti ciljevi u potpunosti koriste ovaj okvir. IN obj-m označava objektni fajl koji će biti konvertovan u modul. Sintaksa koja prepravlja main.o в icmpshell.o (icmpshell-objs = main.o) ne izgleda mi baš logično, ali neka bude 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

prikupljamo: make. Učitavanje: insmod icmpshell.ko. Gotovo, možete provjeriti: sudo ./send.py 45.11.26.232 "date > /tmp/test". Ako imate datoteku na svom stroju /tmp/test i sadrži datum slanja zahtjeva, što znači da ste uradili sve kako treba, a ja sam sve uradio kako treba.

zaključak

Moje prvo iskustvo s nuklearnim razvojem bilo je mnogo lakše nego što sam očekivao. Čak i bez iskustva u razvoju u C-u, fokusirajući se na nagoveštaje kompajlera i Google rezultate, mogao sam da napišem radni modul i osećam se kao kernel haker, a u isto vreme i kao klinac skripte. Osim toga, otišao sam na Kernel Newbies kanal, gdje mi je rečeno da koristim schedule_work() umesto da zovem call_usermodehelper() unutar same udice i osramotio ga, s pravom sumnjajući na prevaru. Stotinu linija koda me koštalo oko nedelju dana razvoja u slobodno vreme. Uspješno iskustvo koje je uništilo moj lični mit o ogromnoj složenosti razvoja sistema.

Ako neko pristane da uradi pregled koda na Githubu, bit ću zahvalan. Prilično sam siguran da sam napravio mnogo glupih grešaka, posebno kada sam radio sa žicama.

Nuklearna školjka iznad ICMP-a

izvor: www.habr.com

Dodajte komentar