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