Questions about this topic? Sign up to ask in the talk tab.

LKM

From NetSec
Revision as of 21:44, 24 June 2016 by Null (Talk | contribs) (Creating character devices)

Jump to: navigation, search
RPU0j.png
LKM is currently in-progress. You are viewing an entry that is unfinished.

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.

RPU0j.png 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.

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:

Makefile

 
obj-m+=module.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 module.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 module.ko - this is your kernel object file.

In order to insert it into the kernel, do:

 
$ insmod module.ko arg1=null
 

Then, to confirm it has been inserted:

 
$ lsmod | grep module
 
module        16384    0
 

To unload it from the kernel:

 
$ rmmod module
 

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 kernel message buffer, so let's check that:

 
$ dmesg
 
[ 3728.160984] Hello null from this example LKM!
[ 3730.248728] Goodbye null from this example LKM!
 
$ tail -l 2 /var/log/kern.log
 
Jun 18 20:04:58 Gibson kernel: [ 3728.160984] Hello null from this example LKM!
Jun 18 20:05:00 Gibson kernel: [ 3730.248728] Goodbye null from this example LKM!
 

Compiling multiple source files

If, for whatever reason, your LKM is composed of multiple source files, your Makefile will look a little different. All you need to do is invent an object name for your combined module, then tell make what object files are part of that module. For example, if you have source files named start.c and stop.c:

Makefile

 
obj-m+=startstop.o
startstop-objs := start.o stop.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
 
 

Permanently adding LKMs

If you're writing a rootkit as a kernel module, you're going to want it to persist and load automatically on boot. In order to do this, you must first install it. First of all, copy it to the correct folder and run depmod:

 
$ cp module.ko /lib/modules/$(uname -r)/misc/
$ depmod
 

The depmod utility will check all of the modules in the /lib/modules/whatever/ folder for dependencies, reading each module and determining what symbols it exports and what symbols it needs. This adds it to modprobe's database, but doesn't set it to automatically load on boot. You must add the name of your LKM (without the .ko) to either /etc/modules or /etc/modules.conf, depending on what distro you're using.

This tells modprobe to automatically try to load that module on boot. To test it, try to reboot and see if your module appears in the output of lsmod. If it does, your module has been loaded and will persist between reboots.

Creating character devices

If you have followed this article to this point, you are able to write a basic kernel module with parameters and define its behavior when loaded or unloaded. Writing your own devices is one of the ways you can extend kernel functionality, allowing for interaction between userland and the mysteries of the kernel.

Like the other devices objects found in the /dev folder, a character device is an object that behaves like a regular file, allowing you to read or write from it. Character devices specifically behave like a pipe, meaning that data is written to or read from them instantly in a byte-by-byte stream.

You can identify the type of a device in the /dev folder when running ls:

 
$ ls -l /dev
 
crw------- 1 root root   10, 175 Jun 18 09:27 agpgart
crw------- 1 root root   10, 235 Jun 18 09:27 autofs
crw------- 1 root root   10, 234 Jun 18 09:27 btrfs-control
crw------- 1 root root    5,   1 Jun 18 09:27 console
crw------- 1 root root   10,  62 Jun 18 09:27 cpu_dma_latency
crw------- 1 root root   10, 203 Jun 18 09:27 cuse
brw-rw---- 1 root disk  254,   0 Jun 18 09:27 dm-0
brw-rw---- 1 root disk  254,   1 Jun 18 11:12 dm-1
brw-rw---- 1 root disk  254,   2 Jun 18 11:12 dm-2
brw-rw---- 1 root disk  254,   3 Jun 18 16:36 dm-3
crw-rw---- 1 root video  29,   0 Jun 18 09:27 fb0
 

As you can see, all character devices have a 'c' in the first columns. The other devices, which are identified by a 'b', are block devices, which have a buffer and which reads, writes and seeks can be used on as though they were a regular file.

Every linux device has a major and minor number. The major number is used by the kernel to identify the correct device driver when the device is accessed, while the minor number is internal and is used differently by different drivers. Listing a device also tells you the major and minor numbers of that device.

For example, in the ls output above we can see that the last line contains the numbers "29, 0". This tells us that the major number for /dev/fb0 is 29, and the minor number is 0. This will be important later, as our LKM will need these numbers to successfully create and interact with a device.

Additional includes

Our module will need the three includes used in the previous examples for basic kernel functionality. In addition, we will need to include the following headers:

 
#include <linux/device.h>		
#include <linux/fs.h>			
#include <asm/uaccess.h>		
 

<linux/device.h> is needed to support the kernel driver model, allowing us to register devices with the kernel.

<linux/fs.h> is needed for linux file system support, allowing us to map our device to a node on the root fs.

<asm/uaccess.h> is used for the copy_to_user() function, which sends data from kernel land to userland via the device.

Defining file operations

Before we get started registering a device, we need to define our file operations. These define the ways in which your driver can interact with the device, once it's created. It should be a "struct file_operations", which is a struct defined in <linux/fs.h>.

We can see a full list of the operations we can define in the file_operations struct, in <linux/fs.h>:

 
struct file_operations {
   struct module *owner;                             // Pointer to the LKM that owns the structure
   loff_t (*llseek) (struct file *, loff_t, int);    // Change current read/write position in a file
   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);    // Used to retrieve data from the device
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);   // Used to send data to the device
   ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);  // Asynchronous read
   ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); // Asynchronous write
   ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);            // possibly asynchronous read
   ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);           // possibly asynchronous write
   int (*iterate) (struct file *, struct dir_context *);                // called when VFS needs to read the directory contents
   unsigned int (*poll) (struct file *, struct poll_table_struct *);    // Does a read or write block?
   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // Called by the ioctl system call
   long (*compat_ioctl) (struct file *, unsigned int, unsigned long);   // Called by the ioctl system call
   int (*mmap) (struct file *, struct vm_area_struct *);                // Called by mmap system call
   int (*mremap)(struct file *, struct vm_area_struct *);               // Called by memory remap system call 
   int (*open) (struct inode *, struct file *);             // first operation performed on a device file
   int (*flush) (struct file *, fl_owner_t id);             // called when a process closes its copy of the descriptor
   int (*release) (struct inode *, struct file *);          // called when a file structure is being released
   int (*fsync) (struct file *, loff_t, loff_t, int datasync);  // notify device of change in its FASYNC flag
   int (*aio_fsync) (struct kiocb *, int datasync);         // synchronous notify device of change in its FASYNC flag
   int (*fasync) (int, struct file *, int);                 // asynchronous notify device of change in its FASYNC flag
   int (*lock) (struct file *, int, struct file_lock *);    // used to implement file locking
   …and so on
};
 

File operation examples

Although open() was used as an example above, any of the file operations in <linux/fs.h> can be implemented in order to extend the functionality of a driver. A few examples are given here.

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.