检测程序在linux上的另一个线程启动

我试图通过(其中包括)ELF二进制文件,在它们启动之后对它们进行chroot。 为此,使用CLONE_FS标记克隆的subprocess执行chroot,而父进程通过调用exec函数来运行二进制文件。

这个技巧实际上是在程序加载完所需的共享库之后发生的。 问题是,我无法find一种方法来检测从其他进程实际发生的情况。 有什么办法吗?

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

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

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

注意:绝对不需要标记辅助二进制文件setuid root,或者给沙盒二进制文件赋予任何权限。 我们可以用最小的权限来做到这一点: CAP_SYS_CHROOT功能就足够了。

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

这里整洁的事情是,预加载库也可以插入所需的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; } 

Makefile

 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中的缩进是使用制表符而不是空格。 跑

 make PREFIX=/usr/local clean install 

编译并安装到/usr/local ,但只能由当前用户执行。 你也可以使用clean all来只重新编译一切,或者uninstall卸载二进制文件

这确实需要libcap库。 它作为内核的一部分进行维护,但是您可能需要安装一个libcap-devlibcap-devel或类似命名的软件包,以获取所有必要的文件以进行编译。

安装后,你可以运行例如

 exec-chroot /tmp /tmp ls -alF / 

运行ls -alF / in /tmp ls -alF //tmp 。 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(user),因为passwd和group数据库在chroot中是不可访问的。 但是,正如我已经提到的,可以通过修改和扩展上面的代码来解决这个问题。

尽管我的确在尝试编写代码时仔细处理了错误,但我并没有真正考虑错误情况或安全问题的整体操作。 这就是为什么只安装当前用户可以访问的文件。

有问题吗?