📜 ⬆️ ⬇️

Writing a simple Linux kernel module

Capture the Golden Ring-0


Linux provides a powerful and extensive API for applications, but sometimes it is not enough. To interact with equipment or perform operations with access to privileged information in the system, a kernel driver is needed.

The Linux kernel module is a compiled binary code that is inserted directly into the Linux kernel, running in ring 0, the innermost and least protected command execution ring in the x86–64 processor. Here the code is executed completely without any checks, but at an incredible speed and with access to any system resources.

Not for mere mortals


Writing a Linux kernel module is not for the faint of heart. By changing the kernel, you risk losing data. There is no standard protection in the kernel code, as in normal Linux applications. If you make a mistake, hang the whole system.

The situation is worsened by the fact that the problem does not necessarily manifest immediately. If the module hangs up the system immediately after loading, then this is the best failure scenario. The more code there, the greater the risk of endless cycles and memory leaks. If you are not careful, the problems will gradually increase as the machine works. In the end, important data structures and even buffers can be overwritten.
')
You can mostly forget the traditional application development paradigms. In addition to loading and unloading a module, you will write code that reacts to system events, and does not work according to a sequential pattern. When working with the kernel, you write the API, not the applications themselves.

You also do not have access to the standard library. Although the kernel provides some functions like printk (which serves as a replacement for printf ) and kmalloc (works like malloc ), you are mostly left alone with the hardware. In addition, after unloading the module, it should be completely cleaned after itself. There is no garbage collection.

Required components


Before you begin, make sure you have all the necessary tools for the job. Most importantly, I need a machine for Linux. I know this is unexpected! Although any Linux distribution will work, in this example I use Ubuntu 16.04 LTS, so if you use other distributions, you may need to slightly change the installation commands.

Secondly, you need either a separate physical machine or a virtual machine. Personally, I prefer to work on a virtual machine, but choose yourself. I do not advise you to use your main machine due to data loss when you make a mistake. I say “when”, not “if”, because you will definitely hang up the car at least several times in the process. Your latest code changes may still be in the write buffer at the time of a kernel panic, so your sources may be damaged. Testing in a virtual machine eliminates these risks.

Finally, at least you need to know a little C. The C ++ working environment is too large for the kernel, so you need to write on a clean bare C. Some interaction with the equipment will not interfere with some knowledge of the assembler.

Development Environment Setup


On Ubuntu you need to run:

 apt-get install build-essential linux-headers-`uname -r` 

Install the most important development tools and kernel headers needed for this example.

The examples below assume that you are running as a regular user, not root, but that you have sudo privileges. Sudo is required to load kernel modules, but we want to work as much as possible outside of the root.

Getting started


Let's start writing the code. Prepare our environment:

 mkdir ~/src/lkm_example cd ~/src/lkm_example 

Launch your favorite editor (in my case it’s vim) and create a file lkm_example.c following content:

 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“A simple example Linux module.”); MODULE_VERSION(“0.01”); static int __init lkm_example_init(void) { printk(KERN_INFO “Hello, World!\n”); return 0; } static void __exit lkm_example_exit(void) { printk(KERN_INFO “Goodbye, World!\n”); } module_init(lkm_example_init); module_exit(lkm_example_exit); 

We have constructed the simplest possible module, we will consider in more detail the most important parts of it:


However, while we can not compile this file. Need a makefile. This basic example is still enough. Note that make very picky about spaces and tabs, so make sure you use tabs instead of spaces where appropriate.

 obj-m += lkm_example.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 

If we run make , it should successfully compile our module. The result will be the lkm_example.ko file. If any errors pop up, check that the quotes in the source code are set correctly, and not randomly in UTF-8.

Now you can embed the module and test it. To do this, run:

 sudo insmod lkm_example.ko 

If everything is normal, then you will not see anything. The printk function does not output to the console, but to the kernel log. To view you need to run:

 sudo dmesg 

You should see the string “Hello, World!” With a time stamp at the beginning. This means that our kernel module has loaded and successfully written to the kernel log. We can also check that the module is still in memory:

 lsmod | grep “lkm_example” 

To remove a module, run:

 sudo rmmod lkm_example 

If you run dmesg again, you will see “Goodbye, World!” In the log. You can run lsmod again and make sure the module is unloaded.

As you can see, this testing procedure is a bit tedious, but it can be automated by adding:

 test: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg 

at the end of the Makefile, and then running:

 make test 

to test the module and check the output to the kernel log without having to run separate commands.

Now we have a fully functional, albeit completely trivial kernel module!

A bit more interesting


Let's dig a little deeper. Although kernel modules are capable of performing all kinds of tasks, interacting with applications is one of the most common use cases.

Since applications are not allowed to view memory in kernel space, they have to use an API to interact with them. Although technically there are several ways to do this, the most familiar is to create a device file.

