Jedrska lupina nad ICMP

Jedrska lupina nad ICMP

TL; DR: pišem modul jedra, ki bo prebral ukaze iz koristnega tovora ICMP in jih izvedel na strežniku, tudi če se vaš SSH zruši. Za najbolj nestrpne je vsa koda github.

Pozor! Izkušeni programerji C tvegajo, da planejo v krvave solze! Morda se celo motim v terminologiji, a vsaka kritika je dobrodošla. Objava je namenjena tistim, ki imajo zelo okvirno predstavo o programiranju C in želijo pogledati v notranjost Linuxa.

V komentarjih k mojemu prvemu članek omenil SoftEther VPN, ki lahko posnema nekatere "navadne" protokole, zlasti HTTPS, ICMP in celo DNS. Predstavljam si, da deluje le prvi, saj zelo dobro poznam HTTP(S) in sem se moral naučiti tuneliranja preko ICMP in DNS.

Jedrska lupina nad ICMP

Da, leta 2020 sem izvedel, da lahko v pakete ICMP vstavite poljuben tovor. Toda bolje pozno kot nikoli! In ker je glede tega mogoče nekaj narediti, potem je to treba storiti. Ker v vsakdanjem življenju najpogosteje uporabljam ukazno vrstico, tudi prek SSH, mi je najprej padla na misel ideja o lupini ICMP. In da bi sestavil popoln bullshield bingo, sem se odločil, da ga napišem kot modul Linuxa v jeziku, ki ga imam samo okvirno. Takšna lupina ne bo vidna na seznamu procesov, lahko jo naložite v jedro in je ne bo v datotečnem sistemu, na seznamu poslušajočih vrat ne boste videli nič sumljivega. Kar zadeva njegove zmogljivosti, je to popoln rootkit, vendar upam, da ga bom izboljšal in uporabil kot zadnjo možnost, ko je povprečje obremenitve previsoko za prijavo prek SSH in izvajanje vsaj echo i > /proc/sysrq-triggerza obnovitev dostopa brez ponovnega zagona.

Prevzamemo urejevalnik besedil, osnovno znanje programiranja v Python in C, Google in virtualni ki ga ne moti dati pod nož, če se vse pokvari (opcijsko - lokalni VirtualBox/KVM/itd) in gremo!

Stranka za stranke

Zdelo se mi je, da bom za stranko moral napisati scenarij s približno 80 vrsticami, vendar so bili prijazni ljudje, ki so to naredili namesto mene vse delo. Koda se je izkazala za nepričakovano preprosto in se prilega v 10 pomembnih 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 ima dva argumenta, naslov in obremenitev. Pred pošiljanjem je koristni tovor pred ključem run:, ga bomo potrebovali za izključitev paketov z naključnimi koristnimi obremenitvami.

Jedro zahteva privilegije za izdelavo paketov, zato bo treba skript izvajati kot superuporabnik. Ne pozabite dati dovoljenj za izvajanje in namestiti sam scapy. Debian ima paket imenovan python3-scapy. Zdaj lahko preverite, kako vse skupaj deluje.

Zagon in izpis ukaza
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!

Takole izgleda v vohljaču
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

Tovor v odzivnem paketu se ne spremeni.

Modul jedra

Za vgradnjo virtualnega stroja Debian boste potrebovali vsaj make и linux-headers-amd64, ostalo bo prišlo v obliki odvisnosti. V članku ne bom navedel celotne kode; lahko jo klonirate na Githubu.

Nastavitev kljuke

Za začetek potrebujemo dve funkciji, da naložimo modul in ga odstranimo. Funkcija za razkladanje ni potrebna, ampak potem rmmod ne bo delovalo; modul se bo raztovoril šele, ko je izklopljen.

