The code v1.1
(support for kernel 2.0.38).
Sealing the kernel
The goal of this project is to create a linux module which will seal
the kernel so that arbitrary kernel mode access is not given to root.
Why is this useful? The
Internet Auditing project details a slick attack on one of their
machines where the attacker used a stealth kernel module. A kernel
module is turing complete - it can do anything that the kernel itself
can do. These arbitrary possibilities include:
- Kicking an ethernet card into kernel mode.
- Doing packet sniffing on an ethernet card entirely in kernel mode.
- Hiding information and programs in (previously) unused inodes.
- Overwriting the kernel on disk so that a new "improved" kernel boots the next time.
None of these tricks are terribly new. What is scary about a kernel
module doing them is that they often "fall under the radar" of
existing anti-intrusion detection systems such as tripwire
and antisniff.
Therefore, our goal is to deny a 'stealthy' malevolent root. An
attacker with root access to a machine will be able to bring it down,
use it for packet sniffing, or whatever but s/he will not be able to
do this in a way which is indetectable by standard intrusion detection
tools.
The obvious approach to this is to disable module loading when the
kernel is built. We did not choose this method for three reasons.
- It's not interesting.
- It's inconvenient for the end user who must recompile a new kernel
for every machine he wants to protect. Inconvenient security is
unused security.
- It doesn't work. There are subsystems (pcmcia for example and
maybe sound) which inherently require modules. There are also systems
which practically require modules - such as those which use
binary-only modules.
Instead, we followed a suggestion from phrack-55
and concentrated our efforts on building a module which would be
loaded into a live kernel and seal it from further (arbitrary) kernel
mode access. The known methods of kernel mode access we accounted for
are:
- The module loading subsystem
- /dev/kmem
- /dev/mem
Sealing these off was quite simple. We constructed a module 'seal.o'
which does this nicely. Typical usage would be:
#Seal the kernel
/sbin/insmod seal
#Seal the kernel and deny read access to /dev/mem and /dev/kmem
/sbin/insmod seal read=no
More complicated was making small holes in the seal. This was
necessary for two reasons: X and system dependencies on modules.
Allowing selective module loading
Working linux kernels can unload and load kernel modules on a fairly
regular basis and it would be nice to avoid compromising that ability.
The following approach is conceptually natural:
- Pass (as an argument to 'seal.o') the md5sums of "allowed" kernel
modules.
- Chain the module loading system calls like this:
sys_call_table -> my_function -> real_sytem_call
- Have "my_function" check that the md5sum of the loading module
matches an "allowed" md5sum. success -> real_system_call while failure -> exit
The natural approach does not work easily and the reason has to do
with the fact that modules are relocated in user-space not kernel
space. Consequently, the md5sum will vary with the base address given
by the kernel to the userland program, "insmod", which does the
relocation.
An alternative suggested by Peter
Benie is to force the authentication into userland then (just)
guarantee that the "right" code is running in userland. Making this
guarantee is tricky and I'm not yet sure we have all issues settled.
Our system relies on lids or
critical files being on read only media. The goal of lids is to make
files unmutable by anyone which is necessary for userland secure
userland authentication. In fact, 'seal.o' and 'lids' are nicely
complementary because 'lids' can not make such a guarantee without
'seal.o' and so some variant of 'seal.o' will surely be incorporated
into 'lids'.
The system we constructed works like this in the kernel:
- Pass (as an argument to 'seal.o') the location (full filename) of
the "right" userland module loading program.
- Chain the module loading system calls like this:
sys_call_table -> my_function -> real_sytem_call
- Have "my_function" check that the calling process is the "right"
one. success -> real_system_call while failure -> exit
And like this in userland:
- There is a (protected by 'lids') file /lib/modules/md5sums in the
format:
8cd1c9e6b315e31557a5d18d7ef5d25a /lib/modules/2.2.12-20/block/DAC960.o
5f97cf8c42c1fcf306b203a938db3fea /lib/modules/2.2.12-20/block/ide-floppy.o
84f618ecf15452339ffc0048f79e5409 /lib/modules/2.2.12-20/block/ide-tape.o
7f130f55bf5065f761060d9ca490a6d7 /lib/modules/2.2.12-20/block/linear.o
- "insmod" is instrumented so that it reads /lib/modules/md5sums and
checks to see that the loading module has a good md5sum. The second
entry in each line is actually ignored and so a "good" md5sum is one
which matches any entry in the /lib/modules/md5sums. This might sound
atrocious security wise but it should be acceptably secure. Even if
there are 2^20 modules a brute force search would have to look at
2^108(ish) different modules to find a random match. That's a
reasonably large number.
success -> continue loading
failure -> exit
Another approach which might be even easier is to use 'lids' for
everything here. Instead of just applying 'lids' to
/lib/modules/md5sums and insmod, apply it to every kernel module on
disk. Then, messing with md5sums is unnecessary by insmod - all that
is required is checking that the module is "protected" by lids.
Example usage:
#load seal and allow further module loading by the /sbin/insmod program
/sbin/insmod seal.o insmod=/sbin/insmod
Allowing X to run
Because of performance issues (and inertia...), the X server on
typical systems is setup to use raw memory access to communicate with
the video card. In order to do this, it needs to open /dev/mem with
write permission. It's somewhat scary to ponder the possibility that
such a large and vital binary has total access to any location in
memory. Nonetheless, some people might want to try to run X with a
sealed kernel. We allow this with the same technique as was used for
kernel module loading. The seal module is passed a filename on
loading. Future write access to /dev/mem will be denied except by
programs which are loaded from the filename.
Example usage:
#load seal and allow X write access to /dev/mem
/sbin/insmod seal.o mem_allow=/usr/X11R6/bin/XF86_SVGA
One more random argument
We figured that some people might want to deny unloading of modules as
well as denying loading although the justifications for this are
somewhat murkier. It was an easy alteration and is done like this:
#load seal and disallow unloading of modules
/sbin/insmod seal.o no_remove=true
Conclusion
The seal system works in a reasonable easy manner without much
alteration of the underlying system. Use of 'seal' without any of the
special holes above should be secure. Use of 'seal' with the holes is
somewhat less secure because of it's reliance on the 'lids' code which
involves considerably more kernel and userland alterations to provide
userland security. The seal code is available
under GPL. Pass any bug reports to one of us.
jcl@cmu.edu and dmarg+@cs.cmu.edu