注意:所谓的骗子不会以任何方式回答这个问题;特别是,可以通过使用 编译轻松修复线程本地示例-fPIC
,就像下面已经提到的 bugzilla 示例一样。其余的只是未经询问的意见和毫无根据的主张。
在最近的 Linux 系统(如 Debian 10、RHEL 8 等)中,您可以创建一个 ELF 文件,它既是位置无关的可执行文件 (PIE),又是动态加载的库/共享对象。
这是非常有用的,并且有很多应用程序,例如创建包装程序,这些程序预先加载然后执行另一个程序(参见下面的图表 2),或者将虚拟机/语言环境作为单个对象,可以嵌入或作为独立的解释器运行。所有这些都无需处理硬连线安装目录、符号链接漏洞、$ORIGIN
s 或其他此类安全和文件系统策略噩梦。
但是,在 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
程序应该工作相同。main
loader
面临的挑战是演示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_create
d 文件时一样。它经过测试可以在 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