KPlugs is a Linux kernel module which provides an interface for dynamically executing scripts inside the Linux kernel. KPlugs uses a simple bytecode interpreter (the KPlugs Virtual Machine), and an interface that allows a user to dynamically load scripts into the kernel and execute them directly from user space. Because the interface is dynamic, it's easy to implement a user-mode library that wraps anything in the kernel!

KPlugs comes with a Python library that compiles a subset of the Python language to the KPlugs bytecode, and lets you easily load and execute your "kernel Python script".

The Python compiler is very basic and was made to fit our own purposes, but if it's not exactly what you are looking for, you can modify it to create your own subset.

A few reasons why KPlugs is a strong and unique tool:
  • The bytecode is generic and is not specific to any scripting language. If you prefer another language to Python, write a compiler for your preferred language that compiles to KPlugs bytecode, and you'll be able to run scripts written in that language in the kernel!
  • When you load a function, the return value is a valid function pointer for all intents and purposes. You can call it from kernel mode or pass it as a callback to a kernel function, as long as you have the right calling convention (see the FAQ).
  • The VM provides a safe environment for the bytecode to run, where every operation is checked to avoid system crashes. This works even though you are free to pass user space and kernel space buffers interchangeably to your function at your convenience; KPlugs does all the worrying for you. Of course, if you incorrectly call external functions in your bytecode, they may crash.
  • The bytecode is a high-level interpreted language (supporting exceptions, for example), but its basic variable type is the standard CPU word. This allows you to write code in a high level scripting language such as Python on one hand, and interface naturally with common kernel functions on the other hand.

Get the last version sources here.

A simple KPlugs Python script:

#!/usr/bin/python

import kplugs

try:
    kernel_func = r'''

def hello_world(string):
    buffer(string, 0x100)
    print "%s" % string
'''

    plug = kplugs.Plug()
    hello_world = plug.compile(kernel_func)[0]
    hello_world("Hello World!")

finally:
    kplugs.release_kplugs()


The KPlugs Python library comes with some example classes (and you are encouraged to use them in your scripts):

  • kplugs.Mem - access all of the computer's memory of the computer (kernel and all processes' user space)
  • kplugs.Symbol - resolve kernel symbols
  • kplugs.Hook - hook kernel functions with a KPlugs function
  • kplugs.Caller - call an exported kernel function
#!/usr/bin/python

import kplugs
import struct

try:
    plug = kplugs.Plug()
    mem = kplugs.Mem()
    sym = kplugs.Symbol()
    caller = kplugs.Caller()
    hook = kplugs.Hook()

    jiffies = sym["jiffies"] # Getting the current time
    kernel_buf = mem.alloc(kplugs.WORD_SIZE) # Allocating a buffer in kernel space
    mem[kernel_buf] = mem[jiffies:jiffies+kplugs.WORD_SIZE] # Copying the current time to our kernel buffer
    print "The current time is: ", struct.unpack("P", mem[kernel_buf:kernel_buf+kplugs.WORD_SIZE])[0]

    kernel_func = r'''

def my_hook(kp, regs):
    print "The registers are stored in %p" % regs
    return 0
'''
    my_hook = plug.compile(kernel_func)[0]
    hook.hook("vmalloc", my_hook) # Hook the kernel function - vmalloc
    p = caller["vmalloc"](0x100) # Execute the kernel function - vmalloc. the hook should be executed
    caller["vfree"](p)
    hook.unhook(my_hook)

finally:
    kplugs.release_kplugs()


Exception handling:

#!/usr/bin/python

import kplugs

try:
    kernel_func = r'''
STATIC("my_helper")

def my_function():
    try:
        my_helper()
        print "OK"
    except word as err:
        print "Exception number: %d" % -err
        raise err

def my_helper():
	KERNEL_undefined_function()

'''

    plug = kplugs.Plug()
    my_function = plug.compile(kernel_func)[0]
    my_function()

finally:
    kplugs.release_kplugs()


