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?
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
.
On x86_64, a near jump (jmp rel32
) is 5 bytes: E9 xx xx xx xx
.
rel32 = dest_address - (patch_address + 5)
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.