OS2:System Call Interception

From SOFTICE

Jump to: navigation, search

Contents



System Calls Interception

Pedagogical Objectives
  • Kernel Data Structures related to system calls
  • writting a LKM able to substitue our own code to a system call
  • accepting parameters at insmod time
  • creating & applying kernel patches
Development History Warning The update to the new kernel (2.6.24.3) running in the SOFTICE OS Labs virtual appliance is still underway. Some sections might not have been updated yet below.

Synopsis

In a nutshell, we want to write a Loadable Kernel Module (LKM) which is going to modify an entry in the system call table of the kernel. This will enable us to have a function of our own invoked instead of the kernel code implementing a given system call (e.g. fork()). We can use this technique to log information everytime the chosen system call is invoked before to let the kernel execute the original system call implementation. With a little more efforts, we can even use such a LKM to deny access to selected system calls to specific users on the system.

[Prelude] Patching a kernel (optional)

The previous lab offered an optional activity meant to get you to recompile the kernel used by the SOFTICE OS Labs virtual appliance. This time you will be able to apply some trivial changes to the kernel, save them in a patch and apply them to your new kernel. Please note that the kernel you are running had already this patch applied so this activity is optional. Should you be interested in working on it, make sure you do so on a copy of the virtual appliance you downloaded so to let you work on the other labs if you break your virtual appliance.

Intercepting system calls: what's changed?

The above-mentionned manipulation is a fairly classic exercise to get used to Linux kernel programming whether you are using LKMs, as in these laboratories, or whether you are modifying the kernel source code directly. Before we proceed with the writing of such a module we are going to discuss issues related to accessing, from within our LKM, the kernel data structure representing the system call table. We will use the outcomes of this discussion to provide us with a motivation to learn about kernel patching techniques and apply them.

Until the kernel series 2.4.x, the sys_call_table data structure was an exported kernel symbol. This meant that any LKM could reference this variable from within its code and, during the loading of the module, it would be appropriately linked. Since the kernel series 2.6.x, Linus Torvalds decided to no longer export this symbol. This decision can be easily justified; making it easy for LKMs to intercept system calls isn't beneficial to the overall kernel security. Indeed, many rootkits have been exploiting this technique to enable an intruder to maintain a presence (and a backdoor) on your system after leaving while making sure these new "features" would take great care to hide themselves from any curious eye. By making the sys_call_table symbol less accessible, the simplest system call interceptions methods are made unusable. Of course, new ones have arised since then but their level of complexity prevent most beginners to write their own root-kit in less than 10 minutes.

Since we are using a 2.6.x kernel series, we had to find a solution in order for you to be able to modify the sys_call_table from within your own LKMs. Two possibilities are available:

  • Solution #1: host kernel patching

The host kernel patching approach involves modifying the sources of the kernel we want to use, recompile it and then write a LKM destined to be loaded in this particular kernel only. While this looks like a way to implement a poorly portable LKM, it lets us reverse Linus' decision by hand in our own kernel and therefore conduct the manipulation without further burden. As a side benefit, this provides a minor level of security, as the kernel running on the host server will not have sys_call_table exported, and therefore will not accept a module that requires it; if a student were to exploit a security hole to insert a module of his or her choosing, this would reduce the likelihood of such an attack bearing fruit.

  • Solution #2: hot patching technique

Historically, this second approach has been introduced to be able to patch the linux kernel without having to restart it even when LKM had access to such symbol as sys_call_table. The objective was to circumvent security measures such as forbidding the loading of LKMs on a given system and / or using a hardened kernel version such as a 2.6.x series which doesn't let LKMs intercept system calls easily.

We decided to go with the former solution but you're more than encoutaged to refer to [phrack] for more details on how to implement the second one. The following will guide you step by step through the modification of the kernel sources, the recompilation of your own UML and a brief introduction to kernel patching techniques.

Exporting syscall table

First, refer to the previous lab and more specifically the "compiling your own kernel" section. Now that we know how to build a kernel of our own, we can start tinkering with its sources.

In order to export sys_call_table in kernel 2.6.24.3, we have to edit the kernel symbols file for our architecture. The SOFTICE OS Labs virtual appliance runs inside VMware which virtualizes intel hardware. Therefore In these labs, we will be always working on the x86 architecture.

