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

avoid LD_PRELOAD: Wrap library and provide functionality requested from libc

我有一个共享库,比如 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:

这里有一些来自mylib.so的信息(编辑后somelib.so)通过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

在 'patching' mylib.so 之后,它还显示了新函数:

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

我 'versioned' 并从我的 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 吗?

如果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 年初)。


如果替换符号名称的长度与原始名称的长度完全相同,我们可以使用一个丑陋的 hack 来解决这个问题。符号名称是 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 行为 mywrt 指定版本 GLIBC_2.2.5

版本取决于所使用的 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 和 re运行 ./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 类型,第三个参数是指针或长整型或无符号长整型——在所有 Linux 架构指针和 longs/unsigned 中longs 具有相同的大小 --,因此它应该可以工作,除非实现 ioctl() 的驱动程序也已关闭,在这种情况下,您只是简单地使用软管,并且仅限于希望它可以工作,或者切换到其他具有适当硬件的硬件Linux 支持和开源驱动程序。

以上特殊情况都是原始符号,并将它们硬连接到替换函数。 (我称这些符号为 replaced 而不是 interposed 符号,因为我们确实用这些符号替换了 mylib.so 调用的符号,而不是插入对 ioctl()dlsym() 的调用。)

这是一种相当粗暴的方法,但除了使用 sed 之外,由于 objcopy 中缺乏动态符号重新定义支持,它非常​​稳健且清楚地说明了做什么和做什么实际发生了。