Linux 匿名文件之 O_TMPFILE, memfd
Linux 系统编程都在跟进程与文件打交道。Linux 上创建文件描述符的接口普通文件open信号,定时器,事件signalfd, eventfd, timerfd_create可进程间通信的特殊文件shm_open, pipe, mkfifo, socket监控文件事件的文件inotify_init内存文件memfd_create当然,还有 dup 及 fcntl 能用来复制文件描述符。所有这些
Linux
系统编程都在跟进程与文件打交道。
Linux
上创建文件描述符的接口
普通文件
open
信号,定时器,事件
signalfd, eventfd, timerfd_create
可进程间通信的特殊文件
shm_open, pipe, mkfifo, socket
监控文件事件的文件
inotify_init
内存文件
memfd_create
当然,还有 dup
及 fcntl
能用来复制文件描述符。
所有这些,跟文件系统有关的有
open, socket(Unix域socket), mkfifo, shm_open, memfd_create.
Unix 域套接字也不一定就有名称,比如 socketpair
在父子进程之间通信,Linux 特有的 Linux
抽象 socket
名空间,将 socket 绑定到一个不会出现在文件系统上的名字。
这里重点关注两个创建匿名文件的方法,匿名文件指的是能够像普通的文件一样进行读写,但是不会再磁盘上创建对应的文件(磁盘上找不到对应的文件名).
O_TMPFILE
标志
open
的 O_TMPFILE
标志 since Linux 3.11
, 可以在一个目录下创建一个匿名文件,一旦关闭文件描述符,文件就自动删除。有O_TMPFILE
标志时,只用指定目录。 可以使用 linkat
为该匿名文件创建一个硬链接(硬链接其实就相当于一个文件名),可以将匿名的临时文件持久化到磁盘上。
#define _GNU_SOURCE //PATH_MAX 需要
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
char path[PATH_MAX];
int fd = open("/root", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR); //对于 O_TMPFILE 表示创建一个匿名的临时文件
write(fd, "hello", 5);
fsync(fd);
printf("pidis %d\n", getpid());
/* 为匿名文件添加文件名,2 中方式*/
//其实即使是匿名文件,在进程的 proc 目录下的 fd 目录,总还是又一个软链接名字的。只是如果是匿名文件,这个软链接名字指向的文件后会被标记为 (deleted)
//1 借助软链接来实现。
snprintf(path, PATH_MAX, "/proc/self/fd/%d", fd);
int ret = linkat(fd, path, AT_FDCWD, "yyyy", AT_SYMLINK_FOLLOW); //如果 yyyy 存在会失败
//2 使用 Linux 特有的 AT_EMPTY_PATH 方式
//int ret = linkat(fd, "", AT_FDCWD, "AAAAAA", AT_EMPTY_PATH); //第二个参数只能是"",不能是空指针,否则会报错。
if(ret < 0)
{
printf("linkat error %d %s\n", errno, strerror(errno));
}
//一旦匿名的临时文件文件有了名字,相当于持久化了。文件描述符关闭之后,就不会像之前的一样被删除。
pause();
return 0;
}
在 proc/self/fd
目录下看到文件描述符软链接指向的文件被删除(deleted). 无论是否是匿名文件,在 Linux 中,至少有一个软链接的名字,在 proc
目录下。
menfd_create
内存文件描述符
shm_open
也能创建一个基于内存的文件描述符,然后结合 mmap
实现进程间的共享内存通信。
一般的用法,都是创建共享内存对象的进程调用 ftruncate
来设置共享内存的大小,其他进程调用 fstat
来获取此大小;问题在于其他进程可能也调用 ftruncate
进行了设置,导致创建者访问未映射的内存,触发 SIGSEGV
信号。而 memfd
创建的文件,可以结合 Linux
内核的新功能,文件封印,阻止其他进程对文件的大小进行调整。
fd = syscall(SYS_memfd_create, "foofile", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd == -1) {
printf("memfd_create error %d %s\n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
int r = fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW);
if(r < 0)
{
printf("memfd_create error %d %s\n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
另外,memfd
可以用来进行无 elf
文件执行,即将可执行文件的内容直接加载进内存(比如从网络上),然后以内存文件的方式进行执行,这样能够规避监控 exece
系统调用追踪到可执行文件。
execve(0x7fff63f2f8c0, 0x7fff63f2f8a0, 0, 0x7fff63f2f8a0 <no return …>
— Called exec() —
strrchr("/proc/self/fd/3", ‘/’)
而 execve
直接运行在磁盘上的 elf
文件,看到的是这样的:
execve(0x7ffe9b9b2c90, 0x7ffe9b9b2c70, 0, 0x7ffe9b9b2c70 <no return …>
— Called exec() —
strrchr("/usr/bin/uname", ‘/’)
#define _GNU_SOURCE //PATH_MAX 需要
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/memfd.h>
#include <sys/syscall.h>
#include <limits.h>
#include <errno.h>
int anonyexec(const char *path)
{
int fd, fdm, filesize;
void *elfbuf;
char cmdline[PATH_MAX];
fd = open(path, O_RDONLY);
filesize = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
elfbuf = malloc(filesize);
read(fd, elfbuf, filesize); //读取 elf 文件
close(fd);
fdm = syscall(__NR_memfd_create, "elf", MFD_CLOEXEC);
ftruncate(fdm, filesize);
write(fdm, elfbuf, filesize); //将elf 文件写入到 memfd 当中
free(elfbuf);
snprintf(cmdline, PATH_MAX, "/proc/self/fd/%d", fdm);
char *argv[] = {cmdline, "-a", NULL};
execve(argv[0], argv, NULL); //执行该匿名文件。
return 0;
}
int main()
{
int result =anonyexec("/usr/bin/uname");
return result;
}
要点其实还在于,即使是匿名文件,在 proc/self/fd
也一定还是有一个软链接的名字可以访问,在 proc/self/fd
目录下看到的内容如下,软链接指向的名字总是 /memfd:xxx (deleted)
, 其中 xxx
是调用 memfd
时传入的名字。
直接执行 /proc/self/fd
下的软链接,目前只发现 memfd_create
可以,通过 O_TMPFILE
和 shm_open
调用产生的传给 execve
系统调用都会返回失败,错误码是 Text file busy。
返回 Text file busy
说明该可执行文件被包含以写方式打开,说明可执行文件只能以只读模式打开,先要将 elf
二进制写入共享对象,所以需要前后两次使用 shm_open
打开,第二次以只读的方式打开。
#define _GNU_SOURCE //PATH_MAX 需要
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/memfd.h>
#include <sys/syscall.h>
#include <limits.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/mman.h>
int anonyexec(const char *path)
{
int fd, fdm, filesize;
void *elfbuf;
char cmdline[PATH_MAX];
fd = open(path, O_RDONLY);
filesize = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
elfbuf = malloc(filesize);
read(fd, elfbuf, filesize);
close(fd);
//fdm = syscall(__NR_memfd_create, "elf", MFD_CLOEXEC);
fdm = shm_open("/elf", O_WRONLY | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IXUSR);
ftruncate(fdm, filesize);
write(fdm, elfbuf, filesize);
free(elfbuf);
close(fdm); //先关闭文件,然后再重新以只读的方式打开。
fdm = shm_open("/elf", O_RDONLY, 0); //只能以只读的方式打开
snprintf(cmdline, PATH_MAX, "/proc/self/fd/%d", fdm);
char *argv[] = {cmdline, "-a", NULL};
int r = execve(cmdline, argv, NULL);
if(r < 0)
{
printf("execve %d %s\n", errno, strerror(errno));
}
return 0;
}
int main()
{
int result =anonyexec("/usr/bin/uname");
return result;
}
总结
memfd
因为是匿名的,所以压根不用考虑读写模式的问题;而 shm_open
只能以只读模式打开时,才能执行。shm_open
将 elf
二进制可执行文件写入 tmpfs
内存文件系统下的 /dev/shm
目录可以达到 memfd
类似的效果 – 执行内存中的可执行文件。
更多推荐
所有评论(0)