Я пытаюсь изолировать исполняемые файлы 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
возможность достаточно.
Я предпочитаю добавлять возможность в двоичный файл только в разрешенный набор, так что сам двоичный файл должен добавить возможность в эффективный набор до chroot()
работает. Я чувствую, что этот подход уменьшает последствия возможных ошибок установки / администратора. Если вы не согласны, не стесняйтесь опускать соответствующий код из exec.c
и использовать =pe
вместо =p
в setcap
Команда в Makefile.
Здесь важно то, что библиотека предварительной загрузки может также вставлять нужные функции 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-dev
или же libcap-devel
или пакет с аналогичным именем, чтобы получить все необходимые файлы для его компиляции.
После установки вы можете запустить, например,
exec-chroot /tmp /tmp ls -alF /
бежать ls -alF /
в /tmp
привязан к /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. Однако, как я уже упоминал, это можно обойти, изменив и расширив вышеприведенный код, как указано выше.
Несмотря на то, что я пытался написать код с тщательной обработкой ошибок, я не очень тщательно рассмотрел всю операцию в отношении условий ошибок или проблем безопасности; поэтому файлы устанавливаются только для текущего пользователя.
Вопросы?