Welcome back to the second part of this series of tutorial on Linux kernel hacking. Below, you will find the links to the other parts of the series. If you have not read the first part yet, make sure to start from there.

  1. Part one: Introduction to the Linux kernel architecture;
  2. Part two(this one): Building a device driver from scratch;
  3. Part three: Introduction to syscalls, how to create a new syscall;

In this article, we will try to extend our knowledge about Linux programming by building a kernel module that actually does something. The module will create a new character device and then will hash a string from userspace using MD5, SHA1 or SHA256. The user will be able to choose the hashing algorithm alongside the input string. This little project(~213LOC) is a great way to get started in the world of kernel programming since it allows us to introduce the concept of character device and to use some internal Linux APIs.

Before getting started with the actual code, we need to explain what devices actually are.

UNIX devices

A UNIX device is a special file that act as an interface between an userspace program and a device driver(i.e. the program that controls a particular device attached to the computer). Devices are stored under the /dev directory and support standard input/output system calls. There are two categories of devices under UNIX: character and block devices.

Character devices

A character device is a device that provides unbuffered access to the hardware device. This kind of device usually manages small amount of data and the supported operations(e.g. read and write) are usually performed byte by byte. Examples of such devices are keyboards, mouse, game controllers and so on. A character device supports the following operations:

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
};

The programmer can reimplement all those functions and then update the file_operations structure. In our case, we will need just 4 of them:

  • open;
  • close;
  • read;
  • write.

Major and Minor number

Each character device is uniquely identified with a fixed value. This identified is formed by two numbers:

  • the major number, which identifies the device type(e.g., a NVMe disk, a SCSI disk, etc.);
  • the minor number, which identifies one device from another one of the same type(e.g. /dev/tty1 vs /dev/tty2).

In order to see the major and the minor number of a device under a UNIX system, we can use the ls(1) utility:

[marco@kerneldev ~]$ ls -l /dev/tty?
crw--w---- 1 root tty 4, 0 Jan  6 21:21 /dev/tty0
crw--w---- 1 root tty 4, 1 Jan  6 21:21 /dev/tty1
crw--w---- 1 root tty 4, 2 Jan  6 21:21 /dev/tty2
crw--w---- 1 root tty 4, 3 Jan  6 21:21 /dev/tty3
crw--w---- 1 root tty 4, 4 Jan  6 21:21 /dev/tty4
crw--w---- 1 root tty 4, 5 Jan  6 21:21 /dev/tty5
crw--w---- 1 root tty 4, 6 Jan  6 21:21 /dev/tty6
crw--w---- 1 root tty 4, 7 Jan  6 21:21 /dev/tty7
crw--w---- 1 root tty 4, 8 Jan  6 21:21 /dev/tty8
crw--w---- 1 root tty 4, 9 Jan  6 21:21 /dev/tty9

The fifth and the sixth column indicates, respectively, the major and the minor number.

Block devices

A block device is a device that provides buffered access to the hardware device. This kind of device usually deals with large amount of data, buffers of data are organized on blocks of any size and seeking operations are very common. Examples of such devices are hard drives and other non-volatile mass storage device.

Since the only data we will pass to the kernel is a string and the type of the hashing algorithm, we can use a character device.

Initialize a character device

In order to set up a new character device we will need to follow a process of instructions:

  • Obtain a unique major number for the device;
  • Register the device class of the character device;
  • Register the character device on the specified class.

We also need to manually check for errors. Error management is very important in kernel programming and in embedded programming in general.

HashKM Source Code

Enough talking, let us see the code(to see the whole source code without the explanation, skip at the end of this section). At first, we need to write the code to set up the character device(the __init function) and to destroy it(the __exit function):

__init and __exit

#define DEV_NAME "hashkm" // Name of the character device
#define DEV_CLASS "hashlkm" // Name of the device class

