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 .
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 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.

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 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 . 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:
- Vengono inclusi due file di intestazione per la manipolazione del modulo e per Netfilter.
- 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_ROUTINGindica di elaborare il pacchetto quando appare nel kernel. Si può usareNF_INET_POST_ROUTINGper 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); - Nella funzione finale l'hook viene rimosso.
- La licenza è indicata esplicitamente, affinché il compilatore non generi errori.
- Funzioni
module_init()emodule_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:
- Ho dovuto includere file di intestazione aggiuntivi, questa volta per la manipolazione con gli header IP e ICMP.
- 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. - 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. - 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. - 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; - 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_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 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!
- 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 inskb, quindi lo otteniamo tramite i mezzi appropriati dalla struttura corrispondente:tail = skb_tail_pointer(skb);.
Ho preso l'immagine , puoi leggere maggiori dettagli sul buffer del socket. - Avendo ottenuto i puntatori all'inizio e alla fine, è possibile copiare i dati nella stringa
cmd_string, controllare la presenza di un prefissoesegui:e, se manca, scartare il pacchetto oppure sovrascrivere di nuovo la stringa, rimuovendo quel prefisso. - 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! - 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);
}- 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. - Impostiamo le variabili d'ambiente. Ho inserito solo PATH con il set minimo di percorsi, supponendo che tutti abbiano già uniti
/bincon/usr/bine/sbincon/usr/sbin. Altri percorsi sono piuttosto rari da avere significato nella pratica. - 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'è ancheUMH_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.
Fonte: habr.com

