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:

  1. Kicking an ethernet card into kernel mode.
  2. Doing packet sniffing on an ethernet card entirely in kernel mode.
  3. Hiding information and programs in (previously) unused inodes.
  4. 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.

  1. It's not interesting.
  2. 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.
  3. 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:
  1. The module loading subsystem
  2. /dev/kmem
  3. /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:
  1. Pass (as an argument to 'seal.o') the md5sums of "allowed" kernel modules.
  2. Chain the module loading system calls like this: sys_call_table -> my_function -> real_sytem_call
  3. 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:

  1. Pass (as an argument to 'seal.o') the location (full filename) of the "right" userland module loading program.
  2. Chain the module loading system calls like this: sys_call_table -> my_function -> real_sytem_call
  3. Have "my_function" check that the calling process is the "right" one. success -> real_system_call while failure -> exit
And like this in userland:
  1. 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
    
  2. "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