Professional Documents
Culture Documents
Industrial-Strength Linux Lockdown, Part 1
Industrial-Strength Linux Lockdown, Part 1
Industrial-Strength Linux Lockdown, Part 1
23 May 2007
For technical and non-technical users alike, maintaining a large installed base of
Linux machines can be a harrowing experience for an administrator. Technical users
take advantage of Linux®'s extreme configurability to change everything to their
liking, while non-technical users running amok within their own file systems. This
tutorial is the first in a two-part series that shows you how and why to lock those
machines down to streamline the associated support and administration processes.
In this tutorial, you learn how to remove the interpreters from the installation base
system.
over the edge of that precipice. Every machine may have been radically tweaked
with additional applications, configurations, and installations of unknown software.
This series of tutorials is for anyone who has ever wanted to have the ability to
painlessly manage and install such a large-scale Linux installation across an
enterprise.
Objectives
In this tutorial, you learn about some of the security issues that you must consider
when supporting a large-scale Linux installation and how to head them off before
they cost additional effort to put right. You will see how to set up the hardware and
firmware to prevent basic tampering in the first instance; ultimately, you will take
away the standard Linux interpreters to minimize the risk of users running unaudited
code in your secure environment. By the time you've finished the series, you'll be
able to configure a industrial-grade, locked-down Linux distribution that cannot be
injected with applications that you have not personally audited and signed off.
Prerequisites
This tutorial is written for Linux administrators whose skills and experience are at an
intermediate to advanced level. You should have good familiarity with the Linux boot
process, be comfortable with a command-line shell, and possess a working
knowledge of the C programming language.
System requirements
To follow the steps in this tutorial, you must have root access on a Linux machine
with the ability to reboot the box at will and to destroy all the data stored on it. You
must have an installed compilation environment and a way to get your distribution's
Linux kernel sources and headers as well as special tutorial source code (available
from the Download section) and the freely available dash utility for your flavor of
Linux. (You can use the Debian Linux version of dash , the Ubuntu Linux version of
dash , or whatever version is appropriate to your Linux installation.)
During the development of this tutorial, I used Ubuntu Linux V6.10 installed from a
live installation CD, although except in the finer details, any Linux distribution you're
comfortable with should be fine. If you have access to a copy of VMware and don't
need to try the hardware and firmware sections of the tutorial, VMware's snapshot
utility allows you to experiment more freely, because you can go back in time to a
known good state if the Linux installation stops booting at any stage without
resorting to a rescue disk to diagnose and repair the problem.
or worse, airline flight-control software, fall into this category. Having Linux reject
execution of unsigned or badly signed binaries is an excellent and effective means
of preventing enterprising testers from implementing a hotfix and recompiling the
offending part of the system, then forgetting to pass that fix back through the correct
channels to be integrated into the next build for thorough testing.
Most likely, even this level of lockdown represents complete overkill for your
organization. Yet, it can still offer surprising advantages even where the security and
audit trail itself is not your primary concern. By implementing this type of low-level
machine lockdown, you can avoid some typical workplace problems simply by
removing the enabling applications. For example, if a user's professional role within
the organization doesn't require access to intranet documents, remove all the Web
browsing software from that person's machine; to prevent users from introducing
illegally downloaded music into the network, remove MP3-playing software from all
machines.
You will even find a use for some of the techniques described in this tutorial series if
you're a busy systems administrator responsible for supporting the Linux
installations of a team of non-technical users. By performing a system lockdown,
your users won't be able to introduce broken or version-mismatched software to their
desktops, nor will their machines be capable of executing unauthorized software.
If you can see yourself in these scenarios, you'll find much of interest here.
even a floppy disk. Usually, after you've used the optical or floppy disk drive to install
the operating system, there's really no business reason for users to use these drives
on a day-to-day basis. When buying new machines for a secure network, it's an
unnecessary security risk to have them built into the machine at all. Not only can
they be used to bring unauthorized files into the network, but they're the easiest
means of transporting data off the network.
One of the many advantages of Linux, however, is getting away from the relentless
operating system-driven hardware-upgrade cycle, so chances are that most of the
machines will have these drives installed nonetheless. Physically removing the
drives isn't necessarily the easiest option; instead, you can remove the appropriate
drivers from the installed kernels to render the drives unusable after booting.
$ cd /usr/src
$ tar jxf linux-source-2.6.17.tar.bz2
$ cd linux-source-2.6.17
$ uname -r
2.6.17-11-generic
$ ls /boot/config*
/boot/config-2.6.17-10-generic
/boot/config-2.6.17-11-generic
$ sudo cp /boot/config-2.6.17-11-generic .config
First, however, remove all the CD and floppy disk drivers, which you can find by
using the kernel menuconfig utility. With the Ubuntu V6.10 kernel, that amounts to
making the changes in Listing 2 to your new .config file.
CONFIG_BLK_DEV_IDECD=n
CONFIG_BLK_DEV_IDETAPE=n
CONFIG_BLK_DEV_IDEFLOPPY=n
# Remove support for CD Filesystems
CONFIG_ISO9660_FS=n
CONFIG_UDF_FS=n
# Remove USB and Memory card device drivers
CONFIG_USB=n
CONFIG_MMC=n
Note that this isn't a complete configuration. It merely shows some of the device
drivers you can remove from the kernel you're about to build. You should run make
menuconfig from the source directory and use that interface to turn off these things
so that all the options that depend on the things you've removed are also disabled
correctly. There is much more that you can do at this stage to improve the speed
and security of your kernel, especially if you're familiar enough with the hardware on
which your kernel must run and can remove all the drivers you know you'll never
need.
If you're already familiar with kernel configuration and you can confidently remove
the majority of the modular drivers that you know you won't need, you could even try
to build a kernel that has no support for loadable modules. A kernel built in this
fashion won't be able to load kernel modules, preventing the insertion of foreign
code into the running kernel without recompiling again, something I touch upon
again in Part 2 when you configure the kernel to run only correctly signed binaries.
When you're happy with your configuration, follow the instructions in the kernel
README file to build and install it. Having successfully booted the machine with the
new kernel and satisfied yourself that it works well, you'll need to come back later to
remove the old kernel (and modules) as well as their entries in the boot-loader
before making a disk image to install on your other machines. For now, at least while
you're playing with the system configuration, it would be wise to leave the old kernel
around in case you need to use it for temporary access to any of the drivers you've
removed.
The only way to forestall this eventuality is to go into the basic input/output system
(BIOS) setup and ensure that the machine is configured to boot only from the correct
hard disk drive and to turn off USB, floppy disk drive, and CD-ROM booting entirely.
You must then set the BIOS password to stop anyone from changing these settings
back temporarily.
At this point, the only way to get this machine to boot from a different kernel or to
access data on an external drive that you haven't configured into your new kernel
build (except for the copy of the old kernel you've temporarily left behind for
emergencies) is with a screwdriver! If you also need to hold burgeoning hardware
engineers from installing an alternative bootable hard disk from which to transfer
data, there are various lockable cases and attachments that you can buy when you
purchase the hardware or as after-market add-ons.
On the base installation machine you're configuring, remove all the interpreters from
the machine with the distribution package manager, then try to run each application
on the collated list to determine whether any of them are written with a scripting
engine. If you're very lucky, all the applications will run unaided. More likely, you
must apply some of the techniques described in the rest of this section to get the
interpreted programs to run stand-alone. On Ubuntu Linux, I was able to safely
remove the following interpreters and still boot into a GNOME desktop from
power-up:
• Bash
• Gawk
• Guile
• M4
• Mono
• Perl
• Python
• Ruby
• Tcl
Before removing any of the interpreters above, especially /bin/sh, move the
interpreter to one side and replace it with a simple wrapper program that first logs
each execution to a file before handing control back to the moved interpreter. Listing
3 presents a simple program to do just that.
1 #include <pwd.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <sys/stat.h>
5 #include <sys/types.h>
6 #include <unistd.h>
7
8 #define LOG_FILE "/tmp/wrapper.log"
9 #define LOG_FLAGS O_CREAT|O_APPEND|O_WRONLY
10 #define LOG_MODE
S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
11 #define WRAPPED_PROG "/bin/sh-"
12
13 int
14 main (int argc, char **const argv)
15 {
16 FILE * logfile = fopen (LOG_FILE,
"a");
17 const char * program = argv[0];
18
19 argv[0] = WRAPPED_PROG;
20
21 if (logfile)
22 {
23 struct passwd *pw = getpwuid (geteuid
());
24 int i = 0;
25
26 fprintf (logfile, "(%s)", pw->pw_name);
27 for (i = 0; i < argc; ++i)
28 fprintf (logfile, " '%s'", argv[i]);
29 fprintf (logfile, "\n");
30
31 fclose (logfile);
32
33 /* First writer must set global permissions */
34 chmod (LOG_FILE, LOG_MODE);
35 }
36
37 execv (argv[0], argv);
38
39 /* Not reached. */
40 exit (EXIT_FAILURE);
41 }
$ gcc -o sh wrapper.c
$ sudo mv /bin/sh /bin/sh-
$ sudo cp sh /bin/sh
$ sudo /bin/sh -c 'echo Hello'
Hello
$ /bin/sh
# exit
$ cat /tmp/wrapper.log
(root) '/bin/sh' '-c' 'echo Hello'
(gary) '/bin/sh'
Here in Listing 4, being careful first to move the original /bin/sh to the exact location
named in line 11 of Listing 3, you can see how to install the wrapper program to the
shell's original location and test that it is working correctly. At this point, you can
reboot the machine to log all the calls the shutdown and reboot processes make to
this shell. After you are successfully logged back in to the rebooted machine, you
should move the original shell back over the wrapper program to prevent any further
spurious log entries.
Notice that although the login shell field has been set to /bin/false, the home
directory must remain for storing the user's application configuration and cache files.
Otherwise, the desktop won't be able to start up after Linux has finished booting.
In cases like this, rewriting the script in a compiled language is often quite
straightforward. Listing 6 shows the original script, and Listing 7 shows a rewrite in C
that you can compiled as a drop-in replacement that doesn't use /bin/sh.
1 #!/bin/sh
2 set -e
3 test -f /bin/setupcon || exit 0
4 . /lib/lsb/init-functions
5
6 case "$1" in
7 stop)
8 # keyboard-setup isn't a daemon
9 ;;
10 start|force-reload|restart|reload)
11 if [ -z "$DISPLAY" ]; then
12 log_begin_msg "Setting preliminary keymap..."
13 if setupcon -k --force; then
14 log_end_msg 0
15 else
16 log_end_msg $?
17 fi
18 fi
19 ;;
20 *)
21 echo 'Usage: /etc/init.d/keyboard-setup
{start|reload|restart|force-reload|stop}'
22 exit 1
23 ;;
24 esac
1 #include <stdlib.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/wait.h>
5 #include <unistd.h>
6
7 #define streq(a, b) (strcmp ((a), (b)) == 0)
8
9 int
10 main (int argc, char **argv)
11 {
12 pid_t pid;
13 int status;
14
15 if (argc > 1 && streq (argv[1], "stop")) {
16 return EXIT_SUCCESS;
17 } else if (argc > 1 &&
18 (streq (argv[1], "start") || streq
(argv[1], "force-reload") ||
19 streq (argv[1], "restart") || streq
(argv[1], "reload"))) {
20 const char *display = getenv("DISPLAY");
21
22 if (!display || streq (display, "")) {
23 write (STDOUT_FILENO, "Setting preliminary
keymap...", 29);
24
25 pid = fork();
26 if (pid == 0) {
27 execl ("/bin/setupcon", "setupcon",
"-k", "--force", NULL);
28 exit (EXIT_FAILURE);
29 } else {
30 if ((waitpid (pid, &status, 0) != pid)
||
31 (WIFEXITED(status) &&
WEXITSTATUS(status) != 0))
32 write (STDOUT_FILENO, "failed\n",
7);
33 else
34 write (STDOUT_FILENO, "ok\n", 3);
35 }
36 }
37
38 return EXIT_SUCCESS;
39 }
40
41 write (STDOUT_FILENO,
42 "Usage: /etc/init.d/keyboard-setup
{start|reload|restart|force-reload|stop}\n",
43 74);
44 return EXIT_FAILURE;
45 }
There is nothing tricky in the Listing 7 code, save that there's no analog to lines 2, 3,
and 4 of the shell script (shown in Listing 6), because those lines aren't necessary in
the C code. For simplicity in this tutorial, I've written the whole program as a single
main() function; in practice, there are dozens of scripts you can rewrite in this
fashion, and it would make sense to put common code (such as the fork/exec
code) into functions you can reuse for each rewritten script.
Listing 8 shows a small change to the option processing code so that no matter
which script name you pass to this shell, it will always be overridden by the code you
name in the script_lock parameter. If you don't want to type theses changes
manually, you can download the ready-patched code from the Download section of
this tutorial.
119 void
120 procargs(int argc, char **argv, const char
*script_lock)
121 {
...
136 if (*xargv == NULL) {
137 if (xminusc)
138 sh_error("-c requires an
argument");
139 }
...
155 } else {
156 if (script_lock) {
157 setinputfile(script_lock,
0);
158 } else {
159 setinputfile(*xargv, 0);
160 }
...
175 }
Listing 9 makes the matching changes in the main loop of dash to make sure that
when you compile it with the SCRIPT_LOCK preprocessor symbol defined,
procargs is called correctly with the additional script_lock argument. Simply
forcing the interpreter to execute a particular script stored somewhere in the file
system wouldn't give you any additional security, as it would be easy for a malicious
user to edit the contents of that script to execute that user's code, which would rather
defeat the purpose of this exercise. At line 72, you include a new file, regurgitate.c,
which in turn will contain the contents of the original script.
71 #ifdef SCRIPT_LOCK
72 #include "regurgitate.c"
73 #endif
...
100 int
101 main(int argc, char **argv)
102 {
103 const char *script_lock = 0;
104 char *shinit;
...
113 #if PROFILE
114 monitor(4, etext, profile_buf, sizeof
profile_buf, 50);
115 #endif
116
117 #ifdef SCRIPT_LOCK
118 script_lock = regurgitate ();
119 #endif
120
121 state = 0
...
167 init();
168 setstackmark(&smark);
169 procargs(argc, argv, script_lock);
170 if (argv[0] && argv[0][0] == '-') {
...
213 }
1 #!/bin/sh
2
3 scriptfile="$1"
4
5 cat <<EOT
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <unistd.h>
10
Putting this all together in Listing 11, you can now run a shell script needed while
booting or shutting down the machine through the eat script to embed it in the
patched dash sources. Then, you compile those sources to create a one-off
interpreter along with an embedded version of the original script that it will
regurgitate, execute, and clean up.
So, you now have a custom 76 KB dash interpreter that will run only its own
embedded shell script. You can use this process to create one-off binaries to replace
the more complicated shell scripts needed to boot and shut down the Linux machine,
at which point you're almost ready to remove /bin/sh itself.
Further study
Some small problems remain with this approach, for which Part 2 of this tutorial
series will offer solutions. With the text of the embedded shell script stored so plainly
inside the compiled interpreter, it would be easy to open the binary in an editor and
change the contents of the script. Obfuscating the code by compressing it, for
instance, would make it a little harder to find the right section to edit, but a
determined user could still copy a binary that embeds a large shell script and replace
the right bits of the copy with a custom compressed shell script, effectively giving the
user access to his or her own shell as if you had never bothered to remove it from
the system in the first place. Another option might be to embed a checksum in the
embedded binary, but again, a determined user could simply replace that checksum
with a new one that matches the new code the user injected.
A good way to solve all these problems without resorting to simple obfuscation is to
sign the script and have the patched shell check the signature on the regurgitated
script before executing it. You'll learn how to do this and get more details on having
the kernel execute only signed binaries in the next installment.
Section 7. Summary
With the techniques covered in this tutorial, you see how it's now possible to create a
Linux installation that can boot to the desktop without the need for an interpreter.
Depending on which distribution of Linux you begin with, you'll have more or less
work to remove the shell scripts used to boot the machine. But with a few days to
spare, you'll soon be the proud owner of a industrial-strength, locked-down
installation. The only chink in the armor as it stands is that a user can still download
foreign pre-compiled binaries and use the desktop file manager to run them. In Part
2 of this tutorial series, you'll learn how to close this avenue, too -- first by signing the
legitimate programs that can be used on the machine, and second by changing the
kernel to refuse to execute anything other than correctly signed binaries.
Downloads
Description Name Size Download method
Sample files for this tutorial sample_files.zip 236KB HTTP
Resources
Learn
• In the developerWorks Linux zone, find more resources for Linux developers,
including more Linux tutorials, as well as our readers' favorite Linux articles and
tutorials over the last month.
• Stay current with developerWorks technical events and Webcasts.
Get products and technologies
• Download Ubuntu Linux V6.10.
• Download the dash utility for Debian Linux.
• Download the dash utility for Ubuntu Linux.
• Order the SEK for Linux, a two-DVD set containing the latest IBM trial software
for Linux from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
• With IBM trial software, available for download directly from developerWorks,
build your next development project on Linux.
Discuss
• Get involved in the developerWorks community through our developer blogs,
forums, podcasts, and community topics in our new developerWorks spaces.
Trademarks
DB2, Lotus, Rational, Tivoli, and WebSphere are trademarks of IBM Corporation in
the United States, other countries, or both.
Linux is a trademark of Linus Torvalds in the United States, other countries, or both.
Microsoft, Windows, Windows NT, and the Windows logo are trademarks of Microsoft
Corporation in the United States, other countries, or both.
UNIX is a registered trademark of The Open Group in the United States and other
countries.