c++clinkerldobjcopy

How can I hook a static function?


I'm trying to mock a static function without modifying the source code. This is because we have a large legacy base of code and we would like to add test code without requiring developers to go through and change a bunch of the original code.

Using objcopy, I can play with functions between object files, but I can't affect internal linkages. In other words, in the code below, I can get main.cpp to call a mocked up foo() from bar.c, but I cannot get UsesFoo() to call the mocked up foo() from bar.c.

I understand this is because foo() is already defined within foo.c. Aside from changing the source code, is there any way I can use ld or another tool to rip out foo() so that final linking pulls it in from my bar.c?

foo.c

#include <stdio.h>

static void foo()
{
    printf("static foo\n");
}

void UsesFoo()
{
    printf("UsesFoo(). Calling foo()\n");
    foo();
}

bar.c

#include <stdio.h>

void foo()
{
    printf("I am the foo from bar.c\n");
}

main.cpp

#include <iostream>

extern "C" void UsesFoo();
extern "C" void foo();

using namespace std;

int main()
{
    cout << "Calling UsesFoo()\n\n";
    UsesFoo();
    cout << "Calling foo() directly\n";
    foo();
    return 0;
}

compiling:

gcc -c foo.c
gcc -c bar.c
g++ -c main.c
(Below simulates how we consume code in the final output)
ar cr libfoo.a foo.o
ar cr libbar.a bar.o
g++ -o prog main.o -L. -lbar -lfoo
This works because the foo() from libbar.a gets included first, but doesn't affect the internal foo() in foo.o

I have also tried:

gcc -c foo.c
gcc -c bar.c
g++ -c main.c
(Below simulates how we consume code in the final output)
ar cr libfoo.a foo.o
ar cr libbar.a bar.o
objcopy --redefine-sym foo=_redefinedFoo libfoo.a libfoo-mine.a
g++ -o prog main.o -L. -lbar -lfoo-mine
This produces the same effect. main will call foo() from bar, but UsesFoo() still calls foo() from within foo.o

Solution

  • long.kl's answer works if you're willing to change the source code. Unfortunately, because we want to keep source code as pristine as possible, this was not usable for us.

    Despite what AndrewHenle thinks in his responses, we can rewrite the object file to allow us to overwrite the static function. This requires understanding and parsing the ELF format the object file is written with.

    The chief issue is that functions within your object file will use relative jumps/branches/calls to addresses in the text segment. In other words, let's assume we have the following code:

    #include <stdio.h>
    
    static void foo() 
    {
        printf("static foo\n");
    }
    
    void UsesFoo()
    {
        printf("UsesFoo(). Calling foo()\n");
        foo();
    }
    

    In this case, with no optimizations ("gcc -c foo.c"), this produces an object file, foo.o, which has the following disassembly:

    objdump -d foo.o
    
    foo.o:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000000000 <foo>:
       0:   55                      push   %rbp
       1:   48 89 e5                mov    %rsp,%rbp
       4:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # b <foo+0xb>
       b:   e8 00 00 00 00          callq  10 <foo+0x10>
      10:   90                      nop
      11:   5d                      pop    %rbp
      12:   c3                      retq   
    
    0000000000000013 <UsesFoo>:
      13:   55                      push   %rbp
      14:   48 89 e5                mov    %rsp,%rbp
      17:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 1e <UsesFoo+0xb>
      1e:   e8 00 00 00 00          callq  23 <UsesFoo+0x10>
      23:   b8 00 00 00 00          mov    $0x0,%eax
      28:   e8 d3 ff ff ff          callq  0 <foo>
      2d:   90                      nop
      2e:   5d                      pop    %rbp
      2f:   c3                      retq   
    

    Take a look at instructions 0xb and 0x1e. Those are the calls that printf() in the c code was translated to. You'll notice after the opcode 0xe8, the rest of the bytes are 0x00. This is because they will be replaced by the linker during final compilation to the address of puts (assuming this is a static linkage).

    Now notice the call instruction at 0x28 is using the address of 0xd3 ff ff ff for it's call. If this was a non-static function, we'd see the same 0x00 bytes after the opcode, but in this case we see 0xd3ffffff. This is a 32 bit relative call that corresponds to -1 in 2's compliment (the final address will become 0 in the instruction pointer). This means our text segment (code) has been hardcoded to use that address.

    In order to get around this, we will have to re-write the ELF to change how the call to foo() is handled. There's a couple of options:

    1. We add another .text.[somename] section to our file that contains code to act as a trampoline, ie: FakeFoo(). We then re-write the first instruction of foo() to jump immediately to FakeFoo(). Hacky, but probably works with loss of debugging information.

    2. The .rela.text section contains function relocations. These are used to tell the linker that we need to replace bytes for calls with final locations. When the linker sees this section, it will replace the addresses in the "offset" field with the real, calculated, addresses in the final binary. For our binary, we see:

    readelf -r foo.o
    
    Relocation section '.rela.text' at offset 0x280 contains 4 entries:
      Offset          Info           Type           Sym. Value    Sym. Name + Addend
    000000000007  000500000002 R_X86_64_PC32     0000000000000000 .rodata - 4
    00000000000c  000b00000004 R_X86_64_PLT32    0000000000000000 puts - 4
    00000000001a  000500000002 R_X86_64_PC32     0000000000000000 .rodata + 7
    00000000001f  000b00000004 R_X86_64_PLT32    0000000000000000 puts - 4
    

    The offsets 0xc and 0x14 are where the call instructions in foo() and UsesFoo() are looking for the puts() function (note: the compiler translated our call to "printf()" to use "puts()").

    So, we can add another entry here for the call at instruction 0x28, and have the linker look for another function called "foo()" somewhere in the code that is not declared static.

    This will also require fixing up the .symtab entry of the ELF file, because it will contain a reference to the local function foo():

    readelf -s foo.o
    
    Symbol table '.symtab' contains 13 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foo.c
         2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
         3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
         4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
         5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
         6: 0000000000000000    19 FUNC    LOCAL  DEFAULT    1 foo
         7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
         8: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
         9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
        10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
        11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
        12: 0000000000000013    29 FUNC    GLOBAL DEFAULT    1 UsesFoo
    

    In order to make the linker look for foo() outside of this object file, we'll have to change the entry for foo to be a "NOTYPE GLOBAL" "UND" type, so the linker doesn't think that it exists in this file.

    There's another section, .rela.eh_frame, used for debugging, that you'll also want to pay attention to.

    Finally, this approach requires you to go through your binary, search for opcodes that correspond to jumps/calls/branches, and create/fix entries so that the linker will look for "foo()" in other object files.

    All of this is just to get the linker to look for foo() in a different file, so that you can replace the original foo() with one that you've written. If you want to call the original foo() after all of this, you'd probably want to rename foo() to something else, ie: _real_foo(), and setup the symbol table (.symtab) so that your fake foo() can do something like:

    bar.c:
    
    void foo()
    {
      printf("I am the fake foo! Calling the real foo!\n");
      __real_foo();
    }
    

    Ultimately, it would be far better (and way easier) if your developers moved the bulk of their functionality from static methods to global ones. However, if you want to re-write the object file after it has been created, under the right circumstances, it can be done with a fair amount of effort.