UNIX Setuid & LD_PRELOAD
Computer
Security Lecture, Dr.
Lawlor
On UNIX systems, a common way to provide user-level access to
system-level functionality is by making a setuid executable.
For example, on most systems sudo is setuid to give authorized users
a way to become root, the ping program is setuid so it can fabricate
ICMP packets, /bin/mount is setuid so it can mount filesystems
explicitly allowed in /etc/fstab for normal users, etc.
The mechanics of setuid are:
- Make an executable. (It can't be a script; you'd need to
mark the interpreter setuid instead.)
gcc danger.c -o danger
- Change the ownership of the executable to root (or another
powerful user account)
sudo chown root:root danger
- Set the setuid mode bit on the executable file:
sudo chmod u+s danger
Here "u+s" sets the User Setuid bit. "+4000" is the equivalent
octal.
- When the executable runs, make the syscall "setuid(0);" to
gain root privileges. Call setuid again to permanently
give up those privileges.
// Become root
if (0!=setuid(0)) { perror("problem becoming root (is exe setuid?)"); }
// Do powerful stuff
if (0!=chroot("/tmp/wacky")) { perror("problem with chroot"); }
// Give up power
if (0!=setuid(wackyuser)) { perror("problem giving up rights"); exit(1); }
// do less powerful stuff
Here's some C code that shows the user ID before and after calling
setuid:
/**
Demonstrate setuid before, during, and after root switch.
Dr. Orion Lawlor, lawlor@alaska.edu, 2017-11-01 (Public Domain)
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void print_ids(const char *where) {
uid_t uid=getuid();
uid_t euid=geteuid();
printf(" %s: uid=%d euid=%d\n",
where, (int)uid, (int)euid);
}
int main() {
print_ids("Startup");
printf("setuid(0) returns %d: becoming root\n", setuid(0));
print_ids("As root");
printf("setuid(1000) returns %d: becoming user\n", setuid(1000));
print_ids("As user");
printf("setuid(0) returns %d: becoming root again\n", setuid(0));
print_ids("Back to root");
return 0;
}
On my machine, where my ID=1000, when I run this code as a normal user none of the setuid(0) calls work, because I'm not root:
Startup: uid=1000 euid=1000
setuid(0) returns -1: becoming root
As root: uid=1000 euid=1000
setuid(1000) returns 0: becoming user
As user: uid=1000 euid=1000
setuid(0) returns -1: becoming root again
Back to root: uid=1000 euid=1000
If I make the executable setuid (using the chown and chmod calls above), the first setuid(0) works, and I do become root. But once I change back to a user, I can't become root again:
Startup: uid=1000 euid=0
setuid(0) returns 0: becoming root
As root: uid=0 euid=0
setuid(1000) returns 0: becoming user
As user: uid=1000 euid=1000
setuid(0) returns -1: becoming root again
Back to root: uid=1000 euid=1000
(If Bash sees it's running with setuid, it automatically does a setuid(getuid()) to give up privileges unless you pass the "-p" flag.)
Setuid is very handy for managing permissions on secure systems, but it can be error prone--an unsecured setuid executable can act as a system backdoor for somebody that knows how to manipulate it, and there are hundreds of CVEs for setuid programs.
There are many things the system does to try to protect setuid executables:
- You cannot debug or trace a setuid executable, because you'd be directly manipulating code owned by root.
- LD_PRELOAD, LD_LIBRARY_PATH, and other shared library variables do not apply to setuid executables ("secure execution mode").
There are still many things an attacker can do to manipulate a setuid executable:
- The running user controls the environment variables, in particular things like PATH or IFS (see attacks & countermeasures for environment variables in setuid programs). Environment variables could cause buffer overflows or weird new behavior deep inside a setuid program.
- The running user can configure weird situations like where ~/.ssh/known_hosts is a softlink to /etc/shadow, so a setuid ssh manages to overwrite the root password. It's also difficult to atomically check for this sort of weirdness, and the usual "check, then write" code makes a timing attack possible, where the user keeps flipping back and forth between normal and weird files, and keeps trying until the check passes on the normal file, then the write proceeds and clobbers the weird file.
It's common to audit systems for setuid executables, using a command like:
sudo find / -perm -u+s
In the long run, setuid is probably the wrong approach, and on Linux the finer-grained capabilities are the recommended replacement.
LD_PRELOAD Call Interception
The environment variable LD_PRELOAD specifies a shared library that gets loaded before any other library, for every executed program. Inside the preloaded library, you can intercept any library calls; for example, here we intercept the setlocale function from <locale.h> by defining our own copy.
/**
Simple shared library intercept:
gcc -fPIC -shared preload.c -o preload.so
export LD_PRELOAD=preload.so
*/
#include <stdio.h>
#include <stdlib.h>
char *setlocale(int category, const char *locale)
{
puts("ALL YOUR BASE ARE BELONG TO US");
return NULL; /* "the request cannot be honored" */
}
Here we're returning NULL as a do-nothing locale, because a non-NULL value gets passed on to the real locale functions and will make some programs crash. (We could also use dlsym(RTLD_NEXT,"setlocale") to forward the request on to the real setlocale.)
We can now compile this code into a shared library, preload the library using LD_PRELOAD, and from that point on, anything run in this shell will have that banner:
gcc -fPIC -shared preload.c -o preload.so
export LD_PRELOAD=`pwd`/preload.so
ls
ALL YOUR BASE ARE BELONG TO US
preload.c preload.so
Several common commands, like echo, pwd, export, and cd, don't run
LD_PRELOAD because they aren't separate programs, they're shell builtin
commands. Other programs don't display anything because they never call setlocale.
LD_PRELOAD is a common way to debug bad shared library paths, inject code into unsuspecting applications, build userland rootkits, or to build a sandbox to intercept bad library calls. The configuration file /etc/ld.so.preload does the same thing on a system-wide basis.
The useful tools "ltrace" and "strace" will show every shared library call, and every kernel syscall, made by an arbitrary program. This is very handy when doing weird things like repackaging programs to live inside sandboxes.