Shell nucleare sopra ICMP

Shell nucleare sopra ICMP

TL;DR: sto scrivendo un modulo del kernel che leggerà i comandi dal payload ICMP ed eseguirà questi comandi sul server anche se SSH è caduto. Per i più impazienti, tutto il codice si trova su github.

Attenzione! I programmatori esperti in C rischiano di piangere lacrime di sangue! Potrei sbagliarmi anche nella terminologia, ma ogni critica è benvenuta. Questo post è destinato a chi ha una vaga idea della programmazione in C e vuole sbirciare dentro Linux.

Nei commenti al mio primo abbiamo chiarito il corretto completamento dei programmi che utilizzano il mediastreamer. hanno menzionato SoftEther VPN, che può mimetizzarsi sotto alcuni protocolli "normali", in particolare HTTPS, ICMP e persino DNS. Io conosco solo il primo, poiché ho una buona familiarità con HTTP(S), mentre è stato necessario studiare il tunneling sopra ICMP e DNS.

Shell nucleare sopra ICMP

Sì, nel 2020 ho scoperto che nei pacchetti ICMP si può inserire un payload arbitrario. Ma meglio tardi che mai! E se si può fare qualcosa di utile, allora bisogna farlo. Poiché nella mia quotidianità utilizzo principalmente il terminale, anche tramite SSH, l'idea di uno shell ICMP è venuta subito in mente. E per completare il tutto, ho deciso di scrivere un modulo Linux in un linguaggio di cui ho solo una vaga nozione. Questo shell non sarà visibile nell'elenco dei processi, può essere caricato nel kernel e non sarà presente nel file system; non vedrete nulla di sospetto nell'elenco delle porte in ascolto. Per capacità, è un vero rootkit, ma spero di migliorarlo e usarlo come un'ultima risorsa quando il carico medio è troppo alto per accedere via SSH e eseguire almeno echo i > /proc/sysrq-trigger, per ripristinare l'accesso senza riavviare.

Prendiamo un editor di testo, competenze di base nella programmazione in Python e C, Google e una macchina virtuale che non mi dispiace sacrificare se tutto si rompe (opzionale: VirtualBox/KVM/etc. locale) e partiamo!

La parte client

Pensavo che per la parte client avrei dovuto scrivere uno script lungo circa 80 righe, ma ci sono state persone gentili che hanno fatto il lavoro al mio posto tutto il lavoro. Il codice si è rivelato sorprendentemente semplice, si può ridurre a 10 righe significative:

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

Lo script accetta due argomenti, un indirizzo e un payload. Prima di inviare il payload, viene preceduto da una chiave esegui:, ci servirà per escludere i pacchetti con payload casuale.

Il kernel richiede privilegi per poter creare pacchetti, quindi lo script deve essere eseguito con i diritti di superutente. Non dimenticate di dare i permessi di esecuzione e installare scapy. In Debian c'è un pacchetto chiamato python3-scapy. Ora possiamo verificare come funziona tutto questo.

Esecuzione e output del comando
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Ciao, mondo!"
Inizio emissione:
.Invio di 1 pacchetto completato.
*
Ricevuti 2 pacchetti, ottenuta 1 risposta, rimasti 0 pacchetti
###[ IP ]###
versione = 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
opzioni
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Ciao, mondo!

Ecco come appare nel sniffatore
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Eseguendo come utente "root" e gruppo "root". Questo potrebbe essere pericoloso.
Catturando su 'wlp1s0'
Frame 1: 59 byte su filo (472 bit), 59 byte catturati (472 bit) sull'interfaccia wlp1s0, id 0
Protocollo Internet Versione 4, Src: 192.168.0.240, Dst: 45.11.26.232
Protocollo di Controllo dei Messaggi Internet
Tipo: 8 (Richiesta di Echo (ping))
Codice: 0
Checksum: 0xd603 [corretto]
[Stato del checksum: Buono]
Identificatore (BE): 0 (0x0000)
Identificatore (LE): 0 (0x0000)
Numero di sequenza (BE): 0 (0x0000)
Numero di sequenza (LE): 0 (0x0000)
Dati (17 byte)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 esegui:Ciao, mondo
0010 21 !
Dati: 72756e3a48656c6c6f2c20776f726c6421
[Lunghezza: 17]

Frame 2: 59 byte su wire (472 bit), 59 byte catturati (472 bit) sull'interfaccia wlp1s0, id 0
Protocollo Internet Versione 4, Src: 45.11.26.232, Dst: 192.168.0.240
Protocollo di Controllo dei Messaggi Internet
Tipo: 0 (Risposta a Echo (ping))
Codice: 0
Checksum: 0xde03 [corretto]
[Stato del checksum: Buono]
Identificatore (BE): 0 (0x0000)
Identificatore (LE): 0 (0x0000)
Numero di sequenza (BE): 0 (0x0000)
Numero di sequenza (LE): 0 (0x0000)
[Pacchetto di richiesta: 1]
[Tempo di risposta: 19.094 ms]
Dati (17 byte)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 esegui:Ciao, mondo
0010 21 !
Dati: 72756e3a48656c6c6f2c20776f726c6421
[Lunghezza: 17]

