Jadrový obal nad ICMP

Jadrový obal nad ICMP

TL; DR: Píšem modul jadra, ktorý bude čítať príkazy z užitočného obsahu ICMP a vykonávať ich na serveri, aj keď váš SSH zlyhá. Pre tých najnetrpezlivejších je celý kód GitHub.

Pozor! Skúsení programátori C riskujú, že sa rozplačú krvou! Možno sa mýlim aj v terminológii, ale každá kritika je vítaná. Príspevok je určený pre tých, ktorí majú veľmi hrubú predstavu o programovaní v C a chcú nahliadnuť do vnútra Linuxu.

V komentároch k môjmu prvému článok spomenul SoftEther VPN, ktorý dokáže napodobňovať niektoré „bežné“ protokoly, najmä HTTPS, ICMP a dokonca aj DNS. Viem si predstaviť, že funguje len prvý z nich, keďže veľmi dobre poznám HTTP(S) a musel som sa naučiť tunelovať cez ICMP a DNS.

Jadrový obal nad ICMP

Áno, v roku 2020 som sa dozvedel, že do paketov ICMP môžete vložiť ľubovoľné užitočné zaťaženie. Ale lepšie neskoro ako nikdy! A keďže sa s tým dá niečo robiť, tak to treba urobiť. Keďže v každodennom živote najčastejšie používam príkazový riadok, a to aj cez SSH, myšlienka shellu ICMP mi prišla na myseľ ako prvá. A aby som mohol zostaviť kompletné bullshield bingo, rozhodol som sa ho napísať ako linuxový modul v jazyku, o ktorom mám len približnú predstavu. Takýto shell nebude viditeľný v zozname procesov, môžete ho načítať do jadra a nebude v súborovom systéme, v zozname počúvajúcich portov neuvidíte nič podozrivé. Z hľadiska svojich možností ide o plnohodnotný rootkit, ale dúfam, že ho vylepším a použijem ako shell poslednej záchrany, keď je priemer zaťaženia príliš vysoký na to, aby sa prihlásil cez SSH a spustil aspoň echo i > /proc/sysrq-triggerobnoviť prístup bez reštartu.

Berieme textový editor, základné programovacie zručnosti v Pythone a C, Google a virtuálne ktorý vám nevadí dať pod nôž, ak sa všetko pokazí (voliteľné - lokálny VirtualBox/KVM/atď) a poďme na to!

Strana klienta

Zdalo sa mi, že pre klientsku časť budem musieť napísať scenár s asi 80 riadkami, ale našli sa milí ľudia, ktorí to urobili za mňa všetku prácu. Kód sa ukázal byť nečakane jednoduchý a zmestil sa do 10 významných riadkov:

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 má dva argumenty, adresu a užitočné zaťaženie. Pred odoslaním je užitočnému zaťaženiu predchádzať kľúč run:, budeme ho potrebovať na vylúčenie balíkov s náhodným zaťažením.

Jadro vyžaduje privilégiá na vytváranie balíkov, takže skript bude musieť byť spustený ako superužívateľ. Nezabudnite udeliť povolenia na spustenie a nainštalovať samotný scapy. Debian má balík s názvom python3-scapy. Teraz môžete skontrolovať, ako to všetko funguje.

Spustenie a výstup príkazu
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!

Takto to vyzerá vo snifferi
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

Užitočné zaťaženie v balíku odpovedí sa nemení.

Modul jadra

Na zabudovanie virtuálneho počítača Debian budete potrebovať min make и linux-headers-amd64, zvyšok príde vo forme závislostí. V článku nebudem poskytovať celý kód, môžete si ho naklonovať na Github.

Nastavenie háku

Na začiatok potrebujeme dve funkcie, aby sme mohli modul načítať a vyložiť. Funkcia pre vykladanie nie je potrebná, ale potom rmmod nebude fungovať, modul sa vysunie až po vypnutí.

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

