Security-specific Programming Errors (Part 2)
Thomas Biege
Race Conditions (not limited to C!)
Race conditions can occur wherever non-atomic calls
for security-specific program sections are used. This section focuses
on a classic and frequent type of race condition. Basically,
race conditions can occur in the most diverse manners and are
not limited to the file system.
Examples:
When a privileged application opens a file belonging to the user
activating the program, the system needs to check whether the user is
entitled to do this in the first place, and needs to do so before the
application opens the file.
Wrong:
[...]
if(access("/home/evil_ed/RythmStick", W_OK) == 0)
{
/* User has write permission */
if((fd = open("/home/evil_ed/RythmStick", O_WRONLY)) < 0)
{
fprintf(stderr, "I'm not allowed to open
the file!\n");
exit(-1);
}
}
[...]
First, it is necessary to understand the user-specific IDs
in Unix:
- User ID
Every user is given a unique number. UID 0 belongs to
root. Anybody working with UID 0 will not encounter any security
barriers from the system. Normal users are generally assigned a UID
starting at 100, for example, or 500. All the UIDs below this belong
to system processes.
- Group ID
Unix systems have several groups. The administrators, for
example, can be assigned to the group root or wheel. Users are
generally assigned to the group called user or to groups that identify
their activities, e.g., fb4, wwwadmin, students, accounting, etc. A
user can belong to 1...NGROUPS_MAX (32) groups.
- SetUID and SetGID
In order for a program to accomplish a task for which it
requires higher-level privileges, the program can either be executed
by a user with these rights, or the system needs to be informed that
the program is to be assigned the necessary rights when executing. The
former is a very laborious and insecure approach as either the
password needs to be known by all, or the person with the password has
to log in to the system to accomplish a task for another user. In the
case of a printer or fax spooler, the effort involved would not be
justifiable. The Unix file system implementation has two files for
this very purpose. The SetUID flag gives the program the
(effective) UID of the user to whom the program belongs and not the
UID of the user executing the program (the real UID). The same applies
to the SetGID flag.
- real UID/GID and effective UID/GID
The real UID is the UID of the user. If user Thomas with UID
543 executes a SetUID program from user root, for example, the program
has the effective UID 0 and the real UID 543. As Unix systems use the
effective UID for access checking on system objects, the UIDs can be
substituted and the effective UID can even be discarded. If the real
UID is not 0, a UID that has been discarded cannot be retrieved. A
substituted UID, on the other hand, can.
- saved UID/GID (POSIX)
The saved IDs are only implemented when _POSIX_SAVED_IDS
is set, which should be the case with today's Unix alternatives. The
saved UID is set by the exec*(3) functions and allows the UID
to be changed. For example, if the real and effective UID is 543 and
the saved UID 0, then it is possible to change, even though the
effective UID is not 0. The saved UID is only discarded when
setuid(2) is called and the effective UID is 0. Moreover,
SetUID applications whose SetUID is not 0 must use
setreuid(getuid(), getuid()) to get rid of their privileges. A
simple setuid(2) doesn't help. Of course, the same applies to
the GID, too.
The access(2) call uses the real UID and real
GID to check the rights. This means that the effective
UID/GID of SetUID/-GID programs does not apply. In the case
of the access check with open(2), on the other hand, the
effective UID/GID is used. This fact wouldn't be worth
mentioning - in fact, it would even be desirable - if access(2)
and open(2) were an atomic function i.e., a system call.
Between access(2) and open(2), however, there is now a
time window during which the program is vulnerable. The attacker can
now delete the file /home/evil_ed/RythmStick and replace it
with a link pointing to /etc/security/shadow, for
example. The open(2) call thus does not then open
/home/evil_ed/RythmStick, but /etc/security/shadow
via the link instead, and processes the data which the user is not
actually allowed to access.
To summarize:
There is a file called /home/evil_ed/RythmStick. The user
EvilEd has write permission for this file, which is checked with
access("...", W_OK). EvilEd now deletes the original
file and places a link to a file for which he does not have write
permission. Open(2) follows the link and opens the protected
file.
Right:
This problem can be encountered in different ways.
- faccess()
Unfortunately, faccess() does not exist on all systems,
e.g., not on Linux, OpenBSD, Solaris and
AIX.
[...]
fd = open("file", O_WRONLY);
if(faccess(fd, W_OK) != 0)
{
fprintf(stderr, "Nice try!\n");
exit(-1);
}
[...]
A further disadvantage of faccess() is that open(2)
needs to be called first. When open(2) is used on device files
(Unix), an action can already be performed with the corresponding
device depending on the implementation of the device driver. Magnetic
tapes are one example - they have to be rewound when the device file
is opened. In the case of iterative backups, this can lead to loss of
the old backup data.
- O_NOFOLLOW
The open(2) option O_NOFOLLOW forbids
open(2) for following symbolic links. This can be
circumvented with hard links, so it is of little use.
- setegid(2) and seteuid(2)
With seteuid(2) and setegid(2), the program's
higher-level privileges can be discarded and the rights of the user
accepted for the period during which open(2) is being
executed. The old rights can be restored after open(2).
The sete(u|g)id(2) and setre(u|g)id(2) functions
conform to BSD4.3 and are included in the extended POSIX
standard when _POSIX_SAVED_IDS is defined. The operating system
must thus support saved IDs in addition to the real and
effective IDs. It is important to make sure that the group
ID is placed before the User ID as the GID cannot otherwise be
changed when the UID has been reduced. This fact is often forgotten,
which means that the program still has a security flaw. Moreover, the
return value of setuid(2) and setgid(2) should always be
checked in order to be able to react to unpleasant events.
[...]
uid_t euid, ruid;
gid_t egid, rgid;
euid = geteuid();
egid = getegid();
ruid = getuid();
rgid = getgid();
if(setegid(rgid) < 0)
[Exit]
if(seteuid(ruid) < 0)
[Exit]
open("...", ...);
if(setegid(egid) < 0)
[Exit]
if(seteuid(euid) < 0)
[Exit]
[...]
- fork(2)
The only portable, most secure (and most elaborate) way to
create a child process with fork(2) is to discard the
privileges permanently, open the file and end the child process
after the file descriptor has been returned to the parent
process.
[...]
pid_t child_pid;
[...]
if((child_pid = fork()) < 0)
[Exit]
else if(child_pid > 0) // Parent
[Get Filedescriptor and Wait for Child]
else // Child
{
inf fd;
if(setgid(getgid()) < 0)
[Exit]
if(setuid(getuid()) < 0)
[Exit]
/*
** When the EUID of the process is 0, the
** _all_ IDs are changed with set(u|g)id(2)
** (real, effective, saved)!
** Otherwise, _only_ the effective ID is set!
*/
if( (fd = open("userfile", O_WRONLY)) < 0)
[Exit]
/*
** Now, the file descriptor can be passed to the parent
** process. Unfortunately, there is not yet
** a standardized function for this. SystemV and BSD Unix
** offer different possibilities.
** SVR4:
** - ioctl(I_SENDFD)
** - Unix Domain Sockets
** 4.3BSD:
** - sendmsg(2) and recvmsg(2)
** - Unix Domain Sockets
*/
[...]
}
[...]
|
|
|