Dynamic linker hijacking
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:
- The dynamic loader (ld.so) loads all required shared objects (e.g., libc.so , the standard C library) into the process’s address space.
- Function calls are resolved to the correct memory addresses based on the dynamic linking symbols.
- LD_PRELOADChanges This Process: When- LD_PRELOADis 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_PRELOADlibrary 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 direntis 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 DIRstructure 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 direntdescribing the entry, orNULLif 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"
