Linux内核auditsyscall的机制和原理

Linux 内核中的系统调用审计机制(通常被称为 auditsyscall)是 Linux Auditing Framework (auditd) 的核心组成部分。它允许系统管理员详细记录特定进程执行了哪些系统调用、何时执行、以什么参数执行,以及调用结果如何。这对于安全审计、入侵检测、故障排查和合规性至关重要。

核心机制与原理:

  1. 审计子系统集成:

    • Linux 内核内置了一个名为 CONFIG_AUDIT 的编译选项(通常默认启用),它提供了审计的基础设施。
    • 审计子系统维护一个内核空间的事件队列(通常基于 sk_buff 结构实现)。
    • 用户空间的审计守护进程 auditd 通过 NETLINK_AUDIT 套接字(一种特殊的 IPC 机制)连接到内核审计子系统,持续读取队列中的审计事件。
  2. 系统调用入口点的挂钩:

    • 这是审计发生的最关键点。内核在执行任何系统调用之前(在系统调用分发函数,如 do_syscall_64ia32_do_call 中)和系统调用返回之后,插入了特定的审计钩子函数。
    • 主要钩子函数:
      • audit_syscall_entry(int arch, unsigned long syscall, ...): 在系统调用实际处理函数执行之前被调用。
        • 参数:arch (体系结构标识符), syscall (系统调用号), args (包含系统调用参数的数组,通常最多 6 个参数)。
        • 作用:记录“系统调用开始”事件。它捕获即将执行的系统调用的编号和参数。
      • audit_syscall_exit(long retval): 在系统调用实际处理函数执行之后、结果返回给用户空间之前被调用。
        • 参数:retval (系统调用的返回值/错误码)。
        • 作用:记录“系统调用结束”事件。它捕获系统调用的执行结果(成功值或错误码)。同时,它会将 entryexit 事件关联起来(通常使用当前任务的 audit_context 结构)。
  3. 审计上下文:

    • 每个进程(任务)在内核中都有一个 task_struct 结构。审计子系统为每个需要审计的进程维护一个 audit_context 结构(通常是 task_struct->audit_context)。
    • 这个上下文在进程生命周期内(或至少在需要审计的事件期间)存在,用于:
      • 临时存储与当前正在审计的事件链相关的信息(例如,关联 entryexit 事件)。
      • 存储过滤器匹配结果,避免对同一个系统调用多次评估规则。
      • 记录额外的审计点信息(如路径名、文件描述符等),这些信息可能在系统调用执行过程中被填充。
  4. 审计规则与过滤器:

    • 用户空间工具 auditctl 用于配置审计规则。规则定义了哪些事件需要被记录。
    • 规则被编译成一种高效的、在内核中运行的 BPF (Berkeley Packet Filter) 字节码。这些规则会被加载到内核审计子系统中。
    • 规则匹配过程:
      • audit_syscall_entry 被调用时,内核会使用加载的 BPF 过滤器检查当前系统调用(结合进程的凭证 uid, gid, euid, egid, suid, sgid, fsuid, fsgid,进程ID pid, 父进程ID ppid, 可执行文件路径 exe, 命令行 comm, 架构 arch, 系统调用号 syscall 和参数 a0-a5 等上下文信息)是否匹配任何审计规则。
      • 如果匹配成功,内核会:
        • audit_context 中设置标志,表明这个系统调用需要被审计。
        • 记录 entry 事件(包含系统调用号和参数)。
      • audit_syscall_exit 被调用时,它会检查 audit_context 中的标志:
        • 如果标志被设置(表示在 entry 时匹配了规则),则记录 exit 事件(包含系统调用号和返回值 retval)。
        • 即使 exit 时没有显式规则匹配 retval,只要 entry 匹配了,exit 事件就会被记录(因为规则通常关注的是“尝试执行某个动作”)。
      • 规则也可以基于返回值进行过滤(例如,只记录失败 openexit 事件)。
  5. 审计记录的生成与传递:

    • 当决定记录一个事件(SYSCALL 类型的 entryexit)时:
      • 内核分配一个 audit_buffer
      • 使用 audit_log_format 系列函数将事件信息格式化写入缓冲区。信息包括:
        • 类型: SYSCALL
        • 时间戳: 精确到纳秒。
        • 序列号: 唯一标识事件的递增数字。
        • 进程信息: pid, ppid, uid, gid, euid, suid, fsuid, egid, sgid, fsgid, auid (审计用户ID,通常是登录用户的原始UID), ses (会话ID)。
        • 可执行文件信息: exe (路径), comm (命令行)。
        • 系统调用信息: syscall (编号), arch (体系结构), success (是/否,基于 exitretval), exit (具体的返回值/错误码)。
        • 参数: a0 - a5 (对于 entry 事件)。
        • 其他上下文信息: 如当前工作目录 (cwd), 文件路径(如果审计点捕获了,如 openpath 规则项)。
      • 格式化的记录被放入内核审计队列。
    • 用户空间的 auditd 守护进程持续监控队列。一旦有新记录到达:
      • auditd 通过 netlink 套接字读取记录。
      • auditd 根据其配置文件 (auditd.conf) 将记录写入日志文件(通常是 /var/log/audit/audit.log)。
      • auditd 也可以配置为实时发送警报或执行其他操作。
  6. 性能考量:

    • 审计,尤其是记录所有系统调用,会带来显著的性能开销。内核做了大量优化:
      • BPF 过滤器: 规则编译成高效的字节码,在内核中快速评估。
      • 快速路径/慢速路径: 如果 audit_context 为空且没有全局规则要求审计,内核会快速跳过审计检查。只有在需要时才创建 audit_context 并执行完整规则匹配。
      • 异步记录: 事件记录是异步进行的,放入队列后系统调用即可继续执行,不阻塞调用进程(auditd 处理队列)。
      • 选择性审计: 管理员应谨慎定义规则,只审计真正需要的特定系统调用、用户或文件,以最小化性能影响。

