Nuklearna granata iznad ICMP-a

Nuklearna granata iznad ICMP-a

TL; DR: Pišem kernel modul koji će čitati naredbe iz ICMP korisnih podataka i izvršavati ih na poslužitelju čak i ako se vaš SSH sruši. Za one najnestrpljivije, sav kod je Github.

Oprez! Iskusni C programeri riskiraju da briznu u plač! Možda čak i griješim u terminologiji, ali svaka kritika je dobrodošla. Post je namijenjen onima koji imaju vrlo grubu predodžbu o C programiranju i žele zaviriti u unutrašnjost Linuxa.

U komentarima na moj prvi članak spomenuo SoftEther VPN, koji može oponašati neke "regularne" protokole, posebice HTTPS, ICMP pa čak i DNS. Mogu zamisliti da radi samo prvi od njih, jer sam dobro upoznat s HTTP(S-om), a morao sam naučiti tuneliranje preko ICMP-a i DNS-a.

Nuklearna granata iznad ICMP-a

Da, 2020. naučio sam da u ICMP pakete možete umetnuti proizvoljan sadržaj. Ali bolje ikad nego nikad! A budući da se nešto može učiniti po tom pitanju, onda to treba učiniti. Budući da u svakodnevnom životu najčešće koristim naredbeni redak, uključujući i putem SSH-a, prva mi je na pamet pala ideja o ICMP shell-u. A kako bih sastavio kompletan bullshield bingo, odlučio sam ga napisati kao Linux modul na jeziku o kojem imam samo grubu predodžbu. Takva ljuska neće biti vidljiva na listi procesa, možete je učitati u kernel i neće biti na datotečnom sustavu, nećete vidjeti ništa sumnjivo na listi slušajućih portova. Što se tiče njegovih mogućnosti, ovo je potpuni rootkit, ali nadam se da ću ga poboljšati i koristiti ga kao posljednje utočište kada je prosjek opterećenja previsok za prijavu putem SSH-a i izvršavanje barem echo i > /proc/sysrq-triggerza vraćanje pristupa bez ponovnog pokretanja.

Uzimamo uređivač teksta, osnovne vještine programiranja u Python i C, Google i virtualan koje ne mislite dati pod nož ako se sve pokvari (opcionalno - lokalni VirtualBox/KVM/itd) i idemo!

Strana klijenta

Činilo mi se da ću za klijentski dio morati napisati scenarij od 80-ak redaka, no našli su se ljubazni ljudi koji su to učinili umjesto mene sav posao. Kôd se pokazao neočekivano jednostavnim, uklapajući se u 10 značajnih redaka:

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, sadržaju prethodi ključ run:, trebat će nam da isključimo pakete sa nasumičnim sadržajem.

Kernel zahtijeva privilegije za izradu paketa, tako da će se skripta morati pokrenuti kao superkorisnik. Ne zaboravite dati dopuštenja za izvođenje i instalirati sam scapy. Debian ima paket tzv python3-scapy. Sada možete provjeriti kako sve to radi.

Izvođenje i ispis 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

Korisni teret u paketu odgovora se ne mijenja.

Kernel modul

Za ugradnju Debian virtualnog stroja trebat će vam najmanje make и linux-headers-amd64, ostatak će doći u obliku ovisnosti. Neću dati cijeli kod u članku; možete ga klonirati na Githubu.

Postavljanje kuke

Za početak, potrebne su nam dvije funkcije za učitavanje i istovar modula. Funkcija za istovar nije potrebna, ali onda rmmod neće raditi; modul će se isprazniti samo kada je isključen.

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

Što se ovdje događa:

  1. Uvlače se dvije datoteke zaglavlja za manipuliranje samim modulom i netfilterom.
  2. Sve operacije prolaze kroz netfilter, u njemu 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šiti kao hook: nfho.hook = icmp_cmd_executor; Doći ću do same funkcije kasnije.
    Zatim postavljam vrijeme obrade za paket: NF_INET_PRE_ROUTING specificira obradu paketa kada se prvi put pojavi u kernelu. Može se koristiti NF_INET_POST_ROUTING za obradu paketa dok izlazi iz kernela.
    Postavio sam filtar na IPv4: nfho.pf = PF_INET;.
    Svojoj udici dajem najveći prioritet: nfho.priority = NF_IP_PRI_FIRST;
    I registriram strukturu podataka kao stvarnu kuku: nf_register_net_hook(&init_net, &nfho);
  3. Završna funkcija uklanja kuku.
  4. Licenca je jasno naznačena tako da se prevoditelj ne buni.
  5. Funkcije module_init() и module_exit() postaviti druge funkcije za inicijalizaciju i prekid modula.

Dohvaćanje korisnog tereta

Sada moramo izvući teret, pokazalo se da je to najteži zadatak. Kernel nema ugrađene funkcije za rad s korisnim sadržajima; možete analizirati samo zaglavlja protokola više razine.

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

