Shared Library Hijacking via LD_LIBRARY_PATH
As we mentioned previously, when an application runs,
it checks for its libraries in an ordered set of locations.
After checking its internal RPATH values for hard coded paths,
it then checks for an environment variable
called LD_LIBRARY_PATH.
Setting this variable allows a user to override the default behavior of a program
and insert their own versions of libraries.
An intended use cases for this include testing new library versions
without modifying an existing libraries
or modifying the program’s behavior temporarily
for debugging purposes.
As an attacker, we can also use it to maliciously change the intended behavior
of the program being run,
We’ll exploit a victim user’s application
by creating a malicious library
and then use LD_LIBRARY_PATH
to hijack the application’s normal flow
and execute our malicious code
to escalate privileges.
Note that for demonstration purposes,
we are explicitly setting the environment variable before each call.
However, as an attacker, we would want to insert
a line in the user’s .bashrc or .bash_profile
files to define the LD_LIBRARY_PATH variable
so it is set automatically when the user logs in.
One difficulty with using LD_LIBRARY_PATH for exploitation
is that on most modern systems,
user environment variables are not passed on
when using sudo.
This setting is configured in the /etc/sudoers file
by using the env_reset keyword as a default.
Some systems are configured to allow a user’s environment
to be passed on to sudo.
These will have env_keep set instead.
We could bypass env_reset setting with our previously mentioned .bashrc alias
for the sudo command.
We mentioned this approach earlier
when we set the sudo command to sudo -E.
As a normal user, it’s not typically possible to read /etc/sudoers
to know if env_reset is set,
so it may be useful to create this alias setting regardless.
We’ll need to tweak this process to make LD_LIBRARY_PATH work with sudo.
We’ll discuss how to do this later in this section.
Let’s walk through an example of a simple malicious, shared library
using the C programming language
and save it as hax.c.
The first three lines include header files
as discussed in earlier examples.
The fourth line provides a function declaration
for a constructor function called
runmahpayload.
Constructor functions are run when the library is first initialized
in order to set up code for the library to use.
By doing this, we’re just letting the compiler know that a function of this name
will be defined later.
We are creating a constructor function
so that our malicious code will run when our library is loaded,
regardless of what the original program is trying to do with it.
In other words, the original program will try to load the library,
which will then run our constructor function,
triggering our malicious payload.
The remainder of the lines contain the function’s actual code itself.
This is where we’ll put our malicious actions.
In our case, we initially set the user’s UID and GID to “0”,
which will make the user root
if run in a sudo context.
We’ll then print a message to the screen
to show that it functioned correctly,
and modify a file
to show an action on the file system.
We’ll compile our shared library using two commands.
In the first command, we use the -Wall parameter,
which gives more verbose warnings when compiling.
The -fPIC tells the compiler to use position independent code,
which is suitable for shared libraries
since they are loaded in unpredictable memory locations.
The -c flag tells gcc to compile but not link the code
and -o
tells the compiler to produce an output file
with the name immediately following the parameter.
Finally, the last item is the source code file we’ve written.
In the second command, we’re again using gcc to compile.
However, this time we use the -shared parameter
to tell gcc we’re creating a shared library
from our object file.
We then specify an output file again,
this time with the name libhax.so,
and then we specify our input object file.
This produces a libhax.so. shared library file.
One important thing to note
is that shared libraries in Linux use the soname naming convention.
This is typically something like lib.so,
which may also include a version number appended to the end
with a period or full-stop character.
For example, we might see lib.so.1.
Naming our libraries following this convention will help us
with the linking process.
Now that we have a malicious shared library, we need a place to use it.
We want to hijack the library of a program that a victim is likely to run,
especially as sudo.
We also need to remember that whichever library we’re hijacking
will be unavailable to the requesting program.
As such, we want to find something that won’t break the system
if all programs are prevented from using it.
Let’s try targeting the top command,
which is used to display processes in real time on a Linux system.
It’s likely that a user might run this as sudo
in order to display processes with elevated permissions,
so it’s a good candidate.
We’ll run the ldd command in the target machine on the top program.
This will give us information on which libraries are being loaded
when top is run.
The last library listed appears to be a library for error reporting
called LibGPG-Error.
This is likely to be loaded by the application
but not likely to be called unless the program encounters an error,
therefore this shouldn’t prevent normal use of the application.
Let’s try to hijack that and find out what happens.
Note that it may require some trial
and error to find a library that behaves favorably
to run our code
and not have adverse side effects on the system.
Ideally, we want to target a library that also allows the program to run correctly
even after our exploit is run,
but this may not always be possible.
We set our environment variable for LD_LIBRARY_PATH,
and rename our .so file to match the one we’re hijacking.
Now we can run our top program and examine what happens.
Unfortunately, we have a problem.
The error message states that we’re missing the symbol
gpgrt_lock_lock
with a version of GPG_ERROR_1.0.
The program has not yet run our library’s constructor,
but it’s already giving an error that we’re missing symbols.
This means that certain variables or functions that the program expects to find
when loading the original library have not been defined in our malicious library.
As a result, the program won’t even attempt to run our library’s constructor.
Fortunately, this is fairly easy to fix.
When loading a library, a program only wantsto know that our library
contains symbols of that name.
It doesn’t care anything about validating their type or use.
Because of that, we can simply define some variables
with the same names that it expects
and top should run.
We have an additional advantage
in that the original shared library exists on the file system.
Let’s examine it
and determine what symbols it contains using the readelf utility.
The -s parameter will give a list of available symbols in the library.
Not all of the listed symbols are needed
since some of them refer to other libraries.
The error message specified that the symbol it’s looking for is tagged with
GPG_ERROR_1.0.
We can infer that it’s part of the library we’re replacing.
The readelf output for the original shared library displays many defined symbols.
However, with the use of some bash command-line utilities,
we can parse out the information we need specifically
and put it into a format that we can paste directly into our library source code file
to define variables.
To do this, we’ll again call the readelf command
this time with the -s flag.
We’ll also include the --wide flag
to force it to include the untruncated names of the symbols,
as well as the full path
to the original
shared library file.
We’ll pipe that into grep
and search for lines containing
“FUNC”
representing symbols
we need to capture.
We’ll then pipe this to grep again
and filter out only the results that also contain
“GPG_ERROR”,
indicating they are stored in our library
and not in an unrelated dependency.
Once we’ve done that, we pipe the resulting lines to awk
to capture only a specific column of the lines returned,
while prepending “int” to it.
This will help us more easily define variables in our code
to represent the symbols we're missing.
Finally, we pipe that output to sed
to replace the version information with a semicolon
in order to finalize the variable definitions.
The result is a list of variable definitions,
one for each missing symbol,
that we can copy and paste just under our initial constructor definition
in our hax.c
source code file.
After recompiling,
and setting the LD_LIBRARY_PATH variable again,
this time when we run top,
we get the result we wanted.
Unfortunately, we notice an obvious error message
about the shared library’s version information.
Not all supporting libraries require version information,
so this does not always occur.
If we were to hijack a different library, we may not receive this error.
In this case, however, it seems that libgcrypt
does require version information in associated libraries.
Thankfully, we can fix this with the help of a map file
that identifies particular symbols as being associated
with a given version of the library.
First, we’ll run a modified version of our previous readelf command,
this time omitting “int”
before the symbol names.
This simply provides a list of symbols
that we can then “wrap”
into a symbol map file
for the compiler to use.
We’ll call this file gpg.map.
The version number for these symbols
doesn’t have any direct impact on our exploit,
but it fulfills the version requirement that is causing our earlier error message.
Once the file is created,
we can compile our shared library again,
and include the symbol file
with --version-script.
We set our LD_LIBRARY_PATH environment variable as we did before,
and run the application again.
This time, we do not receive an error message.
We can look for the file our library was supposed to modify in /tmp.
The results show the file was created.
In this case, we were somewhat lucky in that our application ran properly
without the libgpg_error library.
If an error occurred that required libgpg_error,
the application would likely crash.
Earlier, we discussed how in modern Linux distributions
a user’s environment variables
aren’t normally passed to a sudo context.
To get around this, we created an alias for sudo
in the user’s .bashrc file
replacing sudo with sudo -E.
However, some environment variables are not passed even with this approach.
Unfortunately, LD_LIBRARY_PATH
is one of those.
If we try to run top with sudo, our module is not run.
There is a workaround.
We can modify the alias we created for this purpose to include our
LD_LIBRARY_PATH variable explicitly.
This forces it to be passed to the sudo environment.
If we source the .bashrc file to load the changes we made,
when we run the command with sudo,
the command executes as root.
We successfully exploited an application
using LD_LIBRARY_PATH
and a malicious shared library file.
In the next section, we’ll use LD_PRELOAD
to hijack library functions.