Thursday, February 9, 2012

Linux terminal keylogger in userspace


Sometimes, during a pentest, you have access to a certain system user's password, can actually successfully login on the system(using ssh for example), but cannot gain root privileges. This can be caused due to the fact that the user is not a sudoer, nor is sudo installed on the system, and of course, that the password is not the same as the root user.

However, this user might actually use "su" to become the superuser. In this case, we have access to his settings and configuration files, located in his home directory, which can be modified and used to obtain the root's password.

There are several environment variables that libc uses to modify an application's behaviour when launching it. Among many, there is one that allows the user to load one or more arbitrary shared objects right before launching a certain application. This environment variable is named LD_PRELOAD.

The LD_PRELOAD environment variable

Using LD_PRELOAD, a user can launch an application and force it to load any shared object he wants. So how can we take advantage of LD_PRELOAD? On GNU/Linux, programs are usually compiled dynamically, allowing the symbols used in it to be loaded when the application is launched. When the application is executed, the shared objects that contain the symbols used are loaded, and those symbol's addresses are resolved. Every reference to the loaded symbols will use those addresses.

Fortunately for us, this symbol loading is done after the shared objects pointed by LD_PRELOAD are loaded. Therefore, if we compile a shared object which contains a function that has the same signature(name + return type + arguments) as one that another application expects to resolve, it will use ours instead of the "original" one, allowing us to execute arbitrary code.

Okay, so now we can inject arbitrary code, now how can we use this technique? We could hook the read system call, then wait for the user to launch su. Our object would be loaded and we would be able to read the data typed by the user, in which we will eventually find the root's password.

However, we cannot do this, since su is a suid file. The libc does not allow us to execute an application that has the suid bit set, and instruct it to load our library on startup. If this was possible, we could execute ping, for example, make our shared object load and drop a root shell right away. Therefore LD_PRELOAD + suid binary is not a possibility.

Anyhow, we can use another approach. We can hook the execve syscall, which is used by bash to execute commands, and modify its behaviour. This way we could execute whatever we want, no matter what bash is trying to execute.

The actual keylogger

So what i did was to instead of trying to hook the read syscall and log every byte read by the application, was to hook execve and fork() right before calling the original execve and log the data that is written to stdin(which should be read by the execve'd application) into a file. This way, no matter what was being executed, suid files or regular, we can still read stdin, since we are not hooking the syscall on the executed application, but on bash. My execve is basically a proxy that logs everything that is written to stdin before it is sent to the child process.

Finally, we can read every byte written by the user, but we require bash to load our shared object using the LD_PRELOAD environment variable. We can achieve this by editing(or creating) the ~/.pam_environment file and inserting this line into it:
In my case, i have inserted the line:
After a restart(or logout performed by the user) that environment variable will be set, loading our shared object everytime bash starts.

Note that we can't edit the .bashrc file, since this file is interpreted by bash after the process is created. What we require is a way of editting the LD_PRELOAD environment variable before bash is started, which can be achieved through ~/.pam_environment.

The source code of this PoC is available here Download both keylogger.c and Makefile. In order to compile, just execute make, will be the object generated.

This is a screenshot of the keylogger, storing the password and commands typed by the user when executing su:

By default the log file is located in /tmp/output. If you want to change it, just edit the sourcecode and edit the OUTPUT_FILE define, or add a CFLAG to the Makefile:
CFLAGS=-c -Wall -O3 -fPIC -DOUTPUT_FILE=/home/somebody/blah.log
There is an array named injected_files which contains a list of all the files that will be "logged" when executed. This is used because there are lots of applications that we don't want to log. This array contains /bin/su and /usr/bin/ssh. Feel free to add any other application that you want to keylog.

Finally, there's a code snippet at the end of the file which removes the value of the LD_PRELOAD variable right after the shared object is loaded. This is just a mechanism to hide our keylogger. Otherwise, if the user executed "echo $LD_PRELOAD" while using bash, he would see the location of our keylogger as the output. Note that by removing the LD_PRELOAD variable, child processes will not load our shared object, therefore in a GUI environment, where the first application that is executed is gnome-terminal or other graphical terminal, and this application is the one that executes bash, the shared object will be loaded by gnome-terminal, but won't be loaded by bash. Therefore, if you plan on using this keylogger on a GUI environment, remove the body of the "void init(void)" function, located at the end of keylogger.c.

Hope you find it useful!


  1. how to start up ur script ??
    I try insert LD_PRELOAD=/home/matias/ to .bashrc but can not running

    How to solution ??

    1. As mentioned above, you need to hook bash *before* it is executed. Read the part that mentions the file ~/.pam_environment.

  2. and how to about command "scp" ?


  3. Kinda messy for a pentest, isn't it? :-P
    Excellent approach mate, congratz!

  4. excellent approach. i was messing with LD_PRELOAD over the summer but stopped when I realized abut the set uid limit. then you geniously went around that with hooking into non setuid functions before these programs are called!! GREAT!!!

    Since i stopped with LD_PRELOAD, i went ahead and took a different approach. If you care to hear about it. email me (i think you would find it interesting)

    1. Actually, after taking an extremely quick glimpse at your code. you are ALSO allocating pseudo terminal. but I was assuming YOU wouldn't have to do that... perhaps it is for the same reason that I had to.. because password sensitive info seems to be fetched directly from the tty master.

    2. Hi! Sorry, I somehow didn't see this earlier. I'd be very interested in hearing about your approach!

      Regarding the pty, as you say, it's mandatory to open one. Otherwise passwords won't be sniffed.

  5. excellent idea, but the problem is, as per the man page; "By default user_readenv option is off as user supplied environment variables in the PAM environment could affect behavior of subsequent modules in the stack without the consent of the system administrator." Since editing the pam_env.conf or pam.d is not possible, our ~/.pam_environment approach will fail.
    So how you would tackle this?? I'm really curious to know.
    Any idea??


    1. Hi! What's that man page from? man pam_env says that option is on by default. Well, anyway, one simple way in which this could work without using ~/.pam_environment is to use the .bashrc file. You could add a line that looks like this:

      # modify this condition plx
      if [ bash is not already hooked ]
      LD_PRELOAD=PathToTheSharedObject bash

      Then a new, hooked, bash instance will be executed. It's a little nasty since there will be 2 bash instances instead of one, but it should work.

      If some cleaner technique comes to my mind, I'll let you know!


    2. Yup, that is a solution anyway, I tried it already. But the problem is, as you mentioned, there will be two bash instances, or in other words, needs two exit commands to exit the shell, which will be a no go solution if we are attempting to fool a unix admin. I already tested it and it'll work in the lab, but of no use for a break-in attempt/pentest point of view (unless we are targeting a less techy user).

      Also, if you want to see the man page please see

    3. You could use exec to get around that:

      LD_PRELOAD=… exec bash

  6. ok, its a somewhat progress to set,
    LD_PRELOAD=... bash
    as you mentioned to exit the first bash automatically. But cant say for sure, the first bash will be blocked until the child is exited and if the user tries to close the window rightaway, it'll show a warning since the child has not exited.
    Tried, LD_PRELOAD=... bash &
    but of no use.


  8. This is brilliant, thanks!

  9. as of now 2018 this is only the keylogger with serial complete instuctions that works smooth with me without any problem or error
    note without any survey or ads