Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The kernel is the program at the heart of the Operating System. It is responsible for controlling every aspect of the computer, from the nature of syscalls to the integration between software and hardware. As such, exploiting the kernel can lead to some incredibly dangerous bugs.
In the context of CTFs, Linux kernel exploitation often involves the exploitation of kernel modules. This is an integral feature of Linux that allows users to extend the kernel with their own code, adding additional features.
You can find an excellent introduction to Kernel Drivers and Modules by LiveOverflow here, and I recommend it highly.
Kernel Modules are written in C and compiled to a .ko
(Kernel Object) format. Most kernel modules are compiled for a specific version kernel version (which can be checked with uname -r
, my Xenial Xerus is 4.15.0-128-generic
). We can load and unload these modules using the insmod
and rmmod
commands respectively. Kernel modules are often loaded into /dev/*
or /proc/
. There are 3 main module types: Char, Block and Network.
Char Modules are deceptively simple. Essentially, you can access them as a stream of bytes - just like a file - using syscalls such as open
. In this way, they're virtually almost dynamic files (at a super basic level), as the values read and written can be changed.
Examples of Char modules include /dev/random
.
I'll be using the term module and device interchangeably. As far as I can tell, they are the same, but please let me know if I'm wrong!
Heavily beta
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!
The most simple of vulnerabilities
A double-fetch vulnerability is when data is accessed from userspace multiple times. Because userspace programs will commonly pass parameters in to the kernel as pointers, the data can be modified at any time. If it is modified at the exact right time, an attacker could compromise the execution of the kernel.
Let's see it in action.
Let's say we wish to replace the authentication of the kernel with our own module to handle it. The password to all users on this system is p4ssw0rd
. However, for security purposes, we do not wish to allow anybody to log in as root
(yes, it's a very specific case, but it helps to make it clear!).
We're going to create a really basic authentication module that allows you to read the flag if you input the correct password. Here is the relevant code:
If we attempt to read()
from the device, it checks the authenticated
flag to see if it can return us the flag. If not, it sends back FAIL: Not Authenticated!
.
In order to update authenticated
, we have to write()
to the kernel module. What we attempt to write it compared to p4ssw0rd
. If it's not equal, nothing happens. If it is, authenticated
is updated and the next time we read()
it'll return the flag!
Let's first try and interact with the kernel by reading from it.
Make sure you sudo chmod 666 /dev/authentication
!
We'll start by opening the device and reading from it.
Note that in the module source code, the length of read()
is completely disregarded, so we could make it any number at all! Try switching it to 1
and you'll see.
After compiling, we get that we are not authenticated:
Epic! Let's write the correct password to the device then try again. It's really important to send the null byte here! That's because copy_from_user()
does not automatically add it, so the strcmp
will fail otherwise!
It works!
Amazing! Now for something really important:
The state is preserved between connections! Because the kernel module remains on, you will be authenticated until the module is reloaded (either via rmmod
then insmod
, or a system restart).
So, here's your challenge! Write the same kernel module, but using ioctl
instead. Then write a program to interact with it and perform the same operations. ZIP file including both below, but no cheating! This is really good practise.
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.
We then register the purposes of the functions using module_init()
and module_exit()
.
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
The code below will be the contents of the read()
function of a kernel. I've removed , but this is the relevant part.
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 (even if just void).
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 .
Compiling a Kernel Object can seem a little more complex as we use a , but it's surprisingly simple: