如何在任意進程中修改內存保護屬性
作者:xiaohui
最近,我們在進行一項安全研究時,需要在任意進程中修改內存空間的保護標志。在解決這些問題的過程中,我們學到了一些新的東西,主要是關于Linux機制和內核開發的。本文將介紹我們所采取的三種方法以及每次尋求更好解決方案的原因。
最近,我們在進行一項安全研究時,需要在任意進程中修改內存空間的保護標志。起初,我們發現這項任務看起來很簡單,但在實際操作中,卻發現困難重重,還好這些都不是什么大問題。在解決這些問題的過程中,我們還學到了一些新的東西,主要是關于Linux機制和內核開發的。在以下的詳解中,我們會介紹我們所采取的三種方法以及每次尋求更好解決方案的原因。
背景介紹
在現代操作系統中,每個進程都有自己的虛擬地址空間(從虛擬地址到物理地址的映射)。此虛擬地址空間由內存頁面(某些固定大小的連續內存塊)組成,且每個頁面都有保護標志,這些保護標志決定了允許對該頁面的訪問類型(讀取、寫入和執行)。不過,這種機制依賴于架構頁表(architecture page table)。不過要注意的是,在x64的架構中,你不能只進行頁面寫入,即使你是特意從操作系統請求的,也都同時具有頁面寫入和可讀的功能。
在Windows中,你可以使用API函數VirtualProtect或VirtualProtectEx修改內存空間的保護。VirtualProtectEx使我們的修改任務變得非常簡單:因為它的***個參數hProcess是“要修改其內存保護的進程的句柄”。
不過,在Linux中,修改過程就沒有這么簡單了,因為修改內存保護的API是系統調用mprotect或pkey_mprotect的結果,并且這兩個函數始終在當前進程的地址空間上運行。現在讓我們想辦法解決一下如何在x64架構上的Linux中解決修改的問題,不過前提條件是,我們具有修改設備的root權限。
方法一:代碼注入
如果mprotect總是在當前進程中運行,我們需要讓目標進程從它自己的上下文中調用它。這時就要用到代碼注入了,該方法可以通過許多不同的方式實現。我們可以選擇使用ptrace機制實現它,該機制允許一個進程“觀察和控制另一個進程的執行”,包括修改目標進程的內存和寄存器的能力。這種機制用于調試器(如gdb)和跟蹤實用程序(如strace),使用ptrace注入代碼所需的步驟如下:
1.使用ptrace附加到目標進程,如果進程中有多個線程,那么***停止所有其他線程;
2.找到一個可執行的內存空間(通過檢查/ proc / PID / maps),并在這個空間編寫操作碼syscall(十六進制:0f05);
3.根據調用約定來修改寄存器,首先,將rax修改為mprotect的系統調用號(即10);然后,前三個參數(即起始地址、長度和所需的保護)分別存儲在rdi、rsi和rdx中;***,將rip修改為步驟2中使用的地址;
4.繼續這個過程,直到系統調用返回(ptrace允許你跟蹤系統調用的進入和退出);
5.恢復被修改的內存和寄存器,從進程中將其分離并恢復正常執行;
這種方法是我們的采用的***個也是最直觀的方法,并且非常有效。不過在我們發現了Linux中的另一種完全破壞機制:利用seccomp進行破壞之后,該方法就不是我們的***選擇了。基本上,它是Linux內核中的一個安全工具,允許進程輸入某種形式的“監獄”,除了read,write,_exit和sigreturn之外,它不能進行任何系統調用。還有一個選項,可以指定任意的系統調用及針對它們的過濾參數。
因此,如果進程啟用了seccomp模式并且我們嘗試將一個對mprotect的調用注入其中,那么內核將終止進程,因為該進程是不允許使用此系統調用的。因此,要對這些進程進行調用,就要采用方法二。
方法二:在內核模塊中模擬mprotect系統調用
seccomp(全稱securecomputing mode)是linuxkernel從2.6.23版本開始所支持的一種安全機制。
在Linux系統里,大量的系統調用直接暴露給用戶態程序。但是,并不是所有的系統調用都被需要,而且不安全的代碼濫用系統調用會對系統造成安全威脅。通過seccomp,我們限制程序使用某些系統調用,這樣可以減少系統的暴露面,同時是程序進入一種“安全”的狀態。
由于Linux中存在另一種完全破壞機制:利用seccomp進行破壞,因此這個方法肯定要在內核模式中進行。在Linux內核中,每個線程(包括用戶線程和內核線程)都由一個名為task_struct的結構表示,并且當前線程(任務)可以通過pointer current訪問。內核中mprotect的內部實現使用了pointer current,因此我們的***個想法是,只要將mprotect的代碼復制粘貼到內核模塊中,并將每次出現的current替換為指向目標線程task_struct的指針,不就可以了嗎?
接下來的事情你可能已經猜到了,就是復制C代碼,不過復制過程并不是你想的那么簡單,因為其中存在大量使用我們無法訪問的未導出的函數、變量和宏。某些函數說明會在標頭文件中導出,但是它們的實際地址不是由內核導出的。如果內核是用linux內核符號表kallsyms編譯的,那么通過文件/ proc / kallsysm導出所有內部符號,這個特定的問題就可以解決。因為kallsyms在進行源碼調試時具有相當重要的作用,它可以描述所有不處在堆棧上的內核符號。linux內核在編譯的過程中,將內核中所有的符號(所有的內核函數以及已經裝載的模塊)及符號的地址以及符號的類型信息都保存在了/proc/kallsyms文件中。
盡管存在這個特定問題,我們仍然試圖實現mprotect調用。為此,我們特意編寫一個內核模塊,利用該模塊獲取目標PID和參數以進行mprotect,并模仿其調用行為。首先,我們需要獲取所需的內存映射對象,用它表示線程的地址空間:
- /* Find the task by the 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的輸出,我們已經嘗試過了),詳情請點擊這里。
在對內核代碼進行了一些閱讀和深入研究之后,我們發現要真正攻破內存空間的保護,最重要的工作是以下3方面:
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。正如use_mm的介紹中明確指出的那樣“此例程僅會被用于從內核線程上下文中進行調用”。由于這些線程是在內核中創建的,不需要任何用戶地址空間,因此可以修改它們的地址空間(地址空間內的內核區域在每個任務中都以相同的方式映射)。
在內核線程中運行代碼的一種簡單方法就是通過內核的運行隊列接口(queue interface),它允許你使用特定例程和特定參數來進行進程調用。我們的工作例程也非常簡單,它會獲取所需進程的內存映射對象和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中找到這個概念驗證內核模塊的源代碼。
責任編輯:武曉燕
來源:
4hou