Library injection is a powerful technique that allows modifying the behavior of an existing process by dynamically loading external libraries. In this article, we will explore the principles of library injection, its different approaches, and its practical applications.
Library injection is a common technique used by attackers to execute arbitrary code within the address space of a legitimate process. This technique is widely used in malware, rootkits, and red team operations. In Capture The Flag (CTF) competitions, understanding library injection is valuable for binary exploitation, reverse engineering, and forensics challenges.
This article is write as a report from a given technical conference. It’s divided in two parts. The firts one with concept of LD_PRELOAD (subject commonly matered in development and cybersecurity community) and the second one the injection of a librairie with help of ptrace.
LD_PRELOAD
LD_PRELOAD is an environment variable that contains a list of shared libraries, which are loaded before all other dynamically linked libraries when running a program on Linux. This variable is very useful for rewriting library functions for testing and debugging purposes. For example, I sometimes use it to detect possible memory leaks or use-after-free issues by rewriting the malloc and free functions.
The next section will describe a solution to bypass certain checks by creating new function behaviors. It will also include an example of how to log interesting information without altering the function’s result, based on the legitimate one.
Anti debug bypass
Here is a classic case of protection against dynamic analysis with a debugger. In a Linux environment, a program can attach to a process, allowing it to stop execution, monitor its behavior, or modify memory and registers. In other words, it is possible to have debugging capabilities.
For obvious reasons, only one program can have this capability at a given time. That’s why, to detect if the current process is attached to another one and to prevent anyone from doing so, a process can attach to itself. This technique is very common in crackme challenges :
//gcc antidebug.c -o antidebug
#include<stdio.h>
#include<stdlib.h>
#include<sys/ptrace.h>
#include<string.h>
int main(){
char input[50];
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
puts("being traced");
exit(1);
}
puts("not being traced");
return 0;
}
This code is very simple and illustrates the principle explained earlier. The ptrace() system call provides the parent process with a way to control the execution of another process and modify its memory image. If a program is already being traced (by a debugger), ptrace(PTRACE_TRACEME, 0, NULL, NULL) fails with an error, allowing the program to detect debugging attempts. The following example illustrates its execution in an environment without debugging:
▶ ./antidebug
not being traced
The following example illustrates its execution in an environment without debugging:
> gdb ./antidebug
[...]
Starting program: /home/gerboise/Documents/injection/antidebug
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
being traced
[Inferior 1 (process 15630) exited with code 01]
How can this type of protection be bypassed? In this context, the LD_PRELOAD variable can be very useful. One solution is to change the behavior of ptrace and force it to return a success value. The first step in doing this is to find the function signature. The Linux documentation (man ptrace) provides the following informations:
NAME
ptrace - process trace
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request op, pid_t pid,
void *addr, void *data);
At this point, it is enough to implement the function with the desired behavior and compile it as a shared library:
// gcc -shared ptracelib.c -o ptrace.so
long ptrace(int request, int pid, void *addr, void *data) {
return 0;
}
Here is an example of execution in a debugging environment with the LD_PRELOAD environment variable configured (LD_PRELOAD=./ptrace.so):
> gdb ./antidebug
[...]
(gdb) set environment LD_PRELOAD=./ptrace.so
(gdb) ./run
Undefined command: ".". Try "help".
(gdb) run
Starting program: /home/gerboise/Documents/injection/antidebug
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
not being traced
[Inferior 1 (process 15238) exited normally]
(gdb)
Extending functionalities with LD_PRELOAD
As seen earlier, it is possible to reuse the original function to extend its functionality or wrap additional processing around it. The following code is a simple, classic crackme example to illustrate this possibility:
//gcc antidebug.c -o antidebug
#include<stdio.h>
#include<stdlib.h>
#include<sys/ptrace.h>
#include<string.h>
#define PASSWORD "password"
int main(){
char input[50];
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
puts("being traced");
exit(1);
}
puts("not being traced");
puts("Enter password: ");
fgets(input, sizeof(input)-1, stdin);
size_t len = strlen(input);
if (len > 0 && input[len - 1] == '\n') {
input[len - 1] = '\0';
}
if (strcmp(input, PASSWORD) == 0) {
puts("Access granted.");
} else {
puts("Access denied.");
}
return 0;
}
As before, the process begins by first identifying the exact function signature and then rewriting its behavior. To retrieve and execute the original function, I use the dlsym function. dlsym(void *restrict handle, const char *restrict symbol)
is a function from the dl library on Linux (and other Unix systems) that retrieves the address of a function or a symbolic variable in a loaded dynamic library. RTLD_NEXT searches for the symbol in libraries loaded after the current library.
The following example illustrates this principle with a rewrite of the strcmp function to display its arguments:
// gcc -shared strcmp.c -o strcmp.so
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
// Create a function pointer type with strcmp signature
typedef int (*strcmp_t)(const char *, const char *);
int strcmp(const char *s1, const char *s2) {
strcmp_t func = (strcmp_t) dlsym(RTLD_NEXT, "strcmp");
printf("[HOOK] strncmp called:\n");
printf(" s1: \"%s\"\n", s1);
printf(" s2: \"%s\"\n", s2);
return func(s1, s2);
}
It works correctly:
▶ LD_PRELOAD=./strcmp.so ./antidebug
not being traced
Enter password:
a
[HOOK] strncmp called:
s1: "a"
s2: "password"
Access denied.
Conclusion
LD_PRELOAD is a very useful tool in debugging and reverse engineering contexts. It is also a powerful asset in CTF environments. This type of library injection, allowing function behavior modification, is just one interesting aspect of Linux library injection. The next article will provide an example of a real library dynamically injected into a program to execute inconspicuous payloads.