使用LD_PRELOAD覆盖从另一个libc函数调用的libc函数

6
我有一个项目,旨在为大量虚拟主机(超过10k个虚拟主机)运行php-cgi chrooted,每个虚拟主机都有自己的chroot,在Ubuntu Lucid x86_64下。我希望避免在每个chroot内创建必要的环境,例如/dev/null、/dev/zero、locales、icons等等,以及可能被php模块需要但认为它们在chroot外运行的任何其他内容。 目标是使php-cgi在chroot内运行,但允许其访问chroot外的文件,只要这些文件(对于大多数文件)以只读模式打开,并且在允许列表中(/dev/log、/dev/zero、/dev/null、本地环境变量的路径等等)。
显然的方法似乎是创建(或使用已存在的)内核模块,该模块可以挂钩和重定向受信任的open()路径,超出chroot之外。但我认为这不是最简单的方法:
  • 我从未做过内核模块,因此无法正确评估难度。
  • 似乎有多个系统调用来挂钩文件“打开”(打开、连接、mmap...),但我想与文件打开相关的所有内容都有一个共同的内核函数。
我希望尽量减少对PHP或其模块的补丁数量,以便在每次更新我们的平台到最新稳定版PHP时,需要的工作量最小化(因此更经常更快地从上游PHP版本进行更新),因此我认为最好从外部修补PHP的行为(因为我们有一个特殊的设置,所以修补PHP并向上游提出补丁不相关)。
相反,我目前正在尝试一种用户空间解决方案:使用LD_PRELOAD挂钩libc函数,在大多数情况下都能很好地运行,并且实现起来非常快,但是我遇到了一个问题,无法独自解决。(想法是与在chroot之外运行的守护进程通信,并使用ioctl SENDFD和RECVFD获取文件描述符)。
当我调用syslog()(没有先调用openlog())时,syslog()会调用connect()来打开文件。
例如:
folays@phenix:~/ldpreload$ strace logger test 2>&1 | grep connect
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
connect(1, {sa_family=AF_FILE, path="/dev/log"}, 110) = 0

到目前为止,我尝试挂钩libc的connect()函数,但没有成功。我还尝试在我的preload库的_init()函数中放置一些dlopen()标志来测试是否有一些标志可以使其工作,但也没有成功。
以下是我preload库的相关代码:
void __attribute__((constructor)) my_init(void)
{
  printf("INIT preloadz %s\n", __progname);
  dlopen(getenv("LD_PRELOAD"), RTLD_NOLOAD | RTLD_DEEPBIND | RTLD_GLOBAL |
                               RTLD_NOW);
}

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
  printf("HOOKED connect\n");
  int (*f)() = dlsym(RTLD_NEXT, "connect");
  int ret = f(sockfd, addr, addrlen);
  return ret;
}

int __connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
  printf("HOOKED __connect\n");
  int (*f)() = dlsym(RTLD_NEXT, "connect");
  int ret = f(sockfd, addr, addrlen);
  return ret;
}

但是标准C库的connect()函数仍然优先于我的函数:

folays@phenix:~/ldpreload$ LD_PRELOAD=./lib-preload.so logger test
INIT preloadz logger
[...] no lines with "HOOKED connect..." [...]
folays@phenix:~/ldpreload$

