Skip to main content Link Search Menu Expand Document (external link)

50.005 Computer System Engineering
Information Systems Technology and Design
Singapore University of Technology and Design
Natalie Agus (Summer 2024)

Time of Check, Time of Use Attack

In this lab, you are tasked to investigate a program with TOCTOU (Time of Check, Time of Use) race-condition vulnerability.

A “Time of Check to Time of Use” (TOCTOU) attack is a type of cybersecurity vulnerability that arises when a system checks the state of a resource or condition at one point in time, but uses that resource or relies on that condition at a later time. During the gap between the “check” and the “use,” an attacker can manipulate the resource or condition, leading to unauthorized access, data corruption, or other security breaches.

The lab is written entirely in C, and it is more of an investigative lab with fewer coding components as opposed to our previous lab. At the end of this lab, you should be able to:

  • Understand what is a TOUTOU bug and why is it prone to attacks
  • Detect race-condition caused by the TOCTOU bug
  • Provide a fix to this TOCTOU vulnerability
  • Examine file permissions and modify them
  • Understand the concept of privileged programs: user level vs root level
  • Compile programs and make documents with different privilege level
  • Understand how sudo works
  • Understand the difference between symbolic and hard links

Background

Race Condition

A race condition occurs when two or more threads (or processes) access and perform non-atomic operations on a shared variable (be it across threads or processes) value at the same time. Since we cannot control the order of execution, we can say that the threads / processes race to modify the value of the shared variable.

The final value of the shared variable therefore can be non deterministic, depending on the particular order in which the access takes place. In other words, the cause of race condition is due to the fact that the function performed on the shared variable is non-atomic.

In this lab we are going to exploit a program that is vulnerable to race-condition.

  • The program alone is single-threaded, so in the absence of an attacker, there’s nothing wrong with jthe program.
  • The program however is vulnerable because an attacker can exploit the fact that the program can be subjected to race-condition.

You will learn about race condition properly in future lectures. This lab is just a preview.

Setup

Installation

Clone the files for this lab using the command:

git clone https://github.com/natalieagus/lab_toctou

You should find that the following files are given to you:

Now go to the User/ directory and call make. You should find these files in the end.

Do NOT used a shared drive with your Host machine, or a shared external drive with other OS. You will NOT be able to create root files in this case. Clone the above file in /home/<user> directory instead.

Login as Root User

Task 1

TASK 1: To switch user and login as root, you must first set the password for the root account.

Type the following command:

sudo passwd root

You will be prompted for your current user password, then set a new password for root.

Remember this root password! You need this later to switch to root user. If you have a brainblock, use a simple phrase like: rotiprata.

Follow the instructions, and then switch to the root user using the command and the password you just created above for root:

su root

You will see the following new prompt, indicating now you’re logged in as root:

Root user is the user with the highest (administrative) privilege. It has nothing to do with Kernel Mode. Processes spawned while logged in as Root still runs on User Mode.

Task 2

TASK 2: Create files while logged in as root.

While logged in as root, navigate to /FilesForRoot, and type make. You should see a new directory called Root/ created with the following contents:

It is important to check that the newly created files belong to the user root as shown in yellow above (beside the date is the owner of the file).

Add other Users

Task 3

TASK 3: Create 2-3 other users.

In order to proceed with this lab, you need to login as root and create a few new users before proceeding.

For the first user, do the following (the username must be test-user-0):

adduser test-user-0

Give it any password you like (preferably a good one, like LDcwzD&#6JKr), and then add it to the sudo group:

adduser test-user-0 sudo

Then add a few more with different names: e.g test-user-0, test-user-1, with any appropriate password of your choice.

Switch User

Task 4

TASK 4: Switch account as any of these new users to ensure that you’ve created new users successfully.

You can switch to your new user by using the command su <username>:

Once you’re done, switch back to root again using your root password you’ve set above.

Task 5

TASK 5: Check /etc/shadow.

Once you’re done, you should enter the command cat /etc/shadow and see that your newly created users are at the bottom of the file, with some hash values (actual values are different depending on the password you set for these users).

Task 6

TASK 6: Return to your original user account.

You can switch back to your original normal user account by using the same su <username> command:

Now go up one level by typing cd .. and attempt to delete Root/ directory while logged in as your original username. You will find such permission denied message:

🔎 What happened?

File Permission

The reason you faced the permission denied error is because your username doesn’t have the correct permission to edit the Root/ directory.

Notice that a directory is an executable, indicated by the ‘x’ symbol.

The SUID bit

SUID stand for Set Owner User ID. A file with SUID set always executes as the user who owns the file, regardless of the user passing the command.

List the file permission for all files inside Root/ directory:

Notice that instead of an x, we have an s type of permission listed on the two progs: vulnerable_root_prog and rootdo. This is called the SUID bit.

The SUID bit allows normal user to gain elevated privilege (again this does NOT mean Kernel Mode, just privilege levels among regular users) when executing this program.

If a normal user executes this program, this program runs in root privileges (basically, the creator of the program)

Task 7

TASK 7: Reading protected file using regular user account.

While logged in as your original user account, try to read the file /etc/shadow:

cat /etc/shadow

You will be met with permission denied because this file can only be read by root user, and other users in the same group, as shown in the file details below;

What group does root belong to? What about the user account in question (ubuntu in example above)? You can find out using the command groups:

Task 8

TASK 8: Gain privilege elevation.

Now, run the following command. We assume that your current working directory is at /lab_toctou directory. If not, please adjust accordingly.

./Root/rootdo cat /etc/shadow

When prompted, type the word password, and then press enter.

You will find that you can now read this file:

The reason you can now successfully read the file /etc/shadow is because rootdo has the SUID bit. Any other program that is executed by rootdo will run with root (rootdo creator) privileges and not the regular user.

You can open rootdo.c inside /lab_toctou/FilesForRoot/ to examine how it works, especially this part where it just simply checks that you have keyed in password and proceed to execvp (execute) the input command:

    if (!strcmp(password, "password"))
    { // on success, returns 0
        printf("Login granted\n");
        int pid = fork();
        if (pid == 0)
        {
            printf("Fork success\n");
            wait(NULL);
            printf("Children returned\n");
        }
        else
        {
            if (execvp(execName, argv_new) == -1)
            {
                perror("Executable not found\n");
            }
        }
    }

In short, as the SUID bit of rootdo program is set, it always runs with root privileges regardless of which user executes the program.

While rootdo seems like a dangerous program, don’t forget that the root itself was the one who made it and set the SUID bit in the first place, so yes it is indeed meant to run that way.

The sudo Command

sudo is a command in Unix and Linux-based operating systems. It stands for “superuser do” and allows a permitted user to execute a command as the superuser or another user, as specified in the sudoers file (/etc/sudoers). This provides a mechanism for granting administrator privileges, ordinarily reserved for the root user, to normal users in a controlled manner.

When you typo sudo <command>, it prompts you for your password, then the program checks whether the user is verified, before executing with root privileges. In our little rootdo example, we just use hardcoded password to proceed.

Setting of rootdo’s SUID bit is done in the makefile inside FilesForRoot that you execute earlier while logged in as root,

# refresh the root files and root log file
# Set UID for rootprog and rootdo
setup:
	chmod u+s ../Root/vulnerable_root_prog
	chmod u+s ../Root/rootdo

The command chmod u+s filename sets the SUID bit of that filename.

About sudo

sudo is not a built-in shell command. It is an external command provided by a package that is installed on most Unix and Linux-based systems. Built-in commands are part of the shell itself, like cd (change directory), echo, or exit, and they are executed directly by the shell without the need for calling an external program. You will be tasked to implement both built-in command and external command in Programming Assignment 1.

Running a command with sudo has no direct relationship with switching the CPU to Kernel/Machine mode.

The Vulnerable Program

Our vulnerable program can be found in /Root/vulnerable_root_prog. Open /FilesForRoot/vulnerable_root_prog.c to find out what it does.

The program expects two arguments: to be stored at char *fileName, and char *match. It is a supposedly secure program that will allow root to replace *match string inside *fileName with an SHA-512 hashed password 00000.

    if (argc < 3)
    {
        printf("ERROR, no file supplied, and no username supplied. Exiting now.\n");
        return 0;
    }

    char *fileName = argv[1];
    char *match = argv[2];
    FILE *fileHandler;

    ....

It will then check with the access system call on whether or not the caller of this program (it’s REAL ID) has permission to access the target fileName:

    if (!access(fileName, W_OK))
    {
        printf("Access Granted \n");
        ....

    }

Below we paste the documentation for access() from Linux man page:

access() checks whether the calling process can access the file pathname. If pathname is a symbolic link, it is dereferenced.

The check is done using the calling process’s real UID and GID, rather than the effective IDs as is done when actually attempting an operation (e.g., open, fopen, execvp, etc) on the file. Similarly, for the root user, the check uses the set of permitted capabilities rather than the set of effective capabilities; and for non-root users, the check uses an empty set of capabilities.

This allows set-user-ID programs and capability-endowed programs to easily determine the invoking user’s authority. In other words, access() does not answer the “can I read/write/execute this file?” question. It answers a slightly different question: “(assuming I’m a setuid binary) can the ACTUAL user who invoked me read/write/execute this file?”, which gives set-user-ID programs the possibility to prevent malicious users from causing them to read files which users shouldn’t be able to read.”

Other system calls: execvp, open that we used in rootdo or standard sudo only checks the effective ID of the calling process, not the real ID.

Details

rootdo runs with effective root privileges (effective, not real, since the caller to rootdo is only normal user), and that’s enough to run the cat /etc/shadow program since cat doesn’t utilise access() to check for the calling process.

On the other hand, vulnerable_root_prog tries to be more secure by using the access system call to prevent users with elevated privileges to modify files that do not belong to them.

vulnerable_root_prog “security” attempt fails miserably and ends up being susceptible to a particular race condition attack due this weakness called TOCTOU (time-of-check time-of-update)!

The TOCTOU Bug

The time-of-check to time-of-use (often abbreviated asTOCTOU, TOCTTOU or TOC/TOU) is a class of software bug caused by a race condition involving:

  • The checking of the state of a part of a system (such as this check in vulnerable_root_prog using access),
  • And the actual use of the results of that check

We exaggerate the DELAY between:

  1. The time of CHECK of the file using access and
  2. The time of USE (actual usage of the file) using fopen by setting sleep(DELAY) in between the two instructions, where DELAY is specified as 1 to simulate 1 second delay.
...
    if (!access(fileName, W_OK))
    {
        printf("Access Granted \n");
        /*Simulating the Delay*/
        sleep(DELAY); // sleep for 1 sec, exaggerate delay
        ...

        int byte_index = 0;
        int previous_byte_index = 0;
        int byte_match = -1;

        FILE *fp = fopen(fileName, "r+");
        ...
    }

Consider the vulnerable_root_prog being called by a user to modify a text file belonging to the user account as such:

The access() check of course grants the normal user caller to modify userfile.txt because indeed it belongs to the normal user (ubuntu in the screenshot above).

The output doesn’t make sense as of now, but we will explain what those hash values are about really soon. The vulnerable_root_prog does two things:

  1. Accepts a fileName and a string inside that file to match.
  2. It will replace that string inside the file with the following hash value if access() system call grants permission.
$6$jPSpZ3iS84semtGU$DLwyTleAM2Of8NzDrwwNTnuSamJlnTx6NlMgbhPT5L8POT/J1MSCPucOAp1Qt3zRClS2NWT.RksROF9R1XLrn0

We will soon exploit this bug with symbolic link.

Symbolic Link

A symbolic link is a special kind of file that points to (reference) another file, much like a shortcut in Windows or a Macintosh alias. It contains a text string that is automatically interpreted and followed by the operating system as a path to another file or directory. You will learn more about this in the later weeks of lecture.

Task 9

TASK 9: Creating symbolic link.

We can create a text file with the following command and output redirection:

echo "good morning" > goodmorning.txt

Then we can create a symbolic link using the command:

ln -s <source> <symlink>

In this example below, we created a goodmorning_symlink.txt that points to the actual file goodmorning.txt:

There are different terminologies for ln. POSIX states target --> source while GNU states link_name --> target, and hence the word target alone can mean either the symlink or the actual file depending on which manual you read. Be careful!

During this delay between checking (with access) and usage (with fopen):

  1. A malicious attacker can replace the actual file text.txt into a symbolic link pointing to a protected file, e.g: /etc/shadow
  2. Since fopen only checks effective user ID, and vulnerable_root_prog has its SUID bit set (runs effectively as root despite being called by only normal user), the “supposedly secure” rootprog can end up allowing normal user to gain elevated privileges to MODIFY protected file like /etc/shadow.

In the screenshot below, we created a symbolic link userfile.txt to point to /etc/shadow, resulting in the regular user being unable to cat userfile.txt.

We have written the command to create the symbolic link for you. It is inside /User/exploit.sh.

Race Condition

The malicious attacker has to attack and can only successfully launch the attack (modifying userfile.txt -> /etc/shadow) during that time window between time-of-check (access) and time-of-use (fopen), hence the term “race condition vulnerability attack” or “a bug caused by race condition”.

The attacker has to RACE with the vulnerable_root_prog to quickly change the userfile.txt into a symbolic link pointing to /etc/shadow. This can happen ONLY on this very specific time window of AFTER the access() check and BEFORE the fopen().

The Attack

Modify /etc/shadow

We will use this TOCTOU bug in vulnerable_root_prog to modify the content of the file: /etc/shadow.

/etc/shadow is shadow password file; a system file in Linux that stores hashed user passwords and is accessible only to the root user, preventing unauthorized users or malicious actors from breaking into the system.

Try reading it using sudo cat /etc/shadow. You will find that the lines have the following format:

usernm:$y$Apj1GQn.U$ZWWEtt19A8:17736:0:99999:7:::
[----] [---------------------] [---] - [---] ----
|                 |              |   |   |   |||+-----------> 9. Unused
|                 |              |   |   |   ||+------------> 8. Expiration date
|                 |              |   |   |   |+-------------> 7. Inactivity period
|                 |              |   |   |   +--------------> 6. Warning period
|                 |              |   |   +------------------> 5. Maximum password age
|                 |              |   +----------------------> 4. Minimum password age
|                 |              +--------------------------> 3. Last password change
|                 +-----------------------------------------> 2. Hashed Password
+-----------------------------------------------------------> 1. Username

Pay attention to segment 2

It contains the password of the usernm using the $type$salt$hashed format. 1.$type is the method cryptographic hash algorithm and can have the following values:

  • $1$ – MD5
  • $2a$ – Blowfish
  • $y$ – yescrypt
  • $5$ – SHA-256
  • $6$ – SHA-512

If the hashed password field contains an asterisk (*) or exclamation point (!), the user will not be able to login to the system using password authentication. Other login methods like key-based authentication or switching to the user are still allowed (out of scope). You can read more about the file here but it’s not crucial. Let’s move on.

Replacing Hashed Password in /etc/shadow

We aim to:

  1. Replace the targeted username entry in /etc/shadow with a new Hashed Password section
  2. So that we can login to this targeted username using preset password 00000 (five zeroes), effectively overriding the password you set earlier.
  3. “We” being regular user account (NOT root!) but with the help of this TOCTOU bug from vulnerable_root_prog.

These targeted username is none other than test-user-0 that you have created earlier.

Task 10

TASK 10: Launch attack that does the 3 things above by running exploit.sh.

While logged in as a regular user (your original username), change your directory to /User/ and run the script exploit.sh:

cd User
./exploit.sh

The program will run for awhile (20-30 secs) and eventually stop with such message:

If you do a cat /etc/shadow right now, notice how test-user-0 account hashed password section has been changed to match that replacement_text in vulnerable_root_prog:

Login to target user account

Task 11

TASK : Login to user account test-user-0 with password 00000

Now you can login to test-user-0 account with password: 00000 instead of your originally set password:

We have successfully changed a supposedly protected /etc/shadow file while logged in as a regular user (ubuntu in the example above).

What Happened?

Open exploit.sh file inside /User/ directory:

#!/bin/sh
# exploit.sh

# note the backtick ` means assigning a command to a variable
OLDFILE=`ls -l /etc/shadow`
NEWFILE=`ls -l /etc/shadow`

# continue until THE ROOT_FILE.txt is changed
while [ "$OLDFILE" = "$NEWFILE" ]
do
    rm -f userfile.txt
    # create userfile again
    cp userfile_original.txt userfile.txt

    # the following is done simultanously
    # if a command is terminated by the control operator &, the shell executes the command in the background in a subshell.
    # the shell does not wait for the command to finish, and the return status is 0.
    # on the other hand, commands separated by a ; are executed sequentially; the shell waits for each command to terminate in turn.
    # the return status is the exit status of the last command executed.
    ../Root/vulnerable_root_prog userfile.txt test-user-0 & ln -sf /etc/shadow userfile.txt & NEWFILE=`ls -l /etc/shadow`

done

echo "SUCCESS! The root file has been changed"

The first two lines create a variable called OLDFILE and NEWFILE, containing the file information of /etc/shadow.

OLDFILE=`ls -l /etc/shadow`
NEWFILE=`ls -l /etc/shadow`

Then, the main loop is repeated until /etc/shadow is successfully changed (different timestamp and size):

  1. Remove existing userfile.txt if any (from previous loop)
  2. Then, create userfile.txt (this line can be replaced by any other instructions that create this userfile.txt)
  3. Then, runs 3 commands in succession:
    1. ../Root/vulnerable_root_prog userfile.txt test-user-0: runs the vulnerable program with userfile.txt, belonging to currently logged in user account and the targeted username.
    2. ln -sf /etc/shadow userfile.txt: immediately, fork and execute the ln program to change userfile.txt to point to /etc/shadow. Remember from Lab 1 that a shell spawns a new child process upon execution of commands.
    3. NEWFILE=ls -l /etc/shadow: check the file **info** (not <span class="orange-bold">content</span>!) of /etc/shadowand store it into variable NEWFILE`; to be used in the next loop check
  4. It terminates the moment the metadata of the new file NEWFILE differs from OLDFILE: this means /etc/shadow has been recently modified which indicates that our attack is successful (assuming no other process is currently changing /etc/shadow)

Two processes from step (3.1) and (3.2) above are racing, and the script terminates when /etc/shadow has been successfully changed.

Preventing the TOCTOU Bug

Preventing TOCTOU vulnerabilities involves strategies that either minimize the window between the “check” and the “use” or avoid the need for separate check and use steps altogether. Here are some general strategies for mitigating TOCTOU bugs:

  1. Using Atomic operations: Atomicity ensures that a sequence of operations is completed in a single step from the perspective of other tasks. This approach is effective because it inherently eliminates the gap between the “check” (verification) and the “use” (action) phases of an operation, which is where the vulnerability would typically be exploited.
  2. File Locking: Use file locking mechanisms where appropriate. When a file is locked, other processes are prevented from modifying it until the lock is released. However, file locking can introduce its own complexities and is not universally supported across all filesystems and platforms.
  3. Avoid unnecessary checks: Sometimes, the best way to avoid TOCTOU vulnerabilities is to avoid unnecessary checks. If you can perform an operation directly without checking first, especially if the operation is atomic or fails safely if preconditions are not met, you can often sidestep TOCTOU issues.
  4. Privilege management strategies: These strategies focus on minimizing the risk and potential impact of TOCTOU vulnerabilities by controlling the privilege levels at which operations are performed, rather than directly eliminating the timing window between checking and using a resource.

Here we look at two trivial variations of approach (4) above:

seteuid()

One of the ways to patch this TOCTOU bug is to add just one line of instruction after access() (before fopen() is called) to manually set the effective UID of the process as the actual UID of the process.

This approach is more about privilege management rather than avoiding checks per se.

You can do this using the following system call: seteuid(getuid());

Task 12

TASK 12: Modify vulnerable_root_prog.c to add that one line of code above right under the following if-clause.

if (!access(fileName, W_OK))
{
   ...

Then follow these steps:

  1. Login as root and recompile with make inside /FilesForRoot/.
  2. Login back as your original user account, and cd to /User/ again, and run exploit.sh.
  3. Observe the result
  4. Press ctrl+c to cancel the script (yes, this change will cause step 2 to run in infinite loop).

Disable SUID

Of course another way is to disable the SUID bit of vulnerable_root_prog altogether, however in practice sometimes this might not be ideal since there might be other parts of the program that temporarily requires execution with elevated privilege.

This is more of a preventative measure to limit the potential impact of vulnerabilities rather than a method to avoid unnecessary checks in the context of TOCTOU vulnerabilities.

Task 13

TASK 13: Run exploit.sh with root_prog_nosuid.

Open exploit.sh and replace vulnerable_root_prog with root_prog_nosuid, and run the script again (while logged in as user account).

Summary

Ensure that you have answered all questions in edimension corresponding to each task in this handout. No other separate code submission is needed.

By the end of this lab, we hope that you have learned:

  • What SUID bit does, and how can it be utilised to gain elevated privileges to access protected files
  • The differences between root and normal user
  • The meaning of file permission. Although we do not go through explicitly on how it is set, you can read about it here and experiment how to do it using the chmod command.
  • How race condition happens and how it can be used as an attack
  • How to fix (patch) the TOCTOU bug