Čo sa tu deje:

  1. Na manipuláciu so samotným modulom a sieťovým filtrom sú vtiahnuté dva hlavičkové súbory.
  2. Všetky operácie prechádzajú cez sieťový filter, môžete v ňom nastaviť háčiky. Ak to chcete urobiť, musíte deklarovať štruktúru, v ktorej bude hák nakonfigurovaný. Najdôležitejšie je určiť funkciu, ktorá sa vykoná ako hák: nfho.hook = icmp_cmd_executor; K samotnej funkcii sa dostanem neskôr.
    Potom nastavím čas spracovania balíka: NF_INET_PRE_ROUTING určuje spracovanie balíka, keď sa prvýkrát objaví v jadre. Môže byť použité NF_INET_POST_ROUTING na spracovanie paketu pri výstupe z jadra.
    Nastavil som filter na IPv4: nfho.pf = PF_INET;.
    Svojmu háku dávam najvyššiu prioritu: nfho.priority = NF_IP_PRI_FIRST;
    A registrujem dátovú štruktúru ako skutočný hák: nf_register_net_hook(&init_net, &nfho);
  3. Posledná funkcia odstráni háčik.
  4. Licencia je jasne uvedená, aby sa kompilátor nesťažoval.
  5. Funkcia module_init() и module_exit() nastaviť ďalšie funkcie na inicializáciu a ukončenie modulu.

Načítanie užitočného zaťaženia

Teraz musíme extrahovať užitočné zaťaženie, to sa ukázalo ako najťažšia úloha. Jadro nemá vstavané funkcie pre prácu s užitočnými zaťaženiami, môžete analyzovať iba hlavičky protokolov vyššej úrovne.

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

Čo sa deje:

  1. Musel som zahrnúť ďalšie hlavičkové súbory, tentoraz na manipuláciu s hlavičkami IP a ICMP.
  2. Nastavil som maximálnu dĺžku riadku: #define MAX_CMD_LEN 1976. Prečo práve toto? Pretože sa na to kompilátor sťažuje! Už mi naznačili, že musím rozumieť zásobníkom a haldám, raz to určite urobím a možno aj opravím kód. Okamžite som nastavil riadok, ktorý bude obsahovať príkaz: char cmd_string[MAX_CMD_LEN];. Malo by to byť viditeľné vo všetkých funkciách; podrobnejšie o tom budem hovoriť v odseku 9.
  3. Teraz musíme inicializovať (struct work_struct my_work;) štruktúrovať a spájať ju s inou funkciou (DECLARE_WORK(my_work, work_handler);). V deviatom odseku tiež poviem, prečo je to potrebné.
  4. Teraz deklarujem funkciu, ktorá bude háčikom. Typ a akceptované argumenty sú diktované sieťovým filtrom, len nás to zaujíma skb. Toto je vyrovnávacia pamäť soketu, základná dátová štruktúra, ktorá obsahuje všetky dostupné informácie o pakete.
  5. Aby funkcia fungovala, budete potrebovať dve štruktúry a niekoľko premenných vrátane dvoch iterátorov.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Môžeme začať logikou. Aby modul fungoval, nie sú potrebné žiadne iné pakety ako ICMP Echo, takže analyzujeme vyrovnávaciu pamäť pomocou vstavaných funkcií a vyhodíme všetky pakety, ktoré nie sú ICMP a Echo. Návrat NF_ACCEPT znamená prijatie balíka, ale balíky môžete odovzdať aj vrátením 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;
      }

    Netestoval som, čo sa stane bez kontroly hlavičiek IP. Moja minimálna znalosť C mi hovorí, že bez dodatočných kontrol sa určite stane niečo hrozné. Budem rád, ak ma od toho odhovoríte!

  7. Teraz, keď je balík presne toho typu, aký potrebujete, môžete extrahovať údaje. Bez vstavanej funkcie musíte najskôr získať ukazovateľ na začiatok užitočného zaťaženia. Robí sa to na jednom mieste, treba zobrať ukazovateľ na začiatok ICMP hlavičky a presunúť ho na veľkosť tejto hlavičky. Všetko využíva štruktúru icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Koniec hlavičky sa musí zhodovať s koncom užitočného zaťaženia v skb, preto ho získame pomocou jadrových prostriedkov zo zodpovedajúcej štruktúry: tail = skb_tail_pointer(skb);.

    Jadrový obal nad ICMP

    Obraz bol ukradnutý preto, môžete si prečítať viac o vyrovnávacej pamäti zásuvky.

  8. Keď budete mať ukazovatele na začiatok a koniec, môžete údaje skopírovať do reťazca cmd_string, skontrolujte, či neobsahuje predponu run: a buď zahoďte balík, ak chýba, alebo prepíšte riadok znova a odstráňte túto predponu.
  9. To je všetko, teraz môžete zavolať iného handlera: schedule_work(&my_work);. Keďže takémuto volaniu nebude možné odovzdať parameter, riadok s príkazom musí byť globálny. schedule_work() umiestni funkciu spojenú s odovzdanou štruktúrou do všeobecného frontu plánovača úloh a dokončí ju, čo vám umožní nečakať na dokončenie príkazu. Je to potrebné, pretože hák musí byť veľmi rýchly. V opačnom prípade je vašou voľbou, že sa nič nespustí alebo dostanete jadrovú paniku. Oneskorenie je ako smrť!
  10. To je všetko, balík môžete prijať so zodpovedajúcim návratom.

