1. Background
1. Storytelling
In the previous two articles, we introduced the powerful capabilities of Minhook on the Windows platform. In this article, we will discuss how to hook functions on Linux, introducing two methods.
- Lightweight LD_PRELOAD Interception
LD_PRELOAD is a method of intercepting shared libraries. The advantage of this method is that it does not require any modifications to the source program, achieving a non-intrusive effect, which is unimaginable on the Windows platform.
2. Funchook Interception
There are many function hooking libraries available for Linux on GitHub, and I found that the lightweight, active, and open-source one is Funchook.
2. Two Interception Methods
1. How LD_PRELOAD Achieves Interception
To understand how LD_PRELOAD achieves interception, you need to have an understanding of the linker <span>ld.so</span> during the initialization of processes on Linux. In simple terms, its loading order is <span>Executable file of the main program -> Library specified by LD_PRELOAD -> glibc standard library -> Other dependent libraries</span>.
Since the shared object file specified by LD_PRELOAD takes precedence over glibc.so resolution, this method can utilize the first come, first served approach to override subsequent symbols with the same name. So what does ld.so look like? On my Ubuntu, it is <span>ld-linux-x86-64.so.2</span>.
root@ubuntu2404:/data2# cat /proc/5322/maps
60c0f8687000-60c0f8688000 r--p 0000000008:031966089 /data2/main
60c0f8688000-60c0f8689000 r-xp 0000100008:031966089 /data2/main
60c0f8689000-60c0f868a000 r--p 0000200008:031966089 /data2/main
60c0f868a000-60c0f868b000 r--p 0000200008:031966089 /data2/main
60c0f868b000-60c0f868c000 rw-p 0000300008:031966089 /data2/main
60c1266de000-60c1266ff000 rw-p 0000000000:000 [heap]
7efd5c600000-7efd5c628000 r--p 0000000008:032242169 /usr/lib/x86_64-linux-gnu/libc.so.6
7efd5c628000-7efd5c7b0000 r-xp 0002800008:032242169 /usr/lib/x86_64-linux-gnu/libc.so.6
7efd5c7b0000-7efd5c7ff000 r--p 001b0000 08:032242169 /usr/lib/x86_64-linux-gnu/libc.so.6
7efd5c7ff000-7efd5c803000 r--p 001fe000 08:032242169 /usr/lib/x86_64-linux-gnu/libc.so.6
7efd5c803000-7efd5c805000 rw-p 0020200008:032242169 /usr/lib/x86_64-linux-gnu/libc.so.6
7efd5c805000-7efd5c812000 rw-p 0000000000:000
7efd5c964000-7efd5c967000 rw-p 0000000000:000
7efd5c977000-7efd5c979000 rw-p 0000000000:000
7efd5c979000-7efd5c97d000 r--p 0000000000:000 [vvar]
7efd5c97d000-7efd5c97f000 r-xp 0000000000:000 [vdso]
7efd5c97f000-7efd5c980000 r--p 0000000008:032242166 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7efd5c980000-7efd5c9ab000 r-xp 0000100008:032242166 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7efd5c9ab000-7efd5c9b5000 r--p 0002c000 08:032242166 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7efd5c9b5000-7efd5c9b7000 r--p 0003600008:032242166 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7efd5c9b7000-7efd5c9b9000 rw-p 0003800008:032242166 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe03c95000-7ffe03cb6000 rw-p 0000000000:000 [stack]
ffffffffff600000-ffffffffff601000 --xp 0000000000:000 [vsyscall]
Having said so much, let’s demonstrate how to intercept openat. First, define a shared library that needs to be loaded by LD_PRELOAD, as shown in the following code:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/types.h>
static int (*real_openat)(int, const char *, int, ...) = NULL;
int openat(int dirfd, const char *pathname, int flags, ...)
{
mode_t mode = 0;
pid_t pid = getpid();
pid_t tid = gettid();
printf("hooked openat: PID=%d, TID=%d, path=%s\n", pid, tid, pathname);
if (!real_openat)
{
real_openat = dlsym(RTLD_NEXT, "openat");
}
if (flags & O_CREAT)
{
return real_openat(dirfd, pathname, flags, mode);
}
else
{
return real_openat(dirfd, pathname, flags);
}
}
Compile the above hook_openat.c into a dynamic link library, where <span>-ldl</span> indicates that it provides APIs for loading this library, such as (dlopen, dlsym), as shown below:
root@ubuntu2404:/data2# gcc -shared -fPIC -o libhookopenat.so hook_openat.c -ldl
root@ubuntu2404:/data2# ls -lh
total 24K
-rw-r--r-- 1 root root 688 Jun 12 09:14 hook_openat.c
-rwxr-xr-x 1 root root 16K Jun 12 09:20 libhookopenat.so
-rw-r--r-- 1 root root 782 Jun 12 09:18 main.c
After creating the shared library, the next step is to write C code to call it. Here, we will use openat to open a file and let libhookopenat.so intercept it. The reference code is as follows:
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// Create a new file in the current directory
int fd = openat(AT_FDCWD, "example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1)
{
perror("openat failed");
exit(EXIT_FAILURE);
}
// Write some content to the file
const char *text = "This is a test file created with openat!\n";
ssize_t bytes_written = write(fd, text, strlen(text));
if (bytes_written == -1)
{
perror("write failed");
close(fd);
exit(EXIT_FAILURE);
}
// Close the file
close(fd);
printf("File created and written successfully! Wrote %zd bytes.\n", bytes_written);
return 0;
}
root@ubuntu2404:/data2# gcc -o main ./main.c
root@ubuntu2404:/data2# LD_PRELOAD=./libhookopenat.so ./main
hooked openat: PID=4646, TID=4646, path=example.txt
File created and written successfully! Wrote 41 bytes.
From the output, we can clearly see that the hook was successful!
2. How Funchook Achieves Interception
The granularity of the LD_PRELOAD shared library is still too coarse. If the granularity is smaller, it would be more flexible, such as at the function level. This is what we will introduce in this section: Funchook. The source code is available on GitHub: https://github.com/kubo/funchook. The only hassle is that you need to compile the source code to generate the corresponding <span>header file</span>, <span>static link file</span>, and <span>dynamic link library</span>, as shown below:
root@ubuntu2404:/data4# sudo apt install -y git gcc cmake make
root@ubuntu2404:/data4# git clone https://github.com/kubo/funchook.git
root@ubuntu2404:/data4# cd funchook
root@ubuntu2404:/data4# mkdir build && cd build
root@ubuntu2404:/data4# cmake ..
root@ubuntu2404:/data4# make
root@ubuntu2404:/data4/funchook/build# sudo make install
[ 25%] Built target distorm
[ 42%] Built target funchook-shared
[ 60%] Built target funchook-static
[ 71%] Built target funchook_test
[ 85%] Built target funchook_test_shared
[100%] Built target funchook_test_static
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/include/funchook.h
-- Installing: /usr/local/lib/libfunchook.so.2.0.0
-- Installing: /usr/local/lib/libfunchook.so.2
-- Installing: /usr/local/lib/libfunchook.so
-- Installing: /usr/local/lib/libfunchook.a
root@ubuntu2404:/data4/funchook/build# ldconfig
Since it is installed by default in <span>/usr/local/lib</span>, remember to use the <span>ldconfig</span> command to refresh it; otherwise, the program may not find the new library. Finally, here is the C calling code:
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <unistd.h>
#include <funchook.h>
// Original function pointer
static int (*orig_openat)(int dirfd, const char *pathname, int flags, mode_t mode);
// Hook function
int hooked_openat(int dirfd, const char *pathname, int flags, mode_t mode)
{
printf("Hooked openat called: path=%s, flags=0x%x\n", pathname, flags);
// Call the original function
return orig_openat(dirfd, pathname, flags, mode);
}
int main()
{
// Get the address of the original openat function
orig_openat = dlsym(RTLD_NEXT, "openat");
if (!orig_openat)
{
fprintf(stderr, "Failed to find openat: %s\n", dlerror());
return 1;
}
// Create a funchook instance
funchook_t *funchook = funchook_create();
if (!funchook)
{
perror("funchook_create failed");
return 1;
}
// Prepare Hook
int rv = funchook_prepare(funchook, (void **)&orig_openat, hooked_openat);
if (rv != 0)
{
fprintf(stderr, "Prepare failed: %s\n", funchook_error_message(funchook));
return 1;
}
// Install Hook
rv = funchook_install(funchook, 0);
if (rv != 0)
{
fprintf(stderr, "Install failed: %s\n", funchook_error_message(funchook));
return 1;
}
// Test call
printf("=== Testing openat hook ===\n");
int fd = openat(AT_FDCWD, "/etc/passwd", O_RDONLY);
if (fd >= 0)
{
printf("Successfully opened file, fd=%d\n", fd);
close(fd);
}
else
{
perror("openat failed");
}
// Cleanup
funchook_uninstall(funchook, 0);
funchook_destroy(funchook);
return 0;
}
Next is the compilation and execution.
root@ubuntu2404:/data2# gcc -o main main.c -lfunchook -ldl
root@ubuntu2404:/data2# ./main
=== Testing openat hook ===
Hooked openat called: path=/etc/passwd, flags=0x0
Successfully opened file, fd=3
Everything is great! If you want to visualize step-by-step debugging, you can configure it in the tasks.json of Visual Studio, as shown below:
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: gcc build active file",
"command": "/usr/bin/gcc",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}",
"-lfunchook",
"-L/usr/local/lib"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Task generated by Debugger."
}
],
"version": "2.0.0"
}

3. Conclusion
Here, I summarize two injection methods. Although LD_PRELOAD is simple, it has coarse granularity and is suitable for simple non-intrusive scenarios. If you want finer granularity, I recommend using the active Funchook, which is implemented by a developer from Japan.