Patching function pointer bodies

I’m trying to implement a simple hook for function pointers from dynamic libraries.

The library is compiled in the build.zig like this:

const exampleLib = b.addSharedLibrary(.{ .name = "example_math", .target = target, .optimize = optimize });
exampleLib.addCSourceFile(.{ .file = .path = "crsr/add.c" }, .flags = &.{} });
exampleLib.linkLibC();

And the source of the library is just this:

int add(int a, int b) {
  return a + b;
}

However everything I’ve tried to overwrite the function body segfaults.
For example casting the body to a multiple pointer:

const lib = std.DynLib.open("./zig-out/lib/example_math.so");
defer lib.close();

const add = lib.lookup(*fn (c_int, c_int) callconv(.C) c_int, "add").?;
try std.testing.expect(add(1, 2) == 3);

const body: [*]u8 = @ptrCast(add);
bytes[0] = 0; // segfault
Segmentation fault at address 0x7fb3a9393390
/home/de/guh/src/main.zig:73:9: 0x1026a8d in main (guh)
    body[0] = 0;
        ^
/usr/lib/zig/std/start.zig:511:37: 0x10241ba in posixCallMainAndExit (guh)
            const result = root.main() catch |err| {
                                    ^
/usr/lib/zig/std/start.zig:253:5: 0x1023d11 in _start (guh)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)

Am I missing something?

1 Like

Hello @T0bee

Yes, you are trying to write data to the function code, this is not possible because the executable code is not writable.
What are you trying to achieve?

2 Likes

I’m trying to write something similar to minhook that allows me to hook a specific address and inject my own logic.

For example, assume I have 2 functions add and hook:

add: ; int add(int a, int b) { return a + b; }
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     edx, DWORD PTR [rbp-4]
        mov     eax, DWORD PTR [rbp-8]
        add     eax, edx
        pop     rbp
        ret

hook: ; int hook(int a, int b) { return 1; }
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     eax, 1
        pop     rbp
        ret

To “hook” add I’d overwrite the first x bytes with an unconditional jump to hook

add:
        mov %r10, <address of label 
        jmp %r10
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     edx, DWORD PTR [rbp-4]
        mov     eax, DWORD PTR [rbp-8]
        add     eax, edx
        pop     rbp
        ret

That way I can intercept any calls to add with my own logic.
Basically I’m trying to write to the executable code.

You will need to use your OS API to make the memory writeable. E.g. minhook uses Windows’ VirtualProtect.

Since it seems that you are on Linux, you could use mprotect. This requires a little alignment since you need to pass in a page-aligned address and size.

const lib = try std.DynLib.open("./zig-out/lib/example_math.so");
defer lib.close();

const add = lib.lookup(*fn (c_int, c_int) callconv(.C) c_int, "add").?;
try std.testing.expect(add(1, 2) == 3);

var lib_pages = lib.memory;
lib_pages.len = std.mem.alignForward(usize, lib_pages.len, std.mem.page_size);
try std.os.mprotect(lib_pages, std.os.PROT.READ | std.os.PROT.WRITE);

const body: [*]u8 = @ptrCast(add);
body[0] = 0;
6 Likes

Thanks, I didn’t realize the OS has extra APIs for memory access protection.