asisctf final 2021 rbtree writeup

比赛的时候一直在看这个题,最后能够构造稳定的 UAF,可一直没有找到方法泄露或者任意地址写可控内容。感谢出题人放出了 exp。😊

https://github.com/r4j0x00/ctf-challenges/tree/main/ASIS-Finals-2021/pwn-rbtree

exp逻辑十分清晰,所以我只分析一下重要的两个点。

eventfd

这是这个题官方解法中比较重要的一环。通过 efd = eventfd(0, 0); 来创建一个新的 eventfd 文件。此时,在内核中通过 kmalloc 分配了一个 struct eventfd_ctx 结构体,这个结构体的大小为 0x30, slab 大小为 0x40:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// https://elixir.bootlin.com/linux/v5.15.7/source/fs/eventfd.c#L30
struct eventfd_ctx {
struct kref kref; // 0x0
wait_queue_head_t wqh; // 0x8
/*
* Every time that a write(2) is performed on an eventfd, the
* value of the __u64 being written is added to "count" and a
* wakeup is performed on "wqh". A read(2) will return the "count"
* value to userspace, and will reset "count" to zero. The kernel
* side eventfd_signal() also, adds to the "count" counter and
* issue a wakeup.
*/
__u64 count; // 0x20
unsigned int flags; // 0x28
int id; // 0x2c
};


// https://elixir.bootlin.com/linux/v5.15.7/source/fs/eventfd.c#L405
static int do_eventfd(unsigned int count, int flags)
{
struct eventfd_ctx *ctx;
struct file *file;
int fd;

/* Check the EFD_* constants for consistency. */
BUILD_BUG_ON(EFD_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON(EFD_NONBLOCK != O_NONBLOCK);

if (flags & ~EFD_FLAGS_SET)
return -EINVAL;

ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return -ENOMEM;

kref_init(&ctx->kref);
init_waitqueue_head(&ctx->wqh);
ctx->count = count;
ctx->flags = flags;
ctx->id = ida_simple_get(&eventfd_ida, 0, 0, GFP_KERNEL);

flags &= EFD_SHARED_FCNTL_FLAGS;
flags |= O_RDWR;
fd = get_unused_fd_flags(flags);
if (fd < 0)
goto err;

file = anon_inode_getfile("[eventfd]", &eventfd_fops, ctx, flags);
if (IS_ERR(file)) {
put_unused_fd(fd);
fd = PTR_ERR(file);
goto err;
}

file->f_mode |= FMODE_NOWAIT;
fd_install(fd, file);
return fd;
err:
eventfd_free_ctx(ctx);
return fd;
}

在官方解法中,通过 eventfd_ctx 来占位 msg2 被 free 后的空缺。之后依次 free(msg1), free(msg3),从而将双向循环链表恢复合法。

考虑到题目的 queue_msg 结构体的的双向链表正好在 offset 0x18 处,其 prev 与 eventfd_ctx 的 count 重合,所以可以从 eventfd_ctx 的 count 泄露出内核堆地址。

一个小细节,直接通过 read 来读取 eventfd 内的内容会导致 count 清空,在解链时会导致链不合法,所以得这样读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint64_t get_eventfd_count(int efd) {
char buf[0x100];
char file_name[0x100];
sprintf(file_name, "/proc/self/fdinfo/%d", efd);

int efdi = open(file_name, 0);
uint64_t len = read(efdi, buf, 0x100);
if (len < 0) {
puts("Failed to get eventfd count");
exit(1);
}

close(efdi);

uint64_t count = 0;
if (sscanf(strstr(buf, "eventfd-count:") + 15, "%llx", &count) != 1) {
puts("Failed to get eventfd count");
exit(1);
}

return count;
}

/proc/sys/kernel/modprobe

这对于我来说是个内核提权新姿势。内核在 /proc/sys/kernel/ 注册了一堆文件,其中包括 modprobe。在内核中,这些文件的权限控制是由 ctl_table 结构体定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// https://elixir.bootlin.com/linux/v5.15.7/source/include/linux/sysctl.h#L116
/* A sysctl table is an array of struct ctl_table: */
struct ctl_table {
const char *procname; /* Text ID for /proc/sys, or zero */
void *data;
int maxlen;
umode_t mode;
struct ctl_table *child; /* Deprecated */
proc_handler *proc_handler; /* Callback for text formatting */
struct ctl_table_poll *poll;
void *extra1;
void *extra2;
} __randomize_layout;



// https://elixir.bootlin.com/linux/v5.15.7/source/kernel/sysctl.c#L1773
static struct ctl_table kern_table[] = {
{
.procname = "sched_child_runs_first",
.data = &sysctl_sched_child_runs_first,
.maxlen = sizeof(unsigned int),
.mode = 0644,
.proc_handler = proc_dointvec,
},
#ifdef CONFIG_SCHEDSTATS
{
.procname = "sched_schedstats",
.data = NULL,
.maxlen = sizeof(unsigned int),
.mode = 0644,
.proc_handler = sysctl_schedstats,
.extra1 = SYSCTL_ZERO,
.extra2 = SYSCTL_ONE,
},
#endif /* CONFIG_SCHEDSTATS */
#ifdef CONFIG_TASK_DELAY_ACCT
{
.procname = "task_delayacct",
.data = NULL,
.maxlen = sizeof(unsigned int),
.mode = 0644,
.proc_handler = sysctl_delayacct,
.extra1 = SYSCTL_ZERO,
.extra2 = SYSCTL_ONE,
},
#endif /* CONFIG_TASK_DELAY_ACCT */
...
// https://elixir.bootlin.com/linux/v5.15.7/source/kernel/sysctl.c#L2104
#ifdef CONFIG_MODULES
{
.procname = "modprobe",
.data = &modprobe_path,
.maxlen = KMOD_PATH_LEN,
.mode = 0644,
.proc_handler = proc_dostring,
},
{
.procname = "modules_disabled",
.data = &modules_disabled,
.maxlen = sizeof(int),
.mode = 0644,
/* only handle a transition from default "0" to "1" */
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ONE,
.extra2 = SYSCTL_ONE,
},
#endif
...

kern_table 被分配在在内核堆空间,所以我们只需要泄露出内核堆地址就可以修改 /proc/sys/kernel/modprobe 的权限了,把原来的 0644 改成 0666 即可达到和修改 modprobe_path 相同的效果。