避免LD_PRELOAD:包装库并提供libc请求的功能

时间:2017-03-23 21:48:53

标签: c shared-libraries ld-preload

我有一个共享库,比如somelib.so,它使用libc中的ioctl(根据objdump)。

我的目标是编写一个包裹somelib.so的新库,并提供自定义ioctl。我想避免预加载库,以确保只有somelib.so中的来电才能使用自定义ioctl

这是我目前的片段:

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 );
}

但是,在ioctl形式somelib.so的调用未被重定向到我的自定义ioctl实现的意义上,它不起作用。如何强制包裹的somelib.so这样做?

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

@Nominal Animal post之后添加的其他信息:

此处通过readelf -s | grep functionname获取mylib.so(编辑后的somelib.so)的一些信息:

   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

修补后' mylib.so它还将新功能显示为:

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

我已经版本化了#39;并从我的wrap_mylib库中导出符号,readelf现在显示:

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

但是,当我尝试dlopen wrap_mylib时,我收到以下错误:

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

这可能是因为mylib.so尝试从libc.so.6 dlsym iqct1

1 个答案:

答案 0 :(得分:2)

如果binutils' objcopy可以修改动态符号,mylib.so是ELF动态库,我们可以使用

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

将库中的符号名称从ioctl重命名为mylib_ioctl,以便我们实施

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

在链接到最终二进制文件的另一个库或对象中。

不幸的是,this feature is not implemented(截至2017年初至少)。

如果替换符号名称与原始名称的长度完全相同,我们可以使用丑陋的黑客解决此问题。符号名称是ELF文件中的字符串(前面和后面都是一个空字节),因此我们可以使用例如GNU sed:

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

这会将名称从ioctl()替换为iqct1()。它显然不是最优的,但它似乎是最简单的选择。

如果您发现需要向您实施的iqct1()功能添加版本信息,可以使用GCC添加类似于

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

其中版本遵循@字符。

这是一个实际的例子,展示了我在实践中如何测试它。

首先,让我们创建 mylib.c ,代表mylib.c的源代码(OP没有 - 否则只需更改源代码并重新编译库就可以解决问题):

#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;
}

导出的唯一功能是myfunc(message),如 mylib.h 中所声明:

#ifndef   MYLIB_H
#define   MYLIB_H

int myfunc(const char *message);

#endif /* MYLIB_H */

让我们将mylib.c编译成动态共享库mylib.so

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

而不是来自C库的write()(它的POSIX函数就像ioctl(),而不是标准的C),我们希望使用我们自己设计的mywrt()在我们自己的计划中。上面的命令将原始库保存为mylib.so(在内部命名为libmylib.so),因此我们可以使用

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

更改符号名称,将修改后的库保存为libmylib.so

接下来,我们需要一个测试可执行文件,它提供ssize_t mywrt(int fd, const void *buf, size_t count);函数(原型与它替换的write(2)函数相同。 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;
}

.symver行为GLIBC_2.2.5指定版本mywrt

版本取决于使用的C库。在这种情况下,我运行了objdump -T $(locate libc.so) 2>/dev/null | grep -e ' write$',它给了我

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

倒数第二个字段是所需的版本。

因为需要导出mywrt符号才能使用动态库,所以我创建了 test.syms

{
    mywrt;
};

为了编译测试可执行文件,我使用了

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

由于libmylib.so位于当前工作目录中,我们需要将当前目录添加到动态库搜索路径中:

export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH

然后,我们可以运行我们的测试二进制文件:

./test

它将输出类似

的内容
write(2, 0xADDRESS, 14);

因为那是mywrt()函数的作用。如果我们要检查未修改的输出,我们可以运行mv -f mylib.so libmylib.so并重新运行./test,然后输出

Hello, world!

这显示了这种方法,虽然取决于共享库文件的非常粗略的二进制修改(使用sed - 但仅仅因为objcopy尚未支持--redefine-sym动态符号),在实践中应该工作得很好。

这也是开源如何优于专有库的一个完美示例:尝试解决这个小问题所花费的努力量至少比它要高出一个数量级。已经将库源中的ioctl调用重命名为例如mylib_ioctl(),然后重新编译它。

在OP的案例中,似乎有必要在最终二进制文件中插入dlsym()(来自<dlfcn.h>,在POSIX.1-2001中标准化)。

假设使用

修改原始动态库
sed -e 's|\x00ioctl\x00|\x00iqct1\x00|g;
        s|\x00dlsym\x00|\x00d15ym\x00|g;' mylib.so > libmylib.so

我们将两个自定义函数实现为

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");

请检查版本是否与您使用的C库相对应。上面的相应.syms文件只包含

{     i1ct1;     d15ym; };

否则实施应如本答案前面所示的实际例子中所述。

因为ioctl()的实际原型是int ioctl(int, unsigned long, ...);,所以没有保证这适用于ioctl()的所有常规用途。在Linux中,第二个参数是unsigned long类型,第三个参数是指针或long或unsigned long - 在所有Linux体系结构中,指针和long / unsigned long具有相同的大小 - 所以它应该工作,除非实现ioctl()的驱动程序也被关闭,在这种情况下你只是被软管,并且仅限于希望这个工作,或切换到具有适当的Linux支持和开源驱动程序的其他硬件。

以上特殊情况下两个原始符号,并将它们硬连接到替换功能。 (我称之为替换而不是插入符号,因为我们确实用这些符号替换mylib.so调用的符号,而不是插入对{{1}的调用}和ioctl()。)

这是一种相当野蛮的方法,但由于dlsym()缺乏动态符号重新定义支持,除了使用sed之外,它对于完成的内容和实际发生的内容非常强大和清晰