Nĭménhăo,
today let's dive into the world of kernel development.
Don't worry, it might seem hard at first, but everything's hard at first.
What will the kernel module (Linux drivers are actually modules) do?
Well, let's start with something simple: a character device, camouflaged as a device that, at first glance, would hold
nudes.
Yep, you read correctly, we are basically setting up a
honey pot device that will notify us if somebody tries to read from it (ghihi).
Introduction
We will build a
character device here, but with some effort you can basically code any type of kernel module. If you ever worked with arduino, for example, it will show up as
ttyACM0 under /dev/, because somebody took the time and created a driver module that connects the USB port to the physical hardware. Our module would (if you followed up) show up as
Nudes, and talking to it by will raise a kernel log for you, thus will show you if somebody searched your devices.
Programming a kernel module is in some ways different from creating a user-space program.
First of all, you will not have all these lazy methods from the c standard library. Don't worry tho, there are means to achieve anything we want (namely lazy functions from the linux kernel). Also, communication with user space has to be interfaced with several functions, but we will need only one in our case, because I try to keep this tutorial as simple as possible. Another point is that you will have to create a so called
Makefile which will tell the kernel how compile the module, and you will have to plug it in per hand.
(You can also make per hand, but why should you? Instead try to write a plugging-script that will inject the module into the kernel at startup, you should know how to do this at the end of this page)
Basic setup and Makefile
Ok, we're fired up, have our pot of coffee (or Rum if you prefer) and already started our favorite editor. If you create a folder for this project,
make sure there are no spaces in folder names at all!
I once searched for a compilation error due to this for about an hour.
So, let's setup a c source file.
I will call it
nothing.c, since the driver actually does nothing but troll people.
First of all, we will need 3 basic libraries, namely
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
Also, we definitely need an __init function and an __exit function. We will go with c naming conventions here and
let function names begin with double underscores! This is not debatable, since userland programs that would later interact with our module could have problems if you fiddle up the naming conventions.
(Also, having written a bunch of assembler in my life, I am used to strange naming conventions, just deal with it)
So this is what the basic skeleton of the program would look like:
(I called the driver nothing.c and named it maddrive internally, do as you please)
#include <linux init.h="">
#include <linux module.h="">
#include <linux kernel.h="">
static int __init maddrive_init(void)
{
pr_info("Exhibitionist Kernel Driver was loaded\n");
return 0;
}
static void __exit maddrive_exit(void)
{
pr_info("Exhibitionist Kernel driver exited\n");
}
module_init(maddrive_init);
module_exit(maddrive_exit);
At 6 and 12 you can see our init and exit functions, respectively. We will also have to
register these 2 functions, thus having 2 lines that explicitly do this at the end of the file. The reason for this is, on linux, we can just inject modules into the kernel, while "other" operating systems require you to restart every time something in the kernel changes (and recompile the kernel during boot).
Note the
pr_info calls here. They will log stuff into the kernel log file.
See, we can't just use standard c-style printf() calls here since we are not using the c standard library.
Instead we use printk() from
kernel.h, to be specific we are using wrappers.
There are these wrappers available for logging and printing stuff:
- pr_emrg("text, emergency level");
- pr_alert("text for alert log");
- pr_crit("critical failure log(?)");
- pr_err("standard error print");
- pr_warning("this is self explaining");
- pr_notice("another one, lower importance");
- pr_info("for our module loaded information");
- pr_debug("for testing purpose");
To check the log file, you can either run
sudo dmesg or, for a live tracking,
tail -f /var/log/kernel.log
So much to that, now let's look at the
Makefile:
obj-m += nothing.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
This one is pretty simple, right? At the beginning we tell the script what object-file we would like to create, nothing.o in this case.
(This is basicly the same as if you tried to compile a c program and stopped after the preprocessor/assembler step, right before the linker comes and links together all your raw source code)
There is also a reference to the clean command, we will get to this later.
Notice the
$(shell uname -r) and
$(PWD) commands? The nice thing about Linux is that you can inject shellcode into other shellcode, so what this basically does is run a subshell and inject the result in this code.
uname -r: This will print out the kernel version you are currently running, in case you have multiple kernel versions on your machine (5 in my case due to updates)
PWD: Stands for "
print
working
directory", shorter than just writing the global path to your files
Test what we have so far
So, we have learned a bunch of stuff up to this point. Or maybe not, I don't really care what you did with your time.
Time to make the module and try injecting it into the kernel, after that we can go on and form it out.
Don't be mad if it does not work out on the first try, read everything again carefully or write me a message what is failing.
To build our module, just run make inside the folder containing the 2 files we created.
This will either raise a bunch of errors and return an error code (usually 2 in my case), or it will run to the end and create 6 files in your folder.
The one we are interested in is named
nothing.ko (or however your program is named).
Time to insert it into the kernel. It is a good practice to live-track kernel messages while writing this module. To do this, fire up a shell and type
tail -f /var/log/kernel.log
To insert the .ko (
kernel
object) file, we are using the command insmod as superuser, so
sudo insmod nothing.ko
If everything went smooth, the shell logging the tail output should now print out some numbers and the message you specified in the code.
But since our module does nothing at this point, we will plug it out for once, the command is
sudo rmmod nothing
Well, congratulations, you basically wrote and injected a kernel module at this point, although it does nothing but waste RAM. At this point you might as well turn off your computer and send your cv to electronic arts, lul.
Make an actual (virtual) device out of our module
So, at this point we managed to inject a kernel module, but didn't we initially wanted to write a virtual device? Let's get down to business now.
First of all, we will need to include 2 additional headers, namely
#include <linux/uaccess.h>
#include <linux/fs.h>
I will not explain the contents of these headers here, go on and google them if you want.
Next, we will declare the name of our device:
#define DEVICE_NAME "Nudes"
Following up we will need to declare a struct that describes the device functionality of our virtual device. Basically we declare function pointers to open, close, read and write.
If you are new to c, a struct is approximately what an object in an oop-language is, just without all the classes stuff and hidden pointers and whatnot-ugly-stuff java developers came up with.
Also, we will reserve a doubleword (an int, 32bit variable) to hold the major number of our device.
We will need this number later, either by printing it out, or reading the /proc/devices table. The major number of a device is needed to kind of register it within the system.
Here is the modified beginning of our module:
#include <linux init.h="">
#include <linux module.h="">
#include <linux kernel.h="">
#include <linux uaccess.h="">
#include <linux fs.h="">
#define DEVICE_NAME "Nudes"
//device operations
static int dev_open(struct inode*,struct file*);
static int dev_release(struct inode*,struct file*);
static ssize_t dev_read(struct file*,char*,size_t,loff_t*);
static ssize_t dev_write(struct file*,const char*, size_t,loff_t*);
//struct that maps operations
static struct file_operations fops = {
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
static int major;
static int __init fahrer_init(void)
{
//ask to get a major number for the kernel module
major = register_chrdev(0,DEVICE_NAME,&fops);
//check if it worked
if (major < 0)
{
pr_err("Exhibitionist Kernel Driver failed to load, major was:");
return major;
}
pr_info("Exhibitionist Kernel Driver was loaded\n");
return 0;
}
static void __exit fahrer_exit(void)
{
unregister_chrdev(major,DEVICE_NAME);
pr_info("Exhibitionist Kernel driver exited\n");
}
Let's quickly sum up what we did here:
- Line 8
- Define the final device name
- Lines 10-14
- Define prototypes for the device functions
- Prototypes use either a pointer to our struct (inode*), or a pointer to file (file*) since every device in Linux, from a user-space perspective, is seen as a file
- size_t is the size of an allocated memory block, ssize_t is the signed version of this, and loff_t is the offset, but the os will manage this value for us
- Lines 16-22
- The actual struct which basically only maps the device functions to our real functions
- Line 24
- The doubleword that will later hold our major number, imagine this as kind of device id
- Line 29
- This command will register our device with the associated device id
- The device id will be given by the operating system, just take it as it stands
- Line 45
- Everything that once registered must eventually unregister, otherwise you'd at some point have a bunch of dead devices that won't free their pointers forever, this is what this line does
Nice, we are almost done!
Now all that's left is to write out the functions we just made prototypes for.
These 4 functions are not very hard, however the correct arguments and pointers must be provided.
To make things easier, I will just copy the finale program for you:
The final module program code
So, after we did some prototyping and stuff, let's see what you all really came here for: The final kernel driver module. If you actually took the time and read through my explanations, congratulations, you might have learned something today.
Let's elaborate how the kernel module should behave:
- Get loaded and injected into the kernel
- Wait until somebody tries to open the honeypot, immediately log this event to kernel.log
- Kindly respond with a quote from Alien resurrection (1997)
#include <linux init.h="">
#include <linux module.h="">
#include <linux kernel.h="">
#include <linux uaccess.h="">
#include <linux fs.h="">
#define DEVICE_NAME "Nudes"
//device operations
static int dev_open(struct inode*,struct file*);
static int dev_release(struct inode*,struct file*);
static ssize_t dev_read(struct file*,char*,size_t,loff_t*);
static ssize_t dev_write(struct file*,const char*, size_t,loff_t*);
//struct that maps operations
static struct file_operations fops = {
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
static int major;
static int __init fahrer_init(void)
{
//ask to get a major number for the kernel module
major = register_chrdev(0,DEVICE_NAME,&fops);
//check if it worked
if (major < 0)
{
pr_err("Exhibitionist Kernel Driver failed to load, major was:");
return major;
}
pr_info("Exhibitionist Kernel Driver was loaded\n");
return 0;
}
static void __exit fahrer_exit(void)
{
unregister_chrdev(major,DEVICE_NAME);
pr_info("Exhibitionist Kernel driver exited\n");
}
//##########################################################################
//WRITE OUT THE DEVICE OPERATIONS
static int dev_open(struct inode *inodep, struct file *filep)
{
pr_info("SOMEBODY WANTED TO SEE NUDES!!!!");
return 0;
}
static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset)
{
pr_info("Nudes folder is read-only, sry mate");
return -EFAULT;
}
static int dev_release(struct inode *inodep, struct file *filep)
{
pr_info("Nudes device was closed");
return 0;
}
static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset)
{
int errors = 0;
char *message = "Father's dead, asshole. Intruder on level one. All aliens, please proceed to level one.\n";
int message_len = strlen(message);
errors = copy_to_user(buffer,message,message_len);
return errors == 0 ? message_len : -EFAULT;
}
module_init(fahrer_init);
module_exit(fahrer_exit);
You should understand most of the code if you followed up until here, don't worry if the function arguments seem a bit weird, tho. Notice the copy_to_user function marked in the line 74. This will transfer messages from kernel-space to user-space. As you might imagine, there is also a vice-versa function.
TODO
If you are interested in this topic and want to learn more, try writing a device that takes user input and returns something depending on the input. For example, try mapping each password for different web pages and return them if the user asks for it (and of course require sudo permission for this)
So, all set up, let's make this and inject it into the kernel
Inject the module to the kernel
So, after all is said and done, try and make your module.
First, if you made the test module earlier, you will now need to type make clean (remember how we set this up in the Makefile?), this will delete all the stuff previously created. If you want you can do this manually, but why bother?
Again, after make, use sudo insmod nothing.ko to insert the kernel module.
Now we are almost finished!
Remember how I told you we would need the major number of our module? Now is the time for this.
Use
cat /proc/devices and look out for your module. You want to remember the
major number it shows you there. In the picture I added you can see my Nudes module with major number 241. (use grep command for ease of use)
Now, head over to /dev/
Use sudo mknod Nudes c 241 0 to finally register the device with the system.
Nudes in this case is the name which you want the device to have, c stands for character device, the 241 is the major number, and a trailing 0 is required in this case for the mknod command.
If you for whatever reason want to deregister the device, the command would be as simple as sudo rm /dev/Nudes.
However, after a machine restart the device would be gone anyway and you had to register it again.
This is where shellscripting comes in handy, so try to fiddle together a script that automatically loads your module at startup.
Like before, check via tail -f /var/log/kernel.log that everything went smooth.
If you are ready, list the devices in /dev/ and check whether your Nudes device shows up.
If it does, there is one final test to make: Read from the device.
You can do so however you want, I always giggle by typing in more Nudes, and watch the fun happen. Again, check sudo dmesg, you should now have a log about somebody beeing a bit to curious about your devices...
And that's it, cybermonkeys. I hope you had fun today messing around with devices.
Of course this is not the best honeypot, and it is rather fishy to begin with, but it is a good opportunity to learn about kernel driver programming, and from here on you can go and build your own awesome modules. Don't forget to stop in and give me a holler when you managed to get a famous kernel hacker.
I feel the wind rustle through my feathers.
"Come home", he whispers, "where you belong."
"I'm afraid", I respond, "it's not going to be easy like this."
- numb.3rs
Comments
Post a Comment