Predha bërthamore mbi ICMP

Predha bërthamore mbi ICMP

TL; DR: Po shkruaj një modul kernel që do të lexojë komanda nga ngarkesa e ICMP dhe do t'i ekzekutojë ato në server edhe nëse SSH-ja juaj rrëzohet. Për më të paduruarit, i gjithë kodi është Github.

Kujdes! Programuesit me përvojë C rrezikojnë të shpërthejnë në lot gjaku! Edhe në terminologji mund të kem gabim, por çdo kritikë është e mirëpritur. Postimi është menduar për ata që kanë një ide shumë të përafërt të programimit C dhe duan të shikojnë në brendësi të Linux.

Në komentet për të parën time artikull përmendi SoftEther VPN, i cili mund të imitojë disa protokolle “të rregullta”, në veçanti HTTPS, ICMP dhe madje edhe DNS. Mund ta imagjinoj vetëm të parin duke punuar, pasi jam shumë i njohur me HTTP(S) dhe më është dashur të mësoj tunelimin mbi ICMP dhe DNS.

Predha bërthamore mbi ICMP

Po, në vitin 2020 mësova se mund të futni një ngarkesë arbitrare në paketat ICMP. Por më mirë vonë se kurrë! Dhe meqenëse mund të bëhet diçka për këtë, atëherë duhet bërë. Meqenëse në jetën time të përditshme përdor më së shpeshti linjën e komandës, përfshirë përmes SSH, ideja e një guaskë ICMP më erdhi në mendje së pari. Dhe për të mbledhur një bingo të plotë bullshield, vendosa ta shkruaj atë si një modul Linux në një gjuhë për të cilën kam vetëm një ide të përafërt. Një predhë e tillë nuk do të jetë e dukshme në listën e proceseve, mund ta ngarkoni në kernel dhe nuk do të jetë në sistemin e skedarëve, nuk do të shihni asgjë të dyshimtë në listën e porteve të dëgjimit. Për sa i përket aftësive të tij, ky është një rootkit i plotë, por shpresoj ta përmirësoj dhe ta përdor si mjetin e fundit kur mesatarja e ngarkesës është shumë e lartë për t'u identifikuar nëpërmjet SSH dhe për të ekzekutuar të paktën echo i > /proc/sysrq-triggerpër të rivendosur aksesin pa rindezje.

Ne marrim një redaktues teksti, aftësi bazë programimi në Python dhe C, Google dhe Virtual të cilën nuk e keni problem ta vendosni nën thikë nëse gjithçka prishet (opsionale - VirtualBox/KVM/etj lokale) dhe le të shkojmë!

Nga ana e klientit

Më dukej se për pjesën e klientit do të më duhej të shkruaja një skenar me rreth 80 rreshta, por kishte njerëz të sjellshëm që e bënë për mua. gjithë punën. Kodi doli të ishte papritur i thjeshtë, duke u përshtatur në 10 rreshta domethënës:

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

Skripti merr dy argumente, një adresë dhe një ngarkesë. Përpara dërgimit, ngarkesa paraprihet nga një çelës run:, do të na duhet për të përjashtuar paketat me ngarkesa të rastësishme.

Kerneli kërkon privilegje për të krijuar paketa, kështu që skripti do të duhet të ekzekutohet si superpërdorues. Mos harroni të jepni lejet e ekzekutimit dhe të instaloni vetë scapy. Debian ka një paketë të quajtur python3-scapy. Tani mund të kontrolloni se si funksionon gjithçka.

Ekzekutimi dhe nxjerrja e komandës
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!

Kështu duket në sniffer
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

Ngarkesa në paketën e përgjigjes nuk ndryshon.

Moduli i kernelit

Për të ndërtuar në një makinë virtuale Debian do t'ju duhet të paktën make и linux-headers-amd64, pjesa tjetër do të vijë në formën e varësive. Unë nuk do të jap të gjithë kodin në artikull; ju mund ta klononi atë në Github.

Konfigurimi i grepit

Për të filluar, na duhen dy funksione për të ngarkuar modulin dhe për ta shkarkuar atë. Funksioni për shkarkim nuk kërkohet, por atëherë rmmod nuk do të funksionojë; moduli do të shkarkohet vetëm kur të fiket.

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