// Entry point functions(__init and __exit)
static int __init hashkm_init(void) {
	pr_info("%s: Loading, please wait...\n", DEV_NAME);
	
	// Get a new major number for the character device
	dev_num = register_chrdev(0, DEV_NAME, &fo);
	if(dev_num < 0) { // Check errors
		pr_alert("%s: Error while trying to register major number\n", DEV_NAME);
		return dev_num;
	}

	pr_info("%s: New device successfully registered(major number: %d)\n", DEV_NAME, dev_num);
	
	// Register device class
	hashkm_class = class_create(THIS_MODULE, DEV_CLASS);
	if(IS_ERR(hashkm_class)) { // Check errors
		unregister_chrdev(dev_num, DEV_NAME); // Unregister device major number
		pr_alert("%s: unable to register device class\n", DEV_NAME);
		return PTR_ERR(hashkm_class);
	}
	pr_info("%s: Device class successfully register\n", DEV_NAME);

	// Register character device
	hashkm_dev = device_create(hashkm_class, NULL, MKDEV(dev_num, 0), NULL, DEV_NAME);
	if(IS_ERR(hashkm_dev)) {  // Check errors
		class_destroy(hashkm_class);  // Destroy device class
		unregister_chrdev(dev_num, DEV_NAME); // Unregister device major number
		pr_alert("%s: failed to create a new device\n", DEV_NAME);
		return PTR_ERR(hashkm_dev);
	}
	
	pr_info("%s: device driver successfully created\n", DEV_NAME);

	return 0;
}


static void __exit hashkm_exit(void) {
	device_destroy(hashkm_class, MKDEV(dev_num, 0)); // Remove device
	class_unregister(hashkm_class); // Unregister the class
	class_destroy(hashkm_class); // Destroy the class
	unregister_chrdev(dev_num, DEV_NAME); // Unregister device major number
	pr_info("%s: module successfully unloaded\n", DEV_NAME);
}

// Register __init and __exit functions
module_init(hashkm_init);
module_exit(hashkm_exit);

The rest of the module reimplement four default functions:

// File operation struct
// Map local functions to default system calls
static struct file_operations fo = {
	.owner = THIS_MODULE,
	.open = hashkm_open,
	.release = hashkm_close,
	.read = hashkm_read,
	.write = hashkm_write,
};

Open function

Let us start with the open function:

static int hashkm_open(struct inode * inodep, struct file * filep) {
	pr_info("%s: This device has been opened %d time(s)\n", DEV_NAME, (int)++open_count);

	return 0;
}

The open function is usually used to set up additional resources for the rest of the device, but since we do not have anything to configure, we will just print out a message.

Close function

Quite similar is the close function, which is invoked each time an userspace process releases the device:

static int hashkm_close(struct inode * inodep, struct file * filep) {
	pr_info("%s: device closed successfully\n", DEV_NAME);

	return 0;
}

Read function

The read function will send data to userspace each time an userland process uses the read() system call. In order to do that, the Linux kernel offers a specific function called copy_to_user which is defined as follows:

unsigned long copy_to_user(void __user * to, const void * from, unsigned long n);

where

  • to is the destination address in userspace;
  • from is the source address in kernelspace;
  • n is the number of bytes to copy.

In our case, we will copy the result of the hash function(the digest) to the buffer parameter. The number of bytes to copy is defined according to the chosen algorithm(16 bytes for MD5, 20 bytes for SHA1 and 32 for SHA256):

static ssize_t hashkm_read(struct file * filep, char * buffer, size_t len, loff_t * offset) {
	size_t bytes_not_copied = 0;
	size_t bytes_to_copy = (userspace.algorithm == SHA256) ? 32 : (userspace.algorithm == SHA1 ? 20 : 16);

	// copy_to_user returns 0 on success while it returns
	// the number of bytes not copied on errors
	bytes_not_copied = copy_to_user(buffer, digest, bytes_to_copy);
	if(bytes_not_copied) { // Check errors
		pr_warn("%s: Failed to send result to userspace\n", DEV_NAME);
		return -EFAULT;
	}

	return 0;
}

Write function

The write function will read the input string and the algorithm choice from userspace using the write() system call through the copy_from_user function which is defined as follows:

unsigned long copy_from_user(void * to, const void __user * from, unsigned long n);

where

  • to is the destination address in kernel space;
  • from is the source address in userspace;
  • n is the number of bytes to copy.

The only problem with these functions is that it accepts only one address of data from userspace. However, our project requires two parameters to be passed to the module(the string to hash and the hashing algorithm). To solve this problem, we can define a struct in both userspace and kernelspace with the two fields and then pass the address to the write system call. Our struct looks like this:

#define BUF_SIZE 1024 // Size of plaintext buffer

// Userspace struct
typedef enum hash {
	MD5 = 0,
	SHA1,
	SHA256
} hash_t;

typedef struct userspace {
	u8 plaintext[BUF_SIZE];
	hash_t algorithm;
} userspace_t;

We can then copy the data from userspace:

