3

注意:所谓的骗子不会以任何方式回答这个问题;特别是,可以通过使用 编译轻松修复线程本地示例-fPIC,就像下面已经提到的 bugzilla 示例一样。其余的只是未经询问的意见和毫无根据的主张。


在最近的 Linux 系统(如 Debian 10、RHEL 8 等)中,您可以创建一个 ELF 文件,它既是位置无关的可执行文件 (PIE),又是动态加载的库/共享对象。

这是非常有用的,并且有很多应用程序,例如创建包装程序,这些程序预先加载然后执行另一个程序(参见下面的图表 2),或者将虚拟机/语言环境作为单个对象,可以嵌入或作为独立的解释器运行。所有这些都无需处理硬连线安装目录、符号链接漏洞、$ORIGINs 或其他此类安全和文件系统策略噩梦。

但是,在 glibc 中更改2c75b54破坏了它:

elf: 拒绝 dlopen PIE 对象 [BZ #24323]

另一个可执行文件已被映射,因此动态链接器无法为第二个可执行文件正确执行重定位

在导致它的讨论中还声称:

也无法正确执行第二个可执行文件的 ELF 构造函数

但这似乎是假的。如下图 1所示,构造函数、重定位和线程局部变量似乎工作正常。

复制重定位不起作用,但是有什么理由也破坏使用复制重定位的程序,就像那些编译的那样-fPIC?(特别是,讨论中的测试用例可以容易地通过编译它来修复)。-fPIC

这个变化也在FreeBSD 12.2 中被选中,主要原因是“glibc 也这样做了”。它在 NetBSD 9.1 和 OpenBSD 6.8 中仍然有效(尽管构造函数在 OpenBSD 中不起作用)。

那么,以这种方式使用 PIE 不起作用的技术原因是什么?明确的情况会打破这种情况(参见图表 1 和 2 中的挑战)会很棒。

图表 1

a)直接执行或b) dl 加载为共享库并通过其函数调用时,libexe程序应该工作相同。mainloader

面临的挑战是演示libexe可以使用的功能,这些功能将排除a)b)或使其以不同方式工作,以不易修复的方式。

cat <<'EOT' > libexe.c
#include <stdio.h>
#include <errno.h>
#include <err.h>

__thread int var;
void set_errno(int e){ errno = e; }

__attribute__((weak))
int main(void){
        set_errno(EPIPE); warn("%s var=%d", __FILE__, var);
}
__attribute__((constructor))
static void init(void){
        var = 33;
        fprintf(stderr, "%s's constructor\n", __FILE__);
}
EOT

cat <<'EOT' > loader.c
#include <dlfcn.h>
#include <stdio.h>
#include <err.h>
#include <errno.h>

int main(void){
        void *dl; char *lib = "./libexe";

        if(!(dl = dlopen(lib, RTLD_LAZY)))
                errx(1, "dlopen: %s", dlerror());
        printf("var=%d in %s\n", *(int*)dlsym(dl, "var"), __FILE__);
        ((void(*)(int))dlsym(dl, "set_errno"))(EBADF); warn("%s", __FILE__);
        return ((int(*)(void))dlsym(dl, "main"))();
}
__attribute__((constructor))
static void init(void){
        fprintf(stderr, "%s's constructor\n", __FILE__);
}
EOT

cc -pie -fPIC -Wl,-E libexe.c -o libexe
cc loader.c -o loader -ldl

############
$ ./loader
loader.c's constructor
libexe.c's constructor
var=33 in loader.c
loader: loader.c: Bad file descriptor
loader: libexe.c var=33: Broken pipe

图表 2

这个小程序会自行预加载,然后运行另一个可执行文件。与典型的. 不同LD_PRELOAD=/some/path ./cmd,这也可以通过fexecve/execveat(AT_EMPTY_PATH)在 Linux 上正常运行,就像在执行memfd_created 文件时一样。它经过测试可以在 Debian >= 9、Centos/RHEL >= 7 等上运行。

挑战在于演示一个可执行文件,该可执行文件在以这种方式执行链式执行时会失败,但在预加载的代码位于单独的库中时会正常工作,例如stdbuf/libstdbuf.so

cat <<'EOT' > read-eio.c
#define _GNU_SOURCE
#include <unistd.h>
#include <err.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
int main(int ac, char **av){
        int fd; char buf[32];
        if(ac < 2 || !av[1])
                errx(1, "usage: %s cmd args..", av[0]);
        if((fd = open("/proc/self/exe", O_PATH)) == -1)
                err(1, "open /proc/self/exe");
        snprintf(buf, sizeof buf, "/dev/fd/%d", fd);
        if(setenv("LD_PRELOAD", buf, 1))
                err(1, "setenv");
        execvp(av[1], av + 1);
        err(1, "execvp %s", av[1]);
}
ssize_t read(int fd, void *b, size_t z){
        errno = EIO; return -1;
}
EOT

cc -fPIC -pie read-eio.c -o read-eio

###########
$ ./read-eio cat
cat: -: Input/output error
4

0 回答 0