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

Difference between revisions of "LKM"

From NetSec
Jump to: navigation, search
(Example driver)
(Example drivers)
Line 493: Line 493:
  
 
static int device_open(struct inode *, struct file *);
 
static int device_open(struct inode *, struct file *);
 +
 +
//file_operations struct mapping file operations to custom functions
  
 
static struct file_operations fops =
 
static struct file_operations fops =

Revision as of 23:07, 24 June 2016

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;                             
   loff_t (*llseek) (struct file *, loff_t, int);    
   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 
   ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);  
   ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); 
   ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);          
   ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);       
   int (*iterate) (struct file *, struct dir_context *);       
   unsigned int (*poll) (struct file *, struct poll_table_struct *);    
   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
   long (*compat_ioctl) (struct file *, unsigned int, unsigned long);  
   int (*mmap) (struct file *, struct vm_area_struct *);          
   int (*mremap)(struct file *, struct vm_area_struct *);         
   int (*open) (struct inode *, struct file *);          
   int (*flush) (struct file *, fl_owner_t id);        
   int (*release) (struct inode *, struct file *);         
   int (*fsync) (struct file *, loff_t, loff_t, int datasync);  
   int (*aio_fsync) (struct kiocb *, int datasync);         
   int (*fasync) (int, struct file *, int);                 
   int (*lock) (struct file *, int, struct file_lock *);    
   …and so on
};
 

So lets say we just want our driver to do something every time the device is opened, and not do anything else with it (for simplicity's sake). We can see the file_operations struct has a definition for an open() function:

 
   int (*open) (struct inode *, struct file *);             // first operation performed on a device file
 

We need to implement the function we want to map to open() in our own code, first of all:

 
static int device_open(struct inode *inode_pointer, struct file *file_pointer)
{
	printk(KERN_INFO "The device was just opened!\n");
	return 0;
}
 

Now that we've done that, we can create a "struct file_operations" variable that maps our function to the declaration in <linux/fs.h>:

 
static struct file_operations fops =
{
	.open = device_open,	//map device_open() to the open() file op
};
 

Note that if your struct declaration appears before you define device_open() in the code, you will need a function prototype:

 
static int device_open(struct inode *, struct file *);
 

In this case we have used the open() hook, but you can do this with any of the decalarations in the file_operations struct that you want to hook.

Registering a major number

Now that we have a struct that defines how our LKM will interact with the device file once it's created, we can begin going through the steps of creating the device.

If you want to open a device file, you will need to register a major number in your init function. This requires you to use the register_chrdev() function. This function returns the major number and takes 3 arguments. The first argument is the major number you want to request - or 0 to dynamically assign an unused number. The second argument is a string containing the name of the device you would like register. The third argument is the address of the file_operations struct we mentioned earlier.

Here is an example of how registration of a major number works:

 
static int majorNumber; //this should be defined outside of the init function, just in case
majorNumber = register_chrdev(0, "myModule", &fops);
 

If successful, a positive integer will be returned into majorNumber, which is the major number you have been assigned. Otherwise, the return value will be negative. There is a corresponding unregister_chrdev() function which should be part of your cleanup function:

 
unregister_chrdev(majorNumber, "myModule");
 

Registering a class

Devices have both a device name and a class name. Once you have registered a major number for the device, you must register a class.

 
static struct class* myModule_class = NULL;	//this should be defined outside of the init function
myModule_class = class_create(THIS_MODULE, "myMod");
 

After you create the class, there will be a corresponding folder at /sys/class/myMod. As with the major number, there are corresponding functions to destroy the class, which should be part of your cleanup before the major number is unregistered:

 
class_unregister(myModule_class);
class_destroy(myModule_class);
 

Registering the device

Now that we have registered a major number and a class for the device, we are finally ready to create the device itself, which requires that you have both the major number and the device class:

 
static struct device* myModule_device = NULL;	//this should be defined outside of the init function
myModule_device = device_create(myModule_class, NULL, MKDEV(majorNumber, 0), NULL, "mod0");
 

Once this is done, your device file should have been faithfully created at /dev/mod0. If it's there, then your device was successfully created. Remember that we mapped open() on our device to our device_open() function in the file_operations pointer that we passed when we registered a major number. Therefore, the function should execute every time someone tries to open the device. You can test to see if the open() hook works by using any command that opens the device:

 
$ cat /dev/mod0
 

Then check dmesg, and you should see something like:

 
[22973.469043] The device was just opened!
 

If you see that, then congratulations! You have successfully created a character driver that prints to the kernel every time it is opened. Note that, like the other functions, there is a corresponding function to destroy the device. This should be part of your cleanup, before you unregister the class or the major number:

 
device_destroy(myModule_class, MKDEV(majorNumber, 0));
 

Example drivers

If you put all the sample code above into a single program, you get a functional (though pointless) device driver that simply writes to the kernel output every time it is opened.

 
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Dade Murphy");
MODULE_DESCRIPTION("1507 systems in one day");
MODULE_VERSION("0.1");
 
//initialize variables used by the driver
 
static int majorNumber;
static struct class* myModule_class = NULL;
static struct device* myModule_device = NULL;
 
//function prototype for our file operations struct
 
static int device_open(struct inode *, struct file *);
 
//file_operations struct mapping file operations to custom functions
 
static struct file_operations fops =
{
	.open = device_open
};
 
static int __init myModule_init(void)
{
	majorNumber = register_chrdev(0, "myModule", &fops);
	myModule_class = class_create(THIS_MODULE, "myModule");
	myModule_device = device_create(myModule_class, NULL, MKDEV(majorNumber, 0), NULL, "mod0");
	printk(KERN_INFO "Hello, my major number is %d.\n", majorNumber);
	return 0;
}
 
static void __exit myModule_exit(void)
{
	device_destroy(myModule_class, MKDEV(majorNumber, 0));
	class_unregister(myModule_class);
	class_destroy(myModule_class);
	unregister_chrdev(majorNumber, "myModule");
	printk(KERN_INFO "This device, class and major number were successfully destroyed.\n");
}
 
static int device_open(struct inode *inode_pointer, struct file *file_pointer)
{
	printk(KERN_INFO "The device was just opened!\n");
	return 0;
}
 
module_init(myModule_init);
module_exit(myModule_exit);
 

For a more functional example of the power of the functionality of a device driver, check here for a piece of code that allows data to be both written to and read from with the cat command, storing the last thing that was sent to it.

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.