6

我有一个项目旨在运行 php-cgi chrooted用于大规模虚拟主机(超过 10k 虚拟主机),每个虚拟主机都有自己的 chroot,在 Ubuntu Lucid x86_64 下。

我想避免在每个 chroot 中为 /dev/null、/dev/zero、语言环境、图标等创建必要的环境,以及认为它们在 chroot 之外运行的 php 模块可能需要的任何东西。

目标是让 php-cgi 在 chroot 内运行,但允许他访问 chroot 之外的文件,只要这些文件(对于大多数文件)以只读模式打开,并且在允许的列表(/dev/日志、/dev/zero、/dev/null、语言环境的路径...)

显而易见的方法似乎是创建(或使用如果存在)一个内核模块,该模块可以在 chroot 之外挂钩和重定向受信任的 open() 路径。但我认为这不是最简单的方法:

  • 我从来没有做过内核模块,所以我没有正确估计难度。
  • 似乎有多个系统调用来挂钩文件“打开”(打开,连接,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() 函数,但没有成功。我还尝试在我的预加载库的 _init() 函数中为 dlopen() 放置一些标志,以测试其中一些标志是否可以使其工作,但没有成功

这是我的预加载库的相关代码:

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

但是 libc 的 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 的 libc 连接符号上设置断点:

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() 调用。

我看到如何做到的其他方式:

  • 使用 dlopen 从 LD_PRELOAD 加载 libc.so,使用 dlsym() 获取 libc 的 __connect 的地址,然后修补函数(ASM 明智)以使钩子工作。这似乎真的矫枉过正并且容易出错。
  • 使用修改后的 PHP 自定义 libc 直接在源头修复这些问题(打开/连接/mmap 函数...)
  • 编写 LKM 代码,将文件访问重定向到我想要的位置。优点:不需要 ioctl(SENDFD),也不需要 chroot 之外的守护进程。

如果可能的话,我真的很感激学习如何仍然挂钩对 openlog_internal 发出的 __connect() 的调用、建议或与系统调用挂钩和重定向相关的内核文档的链接。

我与“hook syscalls”相关的谷歌搜索发现了很多对LSM的引用,但它似乎只允许 ACL 回答“是”或“否”,但不允许重定向 open() 路径。

谢谢阅读。

4

1 回答 1

3

如果不构建您自己的大量修改的 libc,这绝对是不可能的LD_PRELOAD,在这种情况下,您不妨直接将重定向黑客直接放入其中。不一定要调用open,connect等。相反,可能会调用在库创建时绑定的类似隐藏函数(不能动态重新绑定),甚至是内联系统调用,这当然会随着版本的变化而发生不可预测的变化。

您的选项要么是内核模块,要么可能ptrace在“chroot”内的所有内容上使用,并在跟踪过程遇到需要修补的时候修改系统调用的参数。听起来都不容易……

或者您可以接受您需要一组最少的关键设备节点和文件才能存在于 chroot 中以使其工作。如果可能,使用不同的 libc 代替 glibc 将帮助您最大限度地减少所需的附加文件的数量。

于 2011-09-30T15:49:31.973 回答