It’s about time I post a detailed explanation about how my tpe-lkm module is able to enforce its security policy. This post is very technical, readers beware. Note that this writeup is based on the code as it was the latest commit, which was of this writing, was the one on Dec 10th, 2011. I’ll keep all the links relative to that date.
It all starts in the module.c file;
- tpe_config_init() initializes the sysctl configuration
- malloc_init(), defined in malloc.c, locates the addresses of module_alloc() and module_free() kernel symbols in order to dynamically allocate kernel memory where kernel functions can run
- hijack_syscalls() does the dirty work
- module initialization is complete
Fairly simple, right? Well, as you may have guessed, the real magic happens during the hijack_syscalls() function. This is defined in security.c, and it does the following;
- calls symbol_hijack() on each symbol we need to “hook into”
- if it fails on any of them, it may try another possible symbol to hook, as kernel developers can’t keep their function names straight
- the rest of the file defines the kernsym structures that hold symbol addresses, and the functions that wrap the given hooked system calls
Again, not all that complicated. Before I get into the complicated part, I’ll briefly describe what a typical function used with symbol_hijack() looks like;
- define the pointer to the function that we’re wrapping in the *run() variable; very important to initialize it equal to the run pointer of the appropriate kernsym struct!
- define the variable for our return value
- if we’re pre-hooking the function, do some code
- based on the previous code, decide if we’re going to call the function or not
- if we’re post-hooking, run additional code
So in the case of what this module is doing – implementing trusted path execution – check to make sure the appropriate file meets a set of conditions via the tpe_allow_file() function, located in core.c.
Now back to the symbol_hijack() function, defined in hijacks.c;
- arguments are the kernsym structure we’re defining for this symbol (see the top of security.c), the exact name of the function we’re hooking into (as a string), and the address of the function we’re calling in its place.
- it calls find_symbol_address() to locate the given function in kernel memory
- it allocates memory to store a copy of the function we’re hooking and stores it inside sym->new_addr
- it walks along the function and copies the code to our newly allocated memory, fixing any relative jumps via the insn code.
- write-protect on the kernel function is temporarily disabled, so we won’t run into a “general protection fault” or other error
- now that we have a working copy of the function we’re hooking, the first few bytes of the original function are replaced with a jump code to the address of the function we want to call in its place.
- write-protect on the kernel function is re-enabled
Voila – when that kernel function is called, it gets redirected to the function we defined to take its place. If we so desire, we call the original function by calling the copy we made of it, since it doesn’t start with a jump code.
You no doubtingly have questions about the insn code. Honestly, I don’t quite understand how it works, I didn’t write it. We can thank Eugene Shatokhin for his contribution to the code base by answering this question; Having trouble wrapping functions in the linux kernel. Hopefully the info there can answer any questions you have.
Finally, you may have some questions about find_symbol_address(). It’s defined in symbols.c;
- for older kernels, it does an ugly hack by fetching the symbol address out of /proc/kallsyms, or if it doesn’t exist, /boot/System.map
- otherwise, it uses kernel’s kallsyms_on_each_symbol() function to locate symbol addresses, and populate the given kernsym struct with data about it, such as its name, address in memory, and size.
When the module gets unloaded, the undo_hijack_syscalls() function gets called, which undoes what hijack_syscalls() did, restoring the original kernel address’s first bytes to what they were before the jump codes were written there.
I should note that a rootkit works very much the same way. In fact, you could use this code base to do just that. Also, Ksplice uses a similar method to do its runtime patching of the linux kernel, but I should also mention that Ksplice violates the GPL by not releasing their updated source code. Shame on Oracle! I’ll eventually settle the score by putting out my own fork of their ass-old codebase with some badly needed updates, unless of course they’re otherwise compelled to start complying with the GPL.
Well, that about does it. Hope I explained it well enough!