The ELF format is pretty common across various unix versions, having superseded previous binary formats such as a.out and COFF. Pretty much, today, if you see a unix binary then it’s probably ELF format.
One of features of the ELF format is that the run time linker can be
smart about how it resolves dependencies, and this smartness can be
tuned. A typical tuning many people know is the LD_LIBRARY_PATH
variable, which
can be used to add new directories to be searched for the needed libraries.
Another one, the focus of this entry, is LD_PRELOAD
. This can
be used to force a library to be loaded and this can be used to
alter the way the standard libc calls are processed.
This entry may get a little more technical than normal for this blog; I just felt the need to post some code! I’ll present three unusual use cases:
The snowflake
In small companies or self-managed departments we typically have usernames
that are easy to use. So my login may be sweh
or sweharris
or
stephen
or similar. This is nice for humans, and makes it easy; an
ls
or a ps
will show the username and the human looking at it will
know what person is really being referenced. Unfortunately this isn’t
scalable (what if you have two people named “Stephen”). Larger companies
tend to give people generated names. The login name may be random
or refer to the employee ID. Now xyz123ab
or x123456
doesn’t
really help.
I had to deal with a manager who ran servers that were being migrated
from “self-managed” into the corporate management tool. Of course this
meant the “fred” account now had to become “z987654” and he was angry.
He demanded to be a unique and special snowflake and keep human friendly
names. We weren’t going to do this, but it gave me an idea for a joke.
I could create a snowflake.so
library that could be pre-loaded. It
would override the normal getpwuid()
call and rewrite the results.
Ultimately, after the Managing Director told him to shut up, he gave up.
Now a full version of the code had configuration files, but here is a simple variant; it will replace the current user’s username with a variable:
/* This routine intercepts getwpuid()
* It will call the original, and if the called
* uid == getuid() and $OVERRIDE_USER is set then
* replaces the username with $OVERRIDE_USER
*
* gcc -o getpwuid_modify.so -fPIC -shared getpwuid_modify.c -ldl
* export LD_PRELOAD=$PWD/getpwuid_modify.so
* export OVERRIDE_USER="dummy_username"
* run the app
* unset LD_PRELOAD
*/
#define _GNU_SOURCE
#include <sys/types.h>
#include <pwd.h>
#include <stdlib.h>
#include <dlfcn.h>
struct passwd *getpwuid(uid_t uid)
{
static void * (*func)();
static uid_t my_uid;
static char *override;
struct passwd *p;
if(!func)
{
func = (void *(*)()) dlsym(RTLD_NEXT, "getpwuid");
my_uid = getuid();
override = getenv("OVERRIDE_USER");
}
p=func(uid);
if (p && uid == my_uid && override)
p->pw_name=override;
return(p);
}
What this code does is intercept the getpwuid
function. It then looks
for the original function and calls that, then manipulates the result.
To the calling program we just get a different string:
$ gcc -o getpwuid_modify.so -fPIC -shared getpwuid_modify.c -ldl
$ export LD_PRELOAD=$PWD/getpwuid_modify.so
$ whoami
sweh
$ OVERRIDE_USER=root whoami
root
Let’s hope there’s no programs that test the result of whoami
to
determine what functions are allowed!
Shifting time
Recently, on twitter, a friend asked if there was a good way of setting his computer clock 5 minutes fast. I guess he’s one of these people who needs his alarm to start 5 minutes early. Now you don’t want to play with the system clock because NTP would fight you. The normal way would be to create your own unique tzdata entry (easily based off your local timezone). This will work for most people because their TZ rules don’t change often.
Or you can cheat and override the library calls :-)
/* This routine intercepts time() and clock_gettime() adds 300 seconds.
*
* gcc -o time_modify.so -fPIC -shared time_modify.c -ldl
* export LD_PRELOAD=$PWD/time_modify.so
* run the app
* unset LD_PRELOAD
*/
#define _GNU_SOURCE
#include <sys/types.h>
#include <time.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>
time_t time(time_t *tloc)
{
static time_t (*func)();
time_t t;
if(!func)
{
func = (time_t (*)()) dlsym(RTLD_NEXT, "time");
}
t=func(tloc) + 300;
if (tloc) *tloc=t;
return(t);
}
int clock_gettime(clockid_t clk_id, struct timespec *tp)
{
static int i;
static int (*func)();
if(!func)
{
func = (int (*)()) dlsym(RTLD_NEXT, "clock_gettime");
}
i=func(clk_id,tp);
tp->tv_sec += 300;
return(i);
}
We can see the code logic is very very similar and that multiple functions can be overridden.
$ date ; LD_PRELOAD=$PWD/time_modify.so date
Sun Mar 5 18:48:05 EST 2017
Sun Mar 5 18:53:05 EST 2017
I think he eventually went with timezone creation; it’s pretty easy for America/New_York :-)
Overcoming license restrictions
This one was big in the 90s. Sun SPARC servers had an NVRAM chip, and
each host had a unique hostid
value. Some licensed software had a
license file that was based on this hostid. If you tried to run it on
another machine then it’d fail to start.
A number of solutions were available for this, including reprogramming the NVRAM, but a common one was to use LD_PRELOAD.
In this case the code is a lot simpler; we don’t need to call out to the original routine or do anything conditional; just return the number we want. (the compile sequence is different here; it’s for Solaris)
long gethostid(void) {
return 0x12345678;
}
$ gcc -fPIC -c newhostid.c
$ gcc -shared -o newhostid.so newhostid.o
$ export LD_PRELOAD=/tmp/newhostid.so
$ /usr/bin/hostid
12345678
Of course this was never used to allow license keys to be reused on multiple servers at the same time. Oh no, that would be wrong…
Summary
LD_PRELOAD
isn’t really something I’d recommend for production
quality code. I can see it being useful during development (“I want to
test this new function without rebuilding a whole library”) and, as can
be seen, it can be used to circumvent some forms of protection. But I
wouldn’t consider it supportable!
And, no, you can’t use it against setuid programs; the linker ignores
these variables in this case. You can’t make su
give you a root
shell by overriding library calls :-)