c++ - 在 linux 的另一个线程中检测程序启动

标签 c++ c linux chroot ptrace

<分区>

我正在尝试通过(除其他外)在启动后对它们进行 chroot 来对 ELF 二进制文件进行沙盒处理。为此,使用 CLONE_FS 标记克隆的子进程执行 chroot,而父进程通过调用 exec 函数运行二进制文件。

如果 chroot 发生在程序加载完它需要的共享库之后,这个技巧就真的有用了。问题是我找不到一种方法来检测其他进程何时实际发生这种情况。有什么办法吗?

最佳答案

您可以使用一个预加载库,其中包含一个在 main() 之前执行的函数,一个具有 CAP_SYS_CHROOT 允许的文件系统功能的辅助二进制文件,以及一个 Unix 域套接字对两个。

辅助二进制文件创建套接字对,然后使用clone(CLONE_FS) 派生一个共享文件系统信息的辅助进程,设置LD_PRELOAD 加载预加载库,并执行沙盒二进制文件。 (exec 根据沙盒二进制文件系统功能重置功能,因此沙盒二进制文件根本没有任何额外权限。)

辅助进程将CAP_SYS_CHROOT添加到有效集,等待沙盒二进制文件(预加载库)通过套接字通知它,调用chroot(),并通知成功的沙盒二进制文件(预加载库)。

注意:绝对没有必要将辅助二进制文件标记为 setuid root,或者为沙盒二进制文件提供任何功能或特权。我们可以用最小的权限来做到这一点:CAP_SYS_CHROOT capability就足够了。

我更喜欢只将功能添加到允许的集合中的二进制文件,这样二进制文件本身必须在 chroot() 工作之前将功能添加到有效集合中。我觉得这种方法减少了可能的安装/管理员错误的影响。如果您不同意,请随意省略 exec.c 中的相应代码,并在 中使用 =pe 而不是 =p Makefile 中的 setcap 命令。

这里巧妙的是,preload 库还可以插入所需的 C 函数,并使用 unix 域套接字从辅助进程获取必要的信息;你甚至可以使用 SCM_RIGHTS将文件描述符从 chroot 外部传输到沙盒二进制文件的辅助消息。 (本质上,这就是 fakeroot 所做的,但相反:不是伪造一个 chroot 环境,您可以选择沙盒二进制文件可以从 chroot 环境外部访问的文件。)只要让辅助进程保持事件状态即可只要套接字的另一端仍然打开,它就会在沙盒二进制文件退出后退出。

这是我的示例实现,它将辅助进程作为沙盒二进制文件的子进程启动,辅助进程在沙盒 main() 启动之前退出(并预加载库获取它)。

exec.c:

#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <sys/capability.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sched.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#ifndef  SOCKET_FD
#error SOCKET_FD not defined!
#endif

#ifndef  LIBRARY_PATH
#error LIBRARY_PATH not defined!
#endif

static size_t            helper_stack_size = 32768;
static void             *helper_stack = NULL;
static const char       *helper_chroot = NULL;
static const cap_value_t helper_cap[] = { CAP_SYS_CHROOT };
static const int         helper_caps = sizeof helper_cap / sizeof helper_cap[0];
static int               socket_fd[2] = { -1, -1 };

#ifdef __hppa
#define helper_endstack  (helper_stack)
#else
#define helper_endstack  ((void *)((char *)helper_stack + helper_stack_size - 1))
#endif

