Panel For Example Panel For Example Panel For Example

Two Common Hook Methods on Linux

Author : Adrian October 30, 2025

Linux function hooking using LD_PRELOADOverview

Hooking changes the behavior of existing functions by replacing them before system calls or function calls are executed. This article describes two common methods for function hooking on Linux: using LD_PRELOAD for preloading shared libraries and using ptrace to inject hooks into running processes.

Using LD_PRELOAD for hooking

Linux provides an environment variable named LD_PRELOAD. This variable allows specifying one or more shared library paths. When a program starts, the dynamic loader will load the shared libraries specified by LD_PRELOAD before loading the C runtime library. This mechanism is called preloading.

Preloading lets a user insert a custom shared library before program execution to modify or extend program behavior. These custom libraries can contain overridden function definitions. When the program attempts to call those functions, the dynamic loader will prefer the preloaded definitions over the default ones. Combining LD_PRELOAD and preloading, a function hook can be implemented.

First we write a target program that waits for user input, causing it to block.

#include int main() { printf("please input a number:\n"); int val = 0; scanf("%d", &val); printf("already recv your number!\n"); return 0; }

Then write a hook function that overrides scanf and prints a message so the target program can continue without waiting for user input.

#include int main() { printf("please input a number:\n"); int val = 0; scanf("%d", &val); printf("already recv your number!\n"); return 0; }

Compile the target program and the hook shared object as follows.

gcc ./target.c -o target gcc --shared hook.c -o hook.so -fPIC

Run the program with LD_PRELOAD to apply the hook. You will see that scanf no longer waits for user input and instead outputs a message.

LD_PRELOAD=./hook.so ./target

Using ptrace for hooking

The LD_PRELOAD method only works for programs started after the preload. For already running programs, ptrace can be used to implement hooking.

ptrace allows one process to monitor and control the execution of another process. It is the foundation for debuggers such as GDB. The ptrace-based hooking process typically follows these steps:

  1. The hooking process attaches to the running target process via ptrace, retrieves the target's execution context, and saves the original register values.
  2. Locate the pointer to the link_map linked list and, by iterating it and matching symbol names, find the actual function addresses. The link_map address is referenced from the .got.plt section, and that section's load address can be obtained from the DYNAMIC segment DT_PLTGOT. In this flow, dlopen is usually located.
  3. Modify the target process registers and stack so that it calls dlopen to load hook.so into its address space.
  4. Replace the original function address with the new function address from hook.so. Because hook.so has been loaded into the target process, the address for the overridden function can be found as in step 2.
  5. Restore the original register values and call PTRACE_DETACH to finish the attachment.

The following sections describe an implementation in more detail.

Attach to the target with ptrace and save registers

void ptrace_attach(pid_t pid, struct user_regs_struct *regs) { if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) { printf("ptrace_attach error\n"); } waitpid(pid, NULL, WUNTRACED); if(ptrace(PTRACE_GETREGS, pid, NULL, regs)) { printf("ptrace_getregs error!\n"); } }

The code that locates the link_map pointer by parsing the ELF file structure is omitted here. The next function shows how to iterate the link_map linked list to find a specified function address.

Elf_Addr find_symbol(int pid, Elf_Addr lm_addr, char *sym_name) { struct link_map lmap; // store the contents of lmap unsigned int nlen = 0; while (lm_addr) { // With a link_map pointer, read the link_map structure contents ptrace_getdata(pid, lm_addr, &lmap, sizeof(struct link_map)); // Get the pointer to the next link_map structure lm_addr = (Elf_Addr)(lmap.l_next); // Check whether the shared library name is valid if (0 == lmap.l_name) { continue; } Elf_Addr sym_addr = find_symbol_in_linkmap(pid, &lmap, sym_name); if (sym_addr) { return sym_addr; } } return 0; }

Using find_symbol above, obtain the dlopen address. Simulate calling dlopen by setting up the stack with the absolute path of the library to load, then call dlopen to load the shared object into the target process.

int inject_code(pid_t pid, unsigned long dlopen_addr, char *libc_path) { char sbuf1[STRLEN], sbuf2[STRLEN]; struct user_regs_struct regs, saved_regs; int status; ptrace_getregs(pid, ®s); // get all register values ptrace_getdata(pid, regs.rsp + STRLEN, sbuf1, sizeof(sbuf1)); ptrace_getdata(pid, regs.rsp, sbuf2, sizeof(sbuf2)); /* ret content used to trigger SIGSEGV */ unsigned long ret_addr = 0x666; ptrace_setdata(pid, regs.rsp, (char *)&ret_addr, sizeof(ret_addr)); ptrace_setdata(pid, regs.rsp + STRLEN, libc_path, strlen(libc_path) + 1); memcpy(&saved_regs, ®s, sizeof(regs)); regs.rdi = regs.rsp + STRLEN; regs.rsi = RTLD_NOW|RTLD_GLOBAL|RTLD_NODELETE; regs.rip = dlopen_addr+2; if (ptrace(PTRACE_SETREGS, pid, NULL, ®s) < 0) { printf("inject_code:PTRACE_SETREGS 1 failed!"); } if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) { printf("inject_code:PTRACE_CONT failed!"); } waitpid(pid, &status, 0); if (ptrace(PTRACE_SETREGS, pid, 0, &saved_regs) < 0) { printf("inject_code:PTRACE_SETREGS 2 failed!"); } ptrace_setdata(pid, saved_regs.rsp + STRLEN, sbuf1, sizeof(sbuf1)); ptrace_setdata(pid, saved_regs.rsp, sbuf2, sizeof(sbuf2)); return 0; }

Next, find the target program's GOT table and replace the target function's GOT entry with the new function address.

Elf_Addr find_sym_in_rel(int pid, char *sym_name) { Elf_Rel *rel = (Elf_Rel *) malloc(sizeof(Elf_Rel)); Elf_Sym *sym = (Elf_Sym *) malloc(sizeof(Elf_Sym)); int i; char str[STRLEN] = {0}; unsigned long ret; struct lmap_result *lmret = get_dyn_info(pid); for (i = 0; i < lmret->nrelplts; i++) { ptrace_getdata(pid, lmret->jmprel + i*sizeof(Elf_Rela), rel, sizeof(Elf_Rela)); ptrace_getdata(pid, lmret->symtab + ELF64_R_SYM(rel->r_info) * sizeof(Elf_Sym), sym, sizeof(Elf_Sym)); int n = ptrace_getstr(pid, lmret->strtab + sym->st_name, str, STRLEN); if (strcmp(str, sym_name) == 0) break; } if (i == lmret->nrelplts) ret = 0; else ret = rel->r_offset; free(rel); return ret; }

Example: automatic input injection

Modify the target program to loop 10 times, each iteration reading console input and printing it.

#include #include int main() { int val = 10; while (val--) { sleep(2); printf("please input a number:\n"); int val = 0; scanf("%d", &val); printf("your val is %d\n", val); } return 0; }

In the hook code, automatically assign values to the val variable.

#include #include int num = 1; int hookscanf(const char *format,...) { va_list ap; int retval; va_start(ap, format); int* pval = va_arg(ap, int*); printf("automatic input:%d\n", num); *pval = num++; return 0; }

After compiling and running, the program no longer requires console input; val is set automatically.

Summary

This article described two common Linux hooking methods. The LD_PRELOAD approach is straightforward but cannot attach to already running processes. The ptrace-based approach can hook running processes but is more complex and requires deeper understanding of the ELF format and related structures.