The file we need to modify is in the folder /usr/src/linux/arch/x86/kernel/ and is named i386_ksyms_32.c. We will need to add the following lines to that file;

extern void* sys_call_table[];                                                       
EXPORT_SYMBOL(sys_call_table);

This modification will allow us to recompile a kernel which exports the symbol sys_call_table so that it may be visible from within our LKMs. We need to recompile our modified kernel and test it.

make vmlinuz
make install 

There are several ways to check whether or not the symbol was exported before to actually try to write a LKM using it:

  • Check the /usr/src/linux/System.map file in the kernel source tree
cd /usr/src/linux/
cat System.map |grep sys_call_table 
081fca94 D sys_call_table
  • At run time (after booting your new kernel)
cat /proc/kallsyms |grep sys_call_table

Patching kernels source trees

Now that we have a kernel source modification that we are proud of, the problem is to find a way to pass it onto other programmers? There are a number of naïve solutions:

  • We could archive the whole source tree. This represents an archive of approximatively 46mb compressed and without any executable or object file in the source code tree. When only a few hundred bytes are changed within the hundreds of megabytes of source code, it is unreasonable to distribute the entire archive.
  • Another possibility would be to describe our modification and email it to another programmer. This, however, introduces the aspect of error; unless we describe precisely what has been done, character for character, the programmer we send the information to may interpret our instructions differently than we intend. Also, after we modify it 4 times, we'll start regretting to have to update both the code and its documentation at every minor change.

There is a more elegant solution: the diff and patch tools.

diff records the differences---and only the differences---between an old set of files and a new set of files in a standard format. patch accepts this record (known as a "patch file") and applies the same changes to another source tree. The patch file is considered human readable, as it's written in plain text and is fairly easy to understand; this is convenient if, for example, the old file has been modified beyond the point where patch is able to recognize it. The size of a patch file is proportional to the size of the change in the original source files; even for very significant changes it rarely exceeds a few megabytes.

Let's take an example to illustrate how to patch source code trees. We will assume that a user lambda has two directories containing the same kernel source tree:

  • /home/lambda/Linux/linux-2.6.16.20.pristine
  • /home/lambda/Linux/linux-2.6.16.20

Obviously, the latter will be modified as we, for instance, export the sys_call_table symbol discussed above. Now we are going to generate a patch file containing all differences between the original kernel tree and the modified one as follows:

cd /home/lambda/Linux/ 
diff -Naur linux-2.6.16.20.pristine linux-2.6.16.20 > syscalltable.uml.patch 

We can then have a look at the format used by the patch file itself:

diff -Naur linux-2.6.16.20.pristine/arch/um/kernel/ksyms.c linux-2.6.16.20/arch/um/kernel/ksyms.c
--- linux-2.6.16.20.pristine/arch/um/kernel/ksyms.c       2006-06-05 13:18:23.000000000 -0400
+++ linux-2.6.16.20/arch/um/kernel/ksyms.c  2006-06-29 22:06:26.000000000 -0400
@@ -107,4 +107,7 @@
 extern void FASTCALL( __read_lock_failed(rwlock_t *rw));
 EXPORT_SYMBOL(__read_lock_failed);

+extern void* sys_call_table[];
+EXPORT_SYMBOL(sys_call_table);
+
 #endif

Even without knowing the exact syntax, you should be able to decypher this patch since it covers such a small modification.

If we email this patch file to another developer, (s)he will be able to apply it simply by copying the file inside of her source tree directory:

cp /from/whatever/syscalltable.uml.patch /home/john/code/
cd /home/john/code/linux-2.6.16.20.edited/
cat ../syscalltable.uml.patch | patch -p1

Notice the option -p1 when using the patch command. It instructs the command to remove the first part of every file path name used in the patch itself. This means that, when the patch was generated, we compared two subdirectories named respectively linux-2.6.16.20.pristine and linux-2.6.16.20. These subdirectories do not exist in /home/john/code/ and there are no valid reasons to force all developers to store their code in the same subdirectories so we ask patch to drop the directory level in the names of the files. Wherever the patch file will reference linux-2.6.16.20/arch/um/kernel/ksyms.c we will replace it by arch/um/kernel/ksyms.c which much more portable. Back to the example above, we cd into the /home/john/code/linux-2.6.16.20.edited/ directory and, from there, feed the patch command with the contents of our patch file and let it modify the file arch/um/kernel/ksyms.c as it, indeed, should.

