📜 ⬆️ ⬇️

Intercepting Linux system calls using LSM



Recently there were such tasks: to assemble the Linux kernel, write a module for it and use it to intercept system calls. And if I did the first two without any problems, then in the process of doing the third, I had the impression that working with system calls went out of fashion 10 years ago.

Periodically, I found articles on the Internet that were close to what I was looking for, some were even very well written, but everyone had a significant drawback - they were outdated.

Initial conditions



To edit the kernel configuration in pseudographic mode, you need ncurses:
')
sudo apt-get update sudo apt-get install libncurses5-dev 

Building a clean kernel


I recommend building a clean kernel before starting the development of modules. There are 2 reasons for this:

  1. The first kernel build is a fairly long process. Most often it lasts from 20 minutes to 3 hours. If you build in advance, you will get most of the kernel binaries that will not need to be recompiled. This will allow to concentrate fully on the development of the module, without suffering, waiting for the answer to the question “Will my first Hello World start?”

  2. Having successfully assembled a clean core, you will see that at this stage there are no problems and that you can proceed to the next one. Sometimes booting with a freshly assembled kernel can be unsuccessful, and if you compiled it with the module, it will be difficult to understand what the system put.

So, the kernel build:

  1. Download the source archive:

     wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-xxxtar.xz 

    where xxx is the kernel version.

    Or you can download the archive with your hands from kernel.org

  2. Extract data from the archive:

     tar -xpJf linux-xxxtar.xz 

  3. Go to the newly unzipped folder:

     cd linux-xxx 

  4. We generate the default kernel configuration:

     make defconfig 

    For advanced:
     make menuconfig 

    The pseudo graphic kernel configuration interface opens. Most options are not difficult to understand, but without an understanding of each variable parameter it is very easy to break everything. For the first build, I recommend using the default parameters.

  5. We start directly the assembly of a kernel and modules:

     make && make modules 

    The assembly will last from 20 minutes to 3 hours.

    Life hacking:
     make -jx && make modules -jx 

    Where x is the number of processor cores + 1. That is, in my case x = 5.
    This value is recommended to be installed in all manuals, but in fact you can set any value. I decided to “double the number of cores”, that is, to start the assembly with the -j 9 parameter. This does not speed up the assembly by 2 times, but increases the competitiveness of the assembly processes in relation to all other processes in the system.

    In addition, in the system monitor (gnome-system-monitor) to all make-processes, I set the maximum priority. The system literally hung up after that, but the assembly was completed in 6 minutes. Use this method at your own risk.

    After a successful build, you need to install everything that we have collected. This requires root rights.

  6. Setting Headers:

     sudo make headers_install 

  7. Installation of modules:

     sudo make modules_install 

  8. Installing the kernel directly:

     sudo make install 

  9. Installation commands must generate an initial RAM disk and update grub. If suddenly the initial RAM disk is not generated - the system with the new kernel will not start.

    You can check this by the presence of the file "/boot/initrd.img-xxx" (xxx is the kernel version)
    If the file is not found, generate it with your hands:

     sudo update-initramfs –c –k xxx 

  10. Update grub bootloader:

     sudo update-grub 

Done! After restarting, the system will start with the new kernel. Check current kernel version:

 uname -r 

If suddenly something went wrong, and the system does not boot with a new kernel, reboot the computer, go to advanced options in the grub menu and select a different kernel version (the one you used to boot before, usually the default version is added to the versions )

Module creation


Creating a kernel module is a lot like writing a regular C user program, except for some differences related to the kernel:


Hello world!


Let's take a concrete example to consider “Hello world” as a kernel module. Create a file hello.c in any folder convenient for you:

 // hello.c #include <linux/module.h> #include <linux/kernel.h> static int __init myinit(void) { printk("%s\n","<my_tag> hello world"); return 0; } static void __exit myexit(void) {} module_init(myinit); module_exit(myexit); MODULE_LICENSE("GPL"); 

