首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >零系统调用的暗度陈仓:深度解构io_uring新型Rootkit的攻击防护

零系统调用的暗度陈仓:深度解构io_uring新型Rootkit的攻击防护

作者头像
云鼎实验室
发布2026-04-09 11:53:57
发布2026-04-09 11:53:57
1290
举报

背景 最近,有媒体报道了关于ARMO开发出的高隐秘新型Rootkits工具curing,该工具使用io_uring提供的opcode,可在不使用open、connect等系统调用的情况下,实现C2服务器通信,敏感文件读取等,号称可以绕过大多数主机安全检测工具(Falco、Tegragon、Microsoft Defender)。为了给大家提供防守侧解决方案,特做技术分析以供业界参考。 io_uring技术详解

io_uring介绍

io_uring是一种支持异步IO执行的机制,在扩展性上支持多种异步操作,例如read、write、open、close等,以下通过一个简单的示例介绍io_uring如何使用liburing库实现文件读取操作,在读取文件过程中,没有任何open和read系统调用。

代码语言:javascript
复制
#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函数调用。

代码语言:javascript
复制
//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的特点

根据官方描述,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,直接在内核中完成不同功能调用,从而绕过大部分安全产品。

|

curing源码

根据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实现零系统调用操作,直接绕过传统安全工具的监控逻辑,这种攻击模式体现了攻击技术创新与安全防护体系迭代的持续对抗。安全从业者必须完成从“系统守护”到“技术先知”的角色转变。不仅要守护好已有的防护方案,更要洞察技术追踪新动向,唯有自身内功深厚方能在技术爆炸的时代筑牢数字防线。

参考链接

  • GitHub - armosec/curing: io_uring based rootkit
  • io_uring Rootkit Bypasses Linux Security Tools - ARMO
  • https://github.com/axboe/liburing/tree/master

END

更多精彩内容点击下方扫码关注哦~

关注云鼎实验室,获取更多安全情报

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-05-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云鼎实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景 最近,有媒体报道了关于ARMO开发出的高隐秘新型Rootkits工具curing,该工具使用io_uring提供的opcode,可在不使用open、connect等系统调用的情况下,实现C2服务器通信,敏感文件读取等,号称可以绕过大多数主机安全检测工具(Falco、Tegragon、Microsoft Defender)。为了给大家提供防守侧解决方案,特做技术分析以供业界参考。 io_uring技术详解
  • io_uring介绍
  • curing的特点
    • curing源码
      • 文件
      • 网络
  • 腾讯安全检查能力
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档