Linux上执行内存中的脚本和程序

linux · 浏览次数 : 0

小编点评

**内存映射** 在 Linux 中,不需要脚本或二进制程序的文件在文件系统上实际存在,只需有对应的数据在内存中即可执行这些脚本和程序。此原理基于内存映射,将一块内存映射到文件描述符中。 **memfd_create** `memfd_create()` 函数提供了一个创建内存到文件描述符的映射的系统调用。该函数会返回一个文件描述符,关联到一块内存上,默认大小是 0,大多数情况下可用于普通文件描述符的各种操作。 **脚本在内存中执行** 当所有引用该块内存的 fd 被 `close()` 时,该内存会被自动释放。因此,脚本或二进制程序的执行过程可以完全在内存中完成。 ** Shebang 指令** 由于 `memfd_create()` 创建的页面默认具有可执行权限,因此脚本需要带有 Shebang 指令,例如 `#!/usr/bin/env python3`,才能在执行时被识别。 ** fexecve 系统调用** `fexecve()` 函数可以直接调用文件描述符,从而执行该文件描述符所指向的程序。 ** 示例** ```go package main import ( "fmt" "os" "os/exec" "time" "golang.org/x/sys/unix" ) func main() { // 创建内存到文件描述符的映射 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\import math\print('Hello, world!')\\")) if err != nil { panic(err) } _, err = file.Write([]byte("print(f'{math.sqrt(2)=}')\\")) if err != nil { panic(err) } // 因为设置了CLOEXEC,子进程里execve之后看不到这个描述符,会导致调用失败 // 只能用父进程的 cmd 命令执行 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) } } ```

正文

在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

与Linux上执行内存中的脚本和程序相似的内容:

Linux上执行内存中的脚本和程序

在Linux中可以不需要有脚本或者二进制程序的文件在文件系统上实际存在,只需要有对应的数据在内存中,就有办法执行这些脚本和程序。 原理其实很简单,Linux里有办法把某块内存映射成文件描述符,对于每一个文件描述符,Linux会在/proc/self/fd/<文件描述符>这个路径上创建一个对应描述符的

Linux命令总结

Linux命令总结 1、基本命令 uname -m #显示机器的处理器架构 uname -r #显示正在使用的内核版本 dmidecode -q #显示硬件系统部件 hdparm -i /dev/hda #罗列一个磁盘的架构特性 hdparm -tT /dev/sda #在磁盘上执行测试性读取操作

[转帖]k8s系列-06-containerd的基本操作

https://www.jianshu.com/p/52a2884594f5 上一篇我们介绍了containerd的安装,本篇我们来简单介绍下如何进行使用。 环境 linux服务器 前置安装 躺过的坑,需要安装下面内容,否则执行ctr run的时候会报错如下: ctr: failed to crea

[转帖]远程执行Linux命令和使用for循环执行Linux命令

记录:363 场景:在CentOS 7.9操作系统上,在主机A上远程执行主机B上的Linux命令。使用for循环执行Linux命令,比如把主机A的/etc/yum.repos.d目录下文件,分发到集群其它节点。 1.使用for循环执行Linux命令 场景:把k8s-master01主机文件分发(拷贝

[转帖]Linux权限详解(chmod、600、644、666、700、711、755、777、4755、6755、7755)

https://www.cnblogs.com/monjeo/p/12191673.html 权限简介Linux系统上对文件的权限有着严格的控制,用于如果相对某个文件执行某种操作,必须具有对应的权限方可执行成功。Linux下文件的权限类型一般包括读,写,执行。对应字母为 r、w、x。Linux下权限

[转帖]linux 批量修改文件格式

将Windows上的shell脚本拷贝到Linux时,脚本的编码格式还是docs,需要改成unix才可执行,在文件不多的情况下可以直接手动更改,但是在脚本文件比较多的时候,手动改起来就太麻烦了,此时就可以使用shell命令批量来进行更改。 批量更改脚本如下: for i in `find . -ty

pwn杂项之linux命令执行

通常pwn题目,时常会考到对Linux命令的一些使用,比如当cat被禁用的时候,可以使用tac,或者别的命令代替 下面是buu上的应该题目,考察的就是对liunx命令的理解,以及对程序的分析。 题目地址:BUUCTF在线评测 (buuoj.cn) 扩展:1.当我们输入第一个命令之后加上;然后后面

Linux:进程模型和进程管理

在Linux系统中,执行一个程序或命令就可以触发一个进程,系统会给予这个进程一个ID,称为PID,同时根据触发这个进程的用户与相关属性关系,基于这个PID一组有效的权限设置。举个常见的例子,我们要操作系统的时候通常是利用ssh连接程序或直接在主机上登录,然后获取shell。默认的shell是bash,对应的路径为/bin/bash,那么同时间的每个人登录都是执行/bin/bash,不过每个人获取的

[转帖]Linux环境使用apt-get安装telnet、curl、ifconfig、vim、ping等工具

https://www.cnblogs.com/east7/p/15834866.html 当在Linux服务器执行Telnet命令时,如果提示command not found: telnet,说明服务器上并未安装Telnet命令,需要安装此命令。下面介绍在linux服务器如何安装telnet、c

Python学习之三: 编译二进制

Python学习之三: 编译二进制 摘要 每次使用python 执行py文件其实是比较麻烦的 主要是还得安装python的虚拟机,以及安装对应的pip包. 感觉比较繁杂 理论上最快捷的方式是编译成 二进制直接运行. 所以这里主要是说一下通过pycharm和linux机器进行二进制编译的过程 Pych