技术博客_auditd内核审计详解

发布时间:2026/10/29 11:12:11
技术博客_auditd内核审计详解

内核里的黑匣子:auditd 如何看见 root 也改不掉的事

——命令审计系列之三:为什么分层才是证据与线索的分界线

系列前作:

  • 之一《TTY 为空的命令,为什么最危险?》—— 能被伪造的字段只能做线索,不能被伪造的字段才能做证据
  • 之二《实时 ≠ 可靠:为什么 PROMPT_COMMAND 挡不住一行 unset》—— 能被一条命令关掉的采集源,同样只能做线索

这篇讲前两篇共同指向的那个答案:分层。让记录者和执行者不在同一个层级,是审计从"线索"升级为"证据"的唯一路径。


一条 sudo 日志引发的追责僵局

某公司一台数据库服务器被人删了一张核心表。查命令审计日志,只有一行:

root 执行了 DROP TABLE orders;

执行身份是 root。但公司里有十一个人能 su 到 root。

运维说不是我,DBA 说不是我,研发也说不是我。日志摆在那里,但它证明不了是谁。

团队这才去翻/var/log/audit/audit.log,找到了这条记录:

type=SYSCALL ... uid=0 euid=0 tty=pts1 ses=4 auid=1007 comm="mysql" exe="/usr/bin/mysql"

auid=1007。

这个字段是最初登录的那个人的 UID——即使用户后来su到了 root,它也不会变。查 1007,是某个外包 DBA 的账号,登录时间是凌晨两点,登录 IP 不在公司办公网段。

僵局解开。

auid这个字段,bash_history给不出,PROMPT_COMMAND也给不出。只有内核层的 auditd 能给。

这篇文章讲的就是这件事:auditd 凭什么能给出别人给不出的东西,它的代价是什么,以及在新机器上它为什么正在被 eBPF 取代。


一、它是什么:内核子系统 + 用户态守护进程

auditd这个名字,严格来说是两样东西的组合:

组件位置作用
audit 子系统内核在内核关键路径上埋点,产生审计事件
auditd 守护进程用户态接收内核事件,按规则过滤写盘

关键在第一行,也只在第一行。

埋点在内核,意味着内核是唯一能看到所有系统调用的地方,也是攻击者(即便拿到 root)最难篡改的层级。这就是它和前两篇讲的两个方案的根本差异——不是"更好",是"不同层"。

配套的还有一套工具:

命令用途
auditctl配置规则、查状态
ausearch查日志(按 key、UID、时间、系统调用)
aureport出汇总报表
augenrules加载持久化规则
audispd事件分发器(外送就靠它)

二、三层架构:数据是怎么流出来的

┌─ 内核态 ────────────────────────────────┐ │ 系统调用入口 / 文件访问 / 网络 │ │ ↓ 埋点产生审计事件 │ │ kauditd(内核线程) │ │ ↓ │ │ ┌──────────────┐ │ │ │ 内核缓冲区 │ ← 满了会丢事件! │ │ └──────────────┘ │ └────────────┬────────────────────────────┘ │ netlink 套接字 ┌────────────▼────────────────────────────┐ │ auditd(用户态守护进程) │ │ · 按规则过滤 │ │ · 写 /var/log/audit/audit.log │ │ · 通过 audispd 分发 │ └────────────┬────────────────────────────┘ │ audisp-remote(外送) │ ┌────▼─────┐ │ 远端日志服务器 / SIEM │ └──────────┘

注意audispd这一层——它是日志外送的正规通道。配置audisp-remote可以把审计日志实时推到远端,这是防篡改体系里"秒级外送"那一步的标准做法。

还要注意那个内核缓冲区——它是 auditd 最容易出问题的地方(第五节细说)。


三、规则语法:三种类型

① 系统调用规则(命令审计的核心)

auditctl-aalways,exit-Farch=b64-Sexecve-kcmd_audit

逐段拆解:

片段含义
-a always,exit加到exit链表——系统调用返回时记录
-F arch=b64指定 64 位架构。必须写
-S execve监控 execve 系统调用
-k cmd_auditkey,给事件打标签,查询用ausearch -k cmd_audit

两个必须理解的细节:

为什么是exit而不是entry?系统调用有entry(进入时)和exit(返回时)两个埋点。审计要用exit——因为只有返回时你才知道它成功了没有、退出码是多少。用 entry 会记录大量最终失败的系统调用。

为什么arch必须写?不写的话,32 位和 64 位调用会各生成一份事件,日志量直接翻倍,而且审计规则匹配会出各种诡异问题。这是新手最常犯的错。

② 文件监控规则(watch)

auditctl-w/etc/passwd-pwa-kpasswd_change auditctl-w/etc/shadow-prwxa-kshadow_access auditctl-w/root/.ssh/authorized_keys-pwa-kssh_key_tamper

