--- title: "Hijacking malloc() in Any Linux Program Using LD_PRELOAD" source: "https://x.com/Adriksh/status/2030023097290534973" author: - "[[Unknown]]" published: 2026-03-07 created: 2026-03-07 description: tags: - "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](https://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: ```c 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: ```c #define _GNU_SOURCE #include 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 ```c #define _GNU_SOURCE #include #include 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: ```bash 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: ```bash 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: ```bash $ 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: ```c #define _GNU_SOURCE #include #include 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: ```bash gcc -shared -fPIC hook.c -o hook.so -ldl ``` Run with: ```bash 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.