Nuklea ŝelo super ICMP

Nuklea ŝelo super ICMP

TL; DR: Mi skribas kernan modulon, kiu legos komandojn de la utila ŝarĝo de ICMP kaj plenumos ilin en la servilo eĉ se via SSH kraŝos. Por la plej senpaciencaj, la tuta kodo estas GitHub.

Singardemo Spertaj C-programistoj riskas krevi en sangolarmojn! Eble mi eĉ eraras pri la terminologio, sed ĉia kritiko estas bonvena. La afiŝo estas destinita al tiuj, kiuj havas tre malglatan ideon pri C-programado kaj volas rigardi la internojn de Linukso.

En la komentoj al mia unua artikolo menciis SoftEther VPN, kiu povas imiti iujn "regulajn" protokolojn, precipe HTTPS, ICMP kaj eĉ DNS. Mi povas imagi nur la unuan el ili funkcianta, ĉar mi tre konas HTTP(S), kaj mi devis lerni tuneli per ICMP kaj DNS.

Nuklea ŝelo super ICMP

Jes, en 2020 mi eksciis, ke vi povas enmeti arbitran utilan ŝarĝon en ICMP-pakojn. Sed pli bone malfrue ol neniam! Kaj ĉar io povas esti farita pri ĝi, tiam ĝi devas esti farita. Ĉar en mia ĉiutaga vivo mi plej ofte uzas la komandlinion, inkluzive per SSH, unue venis al mia menso la ideo de ICMP-ŝelo. Kaj por kunmeti kompletan bullshield-bingon, mi decidis skribi ĝin kiel Linuksa modulo en lingvo, pri kiu mi nur malglatan ideon. Tia ŝelo ne estos videbla en la listo de procezoj, vi povas ŝargi ĝin en la kernon kaj ĝi ne estos en la dosiersistemo, vi ne vidos ion suspektindan en la listo de aŭskultantaj havenoj. Koncerne ĝiajn kapablojn, ĉi tio estas plentaŭga rootkit, sed mi esperas plibonigi ĝin kaj uzi ĝin kiel lasta rimedo kiam la Ŝarĝo-Mezumo estas tro alta por ensaluti per SSH kaj ekzekuti almenaŭ. echo i > /proc/sysrq-triggerrestarigi aliron sen rekomenci.

Ni prenas tekstredaktilon, bazajn programajn kapablojn en Python kaj C, Google kaj virtuala kiun vi ne ĝenas meti sub la tranĉilon se ĉio rompiĝas (laŭvola - loka VirtualBox/KVM/ktp) kaj ni iru!

Klienta parto

Ŝajnis al mi, ke por la klienta parto mi devos verki skripton kun ĉirkaŭ 80 linioj, sed estis afablaj homoj, kiuj faris ĝin por mi. la tuta laboro. La kodo montriĝis neatendite simpla, konvenante en 10 signifajn liniojn:

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

La skripto prenas du argumentojn, adreson kaj utilan ŝarĝon. Antaŭ sendado, la utila ŝarĝo estas antaŭita de ŝlosilo run:, ni bezonos ĝin por ekskludi pakaĵojn kun hazardaj utilaj ŝarĝoj.

La kerno postulas privilegiojn por krei pakaĵojn, do la skripto devos esti rulita kiel superuzanto. Ne forgesu doni ekzekutpermesojn kaj instali scapy mem. Debian havas pakaĵon nomitan python3-scapy. Nun vi povas kontroli kiel ĉio funkcias.

Rulado kaj eligo de la komando
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!

Jen kiel ĝi aspektas en la snufisto
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

La utila ŝarĝo en la respondpakaĵo ne ŝanĝiĝas.

Kernelmodulo

Por konstrui Debianan virtualan maŝinon vi bezonos almenaŭ make и linux-headers-amd64, la resto venos en formo de dependecoj. Mi ne provizos la tutan kodon en la artikolo; vi povas kloni ĝin sur Github.

Hoko-aranĝo

Por komenci, ni bezonas du funkciojn por ŝargi la modulon kaj malŝarĝi ĝin. La funkcio por malŝarĝo ne estas bezonata, sed tiam rmmod ĝi ne funkcios; la modulo estos malŝarĝita nur kiam malŝaltita.

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