-p权限位:r读 /w写 /x执行 /a属性变更。

注意:-w是语法糖,内核会帮你翻译成-S open -S openat -S openat2 ... -F path=...,底层仍然是系统调用规则。

③ 登录与账户规则(自带)

auditd 装好后默认带一批规则(/usr/share/audit/sample-rules/下有模板),覆盖/etc/passwd修改、登录登出、sudo 提权、SELinux 拒绝等。


四、三个必须记住的字段

看一条真实的 execve 事件——注意它是多条记录组成的一个完整事件,靠同一个时间戳和事件 ID 关联:

type=SYSCALL msg=audit(1729998001.123:45678): arch=c000003e syscall=59 success=yes exit=0 items=2 ppid=8821 pid=8830 auid=1000 uid=33 gid=33 euid=33 suid=33 fsuid=33 tty=(none) ses=5 comm="sh" exe="/bin/dash" key="cmd_audit" type=EXECVE msg=audit(1729998001.123:45678): argc=2 a0="sh" a1="-c" type=CWD msg=audit(1729998001.123:45678): cwd="/var/www/html" type=PATH msg=audit(1729998001.123:45678): name="/bin/dash" ...

①auid—— 追责的杀手锏

auid是Audit UID,即最初登录的那个用户的 UID。

用户登录后su到 root:

  • uid→ 变成 0
  • auid→ 保持不变

这就是为什么开头那个案例能破——uid=0只告诉你"现在是 root",auid=1007才告诉你"最初是谁登进来的"。

特殊值auid=4294967295(即0xFFFFFFFF,显示为unset):表示没有登录会话。系统服务、cron 启动的进程通常是这个值。这个判断在做过滤时非常有用(见第五节)。

②ses—— 会话 ID

同一次登录会话的所有事件共享一个ses。用来串联"这个人登录后都干了什么"——和系列第一篇讲的session_id是一个概念。

③tty—— 系列第一篇的主角

auditd 里无终端显示为(none),有终端显示为pts0(注意不是/dev/pts/0,auditd 的写法不同)。

再次印证第一篇那个结论:TTY 是判定反弹 shell 的关键字段,而只有内核层采集才给得出它。

顺带说comm和exe的区别

字段含义能否伪造
comm进程名(/proc/pid/comm)能,prctl(PR_SET_NAME)可改
exe实际可执行文件路径不能直接改(但可以改名文件本身)

所以检测规则里优先用exe,别用comm。攻击者把进程名改成kworker很容易,但exe会诚实地指向/tmp/.x/malware。


五、性能:它最大的痛点

这是 auditd 在生产上最被诟病的地方,也是它正在被 eBPF 取代的根本原因。

为什么慢

每个被监控的系统调用都要走一遍:

  1. 内核态收集上下文(UID、PID、路径、参数…)
  2. 格式化成文本
  3. 通过 netlink 传到用户态
  4. auditd 收到后写盘

致命问题在于:过滤发生在用户态。

内核把所有匹配规则的事件全部吐出来,auditd 收到后再筛。所以规则写得宽(比如-S all),日志量会爆炸。

实测参考:一台中等负载的 Web 服务器开 execve 全量审计,日均几百万到上千万条,日志文件轻松几十 GB。

五个优化手段

# 1. 【最实用】只审计有登录用户的进程,排除系统服务auditctl-aalways,exit-Farch=b64-Sexecve\-Fauid>=1000-Fauid!=4294967295-kcmd_audit# ↑ 这一句通常能砍掉 60%–80% 的日志# 2. 排除高频噪音(监控 agent、健康检查脚本自己)auditctl-anever,exit-Farch=b64-Sexecve-Fexe=/usr/bin/health_check auditctl-aalways,exclude-Fmsgtype=CONFIG_CHANGE# 3. 调大内核缓冲区,防止队列溢出丢事件auditctl-b8192# 默认常是 320,高负载下极易溢出# 4. 丢事件要能发现auditctl-f1# 内核打印告警;auditd 日志里也有# 5. 控制磁盘水位# /etc/audit/auditd.confmax_log_file=200num_logs=10max_log_file_action=ROTATE space_left_action=SYSLOG admin_space_left_action=SUSPEND# ← 磁盘满时暂停审计(别用 halt,会停机导致业务中断)

第 1 条-F auid>=1000是投入产出比最高的一招。系统服务(systemd、cron、各种 agent)自己启动的进程占了 execve 事件的绝大部分,而它们通常不是威胁狩猎的目标。排除掉之后,日志量骤降,信噪比反而上升。

第 3 条要特别留意:-b默认只有 320,高负载机器上很容易溢出。溢出的表现是audit: backlog limit exceeded,然后事件被静默丢弃——这是最危险的情况,你以为在审计,其实在漏。定期查:

