setuid, res user IDs, exec and ptrace

May 2018

I have been doing stuff on and off with ptrace for a while now. I know that certain rules exist regarding ptrace and setuid binaries, but all sources I found on the internet are vague. Furthermore, the exec family of functions also have their rules when it comes to setuid binaries. This article aims to explain how the real and effective user IDs get along with both exec functions and ptrace.

All code referenced throughout this article is available here. The project consists of a Makefile and two C source files, test.c and runner.c. The Makefile compiles the two sources, sets the setuid bit for the test binary and changes its owner to be root. The two latter commands require root privileges.

I highly recommend reading the code, making changes, and trying things out by yourself. I tried to explain things in the order I thought best, however you will undoubtedly understand much more from running the code.

Real, effective and saved user IDs

During execution, a UNIX process has three user IDs associated with it: real, effective and saved. A fourth file-system user ID is present only on linux. A fairly good description of these user IDs is given on the "User identifier" wikipedia page.

In linux, one can get the values of the three common user IDs via a call to getresuid. See the provided test.c file for example usage. The print_resuid function will display the three user ID values.

When a file is created from a setuid binary, the owner of the file is equal to the effective user ID of the process. In test.c, uncomment line 61 and find that the owner of the newly created file_owner_test is root.

When a process from a setuid binary attempts to open a file, the privileges of the effective user ID are verified. In test.c, uncomment line 66 to open the secret.txt file. Notice that this file is owned and readable only by root.

The setuid bit

The setuid bit is a flag which allows a user to execute a file with the permissions of the file owner. The flag can be set using the chmod command. From the manual page of the command:

A numeric mode is from one to four octal digits (0-7), derived by adding up the bits with values 4, 2, and 1. Omitted digits are assumed to be leading zeros. The first digit selects the set user ID (4) and set group ID (2) and restricted deletion or sticky (1) attributes. The second digit selects permissions for the user who owns the file: read (4), write (2), and execute (1); the third selects permissions for other users in the file's group, with the same values; and the fourth for other users not in the file's group, with the same values.

If a process is started from a setuid binary, then the effective user ID is set to that of the file owner (usually root). If the binary is not setuid, the the effective user ID is equal to the process parent real user ID.

exec from a setuid binary

Regardless of the setuid bit, when a new process is started (i.e. via the exec family of functions), the real user ID is set to that of the parent real user ID, and the saved user ID equal to the effective user ID.

This can be tested by uncommenting lines 78 and 84 in test.c. Line 78 set the real user ID to be equal to the effective user ID. Line 84 performs an execl call to start bash. If line 78 is uncommented, then the bash instance has the real user ID set to root. Otherwise, it has the real user ID set to a normal user. Within bash, echoing the $UID prints the real user ID, and echoing $EUID prints the effective user ID.

ptrace

All the explanations so far have been with ptrace out of the picture. When ptrace is thrown into the mix, new rules appear. With ptrace, two things need to be taken into considerations. First, who starts the tracing (i.e. either the parent of the setuid process, or the setuid process itself). Second, if the setuid process starts the tracing, then is it performed before or after the call to an exec on a setuid binary.

Tracing can be started in two ways. The first, is via PTRACE_TRACEME, used solely by the tracee (i.e. the process which is traced). Once PTRACE_TRACEME is used, the tracee is immediately traced by its parent. This can be tested by uncommenting lines 41 and 42 in runner.c. The second way to start tracing is via PTRACE_ATTACH, used by the tracer.

In the context of a setuid binary, using PTRACE_TRACEME before exec will result in the process acting as if the setuid bit is not set (i.e. the effective user ID is the same as the real user ID). This can be tested by uncommenting line 41 in runner.c and checking the effective user ID of the test process. Otherwise, if PTRACE_TRACEME is used after the call to exec (so by the setuid binary), then ptrace will work only if the parent process has the effective user ID the same as the tracee real user ID. This can be tested by uncommenting lines 71 and 72 in test.c.

If PTRACE_ATTACH is used, then ptrace will work (again) only if the tracer effective ID is the same as the tracee real user ID.