Kio okazas ĉi tie:

  1. Du kapdosieroj estas tiritaj por manipuli la modulon mem kaj la netfiltrilon.
  2. Ĉiuj operacioj trairas netfiltrilon, vi povas agordi hokojn en ĝi. Por fari tion, vi devas deklari la strukturon en kiu la hoko estos agordita. La plej grava afero estas specifi la funkcion kiu estos ekzekutita kiel hoko: nfho.hook = icmp_cmd_executor; Mi venos al la funkcio mem poste.
    Tiam mi fiksis la pretigtempon por la pakaĵo: NF_INET_PRE_ROUTING specifas prilabori la pakaĵon kiam ĝi unue aperas en la kerno. Povas esti uzata NF_INET_POST_ROUTING por prilabori la pakaĵon dum ĝi eliras el la kerno.
    Mi starigis la filtrilon al IPv4: nfho.pf = PF_INET;.
    Mi donas al mia hoko la plej altan prioritaton: nfho.priority = NF_IP_PRI_FIRST;
    Kaj mi registras la datumstrukturon kiel la fakta hoko: nf_register_net_hook(&init_net, &nfho);
  3. La fina funkcio forigas la hokon.
  4. La permesilo estas klare indikita por ke la kompililo ne plendu.
  5. Funkcioj module_init() и module_exit() starigu aliajn funkciojn por pravalorigi kaj fini la modulon.

Reakiro de la utila ŝarĝo

Nun ni devas ĉerpi la utilan ŝarĝon, ĉi tio montriĝis la plej malfacila tasko. La kerno ne havas enkonstruitajn funkciojn por labori kun utilaj ŝarĝoj; vi povas nur analizi titolojn de pli altnivelaj protokoloj.

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

Kio okazas:

  1. Mi devis inkluzivi pliajn kapdosierojn, ĉi-foje por manipuli IP- kaj ICMP-kapojn.
  2. Mi agordas la maksimuman liniolongon: #define MAX_CMD_LEN 1976. Kial ĝuste ĉi tio? Ĉar la kompililo plendas pri ĝi! Ili jam sugestis al mi, ke mi devas kompreni la stakon kaj amason, iam mi certe faros tion kaj eble eĉ korektos la kodon. Mi tuj starigis la linion, kiu enhavos la komandon: char cmd_string[MAX_CMD_LEN];. Ĝi devus esti videbla en ĉiuj funkcioj; mi parolos pri tio pli detale en paragrafo 9.
  3. Nun ni devas pravalorigi (struct work_struct my_work;) strukturi kaj ligi ĝin kun alia funkcio (DECLARE_WORK(my_work, work_handler);). Mi ankaŭ parolos pri kial tio estas necesa en la naŭa alineo.
  4. Nun mi deklaras funkcion, kiu estos hoko. La tipo kaj akceptitaj argumentoj estas diktitaj de la netfiltrilo, ni nur interesiĝas skb. Ĉi tio estas inga bufro, fundamenta datumstrukturo kiu enhavas ĉiujn disponeblajn informojn pri pako.
  5. Por ke la funkcio funkciu, vi bezonos du strukturojn kaj plurajn variablojn, inkluzive de du ripetiloj.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Ni povas komenci per logiko. Por ke la modulo funkciu, ne necesas neniuj pakoj krom ICMP-Eĥo, do ni analizas la bufron per enkonstruitaj funkcioj kaj forĵetas ĉiujn ne-ICMP kaj ne-Eĥajn pakojn. Revenu NF_ACCEPT signifas akcepton de la pako, sed vi ankaŭ povas faligi pakaĵojn per reveno 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;
      }

    Mi ne provis kio okazos sen kontroli la IP-kapojn. Mia minimuma scio pri C diras al mi, ke sen aldonaj kontroloj, io terura okazos. Mi ĝojos, se vi malemigos min pri tio ĉi!

  7. Nun ke la pako estas de la ĝusta tipo, kiun vi bezonas, vi povas ĉerpi la datumojn. Sen enkonstruita funkcio, vi unue devas ricevi montrilon al la komenco de la utila ŝarĝo. Ĉi tio estas farita en unu loko, vi devas preni la montrilon al la komenco de la ICMP-kapo kaj movi ĝin al la grandeco de ĉi tiu kaplinio. Ĉio uzas strukturon icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    La fino de la kaplinio devas kongrui kun la fino de la utila ŝarĝo enen skb, tial ni akiras ĝin uzante nukleajn rimedojn de la ekvivalenta strukturo: tail = skb_tail_pointer(skb);.

    Nuklea ŝelo super ICMP

    La bildo estis ŝtelita de ĉi tie, vi povas legi pli pri la soka bufro.

  8. Post kiam vi havas montrilojn al la komenco kaj fino, vi povas kopii la datumojn en ĉenon cmd_string, kontrolu ĝin por la ĉeesto de prefikso run: kaj, aŭ forĵetu la pakaĵon se ĝi mankas, aŭ reverku la linion denove, forigante ĉi tiun prefikson.
  9. Jen ĝi, nun vi povas voki alian prizorganton: schedule_work(&my_work);. Ĉar ne eblos transdoni parametron al tia voko, la linio kun la komando devas esti tutmonda. schedule_work() metos la funkcion asociitan kun la pasita strukturo en la ĝeneralan atendovicon de la taskoplanilo kaj kompletigos, permesante al vi ne atendi ke la komando finiĝos. Ĉi tio estas necesa ĉar la hoko devas esti tre rapida. Alie, via elekto estas, ke nenio komenciĝos aŭ vi ricevos kernan panikon. Malfruo estas kiel morto!
  10. Jen, vi povas akcepti la pakaĵon kun responda reveno.