总结流程:

  1. 用户进程发起系统调用(如 open)。
  2. 内核陷入系统调用入口。
  3. 调用 audit_syscall_entry(arch, SYS_open, fd, pathname, flags, mode)
  4. 内核审计子系统用加载的 BPF 过滤器检查:(arch, syscall=SYS_open, uid=..., pathname=...) 是否匹配任何规则?
  5. 如果匹配:
    • 为当前任务设置 audit_context(如果还没有)。
    • audit_context 中标记需要审计此系统调用。
    • 生成 SYSCALL entry 记录(含 syscall, args),放入队列。
  6. 内核执行实际的 open 系统调用代码。
  7. 系统调用执行完毕,准备返回用户空间。
  8. 调用 audit_syscall_exit(retval)
  9. 检查 audit_context如果之前 entry 被标记需要审计
    • 生成 SYSCALL exit 记录(含 syscall, success, exit=retval),放入队列。
  10. 内核返回系统调用结果 (retval) 给用户进程。
  11. 用户空间 auditd 从队列读取 entryexit 记录,写入 /var/log/audit/audit.log

关键点:

  • auditsyscall 不是单个函数,而是指代内核在系统调用入口/出口处触发审计记录的整个机制
  • 审计发生在内核空间,由内核代码直接挂钩系统调用执行路径。
  • 是否记录一个系统调用由用户配置的、在内核中运行的 BPF 规则决定。
  • 记录包含极其详细的上下文信息(用户、进程、参数、结果)。
  • 性能影响显著,必须精确配置规则以避免过度开销。
  • 用户空间工具 (auditctl, auditd, ausearch, aureport) 用于配置规则、收集和解读日志。

理解这个机制对于有效利用 Linux Audit 框架进行安全监控和合规性报告至关重要。