You may have previously dealt with device files. Commands mentioning /dev/zero , /dev/null and the like interact with zero and null devices that return the expected values.

In our example, we return “Hello, World”. Although this is not a particularly useful feature for applications, it still demonstrates the process of interacting with an application through a device file.

Here is the full listing:

 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <asm/uaccess.h> MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“A simple example Linux module.”); MODULE_VERSION(“0.01”); #define DEVICE_NAME “lkm_example” #define EXAMPLE_MSG “Hello, World!\n” #define MSG_BUFFER_LEN 15 /* Prototypes for device functions */ static int device_open(struct inode *, struct file *); static int device_release(struct inode *, struct file *); static ssize_t device_read(struct file *, char *, size_t, loff_t *); static ssize_t device_write(struct file *, const char *, size_t, loff_t *); static int major_num; static int device_open_count = 0; static char msg_buffer[MSG_BUFFER_LEN]; static char *msg_ptr; /* This structure points to all of the device functions */ static struct file_operations file_ops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release }; /* When a process reads from our device, this gets called. */ static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) { int bytes_read = 0; /* If we're at the end, loop back to the beginning */ if (*msg_ptr == 0) { msg_ptr = msg_buffer; } /* Put data in the buffer */ while (len && *msg_ptr) { /* Buffer is in user data, not kernel, so you can't just reference * with a pointer. The function put_user handles this for us */ put_user(*(msg_ptr++), buffer++); len--; bytes_read++; } return bytes_read; } /* Called when a process tries to write to our device */ static ssize_t device_write(struct file *flip, const char *buffer, size_t len, loff_t *offset) { /* This is a read-only device */ printk(KERN_ALERT “This operation is not supported.\n”); return -EINVAL; } /* Called when a process opens our device */ static int device_open(struct inode *inode, struct file *file) { /* If device is open, return busy */ if (device_open_count) { return -EBUSY; } device_open_count++; try_module_get(THIS_MODULE); return 0; } /* Called when a process closes our device */ static int device_release(struct inode *inode, struct file *file) { /* Decrement the open counter and usage count. Without this, the module would not unload. */ device_open_count--; module_put(THIS_MODULE); return 0; } static int __init lkm_example_init(void) { /* Fill buffer with our message */ strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN); /* Set the msg_ptr to the buffer */ msg_ptr = msg_buffer; /* Try to register character device */ major_num = register_chrdev(0, “lkm_example”, &file_ops); if (major_num < 0) { printk(KERN_ALERT “Could not register device: %d\n”, major_num); return major_num; } else { printk(KERN_INFO “lkm_example module loaded with device major number %d\n”, major_num); return 0; } } static void __exit lkm_example_exit(void) { /* Remember — we have to clean up after ourselves. Unregister the character device. */ unregister_chrdev(major_num, DEVICE_NAME); printk(KERN_INFO “Goodbye, World!\n”); } /* Register module functions */ module_init(lkm_example_init); module_exit(lkm_example_exit); 

Testing an improved example


Now our example does more than just output the message during loading and unloading, so a less rigorous testing procedure is needed. Modify the Makefile to load the module only, without unloading it.

 obj-m += lkm_example.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 test: # We put a — in front of the rmmod command to tell make to ignore # an error in case the module isn't loaded. -sudo rmmod lkm_example # Clear the kernel log without echo sudo dmesg -C # Insert the module sudo insmod lkm_example.ko # Display the kernel log dmesg 

Now after running make test you will see the output of the highest device number. In our example, it is automatically assigned by the kernel. However, this number is needed to create a new device.

Take the number you received from the make test and use it to create a device file so that you can communicate with our kernel module from user space.

 sudo mknod /dev/lkm_example c MAJOR 0 

(in this example, replace MAJOR with the value resulting from running make test or dmesg )

The c parameter in the mknod command tells mknod that we need to create a character device file.

Now we can get the contents from the device:

 cat /dev/lkm_example 

or even through the dd :

 dd if=/dev/lkm_example of=test bs=14 count=100 

You can also access this file from applications. It does not have to be compiled applications - even Python, Ruby and PHP scripts have access to this data.

When we are done with the device, remove it and unload the module:

 sudo rm /dev/lkm_example sudo rmmod lkm_example 

Conclusion


Hope you enjoyed our pranks in kernel space. Although the examples shown are primitive, these structures can be used to create your own modules that perform very complex tasks.

Just remember that in the kernel space everything is under your responsibility. There is no support or second chance for your code. If you are doing a project for a client, plan in advance a double, if not triple, time for debugging. Kernel code should be as perfect as possible to ensure the integrity and reliability of the systems on which it runs.

Source: https://habr.com/ru/post/343828/


All Articles