COMP4108 — Fall 2012

Computer Systems Security

As mentioned on the General Page you will be completing this assignment from within a virtual environment provided to you. For each question include both your answer as well as the process by which you determined this answer. I.e. the exact commands you ran, and the output those commands provided. Also include any code you wrote, or scripts you edited.

When SSHing into your VM for this assignment you will be warned that the host key for your VM has changed. This is because since the last time you've connected your VM has been replaced with a new one at the same port/ip. You are safe to remove the old hostkey and trust the new one. For Linux/OSX users, edit ~/.ssh/known_hosts. On Windows with Putty I believe you just have to accept the new key when prompted.

For this assignment you may work in pairs.
PLEASE BOTH SUBMIT YOUR GROUP'S CODE AS WELL AS A PDF REPORT.

You may use one, or both of the VMs assigned to you. Please have both members of your team submit the assignment, clearly indicating who your partner was.

Hint:
Familiarize yourself with the history command. You can use it to refresh your memory on commands you may have entered.
You may prefer to use the script command, which records input and output to a file automatically. See man script for more information.

At the end of Assignment 1 you saw how you could exploit a setuid binary with a race condition in order to gain root privilege on the box. Now what? If you were a real attacker it could be a matter of minutes before the system admin patches the vulnerable program and gives you the boot. How do you hide your traces? How do you install a backdoor that ensures you aren't a one-trick pony?

Linux Kernel Modules:

The answer to both questions (at least as far as this assignment is concerned) is a rootkit. A garden variety Linux rootkit is generally written as a Loadable Kernel Module or LKM. LKM's allow the system administrator to load new code to extend the kernel while the machine is running. Many device drivers are implemented as LKMs.

Only root can insert and remove modules (using insmod and rmmod respectfully), but it just so happens you're root today. Lucky you. There is a free guide to programming Linux Kernel Modules available online. This guide explains benign kernel module functionality, and you'll likely want to read (or at least skim) Sections 1, 2, 3, and 8.


Hint:
This assignment assumes familiarity with the C programming language. If your C kung-fu is rusty you'll probably want to Google some reference material. The classic choice is the K&R book. The Carleton library has at least one copy. I also enjoy Learn C the Hard Way by Zed Shaw, available free online.

You'll want to make sure you understand pointers, memory management, arrays, and structures. You're a kernel programmer now.

Rootkits:

Rootkit LKM's alter the state of the system to present processes interacting with the Kernel sanitized information, or to add new functionality convenient for an attacker. A classic way this is done is by hooking system calls. For an idea of what system calls a processes invokes, you should revisit your use of the strace command in Assignment #1. The guide to programming Linux Kernel Modules introduces syscall hooking briefly.

To hide files from appearing in directory listings for instance, you would find the syscall that ls used to get filesystem directory entries and hook it. By hooking syscalls related to the filesystem a malicious rootkit might hide all files with a $sys$ prefix, allowing it to stash its own files from the system. Rootkits also frequently serve as backdoors that allow a user to elevate their priviledges, or get a remote shell without logging in.

Back to the Future:

You no doubt saw the grave warnings affixed to both the getdents man page and the LKM guide section on hooking syscalls. Not only are these warnings correct, things are worse than you might imagine. These techniques are dangerous, and often unreliable. No sane engineer would design their device driver in this fashion, we're hacking in the true definition.

The details in the LKM guide are unfortunately specific to Linux Kernel versions < 2.6.x and a 32bit architecture. We're living in 2012 and running Linux Kernel version 3.2.x on a 64bit architecture. This affects us in two major ways:

  1. The sys_call_table symbol is no longer exported by the kernel to LKMs. This is to prevent developers from doing stupid things with it. We're going to have to find the address manually so that we can do stupid things with it.
  2. The page of memory where the sys_call_table lives is now marked read only to prevent things from going wrong. We can't write a new hook into the table without first making the page writable, thereby allowing things to go wrong.

Writing a rootkit from scratch is going to be a grueling endeavour. Thankfully your connections in the underground have hooked you up with some super eleet warez. With their C code you should be able to write a respectable piece of kernel malware without losing your mind. Unfortunately your hookup only got you so far. The code's author must have uploaded it before it was completely finished. It looks like you'll have to pick up where they left off...

Important Notes:

You're writing code that runs in kernel space, with full privileges. The slightest mistake in your code is going to lead to legitimately weird things happening including (but not limited to):

    All of the binaries on your system segfaulting. Including ssh.

    Data being lost.

    Kernel modules being stuck loaded.

    Full kernel panics, leaving the box frozen

Don't keep anything on your VM you aren't ready to lose! Keep your code on your own machine and copy it over to compile/test.

You're going to want to work in very small, verifiable steps. Do not attempt to sit down and program the whole assignment. Instead, start with very small steps in mind and progress further only when you get that step working.

For example, in the file hiding task: start by figuring out what to hook, then try hooking it and keeping the original behavior in-tact. Once you can do that without crashing your VM, try printing all filenames in a directory to syslog from your hook. Once that's working start writing code to identifying files you want to hide from those being printed to syslog. Finally attempt to remove the entry from the results.


Oblig. XKCD

Reboot Request Tool

In order to deal with the realities of writing kernel code I've provided you a means to reboot your own VM. If your box becomes unresponsive, or any of the above listed things start happening you should use the reboot tool, examine /var/log/syslog and debug your rootkit code. Reboot your own VM as often as you require, remember to give it some time to boot up before you try to reconnect via SSH.

The reboot request tool will ask for your VM username/password and reboot the node that you are assigned. It will not ask for confirmation after you enter your username and password!

