KPROBES in a nutshell
August 24, 2019
What Kprobes is?
Here is the definition from kernel.org:
“Kprobes enables you to dynamically break into any kernel routine and collect debugging and performance information non-disruptively. You can trap at almost any kernel code address [1]_, specifying a handler routine to be invoked when the breakpoint is hit…
…When a CPU hits the breakpoint instruction, a trap occurs, the CPU’s registers are saved, and control passes to Kprobes via the notifiercallchain mechanism. Kprobes executes the “pre_handler” associated with the kprobe, passing the handler the addresses of the kprobe struct and the saved registers.”
~ So basically it allows you to run 2 functions, prehandler and posthandler, every time the probed function is invoked ~
To be honest, the first time I heard about Kprobes, Jprobes, Kretprobes and so on … It all sounded a bit complicated to me. Happy to say though, that after some hours doing researches, it now starts to make sense.
Note that nowadays there is an easier way to use Kprobes than the one I’ll show you today … but I’ll write about that in one of the next articles. Yes, I am talking about bpf() !
So how are we going to use Kprobes today?
Easy! By creating a simple kernel module, inserting it into our Kernel and testing it. Don’t be scared it is a really simple task even if it sounds tricky.
TUTORIAL GOAL: Create a kernel module that uses Kprobes to count anytime the function ${function} is used.
First thing first: REQUIREMENTS!
You need a Linux machine!
NOTE: I’ve only tested this procedure on my private server (Ubuntu 18.04.2 LTS Bionic Beaver) so you might need to find the right packages names if you’re using a different OS, and the Kernel module we’ll create might not work on different architectures.
-
Create the working directory and install the required packages.
mkdir ./ish-ar.io-lab/ && \ touch ./ish-ar.io-lab/{Makefile,ish.c} && \ cd ./ish-ar.io-lab/ apt-get update && \ apt-get install gcc strace make libelf-dev -y
-
Edit the file
Makefile
as follow:obj-m +=ish.o KDIR= /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -rf *.o *.ko *.mod.* .c* .t*
NOTE: when you need to call
make
inside aMakefile
it is a best practice to use the variable$(MAKE)
not the command.IMPORTANT: Make sure you’re using tabs and not spaces on your Makefile, otherwise you’ll get an error saying:
Makefile:N: *** missing separator (did you mean TAB instead of 4 spaces?). Stop.
-
We need to find out which function we want to count/intercept.
-
In this example, I wanted to count every time a program is executed. So I’ve searched the function I wanted to like this:
strace ls 2>&1 | less
-
At the top you should see something like this:
execve("/bin/ls", ["ls"], 0x7fff38f23780 /* 21 vars */) = 0
-
It looks like
execve
is the function we want to intercept! We now need its memory address to probe it later. So let’s search for it:root@ip-172-31-3-95:~/lab# grep sys_execve /proc/kallsyms ffffffffbcc7f010 T sys_execve ffffffffbcc7f050 T sys_execveat ffffffffbcc7f0b0 T compat_sys_execve ffffffffbcc7f100 T compat_sys_execveat
If you don’t know what this file
/proc/kallsyms
is, you should check out this page -> https://onebitbug.me/2011/03/04/introducing-linux-kernel-symbols/ - So, we have our function called sys_execve and its address is ffffffffbcc7f010.
-
-
Now edit the file
ish.c
:-
We need to include the required libraries, so at the top of our C program let’s type:
NOTE: This library #include<linux/kprobes.h> as you can notice by its name is fundamental to use kprobes.
#include<linux/module.h> #include<linux/version.h> #include<linux/kernel.h> #include<linux/init.h> #include<linux/kprobes.h>
-
Right after the includes, create 2 simple structures. We will need them later.
static unsigned int counter = 0; static struct kprobe kp;
-
Do you remember I’ve written about the prehandler and posthandler functions? Let’s create them first.
Just as a reminder: the prehandler function is executed right before our intercepted function and the posthandler function is executed after it.
int kpb_pre(struct kprobe *p, struct pt_regs *regs){ printk("ish-ar.io pre_handler: counter=%u\n",counter++); return 0; } void kpb_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags){ printk("ish-ar.io post_handler: counter=%u\n",counter++); }
-
Right after these 2 functions let’s create our module entry-point and exit-point.
int minit(void) { printk("Module inserted\n "); kp.pre_handler = kpb_pre; kp.post_handler = kpb_post; kp.addr = (kprobe_opcode_t *)0xffffffff8d67f010; register_kprobe(&kp); return 0; } void mexit(void) { unregister_kprobe(&kp); printk("Module removed\n "); } module_init(minit); module_exit(mexit); MODULE_AUTHOR("Isham J. Araia"); MODULE_DESCRIPTION("https://ish-ar.io/"); MODULE_LICENSE("GPL");
Every time you insert this module the function minit will be triggered and if you remove the kernel module the function mexit will be invoked.
IMPORTANT: Replace
kp.addr = (kprobe_opcode_t *)0xffffffff8d67f010;
with the function memory address you discovered at step 3 —>kp.addr = (kprobe_opcode_t *)0xFUNCTION_MEMORY_ADDRESS;
.
-
-
Your early created Kernel Module should look like this:
#include<linux/module.h> #include<linux/version.h> #include<linux/kernel.h> #include<linux/init.h> #include<linux/kprobes.h> static unsigned int counter = 0; static struct kprobe kp; int kpb_pre(struct kprobe *p, struct pt_regs *regs){ printk("ish-ar.io pre_handler: counter=%u\n",counter++); return 0; } void kpb_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags){ printk("ish-ar.io post_handler: counter=%u\n",counter++); } int minit(void) { printk("Module inserted\n "); kp.pre_handler = kpb_pre; kp.post_handler = kpb_post; kp.addr = (kprobe_opcode_t *)0xFUNCTION_MEMORY_ADDRESS; register_kprobe(&kp); return 0; } void mexit(void) { unregister_kprobe(&kp); printk("Module removed\n "); } module_init(minit); module_exit(mexit); MODULE_AUTHOR("Isham J. Araia"); MODULE_DESCRIPTION("https://ish-ar.io/"); MODULE_LICENSE("GPL");
-
Now let’s build and insert our module:
-
Type inside your working directory:
make
-
You should have an output like this:
make -C /lib/modules/4.15.0-1044-aws/build SUBDIRS=/root/ish-ar.io-lab modules make[1]: Entering directory '/usr/src/linux-headers-4.15.0-1044-aws' CC [M] /root/ish-ar.io-lab/ish.o Building modules, stage 2. MODPOST 1 modules CC /root/ish-ar.io-lab/ish.mod.o LD [M] /root/ish-ar.io-lab/ish.ko make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-1044-aws'
-
To insert the module type:
insmod ish.ko
-
And to see if the module is loaded type:
root@ip-172-31-3-95:~/ish-ar.io-lab# lsmod | grep ish ish 16384 0
-
-
Does it work? Let’s test it! We need to execute something so let’s type
ls
and then see dmesg:root@ip-172-31-3-95:~/ish-ar.io-lab# dmesg output: [ 4813.434548] Module inserted [ 4815.142934] ish-ar.io pre_handler: counter=0 [ 4815.142935] ish-ar.io post_handler: counter=1
So if you have an output like this… YES! It works!
-
To remove the module just type:
rmmod ish
NOTE: the code for this tutorial can be found here -> kprobes-demo
What we’ve learned?
How to use Kprobes using kernel modules, what are the prehandler and posthandler and how to use them to count every time a function is called (e.g.: sys_execve)
Ciao! This is Ish(~) and here's my blog. A bunch of articles, posts and considerations mostly from projects I am currently following.
I would love to read your opinions or answer the questions you might have, so please feel free to get in touch on Twitter! :D