Što se događa:

  1. Morao sam uključiti dodatne datoteke zaglavlja, ovaj put da manipuliram IP i ICMP zaglavljima.
  2. Postavio sam maksimalnu duljinu retka: #define MAX_CMD_LEN 1976. Zašto baš ovo? Zato što se prevodilac žali na to! Već su mi sugerirali da moram razumjeti stack i heap, jednog dana ću to sigurno učiniti i možda čak ispraviti kod. Odmah postavljam liniju koja će sadržavati naredbu: char cmd_string[MAX_CMD_LEN];. Trebao bi biti vidljiv u svim funkcijama; o tome ću detaljnije govoriti u paragrafu 9.
  3. Sada moramo inicijalizirati (struct work_struct my_work;) strukturu i povezati je s drugom funkcijom (DECLARE_WORK(my_work, work_handler);). Također ću govoriti o tome zašto je to potrebno u devetom paragrafu.
  4. Sada deklariram funkciju koja će biti kuka. Tip i prihvaćene argumente diktira netfilter, samo nas zanima skb. Ovo je međuspremnik utičnica, temeljna podatkovna struktura 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 s logikom. Da bi modul radio, nisu potrebni drugi paketi osim ICMP Echo, tako da analiziramo međuspremnik pomoću ugrađenih funkcija i izbacujemo sve pakete koji nisu ICMP i koji nisu Echo. Povratak NF_ACCEPT znači prihvaćanje paketa, ali pakete možete ispustiti i povratom 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 što će se dogoditi bez provjere IP zaglavlja. Moje minimalno poznavanje jezika C govori mi da će se bez dodatnih provjera dogoditi nešto strašno. Bit će mi drago ako me odgovorite na to!

  7. Sada kada je paket točne vrste koju trebate, možete izdvojiti podatke. Bez ugrađene funkcije, prvo morate dobiti pokazivač na početak korisnog opterećenja. To se radi na jednom mjestu, potrebno je postaviti pokazivač na početak ICMP zaglavlja i pomaknuti 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 tereta u skb, stoga ga dobivamo pomoću nuklearnih sredstava iz odgovarajuće strukture: tail = skb_tail_pointer(skb);.

    Nuklearna granata iznad ICMP-a

    Slika je ukradena stoga, možete pročitati više o međuspremniku utičnica.

  8. Nakon što imate pokazivače na početak i kraj, možete kopirati podatke u niz cmd_string, provjerite postoji li prefiks run: i, ili odbacite paket ako nedostaje, ili ponovno napišite redak, uklanjajući ovaj prefiks.
  9. To je to, sada možete pozvati drugog rukovatelja: schedule_work(&my_work);. Budući da takvom pozivu neće biti moguće proslijediti parametar, linija s naredbom mora biti globalna. schedule_work() će funkciju povezanu s proslijeđenom strukturom smjestiti u opći red čekanja planera zadataka i dovršiti, omogućujući vam da ne čekate da se naredba završi. To je neophodno jer udica mora biti vrlo brza. U suprotnom, vaš izbor je da se ništa neće pokrenuti ili ćete dobiti paniku kernela. Kašnjenje je kao smrt!
  10. To je to, možete prihvatiti paket uz odgovarajući povrat.

Pozivanje programa u korisničkom prostoru

Ova funkcija je najrazumljivija. Ime mu je dano u DECLARE_WORK(), vrsta i prihvaćeni argumenti nisu zanimljivi. Uzimamo liniju s naredbom i u cijelosti je prosljeđujemo ljusci. Neka se bavi parsiranjem, traženjem binarnih datoteka i svime 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 nizova argv[]. Pretpostavljam da svi znaju da se programi zapravo izvode na ovaj način, a ne kao kontinuirana linija s razmacima.
  2. Postavite varijable okoline. Umetnuo sam samo PATH s minimalnim skupom putanja, nadajući se da su sve već kombinirane /bin с /usr/bin и /sbin с /usr/sbin. Drugi putovi rijetko su važni u praksi.
  3. Gotovo, idemo! Funkcija jezgre call_usermodehelper() prihvaća ulazak. put do binarne datoteke, niz argumenata, niz varijabli okoline. Ovdje također pretpostavljam da svi razumiju značenje prosljeđivanja staze do izvršne datoteke kao zasebnog argumenta, ali možete pitati. Posljednji argument određuje treba li č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 gledao u to.

zbor

Sastavljanje modula jezgre izvodi se kroz kernel make-framework. Nazvana make unutar posebnog direktorija povezanog s verzijom kernela (definirano ovdje: KERNELDIR:=/lib/modules/$(shell uname -r)/build), a lokacija modula prosljeđuje se varijabli M u argumentima. Icmpshell.ko i clean targeti u potpunosti koriste ovaj okvir. U obj-m označava objektnu datoteku koja će se pretvoriti u modul. Sintaksa koja se prepravlja main.o в icmpshell.o (icmpshell-objs = main.o) ne izgleda mi baš logično, ali 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

Sakupljamo: make. Učitavam: 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 kada je zahtjev poslan, što znači da si ti sve napravio kako treba i ja sam sve napravio 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 savjete prevoditelja i Googleove rezultate, uspio sam napisati radni modul i osjećati se kao kernel haker, au isto vrijeme i skriptar. Osim toga, otišao sam na Kernel Newbies kanal, gdje mi je rečeno da ga koristim schedule_work() umjesto poziva call_usermodehelper() unutar same udice i posramio ga, s pravom sumnjajući na prijevaru. Stotinu redaka koda koštalo me otprilike tjedan dana razvoja u slobodno vrijeme. Uspješno iskustvo koje je uništilo moj osobni mit o ogromnoj složenosti razvoja sustava.

Ako netko pristane napraviti pregled koda na Githubu, bit ću mu zahvalan. Prilično sam siguran da sam napravio mnogo glupih pogrešaka, posebno kad sam radio sa žicama.

Nuklearna granata iznad ICMP-a

Izvor: www.habr.com

Dodajte komentar