arrow_back
Michael Zimmermann

libkhook

Load dynamically linked code into kernel space via /dev/mem

Introduction

/dev/mem is a virtual device on UNIX operating systems that allows you to read/write from physical memory.
It's not that useful for priviglege escalation because you need to be root to use this device.
Also, on many mobile devices it's either completely disabled or restricted by only allowing access to memory regions outside the kernel space.

The reason why I wrote khook was that it's a great opportunity to learn sth. about hardware components like the MMU and to develop a library which makes loading code into kernel space easy as pie.
libkhook was developed for the ARM architecture but with software abstractions to allow adding support for other architectures in future.

The hack

First we need to get the kernel's physical and virtual address range to be able to convert addresses.
The physical one can be read from /proc/iomem while the virtual one can be read from /proc/kallsyms.
/proc/kallsyms can be used to get virtual addresses from any exported symbol. The symbol name of the kernel base is '_text'.

Syscall table

Another useful thing we can find in /proc/kallsyms is the kernel's system call table(SCT).
The SCT is a flat table that holds information about calls, a userspace application can send to the kernel.
Using the SCT the kernel can translate the number of such a system call to a function to execute to.
The SCT's symbol name is 'sys_call_table'.

Once we have the SCT's address we can map it with readwrite access into our hacks's process using /dev/mem.

Running assembler in kernel space

Using the SCT we can run small assembler applications in kernel space.
To do so, we obtain a syscall function's address (I used uname), copy the code at it's location, run the syscall, and restore the function's code.
You need to be very careful when doing this, because the function should not be used by any other program in the meantime, and the code of the function you want to copy into kernel space needs to be smaller than the one you want to overwite so you don't overwrite anything else.

TTBR1

The TTBR1(Translation Table Base Register 1) is a register of the ARM architecture that's used by UNIX systems to hold the address of the kernel's MMU translation table.
This way we can translate any virtual address to a physical address - even if it's outside the static kernel range(i.e. addresses produced by kmalloc).

The TTBR1 register can only be read from the kernel's privileged mode. That's why we have to use the code injection method I explained in the former paragraph.

Kernel space memory allocation

Next, we need to allocate memory in kernel space to be able execute code of any size.
We can use kmalloc for allocating data memory, but we need executable memory to actually execute the code
To archieve this we can use the function '__vmalloc_node_range'. On ARM this function allows executable allocations between 0xbf000000 and 0xbfe00000.
Usually this is used to allocate memory for Loadable Kernel Modules (LKM). The good thing is, that this memory range is available even if LKM are disabled.

Loading Binaries into kernel

At this point, we have everything we need to load code into kernel space and actually ruu it.
Another thing that would be useful is loading relocatable and dynamically linkable binaries so you aren't limited to relative addresses and you can use internal (exported or not) kernel functions without knowing their addresses at compile time
Since this is not an easy task I searched for a easily adaptable solution that already exists which I did.
The Bootloader GRUB supports loading modules from a filesystem. They compile the C code to relocatable ELF binaries and relocate and link them at runtime.
Additionally this code was kinda easy to port to my hack's UNIX C environment.
And that's pretty much it. Now we can load ELF binaries from disk, allocate executable kernel memory, copy the code to that location, relocate it to the proper address, and link all kernel functions visible in /proc/kallsyms.

Summary

As you saw the hack has a few requirements. We need to be root, need a unprotected /dev/mem device, and /proc/kallsyms needs to be enabled.
But if that's the case you can easily load (custom) kernel modules - even if the kernel has LKM disabled.
To protect yourself against this, just enable 'CONFIG_STRICT_DEVMEM' and disable 'CONFIG_KALLSYMS' in your kernel config.


    languages used:

    C