bonanza
bonanza

Reputation: 280

avoid LD_PRELOAD: Wrap library and provide functionality requested from libc

I have a shared library, say somelib.so, which uses ioctl from libc (according to objdump).

My goal is to write a new library that wraps around somelib.so and provides a custom ioctl. I want to avoid preloading a library to ensure that only the calls in somelib.so use the custom ioctl.

Here is my current snippet:

typedef int (*entryfunctionFromSomelib_t) (int par, int opt);
typedef int (*ioctl_t) (int fd, int request, void *data);
ioctl_t real_ioctl = NULL;

int ioctl(int fd, int request, void *data )
{
    fprintf( stderr, "trying to wrap ioctl\n" );
    void *handle = dlopen( "libc.so.6", RTLD_NOW );
    if (!handle)
        fprintf( stderr, "Error loading libc.so.6: %s\n", strerror(errno) );

    real_ioctl = (ioctl_t) dlsym( handle, "ioctl" );
    return real_ioctl( fd, request, data);
}

int entryfunctionFromSomelib( int par, int opt ) {
    void *handle = dlopen( "/.../somelib.so", RTLD_NOW );
    if (!handle)
        fprintf( stderr, "Error loading somelib.so: %s\n", strerror(errno) );

    real_entryfunctionFromSomelib = entryfunctionFromSomelib_t dlsym( handle, "entryfunctionFromSomelib" );
    return real_entryfunctionFromSomelib( par, opt );
}

However, it does not in work in the sense that the calls to ioctl form somelib.so are not redirected to my custom ioctl implementation. How can I enforce that the wrapped somelib.so does so?

======================

Additional information added after @Nominal Animal post:

Here some information from mylib.so (somelib.so after edit) obtained via readelf -s | grep functionname:

   246: 0000000000000000   121 FUNC    GLOBAL DEFAULT  UND dlsym@GLIBC_2.2.5 (11)
 42427: 0000000000000000   121 FUNC    GLOBAL DEFAULT  UND dlsym@@GLIBC_2.2.5


   184: 0000000000000000    37 FUNC    GLOBAL DEFAULT  UND ioctl@GLIBC_2.2.5 (6)
 42364: 0000000000000000    37 FUNC    GLOBAL DEFAULT  UND ioctl@@GLIBC_2.2.5

After 'patching' mylib.so it also shows the new function as:

   184: 0000000000000000    37 FUNC    GLOBAL DEFAULT  UND iqct1@GLIBC_2.2.5 (6)

I 'versioned' and exported the symbols from my wrap_mylib library for which readelf now shows:

25: 0000000000000d15   344 FUNC    GLOBAL DEFAULT   12 iqct1@GLIBC_2.2.5
63: 0000000000000d15   344 FUNC    GLOBAL DEFAULT   12 iqct1@GLIBC_2.2.5

However, when I try to dlopen wrap_mylib I get the following error:

symbol iqct1, version GLIBC_2.2.5 not defined in file libc.so.6 with link time reference

Is that maybe because mylib.so tries to dlsym iqct1 from libc.so.6 ?

Upvotes: 2

Views: 1752

Answers (1)

Nominal Animal
Nominal Animal

Reputation: 39356

If binutils' objcopy could modify dynamic symbols, and the mylib.so is an ELF dynamic library, we could use

mv  mylib.so  old.mylib.so
objcopy --redefine-sym ioctl=mylib_ioctl  old.mylib.so  mylib.so

to rename the symbol name in the library from ioctl to mylib_ioctl, so we could implement

int mylib_ioctl(int fd, int request, void *data);

in another library or object linked to the final binaries.

Unfortunately, this feature is not implemented (as of early 2017 at least).


We can solve this using an ugly hack, if the replacement symbol name is exactly the same length as the original name. The symbol name is a string (both preceded and followed by a nul byte) in the ELF file, so we can just replace it using e.g. GNU sed:

LANG=C LC_ALL=C sed -e 's|\x00ioctl\x00|\x00iqct1\x00|g' old.mylib.so > mylib.so

This replaces the name from ioctl() to iqct1(). It is obviously less than optimal, but it seems the simplest option here.

If you find you need to add version information to the iqct1() function you implement, with GCC you can simply add a line similar to

__asm__(".symver iqct1,iqct1@GLIBC_2.2.5");

where the version follows the @ character.


Here is a practical example, showing how I tested this in practice.

First, let's create mylib.c, representing the sources for mylib.c (that the OP does not have -- otherwise just altering the sources and recompiling the library would solve the issue):

#include <unistd.h>
#include <errno.h>

int myfunc(const char *message)
{
    int retval = 0;

    if (message) {
        const char *end = message;
        int         saved_errno;
        ssize_t     n;

        while (*end)
            end++;

        saved_errno = errno;

        while (message < end) {
            n = write(STDERR_FILENO, message, (size_t)(end - message));
            if (n > 0)
                message += n;
            else {
                if (n == -1)
                    retval = errno;
                else
                    retval = EIO;
                break;
            }
        }

        errno = saved_errno;
    }

    return retval;
}

