2022 年 3 月 7 日,據(jù)一位國外開發(fā)者披露^1,Linux 內(nèi)核存在一個(gè)文件任意覆寫漏洞,低權(quán)限用戶可以利用此漏洞覆寫本沒有寫權(quán)限的文件。由于這個(gè)漏洞是基于 Linux 的管道(pipe)形成的,因此被命名為 Dirty Pipe。漏洞的發(fā)現(xiàn)過程挺有意思的,可以參考發(fā)現(xiàn)者寫的文章(見文末「參考資料」部分)。
漏洞形成原因:
使用 splice(2) ^5系統(tǒng)調(diào)用從一個(gè)只讀文件向一個(gè)管道^6中傳輸數(shù)據(jù)時(shí),會(huì)使管道用于保存數(shù)據(jù)的緩沖區(qū)共享文件的 page cache。由于 PIPE_BUF_FLAG_CAN_MERGE 標(biāo)志位的存在,調(diào)用 splice(2) 之后再向管道中寫入數(shù)據(jù)時(shí),寫入的數(shù)據(jù)會(huì)直接寫到文件的 page cache 中。
漏洞危害:低權(quán)限用戶可利用此漏洞向本沒有寫權(quán)限的文件中寫入數(shù)據(jù),進(jìn)而實(shí)現(xiàn)提權(quán)。
CVE 編號:CVE-2022-0847。
漏洞評分:7.8。
影響版本:
根據(jù)作者的描述,5.8 以上的內(nèi)核均受影響。在 5.16.11、5.15.25、5.10.102 版本中被修復(fù)。
根據(jù) Red Hat 官方通告^2 ,^3,目前還沒有發(fā)布已修復(fù)的內(nèi)核軟件包。受影響的 Red Hat 版本有:
使用 redhat-virtualization-host 內(nèi)核的 Red Hat Virtualization 4。
使用 kernel-rt 或 kernel 內(nèi)核軟件包的 Red Hat Enterprise Linux 8。
根據(jù) Debian 的官方通告^4,已修復(fù)的內(nèi)核版本號為:
stretch :4.9.228-1。
stretch (security):4.9.303-1。
buster:4.19.208-1。
buster (security):4.19.232-1。
bullseye:5.10.84-1。注:這是受影響的版本。官方通告中沒有說明修復(fù)的版本號。
bullseye (security):5.10.103-1。
bookworm:5.16.11-1。
sid:5.16.12-1。
修復(fù)方法:根據(jù)使用的發(fā)行版,關(guān)注官方的漏洞通告并升級內(nèi)核到已修復(fù)的版本。
漏洞分析過程
漏洞的利用過程與 Linux 管道和 splice(2) 系統(tǒng)調(diào)用的實(shí)現(xiàn)機(jī)制有關(guān),因此當(dāng)了解了二者的實(shí)現(xiàn)機(jī)制后,就很容易理解漏洞的形成原因。
因此漏洞分析過程分兩部分:第一部分結(jié)合內(nèi)核源碼介紹管道和 splice(2) 的實(shí)現(xiàn)原理,第二部分通過運(yùn)行 PoC 并動(dòng)態(tài)調(diào)試內(nèi)核,來實(shí)際體驗(yàn)并驗(yàn)證漏洞的觸發(fā)過程。如果已經(jīng)了解先導(dǎo)知識中所講的內(nèi)容,可直接跳到「漏洞復(fù)現(xiàn)」部分。
文中的源碼分析基于 5.10 版本!刚{(diào)試驗(yàn)證」部分基于 5.11 版本。
先導(dǎo)知識^9
pipe 實(shí)現(xiàn)機(jī)制
首先給出一張描述 pipe 相關(guān)內(nèi)核數(shù)據(jù)結(jié)構(gòu)之間關(guān)系的圖^7、^8:
創(chuàng)建 pipe
創(chuàng)建 pipe 的系統(tǒng)調(diào)用有兩個(gè):pipe(2) 和 pipe2(2),原型為:
#include<unistd.h>intpipe(int pipefd[2]);intpipe2(int pipefd[2], int flags);
系統(tǒng)調(diào)用的定義在 /fs/pipe.c (https://elixir.bootlin.com/linux/v5.10/source/fs/pipe.c#L1008)
文件中:
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags){ return do_pipe2(fildes, flags);}SYSCALL_DEFINE1(pipe, int __user *, fildes){ return do_pipe2(fildes, 0);}
兩個(gè)系統(tǒng)調(diào)用的入口都是 do_pipe2() (https://elixir.bootlin.com/linux/v5.10/source/fs/pipe.c#L986)
函數(shù)。這個(gè)函數(shù)的功能是:
調(diào)用 __do_pipe_flags() 函數(shù)創(chuàng)建兩個(gè) struct file 結(jié)構(gòu)體實(shí)例和兩個(gè)對應(yīng)的文件描述符。
調(diào)用 copy_to_user() 函數(shù)將兩個(gè)文件描述符拷貝給系統(tǒng)調(diào)用參數(shù) pipefd。
調(diào)用 fd_install() 函數(shù)將文件描述符和 struct file 結(jié)構(gòu)體實(shí)例關(guān)聯(lián)起來。
__do_pipe_flags()
再看 __do_pipe_flags() (https://elixir.bootlin.com/linux/v5.10/source/fs/pipe.c#L936)
函數(shù)。函數(shù)原型為:
staticint __do_pipe_flags(int *fd, struct file **files, int flags);
第一個(gè)參數(shù) fd 用于保存創(chuàng)建的兩個(gè)文件描述符,第二個(gè)參數(shù)用于保存創(chuàng)建的兩個(gè) struct file 結(jié)構(gòu)體實(shí)例,第三個(gè)參數(shù)是系統(tǒng)調(diào)用參數(shù) flags 的值。
__do_pipe_flags() 函數(shù)的工作為:
檢查非法的標(biāo)志位組合。
調(diào)用 create_pipe_files() 函數(shù)創(chuàng)建兩個(gè) struct file 結(jié)構(gòu)體實(shí)例。
調(diào)用兩次 get_unused_fd_flags() 函數(shù)創(chuàng)建兩個(gè)文件描述符。
調(diào)用 audit_fd_pair() 函數(shù)處理審計(jì)相關(guān)的工作。
create_pipe_files()
再看 create_pipe_files() 函數(shù)(https://elixir.bootlin.com/linux/v5.10/source/fs/pipe.c#L893)。
函數(shù)的用途是根據(jù)傳入的標(biāo)志位創(chuàng)建兩個(gè) struct file 結(jié)構(gòu)體實(shí)例。流程為:
調(diào)用 get_pipe_inode() 函數(shù)創(chuàng)建一個(gè) inode 實(shí)例。
如果標(biāo)志位設(shè)置了 O_NOTIFICATION_PIPE 位,則初始化一個(gè) watch 隊(duì)列。
調(diào)用 alloc_file_pseudo() 函數(shù)創(chuàng)建一個(gè) strcut file 實(shí)例,并將 private_data 字段的值設(shè)置為 inode->i_pipe 的值。
調(diào)用 alloc_file_clone() 函數(shù)拷貝一個(gè) struct file 實(shí)例,同樣將其 private_data 字段的值設(shè)置為 inode->i_pipe 的值。
調(diào)用 stream_open() 函數(shù)打開兩個(gè)文件。
get_pipe_inode()
接下來看看 get_pipe_inode() 函數(shù)是如何創(chuàng)建 inode 實(shí)例的。
調(diào)用 new_inode_pseudo() 函數(shù)創(chuàng)建一個(gè) inode 實(shí)例。
調(diào)用 alloc_pipe_info() 函數(shù)創(chuàng)建一個(gè) pipe_inode_info 實(shí)例。
設(shè)置 inode 實(shí)例的以下字段:
inode->i_pipe 設(shè)置為 pipe 實(shí)例指針。
inode->i_fop 設(shè)置為 pipefifo_fops 變量的指針。
inode->i_state 設(shè)置為 I_DIRTY。
inode->i_mode 設(shè)置為 S_IFIFO | S_IRUSR | S_IWUSR。
inode->i_uid 設(shè)置為 fsuid,inode->i_gid 設(shè)置為 fsgid。
inode->i_atime、inode->i_mtime、inode->i_ctime 均設(shè)置為當(dāng)前時(shí)間。
關(guān)鍵的內(nèi)核數(shù)據(jù)結(jié)構(gòu)
這里涉及到第一個(gè)關(guān)鍵的結(jié)構(gòu)體 struct pipe_inode_info(https://elixir.bootlin.com/linux/v5.10/source/include/linux/pipe_fs_i.h#L57),
內(nèi)核使用這個(gè)結(jié)構(gòu)體來描述一個(gè) pipe:
/** * struct pipe_inode_info - a linux kernel pipe * @mutex: mutex protecting the whole thing * @rd_wait: reader wait point in case of empty pipe * @wr_wait: writer wait point in case of full pipe * @head: The point of buffer production * @tail: The point of buffer consumption * @note_loss: The next read() should insert a data-lost message * @max_usage: The maximum number of slots that may be used in the ring * @ring_size: total number of buffers (should be a power of 2) * @nr_accounted: The amount this pipe accounts for in user->pipe_bufs * @tmp_page: cached released page * @readers: number of current readers of this pipe * @writers: number of current writers of this pipe * @files: number of struct file referring this pipe (protected by ->i_lock) * @r_counter: reader counter * @w_counter: writer counter * @fasync_readers: reader side fasync * @fasync_writers: writer side fasync * @bufs: the circular array of pipe buffers * @user: the user who created this pipe * @watch_queue: If this pipe is a watch_queue, this is the stuff for that **/structpipe_inode_info {structmutexmutex;wait_queue_head_t rd_wait, wr_wait; unsignedint head; unsignedint tail; unsignedint max_usage; unsignedint ring_size;#ifdef CONFIG_WATCH_QUEUEbool note_loss;#endifunsignedint nr_accounted; unsignedint readers; unsignedint writers; unsignedint files; unsignedint r_counter; unsignedint w_counter; structpage *tmp_page;structfasync_struct *fasync_readers;structfasync_struct *fasync_writers;structpipe_buffer *bufs;structuser_struct *user;#ifdef CONFIG_WATCH_QUEUEstructwatch_queue *watch_queue;#endif};
pipe 中的數(shù)據(jù)保存在結(jié)構(gòu)體 pipe_buffer (https://elixir.bootlin.com/linux/v5.10/source/include/linux/pipe_fs_i.h#L26)中的 page 字段:
/** * struct pipe_buffer - a linux kernel pipe buffer * @page: the page containing the data for the pipe buffer * @offset: offset of data inside the @page * @len: length of data inside the @page * @ops: operations associated with this buffer. See @pipe_buf_operations. * @flags: pipe buffer flags. See above. * @private: private data owned by the ops. **/structpipe_buffer {structpage *page;unsignedint offset, len; conststructpipe_buf_operations *ops;unsignedint flags; unsignedlongprivate;};
順便看看 alloc_pipe_info() 函數(shù)是怎樣初始化 pipe_inode_info 結(jié)構(gòu)體的。
使用 kzalloc 函數(shù)創(chuàng)建一個(gè) pipe_inode_info 實(shí)例。kzalloc 函數(shù)與 kmalloc 類似,只不過會(huì)初始化分配的內(nèi)存。
根據(jù)用戶是否有 CAP_SYS_RESOURCE 權(quán)限決定 pipe 緩沖區(qū)的大小,并保存在 pipe_bufs 變量里。緩沖區(qū)的大小以頁為單位。非 root 用戶可以將緩沖區(qū)大小擴(kuò)展為最大 1048576 個(gè)字節(jié),保存在 pipe_max_size 變量中?梢酝ㄟ^ /proc/sys/fs/pipe-max-size 調(diào)整這個(gè)值。默認(rèn)大小為 PIPE_DEF_BUFFERS (16)個(gè)內(nèi)存頁。
檢查當(dāng)前用戶是否創(chuàng)建了過多的 pipe。
調(diào)用 kcalloc 函數(shù)為 pipe_inode_info 結(jié)構(gòu)體的 bufs 字段分配內(nèi)存。kcalloc 與 kzalloc 類似,只不過是分配連續(xù)若干個(gè)指定大小的內(nèi)存塊。
初始化 pipe_buffer 中的其它成員:
初始化讀寫隊(duì)列。
將讀者和寫者的數(shù)量初始化為 1。
pipe 的最大可使用量、緩沖區(qū)大小、記賬個(gè)數(shù)都初始化為 pipe_bufs 變量的值。
設(shè)置用戶為當(dāng)前用戶。
初始化互斥鎖。
讀寫 pipe
上文中提到的 pipefifo_fops 是一個(gè) struct file_operations 類型的常量,表示 pipe 文件支持的文件操作有哪些,以及保存了對應(yīng)操作的函數(shù)指針:
conststructfile_operationspipefifo_fops = { .open = fifo_open, .llseek = no_llseek, .read_iter = pipe_read, .write_iter = pipe_write, .poll = pipe_poll, .unlocked_ioctl = pipe_ioctl, .release = pipe_release, .fasync = pipe_fasync,};
在上面 create_pipe_files() 函數(shù)中,會(huì)將 file 結(jié)構(gòu)體實(shí)例的 f_op 字段設(shè)置成 pipefifo_fops 結(jié)構(gòu)體的指針。用戶態(tài)執(zhí)行上面支持的系統(tǒng)調(diào)用時(shí),VFS 會(huì)調(diào)用結(jié)構(gòu)體中相應(yīng)的函數(shù)。
ssize_t vfs_write(struct file *file, constchar __user *buf, size_t count, loff_t *pos){ ... if (file->f_op->write) ret = file->f_op->write(file, buf, count, pos); elseif (file->f_op->write_iter) ret = new_sync_write(file, buf, count, pos); ...}
以 write(2) 系統(tǒng)調(diào)用為例,進(jìn)入系統(tǒng)調(diào)用入口之后,實(shí)際會(huì)調(diào)用 vfs_write() 函數(shù)。而 pipe 支持 write_iter 而不是 write,因此會(huì)接著執(zhí)行 new_sync_write():
static ssize_t new_sync_write(struct file *filp, constchar __user *buf, size_t len, loff_t *ppos){ structioveciov = { .iov_base = (void __user *)buf, .iov_len = len }; structkiocbkiocb;structiov_iteriter;ssize_t ret; init_sync_kiocb(&kiocb, filp); kiocb.ki_pos = (ppos ? *ppos : 0); iov_iter_init(&iter, WRITE, &iov, 1, len); ret = call_write_iter(filp, &kiocb, &iter); BUG_ON(ret == -EIOCBQUEUED); if (ret > 0 && ppos) *ppos = kiocb.ki_pos; return ret;}
call_write_iter() 是一個(gè)內(nèi)聯(lián)函數(shù):
staticinline ssize_t call_write_iter(struct file *file, struct kiocb *kio, struct iov_iter *iter){ return file->f_op->write_iter(kio, iter);}
其它系統(tǒng)調(diào)用類似,不再贅述?傊瑥 pipe 中讀取數(shù)據(jù)時(shí),最終調(diào)用的是 pipe_read() 函數(shù);向 pipe 中寫入數(shù)據(jù)時(shí),最終調(diào)用的是 pipe_write() 函數(shù)。
pipe_write()
先來看 pipe_write() 函數(shù)的主要流程:
如果 pipe 讀者的數(shù)量為 0,則向進(jìn)程發(fā)送 SIGPIPE 信號,并返回 EPIPE 錯(cuò)誤。
計(jì)算要寫入的數(shù)據(jù)總大小是否是頁幀大小的倍數(shù),并將余數(shù)保存在 chars 變量中。
如果 chars 不為零,而且 pipe 不為空,則:
獲取 pipe 頭部的緩沖區(qū)。
如果緩沖區(qū)設(shè)置了標(biāo)志位 PIPE_BUF_FLAG_CAN_MERGE,且緩沖區(qū)中已有的數(shù)據(jù)長度與 chars 的和不超過一個(gè)頁幀的大小,則將 chars 長度的數(shù)據(jù)寫入到當(dāng)前的緩沖區(qū)中。
如果剩余要寫入的數(shù)據(jù)大小為零,則直接返回。
在 for 循環(huán)中:
判斷 pipe 的讀者數(shù)量是否為零。
如果 pipe 沒有被填滿:
獲取 pipe 頭部的緩沖區(qū)。
如果還沒有為緩沖區(qū)分配頁幀,則調(diào)用 alloc_page() 函數(shù)分配一個(gè)。
使用自旋鎖鎖住 pipe 的讀者等待隊(duì)列。再次檢測 pipe 是否被填滿,是則終止當(dāng)前循環(huán),執(zhí)行下一次循環(huán)。
將 struct pipe_inode_info 實(shí)例的 head 字段值增加 1。并釋放自旋鎖。
設(shè)置當(dāng)前緩沖區(qū)的字段。
如果創(chuàng)建 pipe 時(shí)指定了 O_DIRECT 選項(xiàng),則將緩沖區(qū)的 flags 字段設(shè)置為 PIPE_BUF_FLAG_PACKET,否則設(shè)置為 PIPE_BUF_FLAG_CAN_MERGE。
將要寫入的數(shù)據(jù)拷貝到當(dāng)前的緩沖區(qū)中,并設(shè)置相應(yīng)的偏移量字段。
splice 系統(tǒng)調(diào)用
splice() 系統(tǒng)調(diào)用避免在內(nèi)核地址空間與用戶地址空間的拷貝,從而快速地在兩個(gè)文件描述符之間傳遞數(shù)據(jù)。函數(shù)原型為:
#define _GNU_SOURCE#include<fcntl.h>ssize_t splice(int fd_in, off64_t *off_in, int fd_out, off64_t *off_out, size_t len, unsignedint flags);
此次漏洞使用的情況是從文件向管道傳遞數(shù)據(jù),因此 fd_in 指代一個(gè)普通文件,off_in 表示從指定的文件偏移處開始讀取,fd_out 指代一個(gè) pipe,len 表示要傳輸?shù)臄?shù)據(jù)長度,flags 表示標(biāo)志位。詳細(xì)情況可以參考手冊。
看看 splice() 系統(tǒng)調(diào)用的主要流程。系統(tǒng)調(diào)用的定義在 fs/splice.c 文件中,主要工作由 __do_splice() 函數(shù)完成。
__do_splice() 在做完簡單的參數(shù)檢查之后,又調(diào)用 do_splice() 函數(shù)實(shí)現(xiàn)主要工作。
do_splice() 中,會(huì)根據(jù)兩個(gè)文件描述符的類型進(jìn)入不同的分支。當(dāng)前情況下,fd_out 指代一個(gè) pipe,因此會(huì)進(jìn)入 if (opipe) 這個(gè)分支。主要工作通過 do_splice_to() 函數(shù)完成。
/* * Determine where to splice to/from. */longdo_splice(struct file *in, loff_t *off_in, struct file *out, loff_t *off_out, size_t len, unsignedint flags){ structpipe_inode_info *ipipe;structpipe_inode_info *opipe;loff_t offset; long ret; // 判斷兩個(gè)文件描述符的打開模式是否符合條件if (unlikely(!(in->f_mode & FMODE_READ) || !(out->f_mode & FMODE_WRITE))) return -EBADF; ipipe = get_pipe_info(in, true); opipe = get_pipe_info(out, true); // 當(dāng) in 和 out 都是 pipe 的情況if (ipipe && opipe) { if (off_in || off_out) return -ESPIPE; /* Splicing to self would be fun, but... */if (ipipe == opipe) return -EINVAL; if ((in->f_flags | out->f_flags) & O_NONBLOCK) flags |= SPLICE_F_NONBLOCK; return splice_pipe_to_pipe(ipipe, opipe, len, flags); } // 當(dāng) in 是 pipe 的情況if (ipipe) { ...... } // 當(dāng) out 是 pipe 的情況if (opipe) { // 不能為 pipe 設(shè)置偏移量if (off_out) return -ESPIPE; if (off_in) { if (!(in->f_mode & FMODE_PREAD)) return -EINVAL; offset = *off_in; } else { offset = in->f_pos; } if (out->f_flags & O_NONBLOCK) flags |= SPLICE_F_NONBLOCK; // 獲取 pipe 的鎖 pipe_lock(opipe); // 等待 pipe 有可使用的緩沖區(qū) ret = wait_for_space(opipe, flags); if (!ret) { unsignedint p_space; // 計(jì)算能夠讀取的文件長度,不應(yīng)該超過 pipe 剩余的緩沖區(qū)大小/* Don't try to read more the pipe has space for. */ p_space = opipe->max_usage - pipe_occupancy(opipe->head, opipe->tail); len = min_t(size_t, len, p_space << PAGE_SHIFT); // 調(diào)用 do_splice_to() 實(shí)現(xiàn)主要工作 ret = do_splice_to(in, &offset, opipe, len, flags); } // 釋放 pipe 的鎖 pipe_unlock(opipe); if (ret > 0) // 喚醒 pipe 的讀者等待隊(duì)列中的進(jìn)程 wakeup_pipe_readers(opipe); if (!off_in) in->f_pos = offset; else *off_in = offset; return ret; } return -EINVAL;}
do_splice_to()
在 do_splice_to() 中,主要功能是通過輸入文件的 splice_read() 方法實(shí)現(xiàn)的。這里以 ext4 文件系統(tǒng)為例,在 fs/ext4/file.c 文件中查看 ext4_file_operations 變量可知,ext4 文件系統(tǒng)中,splice_read 使用的是定義在 fs/splice.c 中的 generic_file_splice_read() 方法。接著通過調(diào)試可知接下來的函數(shù)調(diào)用鏈:
generic_file_splice_read() -> call_read_iter() -> generic_file_buffered_read() -> copy_page_to_iter() -> copy_page_to_iter_pipe()
call_read_iter() 是一個(gè)定義在 include/linux/fs.h 中的內(nèi)聯(lián)函數(shù),實(shí)際調(diào)用的是輸入文件的 read_iter() 方法。而 ext4 文件系統(tǒng)的 read_iter() 方法是 ext4_file_read_iter()。在當(dāng)前情況下,會(huì)調(diào)用 generic_file_rad_iter(),其接著調(diào)用 generic_file_buffered_read()。
copy_page_to_iter_pipe()
generic_file_buffered_read() 是通用的文件讀取例程,將文件讀取到 page cache 后會(huì)通過 copy_page_to_iter() 函數(shù)將文件對應(yīng)的 page cache 與 pipe 的緩沖區(qū)關(guān)聯(lián)起來。實(shí)際的關(guān)聯(lián)操作通過定義在 /lib/iov_iter.c 中的 copy_page_to_iter_pipe() 實(shí)現(xiàn):
/* * page 是文件對應(yīng)的內(nèi)存頁幀,pipe 實(shí)例被包裹在 struct iov_iter 實(shí)例中*/static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes, struct iov_iter *i){ structpipe_inode_info *pipe = i->pipe;structpipe_buffer *buf;unsignedint p_tail = pipe->tail; unsignedint p_mask = pipe->ring_size - 1; unsignedint i_head = i->head; size_t off; if (unlikely(bytes > i->count)) bytes = i->count; if (unlikely(!bytes)) return0; if (!sanity(i)) return0; off = i->iov_offset; buf = &pipe->bufs[i_head & p_mask]; if (off) { if (offset == off && buf->page == page) { /* merge with the last one */ buf->len += bytes; i->iov_offset += bytes; goto out; } i_head++; buf = &pipe->bufs[i_head & p_mask]; } if (pipe_full(i_head, p_tail, pipe->max_usage)) return0; buf->ops = &page_cache_pipe_buf_ops; // 增加 page 實(shí)例的引用計(jì)數(shù) get_page(page); // 將 pipe 緩沖區(qū)的 page 指針指向文件的 page buf->page = page; buf->offset = offset; buf->len = bytes; pipe->head = i_head + 1; i->iov_offset = offset + bytes; i->head = i_head;out: i->count -= bytes; return bytes;}
漏洞復(fù)現(xiàn)
分析
如果了解了向 pipe 寫入數(shù)據(jù)的過程,以及 splice() 系統(tǒng)調(diào)用從文件向 pipe 傳輸數(shù)據(jù)的過程,就不難理解漏洞的形成原因了。對照漏洞發(fā)現(xiàn)者提供的 PoC 來解釋漏洞形成原因:
首先創(chuàng)建一個(gè) pipe。接著每次向 pipe 中寫入一個(gè)頁幀大小的數(shù)據(jù)。從 pipe_write() 可知,每次寫入都不會(huì)進(jìn)入 if (chars && !was_empty) 這個(gè)分支,因?yàn)閷懭霐?shù)據(jù)的大小為頁幀大小的整數(shù)倍時(shí),chars 的值總為零。創(chuàng)建 pipe 的時(shí)候沒有指定 O_DIRECT 標(biāo)志,因此在 for 循環(huán)中會(huì)將每個(gè) pipe_buffer 的標(biāo)志位設(shè)置為 PIPE_BUF_FLAG_CAN_MERGE。
接下來打開要覆寫的文件,并通過 splice() 系統(tǒng)調(diào)用向 pipe 中寫入一個(gè)字節(jié)。根據(jù) splice() 的實(shí)現(xiàn),將文件從硬盤讀取到 page cache 后,會(huì)把文件對應(yīng)的 page 與 pipe_buffer 的 page 字段關(guān)聯(lián)起來,并且不會(huì)重置 pipe_buffer 的 flags 字段。也就是說,此時(shí) flags 字段的值仍為 PIPE_BUF_FLAG_CAN_MERGE。
最后向 pipe 中寫入小于一個(gè)頁幀大小的數(shù)據(jù)。進(jìn)入 pipe_write() 之后,會(huì)進(jìn)入 if (chars && !was_empty) 分支。由于在 copy_page_to_iter_pipe() 中,將文件的 page 與 pipe_buffer 的 page 字段關(guān)聯(lián)之后,將 pipe_inode_info 實(shí)例的 head 值增加了 1,因此為了將小于一個(gè)頁幀的數(shù)據(jù)寫入到前一個(gè) pipe_buffer 中, if 分支里獲取 pipe_buffer 的時(shí)候?qū)?head 值減 1,從而此時(shí) pipe_buffer 的 page 指向的是文件的 page。
調(diào)試驗(yàn)證
首先創(chuàng)建一個(gè)要覆寫的文件并用隨機(jī)字符串填充:
然后在 GDB 中分別在 pipe_write 和 copy_page_to_iter_pipe 兩個(gè)函數(shù)設(shè)置斷點(diǎn):
然后在 GDB 中使用 continue 命令讓虛擬機(jī)繼續(xù)運(yùn)行,并執(zhí)行 PoC 程序。然后會(huì)在 pipe_write 處停止。使用下面的 GDB 腳本可以看到,pipe 的所有 pipe_buffer 中的標(biāo)志位都為零:
set $index = 0while ($index < pipe->ring_size) print pipe->bufs[$index++]->flagsend
然后接著執(zhí)行 15 次 continue 命令,在第 16 次向 pipe 中寫入數(shù)據(jù)之前停止。再次查看所有 pipe_buffer 的標(biāo)志位,發(fā)現(xiàn)都被置為了 PIPE_BUF_FLAG_CAN_MERGE:
當(dāng)最后一次 pipe_write 執(zhí)行完后,pipe->head 的值為 16。
接著執(zhí)行 continue 命令,會(huì)在 copy_page_to_iter_pipe 處停下來。單步進(jìn)入幾步之后,先把 pipe 變量和文件對應(yīng)的 page 實(shí)例的地址保存到變量中。
因?yàn)楫?dāng)前 pipe->head 的值是 16,而 pipe->ring_size 的值時(shí)默認(rèn)的 16,因此第 395 行代碼中取到的是第一個(gè) pipe_buffer。
接下來將文件的 page 與 pipe_buffer 的 page 字段關(guān)聯(lián)起來,并將 pipe 的 head 字段加一,即此時(shí)為 17。
接著 continue,會(huì)停在 pipe_write 處。接著單步執(zhí)行,會(huì)進(jìn)入觸發(fā)漏洞的 if 分支。然后查看 buf->page 的值,和之前保存的文件的 page 的地址相同。繼續(xù)之后,文件覆寫成功:
低權(quán)限用戶篡改沒有寫權(quán)限文件的驗(yàn)證
在上面的驗(yàn)證過程中,由于使用的是最簡單的內(nèi)核以及 busybox,因此使用 root 用戶。為了驗(yàn)證低權(quán)限用戶可以成功篡改沒有寫權(quán)限的文件,在此使用 ArchLinux 發(fā)行版,以 5.10.69-1-lts 內(nèi)核版本作驗(yàn)證:
結(jié)論
經(jīng)復(fù)現(xiàn)過程可知,漏洞利用方式相對簡單,建議受影響的機(jī)器立即升級到官方最新版本。