^C2 pacchetti catturati

Il payload nel pacchetto di risposta non cambia.

Modulo del kernel

Per compilare in una virtual machine con Debian sono necessari almeno make e linux-headers-amd64, il resto verrà installato come dipendenze. Non fornirò il codice completo nell'articolo, ma potete clonarlo su GitHub.

Configurazione dell'hook

Per iniziare abbiamo bisogno di due funzioni per caricare e scaricare il modulo. La funzione di scarico non è obbligatoria, ma in tal caso rmmod non potrà essere eseguita, il modulo verrà scaricato solo allo spegnimento.

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

Cosa succede qui:

  1. Vengono inclusi due file di intestazione per la manipolazione del modulo e per Netfilter.
  2. Tutte le operazioni passano attraverso Netfilter, dove possono essere impostati gli hook. Per questo è necessario dichiarare una struttura in cui l'hook verrà configurato. La cosa più importante è specificare la funzione che verrà eseguita come hook: nfho.hook = icmp_cmd_executor; Arriverò alla funzione in seguito.
    Poi ho impostato il momento del trattamento del pacchetto: NF_INET_PRE_ROUTING indica di elaborare il pacchetto quando appare nel kernel. Si può usare NF_INET_POST_ROUTING per trattare il pacchetto in uscita dal kernel.
    Imposto il filtro su IPv4: nfho.pf = PF_INET;.
    Assegno al mio hook la massima priorità: nfho.priority = NF_IP_PRI_FIRST;
    E registro la struttura dei dati come hook: nf_register_net_hook(&init_net, &nfho);
  3. Nella funzione finale l'hook viene rimosso.
  4. La licenza è indicata esplicitamente, affinché il compilatore non generi errori.
  5. Funzioni module_init() e module_exit() definiscono altre funzioni come quelle di inizializzazione e termine del modulo.

Estrazione del payload

Ora è necessario estrarre il payload, che si è rivelato il compito più difficile. Nel kernel non ci sono funzioni incorporate per lavorare con il payload, si possono solo analizzare le intestazioni di protocolli di livello superiore.

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

Cosa sta succedendo:

  1. Ho dovuto includere file di intestazione aggiuntivi, questa volta per la manipolazione con gli header IP e ICMP.
  2. Imposto la lunghezza massima della stringa: #define MAX_CMD_LEN 1976. Perché proprio così? Perché il compilatore si lamenta se è maggiore! Mi hanno già suggerito di occuparmi dello stack e della heap, prima o poi lo farò e potrei anche correggere il codice. Subito definisco la stringa in cui sarà memorizzato il comando: char cmd_string[MAX_CMD_LEN];. Deve essere visibile in tutte le funzioni, ne parlerò più dettagliatamente nel punto 9.
  3. Ora è necessario inizializzare (struct work_struct my_work;) la struttura e collegarla a un'altra funzione (DECLARE_WORK(my_work, work_handler);). Di questo ne parlerò anche nel nono punto.
  4. Ora dichiaro la funzione che fungerà da hook. Il tipo e gli argomenti ricevuti sono dettati dal netfilter, a noi interessa solo skb. Questo è il buffer del socket, una struttura dati fondamentale che contiene tutte le informazioni disponibili sul pacchetto.
  5. Per il funzionamento della funzione saranno necessarie due strutture e alcune variabili, compresi due iteratori.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Possiamo passare alla logica. Per il funzionamento del modulo non sono necessari pacchetti diversi dall'ICMP Echo, quindi analizziamo il buffer utilizzando funzioni incorporate ed eliminiamo tutti i pacchetti che non sono ICMP o Echo. Il ritorno NF_ACCEPT significa accettare il pacchetto, ma puoi anche scartare i pacchetti restituendo 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;
      }

    Non ho verificato cosa succederà senza controllare le intestazioni IP. La mia minima conoscenza di C suggerisce che senza ulteriori controlli succederà inevitabilmente qualcosa di terribile. Sarò felice se mi convincerai del contrario!

  7. Ora, quando il pacchetto è sicuramente del tipo giusto, possiamo estrarre i dati. Senza una funzione incorporata dobbiamo prima ottenere un puntatore all'inizio del payload. Questo avviene attraverso un certo punto, dobbiamo ottenere un puntatore all'inizio dell'intestazione ICMP e spostarlo della dimensione di tale intestazione. Per tutto ciò si utilizza la struttura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    La fine dell'intestazione deve coincidere con la fine del payload in skb, quindi lo otteniamo tramite i mezzi appropriati dalla struttura corrispondente: tail = skb_tail_pointer(skb);.

    Shell nucleare sopra ICMP

    Ho preso l'immagine da qui, puoi leggere maggiori dettagli sul buffer del socket.

  8. Avendo ottenuto i puntatori all'inizio e alla fine, è possibile copiare i dati nella stringa cmd_string, controllare la presenza di un prefisso esegui: e, se manca, scartare il pacchetto oppure sovrascrivere di nuovo la stringa, rimuovendo quel prefisso.
  9. Bene, ora possiamo chiamare un altro gestore: schedule_work(&my_work);. Poiché in una tale chiamata non è possibile passare parametri, la stringa dei comandi deve essere globale. schedule_work() inserirà la funzione associata alla struttura passata nella coda generale dello scheduler e terminerà, permettendo di non attendere il completamento del comando. Questo è necessario perché il hook deve essere molto veloce. Altrimenti, a scelta, niente si avvierà o si riceverà un kernel panic. Ritardare è come morire!
  10. Tutto pronto, possiamo accettare il pacchetto con il ritorno appropriato.

