Introduction
This blog post is for documenting my own interpretation of kernel exploitation techniques. It contains progressive tutorials and steps setting up the environment. Take everything you read here with a grain of salt. I can’t promise that everything I say will be 100 % correct, and again this is just my own interpretation of the knowledge I’ve gathered from here and there. So, if you have some knowledge of user-land exploitation but have never done any ring-0 exploitation, you might find the information here helpful.
Setting up the environment
In these examples I’m using QEMU, I’m assuming you already have QEMU. If not, download it and install it. Also download pre-compiled x86 image, as this is going to be our debugging target kernel. If you prefer a newer kernel and different file-system, you can build either with Buildroot quite easy just don’t forget to include Debugging Symbols. To speed-up the emulation environment make sure to have KVM module installed as well (if possible). With KVM installed you can use the -enable-kvm
QEMU cmdline option, it will allow QEMU to utilize the hardware virtualization features and speed-up the VM. Now that you have QEMU and a working x86 image, you can use the following script to start the VM
#!/bin/bash
qemu-system-i386 -hda debian_wheezy_i386_standard.qcow2 -m 1024 -enable-kvm -nographic -net user,hostfwd=tcp::10022-:22 -net nic -S -s
- The
-m
option will allocate 1024 MB ram for the VM - The
-enable-kvm
will enable hardware virtualization features - The
-net user,hostfwd=tcp::10022-:22
will map local port 10022 to VM’s port 22, so you can login to the VM by SSHing into localhost:10022 -nographic
will disable X- If you decide to compile and use a new kernel you can use the
-kernel
and-append "root=/dev/sda1"
to use a separate kernel file - -S will pause boot until a debugger is attached and continue is passed
- -s will listen for debugger on forwarded port 1234. To use a different port, instead of using
-s
use the-gdb tcp::<port>
Although we can start debugging right away, we can simplify our lives and install a kernel with debugging symbols and optionally we can download kernel sources.
Starting with the debugging symbols, for the pre-compiled image download the following linked deb, install it on the guest VM, reboot into the new kernel and copy /usr/lib/debug/boot/vmlinux-3.2.0-4-686-pae
to the host.
# On host
$ scp -P 10022 ./linux-image-3.2.0-4-686-pae-dbg_3.2.81-1_i386.deb root@localhost:~
# On guest
root@debian-i386:~# dpkg -i ./linux-image-3.2.0-4-686-pae-dbg_3.2.81-1_i386.deb
root@debian-i386:~# shutdown -r now
# On host
$ scp -P 10022 root@localhost:/usr/lib/debug/boot/vmlinux-3.2.0-4-686-pae .
$ file vmlinux-3.2.0-4-686-pae
vmlinux-3.2.0-4-686-pae: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, BuildID[sha1]=725ffd76ce66dcc2aa0cf9d914a28aceef5ead2e, not stripped
For the source files, we can download them via apt-get
on the guest and transfer on the host. Once unpacked on the host, you can use the directory
gdb command to include them into the search path of gdb.
# apt-get update
# apt-get install linux-source-3.2
...
The sources will be in /usr/src/
usually in a tar archive. Move them to the host and extract them.
Now that we have the prepared gdb, kernel debugging symbols and sources we are good to do some debugging.
➜ kernel_dbg gdb
gdb-peda$ file vmlinux-3.2.0-4-686-pae
Reading symbols from vmlinux-3.2.0-4-686-pae...done.
gdb-peda$ source gef.py
gef➤ target remote :1234
Remote debugging using :1234
native_safe_halt () at /build/linux-M3n6wX/linux-3.2.81/arch/x86/include/asm/irqflags.h:50
50 /build/linux-M3n6wX/linux-3.2.81/arch/x86/include/asm/irqflags.h: No such file or directory.
gef➤ p prepare_kernel_cred
$1 = {struct cred *(struct task_struct *)} 0xc10529cc <prepare_kernel_cred>
gef➤ p commit_creds
$2 = {int (struct cred *)} 0xc1052761 <commit_creds>
gef➤
To add the sources:
gef➤ show directories
Source directories searched: $cdir:$cwd
gef➤ directory linux-source-3.2/kernel/
Source directories searched: /home/user/kernel/linux-source-3.2/kernel:$cdir:$cwd
gef➤ directory linux-source-3.2/arch/x86/include/asm/
Source directories searched: /home/user/kernel/linux-source-3.2/arch/x86/include/asm:/home/user/kernel/linux-source-3.2/kernel:$cdir:$cwd
gef➤ list
45 }
46
47 static inline void native_safe_halt(void)
48 {
49 asm volatile("sti; hlt": : :"memory");
50 }
51
52 static inline void native_halt(void)
53 {
54 asm volatile("hlt": : :"memory");
gef➤
Unfortunately PEDA doesn’t work with remote debugging but GEF seems to work.
Loading module’s symbols
Kernel modules are like hot pluggable functions to the kernel. We can load them with insmod
and modprobe
then the module’s functions get exported to the kernel. So far we have loaded the kernel symbols into gdb but adding a new module will not load the module’s functions into our remote debugger. Luckily we can load them manually.
We have already compiled the module with debugging symbols not stripped out.
$ file my_module.ko
my_module.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), BuildID[sha1]=d15c70c2673ea5540685b48cb88ffb4f270fd8da, not stripped
First we need to see the load addresses of the different sections.
root@debian-i386:~# cat /sys/module/my_module/sections/.
./ .gnu.linkonce.this_module .rodata.str1.1
../ .init.text .strtab
.data .note.gnu.build-id .symtab
.exit.text .rodata .text
root@debian-i386:~# cat /sys/module/my_module/sections/.text
0xf8357000
root@debian-i386:~# cat /sys/module/my_module/sections/.data
0xf8359000
root@debian-i386:~#
Now download the module to the host system where you have gdb running and apply the following commands:
gef➤ add-symbol-file my_module.ko 0xf8357000 -s .data 0xf8359000
add symbol table from file "my_module.ko" at
.text_addr = 0xf8357000
.data_addr = 0xf8359000
Reading symbols from my_module.ko...done.
gef➤
We can confirm that the symbols are working as intended (the same way we added the kernel sources we can add our module’s source files, so I will not be going over that…).
gef➤ p my_write
$4 = {ssize_t (struct file *, const char *, size_t, loff_t *)} 0xf8357000 <my_write>
gef➤ disas my_write
Dump of assembler code for function my_write:
0xf8357000 <+0>: push 0xf8358024
0xf8357005 <+5>: call 0xc12c33aa <printk>
0xf835700a <+10>: xor eax,eax
0xf835700c <+12>: call eax
0xf835700e <+14>: pop eax
0xf835700f <+15>: xor eax,eax
0xf8357011 <+17>: ret
End of assembler dump.
gef➤
Building kernel modules
To build kernel modules you’ll see gcc
and make
, as well as the kernel header files. You can obviously cross-compile, but I don’t think it’s worth the effort to build a quick module.
apt-get install gcc
apt-get install make
apt-get install linux-headers-`uname -r`
Make sure you have a Makefile. Here is a super simple template:
obj-m += <module>.o
ifndef KERNELSRC
KERNELSRC=/lib/modules/$(shell uname -r)/build
endif
all:
make -C $(KERNELSRC) M=$(PWD) modules
clean:
make -C $(KERNELSRC) M=$(PWD) clean
Once your build succeeds you should see a .ko file. You can then load it into the kernel with insmod
. to confirm that it loads successfully you can run lsmod
and make sure it’s listed. You can also check dmesg
to see any messages from the module itself.
Miscellaneous
insmod
loads a module into the running kernelmodprobe
loads module and any dependencies into the running kernelrmmod
unloads/removes a module from the kernellsmod
lists the currently loaded modules/dev
filesystem is where devices registered by modules appear. One way of communicating with your module is via the device your module registers- mmap_min_addr is a kernel exploitation mitigation mechanism that dictates the lowest memory address a user-land application can request via mmap. Default value is usually 65535 but the goal is to disallow requesting address 0. Additional way to disable/change it is to overwrite
/proc/sys/vm/mmap_min_addr
- No ASLR in the kernel
/proc/kallsyms
lists all symbols and their load address- Arguments to kernel functions are passed through registers sequentially in EAX, EDX, ECX and if there are more they are pushed to the stack
Null dereference
Starting with null pointer dereference, say that we have a vulnerability that overwrites a function pointer with 0 (NULL).
… TO BE CONTINUED (sorry about that)