examples/AppArmor/demo1/README.md

9.4 KiB
Raw Blame History

AppArmor 入门 Demodemo1技术说明

环境Ubuntu 24.04 x86_64阿里云 ECS内核 6.8.0-124-generic AppArmor parser 4.0.1profile ABI 4.0。 运行日期2026-09-03最终报告见同目录 report.txt

1. Demo 目标

用一个最小可复现的例子演示 AppArmorLinux 内核的强制访问控制 / MAC 模块)最核心的三个行为:

# 场景 预期
1 访问 profile 中显式允许的路径 放行
2 访问 profile 中 deny 显式禁止的路径 静默拒绝EACCES无审计日志
3 访问 profile 中完全没有提及的路径 拒绝 + 内核记录 DENIED 审计日志

同时覆盖两种可执行体形态:

  • ELF 二进制demo_app.c 编译出的 demo_app)—— profile 直接 attach 到二进制路径;
  • Shell 脚本demo_shell.sh)—— AppArmor 不认识"脚本"attach 的是解释器转场transition发生在 exec /bin/bash 时。

2. 文件清单

demo1/
├── demo_app.c       # C 测试程序3 个文件访问探针
├── demo_shell.sh    # bash 测试脚本3 个同类探针
├── profile.elf      # demo_app 的 AppArmor profile
├── profile.script   # demo_shell.sh 的 AppArmor profile
├── run.sh           # 一键运行脚本(需 root
└── report.txt       # 运行产物:完整执行报告

3. 测试程序设计

demo_app.cdemo_shell.sh 是同一组探针的两种实现:

/* 探针1: 白名单 —— profile 允许 rw应当成功 */
fopen("/tmp/demo_elf.log", "w");
/* 探针2: 显式 deny —— deny /etc/shadow r静默拒绝 */
fopen("/etc/shadow", "r");
/* 探针3: 未提及路径 —— 隐式默认拒绝,拒绝并记录 DENIED 日志 */
fopen("/tmp/demo_forbidden.log", "w");

三个探针分别命中 AppArmor 白名单规则的"允许"、deny 关键字、以及 enforce 模式下的默认拒绝default deny

4. Profile 逐行解读

4.1 profile.elfELF 二进制)

abi <abi/4.0>,