static ssize_t hashkm_write(struct file * filep, const char * buffer, size_t len, loff_t * offset) {
	size_t bytes_not_copied = 0;
	struct crypto_shash* algorithm;
	struct shash_desc* desc;
	int err;

	// 'copy_from_user' method returns 0 on success and the number of bytes not copied
	// on error
	bytes_not_copied = copy_from_user(&userspace, buffer, sizeof(userspace_t));
	if(bytes_not_copied) { // Check errors
		pr_warn("%s: error while copying %zu bytes from userspace\n", DEV_NAME, bytes_not_copied);
		return -EFAULT;
	}

    // [...]

Okay, we are ready to interact with the Linux cryptographic APIs. The first thing to do is to select the hashing algorithm according to the user choice. We will support three hashing algorithms: MD5, SHA1 and SHA256.

// [...]
// Select hashing algorithm
switch(userspace.algorithm) {
    case MD5: algorithm = crypto_alloc_shash("md5", 0, 0); break;
    case SHA1: algorithm = crypto_alloc_shash("sha1", 0, 0); break;
    case SHA256: algorithm = crypto_alloc_shash("sha256", 0, 0); break;
    default: pr_alert("%s: hashing algorithm not recognized\n", DEV_NAME); return -EFAULT;
}

// Check if selected algorithm is available in the system
if(IS_ERR(algorithm)) { // Check errors
    pr_alert("%s: Hashing algorithm not supported\n", DEV_NAME);
    return -EFAULT;
}
// [...]

In the last part, we initialize the shash API, we execute the hashing function, we write the result to a digest char buffer, and then we will clean up allocated resources:

	desc = kmalloc(sizeof(struct shash_desc) + crypto_shash_descsize(algorithm), GFP_KERNEL);
	if(!desc) { // check errors
		pr_err("%s: failed to allocate memory(kmalloc)\n", DEV_NAME);
		return -ENOMEM;
	}
	desc->tfm = algorithm;

	// Initialize shash API
	err = crypto_shash_init(desc);
	if(err)  {
		pr_err("%s: failed to initialize shash\n", DEV_NAME);
		goto out;
	}

	// Execute hash function
	err = crypto_shash_update(desc, userspace.plaintext, strlen(userspace.plaintext));
	if(err) {
		pr_err("%s: failed to execute hashing function\n", DEV_NAME);
		goto out;
	}

	// Write the result to a new char buffer
	err = crypto_shash_final(desc, digest);
	if(err) {
		pr_err("%s: Failed to complete hashing function\n", DEV_NAME);
		goto out;
	}

	// Finally, clean up resources
	crypto_free_shash(algorithm);
	kfree(desc);

	pr_info("%s: String successfully hashed. Read from this device to get the result\n", DEV_NAME);

	return 0;

out: // Manage errors
	crypto_free_shash(algorithm);
	kfree(desc);
	return err;
}

Here’s the full source code:

/* This linux kernel module allows string hashing from 
 * kernel mode through the creation of a character device
 * called '/dev/hashkm'. This code is part of a series of 
 * tutorials. For further information regarding this module
 * please refer to the following web page:
 * http://marcocetica.com/posts/kernel_hacking_part2/
 * 
 * Developed by Marco Cetica<email@marcocetica.com> (c) 2020-2022
 *
 */
#include <linux/init.h> // Default macros for __init and __exit functions
#include <linux/module.h> // module functions
#include <linux/device.h> // Needed to support kernel driver module
#include <linux/kernel.h> // Debug macros
#include <linux/fs.h> // Linux file systems support
#include <linux/uaccess.h> // Needed for 'copy_to_user'/'copy_from_user' functions
#include <crypto/hash.h> // Support to hashing cryptographic functions
#include <linux/slab.h> // In-kernel dynamic memory allocation
#include <linux/types.h> // Required for custom data types
#define DEV_NAME "hashkm" // Name of the character device
#define DEV_CLASS "hashlkm" // Name of the device class
#define BUF_SIZE 1024 // Size of plaintext buffer
// Module info
MODULE_AUTHOR("Marco Cetica");
MODULE_DESCRIPTION("Hash buffer of char in kernel mode using MD5,SHA1 and SHA256");
MODULE_VERSION("0.2");
MODULE_LICENSE("Dual BSD/GPL");

// Userspace struct
typedef enum hash {
	MD5 = 0,
	SHA1,
	SHA256
} hash_t;

typedef struct userspace {
	u8 plaintext[BUF_SIZE];
	hash_t algorithm;
} userspace_t;