Using kernel functions:

#!/usr/bin/python

import kplugs
import ctypes

try:
    kernel_func = r'''
BUFFER_SIZE = 0x10
ERROR_POINT = 12 # taken from types.h

def get_initramfs(user_initram):
    buffer(user_initram, BUFFER_SIZE)
    pointer(initram)
    pointer(sizeptr)

    initram = KERNEL_kallsyms_lookup_name("__initramfs_start")
    sizeptr = KERNEL_kallsyms_lookup_name("__initramfs_size")
    if initram == 0 or sizeptr == 0:
        raise ERROR_POINT

    size = DEREF(sizeptr)
    if size > BUFFER_SIZE:
        size = BUFFER_SIZE
    KERNEL_memcpy(user_initram, initram, size)
    return size
'''

    plug = kplugs.Plug()
    buf = ctypes.c_buffer(0x10)
    get_initramfs = plug.compile(kernel_func)[0]
    size = get_initramfs(ctypes.addressof(buf))
    print "The initramfs starts with: '%s'" % (buf.raw[:size].encode("hex"), )

finally:
    kplugs.release_kplugs()

What is KPlugs?

KPlugs is a linux kernel module that gives an interface for dynamically executing scripts inside the Linux kernel. The user can create KPlugs functions and execute them directly from user space. The functions then run on a virtual machine designed to be simple, but still give the user all he needs to safely implement any functionality. KPlugs comes with a Python library that compiles a subset of the Python language to the KPlugs bytecode, complete with bindings to the kernel module interface, allowing the user to simply load, execute and unload functions directly from a Python script.

License

KPlugs is licensed under GPLv3.

What architectures are supported by KPlugs?

KPlugs was written for x86, both 32 and 64 bits. KPlugs can be changed to support any other architecture by adjusting the assembly file "calling_wrapper.s". More information on that inside the file.

What Linux versions are supported by KPlugs?

KPlugs was only tested on Linux kernel version 3.7.1. However, because it uses only standard kernel functions, it should work on any recent kernel.

How do I use KPlugs?

Download KPlugs from the download section.

KPlugs is a kernel module and as such requires your kernel's sources to compile. The KPlugs Makefile assumes you have the sources of your Linux kernel in the standard location (/lib/modules/`uname -r`/). If you put the sources elsewhere, edit the Makefile and point it to the right location.
You should then be able to build the module using make.

If everything worked, you should have two modules inside the Release and Debug directories: kplugs_release.ko and kplugs_debug.ko. The debug version just adds some debug prints to help you debug the kernel module if you wish.

Finally, loading the kernel module is done by running "insmod ./kplugs_release.ko" (or kplugs_debug.ko if you want to debug the module).

How do I use the KPlugs Python library?

The Python library is (surprisingly) located inside the python directory. It contains three files:

  • __init__.py - A standard Python init file (just loads kplugs.py).
  • core.py - Implements the Python subset compiler and the Plug class. The compiler uses the Python ast library.
  • kplugs.py - Uses core.py to bring the user an easy interface to KPlugs.
To install the Python bindings, simply copy the python directory to your Python installation's Lib directory, and change the name to kplugs.

Now, when writing your Python script you should be able to "import kplugs" and use the library as shown in the examples section.
Always call kplugs_release() at the end of your script, regardless of exit status. We recommend wrapping your script with a try...finally clause.

We invite you to look inside kplugs.py. The file is pretty simple, and will help you understand how KPlugs works.

Why did you create your own bytecode instead of porting the Python engine to kernel mode?

At first, we considered porting the entire Python engine. We eventually decided against it because of two main reasons:

  • One of the things we wanted from KPlugs is the ability to call kernel functions, and to be called from a kernel function. The problem is that kernel functions works with words and pointers, and the Python engine works with Python objects. We would have to create a wrapper for every function to convert inputs to Python objects (and vice versa). The bytecode allows us to work directly with words and pointers, making the integration more natural.
  • We imagined KPlugs would be used when a certain kernel-only functionality is wanted. This doesn't mean that all the logic of the script has to run in kernel-mode. This makes porting the entire engine a potentially huge waste of time - and without good reason.