/home/fnzhang/Project/examples/AppArmor/demo1/demo_app flags=(enforce) {
    /home/fnzhang/Project/examples/AppArmor/demo1/demo_app r,
    /lib/** rm,
    /usr/lib/** rm,
    /usr/lib64/** rm,
    /etc/ld.so.cache r,
    /tmp/demo_elf.log rw,
    deny /etc/shadow r,
}
语法元素 含义
abi <abi/4.0>, 声明 profile 所依据的规则 ABI 版本。不写会按 parser 默认(本机 4.0)并可能告警;显式写出可保证规则语义在不同 parser 版本间稳定
路径 { ... } 匿名 attach 写法profile 的 attachment 点就是这段路径本身,进程 exec 该路径时自动套用此 profile无需 px 转场规则
flags=(enforce) 强制模式:违规即拒绝。对比 flags=(complain) 只记日志不拦截(调试用)
... r, r = read文件规则末尾必须有逗号
/lib/** rm ** 跨目录通配;rm = read + mmap动态库加载需要 m 权限)
/etc/ld.so.cache r 动态链接器要读的缓存
/tmp/demo_elf.log rw 探针 1 的白名单
deny /etc/shadow r 探针 2 的显式拒绝。deny 的语义是无声拒绝且不产生审计日志——与"没写这条规则时 enforce 模式的默认拒绝(有日志)"是两种不同行为

4.2 profile.scriptShell 脚本)

abi <abi/4.0>,

/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh flags=(enforce) {
    /home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh r,
    /bin/bash rm,
    /usr/bin/bash rm,
    ...
    /usr/bin/cat rm,
    /tmp/demo_sh.log rw,
    deny /etc/shadow r,
}

关键点:

  1. attach 点是脚本路径,但真正受约束的是解释器进程。内核 exec 脚本时会切换到 /bin/bash 执行AppArmor 通过脚本头部的 #! 识别原始路径,把 profile 转场给 bash。所以
    • 脚本自身要 rbash 要读它);
    • /bin/bashrmexec 解释器 + mmap
    • 脚本里用到的每个外部命令(这里是 cat)都要单独授权 —— 本 demo 故意cat 授权 /etc/shadow 的读取,让 cat 本身可以 exec、但读文件时被拒从而演示"子进程继承 profile 约束"。
  2. @{HOME} 这类变量需要先 include tunables 才能用#include <tunables/global>)。本 profile 为保持自包含、便于教学阅读,直接写绝对路径。

5. 运行方式

cd demo1
chmod +x run.sh demo_shell.sh
sudo sh run.sh        # 必须以 root 运行:装载/卸载 profile 需要特权

run.sh 流程:

Pre-cleanup  ─ apparmor_parser -R 卸载可能残留的旧 profile幂等容错
Step1        ─ gcc 编译 demo_app.c
Step2        ─ apparmor_parser -Q 只做语法校验,不装载
Step3        ─ cp 到 /etc/apparmor.d/ 并 apparmor_parser -r -W 装载enforce
              └ 之后 sleep 6 等 audit ratelimit 窗口恢复(见 §7
Step4/5      ─ 分别运行 ELF 与脚本,收集 3 个探针的输出
Step6        ─ aa-status --json 过滤出两个 demo profile 的模式
Step7        ─ dmesg 过滤 apparmor="DENIED" 的审计记录
Step8        ─ 卸载 profile、删除 /etc/apparmor.d 下文件、清理 /tmp 探针文件

6. 实测结果report.txt 摘录)

ELF

[probe1] write /tmp/demo_elf.log: ALLOWED          ← 白名单放行
[probe2] open /etc/shadow: Permission denied       ← deny 规则拒绝
[probe3] open /tmp/demo_forbidden.log: Permission denied  ← 默认拒绝
-rw-r--r-- 1 root root 13 /tmp/demo_elf.log        ← 允许的写确实落盘

脚本:

[probe1] write /tmp/demo_sh.log: ALLOWED
demo_shell.sh: line 8: /usr/bin/cat: Permission denied        ← cat 可 exec读 shadow 被拒
demo_shell.sh: line 12: /tmp/demo_forbidden.log: Permission denied

aa-status

/home/fnzhang/Project/examples/AppArmor/demo1/demo_app: enforce
/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh: enforce

内核审计日志dmesg

apparmor="DENIED" operation="mknod" profile="...demo_app"
    name="/tmp/demo_forbidden.log" requested_mask="c" denied_mask="c"
apparmor="DENIED" operation="exec" profile="...demo_shell.sh"
    name="/usr/bin/cat" requested_mask="x" denied_mask="x"

注意 DENIED 日志里只有 probe3 和 cat 的事件,没有 /etc/shadow——这正是 deny 规则(静默)与默认拒绝(记录日志)的区别在日志层面的直接证据。

7. 踩坑记录(本 demo 调通过程中实际遇到)

  1. profile 路径写死:最初 profile 里是 /home/fnzhang/aa_demo/...目录已不存在。AppArmor 按可执行文件绝对路径匹配,路径变了 profile 就完全不生效。改 profile 前先确认二进制的实际路径。
  2. profile 名 flags=() 路径 {} 是非法语法flags 必须放在 attachment 之后或用独立 flags 行;正确写法是 路径 flags=(enforce) { ... }profile 名 路径 flags=(enforce) { ... }。非法语法会导致 systemctl reload apparmor 整体失败,且报错信息不在终端上、只在 journalctl -u apparmor 里。
  3. @{HOME} 未声明报错Found reference to variable HOME, but is never declared——不带 #include <tunables/global> 时必须用绝对路径。
  4. audit ratelimit 吞掉 DENIED 日志systemctl reload apparmor 会重载系统全部 200+ 个 profile产生海量 STATUS 审计事件,触发内核 printk_ratelimitkauditd_printk_skb: xxx callbacks suppressed),把随后几秒内 demo 真正的 DENIED 事件一并丢弃。解法:
    • 装载/卸载单条 profile 直接用 apparmor_parser -r -W <file> / apparmor_parser -R <file>,不要 systemctl reload
    • 装载后 sleep 6(默认 ratelimit 窗口 5 秒)再触发拒绝。
  5. deny 规则不产生日志是设计行为:想在日志里看到拒绝事件,应使用"不写规则、靠 enforce 默认拒绝"的方式demo 的 probe3 即为此设计)。
  6. 脚本 profile 授权对象:给脚本写 profile 时,脚本内用到的外部命令都要逐个授权;漏掉会在运行时以 Permission denied 暴露。

8. 常用命令速查

# 查看 AppArmor 是否启用
cat /sys/module/apparmor/parameters/enabled     # Y
aa-status                                       # 已装载 profile 列表与模式

# 装载 / 替换 / 卸载单条 profile-W 同时写缓存)
sudo apparmor_parser -r -W /etc/apparmor.d/xxx
sudo apparmor_parser -R /etc/apparmor.d/xxx

# 仅语法校验
apparmor_parser -Q /path/to/profile

# 查看某个进程当前的 profile
cat /proc/<pid>/attr/current

# 查看拒绝日志
sudo dmesg | grep 'apparmor="DENIED"'
sudo journalctl -k | grep apparmor

# 临时把某 profile 切到 complain 模式排查问题
sudo aa-complain /etc/apparmor.d/xxx
sudo aa-enforce /etc/apparmor.d/xxx

9. 与 SELinux 的对比(一句话版)

AppArmor SELinux
标签对象 路径(文件路径 + 进程可执行路径) inode 标签label
规则粒度 每应用一个 profile白名单易读 策略全局统一类型强制TE
学习成本 低,aa-logprof 可从日志生成规则
适用发行版 Ubuntu / Debian / SUSE RHEL / Fedora / Android

两者都是 LSMLinux Security Module钩子实现内核只能启用其一CONFIG_LSM 决定)。