ユーザーネームスペース:特定のプログラムに対してのみフォルダをマウントする方法

ユーザーネームスペース:特定のプログラムに対してのみフォルダをマウントする方法

ルートアクセスなしで非FHSシステム(NixO)でFHSシステムを偽造したいと思います。これを行うには、usernamespaceを使用してルートにいくつかのフォルダ(install /tmp/mylibtoなど)をマウントする必要があります/lib(他のソリューションは表示されません)。

残念ながら、動作する方法が見つかりません。フォローしようとしました。このチュートリアルしかし、コードをコピーすると失敗します(bashを起動することもできません)。

$ gcc userns_child_exec.c -lcap -o userns_child_exec
$ id
uid=1000(myname) gid=100(users) groups=100(users),1(wheel),17(audio),20(lp),57(networkmanager),59(scanner),131(docker),998(vboxusers),999(adbusers)

$ ./userns_child_exec -U -M '0 1000 1' -G '0 100 1' bash
write /proc/535313/gid_map: Operation not permitted
bash: initialize_job_control: no job control in background: Bad file descriptor

[nix-shell:~/Documents/Logiciels/Nix_bidouille/2022_04_26_-_nix_fake_FHS_user_namespace/demo]$ 
[root@bestos:~/Documents/Logiciels/Nix_bidouille/2022_04_26_-_nix_fake_FHS_user_namespace/demo]# 
exit

(bashプロンプトが表示されますが、それ以降は何も入力してすぐに終了できません。)

どのように動作させることができるか知っていますか?

パスワード:

/* userns_child_exec.c

   Copyright 2013, Michael Kerrisk
   Licensed under GNU General Public License v2 or later

   Create a child process that executes a shell command in new
   namespace(s); allow UID and GID mappings to be specified when
   creating a user namespace.
*/
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <errno.h>

/* A simple error-handling function: print an error message based
   on the value in 'errno' and terminate the calling process */

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)

struct child_args {
    char **argv;        /* Command to be executed by child, with arguments */
    int    pipe_fd[2];  /* Pipe used to synchronize parent and child */
};

static int verbose;

static void
usage(char *pname)
{
    fprintf(stderr, "Usage: %s [options] cmd [arg...]\n\n", pname);
    fprintf(stderr, "Create a child process that executes a shell command "
            "in a new user namespace,\n"
            "and possibly also other new namespace(s).\n\n");
    fprintf(stderr, "Options can be:\n\n");
#define fpe(str) fprintf(stderr, "    %s", str);
    fpe("-i          New IPC namespace\n");
    fpe("-m          New mount namespace\n");
    fpe("-n          New network namespace\n");
    fpe("-p          New PID namespace\n");
    fpe("-u          New UTS namespace\n");
    fpe("-U          New user namespace\n");
    fpe("-M uid_map  Specify UID map for user namespace\n");
    fpe("-G gid_map  Specify GID map for user namespace\n");
    fpe("            If -M or -G is specified, -U is required\n");
    fpe("-v          Display verbose messages\n");
    fpe("\n");
    fpe("Map strings for -M and -G consist of records of the form:\n");
    fpe("\n");
    fpe("    ID-inside-ns   ID-outside-ns   len\n");
    fpe("\n");
    fpe("A map string can contain multiple records, separated by commas;\n");
    fpe("the commas are replaced by newlines before writing to map files.\n");

    exit(EXIT_FAILURE);
}

/* Update the mapping file 'map_file', with the value provided in
   'mapping', a string that defines a UID or GID mapping. A UID or
   GID mapping consists of one or more newline-delimited records
   of the form:

       ID_inside-ns    ID-outside-ns   length

   Requiring the user to supply a string that contains newlines is
   of course inconvenient for command-line use. Thus, we permit the
   use of commas to delimit records in this string, and replace them
   with newlines before writing the string to the file. */

