TL; DR: J'Ă©cris un module de noyau qui lira les commandes de la charge utile ICMP et les exĂ©cutera sur le serveur mĂȘme si votre SSH plante. Pour les plus impatients, tout le code est .
Attention! Les programmeurs C expĂ©rimentĂ©s risquent de verser des larmes de sang ! Je peux mĂȘme me tromper dans la terminologie, mais toute critique est la bienvenue. Cet article s'adresse Ă ceux qui ont une connaissance de base de la programmation en C et qui souhaitent en dĂ©couvrir les rouages. Linux.
Dans les commentaires de mon premier a mentionnĂ© SoftEther VPN, qui peut imiter certains protocoles « classiques », notamment HTTPS, ICMP et mĂȘme DNS. Je ne peux imaginer que seul le premier d'entre eux fonctionne, car je connais trĂšs bien HTTP(S) et j'ai dĂ» apprendre le tunneling via ICMP et DNS.

Oui, j'ai appris en 2020 qu'il est possible d'insĂ©rer des donnĂ©es arbitraires dans les paquets ICMP. Mieux vaut tard que jamais ! Et s'il est possible d'y remĂ©dier, il faut le faire. Comme j'utilise principalement la ligne de commande dans mon travail quotidien, notamment via SSH, l'idĂ©e d'un shell ICMP m'est venue immĂ©diatement. Et pour une protection optimale, j'ai dĂ©cidĂ© de le dĂ©velopper sous forme de module. Linux Dans un langage que je ne maĂźtrise que rudimentairement. Un tel shell ne sera pas visible dans la liste des processus, il pourra ĂȘtre chargĂ© dans le noyau et ne rĂ©sidera pas sur le systĂšme de fichiers. De plus, rien de suspect n'apparaĂźtra dans la liste des ports d'Ă©coute. Il s'agit d'un rootkit complet, mais j'espĂšre l'amĂ©liorer et l'utiliser comme shell de dernier recours lorsque la charge rĂ©seau est trop Ă©levĂ©e pour se connecter via SSH et effectuer mĂȘme les tĂąches les plus basiques. echo i > /proc/sysrq-triggerpour restaurer l'accĂšs sans redĂ©marrer.
Nous prenons un éditeur de texte, des compétences de base en programmation en Python et C, Google et que cela ne vous dérange pas de mettre sous le bistouri si tout casse (facultatif - VirtualBox/KVM/etc local) et c'est parti !
CÎté client
Il m'a semblé que pour la partie client je devrais écrire un script d'environ 80 lignes, mais il y avait des gens gentils qui l'ont fait pour moi . Le code s'est avéré étonnamment simple, s'inscrivant dans 10 lignes 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() Le script prend deux arguments, une adresse et une charge utile. Avant l'envoi, le payload est précédé d'une clé run:, nous en aurons besoin pour exclure les packages avec des charges utiles aléatoires.
Le noyau requiert des privilĂšges pour la crĂ©ation des paquets ; le script devra donc ĂȘtre exĂ©cutĂ© en tant que superutilisateur (root). N'oubliez pas d'accorder les droits d'exĂ©cution et d'installer Scapy. Debian il existe un paquet appelĂ© python3-scapy. Vous pouvez maintenant vĂ©rifier comment tout cela fonctionne.
Exécuter et sortir la commande
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!
Voilà à quoi ça ressemble dans le renifleur
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:Bonjour, monde
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Durée : 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 message de contrĂŽle Internet
Type : 0 (réponse d'écho (ping))
Code: 0
Somme de contrĂŽle : 0xde03 [correcte]
[Ătat de la somme de contrĂŽle : OK]
Identifiant (BE) : 0 (0x0000)
Identifiant (LE) : 0 (0x0000)
Numéro de séquence (BE) : 0 (0x0000)
Numéro de séquence (LE) : 0 (0x0000)
[Frame 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 run:Bonjour, monde
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Durée : 17]
^Paquets C2 capturés
La charge utile dans le package de réponse ne change pas.
Module noyau
Pour créer une machine virtuelle avec Debian aura besoin d'au moins make О linux-headers-amd64, le reste viendra sous forme de dépendances. Je ne fournirai pas l'intégralité du code dans l'article ; vous pouvez le cloner sur Github.
Configuration du crochet
Pour commencer, nous avons besoin de deux fonctions pour charger le module et le décharger. La fonction de déchargement n'est pas nécessaire, mais alors rmmod cela ne fonctionnera pas, le module ne sera déchargé qu'une fois éteint.
#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:
- Deux fichiers d'en-tĂȘte sont extraits pour manipuler le module lui-mĂȘme et le netfilter.
- Toutes les opérations passent par un netfilter, vous pouvez y installer des hooks. Pour ce faire, vous devez déclarer la structure dans laquelle le hook sera configuré. Le plus important est de préciser la fonction qui sera exécutée en tant que hook :
nfho.hook = icmp_cmd_executor;Je reviendrai sur la fonction elle-mĂȘme plus tard.
Ensuite, je fixe le dĂ©lai de traitement du colis :NF_INET_PRE_ROUTINGspĂ©cifie de traiter le package lorsqu'il apparaĂźt pour la premiĂšre fois dans le noyau. Peut ĂȘtre utilisĂ©NF_INET_POST_ROUTINGpour traiter le paquet Ă sa sortie du noyau.
J'ai défini le filtre sur IPv4 :nfho.pf = PF_INET;.
Je donne à mon hook la plus haute priorité :nfho.priority = NF_IP_PRI_FIRST;
Et j'enregistre la structure de données comme véritable hook :nf_register_net_hook(&init_net, &nfho); - La fonction finale supprime le crochet.
- La licence est clairement indiquée pour que le compilateur ne se plaint pas.
- fonctions
module_init()Оmodule_exit()définir d'autres fonctions pour initialiser et terminer le module.
Récupération de la charge utile
Nous devons maintenant extraire la charge utile, cela sâest avĂ©rĂ© ĂȘtre la tĂąche la plus difficile. Le noyau n'a pas de fonctions intĂ©grĂ©es pour travailler avec des charges utiles, vous ne pouvez analyser que 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;
}Qu'est-ce qui se passe:
- J'ai dĂ» inclure des fichiers d'en-tĂȘte supplĂ©mentaires, cette fois pour manipuler les en-tĂȘtes IP et ICMP.
- J'ai défini la longueur maximale de la ligne :
#define MAX_CMD_LEN 1976. Pourquoi exactement ça ? Parce que le compilateur s'en plaint ! Ils m'ont dĂ©jĂ suggĂ©rĂ© que je devais comprendre la pile et le tas, un jour je le ferai certainement et peut-ĂȘtre mĂȘme corrigerai le code. Je dĂ©finis immĂ©diatement la ligne qui contiendra la commande :char cmd_string[MAX_CMD_LEN];. Il doit ĂȘtre visible dans toutes les fonctions ; jâen parlerai plus en dĂ©tail au paragraphe 9. - Nous devons maintenant initialiser (
struct work_struct my_work;) structure et connectez-la Ă une autre fonction (DECLARE_WORK(my_work, work_handler);). Je parlerai Ă©galement des raisons pour lesquelles cela est nĂ©cessaire dans le neuviĂšme paragraphe. - Maintenant, je dĂ©clare une fonction, qui sera un hook. Le type et les arguments acceptĂ©s sont dictĂ©s par le netfilter, nous nous intĂ©ressons uniquement Ă
skb. Il s'agit d'un tampon de socket, une structure de données fondamentale qui contient toutes les informations disponibles sur un paquet. - Pour que la fonction fonctionne, vous aurez besoin de deux structures et de plusieurs variables, dont deux itérateurs.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - Nous pouvons commencer par la logique. Pour que le module fonctionne, aucun paquet autre que ICMP Echo n'est nécessaire, nous analysons donc le tampon à l'aide de fonctions intégrées et rejetons tous les paquets non ICMP et non Echo. Retour
NF_ACCEPTsignifie l'acceptation du colis, mais vous pouvez Ă©galement dĂ©poser les colis 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 testĂ© 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 va forcĂ©ment se produire. Je serai heureux si vous m'en dissuadez !
- Maintenant que le package est du type exact dont vous avez besoin, vous pouvez extraire les donnĂ©es. Sans fonction intĂ©grĂ©e, vous devez d'abord obtenir un pointeur vers le dĂ©but de la charge utile. Cela se fait en un seul endroit, vous devez placer le pointeur vers le dĂ©but de l'en-tĂȘte ICMP et le dĂ©placer Ă la taille de cet en-tĂȘte. Tout utilise une 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, on l'obtient donc par voie nuclĂ©aire Ă partir de la structure correspondante :tail = skb_tail_pointer(skb);.
La photo a été volée , vous pouvez en savoir plus sur le tampon de socket. - Une fois que vous avez des pointeurs vers le début et la fin, vous pouvez copier les données dans une chaßne
cmd_string, vérifiez-le pour la présence d'un préfixerun:et, soit supprimez le package s'il est manquant, soit réécrivez à nouveau la ligne en supprimant ce préfixe. - Voilà , vous pouvez maintenant appeler un autre gestionnaire :
schedule_work(&my_work);. Puisqu'il ne sera pas possible de passer un paramĂštre Ă un tel appel, la ligne avec la 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, vous permettant de ne pas attendre la fin de la commande. Ceci est nĂ©cessaire car lâhameçon doit ĂȘtre trĂšs rapide. Sinon, vous avez le choix : rien ne dĂ©marre ou vous obtenez une panique du noyau. Le retard, c'est comme la mort ! - VoilĂ , vous pouvez accepter le colis avec un retour correspondant.
Appeler un programme dans l'espace utilisateur
Cette fonction est la plus compréhensible. Son nom a été donné dans DECLARE_WORK(), le type et les arguments acceptés ne sont pas intéressants. Nous prenons la ligne avec la commande et la transmettons entiÚrement au shell. Laissez-le s'occuper de l'analyse, de la recherche de fichiers binaires et de 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éfinir les arguments sur un tableau de chaßnes
argv[]. Je suppose que tout le monde sait que les programmes sont réellement exécutés de cette façon, et non comme une ligne continue avec des espaces. - Définissez les variables d'environnement. J'ai inséré uniquement PATH avec un minimum de chemins, en espérant qu'ils étaient tous déjà combinés
/binŃ/usr/binĐž/sbinŃ/usr/sbin. Les autres voies comptent rarement dans la pratique. - C'est fait, faisons-le ! Fonction noyau
call_usermodehelper()accepte l'entrée. chemin d'accÚs au binaire, tableau d'arguments, tableau de variables d'environnement. Ici, je suppose également que tout le monde comprend l'importance de transmettre le chemin d'accÚs au fichier exécutable en tant qu'argument distinct, mais vous pouvez demander. Le dernier argument spécifie s'il faut attendre la fin du processus (UMH_WAIT_PROC), démarrage du processus (UMH_WAIT_EXEC) ou ne pas attendre du tout (UMH_NO_WAIT). Y en a-t-il d'autresUMH_KILLABLE, je n'ai pas étudié la question.
assemblage
L'assemblage des modules du noyau est effectuĂ© via le framework make-frame du noyau. AppelĂ© make dans un rĂ©pertoire spĂ©cial liĂ© Ă la version du noyau (dĂ©fini ici : KERNELDIR:=/lib/modules/$(shell uname -r)/build), et l'emplacement du module est passĂ© Ă la variable M dans les arguments. Les cibles icmpshell.ko et clean utilisent entiĂšrement ce framework. DANS obj-m indique le fichier objet qui sera converti en module. Syntaxe qui refait main.o ĐČ icmpshell.o (icmpshell-objs = main.o) ne me semble pas trĂšs logique, mais tant pis.
KERNELDIR:=/lib/modules/$(shell uname -r)/build
obj-m = icmpshell.o
icmpshell-objs = main.o
tous : icmpshell.ko
icmpshell.ko:main.c
créer des modules -C $(KERNELDIR) M=$(PWD)
nettoyer:
faire -C $(KERNELDIR) M=$(PWD) nettoyer
Nous collectons: make. Chargement: insmod icmpshell.ko. C'est fait, vous pouvez vérifier : sudo ./send.py 45.11.26.232 "date > /tmp/test". Si vous avez un fichier sur votre machine /tmp/test et il contient la date à laquelle la demande a été envoyée, ce qui signifie que vous avez tout fait correctement et que j'ai tout fait correctement.
Conclusion
Ma premiĂšre expĂ©rience avec le dĂ©veloppement nuclĂ©aire a Ă©tĂ© beaucoup plus facile que prĂ©vu. MĂȘme sans expĂ©rience en dĂ©veloppement en C, en me concentrant sur les astuces du compilateur et les rĂ©sultats de Google, j'ai pu Ă©crire un module fonctionnel et me sentir comme un pirate du noyau, et en mĂȘme temps comme un enfant de script. De plus, je suis allĂ© sur la chaĂźne Kernel Newbies, oĂč on m'a dit d'utiliser schedule_work() au lieu d'appeler call_usermodehelper() Ă l'intĂ©rieur du crochet lui-mĂȘme et lui a fait honte, soupçonnant Ă juste titre une arnaque. Une centaine de lignes de code m'a coĂ»tĂ© environ une semaine de dĂ©veloppement sur mon temps libre. Une expĂ©rience rĂ©ussie qui a dĂ©truit mon mythe personnel sur lâĂ©crasante complexitĂ© du dĂ©veloppement de systĂšmes.
Si quelqu'un accepte de faire une révision de code sur Github, je lui en serai reconnaissant. Je suis presque sûr d'avoir commis beaucoup d'erreurs stupides, surtout lorsque je travaille avec des cordes.
Source: habr.com

