Obus nucléaire sur ICMP

Obus nucléaire sur ICMP

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

Attention! Les programmeurs C expérimentés risquent de fondre en larmes de sang ! Je me trompe peut-être même dans la terminologie, mais toute critique est la bienvenue. L'article est destiné à ceux qui ont une idée très approximative de la programmation C et souhaitent se pencher sur les entrailles de Linux.

Dans les commentaires de mon premier article 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.

Obus nucléaire sur ICMP

Oui, en 2020, j'ai appris que vous pouvez insérer une charge utile arbitraire dans les paquets ICMP. Mais, mieux vaut tard que jamais! Et puisque quelque chose peut être fait, il faut le faire. Puisque dans ma vie quotidienne j'utilise le plus souvent la ligne de commande, y compris via SSH, l'idée d'un shell ICMP m'est venue en premier à l'esprit. Et afin d'assembler un bingo bullshield complet, j'ai décidé de l'écrire sous forme de module Linux dans un langage dont je n'ai qu'une idée approximative. Un tel shell ne sera pas visible dans la liste des processus, vous pouvez le charger dans le noyau et il ne sera pas sur le système de fichiers, vous ne verrez rien de suspect dans la liste des ports d'écoute. En termes de capacités, il s'agit d'un rootkit à part entière, mais j'espère l'améliorer et l'utiliser comme 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-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 virtuel 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 tout le travail. 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 nécessite des privilèges pour créer des packages, le script devra donc être exécuté en tant que superutilisateur. N'oubliez pas de donner les autorisations d'exécution et d'installer scapy lui-même. Debian a 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:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1] [Response time: 19.094 ms] Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

^C2 packets captured

La charge utile dans le package de réponse ne change pas.

Module noyau

Pour construire une machine virtuelle Debian, vous aurez 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:

  1. Deux fichiers d'en-tête sont extraits pour manipuler le module lui-même et le netfilter.
  2. 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_ROUTING spécifie de traiter le package lorsqu'il apparaît pour la première fois dans le noyau. Peut être utilisé NF_INET_POST_ROUTING pour 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);
  3. La fonction finale supprime le crochet.
  4. La licence est clairement indiquée pour que le compilateur ne se plaint pas.
  5. 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:

  1. J'ai dû inclure des fichiers d'en-tête supplémentaires, cette fois pour manipuler les en-têtes IP et ICMP.
  2. 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.
  3. 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.
  4. 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.
  5. 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;
  6. 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_ACCEPT signifie l'acceptation du colis, mais vous pouvez également déposer les colis en retournant 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;
      }

    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 !

  7. 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 dans skb, on l'obtient donc par voie nucléaire à partir de la structure correspondante : tail = skb_tail_pointer(skb);.

    Obus nucléaire sur ICMP

    La photo a été volée par conséquent,, vous pouvez en savoir plus sur le tampon de socket.

  8. 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éfixe run: et, soit supprimez le package s'il est manquant, soit réécrivez à nouveau la ligne en supprimant ce préfixe.
  9. 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 !
  10. 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);
}

  1. 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.
  2. 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.
  3. 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'autres UMH_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

all: icmpshell.ko

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

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

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.

Obus nucléaire sur ICMP

Source: habr.com

Ajouter un commentaire