A regular user program starts with a call to the main () function and works until it returns a value to the system.

The module is essentially a part of the code of the kernel itself, which contains handler functions for some events. The simplest example of such an event is loading or unloading the module itself.

The functions that handle these events are, respectively.

static int __init myinit (void)
static void __exit myexit (void)

They are marked with the __init, __exit macros and are registered using module_init and module_exit as event handlers. The name of these functions can be any, but should not conflict with other functions in the kernel.

Since the kernel does not use the standard C library, we cannot use stdio.h. Instead, we include the file kernel.h, in which the printk function is implemented. This function is similar to printf with the only difference that it displays messages not in the terminal window, but in the system log (/ var / log / syslog).

A lot of messages from the entire system are written to this log, so we need to mark ours with some kind of original tag , so that later with the help of the grep utility we could select only the messages of our module.

Another incomprehensible line is MODULE_LICENSE ("GPL");

It indicates that our module complies with the GPL license. Without this, some of the capabilities inside the kernel will not be available.

Assembly


In order to assemble this module in the same folder as the source code of the module, create a Makefile:

 #       KERNEL_PATH = /path-to-your-kernel/linux-xxx #  ,       obj-m += hello.o all: #  make   -C ,       # KERNEL_PATH.    ,     #    # SUBDIRS -  ,      , #    -   make -C $(KERNEL_PATH) SUBDIRS=$(PWD) modules #   ,      make clean #  ,      clean: rm -f *.o *.mod* Module.symvers modules.order 

After creating the Makefile, go directly to the build:

 make 

After a couple of seconds, the hello.ko file will appear in our folder - a ready-made compiled module.

Loading and unloading


There are 2 ways to load a module into the kernel:

  1. Building the module with the kernel. In this case, the module is loaded as part of the system startup, and the module itself becomes part of the kernel code.

  2. Dynamic loading in an already running system. The above method of creating a module implies just such a method of loading. In this case, loading the module is more like launching a regular user program.

Download module:

 sudo insmod hello.ko 

The insmod command loads a module into kernel space, thereby calling the initialization function.

After that, the module is listed as downloaded. You can check this with the lsmod command:

image

In the initialization function, we added a call to printk , which displays our message to the system log.

To view the system log, there is a dmesg utility:

 dmesg | grep '<my_tag>' 

The above command will output

 <my_tag> hello world 

After we load the module, it will remain hanging in the kernel until it is unloaded. To do this:

 sudo rmmod hello.ko 

This command will call the __exit event handler, but since we have an empty function there, nothing will happen except unloading the module from the kernel.

Life hacking
In order not to enter 2 commands each time for loading and unloading the module during debugging, the value -1 is returned in the initialization function. Such a module, when trying to load, displays an error to the terminal, after which it stops working, but at the same time, the initialization function works completely and correctly, turning, in essence, into an analogue of the main () function of user programs.

 static int __init myinit(void) { printk("%s\n","<my_tag> hello world"); return -1; } 


Next, we will look at the first way to load the module, as well as what this article was originally written for.

Interception of system calls


Unsafe way


Once upon a time, even before the kernel version 2.6, in order to intercept the system call, they wrote a function hook that replaced it: it was executed by another code + it was called directly by the syscall itself (so as not to disrupt the performance of the system).

Since each system call, like a function, has its own address, and in Linux there is a special table where these addresses are stored, the task was reduced to replacing the address of the system call with the address of our function in this table itself.

Later, Linux developers tried to eliminate the possibility of this method, but there are still hacks that allow you to implement this method.

However, it is very unsafe, and I will not describe it. Moreover, to solve the problem at the same time came up with a more elegant and safe solution.

LSM


LSM is a framework for developing kernel security modules. It was created in order to extend the standard DAC security model, to make it more flexible. This framework uses the well-known SELinux security module, as well as several others built into the kernel.

