
io_uring是一种支持异步IO执行的机制,在扩展性上支持多种异步操作,例如read、write、open、close等,以下通过一个简单的示例介绍io_uring如何使用liburing库实现文件读取操作,在读取文件过程中,没有任何open和read系统调用。
#include <fcntl.h>
#include <liburing.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUF_SIZE 4096
#define ENTRIES 4
intdo_open(constchar*path,structio_uring*ring){
// 获取一个sq entry
structio_uring_sqe*sqe =io_uring_get_sqe(ring);
// 设置sq任务
io_uring_prep_openat(sqe, AT_FDCWD, path, O_RDONLY,0);
// 将任务提交到内核
io_uring_submit(ring);
structio_uring_cqe*cqe;
// 等待cq结果
io_uring_wait_cqe(ring,&cqe);
int fd = cqe->res;
// 标记cq已经消费,可以被重用
io_uring_cqe_seen(ring, cqe);
return fd;
}
intdo_read(int fd,structio_uring*ring){
// 提交读取请求
structio_uring_sqe*sqe =io_uring_get_sqe(ring);
char*buf =malloc(BUF_SIZE);
structiovec iov ={.iov_base = buf,.iov_len = BUF_SIZE};
io_uring_prep_readv(sqe, fd,&iov,1,0);
io_uring_sqe_set_data(sqe, buf);
io_uring_submit(ring);
// 等待并处理完成事件
structio_uring_cqe*cqe;
io_uring_wait_cqe(ring,&cqe);
if(cqe->res <0){
fprintf(stderr,"Async read failed: %s\n",strerror(-cqe->res));
}else{
printf("Read %d bytes:\n%.*s\n", cqe->res, cqe->res, buf);
}
free(buf);
close(fd);
io_uring_cqe_seen(ring, cqe);
return cqe->res;
}
intmain(int argc,char**argv){
structio_uring ring;
io_uring_queue_init(ENTRIES,&ring,0);
int fd =do_open(argv[1],&ring);
if(fd <0){
fprintf(stderr,"Failed to open file: %s\n",strerror(-fd));
return1;
}
do_read(fd,&ring);
io_uring_queue_exit(&ring);
return0;
}
io_uring核心数据结构是两个队列,这两个队列通过mmap完成用户态和内核态的映射:
● 提交队列:submission queue (SQ)
● 完成队列:completion queue (CQ)
整体的使用方式为:用户态创建一个异步任务,提交到SQ中,通过系统调用io_uring_enter通知内核处理SQ队列数据,将处理完成的数据放入到CQ,用户态从CQ中取数据。
根据上面的示例代码,详细介绍do_open函数是如何通过liburing实现文件打开:
1. 首先使用io_uring_get_sqe获取一个空闲的sqe,该操作无需系统调用,因为之前已经通过io_uring_queue_init初始化了SQ、CQ,使用mmap映射了用户态和内核态队列。
2. 然后通过io_uring_prep_openat填充sqe:https://github.com/axboe/liburing/blob/master/src/include/liburing.h#L824
a. 填充opcode,用于区分不同的操作,结构体定义:https://github.com/axboe/liburing/blob/master/src/include/liburing/io_uring.h#L30
b. 其他字段赋值
3. 使用io_uring_submit将SQE提交到内核,此时触发系统调用io_uring_enter,主要用于提交 IO 和获取 IO 完成情况,具体功能和初始化时配置的 ring->flags 相关
4. 使用io_uring_wait_cqe等待IO异步操作完成,该操作无系统调用,从CQ队列中取数据,文件打开的fd在cqe->res中。
5. 使用io_uring_cqe_seen标记cqe消费完成,内核可以重用该cqe。
通过strace观察系统调用行为,从结果来看,使用liburing打开文件全过程只触发了一次系统调用,并未触发到openat系统调用,这便是io_uring的灵活之处。

深入到内核中,细看一下内核中如何通过opcode完成不同的功能,在do_open处打断点,可以从内核栈上看到由io_openat2调用了do_filp_open函数,最终完成了文件打开。

