![]() (Info on the pic) |
The fundamental goal of our project is to change the set of primitives available to network daemons, such that they can accomplish their tasks without ever running as root.
It is the fact that so many daemons run as root which makes them such an attractive target for attackers. At the present, if a malicious user gains control of ftpd, then immediately all root privileges are available. We wish to limit the possibility of attack to the usual (or yet to be discovered) vulnerablilities of a user program which allow an attacker to obtain root privileges. Thus we will have made access to root privileges a two-step process rather than one.
At very least, this is a deterrent factor. At best, there might not be a vulnerability that lets the attacker gain root (yeah right). Somewhere in between is the possibility of successfully getting a log of the initial attack to stable, read-only storage before the attacker gains root and cleans the logs.
We intend to work within the existing framework of Linux as it stands today. We aim to develop a system which can in fact be easily used by security-conscious sysadmins. Daemons will require only a few lines of code changes (if any), and kernel changes should be only a few tens of lines (ideally to be incorporated into the mainstream Linux kernel).
The primitives that we will examine and modify:
When someone logs in to FTP as "annonymous" we want to call chroot(2) in order to restrict their access to the section of the file system where the files that are available by anonymous FTP are stored. At this point we do not need to change the process' credentials; ftpd does not become the user in this case, since it is not even known who it is. (It is still desirable to distinguish the daemon user used when ftpd first accepts the connection, and the daemon user used for the duration of an anonymous FTP connection. A design for this is still in flux.)
chroot(2) as it now stands must be called from a program running as root. It makes a permanent change to the file system access that is passed on to any child processes: any file names presented to open(2) will be interpreted relative to chroot(2)'s argument.
The reason chroot(2) must be privileged is subtle. If ordinary users are allowed to chroot(2) to arbitrary places, it changes the names of certain, trusted files. For instance if you make the following call,
chroot("/tmp");
the name /etc/passwd
would now refer to /tmp/etc/passwd and someone in control of the
contents of /tmp (typically any user can write to /tmp)
could have installed whatever files he/she wished as a
subdirectory of /tmp.
Then they could run su(1), which would now only see the malicious forgeries of /etc/passwd and /etc/shadow due to the name change implied by running chroot(2). The ordinary user could now logon as root via the call to su(1), using the passwd and shadow files that he/she had installed.
We will change chroot(2) so that it can run from anywhere but we will disable the ability for any setuid program to run after chroot(2) has been called. This solves the above vulnerability because the jailed process now can never gain privileges.
Due to an implementation detail in the kernel, it is possible for a process to use chroot(2) itself to escape the jail. (And there is not an easy way to fix this.) The following code will do it:
int fd = open("/", O_RDONLY); // get jail's / as an fd
mkdir("testdir");
chroot("testdir"); // make nested jail -- key to escape!
fchdir(fd); // back to first jail's /
for (int i=0; i<10; i++) {
chdir(".."); // successively higher ("/../"=="/")
}
chroot("."); // final "jail" is real /
execl("/bin/sh", "/bin/sh", NULL); // unjailed shell
In current systems, this escape is not possible because chroot(2) is privileged (there are many ways to escape the jail if you get root inside it). In the new system, we can prevent this by disabling chroot(2) inside the jail.
Thus, these changes will eliminate the need for ftpd to run as root in order to restrict access to the file system for anonymous FTP.
sshd must listen to port 22.
bind(2) allows a process to bind a privileged port. At the present one must run as root in order to call bind(2) with a port argument in the range [1,1023]. This is what it means for those ports to be privileged.
We are going to allow programs in a special group to bind(2) privileged ports. By using a group, we can grant the privilege to any subset of daemons we choose.
This means that if someone subverts sshd they will be able to bind privileged ports, but this is no worse than the present situation, where subversion leads to total compromise.
When an ftp session is initiatied by a client, inetd starts the running of ftpd. The ftp daemon runs as root during the authentication phase of establishing a connection with the client who has requested the service. After authentication ftpd runs as the user, with limited privileges.
Currently, programs that need to authenticate do something similar to the following:
// data
char username[80], password[80];
struct passwd *pw;
// get authentication info
prompt(username, 80, "username: ");
prompt(password, 80, "password: ");
pw = getpwnam(username);
// check password
if (0==strcmp(crypt(passwd, pw->pw_passwd), pw->pw_passwd)) {
// become that user
setegid((gid_t)pw->pw_gid); // primary group
initgroups(pw->pw_name, pw->pw_gid); // additional groups
seteuid((uid_t)pw->pw_uid); // user id
}
In the above code, getpwnam only returns a useful password if the process runs as root (because /etc/shadow is only readable by root). Further, the setegid, initgroups, and seteuid calls are all privileged. For these reasons, daemons that authenticate must run as root on current operating systems.
We propose to export the authentication check, and the setuid call, to a trusted subsystem. We will allow processes running as ordinary users to become any other user, by presenting a username and password to a trusted authentication subsystem. The above sequence will change to something like:
// data char username[80], password[80]; // get authentication info prompt(username, 80, "username: "); prompt(password, 80, "password: "); // check it, and if success, become that user auth_check(username, password);
The trusted subsystem will be implemented as a combination of a separate, run-as-root server to do the authentication check, and a loadable kernel module to serve as trusted intermediary and to do the setuid.
The kernel module is needed because there is presently no way for one process, even one running as root, to adjust the process credentials of another process.

In principle, we can use PAM (Pluggable Authentication Modules) to minimize the changes needed in the daemon code, if the daemon has already been written to know about PAM. PAM provides generic authentication interfaces, and we would simply write a new PAM module that talked to the special authentication device driver.
Network daemons are the highest priority, since they are remotely accessible. However, a large number of setuid-root programs could have their setuid bits stripped if they utilize the above mechanisms, especially the authentication subsystem.
As a trivial example, su(1) need not be setuid-root, since its entire job is subsumed by the authentication subsystem. Another example is xlock(1), which just needs to check passwords.