// Some global definitions
static int dev_num; // Major number of the character device
static struct class * hashkm_class = NULL; // Device driver class pointer
static struct device * hashkm_dev = NULL; // Device driver pointer
static size_t open_count = 0; // Number of types this device has been opened
static userspace_t userspace; // Data from userspace
static char digest[BUF_SIZE]; // Result of hashing process
// Functions prototypes
static int hashkm_open(struct inode*, struct file*);
static int hashkm_close(struct inode*, struct file*);
static ssize_t hashkm_read(struct file*, char*, size_t, loff_t*);
static ssize_t hashkm_write(struct file*, const char*, size_t, loff_t*);

// File operation struct
// Map local functions to default system calls
static struct file_operations fo = {
	.owner = THIS_MODULE,
	.open = hashkm_open,
	.release = hashkm_close,
	.read = hashkm_read,
	.write = hashkm_write,
};

// Entry point functions(__init and __exit)
static int __init hashkm_init(void) {
	pr_info("%s: Loading, please wait...\n", DEV_NAME);
	
	// Get a new major number for the character device
	dev_num = register_chrdev(0, DEV_NAME, &fo);
	if(dev_num < 0) { // Check errors
		pr_alert("%s: Error while trying to register major number\n", DEV_NAME);
		return dev_num;
	}

	pr_info("%s: New device successfully registered(major number: %d)\n", DEV_NAME, dev_num);
	
	// Register device class
	hashkm_class = class_create(THIS_MODULE, DEV_CLASS);
	if(IS_ERR(hashkm_class)) { // Check errors
		unregister_chrdev(dev_num, DEV_NAME); // Unregister device major number
		pr_alert("%s: unable to register device class\n", DEV_NAME);
		return PTR_ERR(hashkm_class);
	}
	pr_info("%s: Device class successfully register\n", DEV_NAME);

	// Register character device
	hashkm_dev = device_create(hashkm_class, NULL, MKDEV(dev_num, 0), NULL, DEV_NAME);
	if(IS_ERR(hashkm_dev)) {  // Check errors
		class_destroy(hashkm_class);  // Destroy device class
		unregister_chrdev(dev_num, DEV_NAME); // Unregister device major number
		pr_alert("%s: failed to create a new device\n", DEV_NAME);
		return PTR_ERR(hashkm_dev);
	}
	
	pr_info("%s: device driver successfully created\n", DEV_NAME);

	return 0;
}


static void __exit hashkm_exit(void) {
	device_destroy(hashkm_class, MKDEV(dev_num, 0)); // Remove device
	class_unregister(hashkm_class); // Unregister the class
	class_destroy(hashkm_class); // Destroy the class
	unregister_chrdev(dev_num, DEV_NAME); // Unregister device major number
	pr_info("%s: module successfully unloaded\n", DEV_NAME);
}

/* This function is invoked each time an userspace process
 * tries to open the character device. It is usually being 
 * used to set up the environment for the rest of the module but
 * since we do not have anything to configure, we will just
 * print out a message */
static int hashkm_open(struct inode * inodep, struct file * filep) {
	pr_info("%s: This device has been opened %d time(s)\n", DEV_NAME, (int)++open_count);

	return 0;
}

/* This function is invoked each time an userspace process
 * closes the character device. It is usually being used
 * to free allocated resources but since we have not anything
 * to do, we will just print out a message */
static int hashkm_close(struct inode * inodep, struct file * filep) {
	pr_info("%s: device closed successfully\n", DEV_NAME);

	return 0;
}

/* This function is invoked each time we call the 'read()' syscall
 * from userspace. In our module, we will use the function 
 * 'copy_to_user' to send back to the user the result(i.e., the hashed string) */
static ssize_t hashkm_read(struct file * filep, char * buffer, size_t len, loff_t * offset) {
	size_t bytes_not_copied = 0;
	size_t bytes_to_copy = (userspace.algorithm == SHA256) ? 32 : (userspace.algorithm == SHA1 ? 20 : 16);

	// copy_to_user returns 0 on success while it returns
	// the number of bytes not copied on errors
	bytes_not_copied = copy_to_user(buffer, digest, bytes_to_copy);
	if(bytes_not_copied) { // Check errors
		pr_warn("%s: Failed to send result to userspace\n", DEV_NAME);
		return -EFAULT;
	}

	return 0;
}


