Nukleáris shell ICMP fölött

Nukleáris shell ICMP fölött

Röviden összefoglalva: írok egy kernel modult, amely parancsokat olvas az ICMP payloadból, és végrehajtja azokat a szerveren, még akkor is, ha az SSH leállt. A legnagyobb türelmetleneknek itt a teljes kód github.

Óvatosan! A tapasztalt C programozók vérző könnyekre sírnak! Lehet, hogy tévedek a terminológiával, de minden kritikát szívesen fogadok. A poszt célja, hogy azokhoz szóljon, akiknek csak a legfelületesebb elképzelésük van a C programozásról, és szeretnének bepillantani a Linux belső működésébe.

Az első írásomhoz érkező kommentekben a Spotify döntéshozatali módszertanáról. Egyre több felhasználó preferálja a zenehallgatást mobilon a streaming szolgáltatásban, de a cégnél túlnyomórészt webprogramozók dolgoznak. A Spotify elemezte ezt a helyzetet, és úgy döntött, hogy több mobilfejlesztőt vesz fel, valamint átnevezi a webes szakembereket új irányba: említést nyert a SoftEther VPN, amely képes különféle "szokásos" protokollok, például HTTPS, ICMP és még DNS alatt is rejtőzni. Csak az első működésével tudok elsősorban foglalkozni, mivel jól ismerem a HTTP(S)-t, míg az ICMP és DNS feletti tunnelinget meg kellett tanulmányoznom.

Nukleáris shell ICMP fölött

Igen, 2020-ban tudtam meg, hogy ICMP csomagokba tetszőleges payloadot lehet illeszteni. De jobb későn, mint soha! Mivel pedig ezt meg lehet csinálni, el is kell végezni. Mivel a mindennapjaimban legfőképp a parancssort használom, beleértve az SSH-t is, pont ezért jutott eszembe elsőként az ICMP shell. És hogy teljes legyen a hülyeség bingo, úgy döntöttem, hogy Linux modul formájában írom, egy nyelven, amiről csak csekély fogalmam van. Ez a shell nem fog látszódni a folyamatok listájában, betölthető a kernelbe, és nem fog a fájlrendszeren létezni, nem fog semmi gyanúsat látni a figyelő portok listájában. Képességei szempontjából egy teljes értékű rootkit, de remélem, hogy tovább tudom fejleszteni, és használni tudom mint utolsó remény shellt, amikor a Load Average túl magas ahhoz, hogy SSH-n belépjek és végrehajtsam echo i > /proc/sysrq-trigger, hogy helyreállítsam a hozzáférést újraindítás nélkül.

Fogjunk egy szövegszerkesztőt, alap programozási készségeket Pythonban és C-ben, Google-t, és egy virtuális gépet , amit nem sajnálunk levágni, ha minden tönkremegy (opcionálisan — helyi VirtualBox/KVM/stb), és kezdjünk neki!

Kliensoldal

Azt hittem, hogy az ügyféloldalhoz egy 80 soros scriptet kell írnom, de jónak bizonyultak az emberek, akik elvégezték helyettem az összes munkát.. A kód váratlanul egyszerűnek bizonyult, 10 lényeges sorban elfér:

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 script két argumentumot fogad: az IP címet és a payloadot. A payload elküldése előtt a kulccsal precedálják, run:, szükségünk lesz rá, hogy kizárjuk a véletlenszerű payload-okat.

A kernel jogosultságokat igényel a csomagok létrehozásához, így a scriptet superuser jogokkal kell futtatni. Ne felejtsd el engedélyezni a futtatást és telepíteni a scapy-t. Debian alatt létezik egy csomag, ami így hívják, python3-scapy. Most ellenőrizhetjük, hogyan működik mindez.

A parancs futtatása és kimenete
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Helló, világ!"
Kibocsátás kezdete:
.Befejeződött 1 csomag küldése.
*
2 csomagot kaptunk, 1 válasz érkezett, 0 csomag maradt
###[ IP ]###
verzió = 4
ihl = 5
tos = 0x0
hossz = 45
id = 17218
flagok =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
opciók
###[ ICMP ]###
típus = echo-válasz
kód = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Helló, világ!

Így néz ki a sniffelőben
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
A "root" felhasználóként és a "root" csoportban fut. Ez veszélyes lehet.
Felvevő: 'wlp1s0'
Frame 1: 59 byte az vezetéken (472 bit), 59 byte rögzítve (472 bit) a wlp1s0 interfészen, azonosító 0
Internet Protocol Version 4, Forrás: 192.168.0.240, Cél: 45.11.26.232
Internet Control Message Protocol
Típus: 8 (Echo (ping) kérés)
Kód: 0
Ellenőrző összeg: 0xd603 [helyes]
[Ellenőrző összeg állapot: Jó]
Azonosító (BE): 0 (0x0000)
Azonosító (LE): 0 (0x0000)
Sorrendi szám (BE): 0 (0x0000)
Sorrendi szám (LE): 0 (0x0000)
Adatok (17 byte)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Hossz: 17]