static void
update_map(char *mapping, char *map_file)
{
    int fd, j;
    size_t map_len;     /* Length of 'mapping' */

    /* Replace commas in mapping string with newlines */

    map_len = strlen(mapping);
    for (j = 0; j < map_len; j++)
        if (mapping[j] == ',')
            mapping[j] = '\n';

    fd = open(map_file, O_RDWR);
    if (fd == -1) {
        fprintf(stderr, "open %s: %s\n", map_file, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (write(fd, mapping, map_len) != map_len) {
        fprintf(stderr, "write %s: %s\n", map_file, strerror(errno));
        exit(EXIT_FAILURE);
    }

    close(fd);
}

static int              /* Start function for cloned child */
childFunc(void *arg)
{
    struct child_args *args = (struct child_args *) arg;
    char ch;

    /* Wait until the parent has updated the UID and GID mappings. See
       the comment in main(). We wait for end of file on a pipe that will
       be closed by the parent process once it has updated the mappings. */

    close(args->pipe_fd[1]);    /* Close our descriptor for the write end
                                   of the pipe so that we see EOF when
                                   parent closes its descriptor */
    if (read(args->pipe_fd[0], &ch, 1) != 0) {
        fprintf(stderr, "Failure in child: read from pipe returned != 0\n");
        exit(EXIT_FAILURE);
    }

    /* Execute a shell command */

    execvp(args->argv[0], args->argv);
    errExit("execvp");
}

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];    /* Space for child's stack */

int
main(int argc, char *argv[])
{
    int flags, opt;
    pid_t child_pid;
    struct child_args args;
    char *uid_map, *gid_map;
    char map_path[PATH_MAX];

    /* Parse command-line options. The initial '+' character in
       the final getopt() argument prevents GNU-style permutation
       of command-line options. That's useful, since sometimes
       the 'command' to be executed by this program itself
       has command-line options. We don't want getopt() to treat
       those as options to this program. */

    flags = 0;
    verbose = 0;
    gid_map = NULL;
    uid_map = NULL;
    while ((opt = getopt(argc, argv, "+imnpuUM:G:v")) != -1) {
        switch (opt) {
        case 'i': flags |= CLONE_NEWIPC;        break;
        case 'm': flags |= CLONE_NEWNS;         break;
        case 'n': flags |= CLONE_NEWNET;        break;
        case 'p': flags |= CLONE_NEWPID;        break;
        case 'u': flags |= CLONE_NEWUTS;        break;
        case 'v': verbose = 1;                  break;
        case 'M': uid_map = optarg;             break;
        case 'G': gid_map = optarg;             break;
        case 'U': flags |= CLONE_NEWUSER;       break;
        default:  usage(argv[0]);
        }
    }

    /* -M or -G without -U is nonsensical */

    if ((uid_map != NULL || gid_map != NULL) &&
            !(flags & CLONE_NEWUSER))
        usage(argv[0]);

    args.argv = &argv[optind];

    /* We use a pipe to synchronize the parent and child, in order to
       ensure that the parent sets the UID and GID maps before the child
       calls execve(). This ensures that the child maintains its
       capabilities during the execve() in the common case where we
       want to map the child's effective user ID to 0 in the new user
       namespace. Without this synchronization, the child would lose
       its capabilities if it performed an execve() with nonzero
       user IDs (see the capabilities(7) man page for details of the
       transformation of a process's capabilities during execve()). */

    if (pipe(args.pipe_fd) == -1)
        errExit("pipe");

    /* Create the child in new namespace(s) */

    child_pid = clone(childFunc, child_stack + STACK_SIZE,
                      flags | SIGCHLD, &args);
    if (child_pid == -1)
        errExit("clone");

    /* Parent falls through to here */

    if (verbose)
        printf("%s: PID of child created by clone() is %ld\n",
                argv[0], (long) child_pid);

    /* Update the UID and GID maps in the child */

    if (uid_map != NULL) {
        snprintf(map_path, PATH_MAX, "/proc/%ld/uid_map",
                (long) child_pid);
        update_map(uid_map, map_path);
    }
    if (gid_map != NULL) {
        snprintf(map_path, PATH_MAX, "/proc/%ld/gid_map",
                (long) child_pid);
        update_map(gid_map, map_path);
    }

    /* Close the write end of the pipe, to signal to the child that we
       have updated the UID and GID maps */

    close(args.pipe_fd[1]);

    if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */
        errExit("waitpid");

    if (verbose)
        printf("%s: terminating\n", argv[0]);

    exit(EXIT_SUCCESS);
}

編集する

実際には奇妙です。グループを作成するとエラーが発生しますが、uidでは機能します。

[leo@bestos:~]$ cat /proc/582197/gid_map 

[leo@bestos:~]$ cat /proc/582197/uid_map 
         0       1000          1

[leo@bestos:~]$ ll /proc/582197/gid_map 
-rw-r--r-- 1 leo users 0 mai   18 09:09 /proc/582197/gid_map

[leo@bestos:~]$ ll /proc/582197/uid_map 
-rw-r--r-- 1 leo users 0 mai   18 09:09 /proc/582197/uid_map

ベストアンサー1

あなたが読んでいるチュートリアルは2013年に作成されました。グローバルID2015年カーネル3.19のマッピング。man user_namespaces:

「拒否」と書いてください到着/proc/[pid]/setgroups古いファイルの書き込み /proc/[pid]/gid_map 〜するユーザーの名前空間で setgroups(2) を永久に無効にする親ユーザーの名前空間で、CAP_SETGID機能なしで/proc/[pid]/gid_mapへの書き込みを許可します。

これ/proc/[pid]/setgroupsこのファイルはLinux 3.19に追加されました。しかし、セキュリティの問題を解決したので、以前の多くの安定したカーネルシリーズにバックポートされました。問題は、「rwx---rwx」などの権限を持つファイルに関連しています。これらのファイルは、「その他」よりも「グループ」に対する権限が少なくなります。これは、setgroups(2)を使用してグループを削除すると、以前に持っていなかったプロセスファイルへのアクセスを許可できることを意味します。これは、ユーザーの名前空間が存在する前は問題ではありませんでした。 [...] これにより、以前に許可されていないユーザーがグループを削除して、以前に持っていなかったファイルアクセス権を得ることができました。 [...]

denyしたがって、正しい名前snprintf(map_path, PATH_MAX, "/proc/%ld/setgroups", (long) child_pid);のファイルに単語を書き込むコードを追加しますgid_map

完全なコードは、次のユビキタスコマンドで置き換えることができます。

unshare --user --map-root-user --mount -- bash

(暗黙のものがあります--setgroups=deny

同様に、権限がない場合は、1つのuid / gidのみをマップできます。したがって、マウントが完了したら元のユーザーを偽装する唯一のオプション(完全ではありません)は、元のユーザーに再マップすることです。これは、unshare最新バージョンのTooと2番目のカスケードユーザーネームスペースを使用して実行できます。共有されていません:

# unshare --user --map-user=1000 --map-group=100 -- bash

これにより、この名前空間にuidがあります。ルートももう存在しません(nobodyマップされていない他のUIDのようにマップされたものとして扱われます)。


ノート

他の名前空間や関数とは異なる相互作用があります。これは例です:

CAP_SYS_ADMINプロセスのPIDネームスペースを所有するユーザーネームスペースを保存すると(Linux 3.8以降)、プロセスをマウントできます。/プロセス ファイルシステム

したがって--pid --fork、上記の制限に従うために追加すると、後で必要に応じて既存の/proc制限にインストールできますが、通常は--pid初めて使用する場合にのみ必要です(追加して便利に行うこともできます--mount-proc)。

ネットワークネームスペースとの対話のため、--netマウントも必要です。/sys


これらすべてを一つにまとめ、次/libの内容に置き換えます。/tmp/oOPの例:

unshare --user --map-root-user --mount -- \
    sh -c 'mount --bind /tmp/o /lib; exec unshare --user --map-user=1000 --map-group=100 -- bash'

注:最初のマッピングが完了すると、ほとんどの特権コマンドは正しく使用できなくなります。つまり、ユーザーネームスペースに使用可能な単一のUID 0があるか、またはネストされたユーザーネームスペースに単一のUID 1000が使用可能になります。 。特権コマンドは、2つのUID(そのうちの1つは通常ルートです)と使用できないUIDの間の変換を処理するため、EINVALを使用した特定のシステムコールで失敗することがよくあります。

より良い結果を得るには、最初に他の権限を設定するために特権コマンドとrootアクセスの助けが必要です。例には setuid root コマンドが含まれ、通常は権限newuidmapなしnewgidmapでユーザーからコンテナ全体を起動する必要があります。

おすすめ記事