Volanie programu v užívateľskom priestore

Táto funkcia je najzrozumiteľnejšia. Jeho názov bol uvedený v DECLARE_WORK(), typ a prijaté argumenty nie sú zaujímavé. Vezmeme riadok s príkazom a celý ho prenesieme do shellu. Nechajte ho riešiť parsovanie, hľadanie binárnych súborov a všetko ostatné.

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. Nastavte argumenty na pole reťazcov argv[]. Budem predpokladať, že každý vie, že programy sa v skutočnosti vykonávajú týmto spôsobom a nie ako súvislá čiara s medzerami.
  2. Nastavte premenné prostredia. Vložil som iba PATH s minimálnou sadou ciest v nádeji, že už sú všetky skombinované /bin с /usr/bin и /sbin с /usr/sbin. Iné cesty sú v praxi len zriedka dôležité.
  3. Hotovo, poďme na to! Funkcia jadra call_usermodehelper() prijíma vstup. cesta k binárnemu súboru, pole argumentov, pole premenných prostredia. Tu tiež predpokladám, že každý chápe význam odovzdania cesty k spustiteľnému súboru ako samostatného argumentu, ale pýtať sa môžete. Posledný argument určuje, či sa má čakať na dokončenie procesu (UMH_WAIT_PROC), spustenie procesu (UMH_WAIT_EXEC) alebo nečakať vôbec (UMH_NO_WAIT). Je tam ešte nejaké UMH_KILLABLE, nepozeral som na to.

zhromaždenia

Zostavenie modulov jadra sa vykonáva prostredníctvom rámca na výrobu jadra. Volaný make v špeciálnom adresári viazanom na verziu jadra (definované tu: KERNELDIR:=/lib/modules/$(shell uname -r)/build) a umiestnenie modulu sa odovzdá premennej M v argumentoch. icmpshell.ko a čisté ciele využívajú úplne tento rámec. IN obj-m označuje súbor objektu, ktorý sa skonvertuje na modul. Syntax, ktorá sa prerobí main.o в icmpshell.o (icmpshell-objs = main.o) sa mi nezdá veľmi logické, ale je to tak.

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

Zbierame: make. Načítava: insmod icmpshell.ko. Hotovo, môžete skontrolovať: sudo ./send.py 45.11.26.232 "date > /tmp/test". Ak máte v počítači súbor /tmp/test a obsahuje dátum odoslania žiadosti, čo znamená, že ste urobili všetko správne a ja som urobil všetko správne.

Záver

Moja prvá skúsenosť s jadrovým vývojom bola oveľa jednoduchšia, ako som čakal. Dokonca aj bez skúseností s vývojom v C, so zameraním na rady kompilátora a výsledky Google, som bol schopný napísať funkčný modul a cítiť sa ako hacker jadra a zároveň dieťa so skriptami. Okrem toho som išiel na kanál Kernel Newbies, kde mi bolo povedané, aby som ho použil schedule_work() namiesto volania call_usermodehelper() v samotnom háku a zahanbil ho, oprávnene podozrieval z podvodu. Sto riadkov kódu ma stálo asi týždeň vývoja vo voľnom čase. Úspešná skúsenosť, ktorá zničila môj osobný mýtus o ohromnej zložitosti vývoja systému.

Ak niekto súhlasí s vykonaním kontroly kódu na Github, budem vďačný. Som si celkom istý, že som urobil veľa hlúpych chýb, najmä pri práci so strunami.

Jadrový obal nad ICMP

Zdroj: hab.com

Pridať komentár