The only function exported is myfunc(message), as declared in mylib.h:

#ifndef   MYLIB_H
#define   MYLIB_H

int myfunc(const char *message);

#endif /* MYLIB_H */

Let's compile the mylib.c into a dynamic shared library, mylib.so:

gcc -Wall -O2 -fPIC -shared mylib.c -Wl,-soname,libmylib.so -o mylib.so

Instead of write() from the C library (it's a POSIX function just like ioctl(), not a standard C one), we wish to use mywrt() of our own design in our own program. The above command saves the original library as mylib.so (while naming it internally as libmylib.so), so we can use

sed -e 's|\x00write\x00|\x00mywrt\x00|g' mylib.so > libmylib.so

to alter the symbol name, saving the modified library as libmylib.so.

Next, we need a test executable, that provides the ssize_t mywrt(int fd, const void *buf, size_t count); function (the prototype being the same as the write(2) function it replaces. test.c:

#include <stdlib.h>
#include <stdio.h>
#include "mylib.h"

ssize_t mywrt(int fd, const void *buffer, size_t bytes)
{
    printf("write(%d, %p, %zu);\n", fd, buffer, bytes);
    return bytes;
}
__asm__(".symver mywrt,mywrt@GLIBC_2.2.5");

int main(void)
{
    myfunc("Hello, world!\n");

    return EXIT_SUCCESS;
}

The .symver line specifies version GLIBC_2.2.5 for mywrt.

The version depends on the C library used. In this case, I ran objdump -T $(locate libc.so) 2>/dev/null | grep -e ' write$', which gave me

00000000000f66d0  w   DF .text  000000000000005a  GLIBC_2.2.5 write

the second to last field of which is the version needed.

Because the mywrt symbol needs to be exported for the dynamic library to use, I created test.syms:

{
    mywrt;
};

To compile the test executable, I used

gcc -Wall -O2 test.c -Wl,-dynamic-list,test.syms -L. -lmylib  -o test

Because libmylib.so is in the current working directory, we need to add current directory to the dynamic library search path:

export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH

Then, we can run our test binary:

./test

It will output something like

write(2, 0xADDRESS, 14);

because that's what the mywrt() function does. If we want to check the unmodified output, we can run mv -f mylib.so libmylib.so and rerun ./test, which will then output just

Hello, world!

This shows that this approach, although depending on very crude binary modification of the shared library file (using sed -- but only because objcopy does not (yet) support --redefine-sym on dynamic symbols), should work just fine in practice.

This is also a perfect example of how open source is superior to proprietary libraries: the amount of effort already spent in trying to fix this minor issue is at least an order of magnitude higher than it would have been to rename the ioctl call in the library sources to e.g. mylib_ioctl(), and recompile it.


Interposing dlsym() (from <dlfcn.h>, as standardized in POSIX.1-2001) in the final binary seems necessary in OP's case.

Let's assume the original dynamic library is modified using

sed -e 's|\x00ioctl\x00|\x00iqct1\x00|g;
        s|\x00dlsym\x00|\x00d15ym\x00|g;' mylib.so > libmylib.so

and we implement the two custom functions as something like

int iqct1(int fd, unsigned long request, void *data)
{
    /* For OP to implement! */
}
__asm__(".symver iqct1,iqct1@GLIBC_2.2.5");

void *d15ym(void *handle, const char *symbol)
{
    if (!strcmp(symbol, "ioctl"))
        return iqct1;
    else
    if (!strcmp(symbol, "dlsym"))
        return d15ym;
    else
        return dlsym(handle, symbol);
}
__asm__(".symver d15ym,d15ym@GLIBC_2.2.5");

Do check the versions correspond to the C library you use. The corresponding .syms file for the above would contain just

{ i1ct1; d15ym; };

otherwise the implementation should be as in the practical example shown earlier in this answer.

Because the actual prototype for ioctl() is int ioctl(int, unsigned long, ...);, there are no quarantees that this will work for all general uses of ioctl(). In Linux, the second parameter is of type unsigned long, and the third parameter is either a pointer or a long or unsigned long -- in all Linux architectures pointers and longs/unsigned longs have the same size --, so it should work, unless the driver implementing the ioctl() is also closed, in which case you are simply hosed, and limited to either hoping this works, or switching to other hardware with proper Linux support and open-source drivers.

The above special-cases both original symbols, and hard-wires them to the replaced functions. (I call these replaced instead of interposed symbols, because we really do replace the symbols the mylib.so calls with these ones, rather than interpose calls to ioctl() and dlsym().)

It is a rather brutish approach, but aside from using sed due to the lack of dynamic symbol redefinition support in objcopy, it is quite robust and clear as to what is done and what actually happens.

Upvotes: 3

Related Questions