查看syslog()的代码(apt-get source libc6, glibc-2.13/misc/syslog.c),它似乎调用openlog_internal,后者又调用__connect(),在misc/syslog.c的第386行:

            if (LogFile != -1 && !connected)
            {
                    int old_errno = errno;
                    if (__connect(LogFile, &SyslogAddr, sizeof(SyslogAddr))
                        == -1)
                    {

objdump向我展示了libc的动态符号表中的connect和__connect:

folays@phenix:~/ldpreload$ objdump -T /lib/x86_64-linux-gnu/libc.so.6 |grep -i connec
00000000000e6d00  w   DF .text  000000000000005e  GLIBC_2.2.5 connect
00000000000e6d00  w   DF .text  000000000000005e  GLIBC_2.2.5 __connect

但是在动态重定位项中没有连接符号,所以我猜这就解释了为什么我无法成功覆盖openlog_internal()使用的connect()函数,它可能不使用动态符号重定位,而且可能在硬件中具有__connect()函数的地址(一个相对-fPIC偏移量?)。

folays@phenix:~/ldpreload$ objdump -R /lib/x86_64-linux-gnu/libc.so.6 |grep -i connec
folays@phenix:~/ldpreload$ 

connect是__connect的弱别名:

 eglibc-2.13/socket/connect.c:weak_alias (__connect, connect)

gdb仍然能够在libc的connect符号上设置断点:

folays@phenix:~/ldpreload$ gdb logger
(gdb) b connect
Breakpoint 1 at 0x400dc8
(gdb) r test
Starting program: /usr/bin/logger 

Breakpoint 1, connect () at ../sysdeps/unix/syscall-template.S:82
82      ../sysdeps/unix/syscall-template.S: No such file or directory.
        in ../sysdeps/unix/syscall-template.S
(gdb) c 2
Will ignore next crossing of breakpoint 1.  Continuing.

Breakpoint 1, connect () at ../sysdeps/unix/syscall-template.S:82
82      in ../sysdeps/unix/syscall-template.S
(gdb) bt
#0  connect () at ../sysdeps/unix/syscall-template.S:82
#1  0x00007ffff7b28974 in openlog_internal (ident=<value optimized out>, logstat=<value optimized out>, logfac=<value optimized out>) at ../misc/syslog.c:386
#2  0x00007ffff7b29187 in __vsyslog_chk (pri=<value optimized out>, flag=1, fmt=0x40198e "%s", ap=0x7fffffffdd40) at ../misc/syslog.c:274
#3  0x00007ffff7b293af in __syslog_chk (pri=<value optimized out>, flag=<value optimized out>, fmt=<value optimized out>) at ../misc/syslog.c:131

当然,我可以通过自己进行openlog()来完全跳过这个特定的问题,但我猜我会在一些其他函数中遇到相同类型的问题。
我真的不明白为什么openlog_internal不使用动态符号重定位来调用__connect(),如果使用简单的LD_PRELOAD机制是否可能挂钩此__connect()调用。
我看到它可以完成的其他方式:
  • 从LD_PRELOAD中使用dlopen加载libc.so,使用dlsym()获取libc的__connect地址,然后对该函数进行修补(ASM wise)以使钩子正常工作。这似乎非常繁琐且容易出错。
  • 使用修改过的自定义libc for PHP直接修复这些问题的源代码(打开/连接/映射函数...)
  • 编写LKM,将文件访问重定向到所需位置。优点:无需ioctl(SENDFD)和chroot外部的守护进程。
如果有可能,我真的很想了解如何仍然挂钩由openlog_internal发出的__connect()调用,建议或与系统调用挂钩和重定向相关的内核文档链接,谢谢!
我的谷歌搜索与“hook syscalls”相关,发现了很多关于LSM的参考资料,但它似乎只允许ACL回答“是”或“否”,而不允许重定向open()路径。
谢谢阅读。

第一次提问,非常好而且勇敢的尝试!但是说实话,这超出了我们通常在这里看到的大部分内容,也超出了我们中的大多数人愿意(或有时间)详细阅读的范围。 - Jens Gustedt
我认为,相比于在libc或内核中进行奇怪的操作,特别是在这些领域没有先前经验的情况下,构建传统的chroot会更安全、更易于维护。 - Wyzard
1
抱歉,通常只有在我准备放弃时才会发帖提问,所以我告诉自己我会把我已经分析过的一切都发布上来... :) - folays
@folays:我也经常这样做,最终惊喜地发现了解决方案。大约有50%的人提供了解决方案,而另外50%的人则会通过争论认为没有解决方案,从而产生新的想法,帮助我解决问题。无论如何,欢迎来到SO,并且因为一个有趣而详细的问题给你点赞! - R.. GitHub STOP HELPING ICE
1个回答

3
不经过重度修改的libc不可能使用LD_PRELOAD实现要求,此时您最好直接在内部添加重定向操作。这并不一定涉及open、connect等函数调用,而可能是在创建库时绑定的类似隐藏函数的调用(无法进行动态重新绑定)甚至是内部系统调用,而且版本更新可能会导致不可预测性的变化。
您的选择要么是内核模块,要么是针对“chroot”中的所有内容使用ptrace,并在跟踪进程遇到需要修补的系统调用参数时进行修改。两者听起来都不容易...
或者,您可以接受一个事实:在chroot中,必须存在一组关键设备节点和文件才能使其正常工作。如有可能,使用替代glibc的不同libc将有助于您最小化附加文件的数量。

谢谢建议,ptrace是一种解决方案(在int 80上中断),但感觉很hacky去解析系统调用参数,难以维护,并且对ABI的更改有点不够弹性。我认为会有很多上下文切换,并且我认为它会明显减慢php进程的速度 :( - folays
Syscall ABI 不会改变(或者至少不会删除;虽然可以添加新的内容)。Libc-内部调用约定确实会改变。如果您最终实现了它,那么我会非常感兴趣,因为我正在寻找一个 fakeroot(Debian 实用程序)替代品,它使用 ptrace 而不是 LD_PRELOAD,因此可用于大多数核心程序都是静态链接的系统。当然,如果您最终实现了它,速度会很慢,大致与在 strace 下运行程序的速度相同。 - R.. GitHub STOP HELPING ICE
实际上,另一种方法是在qemu/KVM虚拟化下运行程序(应用级别,而非系统级别),并使用设置/修补过的qemu进行重定向。这可能比ptrace更快。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接