cshared-libraries

Patching a shared library with additional code before loading


I am developing a fully mod-able native game engine, where it loads all of its functionality dynamically through modular shared libraries. However, as mods may be done by various developers, the APIs won't always give the flexibility needed for some ideas, therefore, there is a need for a last resort, to override any code in any module with an arbitrary function.

The general idea is to make a copy of the file and insert a jmp instruction into the desired function inside the binary, while keeping the file fully aligned, and then simply load it as normal.

I have considered mmap tricks to modify the program in runtime and perhaps LD_PRELOAD under Unix, but the former turns out to require way more work to be done, and the latter is limited only to Unix and does not cover all cases, such as with inlined procedures, therefore these two options are not sufficient.

The most promising idea is therefore to modify a copy of the file. How can I do that?


Solution

  • Suppose you want to patch a function foo in libmod.so and redirect it to your own function at some address.

    First, you'd have to find the function's offset:

    Use nm or objdump:

    nm -D libmod.so | grep ' foo$'
    # or
    objdump -d libmod.so | grep '<foo>:'
    

    Suppose you get offset 0x1234.

    2. Calculate Relative JMP

    On x86_64, a near jump (jmp rel32) is 5 bytes: E9 xx xx xx xx.
    rel32 = dest_address - (patch_address + 5)

    3. Patch the Library

    Here’s a C example to patch a function entry in a copied .so file:

    #include <stdio.h>
    #include <stdint.h>
    #include <stdlib.h>
    
    // Helper to write little-endian 32-bit value
    void write_le32(uint8_t *buf, uint32_t val) {
        buf[0] = val & 0xFF;
        buf[1] = (val >> 8) & 0xFF;
        buf[2] = (val >> 16) & 0xFF;
        buf[3] = (val >> 24) & 0xFF;
    }
    
    int main(int argc, char **argv) {
        if (argc != 5) {
            printf("Usage: %s <input.so> <output.so> <func_offset_hex> <hook_addr_hex>\n", argv[0]);
            return 1;
        }
    
        FILE *in = fopen(argv[1], "rb");
        FILE *out = fopen(argv[2], "wb");
        uint64_t func_off = strtoull(argv[3], NULL, 16);
        uint64_t hook_addr = strtoull(argv[4], NULL, 16);
    
        // Copy entire file
        fseek(in, 0, SEEK_END);
        size_t size = ftell(in);
        rewind(in);
        uint8_t *buf = malloc(size);
        fread(buf, 1, size, in);
    
        // Patch: write JMP at function offset
        uint32_t rel = (uint32_t)(hook_addr - (func_off + 5)); // rel32
        buf[func_off] = 0xE9; // JMP rel32
        write_le32(&buf[func_off + 1], rel);
    
        fwrite(buf, 1, size, out);
        fclose(in);
        fclose(out);
        free(buf);
        return 0;
    }
    
    ./patcher libmod.so libmod_patched.so 1234 567890
    

    You must ensure the patch doesn’t clobber more than the prologue of the function (at least 5 bytes for x86_64).

    Addressing is relative to the file, not the loaded address space—position-independent code complicates this.