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 |
|
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 |
|
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 |
|
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 |
|
Again, here we only need minor
, name
and fops
. So ours will be defined as:
1 2 3 4 5 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
Compiling The Device
As with before, we'll need a Makefile
:
1 2 3 4 5 6 7 8 |
|
All that is left is to type make
:
1 2 3 4 5 6 7 8 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 :-)