Linux上執行內存中的腳本和程序

在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"
	"time"

	"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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章