This is an old revision of the document!
LKRG is free and Open Source project distributed primarily in source code form. You can download it and prepare custom build by yourself. However, if you would rather use a commercial product tailored for your specific operating system, please consider LKRG Pro, which is distributed primarily in the form of “native” packages for the target operating systems and in general is meant to be easier to install and use while delivering optimal performance. Additionally, you will help in development of the project (economically). LKRG Pro is available <here>.
The Linux Kernel Runtime Guard protects system by comparing hashes which are calculated from the most important kernel region / sections / structures with the internal database hashes. Additionally, special efforts have been made to individually protect all extensions of the kernel (modules). To make the project fully functional, the module should be initially loaded on a clean system – e.g. directly after installation or after booting clean system. At this moment it is possible to create a trusted database of hashes.
There are two main ideas behind the Linux Kernel Runtime Guard (LKRG):
Currently, we maintain two versions of the LKRG project:
Security promises delivered by the LKRG can't be briefly summarized and need to be discussed in details. Please navigate to the following page describing the threat model of the project:
The database contains hashes calculated from the following parts of the system:
This component is very critical from the stability and security point of view for various reasons. It is very common that CPU has more than one core. Each core from each CPU must be protected separately and functions generating or checking hashes must be run on each individual core as exclusive task to guarantee nothing is changing the data in the meantime.
NOTE: component implementing this functionality is one of the most complicated in the system. Since multiple cores might be present in the system, it is possible that any random core in any random time can go offline and/or online. Whenever this situation happens, LKRG must rebuild internal database and protect the new cores’(if became online) most critical data. Additionally, entire CPU might go offline or be hot-plugged in. This is especially true on the Virtual Machines (VMs). It is not a rare situation when a new Virtual CPUs are assigned to the currently run VM. New (V)CPU might have multiple cores as well, and situation of hot-plugging (V)CPU must be correctly handled by the LKRG to be effective and provide the same functionality whenever this situation appears.
Hot-plugging (V)CPU forces to take into account another corner case scenario, when currently running system has only 1 (V)CPU with 1 core and there is plugged a new core or (V)CPU. Linux boots as UniProcessor (UP) kernel if platform has only 1 (V)CPU with only 1 core. However, as soon as additional core or (V)CPU appears, dynamic SMP boot kernel process will be launched. Newly dynamically loaded SMP kernel overwrites some UP macros, changes the assembly code of the kernel AND all loaded modules! This means entire database with hashes must be recalculated! LKRG correctly takes all of these situations into account and handles all of these corner case scenarios.
For each individual core in all (V)CPUs IPI are sent to exclusively run LKRG function which gathers and calculates critical CPU data including:
Additionally, LKRG keeps information about:
This covers almost entire Linux kernel itself, like syscall tables, all procedures, all function, all IRQ handlers, etc.
Linux Kernel exception table
Entire Linux Kernel .rodata section – it should never change during the running system.
Optionally, this might be enabled but by default it is not taken into account. The reason behind that is that some of the memory ranges might change assignment by dynamically loaded drivers. In corner cases it is also possible that kernel itself may change it. If administrator is 100% sure what he is doing, this section might be enabled.
LKRG is trying to discover how many modules are there currently in the system and keeps tracking them. For each individual module the following information is tracked down based on the module link list:
For each individual module the following information is tracked down based on the KOBJs:
Both information must match (if they exist in both places) and each of them is being tracked individually. Additionally, the following information is being tracked down:
The function for checking the system integrity will be executed:
This list is not closed and will be extended.
Based on the assumption that preventing from the unsupported modifications of the Linux kernel is correctly implemented, LKRG “exports” to the user-mode three very useful features:
The main idea behind this feature is to be able to harden certain processes even from the highest privileged accounts (like “root” account) and to warrant that some secrets remain inaccessible. Proper implementation of Protected Process (PP) is not trivial from a couple of reasons:
There is couple of “official” ways how to interact with the processes, and all of them needed to be hardened:
Additionally, process can be affected via direct memory access:
LKRG leverages *kprobes interfaces to “lock down” all possible ways of interacting with processes. As an end-result no one from user-space is able to interact with the process protected by LKRG. There is one exception for compatibility reasons:
LKRG maintains its own red-black tree to track all PP. When process dies it is automatically removed from the list. There are 2 ways how the process might become Protected Process:
The main idea behind this feature is to be able to harden certain files even from the highest privileged accounts (like “root” account) and to warrant that some secrets remain inaccessible. Proper implementation of Protected File (PF) is not trivial from a couple of reasons:
Files can be modified / deleted using official API, as well as indirectly changed via raw disk access. Both methods must be stopped:
There could be 2 ways of doing it:
2nd option is used by LKRG client user-mode application. Additionally, LKRG makes it PF by default.
Works exactly the same as PF feature but allows file to be opened only in “append” mode. The main idea behind this feature is to be able to harden certain files even from the highest privileged accounts (like “root” account) and to warrant that some secrets remain inaccessible but at the same time give a possibility to append new information to it. Such a functionality might be desired for certain files (e.g. log files) to provide warranty that information which was already written down won't be modified neither deleted.
Proper implementation of the Protected Features requires from LKRG to leverage and virtually extend CAP_SYS_RAWIO capability. Unfortunately, some of the software (e.g. Xorg or doesmu) requires this capability for proper behavior (e.g. accessing /dev/mem device which LKRG must lock down). If Protected Features are being enabled such a software won't properly work.
If you still want to use Protected Features and run the software which requires access to /dev/mem device (uses CAP_SYS_RAWIO capability) you might initialize and run desired software before you load LKRG into the kernel. By doing it, desired software should properly gain necessary resources and correctly use it and LKRG will lock down access to that device after that fact. Note: If for any reasons such a software tries to regain access to restricted device it won't be allowed anymore and proper functionality will be broken.
Some of the examples how to use LKRG can be found here.
As soon as LKRG is loaded in the system, none of the .text section modifications is allowed. This is also true for the official Linux APIs which sometimes does patch(!) it. One of the examples might be kprobes interface. It is injecting 0xCC instruction on the monitored function. That’s why all modules using kprobes must be loaded BEFORE LKRG. Linux kernel can be compiled to heavily consume low-level runtime patching mechanism called “jump label” (CONFIG_JUMP_LABEL=y). Most of the Linux distributions provide kernel compiled with this option (sometimes extra sub-options - *_JUMP_LABEL). It makes Linux kernel a heavily self-modifying code which is very troublesome for this project (since we are comparing hashes!). Especially, *_JUMP_LABEL is just a low-level mechanism which might be consumed by higher level kernel layers or macros, e.g.:
To mitigate this problem LKRG does the following:
NOTE: The following attack is still possible – find injected NOPs (by *_JMP_LABEL) and overwrite them using 'jmp' instruction. Destination of this instruction must point to the random address at the same “symbol name rage”. It allows the attacker to create a rootkit based only on ROP. A few comments are needed here:
There is an undesirable situation in SMP Linux machines when sending an IPI. Unfortunately, it might influence the state of the kernel and generating very confusing logs. They appear to suggest that the problem resides on the correct execution context which is killed and dumped, but not on the actually problematic context, which might not be dumped. This makes it hard to root-cause the problem even if one is aware of this shortcoming of the killings and the logging. More details about it can be found here:
GPLv[2/3] - need to decide.
All rights reserved to Adam 'pi3' Zabrocki.
Patreon.
I would like to thank the following people who helped me at some point during development of this project: