KERNEL HACKING - WRITE A CRYPTOGRAPHIC MODULE(2/3)
2022-01-06
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.
- Part one: Introduction to the Linux kernel architecture;
- Part two(this one): Building a device driver from scratch;
- Part three: Introduction to syscalls, how to create a new syscall;
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
).
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.
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
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 theread()
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.
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 thewrite()
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.
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 usingecho
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. Until then, stay tuned and happy 2022!