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:
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:
There are still many things an attacker can do to manipulate a setuid executable:

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.