Linux 系统编程都在跟进程与文件打交道。

Linux 上创建文件描述符的接口

普通文件

open

信号,定时器,事件

signalfd, eventfd, timerfd_create

可进程间通信的特殊文件

shm_open, pipe, mkfifo, socket

监控文件事件的文件

inotify_init

内存文件

memfd_create

当然,还有 dupfcntl 能用来复制文件描述符。

所有这些,跟文件系统有关的有

open, socket(Unix域socket), mkfifo, shm_open, memfd_create.

Unix 域套接字也不一定就有名称,比如 socketpair 在父子进程之间通信,Linux 特有的 Linux 抽象 socket 名空间,将 socket 绑定到一个不会出现在文件系统上的名字。

这里重点关注两个创建匿名文件的方法,匿名文件指的是能够像普通的文件一样进行读写,但是不会再磁盘上创建对应的文件(磁盘上找不到对应的文件名).

O_TMPFILE 标志

openO_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 目录下。

tmpfile

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 时传入的名字。

memfd
直接执行 /proc/self/fd 下的软链接,目前只发现 memfd_create 可以,通过 O_TMPFILEshm_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;
}

shm_pen

总结

memfd 因为是匿名的,所以压根不用考虑读写模式的问题;而 shm_open 只能以只读模式打开时,才能执行。shm_openelf 二进制可执行文件写入 tmpfs 内存文件系统下的 /dev/shm 目录可以达到 memfd 类似的效果 – 执行内存中的可执行文件。

Logo

更多推荐