Cfare po ndodh ketu:

  1. Dy skedarë kokë janë tërhequr për të manipuluar vetë modulin dhe filtrin net.
  2. Të gjitha operacionet kalojnë përmes një filtri net, mund të vendosni grepa në të. Për ta bërë këtë, duhet të deklaroni strukturën në të cilën do të konfigurohet grepa. Gjëja më e rëndësishme është të specifikoni funksionin që do të ekzekutohet si grep: nfho.hook = icmp_cmd_executor; Do të shkoj te vetë funksioni më vonë.
    Pastaj vendosa kohën e përpunimit për paketën: NF_INET_PRE_ROUTING specifikon për të përpunuar paketën kur ajo shfaqet për herë të parë në kernel. Mund të përdoret NF_INET_POST_ROUTING për të përpunuar paketën kur ajo del nga kerneli.
    E vendosa filtrin në IPv4: nfho.pf = PF_INET;.
    Unë i jap grepit tim prioritetin më të lartë: nfho.priority = NF_IP_PRI_FIRST;
    Dhe unë e regjistroj strukturën e të dhënave si goditjen aktuale: nf_register_net_hook(&init_net, &nfho);
  3. Funksioni përfundimtar heq grepin.
  4. Licenca tregohet qartë në mënyrë që përpiluesi të mos ankohet.
  5. Funksionet module_init() и module_exit() vendosni funksione të tjera për të inicializuar dhe përfunduar modulin.

Marrja e ngarkesës

Tani duhet të nxjerrim ngarkesën, kjo doli të ishte detyra më e vështirë. Kerneli nuk ka funksione të integruara për të punuar me ngarkesa; ju mund të analizoni vetëm titujt e protokolleve të nivelit më të lartë.

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

Cfare po ndodh:

  1. Më duhej të përfshija skedarë shtesë të kokës, këtë herë për të manipuluar titujt e IP dhe ICMP.
  2. Kam vendosur gjatësinë maksimale të linjës: #define MAX_CMD_LEN 1976. Pse pikërisht kjo? Sepse përpiluesi ankohet për këtë! Ata tashmë më kanë sugjeruar që duhet të kuptoj grumbullin dhe grumbullin, një ditë do ta bëj patjetër këtë dhe ndoshta edhe do ta korrigjoj kodin. Vendosa menjëherë rreshtin që do të përmbajë komandën: char cmd_string[MAX_CMD_LEN];. Ajo duhet të jetë e dukshme në të gjitha funksionet; Unë do të flas për këtë më në detaje në paragrafin 9.
  3. Tani duhet të inicializojmë (struct work_struct my_work;) strukturoni dhe lidhni atë me një funksion tjetër (DECLARE_WORK(my_work, work_handler);). Unë gjithashtu do të flas se pse kjo është e nevojshme në paragrafin e nëntë.
  4. Tani unë deklaroj një funksion, i cili do të jetë një goditje. Lloji dhe argumentet e pranuara diktohen nga netfiltri, neve na intereson vetëm skb. Ky është një buffer socket, një strukturë themelore e të dhënave që përmban të gjithë informacionin e disponueshëm për një paketë.
  5. Që funksioni të funksionojë, do t'ju nevojiten dy struktura dhe disa variabla, duke përfshirë dy përsëritës.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Mund të fillojmë me logjikën. Që moduli të funksionojë, nuk nevojiten pako të tjera përveç ICMP Echo, kështu që ne analizojmë bufferin duke përdorur funksionet e integruara dhe hedhim jashtë të gjitha paketat jo-ICMP dhe jo-Echo. Kthimi NF_ACCEPT do të thotë pranim i paketës, por mund t'i lëshoni edhe paketat duke u kthyer 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;
      }

    Unë nuk kam testuar se çfarë do të ndodhë pa kontrolluar titujt e IP-së. Njohuria ime minimale për C-në më thotë se pa kontrolle shtesë, diçka e tmerrshme me siguri do të ndodhë. Do të jem i lumtur nëse më largoni nga kjo!

  7. Tani që paketa është e llojit të saktë që ju nevojitet, mund t'i nxirrni të dhënat. Pa një funksion të integruar, së pari duhet të merrni një tregues në fillimin e ngarkesës. Kjo bëhet në një vend, ju duhet ta çoni treguesin në fillim të kokës ICMP dhe ta zhvendosni atë në madhësinë e këtij titulli. Çdo gjë përdor strukturën icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Fundi i kokës duhet të përputhet me fundin e ngarkesës brenda skb, prandaj e marrim duke përdorur mjete bërthamore nga struktura përkatëse: tail = skb_tail_pointer(skb);.

    Predha bërthamore mbi ICMP

    Fotografia ishte vjedhur prandaj, mund të lexoni më shumë rreth bufferit të prizës.

  8. Pasi të keni tregues në fillim dhe në fund, mund t'i kopjoni të dhënat në një varg cmd_string, kontrollojeni për praninë e një parashtese run: dhe, ose hidhni paketën nëse mungon, ose rishkruani sërish rreshtin, duke hequr këtë prefiks.
  9. Kjo është e gjitha, tani mund të telefononi një mbajtës tjetër: schedule_work(&my_work);. Meqenëse nuk do të jetë e mundur të kalohet një parametër në një thirrje të tillë, linja me komandën duhet të jetë globale. schedule_work() do të vendosë funksionin e lidhur me strukturën e kaluar në radhën e përgjithshme të planifikuesit të detyrave dhe do të përfundojë, duke ju lejuar të mos prisni që komanda të përfundojë. Kjo është e nevojshme sepse grepi duhet të jetë shumë i shpejtë. Përndryshe, zgjedhja juaj është që asgjë nuk do të fillojë ose do të keni një panik kernel. Vonesa është si vdekja!
  10. Kjo është ajo, ju mund të pranoni paketën me një kthim përkatës.