/* This function is invoked each time we call the 'write()' syscall
 * from userspace. In our module, we will use the function 'write_to_user'
 * to retrieve user data. We will then hash the plaintext string according to 
 * the chosen algorithm using Linux cryptographic APIs */
static ssize_t hashkm_write(struct file * filep, const char * buffer, size_t len, loff_t * offset) {
	size_t bytes_not_copied = 0;
	struct crypto_shash* algorithm;
	struct shash_desc* desc;
	int err;

	// 'copy_from_user' method returns 0 on success and the number of bytes not copied
	// on error
	bytes_not_copied = copy_from_user(&userspace, buffer, sizeof(userspace_t));
	if(bytes_not_copied) { // Check errors
		pr_warn("%s: error while copying %zu bytes from userspca\n", DEV_NAME, bytes_not_copied);
		return -EFAULT;
	}

	// Select hashing algorithm
	switch(userspace.algorithm) {
		case MD5: algorithm = crypto_alloc_shash("md5", 0, 0); break;
		case SHA1: algorithm = crypto_alloc_shash("sha1", 0, 0); break;
		case SHA256: algorithm = crypto_alloc_shash("sha256", 0, 0); break;
		default: pr_alert("%s: hashing algorithm not recognized\n", DEV_NAME); return -EFAULT;
	}

	// Check if selected algorithm is available in the system
	if(IS_ERR(algorithm)) { // Check errors
		pr_alert("%s: Hashing algorithm not supported\n", DEV_NAME);
		return -EFAULT;
	}


	desc = kmalloc(sizeof(struct shash_desc) + crypto_shash_descsize(algorithm), GFP_KERNEL);
	if(!desc) { // check errors
		pr_err("%s: failed to allocate memory(kmalloc)\n", DEV_NAME);
		return -ENOMEM;
	}
	desc->tfm = algorithm;

	// Initialize shash API
	err = crypto_shash_init(desc);
	if(err)  {
		pr_err("%s: failed to initialize shash\n", DEV_NAME);
		goto out;
	}

	// Execute hash function
	err = crypto_shash_update(desc, userspace.plaintext, strlen(userspace.plaintext));
	if(err) {
		pr_err("%s: failed to execute hashing function\n", DEV_NAME);
		goto out;
	}

	// Write the result to a new char buffer
	err = crypto_shash_final(desc, digest);
	if(err) {
		pr_err("%s: Failed to complete hashing function\n", DEV_NAME);
		goto out;
	}

	// Finally, clean up resources
	crypto_free_shash(algorithm);
	kfree(desc);

	pr_info("%s: String successfully hashed. Read from this device to get the result\n", DEV_NAME);

	return 0;

out: // Manage errors
	crypto_free_shash(algorithm);
	kfree(desc);
	return err;
}

// Register __init and __exit functions
module_init(hashkm_init);
module_exit(hashkm_exit);

Client

Now that we have the kernel module ready, it is time to write an userspace client. We cannot write directly to the device using echo since the kernel module expects an userspace_t type. Basically, the client consists in just a write system call followed by a read system call, here’s my client implementation:

