在Linux中可以不需要有腳本或者二進位程式的文件在文件系統上實際存在,只需要有對應的數據在記憶體中,就有辦法執行這些腳本和程式。 原理其實很簡單,Linux里有辦法把某塊記憶體映射成文件描述符,對於每一個文件描述符,Linux會在/proc/self/fd/<文件描述符>這個路徑上創建一個對應描述符的 ...
在Linux中可以不需要有腳本或者二進位程式的文件在文件系統上實際存在,只需要有對應的數據在記憶體中,就有辦法執行這些腳本和程式。
原理其實很簡單,Linux里有辦法把某塊記憶體映射成文件描述符,對於每一個文件描述符,Linux會在/proc/self/fd/<文件描述符>
這個路徑上創建一個對應描述符的實體,這個路徑可以當成普通的文件來用,能正常從中讀出數據,因此只要有可執行許可權,就可以載入後運行。
其中第一步是創建記憶體到文件描述符的映射,這一步可以靠memfd_create
這個系統調用實現。這個系統調用會返回一個文件描述符,關聯到一塊記憶體上,預設大小是0,大多數對普通文件描述符可行的操作對這個描述符也都可用,比如read,write,ftruncate,close。write數據進去的時候系統會自動分配合適長度的記憶體。當所有引用這塊記憶體的fd被close之後,這塊記憶體會被自動釋放。
總之memfd_create
提供了像操作文件一樣操作記憶體的能力,是一切皆文件理念的體現之一。
而且memfd_create
創建的頁面預設有可執行許可權,在proc底下的對應的描述符文件也有可執行許可權。
所以我們只要把腳本或者二進位程式的數據寫進memfd_create
返回的描述符就已經做完前兩步了。其中對於腳本有一些要求,需要帶有Shebang(類似#!/usr/bin/env python3
這種)。
有一點需要註意,雖然/proc/self/fd/<文件描述符>
有描述符文件存在,但實際上這就是個軟鏈接,而我們的數據全在記憶體里。
寫入成功後可以利用execve執行proc下的描述符文件,也可以通過fexecve
系統調用直接調用文件描述符。golang沒提供fexecve,所以示例用exec.Cmd
。
例子:
package main
import (
"fmt"
"os"
"os/exec"
"golang.org/x/sys/unix"
)
func main() {
// 名字其實無所謂,傳空字元傳也許,名字只是方便debug沒有其他影響
fd, err := unix.MemfdCreate("memexec", unix.MFD_CLOEXEC)
if err != nil {
panic(err)
}
file := os.NewFile(uintptr(fd), "memexec")
defer func() {
if err := file.Close(); err != nil {
panic(err)
}
}()
_, err = file.Write([]byte("#!/usr/bin/env python\nimport math\nprint('Hello, world!')\n"))
if err != nil {
panic(err)
}
_, err = file.Write([]byte("print(f'{math.sqrt(2)=}')\n"))
if err != nil {
panic(err)
}
// 因為設置了CLOEXEC,子進程里execve之後看不到這個描述符,會導致調用失敗
// 所以只能用父進程的
cmd := exec.Command(fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), fd))
data, err := cmd.Output()
fmt.Println("output:", string(data))
if err != nil {
panic(err)
}
}
golang的話還以配合embed把二進位程式的數據提前嵌入程式內,這樣寫入的時候會比較方便。
安全性:memfd_create創建的東西預設有可執行許可權,同時預設也是可寫的,很可能會被惡意程式利用,所以目前內核也在推進解決這個問題已經添加了flag可以讓不添加可執行許可權,這裡建議是遵守許可權最小化的原則。
memfd原本的用途:用來在記憶體中創建文件(比如不想在存儲器上創建文件時可以用這個),並可以在父子進程間傳遞(最好配合file sealing api使用,防止數據被意外修改);或者乾脆當匿名共用記憶體用。執行記憶體中的程式是附帶效果。
參考資料
https://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html