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 tombait en panne. Pour les plus impatients, tout le code est sur github .
Mise en garde! Les programmeurs C expérimentés courent le risque d'éclater en sanglots! Je peux me tromper même sur la terminologie, mais toute critique est la bienvenue. Le message est destiné à ceux qui ont l'idée la plus approximative de la programmation C et qui souhaitent se pencher sur les rouages de Linux.
Dans les commentaires de mon premier articlea mentionné SoftEther VPN, qui peut imiter certains protocoles «normaux», en particulier HTTPS, ICMP et même DNS. Je ne peux qu'imaginer le travail du premier d'entre eux, car je suis très familier avec HTTP (S), et j'ai dû apprendre le tunneling sur ICMP et DNS.
Oui, j'ai appris en 2020 que vous pouvez insérer une charge utile arbitraire dans des paquets ICMP. Mais, mieux vaut tard que jamais! Et puisque vous pouvez faire quelque chose à ce sujet, vous devez 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. Et afin de créer un bingo à conneries 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 reposera 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 le modifier et l'utiliser comme shell de dernier recours, lorsque la moyenne de charge est trop élevée pour se connecter via SSH et exécuter au moins
echo i > /proc/sysrq-trigger
pour 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 une machine virtuelle que cela ne vous dérange pas de mettre sous le couteau si tout se brise (facultatif - VirtualBox / KVM / etc local) et c'est parti!
Partie client
Il me semblait que pour le client, je devrais écrire un scénario de 80 lignes, mais il y avait des gens gentils qui faisaient tout le travail pour moi . Le code s'est avéré étonnamment simple, il s'inscrit 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, la charge utile est précédée d'une clé
run:
, nous en aurons besoin pour exclure les paquets avec une charge utile aléatoire.
Le noyau a besoin de privilèges pour créer des packages, le script devra donc être exécuté en tant que superutilisateur. N'oubliez pas de donner l'autorisation 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écution et sortie d'une 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!
Voici à 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 paquet de réponse ne change pas.
Module noyau
Pour construire une machine virtuelle avec Debian, vous aurez besoin d'au moins
make
et linux-headers-amd64
, le reste sera resserré en tant que dépendances. Je ne donnerai pas l'intégralité du code dans l'article, vous pouvez le cloner sur github.
Configuration du crochet
Tout d'abord, nous avons besoin de deux fonctions pour charger le module et le décharger. La fonction de déchargement n'est pas requise, mais elle
rmmod
ne fonctionnera pas, le module ne sera déchargé que lorsqu'il sera é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.
- , . , . — , :
nfho.hook = icmp_cmd_executor;
.
:NF_INET_PRE_ROUTING
, .NF_INET_POST_ROUTING
.
IPv4:nfho.pf = PF_INET;
.
:nfho.priority = NF_IP_PRI_FIRST;
:nf_register_net_hook(&init_net, &nfho);
- .
- , .
-
module_init()
module_exit()
.
Nous devons maintenant extraire la charge utile, ce qui s'est avéré être la tâche la plus difficile. Le noyau n'a pas de fonctions intégrées pour travailler avec la charge utile, 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 == '\0')
break;
if (j == MAX_CMD_LEN) {
cmd_string[j] = '\0';
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] == '\0')
break;
}
}
schedule_work(&my_work);
return NF_ACCEPT;
}
Que ce passe-t-il:
- J'ai dû inclure des fichiers d'en-tête supplémentaires, cette fois pour manipuler les en-têtes IP et ICMP.
- Indique la longueur maximale d'une chaîne:
#define MAX_CMD_LEN 1976
. Pourquoi exactement cela? Parce que le compilateur jure contre le grand! Ils m'ont déjà dit que je devais gérer la pile et le tas, un jour je le ferai certainement et peut-être même corrigerai le code. Définit immédiatement une chaîne dans laquelle l'équipe sera basée:char cmd_string[MAX_CMD_LEN];
. Il doit être visible dans toutes les fonctions, j'en parlerai plus en détail au paragraphe 9. - (
struct work_struct my_work;
) (DECLARE_WORK(my_work, work_handler);
). , , . - , . ,
skb
. , , . - , , .
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0;
- . ICMP Echo, ICMP- Echo-.
NF_ACCEPT
, ,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; }
, IP. C : - . , ! - , , . . , ICMP .
icmph
:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
skb
, :tail = skb_tail_pointer(skb);
.
, . - ,
cmd_string
,run:
, , , . - , :
schedule_work(&my_work);
. , .schedule_work()
, . . , , kernel panic. ! - , .
Cette fonction est la plus simple. Son nom a été spécifié dans
DECLARE_WORK()
, le type et les arguments acceptés ne sont pas intéressants. Nous prenons la ligne de commande et la passons entièrement au shell. Laissez-le s'occuper de l'analyse, de la recherche de binaires et de tout le reste lui-même.
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);
}
- Nous définissons les arguments sur un tableau de chaînes
argv[]
. Je suppose que tout le monde sait que les programmes fonctionnent réellement de cette manière, et non comme une ligne pleine avec des espaces. - Définition des variables d'environnement. J'ai inséré uniquement PATH avec un ensemble minimal de chemins, en m'attendant à ce que tous soient déjà combinés
/bin
avec/usr/bin
et/sbin
avec/usr/sbin
. Les autres voies comptent rarement dans la pratique. - , !
call_usermodehelper()
. , , . , , . , (UMH_WAIT_PROC
), (UMH_WAIT_EXEC
) (UMH_NO_WAIT
).UMH_KILLABLE
, .
La construction des modules du noyau se fait via un framework make du noyau. Il est 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. Le icmpshell.ko et les cibles propres utilisent entièrement ce cadre. In obj-m
spécifie le fichier objet qui sera converti en module. La syntaxe qu'il supprime main.o
dans icmpshell.o
( icmpshell-objs = main.o
) ne me semble pas très logique, mais qu'il en soit ainsi.
Mettre: . Charger: . Fait, vous pouvez vérifier: . Si un fichier apparaît sur votre ordinateur et qu'il contient la date à laquelle la demande a été envoyée, alors vous avez tout fait correctement et j'ai tout fait correctement.
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
make
insmod icmpshell.ko
sudo ./send.py 45.11.26.232 "date > /tmp/test"
/tmp/test
Conclusion
Ma première expérience en génie nucléaire a été beaucoup plus simple que ce à quoi je m'attendais. Même sans expérience en développement C, en me concentrant sur les astuces du compilateur et la sortie Google, j'ai pu écrire un module de travail et me sentir comme un hacker du noyau, et en même temps un script kiddie. De plus, je suis allé sur la chaîne Kernel Newbies, où ils m'ont dit d'utiliser
schedule_work()
au lieu d'appeler call_usermodehelper()
à l'intérieur du crochet lui-même et m'ont fait honte, soupçonnant à juste titre une arnaque. Une centaine de lignes de code m'a coûté environ une semaine de développement pendant mon temps libre. Une expérience réussie qui a détruit mon mythe personnel sur l'écrasante complexité du développement de système.
Si quelqu'un accepte de faire une révision de code sur github, je vous en serais reconnaissant. Je suis à peu près sûr que j'ai fait beaucoup d'erreurs stupides, en particulier en ce qui concerne les cordes.