#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. Dve datoteki glave sta potegnjeni za manipulacijo samega modula in netfilterja.
  2. Vse operacije potekajo skozi netfilter, v njem lahko nastavite kljuke. Če želite to narediti, morate deklarirati strukturo, v kateri bo kljuka konfigurirana. Najpomembneje je, da določite funkcijo, ki bo izvedena kot kavelj: nfho.hook = icmp_cmd_executor; Na samo funkcijo bom prišel kasneje.
    Nato nastavim čas obdelave za paket: NF_INET_PRE_ROUTING določa obdelavo paketa, ko se prvič pojavi v jedru. Je lahko uporabljen NF_INET_POST_ROUTING za obdelavo paketa, ko zapusti jedro.
    Filter sem nastavil na IPv4: nfho.pf = PF_INET;.
    Svojemu trnku dajem največjo prednost: nfho.priority = NF_IP_PRI_FIRST;
    Podatkovno strukturo registriram kot dejanski kavelj: nf_register_net_hook(&init_net, &nfho);
  3. Končna funkcija odstrani kavelj.
  4. Licenca je jasno navedena, da se prevajalnik ne pritožuje.
  5. Funkcije module_init() и module_exit() nastavite druge funkcije za inicializacijo in zaključek modula.

Pridobivanje tovora

Zdaj moramo izvleči tovor, to se je izkazalo za najtežjo nalogo. Jedro nima vgrajenih funkcij za delo s koristnimi obremenitvami, lahko samo razčlenite glave protokolov višje ravni.

#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 vključiti dodatne datoteke glave, tokrat za manipulacijo glav IP in ICMP.
  2. Nastavil sem največjo dolžino vrstice: #define MAX_CMD_LEN 1976. Zakaj ravno to? Ker se prevajalnik pritožuje nad tem! Predlagali so mi že, da moram razumeti stack in heap, nekoč bom to zagotovo naredil in morda celo popravil kodo. Takoj nastavim vrstico, ki bo vsebovala ukaz: char cmd_string[MAX_CMD_LEN];. Moral bi biti viden v vseh funkcijah; o tem bom podrobneje govoril v odstavku 9.
  3. Zdaj moramo inicializirati (struct work_struct my_work;) strukturo in jo povežite z drugo funkcijo (DECLARE_WORK(my_work, work_handler);). O tem, zakaj je to potrebno, bom govoril tudi v devetem odstavku.
  4. Zdaj deklariram funkcijo, ki bo kljuka. Vrsto in sprejete argumente narekuje netfilter, nas pa le zanimajo skb. To je medpomnilnik vtičnic, osnovna podatkovna struktura, ki vsebuje vse razpoložljive informacije o paketu.
  5. Za delovanje funkcije boste potrebovali dve strukturi in več 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 začnemo z logiko. Za delovanje modula niso potrebni nobeni drugi paketi razen ICMP Echo, zato medpomnilnik razčlenimo z vgrajenimi funkcijami in izločimo vse pakete, ki niso ICMP in ne-Echo. Vrnitev NF_ACCEPT pomeni prevzem paketa, lahko pa pakete tudi izločite z vračilom 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 preizkusil, kaj se bo zgodilo brez preverjanja glav IP. Moje minimalno znanje C-ja mi pove, da se bo brez dodatnih preverjanj zagotovo zgodilo nekaj groznega. Vesel bom, če me od tega odvrnete!

  7. Zdaj, ko je paket točno tistega tipa, ki ga potrebujete, lahko izvlečete podatke. Brez vgrajene funkcije morate najprej dobiti kazalec na začetek tovora. To se naredi na enem mestu, kazalec morate premakniti na začetek glave ICMP in ga premakniti na velikost te glave. Vse uporablja strukturo icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Konec glave se mora ujemati s koncem tovora v skb, zato ga dobimo z uporabo jedrskih sredstev iz ustrezne strukture: tail = skb_tail_pointer(skb);.

    Jedrska lupina nad ICMP

    Slika je bila ukradena zato, lahko preberete več o medpomnilniku vtičnic.

  8. Ko imate kazalca na začetek in konec, lahko kopirate podatke v niz cmd_string, preverite prisotnost predpone run: in zavrzite paket, če manjka, ali znova napišite vrstico in odstranite to predpono.
  9. To je to, zdaj lahko pokličete drugega upravljavca: schedule_work(&my_work);. Ker takemu klicu ne bo mogoče posredovati parametra, mora biti vrstica z ukazom globalna. schedule_work() bo funkcijo, povezano s posredovano strukturo, postavil v splošno čakalno vrsto načrtovalca opravil in dokončal, kar vam bo omogočilo, da ne boste čakali na dokončanje ukaza. To je potrebno, ker mora biti trnek zelo hiter. V nasprotnem primeru je vaša izbira ta, da se ne bo nič zagnalo ali pa boste dobili paniko jedra. Zamuda je kot smrt!
  10. To je to, paket lahko sprejmete z ustreznim vračilom.

