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

fol*_*ays 5 c chroot preload

我有一个项目旨在运行php-cgi chrooted for mass virtual hosting(超过10k虚拟主机),每个虚拟主机都有自己的chroot,在Ubuntu Lucid x86_64下.

我想避免在每个chroot中创建必要的环境,例如/ dev/null,/ dev/zero,locales,icons ......以及php模块认为它们在chroot之外运行所需的任何东西.

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

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

  • 我从来没有做过内核模块,所以我没有正确估计难度.
  • 似乎有多个系统调用来挂钩文件"open"(open,connect,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
Run Code Online (Sandbox Code Playgroud)

到目前为止一切顺利,我试图挂钩libc的connect()函数,但没有成功.我还尝试在我的preload库的_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;
}
Run Code Online (Sandbox Code Playgroud)

但是libc的connect()函数仍然优先于我的:

folays@phenix:~/ldpreload$ LD_PRELOAD=./lib-preload.so logger test
INIT preloadz logger
[...] no lines with "HOOKED connect..." [...]
folays@phenix:~/ldpreload$
Run Code Online (Sandbox Code Playgroud)

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

            if (LogFile != -1 && !connected)
            {
                    int old_errno = errno;
                    if (__connect(LogFile, &SyslogAddr, sizeof(SyslogAddr))
                        == -1)
                    {
Run Code Online (Sandbox Code Playgroud)

好吧,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
Run Code Online (Sandbox Code Playgroud)

但是动态重定位条目中没有连接符号,所以我想它解释了为什么我无法成功覆盖openlog_internal()使用的connect(),它可能不使用动态符号重定位,并且可能具有__connect()的地址功能是硬(相对-fPIC偏移?).

folays@phenix:~/ldpreload$ objdump -R /lib/x86_64-linux-gnu/libc.so.6 |grep -i connec
folays@phenix:~/ldpreload$ 
Run Code Online (Sandbox Code Playgroud)

connect是__connect的弱别名:

 eglibc-2.13/socket/connect.c:weak_alias (__connect, connect)
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

当然,我可以通过自己做一个openlog()来完全跳过这个特殊问题,但我想我会遇到与其他一些函数相同类型的问题.

我真的不明白为什么openlog_internal不使用动态符号重定位来调用__connect(),如果甚至可以通过使用简单的LD_PRELOAD机制挂钩这个__connect()调用.

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

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

我真的很感激,如果有可能的话,我仍然可以将对open__ternal,调用或者与syscall挂钩和重定向相关的内核文档的链接发出的__connect()的调用挂钩.

我的与"钩子系统调用"相关的谷歌搜索发现了许多对LSM的引用,但似乎只允许ACL回答"是"或"否",但没有重定向的open()路径.

谢谢阅读.

R..*_*R.. 3

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

您的选择要么是一个内核模块,要么可能使用ptrace“chroot”内的所有内容,并在跟踪进程遇到需要修补的情况时修改系统调用的参数。听起来都不容易...

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