Files
Obsidian/X-Articles/system/Hijacking malloc() in Any Linux Program Using LD_PRELOAD.md
T
coco 8b94d76799 a
2026-07-21 13:57:15 +08:00

7.4 KiB

title, source, author, published, created, description, tags
title source author published created description tags
Hijacking malloc() in Any Linux Program Using LD_PRELOAD https://x.com/Adriksh/status/2030023097290534973
Unknown
2026-03-07 2026-03-07
clippings

Every Linux program uses malloc internally. Programs allocate memory constantly, and they all end up calling the same malloc function from the C standard library. But here's something most developers do not realize: you can intercept malloc calls from any program without touching its source code. No recompilation, no source modifications. Linux lets you do this with an environment variable called LD_PRELOAD.

What LD_PRELOAD Actually Does

When you run a program on Linux, the dynamic linker patches up all the function calls. Your program says "call malloc" but the binary does not know where malloc lives. The dynamic linker finds malloc in a shared library like libc.so and wires up the address so the call works at runtime.

The linker resolves symbols from libraries in a specific order. LD_PRELOAD lets you inject your own shared library before everything else. If your library exports a function named malloc, the linker binds all malloc calls to your version instead of the real one. Your function wins because it loads first. This works for any dynamically linked program, from ls to nginx.

Simple malloc Hook

Here is the naive approach most people try first:

void* malloc(size_t size) {
    printf("malloc called: %zu\n", size);
    return malloc(size);  // problem here
}

This looks reasonable. You want to print the size being allocated, then do the actual allocation. But this code will crash or hang. The problem is that printf itself calls malloc internally. When you call printf inside your malloc, you recursively call your malloc again. That printf calls malloc, which calls printf, which calls malloc. You get infinite recursion until the stack overflows.

Even if you removed the printf, the return malloc(size) line still calls your own malloc, not the original one. The dynamic linker already bound malloc to your function. So you end up calling yourself forever.

The Recursion Problem

To hook malloc properly, you need to call the original malloc after doing your work. But how do you reach past your own hook to get the real function!

You cannot just declare extern void* __real_malloc or something like that. The real malloc lives in libc. so, and the dynamic linker has already redirected the symbol to your code. You need a way to bypass the normal symbol resolution and fetch the original function pointer directly from the library.

Using dlsym to Call the Real malloc

The dlsym function from libdl lets you look up symbols dynamically. It normally searches loaded libraries for a function name. But dlsym has a special flag called RTLD_NEXT that changes the search behavior. Instead of starting from the beginning of the library list, RTLD_NEXT starts searching from the next library after the one containing your code.

Since your hook library loaded first, the next library in the chain is libc. so. So dlsym(RTLD_NEXT, "malloc") returns the pointer to the real malloc in the C library, bypassing your hook.

Here is how you call the real malloc:

#define _GNU_SOURCE
#include <dlfcn.h>

void* real_malloc = dlsym(RTLD_NEXT, "malloc");

RTLD_NEXT is the key. Without it, dlsym would find your own malloc and you would be back to the recursion problem.

Building the Hooking Library

Now we can build the complete hook. The code needs to:

  1. Store a pointer to the real malloc
  2. Define our malloc that prints information
  3. Call the real malloc to get actual memory
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

static void* (*real_malloc)(size_t) = NULL;

void* malloc(size_t size) {
    if (!real_malloc) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    printf("malloc(%zu)\n", size);
    return real_malloc(size);
}

We use a static function pointer to cache the result of dlsym. The first time malloc is called, we look up the real function. Every subsequent call reuses the cached pointer.

Note the initialization check: if (!real_malloc). This handles the case where someone calls malloc before the library fully initializes. In practice, dlsym works fine here because libdl is already loaded.

Compiling the Shared Library

To compile this code into a shared library:

gcc -shared -fPIC hook.c -o hook.so -ldl

The flags matter:

  • -shared: Tell the compiler to produce a shared library instead of an executable
  • -fPIC: Generate position-independent code. Shared libraries can load at any memory address, so the code must work regardless of where it ends up
  • -ldl: Link against libdl to get dlsym and RTLD_NEXT

Without -fPIC, the linker will complain about relocations in read-only sections. Without -shared, you get an executable that cannot be loaded as a library.

Using LD_PRELOAD

Now you can hijack malloc in any program:

LD_PRELOAD=./hook.so ls

Every malloc call inside ls now goes through your function before reaching the real implementation. The LD_PRELOAD environment variable tells the dynamic linker to load hook. so first. Your symbols shadow the ones in libc.

This works for any dynamically linked binary. You can preload against cat, python, node, or a database server. The hook runs in the target process's address space with full access to its memory.

Example Output

Running ls with the hook attached:

$ LD_PRELOAD=./hook.so ls
malloc(472)
malloc(120)
malloc(104)
malloc(32)
malloc(16)
malloc(8)
malloc(8)
malloc(16896)
...

You see every allocation the program makes. The sizes tell you about the program's behavior: small allocations for strings and data structures, large allocations for buffers.

If you hook a more complex program like curl, you will see hundreds of allocations as it parses URLs, builds headers, and manages SSL state. You get visibility into code you did not write and cannot modify.

Real-World Use Cases

Memory debugging tools like valgrind and sanitizers use LD_PRELOAD to instrument allocations. Profiling tools track allocation counts and sizes for leak detection. Security researchers intercept malloc to study heap behavior or inject canaries to detect overflows.

You can extend the hook to track allocation counts, log call stacks, or add guard pages around returned pointers. The technique works for any function in a shared library, not just malloc. You can hook open, socket, read, write, or any other library call. Here is the complete working hook:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

static void* (*real_malloc)(size_t) = NULL;

void* malloc(size_t size) {
    if (!real_malloc) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    printf("malloc(%zu)\n", size);
    return real_malloc(size);
}

Compile with:

gcc -shared -fPIC hook.c -o hook.so -ldl

Run with:

LD_PRELOAD=./hook.so ./target_program

Your hook loads first. Your malloc runs instead of the real one. You call the real malloc through the pointer from dlsym. The program keeps working, but you control the allocation path.

LD_PRELOAD is a powerful tool. You can modify program behavior without recompilation, debug code you do not own, and build instrumentation that sits between the program and the operating system. Understanding dynamic linking makes you a better systems programmer.