The most valuable thing for us in this framework is that it is implemented through a set of hooks pre-installed into the kernel (in fact, the way I described above, but safe, because the kernel is pre-calculated for the presence of such hooks).

LSM allows you to insert user calls into your hook code, which allows you to safely work with system calls without changing the symbol table.

Everything is very simple. Consider an example of creating a security module foobar that intercepts the mk_dir system call.

Code writing


  1. We find the folder security in the source code for the kernel, create a folder for our module in it, and in it its source code foobar.c:

     // /security/foobar/foobar.c //---INCLUDES #include <linux/module.h> #include <linux/lsm_hooks.h> //---HOOKS //mkdir hook static int foobar_inode_mkdir(struct inode *dir, struct dentry *dentry, umode_t mask) { printk("%s\n","<my_tag> mkdir hook"); return 0; } //---HOOKS REGISTERING static struct security_hook_list foobar_hooks[] = { LSM_HOOK_INIT(inode_mkdir, foobar_inode_mkdir), }; //---INIT void __init foobar_add_hooks(void) { security_add_hooks(foobar_hooks, ARRAY_SIZE(foobar_hooks)); } 

    The lsm_hooks.h file contains the headers of those most predefined hooks, LSM_HOOK_INIT registers the correspondence of foobar_inode_mkdir () to the hook inode_mkdir (), and security_add_hooks () adds our function to the general list of LSM user hooks.

    Thus, each time mkdir is called, our function foobar_inode_mkdir () will be called.

  2. Add the title of our function to the file “/include/linux/lsm_hooks.h”:

     #ifdef CONFIG_SECURITY_FOOBAR extern void __init foobar_add_hooks(void); #else static inline void __init foobar_add_hooks(void) { } #endif 

    All calls occur in the source security.c file (below), with this step we notify it of the existence of our function.

  3. In the file “/security/security.c” we find the function “int __init security_init (void)” and add the following call to its body:

     foobar_add_hooks(); 

Everything, dependences in the code are configured correctly. It remains only to notify the kernel configuration files that we want to assemble it together with our module.

Build Configuration


  1. In the folder with our module (/ security / foobar /) create the Kconfig file:

     config SECURITY_FOOBAR bool "FooBar security module" default y help Any help text here 

    This will create a menu item with our module.

  2. Open the file / security / Kconfig and add the following text immediately after the line “menu“ Security options ”":

     source security/foobar/Kconfig 

    This will add our menu item to the global kernel settings menu.

  3. Create a Makefile in the folder with our module:

     obj-$(CONFIG_SECURITY_FOOBAR) += foobar.o 

  4. Open the Makefile of the entire security section (/ security / Makefile) and add the following lines to it (by analogy with the same lines for other modules):

     subdir-$(CONFIG_SECURITY_FOOBAR) += foobar obj-$(CONFIG_SECURITY_FOOBAR) += foobar/ 

  5. Run the configuration in pseudographic mode:

     make menuconfig 

    If you go to the “Security options” submenu, the first item will be our module, marked with the “y” symbol (we set this default value when we created the Kconfig file), which means that we integrate our module directly into the kernel code.

Assembly


At this stage we carry out the most ordinary assembly of the kernel, as it was described at the beginning of the article. But since we have already pre-assembled a clean core, the process is a bit simpler:

 make && make modules 

make does not require the -j option, as it rebuilds the kernel with our module in a few seconds.

 sudo make install 

Installation of headers and modules is not required; this was done earlier.

Everything!

It remains to reboot the system, after which our module with the interception mkdir will hang in the kernel. As mentioned earlier, we check this:

 dmesg | grep '<my_tag>' 

Consider that in the system, hiding from your eyes, a lot of processes occur, so do not be surprised when you see there are many interceptions.

I hope that this manual will be useful to someone (if someone wrote it instead of me before I started digging into the core, it would save me 2-3 weeks of life).

Any criticism is welcome.
Thanks for attention.

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


All Articles