Hint:
When in doubt, read the source code! The Linux Kernel is open source. I've placed the sourcecode for the version of the kernel your vm runs in: /usr/src/linux-3.2.0. Another great online resource is the Linux Cross Reference.
  1. Download the rootkit framework code from the Introduction to your VM using wget or by copy pasting it into an editor.
  2. Run sudo bash to give yourself a bash shell with root privileges. We'll pretend that you got this from the race condition in A1. For most of this assignment you're going to be switching between a root user and a normal user, so I recommend you keep two windows open (the guru's might want to try my favourite terminal multiplexer, but it has a somewhat steep learning curve).
  3. 1 Mark Find the address of the sys_call_tablesymbol using the System.map
  4. 0.5 MarksEdit the insert.sh script to provide the right memory address for the table_addr parameter in the insmod command. It should be equal to the address you found in the System.map.
  5. 0.5 MarksConfirm you can build the rootkit framework by running make. You can safely ignore the warning about defined but not used variables, as you will be fixing that as you complete the assignment.
  6. 0.5 MarksConfirm you can insert the rootkit module by running ./insert.sh as root.
  7. 0.5 MarksConfirm you can remove the rootkit module by running ./eject.sh as root.
  8. 2 Marks Fix the rootkit code so that the example open() hook works. Show a snippet of the syslog output it generates once loaded.
Hint:
There is a bit of logging in the incomplete framework. Run tail /var/log/syslog to display the last few lines of the syslog. You may also want to try tail -f /var/log/syslog to interactively tail the syslog file. In interactive mode as new lines are printed to the log your terminal will update immediately. Press ctrl-c (that is ctrl and then c) to end the tail command and get back to the shell.
For this Part of the assignment you will be creating a backdoor for gaining root privileges on the machine. Using this backdoor you can come back to the system at anytime and quietly become the root user. From a kernel module most anything inside the kernel is fair game to be edited and messed with. In general you just have to find it, understand it, and subvert it reliably. For your backdoor you'll be subverting the system call that is used to invoke executables like commands, daemons, and scripts.

  1. 5 Marks Write a new hook for the execve syscall using the framework code from Part A. Consult the execve man page to learn the details and function signature of execve(). You will need to know which __NR_X define is used to find the offset in sys_call_table to hook for execve (where X will vary syscall to syscall). You might find /usr/src/linux-3.2.0/include/asm-generic/unistd.h useful in this regard.

    The hook should print the name of all files being executed, and the effective UID of the user executing the file to syslog using printk. Example output:

    Oct  1 20:49:17 COMP4108-A2 kernel: [81423.749198] Executing /usr/bin/tail
    Oct  1 20:49:17 COMP4108-A2 kernel: [81423.749200] Effective UID 0
    Oct  1 20:49:19 COMP4108-A2 kernel: [81425.950497] Executing /bin/ls
    Oct  1 20:49:19 COMP4108-A2 kernel: [81425.950499] Effective UID 1000
    

    The current_* macros defined in the /usr/src/linux-3.2.0/include/linux/cred.h include will help you get the information you need to include in your printk message.
  2. 10 Marks Modify your hook code so that when the effective UID of the user executing an executable is equal to the value of the root_uid parameter, they are given uid/euid 0 (i.e. root privs). The root_uid parameter must be via the insmod command in insert.sh like the sys_call_table address, and not hard coded. Note that the root_uid parameter should be set to the UID of the user you want to get root, not root itself. You will need to add this behavior.

    Hint:
    The header file /usr/src/linux-3.2.0/include/linux/cred.h and the corresponding code in /usr/src/linux-3.2.0/kernel/cred.c are likely of interest. Specifically, the prepare_kernel_cred(), and commit_creds() functions.

    In order to get full mark you must demonstrate the module working. Set the root_uid param in insert.sh equal to your user's UID, and provide the input/output from:
    1. Building the module code
    2. Runing whoami as a normal user in one terminal
    3. Inserting the module as a root user by running ./insert.sh in a second terminal.
    4. In your normal user terminal running whoami again and being told you are root.
    Example output (from normal user term):

    comp4108@NodeX:/A2/code/rootkit_framework$ whoami
    comp4108
    comp4108@NodeX:/A2/code/rootkit_framework$ whoami
    root
    

With your handy new backdoor from Part B you could come back to the system at anytime and act as the root user without needing to exploit your treasured race condition privilege escalation. From a kernel module most anything inside the kernel is fair game to be edited and messed with. In general you just have to find it, understand it, and modify it for your own purposes, without causing the system to crash when your modified code is executed in place of the original. In this part you will be subverting the interaction between binaries like ls and the OS provided directory abstraction.

  1. 10 Marks Write a hook for the getdents system call (man page here). Once again this will require finding the __NR_* define for the syscall number.

    You will want to familiarize yourself with the struct linux_dirent64 definition available in /usr/src/linux-3.2.0/include/linux/dirent.h.
    Your hook code should print the name of all directory entries returned by a call to getdents() to syslog using printk. Example output:

    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441674] getdents() hook invoked.
    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441704] entry: rootkit.o
    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441706] entry: .rootkit.mod.o.cmd
    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441708] entry: ..
    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441710] entry: insert.sh
    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441711] entry: rootkit.c
    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441712] entry: rootkit.mod.c
    Oct  1 11:44:36 COMP4108-A2 kernel: [ 2266.441714] entry: rootkit.ko
    <snipped>
    
    Hint:
    Make sure your getdents() hook is coded with x86_64 architecture in mind. Most notably, you should be accepting a buffer with type struct linux_dirent64 * not, struct linux_dirent* as the 2nd argument to your hook.
  2. 15 Marks Modify your hook such that the struct linux_dirent64* buffer you return to the calling process does not include any dirent's for filenames that start with magic_prefix. The magic_prefix character array should be provided as a kernel module parameter given to insmod in the insert.sh script. You will need to implement this parameter yourself.

    After coding your getdents hook and implementing the magic_prefix parameter you'll want to test it in action:
    1. Edit the insert.sh script and set the magic_prefix parameter to $sys$
    2. Compile your module by running make
    3. Create a file called $sys$_lol_hidden.txt in your current directory.
    4. Perform a ls -l to see if your $sys$_lol_hidden.txt file was created.
    5. Insert the kernel module by running the insert script ./insert.sh as root.
    6. Run the same ls -l command to validate the $sys$_lol_hidden.txt file is no longer included. It shouldn't be in ls -la either (i.e. isn't just a regular 'hidden' dotfile).
    Hint:
    If you use $sys$ as your magic_prefix value you must remember to escape the $'s in the bash shell. The easiest way is to use \$ instead of $ when trying to create, edit, delete, or otherwise interact with one of your hidden files.
    Example output (from normal user term):

    comp4108@COMP4108-A2:/A2/code/rootkit_framework/test$ touch \$sys\$_lol_hidden.txt
    comp4108@COMP4108-A2:/A2/code/rootkit_framework/test$ ls -la
    total 8
    -rw-rw-r-- 1 comp4108 comp4108 0 Oct  1 11:59 bar.txt
    -rw-rw-r-- 1 comp4108 comp4108 0 Oct  1 11:59 baz.txt
    -rw-rw-r-- 1 comp4108 comp4108 0 Oct  1 11:59 foo.txt
    -rw-rw-r-- 1 comp4108 comp4108 0 Oct  1 12:00 $sys$_lol_hidden.txt
    comp4108@COMP4108-A2:/A2/code/rootkit_framework/test$ ls -la
    total 8
    drwxrwxr-x 2 comp4108 comp4108 4096 Oct  1 12:00 .
    drwxrwxr-x 5 comp4108 comp4108 4096 Oct  1 11:59 ..
    -rw-rw-r-- 1 comp4108 comp4108    0 Oct  1 11:59 bar.txt
    -rw-rw-r-- 1 comp4108 comp4108    0 Oct  1 11:59 baz.txt
    -rw-rw-r-- 1 comp4108 comp4108    0 Oct  1 11:59 foo.txt
    

    Hint:
    Modifying the buffer of dirent64's to hide files is the trickiest bit of the assignment. Luckily it is no more difficult than a typical data structure question (with a few twists).

    Your objective is to sanitize the struct linux_dirent64 *dirp buffer provided as the 2nd argument to the getdents syscall. This buffer is allocated by the calling process (i.e they make sure there is enough memory malloc'd for the struct linux_dirent64s that the syscall puts into the buffer.)

    The most important thing to know is that the dirp buffer is not an array of struct linux_dirent64's of equal size. To save memory each dirent64 struct is only as big as it needs to be. In order to allow iterating through the dirent64 structs in the buffer each dirent64 struct stores its length to be used as an offset to the next dirent64 in the buffer (see the figure). You will need to use this knowledge to determine how you can remove a dirent64 from the buffer. The man page for getdents() has example code for iterating the buffer.

    The second most important thing to know is that the dirp buffer is userland memory You can not edit it directly or bad things will happen. Instead you must first allocate a kernel memory buffer of equivalent size. To do this you must use kalloc and kfree not their user-land counterparts malloc and free. Once you have a kernel buffer of the right size you can use the copy_from_user and copy_to_user functions to copy the userland buffer into your kernel buffer and vice versa.

    So, the steps are:
    1. Call the original getdents() syscall with the dirp buffer your hook receives to have it populated with dirent64 structs.
    2. Allocate a kernel buffer of the correct size using kmalloc
    3. Copy the userland buffer into your kernel buffer using copy_from_user
    4. Perform your edits on the kernel buffer to remove any dirent64 structs you don't want seen.
    5. Copy your edited kernel buffer into the userland buffer with copy_to_user
    6. Call kfree() on the kernel buffer to free it and avoid a leak.