Frame 2: 59 byte vezetékben (472 bit), 59 byte rögzítve (472 bit) a wlp1s0 interfészen, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Típus: 0 (Echo (ping) válasz)
Kód: 0
Ellenőrző összeg: 0xde03 [helyes]
[Ellenőrző összeg állapot: Jó]
Azonosító (BE): 0 (0x0000)
Azonosító (LE): 0 (0x0000)
Sorrendi szám (BE): 0 (0x0000)
Sorrendi szám (LE): 0 (0x0000)
[Kérelmezett keret: 1]
[Válaszidő: 19.094 ms]
Adatok (17 byte)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Hossz: 17]

^C2 csomag rögzítve

A válaszcsomag payload-ja nem változik.

Kernel modul

A Debian virtuális gépen való fordításhoz legalább make és linux-headers-amd64, a többi függőségként fog bekerülni. A cikkben nem adom meg a kódot teljesen, a GitHubon le tudod klónozni.

A hook beállítása

Kezdetben szükségünk lesz két funkcióra a modul betöltéséhez és eltávolításához. Az eltávolító funkció nem kötelező, de akkor sem lesz lehetőség a rmmod futtatására, mivel a modul csak kikapcsoláskor távolítható el.

#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 történik itt:

  1. Két fejlécfájlt húzunk be a modullal és a netfilter-rel való műveletekhez.
  2. Minden művelet a netfilteren keresztül történik, ahol hooks-at lehet beállítani. Ehhez meg kell határozni egy struktúrát, amelyen a hook be lesz állítva. A legfontosabb, hogy meg kell adni a funkciót, amely a hook-ként fog végrehajtódni: nfho.hook = icmp_cmd_executor; a funkcióhoz még el fogok jutni.
    Ezután beállítottam a csomag kezelésének pillanatát: NF_INET_PRE_ROUTING megmondja, hogy a csomagot akkor kell kezelni, amikor az csak most jelent meg a kernelben. Használhatjuk a NF_INET_POST_ROUTING a csomag kimeneti feldolgozásához a kernelből.
    Filter-t állítok be IPv4-re: nfho.pf = PF_INET;.
    A hook-omnak a legmagasabb prioritás van beállítva: nfho.priority = NF_IP_PRI_FIRST;
    És regisztrálom az adatstruktúrát mint hook: nf_register_net_hook(&init_net, &nfho);
  3. A záró funkcióban a hook el lesz távolítva.
  4. A licenc meg van határozva, hogy a fordító ne panaszkodjon.
  5. Funkciók module_init() és module_exit() más funkciókat definiál a modul inicializálásakor és befejezésénél.

Payload kinyerése

Most már ki kell nyernem a payloadot, ez bizonyult a legnehezebb feladatnak. A kernelben nincsenek beépített funkciók a payloaddal való munkához, csak a magasabb szintű protokollok fejléceit lehet 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 header fájlokat kellett csatlakoztatni, ezúttal az IP- és ICMP-fejlécek manipulálásához.
  2. Beállítom a maximális karakterhosszt: #define MAX_CMD_LEN 1976. Miért pontosan így? Mert ha nagyobbat állítok be, a fordító hibát jelez! Már mondták, hogy a stackkel és a heapel kellene foglalkoznom, egyszer biztosan megcsinálom és lehet, hogy javítom a kódot is. Rögtön a következő stringet állítom be, amely tárolni fogja a parancsot: char cmd_string[MAX_CMD_LEN];. Ennek láthatónak kell lennie az összes funkcióban, erről részletesebben beszélek a 9. pontban.
  3. Most inicializálni kell a (struct work_struct my_work;) struktúrát, és összekötni egy másik funkcióval (DECLARE_WORK(my_work, work_handler);). Arról, hogy miért van erre szükség, a kilencedik pontban beszélek.
  4. Most kinyilvánítom azt a funkciót, amely a hook lesz. A típust és a várt argumentumokat a netfilter határozza meg, minket csak a skbérdekel. Ez a socket buffer, egy alapvető adatstruktúra, amely minden elérhető információt tartalmaz a csomagról.
  5. A funkció működéséhez két struktúrára és néhány változóra van szükség, beleértve két iterátort.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Most már elkezdhetjük a logikát. A modul működéséhez egyetlen csomagra van szükség, mégpedig az ICMP Echo-ra, ezért a beépített funkciókkal elemezzük a buffert és kiszórjuk az összes nem ICMP- és nem Echo-csomagot. A visszatérés NF_ACCEPT az elfogadott csomagot jelenti, de a csomagok eldobásához is visszaadhatjátok 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 ellenőriztem, mi történik, ha nem ellenőrizzük az IP-fejléceket. A C nyelvről szerzett minimális tudásom azt sugallja, hogy kiküszöbölve a további ellenőrzéseket biztosan valami szörnyű fog történni. Örülnék, ha meggyőznének az ellenkezőjéről!

  7. Most, hogy a csomag biztosan a kívánt típusú, ki lehet nyerni az adatokat. Beépített funkció nélkül először meg kell szerezni a pointert a payload elejére. Ezt egy bizonyos helyen kell megtenni: a pointert az ICMP header elejére kell helyezni, és el kell mozdítani a header méretével. Mindezhez a következő struktúrát használjuk: icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    A fejlécnek meg kell egyeznie a terhelés végével skb, ezért magunkat nukleáris eszközökkel a megfelelő struktúrából nyerjük ki: tail = skb_tail_pointer(skb);.

    Nukleáris shell ICMP fölött

    A képet elraboltam innen, többet olvashatsz a socket bufferrel kapcsolatban.

  8. Miután megkaptuk a kezdeti és végponti mutatókat, másolni tudjuk az adatokat a karakterláncba cmd_string, ellenőrizzük, hogy van-e prefix run: és vagy kidobjuk a csomagot, ha nincs, vagy újra felülírjuk a karakterláncot a prefix eltávolításával.
  9. Nos, most már hívhatunk egy másik kezelőt: schedule_work(&my_work);. Mivel egy ilyen hívásnál paramétert nem tudunk átadni, a parancsot tartalmazó stringnek globálisnak kell lennie. schedule_work() behelyezi a megadott struktúrával társított funkciót az általános ütemezési sorba, és befejeződik, lehetővé téve, hogy ne várjunk a parancs befejezésére. Ez szükséges, mert a horgonynak nagyon gyorsnak kell lennie. Különben választhatsz: semmi nem fog elindulni vagy kernel panicot kapsz. A késlekedés halálos!
  10. Kész, fogadhatjuk a csomagot a megfelelő visszatéréssel.

