Nucleaire granaat boven ICMP

Nucleaire granaat boven ICMP

TL; DR: Ik ben een kernelmodule aan het schrijven die opdrachten uit de ICMP-payload leest en deze op de server uitvoert, zelfs als uw SSH crasht. Voor de meest ongeduldigen is alle code hetzelfde GitHub.

Let op! Ervaren C-programmeurs lopen het risico in bloedtranen uit te barsten! Misschien heb ik het zelfs mis in de terminologie, maar alle kritiek is welkom. De post is bedoeld voor degenen die een heel globaal idee hebben van C-programmeren en de binnenkant van Linux willen bekijken.

In de reacties op mijn eerste статье noemde SoftEther VPN, dat enkele ‘reguliere’ protocollen kan nabootsen, met name HTTPS, ICMP en zelfs DNS. Ik kan me voorstellen dat alleen de eerste werken, aangezien ik zeer bekend ben met HTTP(S) en ik moest leren tunnelen via ICMP en DNS.

Nucleaire granaat boven ICMP

Ja, in 2020 heb ik geleerd dat je een willekeurige payload in ICMP-pakketten kunt invoegen. Maar beter laat dan nooit! En aangezien er iets aan gedaan kan worden, moet het ook gedaan worden. Omdat ik in mijn dagelijks leven het vaakst de opdrachtregel gebruik, ook via SSH, kwam het idee van een ICMP-shell als eerste bij me op. En om een ​​complete bullshield bingo samen te stellen, besloot ik het als een Linux-module te schrijven in een taal waar ik slechts een globaal idee van heb. Zo'n shell zal niet zichtbaar zijn in de lijst met processen, je kunt hem in de kernel laden en hij zal niet in het bestandssysteem staan, je zult niets verdachts zien in de lijst met luisterpoorten. Qua mogelijkheden is dit een volwaardige rootkit, maar ik hoop hem te verbeteren en als laatste redmiddel te gebruiken wanneer het Load Average te hoog is om via SSH in te loggen en op zijn minst uit te voeren echo i > /proc/sysrq-triggerom de toegang te herstellen zonder opnieuw op te starten.

We nemen een teksteditor, basisprogrammeervaardigheden in Python en C, Google en virtueel die je niet erg vindt om onder het mes te leggen als alles kapot gaat (optioneel - lokale VirtualBox/KVM/etc) en laten we gaan!

Kant van de cliënt

Het leek mij dat ik voor het klantgedeelte een script van ongeveer 80 regels zou moeten schrijven, maar er waren aardige mensen die het voor mij deden al het werk. De code bleek onverwacht eenvoudig en paste in 10 belangrijke regels:

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

Het script heeft twee argumenten nodig: een adres en een payload. Voordat de lading wordt verzonden, wordt deze voorafgegaan door een sleutel run:, hebben we het nodig om pakketten met willekeurige payloads uit te sluiten.

De kernel vereist privileges om pakketten te maken, dus het script zal als superuser moeten worden uitgevoerd. Vergeet niet uitvoeringsrechten te geven en scapy zelf te installeren. Debian heeft een pakket genaamd python3-scapy. Nu kunt u controleren hoe het allemaal werkt.

Het uitvoeren en uitvoeren van de opdracht
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!

Zo ziet het eruit in de 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

De payload in het antwoordpakket verandert niet.

Kernel-module

Om een ​​virtuele Debian-machine in te bouwen heeft u minimaal nodig make и linux-headers-amd64, de rest zal in de vorm van afhankelijkheden komen. Ik zal niet de volledige code in het artikel verstrekken; je kunt deze op Github klonen.

Haak opstelling

Om te beginnen hebben we twee functies nodig om de module te laden en te ontladen. De functie voor lossen is niet nodig, maar dan wel rmmod dit werkt niet; de module wordt pas ontladen als deze is uitgeschakeld.

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