ausearch-mKERN--startrecent|grep-i"backlog"

六、规则持久化(新手最常踩的坑)

auditctl加的规则,重启就没了。

必须写进文件:

# 1. 写规则文件vim/etc/audit/rules.d/10-cmd-audit.rules# 2. 加载(会合并 rules.d 下所有 .rules 生成 audit.rules)augenrules--load# 3. 确认auditctl-l

⚠️ 不要直接改/etc/audit/audit.rules—— 它会被augenrules覆盖,你改的东西下次加载就没了。

文件名建议带数字前缀(10-、20-),因为augenrules按文件名字典序合并,顺序有时会影响规则匹配。


七、防篡改:锁规则

auditctl-e2

-e是"启用标志",三个取值:

值含义
0关闭,什么都不记
1启用,规则可改
2启用并锁定——规则不可增删改,连 root 也不行

锁定后会发生什么?

任何auditctl -D(清规则)、auditctl -d(删规则)、修改规则的操作全部被拒绝,并且会在 audit 日志里留下一条AUDIT_CONFIG_CHANGE记录。

这就是分层的威力:

方案关闭动作是否被记录
PROMPT_COMMAND❌ 不记录(记录者就是被关的那个)
auditd(锁定后)✅记录(记录者是内核,执行者是 root)

因为记录者和执行者不在同一个层级。root 能操作规则,但他操作不了内核记录这个动作的过程。

代价(面试必说):锁定后要改规则,必须重启机器。所以生产上不能随便锁——等规则灰度稳定、且能接受重启窗口后,再分批锁定。

这是防篡改七层体系的第 1 层。完整体系回顾:锁规则 → Agent 防杀 → 秒级外送 WORM → 哈希链 → 锚定 → 断流检测 → 把清痕动作写成规则。


八、容器与 K8s 场景的三个坑

坑一:PID namespace 不一致

auditd 在宿主机视角工作,记录的是宿主机 PID。而容器内看到的 PID 是容器 namespace 里的编号。直接拿日志里的pid去容器里查,会对不上。

解法:通过宿主机 PID 反查容器 ID(/proc/<pid>/cgroup里有容器 ID),再关联 K8s 的 Pod 信息。

坑二:宿主机 auditd 能看见容器内的进程(这是好事)

容器内进程在宿主机上一定有对应的 PID。所以宿主机装了 auditd,容器内执行的命令逃不掉。

这个特性在安全上极有价值:攻击者进到容器里,以为自己的操作只发生在容器内,实际上宿主机的内核全看见了。前提是——宿主机必须装 auditd,而容器镜像里通常没有(也不该有)。

坑三:不适合用 DaemonSet 部署

auditd 需要内核权限,且一台机器只能跑一个实例。所以在 K8s 里它只能是节点级组件,不能按 Pod 部署。

这也是为什么 K8s 场景更适合 eBPF 方案(Falco、Tetragon)——它们以 DaemonSet 形式部署,天然适配容器,还能直接富化出 Pod / namespace / 容器镜像等信息。


九、它正在被 eBPF 取代(但要说清楚为什么)

先看对比:

auditdeBPF
埋点位置内核内核
过滤位置用户态(先全量吐再筛)内核态(边采集边筛)
性能开销大小一个量级
内核要求2.6+(几乎都能跑)4.x+,推荐 5.x
可编程❌ 规则语法固定✅ 可富化、可判定、可联动
容器元数据❌ 要自己关联✅ 原生支持 Pod/namespace
等保认可度极高,标准答案较新,但已是主流

核心差异就一句:过滤在哪一层。

auditd 是"内核全采、用户态筛",eBPF 是"内核里就筛完了再出来"。这就决定了性能差一个数量级。

那 auditd 还有存在的必要吗?有,两个不可替代的场景:

  1. 老内核存量环境。CentOS 7 的 3.10 内核,eBPF 能力严重受限,auditd 是唯一选择。这是它在现实里最常见的生存土壤
  2. 合规标准答案。等保 2.0 测评里提到审计,测评师认的是 auditd。很多单位"有 eBPF 但还得装 auditd",就是为了过等保

一句话定位:eBPF 是未来,auditd 是能力下限的保证者。


十、生产规则模板

一份可直接落地的/etc/audit/rules.d/10-cmd-audit.rules:

