Bomba nuclear sobre ICMP

Bomba nuclear sobre ICMP

TL; DR: Estic escrivint un mòdul del nucli que llegirà ordres de la càrrega útil ICMP i les executarà al servidor fins i tot si el vostre SSH falla. Per als més impacients, tot el codi és GitHub.

Precaució! Els programadors C experimentats corren el risc d'esclatar a llàgrimes de sang! Fins i tot m'equivoco en la terminologia, però qualsevol crítica és benvinguda. La publicació està pensada per a aquells que tenen una idea molt aproximada de la programació en C i volen mirar l'interior de Linux.

En els comentaris al meu primer article va esmentar SoftEther VPN, que pot imitar alguns protocols "normals", en particular HTTPS, ICMP i fins i tot DNS. Em puc imaginar només el primer d'ells treballant, ja que estic molt familiaritzat amb HTTP(S) i vaig haver d'aprendre a fer túnels per ICMP i DNS.

Bomba nuclear sobre ICMP

Sí, el 2020 vaig saber que podeu inserir una càrrega útil arbitrària als paquets ICMP. Però millor tard que mai! I com que es pot fer alguna cosa al respecte, cal fer-ho. Com que a la meva vida diària faig servir més sovint la línia d'ordres, inclòs mitjançant SSH, la idea d'un shell ICMP em va venir al cap primer. I per tal de muntar un bingo de bullshield complet, vaig decidir escriure'l com a mòdul Linux en un llenguatge del qual només tinc una idea aproximada. Aquest shell no serà visible a la llista de processos, podeu carregar-lo al nucli i no estarà al sistema de fitxers, no veureu res sospitós a la llista de ports d'escolta. Pel que fa a les seves capacitats, es tracta d'un rootkit complet, però espero millorar-lo i utilitzar-lo com a shell d'últim recurs quan la mitjana de càrrega sigui massa alta per iniciar sessió mitjançant SSH i executar-lo almenys. echo i > /proc/sysrq-triggerper restaurar l'accés sense reiniciar.

Prenem un editor de text, habilitats bàsiques de programació en Python i C, Google i virtual que no t'importa posar sota el ganivet si tot es trenca (opcional - VirtualBox/KVM/etc local) i anem-hi!

costat del client

Em va semblar que per part del client hauria d'escriure un guió d'unes 80 línies, però hi havia gent amable que ho feia per mi. tota la feina. El codi va resultar ser inesperadament senzill, encaixant en 10 línies significatives:

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

L'script pren dos arguments, una adreça i una càrrega útil. Abans d'enviar, la càrrega útil va precedida d'una clau run:, el necessitarem per excloure paquets amb càrregues útils aleatòries.

El nucli requereix privilegis per crear paquets, de manera que l'script s'haurà d'executar com a superusuari. No oblideu donar permisos d'execució i instal·lar scapy. Debian té un paquet anomenat python3-scapy. Ara podeu comprovar com funciona tot.

Execució i sortida de l'ordre
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!

Això és el que sembla a l'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

La càrrega útil del paquet de resposta no canvia.

Mòdul del nucli

Per crear una màquina virtual Debian necessitareu almenys make и linux-headers-amd64, la resta vindran en forma de dependències. No proporcionaré tot el codi a l'article; podeu clonar-lo a Github.

Configuració del ganxo

Per començar, necessitem dues funcions per carregar el mòdul i per descarregar-lo. La funció de descàrrega no és necessària, però després rmmod no funcionarà; el mòdul només es descarregarà quan estigui apagat.

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