Voki programon en uzantspaco

Ĉi tiu funkcio estas la plej komprenebla. Ĝia nomo estis donita DECLARE_WORK(), la tipo kaj akceptitaj argumentoj ne estas interesaj. Ni prenas la linion kun la komando kaj pasas ĝin tute al la ŝelo. Li okupiĝu pri analizado, serĉado de binaroj kaj ĉio alia.

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. Agordu la argumentojn al tabelo de ŝnuroj argv[]. Mi supozos, ke ĉiuj scias, ke programoj efektive estas ekzekutitaj tiel, kaj ne kiel kontinua linio kun spacoj.
  2. Agordu mediajn variablojn. Mi enmetis nur PATH kun minimuma aro da vojoj, esperante ke ili ĉiuj jam estas kombinitaj /bin с /usr/bin и /sbin с /usr/sbin. Aliaj vojoj malofte gravas en la praktiko.
  3. Farita, ni faru ĝin! Kerna funkcio call_usermodehelper() akceptas eniron. vojo al la duuma, tabelo de argumentoj, tabelo de mediovariabloj. Ĉi tie mi ankaŭ supozas, ke ĉiuj komprenas la signifon de pasi la vojon al la plenumebla dosiero kiel aparta argumento, sed vi povas demandi. La lasta argumento precizigas ĉu atendi ke la procezo finiĝos (UMH_WAIT_PROC), procezo komenco (UMH_WAIT_EXEC) aŭ tute ne atendi (UMH_NO_WAIT). Ĉu estas iom pli UMH_KILLABLE, mi ne rigardis ĝin.

Asembleo

La kunigo de kernaj moduloj estas farata per la kernfara kadro. Vokita make ene de speciala dosierujo ligita al la kernversio (difinita ĉi tie: KERNELDIR:=/lib/modules/$(shell uname -r)/build), kaj la loko de la modulo estas transdonita al la variablo M en la argumentoj. La icmpshell.ko kaj puraj celoj uzas ĉi tiun kadron tute. EN obj-m indikas la objektodosieron kiu estos konvertita en modulon. Sintakso kiu refaras main.o в icmpshell.o (icmpshell-objs = main.o) ne aspektas tre logika al mi, sed tiel estu.

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

Ni kolektas: make. Ŝarĝante: insmod icmpshell.ko. Farite, vi povas kontroli: sudo ./send.py 45.11.26.232 "date > /tmp/test". Se vi havas dosieron en via maŝino /tmp/test kaj ĝi enhavas la daton, kiam la peto estis sendita, kio signifas, ke vi faris ĉion ĝuste kaj mi ĉion faris.

konkludo

Mia unua sperto kun nuklea disvolviĝo estis multe pli facila ol mi atendis. Eĉ sen sperto evoluanta en C, koncentriĝante pri kompililo-sugestoj kaj Guglo-rezultoj, mi povis skribi funkciantan modulon kaj senti min kiel kernhakisto, kaj samtempe skriptfano. Krome, mi iris al la kanalo Kernel Newbies, kie oni ordonis al mi uzi schedule_work() anstataŭ voki call_usermodehelper() interne de la hoko mem kaj hontis lin, prave suspektante fraŭdon. Cent linioj de kodo kostis al mi ĉirkaŭ semajnon da evoluo en mia libera tempo. Sukcesa sperto, kiu detruis mian personan miton pri la superforta komplekseco de sistema evoluo.

Se iu konsentas fari kodan revizion sur Github, mi estos dankema. Mi estas sufiĉe certa, ke mi faris multajn stultajn erarojn, precipe kiam mi laboras kun kordoj.

Nuklea ŝelo super ICMP

fonto: www.habr.com

Aldoni komenton