For these reasons we chose to present a flexible interface, and let you keep most of the code in user-space where it belongs. An added benefit is that users who want to use other scripting languate to use KPlugs are free to do so - they only need to write a compiler.

How do I create functions with KPlugs?

The KPlugs module creates the device file "/dev/kplugs" to communicate with the user. Every file descriptor a user holds to that device is called a plug. Each plug receives a context. A user can load functions to a plug's private context, so that only he could use those functions (unless he gives another process a pointer to his function), or he could load them to a special context - The global context. When a user tries to execute a function using the KPlugs interface, KPlugs tries to find it in the plug's context first, and then it tries to find it in the global context.

Functions loaded to a local context are automatically unloaded when its plug is unplugged - when the user closes the file descriptor, but functions loaded to the global context are unloaded only when the user issues the unload command (or if the KPlugs kernel module is removed using rmmod).

KPlugs doesn't allow loading two functions with the same name to the same context. You may create an anonymous function - a function without a name. An anonymous function uses the function's address as an identifier instead of the function's name.

What are the KPlugs bytecode's variable types?

KPlugs has four variable types: word, pointer, buffer and array:

  • word - An integer the size of the processor word.
  • pointer - Like a word, but can be used to read and write to memory.
  • buffer - A buffer of bytes.
  • array - An array of words.
When the KPlugs VM accesses memory, it performs bounds checking in order to avoid crashes. Pointers are always considered unsafe and are checked before every dereference. Buffers and arrays are slightly more complicated because they can come from user space, but they can't move. for that reason, when a user's buffer or array is used, it is mapped to kernel space so it can be used safely during the function's execution.

If you want to call a kernel function with a buffer (or an array) from user-space as an argument, you can just simply call it from your compiled KPlugs function (i.e., from the KPlugs bytecode), and the VM will map the buffer to kernel-space before calling the function. Naturally, It will not work with pointers because there is no way for KPlugs to know the size of the buffer the pointer points to.

How does KPlugs resolve function names at run-time?

Most of the functions inside the Linux kernel use the same calling convention. This is why you can compile a kernel module and let other modules call your functions. The problem is that you have to know the function prototype at compile-time, which is obviously problematic when you want to load scripts inside the kernel. Other solutions such as SystemTap tackle this problem by compiling a module for every script you want to load.

For KPlugs we had to find another solution because we wanted the symbols to be resolved at run-time. Most of the kernel functions use either fastcall or cdecl as their calling convention (the vast majority uses fastcall). Lucky for us, these two conventions don't require knowing beforehand how many arguments were passed to us, i.e., two identical functions with the same calling convention and a different number of arguments are compiled exactly the same.

We exploit that fact when calling kernel functions from within bytecode. We create functions pointers for every calling convention, and use the right function pointer depending on the chosen calling convention for the specific function. The reason we need more than just the standard calling convention is because sometimes the compiler chooses another calling convention for functions with a variable number of arguments (like printk). For now KPlugs support the standard calling convention and the calling convention of variable-length argument functions. Knowing the function's calling convention, the KPlugs VM calls find_symbol to find the requested function's address, and can then set up the argument and call it.

Also, when you create a KPlugs function you can choose the calling convention for it. Your choice doesn't have any effect when you call your function through the KPlugs interface, but it does when you call your function directly by its address. In that case a proper wrapper function (depending on which calling convention you chose) will be executed. The wrapper will get the caller's arguments (It can't know the exact number so it will just pop the maximum number of arguments that a function takes), and start the VM.

What if I have more questions?

You can contact us, and we'll answer your questions as soon as we can.

If you have any comment, we'd love to hear from you. Contact us at

contact@kplugs.org

Created by Aviel Warschawski