Creating an Unkillable Process
By Silas Brown
Annoying as unkillable processes can be, there are some circumstances where you might legitimately want to create one. For example, if I run an audit tool, or if I want to write a program that makes sure I go to bed on time even when I'm really stuck into something, then I might not want even the root user to be able to stop it from running.
One approach would be to simply disallow any root access to the system, or at least disallow it at critical times, but that can get very complex if you still need to be able to administrate the system and/or cannot tell with certainty which times will not be critical. So I wanted an approach that did not rely on disallowing root access altogether.
As root can do anything (including writing batches of commands which when run will likely out-pace any periodic integrity checks), and even modifying all of root's tools does not preclude the installation of new ones, the only way to really make a process unkillable is to modify the kernel. But if possible I wanted a solution that didn't involve delving into the kernel, for reasons of portability and keeping it simple.
So I had to settle for the lesser goal of "create a process that, if killed, will start again immediately". That's not too difficult: edit /etc/inittab and get init(8) to restart the process whenever it dies. But the root user can change /etc/inittab, and can also change the executable file on disk, which could forestall the process from properly starting again after it's killed.
To prevent such changes, both the executable and /etc/inittab will have to go on a read-only filesystem. But it's not sufficient to take just any filesystem and mount it read-only, because root can simply remount it read/write. You can however make an ISO image and mount that as a loop device; this mount can't possibly be remounted read/write, and changing the underlying ISO file shouldn't affect the mounted filesystem. But you'd still have to stop root from unmounting it and mounting something else in its place (or unmounting/remounting it after changing the ISO file).
You can stop a filesystem from being unmounted by making sure that it's always busy, i.e. there are processes whose current directory is inside it. But that doesn't stop the use of "umount -l" (lazy unmount) which detaches the filesystem from the hierarchy and postpones the actual unmounting until it's no longer busy; root can do a lazy unmount and mount something else, and all new processes will see the new version.
Actually that's not quite true: If root does use "umount -l", then any currently-running processes whose current working directory is in the old mount can continue to see the files from the old mount, and so can their child processes, provided that they always refer to them from the current working directory and not via an absolute path. If they use an absolute path then they'll see the new mount.
So if we can get init(8) to run with the mounted ISO as its current working directory, and to execute our program from the current directory instead of from an absolute path, then it should not be possible to change the contents of that ISO as far as init(8) is concerned, at least not without rebooting or cracking the kernel.
This can be done by moving /sbin/init to /sbin/init.orig, and creating a new /sbin/init, a shell script:
#!/bin/bash mount /sbin/init.iso /init-mnt -o loop cd /init-mnt exec /sbin/init.orig $@
You will also need to ensure that future package upgrades do not overwrite your /sbin/init script with the original binary.
Then chmod +x that script, make the /init-mnt directory, and use mkisofs to make the /sbin/init.iso file containing any binaries you want to run. You can run scripts, but make sure the interpreter binaries are in the ISO and that /etc/inittab calls them from the current directory, for example:
(In the case of python you might also want to ensure that it's reading its standard libraries from the ISO rather than from anywhere else, otherwise there could be a back door that way.)
Although it should now be impossible for root to change your script without rebooting, it is still possible for root to change /etc/inittab and tell init to re-read it. On most systems, init is hard-coded to load /etc/inittab by absolute path, which means you can't get around this without patching init, either to make it load inittab from the current working directory or to prevent it from ever re-reading inittab during its run.
You could hex-edit the init binary and change the string, but the resulting system probably won't boot. It's better to download your distribution's source package for "sysvinit" (or whatever your distribution calls it), change into its "src" directory, edit paths.h and change "/etc/inittab" to "inittab", then type "make" and move the resulting init binary to where you want it. Remember to put an inittab file inside the ISO image: this is the inittab file that will be used (not /etc/inittab), and the only way to change it is to change the underlying ISO file and reboot.
There is still another problem, however. If your process is killed too often, init will refuse to restart it for a while. You could make it more aggressive whenever it restarts (e.g. terminate all root shells and disable the root account for a time to stop it from being killed again too soon), but if root launches a script that repeatedly scans the current processes and kills yours, and that script's loop is small and fast, then your process is not likely to be able to get as far as stopping it.
Perhaps the easiest way around this is to treat the "respawning too fast" condition more seriously. For example, search the init source for the part that prints the "respawning too fast" message (in version 2.86 it's in init.c) and add "exit(1);" after the statement's closing semicolon. This means, if any process respawns too fast (for example because root is running an aggressive script to stop your process from running), init will exit, which will result in a kernel panic and an unusable system. Note however that this also means the system will crash if any inittab task respawns too fast due to a typo, so be careful.
In this article we have put together a way of preventing even the root user from getting rid of a certain process without rebooting. However, there is still the issue of rebooting itself. You can't really stop root from changing /sbin/init or /sbin/init.iso and rebooting the system, especially if it's done quickly without a proper shutdown, so reboots had better be very noticeable. If you want to make things more difficult, though, you could get your program to frequently check the stat() of /sbin/init* for changes, taking care to do so from the main thread (remember that if your program goes multi-threaded then it may be possible to kill some of the threads while preserving others). It would still be possible to do things by booting from a rescue disk however, and perhaps even without booting from a rescue disk in some circumstances, so this is not completely flawless.
[ Pruning root privileges is a tricky business. As the article shows, this endeavor is tied to the filesystem layer. Projects such as Linux capabilities or Security-Enhanced Linux also touch upon filesystems and are worth a look. -- René ]
Silas Brown is a legally blind computer scientist based in Cambridge UK. He has been using heavily-customised versions of Debian Linux since 1999.