Programhívás felhasználói térben

Ez a funkció a legérthetőbb. A neve az DECLARE_WORK(), típusa és a kért argumentumok nem érdekesek. Fogjuk a parancsot tartalmazó stringet, és átadjuk azt az egészben a shellnek. Hadd foglalkozzon ő a parszolással, a binárisok keresésével és mindennel.

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ítsuk be az argumentumokat a karakterlánc tömbben argv[]. Feltételezem, hogy mindenki tudja, hogy a programok valójában így futnak, és nem egy folyamatos stringgel és szóközökkel.
  2. Állítsuk be a környezeti változókat. Csak a PATH-t helyeztem be minimális útvonalakkal, feltételezve, hogy mindenki már összevonta azokat /bin a /usr/bin és /sbin a /usr/sbin. Más útvonalak a gyakorlatban meglehetősen ritkán számítanak.
  3. Kész, futtassuk! A kernel funkció call_usermodehelper() a következő paramétereket várja: a bináris elérési útját, az argumentumok tömbjét, a környezeti változók tömbjét. Itt is feltételezem, hogy mindenki érti az értelmet, hogy az végrehajtható fájl elérési útját külön argumentumként átadjuk, de kérdezhetsz. Az utolsó argumentum azt jelzi, hogy várja-e a folyamat befejezését (UMH_WAIT_PROC), a folyamat indítása (UMH_WAIT_EXEC) vagy egyáltalán nem vár (UMH_NO_WAIT). Van még UMH_KILLABLE, nem akartam ezzel foglalkozni.

Összeállítás

A kernel modulok összeállítása a kernel saját make keretrendszerén keresztül történik. Meghívva make a speciális könyvtáron belül, amely a kernel verziójához van kötve (itt határozzák meg: KERNELDIR:=/lib/modules/$(shell uname -r)/build), míg a modul helyét a változó tartalmazza M az argumentumokban. Az icmpshell.ko és clean célok teljes mértékben ezt a keretrendszert használják. A obj-m meghatározza a objektum fájlt, amelyből modul készül. A szintaxis, amely átkonvertálja main.o be. icmpshell.o (icmpshell-objs = main.o) számomra nem tűnik túl logikusnak, de maradjon így.

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

Összeállítjuk: make. Betöltjük: insmod icmpshell.ko. Kész, ellenőrizhető: sudo ./send.py 45.11.26.232 "date > /tmp/test". Ha a gépeden létrejött egy fájl /tmp/test és abban a lekérdezés küldési dátuma található, akkor mindent jól csináltál, és én is jól csináltam.

Zárás

Az első kernel fejlesztési tapasztalatom sokkal egyszerűbbnek bizonyult, mint amit vártam. Még C fejlesztési tapasztalat nélkül is, a fordító és a Google ajánlásaira támaszkodva írtam egy működő modult, és úgy éreztem magam, mint egy kernel hacker, de egyben script kiddie is. Ezen kívül beléptem a Kernel Newbies csatornára, ahol azt tanácsolták, hogy használjam schedule_work() a hívás helyett call_usermodehelper() maga a hook belsejében, és jogosan gyanakodtak, hogy átverés. Száz sor kód körülbelül egy hetes szabadidős fejlesztést igényelt tőlem. Sikeres tapasztalat, amely lerombolta a személyes mitoszomat a rendszerszintű fejlesztés leküzdhetetlen nehézségéről.

Ha valaki elvállalná a kód-áttekintést a GitHubon, hálás lennék. Majdnem biztos vagyok benne, hogy sok ostoba hibát ejtettem, különösen a szövegekkel való munkában.

Nukleáris shell ICMP fölött

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster