Safe Penetration Testing Labs · advanced · ~25 min

Auditing setuid programs

Run a defensive checklist over any setuid binary.

Overview

setuid programs run with the file owner's privileges. Even a tiny bug can hand an attacker root. The audit is a checklist; everything not on the list is a finding.

Why it matters

When you find a setuid binary on a target system during an authorised assessment, you read its source (or disassemble it) and run the checklist. Every miss is a privilege-escalation candidate.

Core concepts

The 5-item checklist:

  1. Environment scrubbed? A setuid binary that trusts $PATH, $LD_PRELOAD, $LD_LIBRARY_PATH, $IFS, or $ENV is owned. Always clearenv then set known-safe values.
  2. fds 0, 1, 2 open? Always. If they're closed, the next open returns 0 or 1 or 2 — and the next printf writes into your data file.
  3. getuid() vs geteuid()? Both matter. geteuid is the elevated id; getuid is the caller. Auth decisions on the wrong one are a CVE.
  4. No file races? Per TOCTOU and symlink lessons. Every path operation must use openat + O_NOFOLLOW or realpath + prefix-check.
  5. No external commands? system(3) and popen(3) inherit environment. If you must spawn, execve with a sanitised envp.

Pentester mindset. Walk the list explicitly. Each "yes" is a finding worth a separate write-up.

Syntax notes

uid_t getuid(void);          /* real uid — caller */
uid_t geteuid(void);         /* effective uid — privileges in force */
int   setresuid(uid_t r, uid_t e, uid_t s);   /* drop forever */
int   clearenv(void);

Lesson

A setuid binary runs with the file owner's privileges, not the caller's. Setuid root binaries are tiny portals into root. Auditing them is a focused checklist: PATH inheritance, env scrubbing, fd leaks, file-race patterns, and the getuid()/geteuid() distinction.

Code examples

/* Defensive setuid program start-up: */
if (clearenv() != 0) exit(1);                /* strip ALL env first */
setenv("PATH", "/usr/bin:/bin", 1);          /* known-safe PATH */
if (setresuid(geteuid(), geteuid(), geteuid()) < 0) exit(1);

Line by line

/* Drop privs entirely the moment the elevated work is done */
uid_t real = getuid();
if (setresuid(real, real, real) < 0) {
    perror("setresuid"); exit(1);
}
/* Now we run as the original caller — much less attack surface */

Common mistakes

  • Trusting argv[0] for the program name.

Debugging tips

ls -l /usr/bin/sudo shows -rwsr-xr-x — the s is the setuid bit. find / -perm -4000 -type f 2>/dev/null lists them all on a system.

Memory safety

On top of the checklist, the same memory-safety rules apply: bounded copies, validated input, etc.

Real-world uses

sudo, su, ping (historically), mount (historically), passwd. Each ships with the audit baked in; review them for the patterns.

Practice tasks

  1. Walk the 5-item checklist on /usr/bin/sudo's source (open-source). 2. Build a tiny setuid program, walk the checklist on YOUR code. 3. Demonstrate why clearenv matters with LD_PRELOAD.

Summary

5 items: env, fds, uids, races, child processes. Every miss is a privilege-escalation candidate.

Practice with these exercises