Understanding Race Conditions and Preventing Bugs

Race Conditions – A Type of Bugs

A race condition exists when changes to the timing of two or more events can cause a change in behavior. If the proper timing of execution is required for the proper functioning of the program, this is a bug.

  • Most of the time, race conditions present robustness problems.
  • But also many important security implications.

Attackers can sometimes take advantage of small time gaps in the processing of code – window of vulnerability – to interfere with the sequence of operations, which they then exploit.

  • An attacker can increase the window by slowing down the machine or running an attack increase parallel.
  • A software developer should try a way to fix the race condition by reducing the window of vulnerability to zero.

What Happens When a Race Condition Occurs?

A programmer assumes (usually implicitly) that certain operations happen atomically when in reality they do not. Considering two consequent operations in which the latter depends on the first. In the interval in between – the window of vulnerability – an attacker may be able to force something to happen (specially if there is a blocking system call between them), changing the behavior of the system in ways not anticipated by the developer.

TOCTOU – Time of Check Time of Use

It is a common race condition problem. Multiple processes on a single machine can have race conditions between them when they operate on data that may be shared:

  • Variables (count example)
  • Files (File-based race condition)

File Based Race Condition

  • tA: There is a check on the validity of assumption X on an object E, before it is used.
  • tB: E is used, assuming that the assumption X is valid.
  • tC: the assumption X is violated.
  • The program has to execute with elevated privileges (the goal for the attacker is to get an object he couldn’t get with his privileges)

Access-Open Race Condition

A run SUID program has the EUID = Root and RUID = User. Let’s consider a program running setuid root is asked to write a file owed by the user running the program. In order to understand if the user can (or cannot) write a file, in this case, the RUID has to be tested. To do this, programmers use the call access().

access() returns 0 on success

if (!access(file, W_OK)) {
f = fopen(file, “wb+”);
write_to_file(f);
} else {
fprintf(stderr, “Permission denied. ”);
}

In this case:
tA = access()
tB = fopen()

If an attacker can replace a valid file to which the attacker has permissions to write with a file owned by root, all within that time window in a time tC, then the root file will be overwritten.

  • The easiest way to do this is by using a symbolic link that points to a file the attacker can access.
  • The attacker tells the program to open the file pointer.
  • The attacker try to change the link to point a file owned by root, during the window of vulnerability.
  • If the attack succeed, he gains access to the file.

Example of attacks that increase the vulnerability window: FILE SYSTEM MAZE ATTACK, HASH TABLE.

(SETUID) SCRIPT EXECVE RC

int execve(const char *filename, char *const argv[], char *const envp[]);
execve() executes the program pointed to by filename. filename must be either a binary executable, or a script. What happens when execve is invoked on a setuid script?

  1. execve() system call invokes setuid() call prior to executing the setuid script.
  2. The kernel opens the executable, and finds that it starts with #!.
  3. The executable is closed and the interpreter is loaded instead.
  4. The interpreter (with root privileges) is invoked on the script name (it opens the script).
  5. The script is run by the interpreter.

An attacker can replace script content between step 3 and 4. However, Linux ignores the setuid bit on all interpreted executables.

Remove Directory Trees RC (rm -r )

  • rm can remove directory trees, traverses directories depth-first.
  • issues chdir(“..”) to go one level up after removing a directory branch.
  • by relocating subdirectory to another directory (while rm -r is running!), arbitrary files can be deleted.

Races on Temporary Files

Creating temporary files in a shared space (with no special permission to create files inside) such as /tmp is common practice.

  • tA: a program checks if file “/tmp/tmp0001” already exists through stat(“/tmp/tmp0001”, buf).
  • tB: Assuming that the stat returns that the file doesn’t exists, the program will create the file by calling fopen(“/tmp/tmp0001”, w).

An attacker could guess the temporary file name and create a soft link within the two actions specified above (ln –s /etc/target /tmp/tmp0001) and this would cause:

  • the modification of target file (if the effective user has privileges to modify it) or,
  • in the best case, the failure of fopen() call.

Another RC related to temporary files involves Temp cleaners, programs that clean old tmp files.

  • tA: lstat(2) is called to check if a file exists
  • tB: Assuming that the stat returns that the file exists, the program will remove it by calling unlink(2)

If the file checked is a link, an attacker could change the link with the risk of ARBITRARY FILE DELETION.

Secure procedure for dealing with tmp files:
– Pick hard to guess filename using appropriate library function (not your own implementation).
– Set umask appropriately (0066).
– Automatically test for existence AND create the file:
. open(2) with O_CREAT|O_EXCL to create the file in proper mode.
* If the file already exists -> open() will fail -> try again in loop.
– Delete file immediately using unlink: the file will be still usable until it is closed. When the file is closed, it is automatically deleted.
– Use file handles instead of file names (kernel option CONFIG_FHANDLE).

Thread RC

Use-After-Free

More Examples

  • chown(2) and chmod(2) are unsafe because they use file names. Use fchown(2) and fchmod(2) that use file descriptors, instead.
  • Joe Editor vulnerability (DEADJOE file)
  • SQL select before insert
    . Use select to check if a certain element already exists;
    . If the select returns no results, insert a (unique) element
    . If two processes would like to insert the same element and they perform the same check at the same time, a double insertion would happen! SOLUTION: use LOCKING or a single atomic insert that will fail if the key already exists.

Preventing RC

  • Use HANDLES (file handle or file descriptor), instead of filename. By dealing with file descriptors or file pointers, we ensure that the file on which we are operating does not change after we first start dealing with it. Example: use fstat() instead of stat().
  • Avoid to implement your own access checking on files (e.g. avoid access(2))
  • Drop privileges with setuid() and check the return value. if setuid() fails, EUIF is still root! Drop also group privileges with setegid(). When opening arbitrary files, we recommend that you start by using open(2) – with O_CREAT and O_EXCL flags – and then using fdopen(2) to create a FILE object once you’re sure you have the proper file. lstat(“filename”, buf) fd = open(“filename”, O_CREAT | O_EXCL, “w”) //could fail fstat(fd, fbuf) compare 3 fields of buf e fbuf. fdopen(fd, “w”) Unfortunately, many common calls do not have alternatives that operate on a file descriptor, including link(), mkdir(), mknod(), rmdir(), symlink(), unmount(), unlink(), and utime(). What should we do in these cases? Create secure directory, accessible only by the UID of the program performing file operations. Make sure that an attacker cannot modify ANY of the parent directories: create dir with appropriate mode chdir() into it walk up the directory tree until we get to the root and at each stage: checking to make sure that the entry is not a link and making sure that only root or the user in question can modify the directory both checks are essentials!

Locking

File locking can prevent race condition. However, many operation systems uses Advisory Locking.

Non File System Race Conditions

PTRACE (It attach a process for debugging purpose) an attacker to use ptrace, or similar function (procfs), to attach to and, thus, modify a running setuid process. Unprivileged local users can use the ptrace function to take advantage of a privileged program, while that program is performing a privileged operation, to gain privileged access. In new Linux Kernels versions, ptrace can only attach to processes of same UID, except when run by root. the system protect against these monitoring routines if the process is setuid or setgid.

EXECVE

Execute program image Setuid functionality (as before) Not invoked when process is marked as being traced But… 1. first checks whether process is being traced 2. open image (may block) 3. allocate memory (may block) 4. set process EUID according to setuid flags Between step 1 and 4 -> attacker can attack via ptrace. -> Kernel side defence against this attack (locking).

Detection

Static Code Analysis Specify potentially unsafe patterns and perform pattern matching on the source code. Dynamic Code Analysis Deduce data races during runtime