arrow-left

All pages
gitbookPowered by GitBook
1 of 3

Loading...

Loading...

Loading...

Interactivity with IOCTL

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, . 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:

static ssize_t ioctl_handler(struct file *file, unsigned int cmd, unsigned long arg) {
    printk("Command: %d; Argument: %d", cmd, arg);

    return 0;
}
.ioctl has been removed and replaced by .unlocked_ioctl and .compat_ioctlarrow-up-right
#include <sys/ioctl.h>

// [...]

ioctl(fd, 0x100, 0x12345678);    // data is a string
static struct file_operations fops = {
    .ioctl = ioctl_handler
};
static struct file_operations fops = {
    .compat_ioctl = ioctl_handler,
    .unlocked_ioctl = ioctl_handler
};

An Interactive Char Driver

Creating an interactive char driver is surprisingly simple, but there are a few traps along the way.

hashtag
Exposing it to the File System

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:

hashtag
Cleaning it Up

These additional classes and devices have to be cleaned up in the intro_exit function, and we mark the major number as available:

hashtag
Controlling I/O

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!

hashtag
Full Code

Simply use sudo insmod to load it, .

hashtag
Testing The Module

Create a really basic exploit.c:

If the module is successfully loaded, the read() call should read QWERTY into buffer:

Success!

#define DEVICE_NAME "intro"
#define CLASS_NAME "intro"

// setting up the device
int major;
static struct class*  my_class  = NULL;
static struct device* my_device = NULL;

static int __init intro_init(void) {
    major = register_chrdev(0, DEVICE_NAME, &fops);    // explained later

    if ( major < 0 )
        printk(KERN_ALERT "[Intro] Error assigning Major Number!");
    
    // Register device class
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "[Intro] Failed to register device class\n");
    }

    // Register the device driver
    my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "[Intro] Failed to create the device\n");
    }

    return 0;
}
as we did before
static void __exit intro_exit(void) {
    device_destroy(my_class, MKDEV(major, 0));              // remove the device
    class_unregister(my_class);                             // unregister the device class
    class_destroy(my_class);                                // remove the device class
    unregister_chrdev(major, DEVICE_NAME);                  // unregister the major number
    printk(KERN_INFO "[Intro] Closing!\n");
}
major = register_chrdev(0, DEVICE_NAME, &fops);
static ssize_t intro_read(struct file *filp, char __user *buffer, size_t len, loff_t *off) {
    printk(KERN_ALERT "reading...");

    copy_to_user(buffer, "QWERTY", 6);

    return 0;
}

static struct file_operations fops = {
    .read = intro_read
};
ssize_t read(int fd, void *buf, size_t count);
#include <linux/init.h>
#include <linux/module.h>

#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "intro"
#define CLASS_NAME "intro"

MODULE_AUTHOR("ir0nstone");
MODULE_DESCRIPTION("Interactive Drivers");
MODULE_LICENSE("GPL");

// setting up the device
int major;
static struct class*  my_class  = NULL;
static struct device* my_device = NULL;

static ssize_t intro_read(struct file *filp, char __user *buffer, size_t len, loff_t *off) {
    printk(KERN_ALERT "reading...");

    copy_to_user(buffer, "QWERTY", 6);

    return 0;
}

static struct file_operations fops = {
    .read = intro_read
};

static int __init intro_init(void) {
    major = register_chrdev(0, DEVICE_NAME, &fops);

    if ( major < 0 )
        printk(KERN_ALERT "[Intro] Error assigning Major Number!");
    
    // Register device class
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "[Intro] Failed to register device class\n");
    }

    // Register the device driver
    my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "[Intro] Failed to create the device\n");
    }

    return 0;
}

static void __exit intro_exit(void) {
    device_destroy(my_class, MKDEV(major, 0));              // remove the device
    class_unregister(my_class);                             // unregister the device class
    class_destroy(my_class);                                // remove the device class
    unregister_chrdev(major, DEVICE_NAME);                  // unregister the major number
    printk(KERN_INFO "[Intro] Closing!\n");
}

module_init(intro_init);
module_exit(intro_exit);
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main() {
    int fd = open("/dev/intro", O_RDWR);    // Open the device with RW access
    printf("FD: %d\n", fd);                 // print the file descriptor

    char buffer[6];
    memset(&buffer, 'A', 6);                // fill with As
    printf("%s\n", buffer);                 // print
    read(fd, buffer, 6);                    // read from module
    printf("%s\n", buffer);                 // print again
}
$ ./exploit

FD: 3
AAAAAA
QWERTY

Writing a Char Module

hashtag
The Code

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 (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 .

hashtag
Compiling

Compiling a Kernel Object can seem a little more complex as we use a , but it's surprisingly simple:

$(MAKE) is a special flag that effectively calls make, but it propagate all same flags that our Makefile was called with. So, for example, if we call

Then $(MAKE) will become make -j 8. Essentially, $(MAKE) is make, which compiles 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.

hashtag
Using the Kernel Module

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.

circle-info

You can view currently loaded modules using the lsmod command

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Mine!");

static int intro_init(void) {
    printk(KERN_ALERT "Custom Module Started!\n");
    return 0;
}

static void intro_exit(void) {
    printk(KERN_ALERT "Custom Module Stopped :(\n");
}

module_init(intro_init);
module_exit(intro_exit);
requiring parametersarrow-up-right
there are many more typesarrow-up-right
Makefilearrow-up-right
obj-m += intro.o
 
all:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
$ make -j 8
$ sudo insmod test.ko
$ sudo dmesg | tail -n 1
[ 3645.657331] Custom Module Started!
$ sudo rmmod test
$ sudo dmesg | tail -n 1
[ 4046.904898] Custom Module Stopped :(