新建
函数调用链
模板
int io_uring_setup()
{
struct io_uring_params params;
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
memset(¶ms, 0, sizeof(params));
int ring_fd = syscall(__NR_io_uring_setup, 16, ¶ms);
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结构体等传统泄漏方法进行地址泄漏,但稳定性会大受影响,且通用性较差
因此该方法仅作备选,实际使用效果不佳