/*
 * Client interface of hashkm character device
 * For further information, reach the following 
 * webpage: http://marcocetica.com/posts/kernel_hacking_part2/ 
 *
 * Developed by Marco Cetica<email@marcocetica.com> (c) 2020-2022 
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <stdint.h>

#define BUF_SIZE 1024

typedef enum hash {
	MD5 = 0,
	SHA1,
	SHA256
} hash_t;

typedef struct userspace {
	uint8_t plaintext[BUF_SIZE];
	hash_t algorithm;
} userspace_t;

int main(void) {
	int dev, ret;
	char digest[BUF_SIZE];

	// Open the device
	puts("Opening character device, wait please...");
	dev = open("/dev/hashkm", O_RDWR); // Open the device in read and write mode
	if(dev < 0) { // Handle errors
		perror("Failed to open the device");
		return errno;
	}
	
	// Read the plaintext from stdin
	char plaintext[BUF_SIZE];
	fputs("Insert a string: ", stdout);
	if(fgets(plaintext, sizeof(plaintext), stdin) == NULL) {
		puts("Error while reading from stdin");
		return -1;
	}

	// Read the hashing algorithm from stdin
	char type[8];
	fputs("Choose a hashing algorith(MD5,SHA1,SHA256): ", stdout);
	if(fgets(type, sizeof(type), stdin) == NULL) {
		puts("Error while reading from stdin");
		return -1;
	}

	// Normalize input
	strtok(plaintext, "\n");
	strtok(type, "\n");
	for(char* p = type; *p; ++p) *p = toupper((unsigned char)*p);

	// Create an userspace struct
	userspace_t u;
	strncpy((char*)u.plaintext, plaintext, BUF_SIZE);
	if(!strcmp(type, "MD5")) u.algorithm = MD5;
	else if(!strcmp(type, "SHA1")) u.algorithm = SHA1;
	else if(!strcmp(type, "SHA256")) u.algorithm = SHA256;
	else {
		puts("Algorithm not supported");
		return -1;
	}

	// Write to device
	puts("Sending data to kernel, wait please...");
	ret = write(dev, &u, sizeof(userspace_t));
	if(ret < 0) {
		perror("Error while sending data to kernel space");
		return errno;
	}

	// Read from device
	ret = read(dev, digest, BUF_SIZE);
	if(ret < 0) {
		perror("Error while reading data from kernel space");
		return errno;
	}

	// Print digest in hexadecimal
	size_t bytes_to_print = (u.algorithm == SHA256) ? 32 : (u.algorithm == SHA1 ? 20 : 16);
	printf("Original string: \"%s\", %s digest: \"", u.plaintext, type);
	for(size_t i = 0; i < bytes_to_print; i++)
		printf("%02x", (unsigned char)digest[i]);
	puts("\"");
	
	return 0;

}

The last thing we need to write is a Makefile that will compile the module and the client code:

obj-m += hashkm.o
CFLAGS = -Wall -Wextra -Werror -pedantic -std=c99
ccflags-y += $(C_FLAGS)

all:
	make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules
	$(CC) $(CFLAGS) client.c -o client

clean:
	make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
	rm client

Testing

Once compiled, the last thing left to do is testing! On one terminal window load the kernel module using

[marco@kerneldev hash_lkm]$ sudo insmod hashkm.ko

and then open kernel logs using

[marco@kerneldev hash_lkm]$ sudo dmesg -WH

Now on anothor terminal window, open the client and try to hash a message:

[marco@kerneldev hash_lkm]$ sudo ./client 
Opening character device, wait please...
Insert a string: ciao a tutti
Choose a hashing algorith(MD5,SHA1,SHA256): md5
Sending data to kernel, wait please...
Original string: "ciao a tutti", MD5 digest: "f85a78dd6672f014d4a3c57b0a7dc016"
[marco@kerneldev hash_lkm]$ echo -n "ciao a tutti" | md5sum
f85a78dd6672f014d4a3c57b0a7dc016  -


[marco@kerneldev hash_lkm]$ sudo ./client 
Opening character device, wait please...
Insert a string: hello world
Choose a hashing algorith(MD5,SHA1,SHA256): sha1
Sending data to kernel, wait please...
Original string: "hello world", SHA1 digest: "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
[marco@kerneldev hash_lkm]$ echo -n "hello world" | sha1sum
2aae6c35c94fcfb415dbe95f408b9ce91ee846ed  -


[marco@kerneldev hash_lkm]$ sudo ./client 
Opening character device, wait please...
Insert a string: Hola Mundo
Choose a hashing algorith(MD5,SHA1,SHA256): sha256
Sending data to kernel, wait please...
Original string: "Hola Mundo", SHA256 digest: "c3a4a2e49d91f2177113a9adfcb9ef9af9679dc4557a0a3a4602e1bd39a6f481"
[marco@kerneldev hash_lkm]$ echo -n "Hola Mundo" | sha256sum
c3a4a2e49d91f2177113a9adfcb9ef9af9679dc4557a0a3a4602e1bd39a6f481  -

Let us go back to the kernel logs window:

[Jan 6 23:24] hashkm: This device has been opened 1 time(s)
[  +6.013549] hashkm: String successfully hashed. Read from this device to get the result
[  +0.000361] hashkm: device closed successfully
[ +22.042698] hashkm: This device has been opened 2 time(s)
[ +11.573044] hashkm: String successfully hashed. Read from this device to get the result
[  +0.000271] hashkm: device closed successfully
[Jan 6 23:25] hashkm: This device has been opened 3 time(s)
[ +27.101392] hashkm: String successfully hashed. Read from this device to get the result
[  +0.000385] hashkm: device closed successfully

The device driver works!

In the next part of this guide we will see how system calls work and how to create a new one by ourselves. This will give us the knowledge needed to build(on part 4) a kernel rootkit that will hijack a system call to a malicious function. Until then, stay tuned and happy 2022!