static int helper_main(void *arg)
{
    const char *const argv0 = arg;
    pid_t pid;
    cap_t caps;

    close(socket_fd[0]);

    /* Read the target PID. */
    {   char       *p = (char *)(&pid);
        char *const q = (char *)(&pid) + sizeof pid;
        ssize_t     n;

        while (p < q) {
            n = recv(socket_fd[1], p, (size_t)(q - p), MSG_WAITALL);
            if (n > (ssize_t)0)
                p += n;
            else
            if (n != (ssize_t)-1) {
                fprintf(stderr, "%s: %s.\n", argv0, strerror(EIO));
                return 127;
            } else
            if (errno != EINTR) {
                fprintf(stderr, "%s: %s.\n", argv0, strerror(errno));
                return 127;
            }
        }
    }

    if (pid < (pid_t)2) {
        shutdown(socket_fd[1], SHUT_RDWR);
        close(socket_fd[1]);
        return 127;
    }

    /* Enable CAP_SYS_CHROOT. */
    caps = cap_get_proc();
    if (cap_set_flag(caps, CAP_EFFECTIVE, helper_caps, helper_cap, CAP_SET)) {
        shutdown(socket_fd[1], SHUT_RDWR);
        close(socket_fd[1]);
        fprintf(stderr, "%s: %s.\n", argv0, strerror(errno));
        return 127;
    }
    if (cap_set_proc(caps)) {
        shutdown(socket_fd[1], SHUT_RDWR);
        close(socket_fd[1]);
        fprintf(stderr, "%s: %s.\n", argv0, strerror(errno));
        return 127;
    }

    /* Target is ready to be chrooted, so do it now. */
    if (chroot(helper_chroot)) {
        shutdown(socket_fd[1], SHUT_RDWR);
        close(socket_fd[1]);
        fprintf(stderr, "%s: Cannot chroot: %s.\n", argv0, strerror(errno));
        return 127;
    }

    /* Send my own pid, so this process will be reaped. */
    {   const char       *p = (char *)(&pid);
        const char *const q = (char *)(&pid) + sizeof pid;
        ssize_t           n;

        pid = getpid();

        while (p < q) {
            n = send(socket_fd[1], p, (size_t)(q - p), MSG_NOSIGNAL);
            if (n > (ssize_t)0)
                p += n;
            else
            if (n != (ssize_t)-1) {
                fprintf(stderr, "%s: %s.\n", argv0, strerror(EIO));
                return 127;
            } else
            if (errno != EINTR) {
                fprintf(stderr, "%s: %s.\n", argv0, strerror(errno));
                return 127;
            }
        }
    }

    /* We won't be sending anything else. */
    shutdown(socket_fd[1], SHUT_WR);

    /* Ignore further input; wait for other end to close descriptor. */
    {   char    buf[16];
        ssize_t n;

        while (1) {
            n = recv(socket_fd[1], buf, sizeof buf, 0);
            if (n > (ssize_t)0)
                continue;
            else
            if (n == (ssize_t)0)
                break;
            else
            if (n != (ssize_t)-1) {
                fprintf(stderr, "%s: %s.\n", argv0, strerror(EIO));
                return 127;
            } else
            if (errno == EPIPE)
                break;
            else
            if (errno != EINTR) {
                fprintf(stderr, "%s: %s.\n", argv0, strerror(errno));
                return 127;
            }
        }
    }

    /* Close the socket, and exit. */
    shutdown(socket_fd[1], SHUT_RDWR);
    close(socket_fd[1]);
    return 0;
}

int main(int argc, char *argv[])
{
   if (argc < 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s CHROOT WORKDIR COMMAND [ ARGS ... ]\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "Note: . is a valid WORKDIR.\n");
        fprintf(stderr, "\n");
        return 1;
    }

    if (chdir(argv[2])) {
        fprintf(stderr, "%s: %s.\n", argv[2], strerror(errno));
        return 1;
    }

    helper_stack = mmap(NULL, helper_stack_size, PROT_READ | PROT_WRITE,
                              MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN, -1, (off_t)0);
    if ((void *)helper_stack == MAP_FAILED) {
        fprintf(stderr, "Cannot create helper process stack: %s.\n", strerror(errno));
        return 1;
    }
    helper_chroot = argv[1];

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd)) {
        fprintf(stderr, "Cannot create an Unix domain stream socket pair: %s.\n", strerror(errno));
        return 1;
    }

    if (clone(helper_main, helper_endstack, CLONE_FS, argv[0]) == -1) {
        fprintf(stderr, "Cannot clone a helper process: %s.\n", strerror(errno));
        close(socket_fd[0]);
        close(socket_fd[1]);
        return 1;
    }

    close(socket_fd[1]);
    if (socket_fd[0] != SOCKET_FD) {
        if (dup2(socket_fd[0], SOCKET_FD) == -1) {
            fprintf(stderr, "Cannot move stream socket: %s.\n", strerror(errno));
            close(socket_fd[0]);
            close(SOCKET_FD);
            return 1;
        }
        close(socket_fd[0]);
    }

    setenv("LD_PRELOAD", LIBRARY_PATH, 1);

    /* Capabilities are reset over an execve(). */
    execvp(argv[3], argv + 3);

    close(SOCKET_FD);
    fprintf(stderr, "%s: %s.\n", argv[3], strerror(errno));
    return 1;
}

