A Simple Character Device

Posted on Fri 06 June 2014 in Linux Kernel Hacking

This is the second post on Linux kernel hacking. In the first post we created a basic Linux kernel module, but this LKM didn't really do anything except write a message to the system log on load/unload.

Now we will extend this to create a device which we can use to communicate with the LKM, other than system calls, device files are how userland applications communicate with code running in kernelland.

What Is A Device File

There are 2 main types of device files, a character device file and a block device file. The differences are, a block device is buffered (meaning it doesn't offer direct access to the device and ultimately means that you don't know how long it will take before a write is pushed to the actual device) and a block device allows reads or writes of any size, character device reads and writes are aligned to block boundaries.

We will be using a character device because they are simpler to understand (as we will use the device file in exactly the same way that we would use a regular file), we have no need for random access to the device and it provides direct access to the device.

When viewed using ls -l a character device will have c as the first letter, while a block device has a b.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
root@dev:~# ls -l /dev/console
crw------- 1 root root 5, 1 May 29 12:07 /dev/console
root@dev:~# stat /dev/console
  File: `/dev/console'
  Size: 0           Blocks: 0          IO Block: 4096   character special file
Device: 5h/5d   Inode: 1466        Links: 1     Device type: 5,1
Access: (0600/crw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2014-05-29 12:06:54.303999993 +0100
Modify: 2014-05-29 12:07:28.303999993 +0100
Change: 2014-05-29 12:06:54.303999993 +0100
 Birth: -

First on line 1 I use ls to view some of the attributes of the file, as you can see on line 2 it is a character device. On line 3 I use the stat command to view further statistics, here, on line 6, it tells you the major and minor numbers (5 and 1 respectively, these numbers are also shown in the output of ls after the group ownership), inode number and block size (on line 5).

This means that if you delete the file with rm /dev/console, you can create the file again using mknod /dev/console c 5 1 (c is for character device). I will demonstrate this later with our custom character device.

The major and minor numbers uniquely identify a device. The major number defines which driver is going to be called to perform the input/output operation. The minor number is implementation defined, basically its up to the driver what the minor number means, it is just passed as an argument.

Building Our Character Device

For our character device we will implement a basic device which will take a string as an input (when the device file is written to), reverse the words of the string (any string of characters without a space is considered a word here) and output the reversed string when the device file is read from.

In Linux there is a generic character device called misc implemented in the kernel, this is the device we will use to create our character device.

The advantage here is that the misc device deals with the initialisation and cleanup of the device so we can just concentrate on the functionality of it. The major number of the misc device is 10, we can confirm this later once we have created ours and is drivers/char/misc.c in the kernel source.

Every device requires a file_operations struct, this defines what functions are run when certain actions are performed on the devices file, it is defined in includes/linux/fs.h (so we will need to include this header file) as:

1
2
3
4
5
6
7
8
static const struct file_operations __fops = {                          \
    .owner   = THIS_MODULE,                                         \
    .open    = __fops ## _open,                                     \
    .release = simple_attr_release,                                 \
    .read    = simple_attr_read,                                    \
    .write   = simple_attr_write,                                   \
    .llseek  = generic_file_llseek,                                 \
};

You don't need to use all of these, only the ones that you will require based on what you want to do with your device. We only want to do something particular when we read from or write to the device file so our file_operations struct will be like this:

1
2
3
4
struct file_operations reverse_fops = {
    read: reverse_read,
    write: reverse_write
};

All of the functions will contain the name reverse which is what our character device will be called due to the nature of what it does, although the actual names are irrelevant.

Here we are telling the kernel that when a read happens on our device file we want to run the function reverse_read (on line 2) and when a write happens we want to run the function reverse_write (on line 3).

We will use this struct inside our miscdevice struct. The miscdevice struct is defined in include/linux/miscdevice.h (so we will also need to include this header file) as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct miscdevice  {
    int minor;
    const char *name;
    const struct file_operations *fops;
    struct list_head list;
    struct device *parent;
    struct device *this_device;
    const char *nodename;
    umode_t mode;
};

Again, here we only need minor, name and fops. So ours will be defined as:

1
2
3
4
5
static struct miscdevice reverse_misc_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "reverse",
    .fops = &reverse_fops
};

In the include/linux/miscdevice.h header, the symbolic constant MISC_DYNAMIC_MINOR is defined as 255, this means it will pick the next avaliable minor number.

Now we should ensure our device is registered and unregistered when our LKM is loaded and unloaded respectively. The include/linux/miscdevice.h header also includes the declaration of 2 functions that will help us here, misc_register and misc_deregister, and they are decleared as follows:

1
2
extern int misc_register(struct miscdevice * misc);
extern int misc_deregister(struct miscdevice *misc);

So they both take 1 argument, the miscdevice struct created earlier. Other than this our LKM doesn't need to do anything else, so the initialization and exit functions can be written like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static int __init reverse_init(void)
{
    misc_register(&reverse_misc_device);

        return 0;
}

static void __exit reverse_exit(void)
{
    misc_deregister(&reverse_misc_device);
}

Next we need to develop the functionality, for this I wrote a normal C application to make sure it was all working:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char data[513] = "No data";

void insert_word(char *word, unsigned int n)
{
    int i, c;
    char tmpword[512+1];
    for (i = strlen(word)-1, c = 0; i >= 0; i--, c++) {
        tmpword[c] = word[i];
    }
    tmpword[strlen(word)] = '\0';
    if (n == 0) {
        memset(data, 0, sizeof data);
        strcpy(data, tmpword);
    } else {
        data[strlen(data)] = ' ';
        data[strlen(data)+1] = '\0';
        strcat(data, tmpword);
    }
}

void reverse(char *tmpdata)
{
    int i, c;
    unsigned int n = 0;
    char word[512+1];
    for (i = strlen(tmpdata)-1, c = 0; i >= 0; i--, c++) {
        if (tmpdata[i] == ' ') {
            word[c] = '\0';
            insert_word(word, n);
            n += 1;
            c = -1;
        } else
            word[c] = tmpdata[i];

    }
    word[c] = '\0';
    insert_word(word, n);
    data[strlen(tmpdata)] = '\0';
}

int main(int argc, char **argv)
{
    if (argc < 2) {
        printf("Usage: %s <string>\n", argv[0]);
        exit(1);
    }

    printf("Before: %s\n", data);
    reverse(argv[1]);
    printf("After: %s\n", data);
}

Some of you should have noticed the buffer overflow in this application, if you haven't check out my x86-32 linux section. You can write an exploit for this application and figure out how to get a shell. The character device will have a buffer overflow too, but we're not too worried about this as we are the only people that are going to be using it, if you wanted to secure this application you would just create another counter that always incremented and break when it reaches 512.

Anyway, testing this application shows that it seems to work fine:

1
2
3
4
5
6
7
root@dev:~/lkms# gcc -o reverse-test-app reverse-test-app.c
root@dev:~/lkms# ./reverse-test-app "this is a test application"
Before: No data
After: application test a is this
root@dev:~/lkms# ./reverse-test-app "application test a is this"
Before: No data
After: this is a test application

Obviously our "datastore" is only holding the data while the application is running so it isn't permanent but the "datastore" in the LKM will be. I guess its worth mentioning here that the "datastore" that we have in our LKM will be exactly the same as here, just a global character array, we could use any memory really but I'm using a character array for simplicity.

The functions (reverse and insert_word) in the test application can be put into the LKM as is.

Almost done, but a userland application can only write to and read from memory in userland; and LKM's should only write to and read from kernelland, so we need a way to copy from and copy to userland in kernelland. Luckily the kernel provides us with functions to be able to do that.

In the include/asm-generic/uaccess.h header file (which we'll also need to include) copy_from_user and copy_to_user are defined as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static inline long copy_from_user(void *to,
        const void __user * from, unsigned long n)
{
    might_fault();
    if (access_ok(VERIFY_READ, from, n))
        return __copy_from_user(to, from, n);
    else
        return n;
}

static inline long copy_to_user(void __user *to,
        const void *from, unsigned long n)
{
    might_fault();
    if (access_ok(VERIFY_WRITE, to, n))
        return __copy_to_user(to, from, n);
    else
        return n;
}

Both of these functions takes 2 void pointers (1 pointing to memory in userland and 1 pointing to memory in kernelland, they are of type void so that any type of data can be transferred), and a number (the amount of data to be copied).

With all of this information we can finally build our character device:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <linux/module.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

MODULE_AUTHOR("0xe7, 0x1e");
MODULE_DESCRIPTION("A simple character device which reverses the words in a string");
MODULE_LICENSE("GPL");

#define DEVICE_SIZE 512

char data[DEVICE_SIZE+1]="no data has been written yet";

void insert_word(char *word, unsigned int n)
{
    int i, c;
    char tmpword[DEVICE_SIZE+1];
    for (i = strlen(word)-1, c = 0; i >= 0; i--, c++) {
        tmpword[c] = word[i];
    }
    tmpword[strlen(word)] = '\0';
    if (n == 0) {
        memset(data, 0, sizeof data);
        strcpy(data, tmpword);
    } else {
        data[strlen(data)] = ' ';
        data[strlen(data)+1] = '\0';
        strcat(data, tmpword);
    }
}

void reverse(char *tmpdata)
{
    int i, c;
    unsigned int n = 0;
    char word[DEVICE_SIZE+1];
    for (i = strlen(tmpdata)-1, c = 0; i >= 0; i--, c++) {
        if (tmpdata[i] == ' ') {
            word[c] = '\0';
            insert_word(word, n);
            n += 1;
            c = -1;
        } else
            word[c] = tmpdata[i];

    }
    word[c] = '\0';
    insert_word(word, n);
    data[strlen(tmpdata)] = '\0';
}

ssize_t reverse_read(struct file *filep,char *buff,size_t count,loff_t *offp )
{
    if ( copy_to_user(buff,data,strlen(data)) != 0 ) {
        printk( "Kernel -> userspace copy failed!\n" );
        return -1;
    }
    return strlen(data);
}

ssize_t reverse_write(struct file *filep,const char *buff,size_t count,loff_t *offp )
{
    char tmpdata[DEVICE_SIZE+1];
    if ( copy_from_user(tmpdata,buff,count) != 0 ) {
        printk( "Userspace -> kernel copy failed!\n" );
        return -1;
    }
    reverse(tmpdata);
    return 0;
}

struct file_operations reverse_fops = {
    read: reverse_read,
    write: reverse_write
};

static struct miscdevice reverse_misc_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "reverse",
    .fops = &reverse_fops
};

static int __init reverse_init(void)
{
    misc_register(&reverse_misc_device);

        return 0;
}

static void __exit reverse_exit(void)
{
    misc_deregister(&reverse_misc_device);
}

module_init(reverse_init);
module_exit(reverse_exit);

Compiling The Device

As with before, we'll need a Makefile:

1
2
3
4
5
6
7
8
obj-m += hello.o
obj-m += reverse.o

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

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

All that is left is to type make:

1
2
3
4
5
6
7
8
root@dev:~/lkms# make
make -C /lib/modules/3.12-kali1-686-pae/build M=/root/lkms modules
make[1]: Entering directory `/usr/src/linux-headers-3.12-kali1-686-pae'
  CC [M]  /root/lkms/reverse.o
  Building modules, stage 2.
  MODPOST 2 modules
  LD [M]  /root/lkms/reverse.ko