Que està passant aquí:

  1. S'introdueixen dos fitxers de capçalera per manipular el mòdul en si i el filtre net.
  2. Totes les operacions passen per un netfilter, podeu establir-hi ganxos. Per fer-ho, cal declarar l'estructura en què es configurarà el ganxo. El més important és especificar la funció que s'executarà com a ganxo: nfho.hook = icmp_cmd_executor; Més tard passaré a la funció en si.
    A continuació, configure el temps de processament del paquet: NF_INET_PRE_ROUTING especifica processar el paquet quan aparegui per primera vegada al nucli. Pot ser utilitzat NF_INET_POST_ROUTING per processar el paquet a mesura que surt del nucli.
    He configurat el filtre a IPv4: nfho.pf = PF_INET;.
    Dono al meu ganxo la màxima prioritat: nfho.priority = NF_IP_PRI_FIRST;
    I registre l'estructura de dades com a ganxo real: nf_register_net_hook(&init_net, &nfho);
  3. La funció final elimina el ganxo.
  4. La llicència està clarament indicada perquè el compilador no es queixi.
  5. Funcions module_init() и module_exit() configurar altres funcions per inicialitzar i finalitzar el mòdul.

Recuperació de la càrrega útil

Ara hem d'extreure la càrrega útil, aquesta va resultar ser la tasca més difícil. El nucli no té funcions integrades per treballar amb càrregues útils; només podeu analitzar les capçaleres de protocols de nivell superior.

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

Que està passant:

  1. Vaig haver d'incloure fitxers de capçalera addicionals, aquesta vegada per manipular les capçaleres IP i ICMP.
  2. He establert la longitud màxima de la línia: #define MAX_CMD_LEN 1976. Per què exactament això? Perquè el compilador se'n queixa! Ja m'han suggerit que necessito entendre la pila i el munt, algun dia definitivament ho faré i potser fins i tot corregir el codi. Immediatament vaig establir la línia que contindrà l'ordre: char cmd_string[MAX_CMD_LEN];. Hauria de ser visible en totes les funcions; en parlaré amb més detall al paràgraf 9.
  3. Ara hem d'iniciar (struct work_struct my_work;) estructurar i connectar-lo amb una altra funció (DECLARE_WORK(my_work, work_handler);). També parlaré de per què això és necessari al novè paràgraf.
  4. Ara declaro una funció, que serà un ganxo. El tipus i els arguments acceptats els dicta el netfilter, només ens interessa skb. Aquest és un buffer de socket, una estructura de dades fonamental que conté tota la informació disponible sobre un paquet.
  5. Perquè la funció funcioni, necessitareu dues estructures i diverses variables, inclosos dos iteradors.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Podem començar amb la lògica. Perquè el mòdul funcioni, no calen paquets que no siguin ICMP Echo, de manera que analitzem el buffer utilitzant funcions integrades i eliminem tots els paquets que no siguin ICMP i que no siguin Echo. Tornar NF_ACCEPT significa l'acceptació del paquet, però també podeu deixar els paquets tornant-los 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;
      }

    No he provat què passarà sense comprovar les capçaleres IP. El meu coneixement mínim de C em diu que sense comprovacions addicionals, alguna cosa terrible està obligat a passar. M'alegraré si em dissuasiu d'això!

  7. Ara que el paquet és del tipus exacte que necessiteu, podeu extreure les dades. Sense una funció integrada, primer heu d'obtenir un punter al començament de la càrrega útil. Això es fa en un sol lloc, cal portar el punter al començament de la capçalera ICMP i moure'l a la mida d'aquesta capçalera. Tot utilitza estructura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    El final de la capçalera ha de coincidir amb el final de la càrrega útil skb, per tant l'obtenim mitjançant mitjans nuclears de l'estructura corresponent: tail = skb_tail_pointer(skb);.

    Bomba nuclear sobre ICMP

    La imatge va ser robada per tant, podeu llegir més informació sobre el buffer de socket.

  8. Un cop tingueu els punters al principi i al final, podeu copiar les dades en una cadena cmd_string, comproveu-lo per la presència d'un prefix run: i, descarta el paquet si falta, o torna a escriure la línia, eliminant aquest prefix.
  9. Això és tot, ara podeu trucar a un altre gestor: schedule_work(&my_work);. Com que no serà possible passar un paràmetre a aquesta trucada, la línia amb l'ordre ha de ser global. schedule_work() col·locarà la funció associada a l'estructura passada a la cua general del planificador de tasques i la completarà, cosa que us permetrà no esperar a que es completi l'ordre. Això és necessari perquè el ganxo ha de ser molt ràpid. En cas contrari, la vostra elecció és que no s'iniciarà res o tindreu un pànic del nucli. El retard és com la mort!
  10. Això és tot, pots acceptar el paquet amb la devolució corresponent.