premain.c:

#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

#ifndef SOCKET_FD
#error SOCKET_FD is not defined!
#endif

static void init(void) __attribute__ ((constructor (65535)));

static void init(void)
{
    pid_t pid;

    /* Note: We could probably only remove libpremain.so
     *       from the value, instead of clearing it altogether. */
    unsetenv("LD_PRELOAD");

    /* Verify SOCKFD is an Unix domain socket. */
    {   struct sockaddr_un  addr;
        socklen_t           addrlen = sizeof addr;
        memset(&addr, 0, sizeof addr);

        errno = EIO;
        if (getsockname(SOCKET_FD, (struct sockaddr *)&addr, &addrlen))
            switch (errno) {

            case EBADF:
                /* SOCKET_FD is not open. Continue as if libpremain.so was never loaded. */
                errno = 0;
                return;

            case ENOTSOCK:
                /* SOCKET_FD is not a socket. Continue as if libpremain.so was never loaded. */
                errno = 0;
                return;

            default:
                /* All other errors are fatal. */
                exit(127);
            }

        if (addr.sun_family != AF_UNIX) {
            /* SOCKET_FD is not an Unix domain socket. Continue as if libpremain.so was never loaded. */
            errno = 0;
            return;
        }
    }

    /* Make SOCKET_FD blocking and close-on-exec. */
    if (fcntl(SOCKET_FD, F_SETFD, (long)FD_CLOEXEC) ||
        fcntl(SOCKET_FD, F_SETFL, (long)0L))
        exit(127);

    /* Send our PID. */
    {   const char       *p = (const char *)(&pid);
        const char *const q = (const char *)(&pid) + sizeof pid;

        pid = getpid();

        while (p < q) {
            ssize_t n = send(SOCKET_FD, p, (size_t)(q - p), MSG_NOSIGNAL);
            if (n > (ssize_t)0)
                p += n;
            else
            if (n != (ssize_t)-1)
                exit(127);
            else
            if (errno != EINTR)
                exit(127);
        }
    }

    /* Receive the PID from the other end. */
    {   char       *p = (char *)(&pid);
        char *const q = (char *)(&pid) + sizeof pid;

        pid = (pid_t)-1;

        while (p < q) {
            ssize_t n = recv(SOCKET_FD, p, (size_t)(q - p), MSG_WAITALL);
            if (n > (ssize_t)0)
                p += n;
            else
            if (n != (ssize_t)-1)
                exit(127);
            else
            if (errno != EINTR)
                exit(127);
        }
    }

    shutdown(SOCKET_FD, SHUT_RDWR);
    close(SOCKET_FD);

    /* If the PID is > 1, we wait for it to exit.
     * If an error occurs, it's not a problem. */
    if (pid > (pid_t)1) {
        pid_t p;
        do {
            p = waitpid(pid, NULL, 0);
        } while (p == (pid_t)-1 && errno == EINTR);
    }

    /* All done. */
    return;
}

生成文件:

CC  := gcc
CFLAGS  := -Wall -O3
LD  := $(CC)
LDFLAGS := -lcap