Wat is hier aan de hand:

  1. Er worden twee headerbestanden binnengehaald om de module zelf en het netfilter te manipuleren.
  2. Alle handelingen gaan via een netfilter, daarin kun je hooks zetten. Om dit te doen, moet u de structuur declareren waarin de hook zal worden geconfigureerd. Het belangrijkste is om de functie te specificeren die als hook zal worden uitgevoerd: nfho.hook = icmp_cmd_executor; Op de functie zelf kom ik later terug.
    Vervolgens stel ik de verwerkingstijd voor het pakket in: NF_INET_PRE_ROUTING specificeert dat het pakket moet worden verwerkt wanneer het voor het eerst in de kernel verschijnt. Kan worden gebruikt NF_INET_POST_ROUTING om het pakket te verwerken zodra het de kernel verlaat.
    Ik heb het filter ingesteld op IPv4: nfho.pf = PF_INET;.
    Ik geef mijn haak de hoogste prioriteit: nfho.priority = NF_IP_PRI_FIRST;
    En ik registreer de datastructuur als de daadwerkelijke hook: nf_register_net_hook(&init_net, &nfho);
  3. De laatste functie verwijdert de haak.
  4. De licentie wordt duidelijk aangegeven zodat de samensteller niet klaagt.
  5. functies module_init() и module_exit() stel andere functies in om de module te initialiseren en te beëindigen.

Het ophalen van de lading

Nu moeten we de lading eruit halen, dit bleek de moeilijkste taak. De kernel heeft geen ingebouwde functies voor het werken met payloads; je kunt alleen headers van protocollen op een hoger niveau ontleden.

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

Wat is er gaande:

  1. Ik moest extra headerbestanden toevoegen, dit keer om IP- en ICMP-headers te manipuleren.
  2. Ik stel de maximale lijnlengte in: #define MAX_CMD_LEN 1976. Waarom precies dit? Omdat de compiler erover klaagt! Ze hebben me al gesuggereerd dat ik de stapel en de heap moet begrijpen, op een dag zal ik dit zeker doen en misschien zelfs de code corrigeren. Ik stel onmiddellijk de regel in die de opdracht zal bevatten: char cmd_string[MAX_CMD_LEN];. Het zou in alle functies zichtbaar moeten zijn; ik zal hier in paragraaf 9 dieper op ingaan.
  3. Nu moeten we initialiseren (struct work_struct my_work;) structureren en verbinden met een andere functie (DECLARE_WORK(my_work, work_handler);). Waarom dit nodig is, zal ik ook bespreken in de negende paragraaf.
  4. Nu declareer ik een functie, die een hook zal zijn. Het type en de geaccepteerde argumenten worden bepaald door het netfilter, waar we alleen in geïnteresseerd zijn skb. Dit is een socketbuffer, een fundamentele datastructuur die alle beschikbare informatie over een pakket bevat.
  5. Om de functie te laten werken, heb je twee structuren en verschillende variabelen nodig, waaronder twee iterators.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. We kunnen beginnen met logica. Om de module te laten werken zijn er geen andere pakketten nodig dan ICMP Echo, dus parseren we de buffer met behulp van ingebouwde functies en gooien we alle niet-ICMP- en niet-Echo-pakketten weg. Opbrengst NF_ACCEPT betekent acceptatie van het pakket, maar u kunt pakketten ook afgeven door te retourneren 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;
      }

    Ik heb niet getest wat er zal gebeuren zonder de IP-headers te controleren. Mijn minimale kennis van C vertelt me ​​dat er zonder extra controles iets vreselijks zal gebeuren. Ik zal blij zijn als je mij hiervan afhoudt!

  7. Nu het pakket precies van het type is dat u nodig heeft, kunt u de gegevens extraheren. Zonder ingebouwde functie moet je eerst een verwijzing naar het begin van de payload krijgen. Dit gebeurt op één plek, u moet de aanwijzer naar het begin van de ICMP-header brengen en deze verplaatsen naar de grootte van deze header. Alles maakt gebruik van structuur icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Het einde van de header moet overeenkomen met het einde van de payload skb, daarom verkrijgen we het met behulp van nucleaire middelen uit de overeenkomstige structuur: tail = skb_tail_pointer(skb);.

    Nucleaire granaat boven ICMP

    De foto is gestolen vandaar, kunt u meer lezen over de socketbuffer.

  8. Zodra u verwijzingen naar het begin en einde heeft, kunt u de gegevens naar een string kopiëren cmd_string, controleer het op de aanwezigheid van een voorvoegsel run: en verwijder het pakket als het ontbreekt, of herschrijf de regel opnieuw en verwijder dit voorvoegsel.
  9. Dat is alles, nu kunt u een andere handler bellen: schedule_work(&my_work);. Omdat het niet mogelijk is om een ​​parameter aan een dergelijke oproep door te geven, moet de regel met de opdracht globaal zijn. schedule_work() zal de functie die bij de doorgegeven structuur hoort, in de algemene wachtrij van de taakplanner plaatsen en voltooien, zodat u niet hoeft te wachten tot de opdracht is voltooid. Dit is nodig omdat de haak erg snel moet zijn. Anders is uw keuze dat er niets zal starten, anders krijgt u een kernelpaniek. Uitstel is als de dood!
  10. Dat is alles, u kunt het pakket accepteren met een bijbehorende retourzending.

