io_uring 指针数组与4K菜单堆

新建

函数调用链

模板

int io_uring_setup()
{
	struct io_uring_params params;
	struct io_uring_sqe *sqe;
	struct io_uring_cqe *cqe;

	memset(&params, 0, sizeof(params));

	int ring_fd = syscall(__NR_io_uring_setup, 16, &params);
	if (ring_fd < 0)
	{
		perror("io_uring_setup");
		exit(EXIT_FAILURE);
	}

	return ring_fd;
}

分配

函数调用链

源码分析

static __cold void **io_alloc_page_table(size_t size)
{
	unsigned i, nr_tables = DIV_ROUND_UP(size, PAGE_SIZE);

	table = kcalloc(nr_tables, sizeof(*table), GFP_KERNEL_ACCOUNT);     [1]

	for (i = 0; i < nr_tables; i++) {
		unsigned int this_size = min_t(size_t, size, PAGE_SIZE);    [2]

		table[i] = kzalloc(this_size, GFP_KERNEL_ACCOUNT);
	}
}

重点关注[1]处的分配功能,size由用户输入决定,实际的分配决定值nr_table是由size被PAGE_SIZE整除后向上取整

在[2]处会对所分配的table数组中的每一个指针分配一个PAGE_SIZE大小的slab,作为最终的菜单堆指针list

__cold static int io_rsrc_data_alloc(struct io_ring_ctx *ctx, int type,
				     u64 __user *utags,
				     unsigned nr, struct io_rsrc_data **pdata)
{
	if (utags) {
		for (i = 0; i < nr; i++) {
			if (copy_from_user(tag_slot, &utags[i],
					   sizeof(*tag_slot)))         [4]
				goto fail;
		}
	}
}

随后在[4]处根据用户提供的tags指针决定是否进行拷贝,tags不为空则将其指向的大小为用户输入nr_args的空间拷贝分配的内存页中

size实际是由用户输入的nr_args所决定,size = nr_args * sizeof(char *),因此最终分配的table数组大小必然是0x40的倍数

但是nr_args的大小并不是完全任意的,存在一个检测机制

int io_sqe_buffers_register(struct io_ring_ctx *ctx, void __user *arg,
			    unsigned int nr_args, u64 __user *tags)
{
	if (!nr_args || nr_args > IORING_MAX_REG_BUFFERS)                          [3]
		return -EINVAL;
	ret = io_rsrc_data_alloc(ctx, IORING_RSRC_BUFFER, tags, nr_args, &data);
}

在[3]处会检测nr_args是否大于IORING_MAX_REG_BUFFERS,而这个值被固定为1<<14,也就是0x4000

因此我们可以分配的指针数组大小范围为0-0x100,且必须是0x40的倍数,标志为GFP_ACCOUNT

也就是说最终可以分配属于kmalloc-64,kmalloc-128,kmalloc-192,kmalloc-256四种slab的指针数组菜单堆

(由于分配了大量的内存页,该方法也可以用作构造页级堆风水,但是该方法噪音较大,存在多种大小的其他噪音)

模板

int io_uring_alloc(int ring_fd, uint32_t size, char *tags)
{
	struct io_uring_rsrc_register rr;
	memset(&rr, 0, sizeof(rr));
	rr.nr = size / (void *) / (void *) * PAGE_SIZE;
	rr.tags = (uint64_t)tags;

	return syscall(__NR_io_uring_register, ring_fd, IORING_REGISTER_BUFFERS2, &rr, sizeof(rr));
}

编辑

函数调用链

源码分析

static int __io_sqe_buffers_update(struct io_ring_ctx *ctx,
				   struct io_uring_rsrc_update2 *up,
				   unsigned int nr_args)
{
	u64 __user *tags = u64_to_user_ptr(up->tags);

	if (up->offset + nr_args > ctx->nr_user_bufs)
		return -EINVAL;

	for (done = 0; done < nr_args; done++) {
		if (tags && copy_from_user(&tag, &tags[done], sizeof(tag))) {
			err = -EFAULT;
			break;
		}
}

模板

释放

函数调用链

源码分析

直接释放分配的全部内存页,篡改指针数组可以做到任意地址释放,不可控制释放某个固定内存页

static void io_free_page_table(void **table, size_t size)
{
	unsigned i, nr_tables = DIV_ROUND_UP(size, PAGE_SIZE);

	for (i = 0; i < nr_tables; i++)
		kfree(table[i]);
	kfree(table);
}

比如喷射大量的0x2000大小的struct msg_msg,篡改指针数组中的指针,使其指向msg_msg的第二块内存页,并将其释放

最后喷射新的结构体到这块内存页上,通过msg_msg结构体实现数据的泄漏(其他常用泄漏类型结构体方法类似)

但是问题在于,没有泄漏地址的情况下,无法精确将指针修改到victim slab上

而通过枚举扫描的方式,则必须要修改遍历到的每一个内存页,在开启了freelist_harden的情况下,非常容易修改到噪音块上从而导致crash

模板

总结

该方法和USMA类似,都是创建了一个可变大小的指针数组,随后篡改这个指针数组中的指针来实现进一步利用

区别在于USMA不能映射属于buddy system的页,但是可以做到地址泄漏,并且大小完全任意

而io_uring所产生的菜单堆,可以做到任意地址写和释放,不限制内存页标志,但是无法做到地址泄漏,且大小限制相对严格

尽管可以通过任意地址释放,在通过msg_msg结构体等传统泄漏方法进行地址泄漏,但稳定性会大受影响,且通用性较差

因此该方法仅作备选,实际使用效果不佳

 

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