This lab explores race condition vulnerabilities. A race condition is any situation where the timing or order of events affects the correctness of programs or code. For a race condition to occur, some form of concurrency must exist – e.g., multiple processes or threads of control running at the same time – as well as some sort of mutable resource. A race condition occurs when the same data is accessed and written by multiple threads of control or processes.
A common sort of resource for programs to use is files in the
filesystem. If a setuid
program that uses files has a race
condition vulnerability, attackers may be able to run a parallel process
and attempt to subvert the program behaviour.
Is a program with a race condition always guaranteed to work correctly? Is an attack on a program with a race condition always guaranteed to succeed?
The answer to both questions is “No”. By definition, a program with a race condition only works correctly when events occur in the “right” order, and they are not guaranteed to do so. An attack on such a program depends on events ocurring in the “right” order for the attacker, and this, also, is not guaranteed to happen.
What is a symlink attack? See if you can find out how they are typically defined, and how they can be protected against. How do they relate to race conditions? If a race condition is involved, identify the resource being altered.
Symlink attacks were described in lab 4, on setuid
vulnerabilities, and further information is available in the CAPEC
database which describes different attack patterns. They occur when
an attacker creates a symbolic link so that a vulnerable program
accesses the link’s endpoint (say,
/etc/some_sensitive_file
) on the mistaken assumption it is
accessing a file at the link’s source path. The result is that the
vulnerable program reads from or writes to an incorrect file.
Symlink attacks need not involve race conditions: any attack where the attacker induces a program to read from or write to an incorrect file by using a symbolic link counts as a symlink attack. However, a common sort of vulnerability is where:
setuid
) program checks to see
whether it should access some file FThis counts as a race condition because correct operation of the program will only happen if the file is not replaced between steps (i) and (ii); but since they are not guaranteed to be atomic, there is an interval between the steps, and the file can be replaced during that interval. This sort of vulnerability is sometimes called a symlink race, and it is an example of a TOCTOU vulnerability (which we have covered earlier).
How to defend against the attack varies on the logic of the vulnerable program, but the general approaches are:
The resource being accessed as part of the race condition is the file F.
In multithreaded programs, it may be possible for multiple threads to access some memory location. If two threads access the same variable concurrently and at least one of the accesses is a write, then that is a data race, and it is undefined behaviour in C.
Save the following program as race1.c
, and compile it
with:
gcc -std=c11 -pedantic-errors -Wall -Wextra -pthread -o race1 race1.c
Program race1.c
:
#include <pthread.h>
#include <stdio.h>
long GLOBAL;
void *operation1(void *x) {
++;
GLOBALreturn NULL;
}
void *operation2(void *x) {
--;
GLOBALreturn NULL;
}
int main() {
[2];
pthread_t t(&t[0], NULL, operation1, NULL);
pthread_create(&t[1], NULL, operation2, NULL);
pthread_create(t[0], NULL);
pthread_join(t[1], NULL);
pthread_join}
This program uses the Pthreads library to control program threads.
Two threads are created using the pthread_create
function,
and main
waits for them to finish by calling
pthread_join
. One of the threads increments
GLOBAL
, the other decrements it. However, they are doing so
without any sort of synchronization, so this counts as a data race and
is undefined behaviour. If the program operates as the programmer might
expect, then one thread increments GLOBAL
, another
decrements it, and the end result should be that GLOBAL
is
0 at the end of the program. However, because our program invokes
undefined behaviour, any result is possible: the variable could end up
with other values (i.e. data corruption).
We can detect this race condition using ThreadSanitizer
(TSan, for short). Compile again with the following command. (When
compiling, we add the -g
option to improve error messages
printed by TSan, but you can also leave it off.)
gcc -g -std=c11 -pedantic-errors -Wall -Wextra -fsanitize=thread -pthread -o race1 race1.c
Then run the program. You should see output something like the following:
==================
WARNING: ThreadSanitizer: data race (pid=590418)
Read of size 4 at 0x561c214a7014 by thread T2:
#0 operation2 /home/vagrant/race1.c:12 (race1+0x12f1)
Previous write of size 4 at 0x561c214a7014 by thread T1:
#0 operation1 /home/vagrant/race1.c:7 (race1+0x12ac)
Location is global 'GLOBAL' of size 4 at 0x561c214a7014 (race1+0x000000004014)
Thread T2 (tid=590421, running) created by main thread at:
#0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:969 (libtsan.so.0+0x605b8)
#1 main /home/vagrant/race1.c:19 (race1+0x1388)
Thread T1 (tid=590420, finished) created by main thread at:
#0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:969 (libtsan.so.0+0x605b8)
#1 main /home/vagrant/race1.c:18 (race1+0x1367)
SUMMARY: ThreadSanitizer: data race /home/vagrant/race1.c:12 in operation2
==================
0
ThreadSanitizer: reported 1 warnings
When we compile with ThreadSanitizer, our program is instrumented (i.e., extra instructions are added) so that it keeps track of the accesses each thread makes to memory. By default, the last 217, or roughly 128,000, accesses are tracked. It is possible to alter this number when your program is invoked. The following invocation
$ TSAN_OPTIONS="history_size=3" ./race1
will double the number of accesses tracked. If the ThreadSanitizer finds that more than one of those accesses is to the same memory location, and at least one of those accesses was a write, then this will be flagged as being a race condition.
Find out what resources are used by a program with TSan enabled, compared with a program which does not have it enabled.
According to the documentation at https://clang.llvm.org/docs/ThreadSanitizer.html, a sanitized program uses more memory:
At the default settings the memory overhead is 5x plus 1Mb per each thread.
However, ThreadSanitizer is not infallible, as we will demonstrate.
Here is a second program – save it as race2.c
:
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
int GLOBAL;
void* operation1(void *x) {
= 99;
GLOBAL return x;
}
int main(void) {
;
pthread_t t(&t, NULL, operation1, NULL);
pthread_create= 100;
GLOBAL (t, NULL);
pthread_joinif (GLOBAL == 99)
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
Compile it as follows:
$ gcc -g -std=c11 -pedantic-errors -Wall -Wextra -pthread -o race2 race2.c
In this program, a thread is spawned which sets the value of
GLOBAL
to 99, while the main
function
concurrently sets it to 100 – this again, is a data race. Typically, the
main
function will “win”, and the value will be 100, but
sometimes not. We can demonstrate this by running the following Bash
code:
$ i=0 ; while ./race2 ; do echo $i ; i=$((i+1)) ; done
In the cases where the main
function “wins”,
race2
will exit with exit code 1, and the while loop will
continue. However, if the thread “wins”, race2
will exit
with exit code 0, and the while loop will halt. If you run the program,
you should see the main
function “win” many times, but
eventually, the thread will succeed instead – and the value of
i
will show how many times we had to run the program before
this happened. (Typical values are somewhere in the thousands, but it
could sometimes be higher or lower.)
Now compile the program and run it with ThreadSanitizer enabled:
$ gcc -g -std=c11 -pedantic-errors -Wall -Wextra -fsanitize=thread -pthread -o race2 race2.c
$ i=0; while (./race2 ; [ $? -ne 66 ]); do echo $i; i=$((i+1)); done
By default, if TSan detects a race condition, the program exits with
exit code 66 (see the TSan
options documentation). (We could alter this by invoking our program
with, say, TSAN_OPTIONS="exitcode=3" ./race2
if we wanted
to force the exit code to be 3 instead.) Our while
loop
continues to run until TSan does detect a race condition.
You will typically see that TSan does not always detect a race
condition, but eventually does. Why does TSan not always detect the
race? Because sometimes,
the line GLOBAL = 100
is executed before the operating
system has finished creating a new thread at all. In that case, TSan
does not “kick in” until the thread is created, and doesn’t realize that
the thread is altering a variable which was also altered in
main
.
The traditional way to protect against a data race in this program
would be to either use atomic types (i.e. alter the type of
GLOBAL
), or to use locks (e.g. mutexes – “mutual
exclusion locks”). See if you can amend the program to use one of these
two approaches. Which of these approaches can successfully fix the
issue?
One approach is to use atomic types. They were introduced in C11, and you can read about them here. We can write the program in that case as follows:
#include <pthread.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <unistd.h>
;
atomic_int GLOBAL
void* operation1(void *x) {
= 99;
GLOBAL return x;
}
int main(void) {
;
pthread_t t(&t, NULL, operation1, NULL);
pthread_create= 100;
GLOBAL (t, NULL);
pthread_join
if (GLOBAL == 99)
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
We have added the use of the <stdatomic.h>
header,
and changed GLOBAL
to be of type
atomic_int
.
We can run the following bash code to see if TSan detects a data race (hit ctrl-c to stop it):
i=0; while (./race2-atomic ; [ $? -ne 66 ]); do echo $i; i=$((i+1)); done
But no data race should be detected; by using atomics, we have removed the data race, and our program is well-defined.
Alternatively, we can use the mutex types from the Pthreads library
to protect our GLOBAL
variable.
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
int GLOBAL;
= PTHREAD_MUTEX_INITIALIZER; // initialize
pthread_mutex_t mutex
void* operation1(void *x) {
(&mutex); // request lock before accessing shared variable
pthread_mutex_lock= 99;
GLOBAL (&mutex); // release lock
pthread_mutex_unlockreturn x;
}
int main(void) {
;
pthread_t t(&t, NULL, operation1, NULL);
pthread_create
(&mutex); // request lock before accessing shared variable
pthread_mutex_lock= 100;
GLOBAL (&mutex); // release lock
pthread_mutex_unlock
(t, NULL);
pthread_join
(&mutex); // request lock before accessing shared variable
pthread_mutex_lockint tmp = GLOBAL;
(&mutex); // release lock
pthread_mutex_unlock
if (tmp == 99)
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
We can compile and test this with TSan, and again, no data race should be detected.
C11 introduces a “native” type of thread which doesn’t use the
Pthreads API. We can rewrite our program using the new API, which is
found in <threads.h>
.
#include <stdio.h>
#include <threads.h>
int GLOBAL;
;
mtx_t mutex
int operation1(void *x) {
(&mutex); // request lock before accessing shared variable
mtx_lock= 99;
GLOBAL (&mutex); // release lock
mtx_unlockreturn 0;
}
int main(void) {
;
thrd_t t(&mutex, mtx_plain); // Initialize the mutex
mtx_init
(&t, operation1, NULL); // Create a thread
thrd_create
(&mutex); // request lock before accessing shared variable
mtx_lock= 100;
GLOBAL (&mutex); // release lock
mtx_unlock
(t, NULL);
thrd_join
(&mutex); // request lock before accessing shared variable
mtx_lockint global_value = GLOBAL; // Read the value of GLOBAL
(&mutex); // Unlock the mutex after reading the shared variable
mtx_unlock
// Check the value of GLOBAL
if (global_value == 99)
("Exit Success\n");
printfelse
("Exit Failure\n");
printf
(&mutex); // Destroy the mutex
mtx_destroyreturn 0;
}
Unfortunately, the TSan sanitizer may not work with C11 “native” threads – see here.
Recent versions of Ubuntu (10.10 and later) come with a built-in protection against some race condition attacks. Specifically, they mitigate against some symbolic link (symlink) attacks (which we saw in lectures).
In the CITS3007 development environment, we will create a new user
(in addition to the “vagrant
” user we log in as) with their
own home directory:
$ sudo adduser --disabled-password --gecos '' user2
As that user, we’ll create a new file and a symlink to it:
$ sudo su user2 -c 'echo hello > /home/user2/file'
$ sudo su user2 -c 'ln -s /home/user2/file /home/user2/link'
By default, a user’s new files are world readable, so the
vagrant
user can read the file and the symlink:
$ ls -l ~user2
total 4
-rw-rw-r-- 1 user2 user2 6 Sep 27 00:31 file
lrwxrwxrwx 1 user2 user2 16 Sep 27 00:32 link -> /home/user2/file
$ cat /home/user2/file
hello
Note that the permissions of the symlink are “rwx
” for
user, group and the “world” – this is because on Linux, symlinks have no
“permissions” of their own; permissions are taken from the file being
linked to.
As user2
, we’ll try removing “world” permissions from
the symlink:
$ sudo su user2 -c 'chmod o-r /home/user2/link'
Does this make a difference to the permissions of the
link
file, as displayed by ls
? Can the
vagrant
user still access it?
Sample solutions
You should observe that the listed permissions stay exactly the same,
and the vagrant
user can still read the file contents.
Now we’ll try making a symlink again, but putting it in the
/tmp
directory:
$ sudo su user2 -c 'ln -s /home/user2/file /tmp/link'
What happens if you execute the command cat /tmp/link
(as the vagrant user
)?
Sample solutions
You should observe that a “Permission denied” error occurs.
The tmp
directory has special permissions, on Unix-like
systems. Run ls -ld /tmp
, and you should see output like
the following:
$ ls -ld /tmp
drwxrwxrwt 12 root root 4096 Sep 27 00:38 /tmp
The “t
” at the end of the permissions means a permission
bit called the “sticky bit” has been set for the /tmp
directory. When this bit is set on a directory, and some user creates a
file in it, other users (except for the owner of the directory, and of
course root
) are prevented from deleting or renaming the
file.
The sticky bit is set on the /tmp
directory to ensure
one user’s temporary files can’t be renamed or deleted by other users.
In addition to this, the Linux kernel introduced additional protections:
symbolic links in world-writable sticky directories (such as
/tmp
) can only be followed if the follower (i.e.,
the user executing a command) and the directory owner (that is,
root
, in the case of the /tmp
directory) match
the symlink owner.
(Note that these built-in protections are not
sufficient security for safely creating temporary files. It’s usually
best to ensure that only the actual user of a process can even list or
read temporary files: a program should create its own temporary
directory under /tmp
, to which only the actual
user has read, write or execute access, and then create needed temporary
files within that directory.)
This protection can be removed by running the following command, which alters kernel parameters:
$ sudo sysctl -w fs.protected_symlinks=0
If you try the previous exercises again, you should see that this
time, the vagrant
user can run
cat /tmp/link
without a “permission denied” error.
Another protection was added in Ubuntu 20.04: even root cannot write
to files in /tmp
that are owned by others. That can be
disabled by running the following command:
$ sudo sysctl fs.protected_regular=0
In earlier versions of the Linux kernel (for instance, on Ubuntu 12.04), the “symlinks in sticky-bit directories” protection was provided by a Linux security module called “Yama”, and could be disabled using the following command:
$ sudo sysctl -w kernel.yama.protected_sticky_symlinks=0
If you aren’t able to easily run the CITS3007 standard development environment (e.g. because you are using an M-series MacOS computer), and are using an earlier version of Ubuntu instead, then the “yama” version of the command might work instead.
The Linux kernel provides a security framework consisting of various “hooks” which can be used by Linux security modules. For instance, normally in the Linux kernel, read permissions for a file are only checked when a file is opened. However, the security framework provides “file hooks” which allow security modules to specify checks which should be made whenever a read or write is performed on a file descriptor (for example, to revalidate the file permissions in case they have changed).
We will not look in detail at how the security framework and modules work, but if you are interested, the architecture of the framework is described in a 2002 paper, and a guide to some of the modules is provided here.
A list of the currently enabled Linux security modules can be printed by running
$ cat /sys/kernel/security/lsm
In more recent kernels, the “symlinks in sticky-bit directories” protection is built into the kernel.
Consider the following program, append.c
:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
char * filename = "/tmp/XYZ";
char buffer[60];
FILE *fp;
// get user input
("text to append to '%s': ", filename);
printf(stdout);
fflush
("%50s", buffer );
scanf
// does `filename` exist, and can the actual user write
// to it?
if (!access(filename, W_OK)) {
= fopen(filename, "a+");
fp ("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fwrite(fp);
fclose(0);
exit}
("No permission\n");
printf(1);
exit}
It’s intended to be a root-owned setuid program, which takes a string
of input from a user, and appends it to the end of a temporary file
/tmp/XYZ
(if that file exists) – but only if the user who
runs the program would normally have permissions to write to the file.
Because the program runs with root privileges (i.e., has an effective
user ID of 0
), it normally could overwrite any file.
Therefore, the code above uses the access
function
(discussed in lectures) to ensure the actual user running the
program has the correct permissions.
Save the program as append.c
, and compile it with
make append.o append
. Then make it a root-owned setuid
program:
$ sudo chown root:root append
$ sudo chmod u+s append
At first glance the program may not seem to have any problem. However, there is a race condition vulnerability in the program – can you describe what it is? How might an attacker try to exploit this program?
Sample solution
This program has a “TOCTOU” vulnerability, and uses the deprecated
access()
function.
Due to the time window between the file permissions check
(access()
) and the file use (fopen()
), there
is a possibility that the file used by access()
is
different from the file used by fopen()
, even though they
have the same file name /tmp/XYZ
. If a malicious attacker
can somehow make /tmp/XYZ
a symbolic link pointing to a
protected file, such as /etc/passwd
, inside the time
window, the attacker can cause the user input to be appended to
/etc/passwd
and as a result gain root privileges.
How might an attacker exploit this?
If they can alter /etc/passwd
, they could add an extra
line that looks something like this:
sploit:x:0:0::/root:/bin/bash
Here, a new sploit
account is created, which has root
privileges since it has userid 0.
On its own, this is not quite enough, because the attacker still
needs a password to access the new sploit
account.
On Ubuntu systems, however, a particular “magic” password value is
used for [passwordless guest accounts][guest], and the magic value is
U6aMy0wojraho
(the 6th character is zero, not letter O). If
the attacker puts this value in the password field of a user entry, they
can just hit the return key when prompted for a password, and then can
log into the user’s account.
The attacker would still need to unlink the /tmp/XYZ
file and replace it with a symlink to /etc/passwd
in the
narrow time window between the “check” (access()
) and use
of the file. Typically, they would do so by running append
many times,
Would the ThreadSanitizer help in detecting this problem? Why or why not?
Sample solution
It would not. TSan assists in detecting data races, which are to do with multithreaded access to a program variable (where at least one thread performs a write).
But the race condition here is not a data race – the “resource” being contended for is not a variable in a program, but a file on the file system. TSan cannot assist us in preventing such vulnerabilities.