make[1]: Leaving directory `/usr/src/linux-headers-3.12-kali1-686-pae'

Testing The Device

Before we can test the device, we need an application that can read from and write to the device file, here is my application to do that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <paths.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

#define CDEV_DEVICE "reverse"
static char buf[512+1];

int main(int argc, char *argv[])
{
    int fd, len;

    if (argc != 2) {
        printf("Usage: %s <string>\n", argv[0]);
        exit(0);
    }

    if ((len = strlen(argv[1]) + 1) > 512) {
        printf("ERROR: String too long\n");
        exit(0);
    }

    if ((fd = open("/dev/" CDEV_DEVICE, O_RDWR)) == -1) {
        perror("/dev/" CDEV_DEVICE);
        exit(1);
    }

    printf("fd :%d\n",fd);

    if (read(fd, buf, len) == -1)
        perror("read()");
    else
        printf("Before: \"%s\".\n", buf);

    if (write(fd, argv[1], len) == -1)
        perror("write()");
    else
        printf("Wrote: \"%s\".\n", argv[1]);

    if (read(fd, buf, len) == -1)
        perror("read()"); 
    else    
        printf("After: \"%s\".\n", buf);

    if ((close(fd)) == -1) {
        perror("close()");
        exit(1);
    }

    exit(0);
}

This is a very basic application that uses the POSIX open, read, write and close functions to use the device file. Also, I am implementing the bounds check here (on line 20) so I can't write any more than 512 bytes (the size of our character device datastore) but in a real situation you should implement the bounds checking in the LKM itself.

Now we can test the LKM properly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
root@dev:~/lkms# gcc -o reverse-app reverse-app.c 
root@dev:~/lkms# insmod ./reverse.ko
root@dev:~/lkms# lsmod | grep reverse
reverse                12476  0 
root@dev:~/lkms# ls -l /dev/reverse 
crw------- 1 root root 10, 58 Jun  9 23:22 /dev/reverse
root@dev:~/lkms# ./reverse-app 
Usage: ./reverse-app <string>
root@dev:~/lkms# ./reverse-app "I am testing my first character device"
fd :3
Before: "no data has been written yet".
Wrote: "I am testing my first character device".
After: "device character first my testing am I".
root@dev:~/lkms# ./reverse-app "device character first my testing am I"
fd :3
Before: "device character first my testing am I".
Wrote: "device character first my testing am I".
After: "I am testing my first character device".

I check to see if the device file has been created on line 5, and looking at the output it has a major number of 10 and a minor number of 58. I then test it using the test application and it works perfectly.

Its worth noting that you can delete the device file, recreate it and the data will remain there, this is because the data isn't stored in the file, but in the global character array in the LKM itself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
root@dev:~/lkms# rm /dev/reverse 
root@dev:~/lkms# ls -l /dev/reverse 
ls: cannot access /dev/reverse: No such file or directory
root@dev:~/lkms# mknod /dev/reverse c 10 58
root@dev:~/lkms# ./reverse-app "Another test string"
fd :3
Before: "I am testing my first character device".
Wrote: "Another test string".
After: "string test Anotherst character device".
root@dev:~/lkms# ./reverse-app "Another test"
fd :3
Before: "string test Another".
Wrote: "Another test".
After: "test AnotherAnother".

Something funny happened while the application was reading from the device the second time, the data hadn't fully been written yet, this isn't really important to us (as our code is running in kernelland and will get the data straight away) but its worth knowing this if you are going to develop actual drivers and not just rootkits. As you can see though by the time I run the test application again, the data had been fully updated.

Lastly I'd just like to show you that you can create more than 1 device file in different locations, and even with different names, as long as the major and minor numbers are the same:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
root@dev:~/lkms# mknod /root/mynewdevfile c 10 58
root@dev:~/lkms# ls -l /dev/reverse
crw-r--r-- 1 root root 10, 58 Jun  9 23:29 /dev/reverse
root@dev:~/lkms# ls -l /root/mynewdevfile
crw-r--r-- 1 root root 10, 58 Jun  9 23:39 /root/mynewdevfile
root@dev:~/lkms# cp reverse-app.c reverse-app2.c
root@dev:~/lkms# vim reverse-app2.c 
root@dev:~/lkms# gcc -o reverse-app2 reverse-app2.c 
root@dev:~/lkms# ./reverse-app2
Usage: ./reverse-app2 <string>
root@dev:~/lkms# ./reverse-app2 "this is my last test"
fd :3
Before: "test Another".
Wrote: "this is my last test".
After: "test last my is this".
root@dev:~/lkms# ./reverse-app "test last my is this"
fd :3
Before: "test last my is this".
Wrote: "test last my is this".
After: "this is my last test".

Here I've just created a new reverse-app2.c so that it uses the device file at /root/mynewdevfile. As you can see from the output of the applications that both device files are using the same datastore and they both do exactly the same thing.

Lastly, any extra device files will still exist after the LKM has been unloaded (and will need to be manually removed) but the original file (/dev/reverse) will be automatically deleted:

1
2
3
4
5
6
7
8
9
root@dev:~/lkms# rmmod reverse
root@dev:~/lkms# lsmod | grep reverse
root@dev:~/lkms# ls -l /dev/reverse
ls: cannot access /dev/reverse: No such file or directory
root@dev:~/lkms# ls -l /root/mynewdevfile
crw-r--r-- 1 root root 10, 58 Jun  9 23:39 /root/mynewdevfile
root@dev:~/lkms# rm /root/mynewdevfile
root@dev:~/lkms# ls -l /root/mynewdevfile
ls: cannot access /root/mynewdevfile: No such file or directory

Conclusion

Character devices can be very useful for userland/kernelland communication, this can be done with system calls to a degree but its a lot more difficult to implement a system call in an LKM.

When doing any kernel development, the kernel source is a necessity, you can download it from https://www.kernel.org/, see what version of the kernel you have, using uname -r, and download the correct source. Getting used to the kernel source will make you a much better kernel developer and ultimately a better rootkit developer.

Lastly I'd like to highlight again that any form of kernel development is very dangerous to the system you are developing on, you risk crashing the system and even corrupting data, only do this on a development machine and if stuff breaks don't blame me for any damage done!

Happy Hacking :-)