Een programma oproepen in de gebruikersruimte

Deze functie is het meest begrijpelijk. De naam werd opgegeven DECLARE_WORK(), het type en de geaccepteerde argumenten zijn niet interessant. We nemen de regel met het commando en geven deze volledig door aan de shell. Laat hem zich bezighouden met het ontleden, zoeken naar binaire bestanden en al het andere.

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. Stel de argumenten in op een array van tekenreeksen argv[]. Ik neem aan dat iedereen weet dat programma's feitelijk op deze manier worden uitgevoerd, en niet als een doorlopende lijn met spaties.
  2. Stel omgevingsvariabelen in. Ik heb alleen PATH ingevoegd met een minimum aantal paden, in de hoop dat ze allemaal al gecombineerd waren /bin с /usr/bin и /sbin с /usr/sbin. Andere paden doen er in de praktijk zelden toe.
  3. Klaar, laten we het doen! Kernel-functie call_usermodehelper() aanvaardt toegang. pad naar het binaire bestand, reeks argumenten, reeks omgevingsvariabelen. Hier ga ik er ook van uit dat iedereen de betekenis begrijpt van het doorgeven van het pad naar het uitvoerbare bestand als een afzonderlijk argument, maar je kunt het vragen. Het laatste argument geeft aan of er moet worden gewacht tot het proces is voltooid (UMH_WAIT_PROC), processtart (UMH_WAIT_EXEC) of helemaal niet wachten (UMH_NO_WAIT). Is er nog wat UMH_KILLABLE, ik heb er niet naar gekeken.

montage

De assemblage van kernelmodules wordt uitgevoerd via het kernel make-framework. Genaamd make in een speciale map die is gekoppeld aan de kernelversie (hier gedefinieerd: KERNELDIR:=/lib/modules/$(shell uname -r)/build), en de locatie van de module wordt doorgegeven aan de variabele M in de argumenten. De icmpshell.ko en schone doelen gebruiken dit raamwerk volledig. IN obj-m geeft het objectbestand aan dat naar een module wordt geconverteerd. Syntaxis die remakes maakt main.o в icmpshell.o (icmpshell-objs = main.o) lijkt mij niet erg logisch, maar het zij zo.

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

We verzamelen: make. Bezig met laden: insmod icmpshell.ko. Klaar, je kunt het volgende controleren: sudo ./send.py 45.11.26.232 "date > /tmp/test". Als u een bestand op uw computer hebt /tmp/test en het bevat de datum waarop het verzoek is verzonden, wat betekent dat jij alles goed hebt gedaan en ik alles goed heb gedaan.

Conclusie

Mijn eerste ervaring met nucleaire ontwikkeling was veel gemakkelijker dan ik had verwacht. Zelfs zonder ervaring met ontwikkelen in C, waarbij ik me concentreerde op compilerhints en Google-resultaten, kon ik een werkende module schrijven en voelde ik me een kernelhacker en tegelijkertijd een scriptkidddie. Daarnaast ging ik naar het Kernel Newbies-kanaal, waar mij werd verteld dat ik het moest gebruiken schedule_work() in plaats van te bellen call_usermodehelper() in de haak zelf en beschaamde hem, terecht vermoedend van oplichterij. Honderd regels code kostte me ongeveer een week ontwikkeling in mijn vrije tijd. Een succesvolle ervaring die mijn persoonlijke mythe over de overweldigende complexiteit van systeemontwikkeling vernietigde.

Als iemand ermee instemt een codebeoordeling op Github uit te voeren, zal ik dankbaar zijn. Ik ben er vrij zeker van dat ik veel domme fouten heb gemaakt, vooral bij het werken met snaren.

Nucleaire granaat boven ICMP

Bron: www.habr.com

Voeg een reactie