Introduction
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:
LD_PRELOAD=FullPathToTheSharedObject
In my case, i have inserted the line:
LD_PRELOAD=/home/matias/key.so
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
https://github.com/mfontanini/Programs-Scripts/tree/master/keylogger. Download both
keylogger.c and
Makefile. In order to compile, just execute
make,
keylogger.so 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!