Thirrja e një programi në hapësirën e përdoruesit

Ky funksion është më i kuptueshëm. Emri i saj u dha DECLARE_WORK(), lloji dhe argumentet e pranuara nuk janë interesante. Marrim rreshtin me komandën dhe ia kalojmë tërësisht guaskës. Lëreni të merret me analizimin, kërkimin e binareve dhe gjithçka tjetër.

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. Vendosni argumentet në një grup vargjesh argv[]. Unë do të supozoj se të gjithë e dinë se programet në të vërtetë ekzekutohen në këtë mënyrë, dhe jo si një linjë e vazhdueshme me hapësira.
  2. Vendosni variablat e mjedisit. Unë futa vetëm PATH me një grup minimal shtigjesh, duke shpresuar se të gjitha ishin të kombinuara tashmë /bin с /usr/bin и /sbin с /usr/sbin. Rrugët e tjera rrallë kanë rëndësi në praktikë.
  3. U krye, le ta bëjmë! Funksioni i kernelit call_usermodehelper() pranon hyrjen. rruga drejt binarit, grupi i argumenteve, grupi i variablave të mjedisit. Këtu supozoj gjithashtu se të gjithë e kuptojnë kuptimin e kalimit të shtegut në skedarin e ekzekutueshëm si një argument më vete, por ju mund të pyesni. Argumenti i fundit specifikon nëse duhet pritur që procesi të përfundojë (UMH_WAIT_PROC), fillimi i procesit (UMH_WAIT_EXEC) ose mos prisni fare (UMH_NO_WAIT). A ka më shumë UMH_KILLABLE, nuk e shikova.

asamble

Montimi i moduleve të kernelit kryhet përmes kornizës së krijimit të kernelit. I thirrur make brenda një drejtorie të veçantë të lidhur me versionin e kernelit (përcaktuar këtu: KERNELDIR:=/lib/modules/$(shell uname -r)/build), dhe vendndodhja e modulit i kalohet ndryshores M në argumentet. icmpshell.ko dhe objektivat e pastër përdorin tërësisht këtë kornizë. NË obj-m tregon skedarin e objektit që do të konvertohet në një modul. Sintaksë që ribërë main.o в icmpshell.o (icmpshell-objs = main.o) nuk më duket shumë logjike, por kështu qoftë.

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

Ne mbledhim: make. Duke u ngarkuar: insmod icmpshell.ko. Mbaruar, mund të kontrolloni: sudo ./send.py 45.11.26.232 "date > /tmp/test". Nëse keni një skedar në kompjuterin tuaj /tmp/test dhe përmban datën e dërgimit të kërkesës, që do të thotë se ju bëtë gjithçka siç duhet dhe unë bëra gjithçka siç duhet.

Përfundim

Përvoja ime e parë me zhvillimin bërthamor ishte shumë më e lehtë nga sa prisja. Edhe pa përvojën e zhvillimit në C, duke u fokusuar në sugjerimet e përpiluesit dhe rezultatet e Google, unë munda të shkruaj një modul funksional dhe të ndjehesha si një haker kernel, dhe në të njëjtën kohë si një fëmijë i skenarit. Përveç kësaj, shkova në kanalin Kernel Newbies, ku më thanë të përdorja schedule_work() në vend që të telefononi call_usermodehelper() brenda vetë grepit dhe e turpëroi, duke dyshuar me të drejtë për një mashtrim. Njëqind rreshta kodi më kushtuan rreth një javë zhvillim në kohën time të lirë. Një përvojë e suksesshme që shkatërroi mitin tim personal për kompleksitetin dërrmues të zhvillimit të sistemit.

Nëse dikush pranon të bëjë një rishikim të kodit në Github, do të jem mirënjohës. Jam shumë i sigurt se kam bërë shumë gabime të trashë, veçanërisht kur punoj me tela.

Predha bërthamore mbi ICMP

Burimi: www.habr.com

Shto një koment