Crida a un programa a l'espai d'usuari

Aquesta funció és la més entenedora. El seu nom va ser donat DECLARE_WORK(), el tipus i els arguments acceptats no són interessants. Agafem la línia amb l'ordre i la passem completament al shell. Que s'ocupi de l'anàlisi, la recerca de binaris i tota la resta.

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. Estableix els arguments en una matriu de cadenes argv[]. Suposo que tothom sap que els programes realment funcionen així, i no com una línia contínua amb espais.
  2. Establir variables d'entorn. Vaig inserir només PATH amb un conjunt mínim de camins, esperant que ja estiguessin tots combinats /bin с /usr/bin и /sbin с /usr/sbin. Altres camins poques vegades importen a la pràctica.
  3. Fet, fem-ho! Funció del nucli call_usermodehelper() accepta l'entrada. camí al binari, matriu d'arguments, matriu de variables d'entorn. Aquí també suposo que tothom entén el significat de passar el camí al fitxer executable com a argument separat, però podeu preguntar. L'últim argument especifica si s'ha d'esperar que finalitzi el procés (UMH_WAIT_PROC), inici del procés (UMH_WAIT_EXEC) o no esperar gens (UMH_NO_WAIT). N'hi ha alguna més UMH_KILLABLE, no ho vaig mirar.

assemblea

El muntatge dels mòduls del nucli es realitza mitjançant el marc de creació del nucli. Va trucar make dins d'un directori especial lligat a la versió del nucli (definit aquí: KERNELDIR:=/lib/modules/$(shell uname -r)/build), i la ubicació del mòdul es passa a la variable M en els arguments. El icmpshell.ko i els objectius nets utilitzen aquest marc completament. EN obj-m indica el fitxer objecte que es convertirà en un mòdul. Sintaxi que es refereix main.o в icmpshell.o (icmpshell-objs = main.o) no em sembla gaire lògic, però que així sigui.

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

Recollim: make. Carregant: insmod icmpshell.ko. Fet, podeu comprovar: sudo ./send.py 45.11.26.232 "date > /tmp/test". Si teniu un fitxer a la vostra màquina /tmp/test i conté la data en què s'ha enviat la sol·licitud, el que significa que ho has fet tot bé i jo ho he fet tot bé.

Conclusió

La meva primera experiència amb el desenvolupament nuclear va ser molt més fàcil del que esperava. Fins i tot sense experiència desenvolupant en C, centrant-me en les pistes del compilador i els resultats de Google, vaig poder escriure un mòdul de treball i sentir-me com un pirata informàtic del nucli i, alhora, un script kiddie. A més, vaig anar al canal Kernel Newbies, on em van dir que ho fes servir schedule_work() en lloc de trucar call_usermodehelper() dins del propi ganxo i el va avergonyir, sospitant amb raó d'una estafa. Un centenar de línies de codi em van costar aproximadament una setmana de desenvolupament en el meu temps lliure. Una experiència d'èxit que va destruir el meu mite personal sobre la complexitat aclaparadora del desenvolupament del sistema.

Si algú accepta fer una revisió de codi a Github, li estaré agraït. Estic bastant segur que vaig cometre molts errors estúpids, sobretot quan treballava amb cordes.

Bomba nuclear sobre ICMP

Font: www.habr.com

Afegeix comentari