initramfsに関するLinuxのドキュメント とswitch_root
の- ソースコード を読みました。
ドキュメントは言う:
別のルートデバイスを切り替えると、initrdはピボットルートを作成してから、ラムディスクをアンマウントします。しかし、initramfsはrootfsです。pivot_rootrootfsもアンマウントもできません。代わりに、rootfsからすべてを削除してスペースを解放します(find -xdev/-exec rm '{}' ';')、rootfsを新しいルートでオーバーマウントします(cd/newmount; mount- -move。/; chroot。)、stdin/stdout/stderrを新しい/ dev/consoleにアタッチし、新しいinitを実行します。
そしてswitch_root
は本当にそうします:
if (chdir(newroot)) {
warn(_("failed to change directory to %s"), newroot);
return -1;
}
...
if (mount(newroot, "/", NULL, MS_MOVE, NULL) < 0) {
close(cfd);
warn(_("failed to mount moving %s to /"), newroot);
return -1;
}
...
if (chroot(".")) {
close(cfd);
warn(_("failed to change root"));
return -1;
}
なぜマウントポイントを/
に移動する必要があるのですか?
なぜnew_rootにchrootしないのですか?
new_root
を/
にマウントすると、マウントネームスペースのルートディレクトリが変更されます。/
をオーバーマウントせずにchrootすると、システムはchroot
環境になります(ルートディレクトリはマウント名前空間のルートディレクトリと一致しません)。
これはいくつかの問題を引き起こします、例えば:
man 2 unshare
によると、chroot環境では、unshare
ingユーザー名前空間はEPERM
で失敗します。
EPERM (since Linux 3.9) CLONE_NEWUSER was specified in flags and the caller is in a chroot environment (i.e., the caller's root directory does not match the root directory of the mount namespace in which it resides).
$ unshare -U
unshare: unshare failed: Operation not permitted
マウント名前空間を入力すると、プロセスのルートディレクトリがマウント名前空間のルートディレクトリに設定されるため、マウント名前空間に対してsetns
を実行すると、ルートディレクトリがrootfsディレクトリに設定されます。
$ nsenter -m/proc/self/ns/mnt /bin/sh
$ ls -ld /new_root
new_root
Chrootの外にあるnew_rootディレクトリを確認できます。
/
をマウントしても、chrootのエスケープは実際には防止されませんRootユーザーは、このディレクトリをumount
して、そのマウントネームスペース(setns
)を再入力して、rootfsを表示できます。
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
int main() {
int ns = open("/proc/self/ns/mnt", O_RDONLY);
if (ns == -1) {
perror("open");
goto out;
}
if (umount2("/", MNT_DETACH)) {
perror("umount2");
goto out;
}
if (setns(ns, CLONE_NEWNS)) {
perror("setns");
goto out;
}
char *a[] = { "/bin/sh", NULL };
char *e[] = { NULL };
execve(a[0], a, e);
perror("execve");
out:
return 1;
}
$ gcc -o main main.c
$ unshare -m ./main
/ # ls -d new_root
new_root
/ # mount -t proc proc /proc
/ # cat /proc/mounts
none / rootfs rw 0 0
proc /proc proc rw,relatime 0 0
Chrootのエスケープを防ぐには、new_root
を/
にマウントする必要があります。
最小限のinitramfsを作成し、switch_root
バイナリをこのシェルスクリプトに置き換えて、シェルを取得します。
#!/bin/sh exec /bin/sh
また、initramfs内の/bin/sh
を静的にリンクされたbusybox
に変更しました。
次のコードをコンパイルして静的にリンクしました。
#include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main() { int fd = open(".", O_RDONLY | O_CLOEXEC); if (fd < 0) { perror("open"); goto out0; } if (chroot("tmp")) { perror("chroot"); goto out1; } if (fchdir(fd)) { perror("fchdir"); goto out1; } if (chdir("..")) { perror("chdir"); goto out1; } char *const argvp[] = { "sh", NULL }; char *const envp[] = { NULL }; execve("bin/sh", argvp, envp); perror("execve"); out1: close(fd); out0: return 1; }
実際のルートファイルシステムのルートディレクトリに/escape
として配置します。
再起動し、switch_root
が行われる直前にシェルを取得しました。
$ mount --move proc new_root/proc $ mount --move dev new_root/dev $ mount --move sys new_root/sys $ mount --move run new_root/run $ exec chroot new_root $ ./escape $ ls -d new_root new_root
私はchrootを脱出した。
$ mount --move proc new_root/proc $ mount --move dev new_root/dev $ mount --move sys new_root/sys $ mount --move run new_root/run $ cd new_root $ mount --move . / $ exec chroot . $ ./escape $ ls -d new_root ls: cannot access 'new_root': No such file or directory
私はchrootを逃れられません。
Rootfsをオーバーマウントしないと、ユーザーとマウントの名前空間が壊れます。
setns
システムコールは、呼び出し元のルートディレクトリをマウントネームスペースのルートディレクトリに設定し、chroot
を取り消します。