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
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
fops. So ours will be defined as:
1 2 3 4 5
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_deregister, and they are decleared as follows:
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 (
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.
include/asm-generic/uaccess.h header file (which we'll also need to include)
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
1 2 3 4 5 6 7 8
All that is left is to type
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
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
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 :-)