Special care also needs to be taken when your setuid application creates a file. Consider the passwd utility that you can use to change your password. This program is set-user root because it needs to be able to rewrite the shadow database.3.8
Since the shadow file is a plain-text file, the program cannot simply modify it in-place; it has to copy it to a temporary file, modify the caller's account as it goes, and replace the original file with the modified one. A naive implementation would do something like this (omitting any error checks for brevity's sake):
FILE *ifp, *ofp;
ifp = fopen("/etc/shadow", "r");
ofp = fopen("/etc/shadow.new", "w");
while (fgets(buffer, sizeof(buffer), ifp)) {
/* copy to output file; modify passwd of caller */
}
rename("/etc/shadow.new", "/etc/shadow");
/* fix permissions */
chmod("/etc/shadow", 0600);
fclose(ifp);
fclose(ofp);
This code should send your internal security alert klaxons honking madly...
The very first problem is that the new shadow file will be owned by the root user (since passwd is set-user root), but will have the group of the user who ran the program! That's because making a program set-user root only affects the uid; the real and effective gid will remain unchanged. So the resulting file will look something like this:
$ ls -l /etc/shadow -rw------- 1 root users 626 Sep 30 23:40 /etc/shadow
This is more of a wart than a real security problem so far, because due to the restricted permissions members of group users will not be able to do anything with the file. Besides, this is easily fixed by calling chown("/etc/shadow", 0, 0), which will change both the file's owner group to root.
However, the really bad part of the problem is that before the
permissions are fixed via chmod, the caller may very well be
able to get read or even write access to the file. To understand why,
you have to know that when you call fopen(..., "w"), the
fopen library function will ask the kernel to create
the file with a mode of 0666, i.e. read and write access
for everybody. This permission mask is modified by the so-called
creation mask or umask value associated with the process:
the permissions given to the newly created file are the bitwise AND of
the 0666 permissions and the bitwise NOT of the umask.
For instance, if the umask is set to 022, then the file will
be created with a permission set of 0644. Or, if the umask is
0, the file will be created with permissions 0666, i.e.
read and write access to everyone.
XXX: DRAFT OF FIGURE
umask 002 create mode 0666 rw-rw-rw-
(binary 000000010)
\----- bitwise NOT ------------------------> rwxrwxr-x
AND rw-rw-r--
The umask value is inherited when a command is invoked, so the user
can control the permissions with which the shadow.new file is
created. By setting the umask to 02 before invoking our flawed
passwd program, the file would initially have a permission of
0664. Since the file's group is users, the caller is in
fact able to open it for writing during the time passwd is busy
copying the contents of the old shadow file to the new one.
That means, he can modify the file any way he likes!
Depending on the size of the file and the speed of the computer, the window during which this vulnerability exists can be between a couple of microseconds, and several seconds. But the exact numbers don't really matter; what matters is the fact that there is a chance to exploit this bug if the attacker is fast enough.
This is called a race condition, because of the small time window during which the problem exists. Commonly, race conditions can be exploited by brute force: the attacker simply executes some code over and over again until the timinig is right.
Consider the following code snippet:
/* try to open shadow.new read/write until we succeed */
do {
fd = open("/etc/shadow.new", O_RDWR);
} while (fd < 0);
/* overwrite root entry: */
write(fd, "root:::::::\n", 12);
If the attacker runs this little program in one window, and invokes
passwd with a umask of 0 in another, there's a
certain probability that the attack program will get a slice of CPU
time while passwd is in the critical code section where
shadow.new exists but is not sufficiently protected. As a
consequence, the open call succeeds. Now the program owns an
open, read/write file descriptor to what will eventually be installed
as /etc/shadow! A cracker's dream come true...all he needs
to do now is modify the file so that he can log in as super user.
The probability of this happening is not very large. But who cares? The attacker can run the passwd command as many times as he likes!
The proper fix to this problem is to make sure the umask is set properly prior to calling fopen. The umask system call sets the umask of the process to the value given as argument, and returns the previous umask value. So the fixed code looks like this:
FILE *ifp, *ofp;
mode_t oldmask;
ifp = fopen("/etc/shadow", "r");
oldmask = umask(077); /* set umask to 077 */
ofp = fopen("/etc/shadow.new", "w");
umask(oldmask); /* restore previous umask */
while (fgets(buffer, sizeof(buffer), ifp)) {
...
}
This code makes sure that shadow.new is created with proper
permissions 0600 right from the start. This makes sure that
no-one but the file's owner (i.e. the root user) can open it.
We will revisit other race conditions in more depth in chapter 4.