Klicanje programa v uporabniškem prostoru

Ta funkcija je najbolj razumljiva. Njegovo ime je bilo podano v DECLARE_WORK(), vrsta in sprejeti argumenti niso zanimivi. Prevzamemo vrstico z ukazom in jo v celoti posredujemo lupini. Naj se ukvarja z razčlenjevanjem, 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. Nastavite argumente na niz nizov argv[]. Predvidevam, da vsi vedo, da se programi dejansko izvajajo na ta način in ne kot neprekinjena črta s presledki.
  2. Nastavite spremenljivke okolja. Vstavil sem samo PATH z minimalnim naborom poti, v upanju, da so vse že združene /bin с /usr/bin и /sbin с /usr/sbin. Druge poti so v praksi redko pomembne.
  3. Končano, naredimo to! Funkcija jedra call_usermodehelper() sprejme vstop. pot do binarnega zapisa, polje argumentov, polje spremenljivk okolja. Predvidevam tudi, da vsi razumejo pomen posredovanja poti do izvršljive datoteke kot ločen argument, vendar lahko vprašate. Zadnji argument določa, ali je treba počakati na dokončanje postopka (UMH_WAIT_PROC), začetek procesa (UMH_WAIT_EXEC) ali sploh ne čakati (UMH_NO_WAIT). Je še kaj UMH_KILLABLE, nisem pogledal v to.

Skupščina

Sestavljanje modulov jedra se izvaja prek ogrodja za izdelavo jedra. Poklican make znotraj posebnega imenika, povezanega z različico jedra (definirano tukaj: KERNELDIR:=/lib/modules/$(shell uname -r)/build), lokacija modula pa se posreduje spremenljivki M v argumentih. icmpshell.ko in čisti cilji v celoti uporabljajo to ogrodje. IN obj-m označuje objektno datoteko, ki bo pretvorjena v modul. Sintaksa, ki se predeluje main.o в icmpshell.o (icmpshell-objs = main.o) se mi ne zdi preveč logično, ampak tako je.

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

Zbiramo: make. Nalaganje: insmod icmpshell.ko. Končano, lahko preverite: sudo ./send.py 45.11.26.232 "date > /tmp/test". Če imate datoteko na vašem računalniku /tmp/test in vsebuje datum, ko je bila zahteva poslana, kar pomeni, da si ti naredil vse prav in jaz sem naredil vse prav.

Zaključek

Moja prva izkušnja z jedrskim razvojem je bila veliko lažja, kot sem pričakoval. Tudi brez izkušenj z razvojem v C, osredotočanjem na namige prevajalnika in Googlove rezultate, sem lahko napisal delujoč modul in se počutil kot heker jedra ter hkrati skriptar. Poleg tega sem šel na kanal Kernel Newbies, kjer so mi rekli, naj uporabljam schedule_work() namesto klica call_usermodehelper() znotraj samega trnka in ga osramotil, pri čemer je upravičeno sumil prevaro. Sto vrstic kode me je stalo približno teden dni razvoja v prostem času. Uspešna izkušnja, ki je uničila moj osebni mit o izjemni kompleksnosti razvoja sistema.

Če se kdo strinja s pregledom kode na Githubu, bom hvaležen. Prepričan sem, da sem naredil veliko neumnih napak, še posebej pri delu s strunami.

Jedrska lupina nad ICMP

Vir: www.habr.com

Dodaj komentar