您好,登錄后才能下訂單哦!
本篇內容主要講解“Linux如何在任意進程中修改內存保護”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Linux如何在任意進程中修改內存保護”吧!
在現代操作系統中,每個進程都有自己的虛擬地址空間(從虛擬地址映射到物理地址)。此虛擬地址空間由內存頁(某些固定大小的連續內存塊)組成,每個頁都有保護標志,用于確定允許此頁面訪問的類型(讀取,寫入和執行)。這種機制依賴于架構頁表(有趣的是,在x64架構中,你不能使頁面只寫(write-only),就算你特意從操作系統請求,它也總是可讀的)。
在Windows中,你可以使用VirtualProtect或VirtualProtectEx這兩個API更改內存區域的保護。后者讓我們的任務變得非常簡單:它的第一個參數hProcess是“要改變內存保護的進程的句柄”(參見MSDN)。
另一方面,在Linux中,我們并不那么幸運:更改內存保護的API是系統調用mprotect或pkey_mprotect,并且兩者始終在當前進程的地址空間上運行。 我們現在回顧一下在x64架構上的Linux中解決此任務的方法(我們假設是root權限)。
而在Linux中,我們就沒那么幸運了,更改內存保護的API是系統調用(mprotect或pkey_mprotect),并且兩者始終在當前進程的地址空間上運行。所以現在我們來回顧一下在Linux x64架構上解決此問題的方法(假設是root權限)。
如果mprotect總是作用于當前進程,那么我們就需要讓目標進程從它自己的上下文中調用它。這稱為代碼注入,可以通過許多不同的方式實現。我們選擇使用ptrace機制實現它,其允許一個進程“觀察并控制另一個進程的執行”(參見手冊),包括更改目標進程的內存的能力。此機制用于調試器(如gdb)和跟蹤程序(如strace)。使用ptrace注入代碼所需的步驟如下:
1. 通過ptrace附加到目標進程。如果進程中有多個線程,那就終止所有其他線程
2. 找到可執行內存區域(通過檢查/proc/PID/maps)并在那里寫操作碼(hex:0f 05)
3.根據調用約定修改寄存器:首先將rax更改為mprotect的系統調用號(即10)。然后三個參數(起始地址,長度和所需的保護)分別存儲在rdi,rsi和rdx中。最后,將rip更改為步驟2中使用的地址
4. 恢復進程直到系統調用返回(ptrace允許你跟蹤系統調用的進入和退出)
5. 恢復被覆蓋的內存和寄存器,從進程中分離并恢復正常執行
這種方法是第一個也是最直觀的方法,但是我們之后發現Linux中的另一種叫seccomp的機制會工作得更好。它是Linux內核中的一個安全工具,允許進程自己進入某種封閉狀態,除了read,write,_exit和sigreturn之外,它不能調用任何系統調用。不過也可以選擇任意系統調用及其參數來僅僅過濾指定的系統調用。
因此,如果進程啟用了seccomp模式并且我們嘗試將mprotect調用到其中,那么內核將終止進程,因為不允許此系統調用。所以我們要尋求更好的解決方案......
由于seccomp,用戶態中每個解決方案都不可行,因此下一個方法肯定存在于內核態中。在Linux內核中,每個線程(用戶線程和內核線程)都由名為task_struct的結構表示,并且當前線程(任務)可通過指針訪問。內核中mprotect的內部實現使用指針current,所以我們首先想到的是將mprotect的代碼復制粘貼到我們的內核模塊,并用指向目標線程的task_struct的指針替換每次出現的current。
可能你已經猜到了,復制C代碼并不是那么簡單,其中有大量我們無法訪問的,未導出的函數,變量和宏。某些函數聲明在頭文件中導出,但內核不會導出它們的實際地址。如果內核是由kallsyms支持編譯的,那么這個特定的問題就可以解決,然后通過文件/proc/kallsysm導出所有內部符號。
盡管存在這些問題,我們仍以mprotect的本質進行嘗試,甚至僅用于教育目的。因此,我們開始編寫一個內核模塊,它獲取mprotect目標PID和參數,并模仿其行為。首先,我們需要獲取所需的內存映射對象,它表示線程的地址空間:
/* 通過PID尋找任務 */ pid_struct = find_get_pid(params.pid); if (!pid_struct) return -ESRCH; task = get_pid_task(pid_struct, PIDTYPE_PID); if (!task) { ret = -ESRCH; goto out; } /* Get the mm of the task */ mm = get_task_mm(task); if (!mm) { ret = -ESRCH; goto out; } … …out: if (mm) mmput(mm); if (task) put_task_struct(task); if (pid_struct) put_pid(pid_struct);
現在我們有了內存映射對象,就需要深入挖掘。Linux內核實現了一個抽象層來管理內存區域,每個區域由結構vm_area_struct表示。為了找到正確的內存區域,我們使用函數find_vma,它通過所需的地址搜索內存映射。
vm_area_struct包含字段vm_flags,其以與結構無關的方式表示存儲器區域的保護標志,vm_page_prot以體系結構相關的方式表示。單獨更改這些字段不會真正地影響頁表(但會影響proc/PID/maps的輸出,我們已經嘗試過)。 你可以點擊這里獲取更多內容。
在深入研究內核代碼之后,我們發現了真正改變內存區域保護所需的最基本工作:
1. 將字段vm_flags更改為所需的保護
2. 調用函數vma_set_page_prot_func來根據vm_flags字段更新vm_page_prot
3. 調用函數change_protection_func更新頁表中的保護位。
這段代碼雖然有效,但它有很多問題,首先,我們只實現了mprotect的基本部分,但原始函數比我們做的要多得多(例如通過保護標志分割和連接內存區域)。其次,我們使用兩個內核函數,這些函數不是由內核導出的(vma_set_page_prot_func和change_protection_func)。我們可以使用kallsyms來調用它們,但是這很容易出問題(將來可能會更改它們的名稱,或者會改變內存區域的整個內部實現)。所以我們想要一個更通用的解決方案,不考慮內部結構。
這種方法與第一種方法非常相似,因為我們希望在目標進程的上下文中執行代碼。但在這里,我們會用自己的線程中執行代碼,同時使用目標進程的“內存上下文”,這意味著:我們會使用其地址空間。
通過幾個API可以在內核態下更改地址空間,我們使用了use_mm。如文檔明確指出的那樣,“此例程僅用于從內核線程上下文中調用”。這些是在內核中創建的線程,不需要任何用戶地址空間,因此可以更改其地址空間(地址空間內的內核區域在每個任務中以相同的方式映射)。
在內核線程中運行代碼有一種簡單方法,就是內核的工作隊列接口,它允許你使用特定例程和特定參數來安排工作。我們的例程獲取所需進程的內存映射對象和mprotect的參數,并執行以下操作(do_mprotect_pkey是內核中實現mprotect和pkey_mprotect系統調用的內部函數):
use_mm(suprotect_work->mm);suprotect_work->ret_value = do_mprotect_pkey(suprotect_work->start, suprotect_work->len, suprotect_work->prot, -1);unuse_mm(suprotect_work->mm);
當我們的內核模塊在某個進程(通過一個特殊的IOCTL)獲得更改保護的請求時,它首先找到所需的內存映射對象,然后使用正確的參數來調度工作。這個方案仍有一個小問題:函數do_mprotect_pkey_func不由內核導出,需要使用kallsyms獲取。與前一個解決方案不同,這個內部函數不太容易發生變化,因為它與系統調用pkey_mprotect有關,而且我們無需處理內部結構。
如果你有興趣,可以在github中找到這個PoC內核模塊的源代碼。
到此,相信大家對“Linux如何在任意進程中修改內存保護”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。