Dynamic linker hijacking

8 minute read

Published:

Dynamic linker hijacking

This post will explain the implementation details and the reason that why dynamic linker hijacking works. The content and source code is based on Linux rootkits explained – Part 1: Dynamic linker hijacking. I recommend reading the original post first.

Implementation

Here is a C implementation of the attack. Before explaining the code, let’s understand the techcnique behind.

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
// Function pointer typedef for the original readdir ls function
typedef struct dirent* (*ls_t)(DIR*);
// Interposed ls function
struct dirent* readdir(DIR* dirp) {
  // Get the original readdir address
  ls_t original_readdir = (ls_t)dlsym(RTLD_NEXT, "readdir");
  struct dirent* entry;
  do {
    // Call the original ls function to get the next directory entry
    entry = original_readdir(dirp);
    // Check if the entry is the file we want to hide
    if (entry != NULL && strcmp(entry->d_name, "malicious_file") == 0) {
      // Skip the file by calling the original ls function again
      entry = original_readdir(dirp);
    }
  } while (entry != NULL && strcmp(entry->d_name, "malicious_file") == 0);
  return entry;
}

Dynamic Linking and Symbol Resolution

When a dynamically linked executable is run:

  1. The dynamic loader (ld.so) loads all required shared objects (e.g., libc.so , the standard C library) into the process’s address space.
  2. Function calls are resolved to the correct memory addresses based on the dynamic linking symbols.
  3. LD_PRELOAD Changes This Process: When LD_PRELOAD is set, it tells the dynamic loader to load the specified shared library (e.g., libhijackls.so, the shared library that the code above compiled to) **before other shared libraries (like libc.so). This affects symbol resolution: functions defined in the LD_PRELOAD library take precedence over functions in the original shared objects.

Main Idea of the Implementation

Function Redefinition: In the shared library (libhijackls.so), we define a custom implementation of readdir. Symbol Precedence via LD_PRELOAD: When LD_PRELOAD=./libhijackls.so is set, the dynamic loader sees that your shared library should be loaded first. This means any call to readdir in the process (e.g., by the ls command) will use your custom readdir instead of the original libc.so version. Accessing the Original readdir: Even though your readdir overrides the original, the original function still exists in memory (in libc.so). You can explicitly access the original function using dlsym with the RTLD_NEXT flag.

ls_t original_readdir = (ls_t)dlsym(RTLD_NEXT, "readdir");

RTLD_NEXT tells dlsym to look for the next occurrence of readdir after the current library (libhijackls.so) in the dynamic linking chain. This ensures you bypass your custom readdir and call the original libc.so version. Calling the Original Function: With the function pointer returned by dlsym, you can call the original readdir

struct dirent* entry = original_readdir(dirp);

Syntax and Code breakdown

Function pointer declaration Defines a function pointer type (ls_t) for the readdir function

typedef struct dirent* (*ls_t)(DIR*);

*ls_t is a pointer to a function, a programmer-defined identifier. struct dirent*: The return type of the function (readdir), which is a pointer to a struct dirent (structure representing a directory entry). (DIR*): Specifies the argument type of the function, which is a pointer to a DIR structure (representing an open directory). when we need to declare a function pointer for this specific type, you would write:

ls_t original_readdir;

Function definition

struct dirent* readdir(DIR* dirp) {
}

This definition is the same as readdir(3) — Linux manual page.

1. struct dirent*:

  • Return Type: This function returns a pointer to a struct dirent.
  • struct dirent: Represents a directory entry in C. It typically contains fields like the name of the file (d_name) and other metadata. The struct dirent is defined in <dirent.h> and varies slightly between systems. A typical definition include
    struct dirent {
    ino_t          d_ino;       // Inode number
    off_t          d_off;       // Offset to the next dirent
    unsigned short d_reclen;    // Length of this record
    unsigned char  d_type;      // Type of file
    char           d_name[256]; // Null-terminated filename
    };
    

2. readdir(DIR* dirp):

  • Function Signature: The function takes a pointer to a DIR structure as its argument.
  • DIR* dirp: Represents an open directory stream, obtained by calling opendir().

3. What readdir Does:

  • Reads the next entry from the directory stream.
  • Returns a pointer to a struct dirent describing the entry, or NULL if no more entries are available.

The rest of the code are easy to understand:

ls_t original_readdir = (ls_t)dlsym(RTLD_NEXT, "readdir");

LIne above finds the address of the next readdir function in the shared object search order (i.e., the original readdir).

if (entry != NULL && strcmp(entry->d_name, "malicious_file") == 0) { 
// Skip the file by calling the original readdir function again entry = original_readdir(dirp); 
}

When it encounters a directory entry with the name "malicious_file", the condition strcmp(entry->d_name, "malicious_file") == 0 evaluates to true. Instead of returning this entry to the calling program (e.g., ls), the custom readdir function calls the original readdir function again to fetch the next directory entry. This effectively “hides” the "malicious_file" entry by not letting it be seen by the calling program. Basically, the do while loop will terminate immediately If a directory entry is not named "malicious_file"