内核中通过根据不同的opcode选择不同的函数执行功能,具体流程在io_issue_sqe函数中,最终通过def->issue()完成io_openat函数调用。
//opdef.c
const struct io_issue_def io_issue_defs[] = {
...
[IORING_OP_OPENAT] = {
.prep = io_openat_prep,
.issue = io_openat,
},
...
};
//io_uring.c
static int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags)
{
// 内核中定义了opcode函数表,根据opcode执行不同函数
const struct io_issue_def *def = &io_issue_defs[req->opcode];
...
ret = def->issue(req, issue_flags);
...
return 0;
}io_uring通过不同的opcode可以实现不同的功能,详细定义可以参考:
https://github.com/axboe/liburing/blob/master/src/include/liburing/io_uring.h#L207
|
根据官方描述,curing仅通过io_uring系统调用即可完成:
1. 读写文件
2. 创建软连接
3. C2服务器通信
curing在底层技术上通过使用io_uring不同的opcode实现不同的行为,从而完成绕过攻击行为syscall(例如connect、open),使得falco等依赖监控syscall行为的安全产品失去能力。
|
技术上分析
curing是如何绕过falco等安全产品,如下图所示,curing利用io_uring的异步IO能力,使用不同的opcode完成了与server建立连接,读取敏感文件,发送数据一系列操作,而这一系列操作均没有相关行为的syscall触发(即connect、open等syscall),因此,在syscall入口处监控的安全产品均无法感知curing的任何恶意行为。如下图所示,通常的安全产品hook点在syscall入口处,而curing使用io_uring的能力,通过io_uring_setup + opcode这种组合方式,不经过syscall,直接在内核中完成不同功能调用,从而绕过大部分安全产品。

|
根据io_uring上述分析结果,curing的实现方式也不难理解,主要就是通过不同的opcode组合,完成了不同的功能。从curing源码分析来看,打开文件使用Openat函数,通过传入IORING_OP_OPENAT完成打开文件操作,具体在内核中通过IORING_OP_OPENAT对应的函数回调io_openat完成内核操作。


根据分析结果,就不难对curing的运行情况进行监控,下面在不同的位置打断点,查看curing的文件和网络行为。
|

在内核中合适的位置打断点,监控文件打开行为的具体内核调用栈,如图所示,io_uring通过IORING_OP_OPENAT,在内核中使用io_openat完成对shadow文件的打开,最终调用的内核函数仍为vfs_open,并且在当前上下文中进程仍为client,因此,使用kprobe对vfs_open函数进行hook有效且可行,可以采集到行为,同时可以采集进程上下文。
|

同样,在合适的位置打断点查看网络行为,io_uring通过IORING_OP_CONNECT,在内核中使用io_connect完成了网络连接,同样的道理,在进程堆栈上可以选择任意的位置进行kprobe。
如何防御
追其本质,操作系统底层资源仍然有迹可循,拥有共性。抛砖引玉,在这里我们提供几个方向供大家参考,大方向上是深入内核,不继续停留在syscall表层:
1. 使用LSM的相关hook点,稳定且通用,但是依赖系统内核支持;
2. 根据需求分析具体行为,在内核中找到合适的hook点,通过kprobe监控恶意行为,缺陷是对技术人员底层技术要求较高。例如
a. openat选择vfs_open
b. connect选择ip_route_output_flow
3. 根据需求分析具体行为,在内核中找到合适的hook点,使用ebpf在合适的hook点采集数据
4. 针对io_uring相关tracepoint进行采集数据,分析io_uring行为
云鼎实验室研发的OneAgent插件使用ebpf技术可以正常捕获curing的恶意行为,云鼎团队在内核更深的函数进行hook,从而使得curing绕过syscal的行为在检测面前变得无效。这充分证明了深入内核层面进行监控的重要性,也为我们提供了一种有效的防御思路。
总结
攻击技术演进与安全防御的博弈本质上是认知速度的竞赛 。以curing这个新颖的Rootkit 为例,攻击者利用io_uring实现零系统调用操作,直接绕过传统安全工具的监控逻辑,这种攻击模式体现了攻击技术创新与安全防护体系迭代的持续对抗。安全从业者必须完成从“系统守护”到“技术先知”的角色转变。不仅要守护好已有的防护方案,更要洞察技术追踪新动向,唯有自身内功深厚方能在技术爆炸的时代筑牢数字防线。
参考链接

END
更多精彩内容点击下方扫码关注哦~
关注云鼎实验室,获取更多安全情报