[Briefing] Syscalls-related Kernel Data Structures

sys_call_table

The kernel uses an array as system calls table: sys_call_table. This is the data structure that we need to modify from within our modules in order to be able to intercept system calls at will. This table contains as many entries as there are system calls. Each system call is identified inside of the kernel by a constant (think #define) representing an integer number. These numbers are used as index in the sys_call_table to retrieve the address of the kernel function which implements a given system call.

When a user space program needs to call a given system call, the interrupt 0x80 is raised after loading the EAX register with the system call number. The interrupt handler, in kernel space, then uses the contents of EAX as an index in the sys_call_table array and call the function which address is stored there. Please note that modern processors offer more efficient ways to implement system calls than raising the 0x80 interupt but we will focus on this simple approach for the purpose of this explanation since it relies on the same kernel data structures than more advanced ones.

If we change the address contained in sys_call_table at the index corresponding to the fork system call number, we can replace it by the address of a function written inside of our LKM. The next time a system call to fork is processed, our function will be invoked instead of the default one. If we are careful about saving the address of the original system call function, we can even invoke it from within our own "interceptor code". This would allow us, for instance, to display a message everytime a fork system call is invoked, call the original code so that the kernel properly handles the system call, and finally return as if nothing happened.


To sum up, our LKM will have to:

  • Modify at load-time the sys_call_table so that a given system call is now implemented by a function of our own
  • Provide the above-mentionned function in the module itself so that it will be loaded as part of the kernel and therefore addressable by it
  • Modify again at unload time the sys_call_table to restore the original system call


Relevant source files

The following source files will be useful to you when dealing with system calls interception;

/include/linux/syscalls.h Signatures of system calls Original LXR
/include/asm-um/unistd.h Header for definition of system calls (includes the one below in the um architecture Original LXR
/include/asm-um/arch/unistd.h Definition of the __NR constants corresponding to system calls internal numbers Original LXR

The last link will also provide you with the signature of the sys_fork system call which we will use in the section below.

[Solved] Intercepting system calls w/ an LKM

Since we have a UML box running a patched kernel, we can write inside it a LKM that is going to intercept a system call by modifying the sys_call_table array.

Here is an idea of how we can do it;

// base modules inclusions 
#include <linux/init.h>
#include <linux/module.h>

// for getpid() 
#include <linux/unistd.h>
#include <asm/arch/unistd.h>


MODULE_LICENSE("GPL"); 


extern void* sys_call_table[];

The sys_call_table symbol is exported by the kernel but we still need to have our code indicate that we are referencing an array of void pointers defined somewhere else.

int (*original_fork)(struct pt_regs);
// we define original_fork as a pointer on function w/ the same prototype 
// as the fork system call. It is meant to store the address of the original 
// fork function in the kernel while we replace it w/ our own 

int edu_fork(struct pt_regs regs)
{
  pid_t pid;
  
  // loging the syscall
  printk(KERN_ALERT "[edu]   fork syscall intercepted from %d\n", current->pid); 

Why not using getpid()? Get used to it, we are in kernel code now, APIs that you used to use in user space are no longer available. current is a global kernel pointer refering to the currently executing process' struct task_struct (its process control block). We're using here the pid field of this structure which, as you can guess, contains the process' pid.

  // making the call to the original fork syscall
  pid = (*original_fork)(regs);
    
  return pid;
}

The above lines are there to maintain the functionality of the system. Try to remove them and you would have broken your system; no way for the shell to fork a child to execute a command to start off with! Our function acts as a wrapper around fork; we make the call to the original system call BUT we can add before and/or after it some code of our own (logging information for isntance).

Consider that this is practical but also slows down execution speed on one of the most critical execution paths of the kernel: system calls.

static int edu_init(void)
{
  printk( KERN_ALERT "[edu]   Module successfully loaded\n"); 
  printk( KERN_ALERT "[edu]   Intercepting fork() syscall... "); 

  original_fork = sys_call_table[__NR_fork];
  sys_call_table[__NR_fork] = edu_fork;  

This is where the theft takes place, note the use of the constant __NR_fork instead of hardcoding the syscall number.

  printk( KERN_ALERT "done/n"); 
  printk( KERN_ALERT "[edu]   Starting Loging system calls\n"); 
  return 0; 
}

static void edu_exit(void)
{
  sys_call_table[__NR_fork] = original_fork;
  printk(KERN_ALERT "[edu]   Stopping loging system calls\n"); 
}

Not much to add here once you understand the initialization of the module you're pretty much already ok with its shutdown function.

module_init(edu_init); 
module_exit(edu_exit); 

These macros are there to "register" the functiosn edu_init and edu_exit as the functions to be called respectively when the LKM is loaded and unloaded from the kernel.


[Exercises]

Exercise 01-1: Intercepting the open system call

Your first exercise will consist in writing a LKM to intercept a different system call than the one described in the previous sections. This new LKM will intercept the open system call and log the name of every file being opened to the system console as well as the uid of the user the process calling open belongs to.

  • You can find the signature for the open system call and the __NR constant serving as its index in the sys_call_table in the above-mentioned files (follow the LXR links).
  • If you want to be able to use a char* pointer passed as parameter to the system call, you will need to use the getname function (e.g. char* tmp = getname( param )). This will copy data from user to kernel space. Once you're done using this data, call putname on your pointer (e.g. putname(tmp)).
  • Check the documentation in the references section [LDD3] of this page to explain exactly what is going on with the getname and putname functions and why they are necessary.

Exercise 01-2: Passing parameters to LKMs through insmod

We're now going to extend the previously described fork-intercepting module so that it can deny the fork system call to a specific process which PID was passed to the module at insmod time. The new trick is, as you've guessed, to find a way to pass parameters to a module when you are loading it into the kernel.

The syntax to pass parameters at load-time using the insmod command is straightfoward:

insmod ./edu.ko pid=890

The LKM code will have to be modified to accept parameters:

module_param(pid, int, 0);
MODULE_PARM_DESC ( pid , "The PID to which we deny fork()" ) ;

module_param takes three arguments: the variable name (the variable must have already been declared), the type of that variable, and the permissions for the corresponding entry in SysFS (we will cover SysFS in a future lab; for now it should remain 0).

Once these macros are inserted, you still need to declare variables inside of your module that will hold the parameters values. insmod will be responsible for initializing them with the proper value. The declaration is straigthforward:

pid_t pid = 0 ;

Exercise 01-3: Let's not open "softice"

Extend your open system call "activity logging" LKM so that it will forbid opening of a given file name (use "softice") to any processes.

  • To forbid access, simply return an error code just as if the file didn't exist.
  • To know which value to return, you can lookup documentation on both the standard C library open function and the kernel open system call (look at the code by using LXR if you need to).
  • You will make sure that the filename is stored inside the LKM as a parameter accessible during insmod.
  • Remark; in the kernel, a strcmp function similar to the C standard library one, is available.

[Projects]

Project 1: The "Shackles" LKM

We want a generic version of the previous module which takes two parameters at load-time:

  • the PID of the process for which we want to restrict access to a specific system call
  • The internal number of the above-mentionned system call (__NR constant)

To make it easier to implement, we will ask that you allow only 4 different system call numbers. For each, you will have to implement in your LKM a replacement function which proceed with the original system call only if the calling process has a PID different than the one the LKM is banning. You can obtain the PID of the calling process by using current->pid. This will be explained in details in the next lab.

When the LKM is loaded, you will have to intercept the system call which number is given to you as parameter and substitute its replacement function. When the LKM exits, you will need to ensure that the system is back to its original state.

While you develop this code, make sure to rmmod the LKMs before to re-execute it or you might end up with several LKMs intercepting the same system call which will most likely lead to crashes.

Also, as for every exercise and project in these labs, you need to provide user-space programs which proove that you LKM implements the requested features.

References

[LDD3] Linux Device Drivers 3E (safary reference)

[LKMPG] Linux Kernel Module Programmer's Guide


[Phrack] Linux on-the-fly kernel patching without LKM


[LDC] General article on the changes between 2.4.x & 2.6.x


References (Textbooks)

[NUTT] Operating Systems, 3/e


[STALL] Operating Systems, Internals and Design Principles


[SILB] Operating System Concepts with Java

  • http://os-book.com/
  • Abraham Silberschatz, Peter Baer Galvin, Greg Gagne
  • Wiley, ISBN: 978-0-471-76907-1
  • Chapter(s): #3 (Processes)


[DEIT] Operating Systems

  • Deitel, ISBN: 0131828274
  • Chapter(s): #3 (Processes)