LKM
LKM stands for "Linux Kernel Module" or "Loadable Kernel Module". As the name implies, it is a way to allow code to interact directly with the kernel and extend its functionality. The ability to insert modular components into the kernel allows it to remain relatively lightweight, as it does not need to include every driver ever created. The ability to load modules on-the-fly also saves us from having to recompile every time a change needs to be made.
It goes without saying that you need root to modify the kernel. With this restriction in mind, however, LKMs can be very powerful if used correctly, since the kernel operates under significantly elevated privileges compared to userland. In particular, the functionality provided by extending the kernel can be used to great effect in the development of Rootkits.
LKMs interact with your system on the kernel level, executing with the highest possible level of privilege. A poorly-designed kernel module may make your OS unstable, corrupt your filesystem and even brick your computer. You have been warned. |
Contents
You can see a list of currently loaded kernel modules in two ways:
$ lsmod $ cat /proc/modules |
You can (as root) add new modules to your kernel with the insmod and rmmod commands:
$ insmod modname.ko $ rmmod modname |
These two utilities provide a simple, clean way to insert or remove modules from the kernel. If you need more advanced control over the insertion, removal and alteration of modules in the kernel, use the more fully-featured modprobe utility instead.
Writing a basic LKM
Linux kernel modules are written in C and compiled from one or more source files into a kernel object (.ko) file. In order to write an LKM, you will need a strong grasp of the fundamentals of C programming and at least a basic understanding of the way linux manages files, processes and devices.
Although they are written in C, there are several differences you should keep in mind before you begin writing your first module.
- There is no standard entry point for an LKM - no main() function. Instead, an initialization function runs and terminates when the module is first loaded, setting itself up to handle any requests it receives - an event-driven model.
- LKMs operate at a much higher level of privilege than userland programs. In addition to being able to do and access more, this means that they are assigned higher priority when handing out CPU cycles and resources. A poorly-written LKM can easily consume too much of a machine's processing power for anything else to function properly.
- LKMs do not have automatic cleanup, garbage collection, or many of the other convenience functionality that userland applications do. If you allocate memory without freeing it, it will remain allocated. If your module continues to allocate memory over time, it will negatively affect your system's performance.
- LKMs can be simultaneously accessed by multiple processes, and they need to be able to gracefully handle being interrupted. If two processes ask a module for output at the same time, it needs to be able to keep track of which is which and avoid mixing the data.
Essential includes
A large-number of low-level and kernel-level headers are available for inclusion, as we will see when designing more fully-featured modules. However, in order to support a module's basic functionality, we will need only three includes:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> |
<linux/init.h> contains macros needed for various purposes, such as marking up functions such as __init and __exit.
<linux/module.h> is the core header for loading modules into the kernel. It includes the macros and functions that allow you to register various aspects of your module with the kernel.
<linux/kernel.h> provides various functions and macros for interacting with the kernel - for example, this header is where we find the printk() function.
Registering your module
Introduced by <linux/module.h> is a series of macros used to declare information about your LKM. This information will be displayed when someone uses a command like modinfo on your module:
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Dade Murphy"); MODULE_DESCRIPTION("1507 systems in one day."); MODULE_VERSION("0.1"); |
Registering parameters
It is possible to pass command-line arguments to the module at the time it is inserted into the kernel. In order to specify a parameter for your module, you must first create a static variable initialized with a default value. As a rule, variables within kernel modules should be static and not global, as global variables are shared kernel-wide.
The next step is to register the parameter with the module_param() function - and optionally with MODULE_PARM_DESC(), which is used to give the parameter some descriptive text for modinfo. The module_param() function takes three arguments:
- The variable used to store the parameter.
- The datatype of the parameter, which can be one of: byte, int, uint, long, ulong, short, ushort, bool, an inverse Boolean invbool, or a char pointer charp.
- The permissions of the parameter - these can either be classic octal permissions(i.e. "0664") or the macro equivalents (i.e. "S_IRUSR|S_IWUSR").
For example:
static char *arg1 = "default"; module_param(arg1, charp, 0664); MODULE_PARM_DESC(arg1, "The description to display in /var/log/kern.log"); |
Initialization and cleanup
In order for your module to actually do anything after insertion, it needs an __init and __exit function. Any setup, preparation of devices, hooking of syscalls and so on should go into the initialization function. Any cleanup, deallocation of memory, and restoration of changes should go into the cleanup function.
To define the LKM initialization function, create a static function with the "int __init" datatype, which returns 0 on success. This is the function that will execute when the module is loaded into the kernel. The __init macro specifies that the function is only used at initialization time and that it can be discarded after that point:
static int __init myModule_init(void) { printk(KERN_INFO "Hello %s from this example LKM!\n", arg1); return 0; } |
The exit function is similar - it should be of type "void __exit", and is executed when the module is unloaded from the kernel:
static void __exit myModule_exit(void) { printk(KERN_INFO "Goodbye %s from this example LKM!\n", arg1); } |
After you have defined your init and exit functions, you must register them so that the kernel knows about them:
module_init(myModule_init); module_exit(myModule_exit); |
Example code
Based on all of the examples we have given so far, it is possible to construct a (very basic) kernel module. It won't do much besides print to the kernel log when it is loaded or unloaded, but it should compile into a kernel object without any issues.
module.c #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Dade Murphy"); MODULE_DESCRIPTION("1507 systems in one day."); MODULE_VERSION("0.1"); static char *arg1 = "default"; module_param(arg1, charp, 0664); MODULE_PARM_DESC(arg1, "The description to display in /var/log/kern.log"); static int __init myModule_init(void) { printk(KERN_INFO "Hello %s from this example LKM!\n", arg1); return 0 } static void __exit myModule_exit(void) { printk(KERN_INFO "Goodbye %s from this example LKM!\n", arg1); } module_init(myModule_init); module_exit(myModule_exit); |
Compiling your LKM
In order to compile the module we have just written, you will need to write a Makefile that looks something like this:
obj-m+=myModule.o all: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean |
The first line of this module is a goal definition, which defines the object to be built. The "obj-m" keyword defines a loadable module goal, as opposed to something like "obj-y" which would be a built-in object goal. The remaining lines are more like a traditional Makefile. The -C parameter is used to make sure we are in the module directory before performing any make tasks, using "uname -r" to figure out where that is. The "M=$(PWD)" part tells the make command where the actual project files exist, and the "modules" target is the default target for kernel modules.
In order to compile the module, simply run "make" as root in the same directory as the Makefile and myModule.c; assuming your code did not contain any errors and you have the correct version of the linux kernel headers, it should compile, producing a number of files in the current directory. One of these files will be called something like myModule.ko - this is your kernel object file.
You can check out information about your module with:
$ modinfo myModule.ko |
In order to insert it into the kernel, do:
$ insmod myModule.ko arg1=haxxor
Then, to confirm it has been inserted:
$ lsmod | grep myModule
To unload it from the kernel:
$ rmmod myModule
Now you have managed to compile your module, insert it and remove it from the kernel, but how do you know if it actually worked? We used the printk() function to print to the kerne message buffer, so let's check that:
$ dmesg
[ 3728.160984] Hello haxxor from this example LKM! [ 3730.248728] Goodbye haxxor from this example LKM!
$ tail -l 2 /var/log/kern.log
Jun 18 20:04:58 Gibson kernel: [ 3728.160984] Hello haxxor from this example LKM! Jun 18 20:05:00 Gibson kernel: [ 3730.248728] Goodbye haxxor from this example LKM!
Creating character devices
Device operations
Hooking system calls
See also
The Linux Kernel Module Programming Guide- an outdated but solid tutorial covering many of the concepts that will help you to understand the linux kernel.