Chiamata di un programma nello user space

Questa funzione è la più comprensibile. Il suo nome è stato definito in DECLARE_WORK(), il tipo e gli argomenti accettati non ci interessano. Prendiamo la stringa con il comando e la passiamo interamente alla shell. Lasciamo che sia lei a occuparsi del parsing, della ricerca dei binari e di tutto il resto.

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. Impostiamo gli argomenti nell'array di stringhe argv[]. Presumo che tutti sappiano che i programmi vengono effettivamente eseguiti in questo modo, e non come una stringa continua con spazi.
  2. Impostiamo le variabili d'ambiente. Ho inserito solo PATH con il set minimo di percorsi, supponendo che tutti abbiano già uniti /bin con /usr/bin e /sbin con /usr/sbin. Altri percorsi sono piuttosto rari da avere significato nella pratica.
  3. Fatto, eseguiamo! La funzione del kernel call_usermodehelper() accetta in input. il percorso verso il binario, l'array degli argomenti, l'array delle variabili d'ambiente. Qui presumo anche che tutti comprendano il significato del passaggio del percorso verso il file eseguibile come argomento separato, ma potete chiedere. L'ultimo argomento indica se aspettare il completamento del processo (UMH_WAIT_PROC), avviare il processo (UMH_WAIT_EXEC) o non aspettare affatto (UMH_NO_WAIT). C'è anche UMH_KILLABLE, non mi sono fermato a studiarlo.

Compilazione

La compilazione dei moduli del kernel viene eseguita tramite il framework make del kernel stesso. Viene invocato make all'interno di una directory speciale collegata alla versione del kernel (definita qui: KERNELDIR:=/lib/modules/$(shell uname -r)/build), e la posizione del modulo viene passata tramite la variabile M negli argomenti. Nel target icmpshell.ko e clean, viene utilizzato interamente questo framework. In obj-m si indica il file oggetto che sarà trasformato in modulo. La sintassi che trasforma main.o in icmpshell.o (icmpshell-objs = main.o) per me sembra poco logica, ma che sia così sia.

KERNELDIR:=/lib/modules/$(shell uname -r)/build

obj-m = icmpshell.o
icmpshell-objs = main.o

tutto: icmpshell.ko

icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules

clean:
make -C $(KERNELDIR) M=$(PWD) clean

Compiliamo: make. Carichiamo: insmod icmpshell.ko. Fatto, puoi controllare: sudo ./send.py 45.11.26.232 "date > /tmp/test". Se sul tuo computer è apparso un file /tmp/test e contiene la data di invio della richiesta, significa che hai fatto tutto correttamente e anch'io ho fatto tutto correttamente.

Conclusione

La mia prima esperienza di sviluppo del kernel si è rivelata molto più semplice di quanto mi aspettassi. Anche senza esperienza nello sviluppo in C, seguendo i suggerimenti del compilatore e le indicazioni di Google, sono riuscito a scrivere un modulo funzionante e a sentirmi un hacker del kernel, e per giunta un script kiddie. Inoltre, sono entrato nel canale Kernel Newbies, dove mi hanno suggerito di usare schedule_work() invece della chiamata call_usermodehelper() all'interno del hook stesso e mi hanno giustamente rimproverato, sospettando di un scam. Centinaia di righe di codice mi sono costate circa una settimana di sviluppo nel tempo libero. È stata un'esperienza positiva che ha distrutto il mio personale mito sulla complessità insormontabile dello sviluppo sistemico.

Se qualcuno fosse disposto a eseguire una revisione del codice su GitHub, ne sarei grato. Sono quasi certo di aver commesso molti errori stupidi, specialmente nella gestione delle stringhe.

Shell nucleare sopra ICMP

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster