* 10 *

Software security II

Fact of the week

The Java security model is called a sandbox model. The idea is to put all activity inside a secure wrapper, so that users have no privilege to do anything dangerous.

Chapter 8,11 Gollmann

Race conditions

Multitasking systems can sometimes be tricked into reading false information or into carrying out erroneous tasks because of context switching.

Suppose the president of the United States is signing papers in the oval office. Every now and then, the phone rings and the president is distracted by another task. While the president is busy with another task, his evil advisor switches the papers around so that the president signs a piece of paper declaring the advisor to be the new president, effective immediately.

James Bond is hanging on a thread above the Brainiac Supercomputer Weapons Control Centre of Specter's Secret Death Ray, watching the security camera swing back and forth. As the camera is pointing away from the targeting computer, he drops down and reprograms the system so that the weapon will destroy Specter headquarters instead of London. Then there is just enough time to kiss the girl and climb to safety before the the camera discovers him...

These scenarios are called races or race conditions, because there is a race against time to perform some trickery while the system isn't looking.

The same thing can happen with processes in time-sharing systems. If a process (A) is interrupted, other processes (B,C..) get a chance to change something on the system while process A is put on hold. Normally one process does not have the access rights to change something for another process, but if we are sloppy with access controls it is possible. Race conditions are a common form of attack against software which relies on publicly accessible communications medium, e.g.

Note that the publically writable area acts as a covert channel for sending information past security mechanisms.

Restricting privilege

By now, you are probably tired of hearing the fundamental principle of security:
The fundamental prerequisite for security is the abilty to restrict privilege or access to data.
This idea is so simple and yet so central to security that it cannot be emphasized enough. Let's see how it applies to software systems:

Non-privileged user ID

Any Orange-Book C2 compliant operating system gives users distinct identities (user names or UID/SID numbers) and allows discretionary access control (DAC). This restricts the possibility for normal users to gain access to restricted resources, but many system processes are started by the administrator/superuser account. System processes therefore often run with unlimited privilege. This can be dangerous and there is often no need for it. Modern software servers usually change their working UID in order to prevent the process from having too much privilege.

For example, the Unix Apache WWW server is started by the root user and connects to privileged port 80. But immediately afterwards it changes its UID to a non-privileged user called www so that it no longer has read/write access to files which are not intended for the web. The MySQL and lprng servers take the same approach.

A rule of thumb for system software is:

Processes which do not need to run with privilege should relinquish all privilege as soon as possible.
In the POSIX/Unix model each process has a real and an effective UID. The real UID is the the highest attainable security level of the process (think of the BLP security model). The effective UID is the currently used security level. Both concepts are needed in general. For instance, a privileged process can set both its UIDs to a non-privileged user ID with the system call:
setuid(non_priv_uid);
But once this has been executed it cannot regain privileged access, since it no longer has privilege! For servers which do need to regain privilege, we use:
priv_uid = getuid();                    /* Normally root */

if (setreuid(-1,non_priv_uid) == -1)   // setreuid(real,eff)
   {
   perror("seteuid");
   return false;
   }

 // Non-privileged work

if (setreuid(-1,priv_uid) == -1)
   {
   perror("seteuid");
   exit(error);
   }
This call sets the effective user ID only. Setting both the real and effective ID's is equivalent to the setuid() call.

Sandboxing

For ultimate security, we would like to be able to lock a process into a closed box. One way to do this is to use the chroot function to lock a process into a sub-tree of the filesystem.

                        root

                       /     \
                     home    /\
                     /  \
                    /   --ftp--
                   /   |       |
                  /\   |       |
                 /\     -------

Anonymous FTP does this: anonymous FTP users cannot see any files outside of the FTP area. This should be combined with a change to a non-privileged UID, since root can get out of the box with fchroot.
  1. Create a private directory.
  2. Make sure there are no setuid root executables under privatedir.
  3. Make sure there are no links which give privileged access to memeory, e.g. /dev/kmem.
  4. Do chroot(privatedir);
  5. Do setuid() to a non-privileged user.
Java tries to use this approach to restrict the possibilities available to users connecting to applets. The web itself attempts a partial sandbox by restricting the UID of web processes. Java goes further by using the elements of object orientation, as we discussed in a previous lecture, to limit access to functionality. Web browsers have similar security policies, though this is by choice (often based on painful experience) rather than by design.

exec functions, system() and popen()

The exec family of functions runs a child process by swapping the current process image for a new one. There are many versions of the exec function which work in different ways. They can be classified into groups which See man execlp and man execve for details. Starting sub-processes involves issues of dependency and inheritance of attributes which we have seen to be problematical. The functions execlp and execvp use a shell to start programs. They make the execution of the program depend on the shell setup of the current user. e.g. on the value of the PATH and on other environment variables. execv() is safer since it does not introduce any such dependency into the code. There is no return from an exec-function. The original calling process is destroyed, so the execution of internal commands has to be accompanied by a fork() to a new process.

The system(string) call passes a string to a shell for execution as a sub-process (i.e. as a separate forked process). It is a convenient front-end to the exec-functions. However, as with all conveniences, it compromises security by opening processes to shell based attacks. e.g.

 system("/bin/ls -a | /bin/grep .");
Because of the dependencies it introduces, and the corresponding lack of control, the use of this function is inherently insecure.

The standard implementation of popen() is a similar story. This function opens a pipe to a new process in order to execute a command and read back any output as a file stream. e.g.

FILE *pp;
char line[1024];

pp = popen("/bin/ls -a","r");

while (!feof(pp))
   {
   fgets(line,1023,pp);

   printf("comm: %s\n",line);
   }

pclose(pp);
This function also starts a shell in order to interpret command strings. A safer version of the command which does not invoke a shell (i.e. cuts out the middle man) was written for cfengine. cfpopen() takes the approach used by several other software packages, of interpreting the command string itself and then passing it directly for execution. Note however that, if no specific path is specified, these functions do attempt to open the file in the current working directory. Exact paths should always be specified to avoid Trojan horse execution.

Note that, when a shell is executed, any commands in the shell-startup files will be executed. If a false shell-startup file can be introduced, arbitrary commands might be executed!

Spoofing races and temporary files: symbolic link troubles

Unix filesystems introduced the idea of symbolic links, or aliases to files. Privileged programs which carelessly open and write to files are easily spoofed into destroying important system files. For example, suppose we we write a program which opens and writes to a file, without first checking to see whether or not it exists. All an attacker has to do in order to destroy the system is the link that file to /etc/passwd or to an important shared library. When the file is opened and written to, the link converts to file request into a write operation on the password file and the password file is duly replaced with garbage.

This applies typically to software which opens temporary files in a publicly writable areas. Any user could make a link to /etc/passwd in /tmp. The same thing applies to writing to files in users' private home directories with a privileged process. For instance, with default settings, this would destroy a Unix database server:

cd /tmp
ln -s /etc/passwd /tmp/mysql.socket
su
mysqld &
In order to open a temporary file safely in a publicly writable area, we must first check that: Why this last step? Because there could still be a race between processes during the two system calls. If the writing process were suspended right after deleting the file, another process could still go in and reinstate the link during the suspension. Here is what we have to do:
int fd;
char *filename;

unlink(new);  // delete any existing object

 // Any context switch here?
 
if ((fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC|O_EXCL, 0600)) == -1)
   {
   perror("open");
   close(sd);
   unlink(new);
   return false;
   }

First we remove any file which might be there from before (or move/rename it). Then we open a file with the flag O_EXCL which causes open to fail of the file exists already. In other words, if a race has managed to reinstate a link to another file during a context switch, the open will fail rather than write to the incorrect file.

The impossible problem: establishing trusted identity for access control

Let us end this week's lecture with a particularly depressing insight. If you haven't understood this yet you should realize that everything in security ends up as an issue of trust.
It is impossible to truly verify the identity of a user or individual.
When we write protocols for connecting to services, identifying a user is an important issue. Even on a local machine, user ID is important. Normally we trust user IDs because they are run by the kernel, which is an implicitly trusted process (if the kernel is not trustworthy, you might as well give up). Somehow, we need to establish identity when communicating between separate kernels on separate hosts. To see how difficult this is, let's think up a number of scenarios of varying stupidity:
  1. We declare our user ID by sending it as part of a protocol string: e.g. "LOGIN mark UID 56". This is clearly inadequate. Any one could write a new program to send a false string to the server. For a server to trust this protocol would be absurd.
  2. We verify the identity with some kind of shared secret: by using a password or public/private key system, we can be more certain of the authenticity of the claimed identity. The problem here is that passwords can be guessed and keys can be spoofed. How do we know that the person who set the password or key is who he/she/it claims to be? e.g. think of PGP. Anyone can claim that a public key belongs to the president of the US, but how do we really know that is true? PGP uses signatures, but how do we know that the signatures are authentic? This problem is never ending.
  3. We attempt to contact the client machine's kernel in order to verify the authenticity of the claim: but then we have to trust the mechanism for verfying the identity. e.g. the pidentd protocol.
No matter how hard we worth to authenticate identity, there is a spiralling problem of trust. We should work hard to minimize the need for trust, but the problem never really goes away.

Summary

The thumb-rules for software security:
  • Build sensible user interfaces. Make dangerous functions difficult to access.
  • Restrict privilege: try to minimize the destructive power of the user at all times.
  • Remember that all software has implicit dependencies. What are they? How can we protect ourselves against exploitation?
  • Work hard to identify users, if you need to use identity in your software. Use established authentication mechanisms and make sure that they are difficult to spoof.
  • Avoid using too many local variables, they can overflow a process/thread stack. (The stack has a finite size allocated)

  • Use secure protocols, development languages which give tight security and always pay attention to compiler warnings.
  • If software fails, make sure that it fails safely (i.e. make sure there are safe defaults and that a failure does not leave the system in a vulnerable state.).
  • Do not leave anything to chance. Specify every detail explicitly, i.e. every PATH to commands, and link software statically to avoid shared-library spoofing. The administrators PATH variable should NOT contain the current working directory "."
  • Never write files to an area of the filesystem which is not completely private, without first making sure that the object you are writing to does not exist.
  • Always check the bounds on arrays.
  • Always limit the amount of input which is read into a buffer, to avoid overflow. (i.e. never user gets() from the C library or the iostream ">>" operator in C++)
  • Validate input to make sure that it does not contain hidden commands.
  • Use strncpy() instead of strcpy() when you do not know string sizes. Use sprintf with maximum string length %.xxxs
  • Run programs with Purify/Electric Fence to debug them. This is very easy. These programs simply rplace the system linker with a bounds checking analyzer. In your makefiles you just replace "ld" with "purify gcc" or add "-lefence".

Back