linuxgcclinkerldelf

Override symbols in non-dynamic sections of dynamically linked executable


Is it possible to override symbols in the non-dynamic segments of dynamically linked executable? (For example: the .text and .(ro)data segments of an ELF executable on Linux.)

I know that it is possible to override references in the dynamic section using LD_PRELOAD.

An answer at What is the difference between LD_PRELOAD_PATH and LD_LIBRARY_PATH? claims that LD_PRELOAD also allows overriding statically linked functions, but I'm not sure how. The man page says

LD_PRELOAD A list of additional, user-specified, ELF shared objects to be loaded before all others. This feature can be used to selectively override functions in other shared objects.

so unless everything in a dynamically linked executable is a shared object (only some are included and some are referenced?) this sounds like what I want isn't possible using LD_PRELOAD, but maybe still using some other tool?


Solution

  • Your terminology suggests some haziness about the anatomy of an ELF program but I believe you are asking: If the definition of symbol foo is statically linked into a program, can that definition by dynamically overridden with an alternative definition of foo provided by a shared library pre-loaded at runtime?

    In that case the answer is No.

    You'll see why if you know how some parts of an ELF program actually enable the dynamic linker, at runtime, to bind symbols it can see in the program to definitions that it finds in shared libraries (whether or not such a binding is overriding any prior or alternative one), and if you also know how symbols that have statically linked definitions in the program may or may not be visible to the dynamic linker to do anything with.

    When I talk about symbols in an ELF file here, I always exclude local symbols (a.k.a symbols with internal linkage) such as are created by the C storage-class specifier static. They are not visible either to the static linker or the dynamic linker. When I say that a symbol is statically linked into the program, I mean that the static linker physically merges its definition into a code or data section of the program executable.

    Gist

    By default, the static linker does not enter any symbol in the dynamic symbol table of a program that is statically defined in the program. It is impossible for the dynamic linker to bind such a symbol to another definition from a shared library, whether pre-loaded or loaded in the normal course of events, because the dynamic linker does not see any such symbol. The static linker can be expressly directed to add symbols that are statically defined in the program to its dynamic symbol table, but they appear there as defined and the dynamic linker cannot be coerced to redefine a defined symbol that it sees there.

    The dynamic symbol table and dynamic section of a program

    A symbol referenced by an ELF program that is visible to the dynamic linker is a dynamic symbol. A dynamic symbol is exposed to the dynamic linker in the dynamic symbol table (.dynsym section) of the program. The dynamic symbols are the only ones that the dynamic linker can bind to definitions provided by shared libraries because they are the only symbols seen by the dynamic linker.

    By default all the symbols in a the dynamic symbol table of a program will be undefined symbols. That's because the dynamic symbol table of a program is put there by the static linker at buildtime and its default purpose is to inform the dynamic linker, at runtime, of the symbols which it must bind to definitions it finds in the shared libraries it loads at the program's request. So such symbols are undefined in the program: they are defined in the shared libraries it needs to load. If any symbols appear in the dynamic symbol table as defined, that means they are defined in the program and the dynamic linker of course will not bother trying to find other definitions for them in the shared libraries.

    All symbols that are referenced in the program executable are referenced in object files that are statically linked into it (either object files explicitly input in the static linker's commandline or object files selected for extraction from static libraries input in the commandline).

    The undefined symbols that are entered into the program's dynamic symbol table by default are the ones for which the static linker has found references in statically linked object files and:

    By definition, those shared libraries wont't (and can't) be statically linked into the program, hence these symbols cannot be defined in the program. Instead, the static linker just puts into the program the fact that it needs those shared libraries, for the information of the dynamic linker at runtime. It puts a dynamic section into the program (.dynamic section) and in that section it writes (among other things) a list of the shared libraries that the dynamic linker will need to load to resolve all the undefined symbols that it has written into the program's dynamic symbol table.

    An runtime, the dynamic linker reads the program's dynamic symbol table (.dynsym) and learns what undefined dynamic symbols it has to resolve using shared libraries. It reads the program's dynamic section (.dynamic) and learns what shared libraries it must find and load to resolve those symbols. It goes to work, using an algorithmically defined sequence of directories in which to search for shared libraries. The work is recursive, because each shared library that it loads has its own dynamic symbol table, listing the symbols that are defined in it, as well as the ones that it references but does not define; and it has its own dynamic section that lists the shared libraries it needs to resolve its undefined symbols. If the work finishes with all the recursively needed shared libraries found and all of the recursively discovered undefined references resolved by the recursively discovered definitions, then the program gets to start successfully.

    The global symbol table of a program

    The dynamic symbol table of a program is distinct from its global symbol table (.symtab section). The global symbol table lists all global symbols in the program that were referenced in the object files statically linked into the program. By default all of these symbols will appear as defined, because if any symbol is referenced in the program and is undefined in the program, then either:-

    a) a definition was found by the static linker in some shared library, or

    b) no definition was found by the static linker at all.

    In case a) the symbol will be listed in the dynamic symbol table (as undefined) and not listed in the global symbol table. In case b) the static linker by default will fail the linkage with an unresolved reference error, so the program will not even exist.

    But a statically linked definition with its symbol in the global symbol table is invisible to the dynamic linker. The global symbol table can be stripped out (man strip) with no effect on execution of the program. Its existence only supports tools that investigate or manipulate ELF files.

    But that's all by default. What about coercion?

    That's a dead end too.

    You can coerce the static linker to link a program that contains unresolved symbol references (linker option -z=undefs). That is obviously of no help because you are interested in dynamically overriding the statically linked definition of a defined symbol.

    You can also coerce the static linker to add all, or a selection, of the symbols it puts in global symbol table of a program into the dynamic symbol table as well: linker option --export-dynamic to add them all (the GCC linkage option -rdynamic enables that one), linker option --export-dynamic-symbol=sym to add one, linker option --export-dynamic-symbol-list=file to export a selection. But that doesn't help either because these statically defined symbols will be added to the dynamic symbol table as defined. The usefulness of that is that it enables shared libraries that are loaded by the program to contain undefined references to symbols that the dynamic linker will resolve to definitions in the program. It is of no use for dynamically overriding such a definition, because the dynamic linker will not seek to resolve a symbol that is already defined.

    A static linker could be written that classifies a symbol in the dynamic symbol table with a new type, say, defined-as-last-resort, meaning the the dynamic linker should bind it to the definition statically linked into the file that exports it failing a definition in any (other) shared library. Perverse, but it would do the trick. But if such a static linker has been written, it's not the GNU/linux linker.

    A worked illustration

    $ cat main.c
    void statically_linked(void);
    void dynamically_linked(void);
    
    int main(void)
    {
        statically_linked();
        dynamically_linked();
        return 0;
    }
    
    $ cat static.c 
    #include <stdio.h>
    
    void statically_linked(void)
    {
        puts(__FUNCTION__);
    }
    
    $ cat dynamic.c 
    #include <stdio.h>
    
    void dynamically_linked(void)
    {
        puts(__FUNCTION__);
    }
    
    $ cat preload_static.c
    #include <stdio.h>
    
    void statically_linked(void)
    {
        printf("%s: pre-loaded\n",__FUNCTION__);
    }
    
    $ gcc -shared -o libdynamic.so dynamic.c
    $ gcc -shared -o libpreload_static.so preload_static.c
    
    $ gcc -o prog main.c static.c -L. -ldynamic
    
    $ LD_LIBRARY_PATH=./ ./prog
    statically_linked
    dynamically_linked
    
    $ readelf --syms --wide prog | egrep \(.symtab\|.dynsym\|ally_linked\)
    Symbol table '.dynsym' contains 8 entries:
         5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dynamically_linked
    Symbol table '.symtab' contains 40 entries:
        32: 0000000000001182    26 FUNC    GLOBAL DEFAULT   16 statically_linked
        33: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dynamically_linked
        
    

    The dynamic section of prog contains:

    $ readelf --dynamic --wide ./prog | grep NEEDED
     0x0000000000000001 (NEEDED)             Shared library: [libdynamic.so]
     0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
     
    

    The dynamic linker will find libc.so.6 by default search. It will have to be told where to look for libdynamic.so, which we did with LD_LIBRARY_PATH=./. Alternatively we could have got the static linker to add this information to the dynamic section:

    $ gcc -o prog main.c static.c -L. -ldynamic -Wl,-rpath=$(pwd)
    
    $ readelf --dynamic --wide ./prog | egrep \(NEEDED\|RUNPATH\)
     0x0000000000000001 (NEEDED)             Shared library: [libdynamic.so]
     0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
     0x000000000000001d (RUNPATH)            Library runpath: [/home/imk/develop/so/scrap]
     
    $ unset LD_LIBRARY_PATH
    $ ./prog
    statically_linked
    dynamically_linked
    

    statically_linked appears as defined in the global symbol table only. dynamically_linked appears as undefined (UND) in both the global symbol table and the dynamic symbol table. dynamically_linked is the only symbol that is seen by the dynamic linker, so it is the only one for which the dynamic linker could be induced to seek a definition in a some shared library pre-loaded before libdynamic.so, where it will otherwise find the same definition discovered by the static linker.

    Let's rebuild prog this time adding statically_linked to the dynamic symbol table:

    $ gcc -o prog main.c static.c -rdynamic -L. -ldynamic -Wl,-rpath=$(pwd)
    $ readelf --syms --wide prog | egrep \(.symtab\|.dynsym\|ally_linked\)
    Symbol table '.dynsym' contains 17 entries:
         5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dynamically_linked
        16: 0000000000001182    26 FUNC    GLOBAL DEFAULT   16 statically_linked
    Symbol table '.symtab' contains 40 entries:
        34: 0000000000001182    26 FUNC    GLOBAL DEFAULT   16 statically_linked
        35: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dynamically_linked
            
    

    There it is - it's defined (in section 16, which is .text). Let's try to override the statically linked definition by pre-loading libpreload_static.so. That should result in the program outputing statically_linked: preloaded instead of statically_linked

    $ LD_PRELOAD=./libpreload_static.so ./prog
    statically_linked
    dynamically_linked
    

    But it doesn't. statically_linked is defined in the dynamic symbol table. The dynamic linker sees nothing to be done.

    If that's convincing, stop. But I anticipate...

    What about the cited example where `LD_PRELOAD` supposedly trumps a statically linked definition?

    This is the example presented by @hackerb9 in this answer, which purportedly showed that:

    LD_PRELOAD can replace functions statically linked into a binary.

    But the answer does not actually demonstrate that. It just implies that @hackerb9 observed it to be so after doing what is described in the example.

    The implied claim is that:

    Since that defies first principles, our only resort is to test it by reproduction. Sadly that's a long addendum, but...

    Let's do it!

    For this, my modified readline shared library will be built from the source:

    $ cat badrl.c 
    #include <stddef.h>
    #include <assert.h>
    
    char * readline (const char *prompt) {
        assert(0);
        return NULL;
    }
    
    #ifdef PROGRAM
    int main(void)
    {
        (void)readline(NULL);
        return 0;
    }
    #endif
    

    A program that calls my readline function is:

    $ gcc -o prog -DPROGRAM badrl.c
    

    and it aborts:

    $ ./prog
    prog: badrl.c:5: readline: Assertion `0' failed.
    Aborted (core dumped)
    

    Build my shared readline library, called libbadrl.so:

    $ gcc -shared -o libbadrl.so badrl.c
    

    My installed bash version is 5.2.21. I'll build that one locally from source to tinker with it:

    $ wget https://ftp.gnu.org/gnu/bash/bash-5.2.21.tar.gz
    ...[all good]...
    $ tar -xzf bash-5.2.21.tar.gz 
    $ cd bash-5.2.21/
    $ ./configure
    ...[all good]...
    

    I'll grep the gcc linkage line for bash out of the make output for later use:

    $ make 2>&1 | grep 'gcc .*-o bash .*'
    gcc -L./builtins -L./lib/readline -L./lib/readline -L./lib/glob -L./lib/tilde -L./lib/malloc -L./lib/sh  -rdynamic -g -O2   -o bash shell.o eval.o y.tab.o general.o make_cmd.o print_cmd.o  dispose_cmd.o execute_cmd.o variables.o copy_cmd.o error.o expr.o flags.o jobs.o subst.o hashcmd.o hashlib.o mailcheck.o trap.o input.o unwind_prot.o pathexp.o sig.o test.o version.o alias.o array.o arrayfunc.o assoc.o braces.o bracecomp.o bashhist.o bashline.o  list.o stringlib.o locale.o findcmd.o redir.o pcomplete.o pcomplib.o syntax.o xmalloc.o  -lbuiltins -lglob -lsh -lreadline -lhistory -ltermcap -ltilde -lmalloc    -ldl
    

    Notice - scroll right - that the gcc linkage options include -rdynamic. That enables --export-dynamic for the linker and means that all the symbols in the global symbol table will be added to the dynamic symbol table. We've already observed that makes no difference.

    I've got a ./bash from that make. Let's check the symbol tables for readline:

    $ readelf -Ws ./bash | egrep \(.symtab\|.dynsym\|FUNC.*readline$\)
    Symbol table '.dynsym' contains 2603 entries:
       932: 00000000000d1c90   153 FUNC    GLOBAL DEFAULT   16 readline
      1323: 0000000000099510  2039 FUNC    GLOBAL DEFAULT   16 initialize_readline
    Symbol table '.symtab' contains 3995 entries:
      1846: 0000000000099510  2039 FUNC    GLOBAL DEFAULT   16 initialize_readline
      2260: 00000000000d1c90   153 FUNC    GLOBAL DEFAULT   16 readline
      
    

    Yes, it's defined and has been statically linked per the .symtab, and -rdynamic has put it into .dynsym as well.

    Check my bash runs, and its read command works:

    $ ./bash
    $ read
    Hello World!
    $ echo $REPLY
    Hello World!
    $ exit
    exit
    

    Good. Now I'll compile my aborting badrl.c into a object file (PROGRAM not defined):

    $ gcc -c -o badrl.o ../badrl.c
    

    And I'll repeat the gcc linkage command for bash with the following tweaks:

    That gives:

    $ gcc -L./builtins -L./lib/glob -L./lib/tilde -L./lib/malloc -L./lib/sh  -rdynamic -g -O2   -o badrlbash badrl.o shell.o eval.o y.tab.o general.o make_cmd.o print_cmd.o  dispose_cmd.o execute_cmd.o variables.o copy_cmd.o error.o expr.o flags.o jobs.o subst.o hashcmd.o hashlib.o mailcheck.o trap.o input.o unwind_prot.o pathexp.o sig.o test.o version.o alias.o array.o arrayfunc.o assoc.o braces.o bracecomp.o bashhist.o bashline.o  list.o stringlib.o locale.o findcmd.o redir.o pcomplete.o pcomplib.o syntax.o xmalloc.o  -lbuiltins -lglob -lsh -Wl,--push-state,-Bdynamic,-lreadline,--pop-state -lhistory -ltermcap -ltilde -lmalloc    -ldl
    /usr/bin/ld: ./lib/sh/libsh.a(tmpfile.o): in function `sh_mktmpname':
    /home/imk/develop/so/scrap/bash-5.2.21/lib/sh/tmpfile.c:160:(.text+0x18f): warning: the use of `mktemp' is dangerous, better use `mkstemp' or `mkdtemp'
    

    And I've got:

    $ ./badrlbash 
    badrlbash: ../badrl.c:5: readline: Assertion `0' failed.
    Aborted (core dumped)
    

    Now if @hackerb9 is right, then I can fix this broken bash by overriding the statically linked definition of readline by pre-loading the system libreadline.so, which is:

    $ find /usr/lib -name libreadline.so
    /usr/lib/x86_64-linux-gnu/libreadline.so
    

    Here goes:

    $ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libreadline.so ./badrlbash 
    badrlbash: ../badrl.c:5: readline: Assertion `0' failed.
    Aborted (core dumped)
    

    But it makes no difference.

    Likewise, if @hackerb9 is right, then by pre-loading my libbadrl.so I can cause an abort in the stock bash 5.2.21 I built first.

    $ LD_PRELOAD=../libbadrl.so ./bash
    $ read
    Hello World!
    $ echo $REPLY
    Hello World!
    $ exit
    exit
    

    But that doesn't happen either.