Writing a Char Module is suprisingly simple. First, we specify what happens on init
(loading of the module) and exit
(unloading of the module). We need some special headers for this.
It looks simple, because it is simple. For now, anyway.
First we set the license, because otherwise we get a warning, and I hate warnings. Next we tell the module what to do on load (intro_init()
) and unload (intro_exit()
). Note we put parameters as void
, this is because kernel modules are very picky about requiring parameters (even if just void).
We then register the purposes of the functions using module_init()
and module_exit()
.
Note that we use printk
rather than printf
. GLIBC doesn't exist in kernel mode, and instead we use C's in-built kernel functionality. KERN_ALERT
is specifies the type of message sent, and there are many more types.
Compiling a Kernel Object can seem a little more complex as we use a Makefile
, but it's surprisingly simple:
We use make
to compile the module. The files produced are defined at the top as obj-m
. Note that compilation is unique per kernel, which is why the compiling process uses your unique kernel build section.
Now we've got a ko
file compiled, we can add it to the list of active modules:
If it's successful, there will be no response. But where did it print to?
Remember, the kernel program has no concept of userspace; it does not know you ran it, nor does it bother communicating with userspace. Instead, this code runs in the kernel, and we can check the output using sudo dmesg
.
Here we grab the last line using tail
- as you can see, our printk
is called!
Now let's unload the module:
And there our intro_exit
is called.
You can view currently loaded modules using the lsmod
command
A more useful way to interact with the driver
Linux contains a syscall called ioctl
, which is often used to communicate with a driver. ioctl()
takes three parameters:
File Descriptor fd
an unsigned int
an unsigned long
The driver can be adapted to make the latter two virtually anything - perhaps a pointer to a struct or a string. In the driver source, the code looks along the lines of:
But if you want, you can interpret cmd
and arg
as pointers if that is how you wish your driver to work.
To communicate with the driver in this case, you would use the ioctl()
function, which you can import in C:
And you would have to update the file_operations
struct:
On modern Linux kernel versions, .ioctl
has been removed and replaced by .unlocked_ioctl
and .compat_ioctl
. The former is the replacement for .ioctl
, with the latter allowing 32-bit processes to perform ioctl
calls on 64-bit systems. As a result, the new file_operations
is likely to look more like this:
Creating an interactive char driver is surprisingly simple, but there are a few traps along the way.
This is by far the hardest part to understand, but honestly a full understanding isn't really necessary. The new intro_init
function looks like this:
A major number is essentially the unique identifier to the kernel module. You can specify it using the first parameter of register_chrdev
, but if you pass 0
it is automatically assigned an unused major number.
We then have to register the class and the device. In complete honesty, I don't quite understand what they do, but this code exposes the module to /dev/intro
.
Note that on an error it calls class_destroy
and unregister_chrdev
:
These additional classes and devices have to be cleaned up in the intro_exit
function, and we mark the major number as available:
In intro_init
, the first line may have been confusing:
The third parameter fops
is where all the magic happens, allowing us to create handlers for operations such as read
and write
. A really simple one would look something like:
The parameters to intro_read
may be a bit confusing, but the 2nd and 3rd ones line up to the 2nd and 3rd parameters for the read()
function itself:
We then use the function copy_to_user
to write QWERTY
to the buffer passed in as a parameter!
Simply use sudo insmod
to load it, as we did before.
Create a really basic exploit.c
:
If the module is successfully loaded, the read()
call should read QWERTY
into buffer
:
Success!