PREFIX  := /usr
BINDIR  := $(PREFIX)/bin
LIBDIR  := $(PREFIX)/lib

SOCKFD  := 15

.PHONY: all clean

all: clean libpremain.so exec-chroot

clean:
    rm -f libpremain.so exec-chroot

libpremain.so: premain.c
    $(CC) $(CFLAGS) -DSOCKET_FD=$(SOCKFD) -fPIC -shared $^ -ldl -Wl,-soname,$@ $(LDFLAGS) -o $@

exec-chroot: exec.c
    $(CC) $(CFLAGS) -DSOCKET_FD=$(SOCKFD) -DLIBRARY_PATH='"'$(LIBDIR)/libpremain.so'"' $^ $(LDFLAGS) -o $@

install: libpremain.so exec-chroot
    sudo rm -f $(LIBDIR)/libpremain.so $(BINDIR)/exec-chroot
    sudo install -o `id -un` -g `id -gn` -m 00770 libpremain.so $(LIBDIR)/libpremain.so
    sudo install -o `id -un` -g `id -gn` -m 00770 exec-chroot $(BINDIR)/exec-chroot
    sudo setcap 'cap_sys_chroot=p' $(BINDIR)/exec-chroot

uninstall:
    sudo rm -f $(LIBDIR)/libpremain.so $(BINDIR)/exec-chroot

请注意,Makefile 中的缩进是 tab,而不是空格。运行

make PREFIX=/usr/local clean install

编译安装到/usr/local,但只能由当前用户执行。您还可以使用 clean all 仅重新编译所有内容,或使用 uninstall 卸载二进制文件。`

这确实需要 libcap图书馆。它作为内核的一部分进行维护,但您可能需要安装 libcap-devlibcap-devel 或类似名称的包来获取所有必要的文件进行编译反对。

安装后,你可以运行例如

exec-chroot /tmp /tmp ls -alF /

/tmp chroot 到 /tmp 中运行 ls -alF/。 Ubuntu 机器上的输出通常类似于

drwxrwxrwt 11    0    0  4096 May 29 23:55 ./
drwxrwxrwt 11    0    0  4096 May 29 23:55 ../
drwxrwxrwt  2    0    0  4096 May 29 17:15 .ICE-unix/
-r--r--r--  1    0    0    11 May 29 17:15 .X0-lock
drwxrwxrwt  2    0    0  4096 May 29 17:15 .X11-unix/
drwx------  2 1000 1000  4096 May 29 17:15 .esd-1000/
drwx------  2    0    0 16384 Dec  2  2011 lost+found/
drwx------  2 1000 1000  4096 May 29 17:15 pulse-xxxxxxxxx/
drwx------  2    0    0  4096 May 29 17:15 pulse-yyyyyyyyy/

所有者和组分别为 0(root)和 1000(用户),因为密码和组数据库无法从 chroot 中访问。但是,正如我已经提到的,可以通过修改和扩展上述代码来解决这个问题。

虽然我确实尝试编写带有仔细错误处理的代码,但我并没有真正全面地考虑错误条件或安全问题方面的整体操作;这就是为什么安装的文件只能由当前用户访问的原因。

有问题吗?

关于c++ - 在 linux 的另一个线程中检测程序启动,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23914236/

相关文章:

c - 为什么下面同一个 c 程序有两个输出?

linux - 创建不随程序终止的屏幕 session

linux - 创建 SOCK_RAW 套接字只是为了发送数据而不需要任何 recvform()

c - 我怎样才能导致指令缓存未命中?

c++ - 如何增加 Windows Mobile 上进程的可用内存?

c - scanf() 可变长度说明符

c++ - Qt QML - 无法分配给不存在的属性 "onClicked"

c - 为什么我不能在 gdb 中将 printf 与临时结构的字符串成员一起使用

c++ - 具有错误值的二维 vector

c++ - gcc C++14 与 msvc++ 2015 中的级联宏