## 先删除已有规则(加载时会重置) -D ## 调大缓冲区,防丢事件 -b 8192 ## 丢事件时内核告警 -f 1 ## 命令执行审计(核心):只审计有登录用户的进程 -a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=4294967295 -k cmd_audit -a always,exit -F arch=b32 -S execve -F auid>=1000 -F auid!=4294967295 -k cmd_audit ## 排除监控 agent 自身(按需调整路径) -a never,exit -F arch=b64 -S execve -F exe=/usr/local/bin/hids_agent ## 关键文件监控 -w /etc/passwd -p wa -k identity_file_change -w /etc/shadow -p wa -k identity_file_change -w /etc/sudoers -p wa -k sudoers_change -w /root/.ssh/authorized_keys -p wa -k ssh_key_tamper -w /etc/crontab -p wa -k cron_tamper -w /etc/audit/audit.rules -p wa -k audit_rule_tamper -w /etc/audit/rules.d/ -p wa -k audit_rule_tamper ## 审计日志被删除/清空(最强的信号之一) -w /var/log/audit/ -p wa -k audit_log_tamper ## 锁定(确认规则稳定后再开!开启后需重启才能改) # -e 2

最后两条-w /etc/audit/和-w /var/log/audit/是故意加的——它们把"攻击者篡改审计本身"这个动作变成了可检测的告警。这就是系列第二篇强调的思路:把对抗变成检测。


十一、常见查询速查

# 按 key 查命令审计ausearch-kcmd_audit--starttoday# 查某个用户(含 su 前的原始身份)的所有操作ausearch-ua1007--starttoday# 查某台机器某个时间段的失败事件ausearch--start02:00:00--end03:00:00-svno# 查所有对 /etc/shadow 的访问ausearch-kshadow_access# 出汇总报表aureport-u# 按用户aureport-x# 按可执行文件aureport--failed# 失败事件汇总aureport-k# 按 key 汇总# 实时监控(排查问题时很有用)tail-f/var/log/audit/audit.log|ausearch-kcmd_audit--interpret

--interpret会把 UID 翻译成用户名、把数字翻译成可读名称,排查时必加。


十二、auditd一句话总括

"auditd 是内核 audit 子系统加用户态守护进程的组合,最大的价值是埋点在内核——攻击者拿到 root 也不容易改掉内核眼里的记录。auid字段更是追责的关键,它记录最初登录的 UID,su到 root 也不变,这是应用层采集给不出的。

命令审计的核心规则就一条:-a always,exit -F arch=b64 -S execve -k cmd_audit,用exit是因为只有返回时才知道成功与否,arch必须写否则日志翻倍。

它的主要问题是性能——过滤在用户态,内核全量吐事件。所以规则要加-F auid>=1000排除系统服务,能砍掉大半日志;同时要调大-b缓冲区防丢事件,因为默认 320 在高负载下会溢出,而溢出是静默丢弃。

规则要用augenrules持久化,否则重启就没;防篡改用auditctl -e 2锁规则,代价是要重启才能改。

定位上,eBPF 是未来,但 auditd 是能力下限的保证者——老内核能用、等保认它。"


总结:三句话

  1. 分层是证据与线索的分界线。PROMPT_COMMAND关闭自己不被记录,auditd 锁定后关闭动作会被内核记下——差别不在于谁更强,而在于记录者和执行者在不在同一层。

  2. auid是审计的灵魂字段。uid告诉你"现在是谁",auid告诉你"最初是谁"。应用层采集给不出这个字段,而它恰恰是追责的唯一依据。

  3. 性能是它的阿喀琉斯之踵。过滤在用户态决定了它的开销上限,所以规则必须精挑——-F auid>=1000是性价比最高的一招,调大-b是防"静默丢事件"的保命操作。

回扣系列三篇的主线:

  • 之一:能被伪造的字段,只能做线索
  • 之二:能被一条命令关掉的采集源,只能做线索
  • 之三:让记录者和执行者不在同一个层级,线索才能升级为证据

三句话合起来,就是 Linux 命令审计完整的设计哲学——不是堆字段,不是堆规则,是让证据的产生过程独立于被审计者的意志。


附录:一分钟自查清单

  • 规则是不是写进了/etc/audit/rules.d/?只跑过auditctl的话,重启就没了
  • 有没有用augenrules --load加载?有没有直接改audit.rules?(会被覆盖)
  • 规则里有没有-F arch=b64?(没有的话日志翻倍)
  • 用的是always,exit还是always,entry?(必须是 exit)
  • 有没有加-F auid>=1000排除系统服务噪音?
  • -b缓冲区调大了吗?有没有定期查 backlog 溢出?(静默丢事件最危险)
  • 有没有监控/etc/audit/和/var/log/audit/的篡改?(把对抗变成检测)
  • 日志有没有通过audisp-remote实时外送?(本地留着等于等着被删)
  • 规则稳定后有没有考虑auditctl -e 2锁定?接受重启代价吗?
  • 在授权靶场实测:auditctl -D之后,你的告警多久能发现?

最后一条最实在:别相信配置,去打一遍。在授权环境里清掉规则,然后掐表——从规则消失到告警产生,用了多久?这个数字才是你这套体系的真实水平。