From 4b4b933e3d8091b38fa778d982ff62558423737e Mon Sep 17 00:00:00 2001 From: NoDistanceY Date: Thu, 3 Sep 2026 10:39:21 +0800 Subject: [PATCH] =?UTF-8?q?cmake=E5=8F=AF=E8=A7=81=E6=80=A7=E5=92=8CAppArm?= =?UTF-8?q?or=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppArmor/demo1/.gitignore | 1 + AppArmor/demo1/README.md | 207 ++++++++++++++++++ AppArmor/demo1/demo_app.c | 36 +++ AppArmor/demo1/demo_shell.sh | 14 ++ AppArmor/demo1/profile.elf | 11 + AppArmor/demo1/profile.script | 27 +++ AppArmor/demo1/report.txt | 46 ++++ AppArmor/demo1/run.sh | 123 +++++++++++ cmake/visibility/CMakeLists.txt | 35 +++ cmake/visibility/README.md | 64 ++++++ cmake/visibility/app/main.cpp | 19 ++ cmake/visibility/color/include/rgb_convert.h | 4 + cmake/visibility/color/public/color/color.h | 11 + cmake/visibility/color/src/color.cpp | 22 ++ cmake/visibility/color/src/rgb_convert.cpp | 8 + .../printer/public/printer/printer.h | 9 + cmake/visibility/printer/src/printer.cpp | 12 + cmake/visibility/timeutil/timeutil.cpp | 9 + cmake/visibility/timeutil/timeutil.h | 3 + 19 files changed, 661 insertions(+) create mode 100644 AppArmor/demo1/.gitignore create mode 100644 AppArmor/demo1/README.md create mode 100644 AppArmor/demo1/demo_app.c create mode 100755 AppArmor/demo1/demo_shell.sh create mode 100644 AppArmor/demo1/profile.elf create mode 100644 AppArmor/demo1/profile.script create mode 100644 AppArmor/demo1/report.txt create mode 100755 AppArmor/demo1/run.sh create mode 100644 cmake/visibility/CMakeLists.txt create mode 100644 cmake/visibility/README.md create mode 100644 cmake/visibility/app/main.cpp create mode 100644 cmake/visibility/color/include/rgb_convert.h create mode 100644 cmake/visibility/color/public/color/color.h create mode 100644 cmake/visibility/color/src/color.cpp create mode 100644 cmake/visibility/color/src/rgb_convert.cpp create mode 100644 cmake/visibility/printer/public/printer/printer.h create mode 100644 cmake/visibility/printer/src/printer.cpp create mode 100644 cmake/visibility/timeutil/timeutil.cpp create mode 100644 cmake/visibility/timeutil/timeutil.h diff --git a/AppArmor/demo1/.gitignore b/AppArmor/demo1/.gitignore new file mode 100644 index 0000000..c3208fa --- /dev/null +++ b/AppArmor/demo1/.gitignore @@ -0,0 +1 @@ +!*.elf \ No newline at end of file diff --git a/AppArmor/demo1/README.md b/AppArmor/demo1/README.md new file mode 100644 index 0000000..d7af219 --- /dev/null +++ b/AppArmor/demo1/README.md @@ -0,0 +1,207 @@ +# AppArmor 入门 Demo(demo1)技术说明 + +> 环境:Ubuntu 24.04 x86_64(阿里云 ECS),内核 6.8.0-124-generic, +> AppArmor parser 4.0.1,profile ABI 4.0。 +> 运行日期:2026-09-03,最终报告见同目录 `report.txt`。 + +## 1. Demo 目标 + +用一个最小可复现的例子演示 AppArmor(Linux 内核的强制访问控制 / 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.c` 与 `demo_shell.sh` 是同一组探针的两种实现: + +```c +/* 探针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.elf`(ELF 二进制) + +``` +abi , + +/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 ,` | 声明 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.script`(Shell 脚本) + +``` +abi , + +/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。所以: + - 脚本自身要 `r`(bash 要读它); + - `/bin/bash` 要 `rm`(exec 解释器 + mmap); + - 脚本里用到的**每个外部命令**(这里是 `cat`)都要单独授权 —— 本 demo 故意**不**给 `cat` 授权 `/etc/shadow` 的读取,让 `cat` 本身可以 exec、但读文件时被拒,从而演示"子进程继承 profile 约束"。 +2. `@{HOME}` 这类**变量需要先 include tunables 才能用**(`#include `)。本 profile 为保持自包含、便于教学阅读,直接写绝对路径。 + +## 5. 运行方式 + +```bash +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 ` 时必须用绝对路径。 +4. **audit ratelimit 吞掉 DENIED 日志**:`systemctl reload apparmor` 会重载系统全部 200+ 个 profile,产生海量 `STATUS` 审计事件,触发内核 `printk_ratelimit`(`kauditd_printk_skb: xxx callbacks suppressed`),把随后几秒内 demo 真正的 DENIED 事件一并丢弃。解法: + - 装载/卸载单条 profile 直接用 `apparmor_parser -r -W ` / `apparmor_parser -R `,不要 `systemctl reload`; + - 装载后 `sleep 6`(默认 ratelimit 窗口 5 秒)再触发拒绝。 +5. **`deny` 规则不产生日志是设计行为**:想在日志里看到拒绝事件,应使用"不写规则、靠 enforce 默认拒绝"的方式(demo 的 probe3 即为此设计)。 +6. **脚本 profile 授权对象**:给脚本写 profile 时,脚本内用到的外部命令都要逐个授权;漏掉会在运行时以 `Permission denied` 暴露。 + +## 8. 常用命令速查 + +```bash +# 查看 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//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 | + +两者都是 LSM(Linux Security Module)钩子实现,内核只能启用其一(`CONFIG_LSM` 决定)。 diff --git a/AppArmor/demo1/demo_app.c b/AppArmor/demo1/demo_app.c new file mode 100644 index 0000000..808a04a --- /dev/null +++ b/AppArmor/demo1/demo_app.c @@ -0,0 +1,36 @@ +#define _GNU_SOURCE +#include +#include + +int main(void) +{ + /* 探针1: 白名单文件 —— profile 中 /tmp/demo_elf.log rw, 应当成功 */ + FILE* fp = fopen("/tmp/demo_elf.log","w"); + if(fp){ + fprintf(fp,"elf write ok\n"); + fclose(fp); + printf("[probe1] write /tmp/demo_elf.log: ALLOWED\n"); + }else{ + perror("[probe1] open /tmp/demo_elf.log"); + } + + /* 探针2: 显式 deny 规则 —— deny /etc/shadow r, 静默拒绝, 不产生审计日志 */ + fp = fopen("/etc/shadow","r"); + if(fp){ + printf("[probe2] read /etc/shadow: ALLOWED (unexpected!)\n"); + fclose(fp); + }else{ + perror("[probe2] open /etc/shadow"); + } + + /* 探针3: 未在 profile 中列出的路径 —— 隐式默认拒绝(enforce), 拒绝并记录 DENIED 日志 */ + fp = fopen("/tmp/demo_forbidden.log","w"); + if(fp){ + fprintf(fp,"should not be here\n"); + fclose(fp); + printf("[probe3] write /tmp/demo_forbidden.log: ALLOWED (unexpected!)\n"); + }else{ + perror("[probe3] open /tmp/demo_forbidden.log"); + } + return 0; +} diff --git a/AppArmor/demo1/demo_shell.sh b/AppArmor/demo1/demo_shell.sh new file mode 100755 index 0000000..456eedc --- /dev/null +++ b/AppArmor/demo1/demo_shell.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# 探针1: 白名单文件 —— profile 中 /tmp/demo_sh.log rw, 应当成功 +echo "[probe1] shell try write /tmp/demo_sh.log" +echo "shell write ok" > /tmp/demo_sh.log && echo "[probe1] write /tmp/demo_sh.log: ALLOWED" + +# 探针2: 显式 deny —— deny /etc/shadow r, 静默拒绝 +echo "[probe2] shell try read /etc/shadow" +cat /etc/shadow && echo "[probe2] read /etc/shadow: ALLOWED (unexpected!)" + +# 探针3: 未列出的路径 —— 隐式默认拒绝, 记录 DENIED 日志 +echo "[probe3] shell try write /tmp/demo_forbidden.log" +echo "should not be here" > /tmp/demo_forbidden.log && echo "[probe3] write /tmp/demo_forbidden.log: ALLOWED (unexpected!)" + +exit 0 \ No newline at end of file diff --git a/AppArmor/demo1/profile.elf b/AppArmor/demo1/profile.elf new file mode 100644 index 0000000..39cc47e --- /dev/null +++ b/AppArmor/demo1/profile.elf @@ -0,0 +1,11 @@ +abi , + +/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, +} diff --git a/AppArmor/demo1/profile.script b/AppArmor/demo1/profile.script new file mode 100644 index 0000000..d675253 --- /dev/null +++ b/AppArmor/demo1/profile.script @@ -0,0 +1,27 @@ +abi , + +/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, + /etc/ld.so.cache r, + /lib/** rm, + /usr/lib/** rm, + /usr/lib64/** rm, + + # bash 运行所需的基础文件 + /etc/bash.bashrc r, + /etc/profile r, + /etc/profile.d/** r, + /home/fnzhang/.bashrc r, + /home/fnzhang/.profile r, + /dev/tty rw, + + # 外部命令 + /usr/bin/cat rm, + + # 允许写目标文件、拒绝读 shadow + /tmp/demo_sh.log rw, + deny /etc/shadow r, +} diff --git a/AppArmor/demo1/report.txt b/AppArmor/demo1/report.txt new file mode 100644 index 0000000..d3f5f3c --- /dev/null +++ b/AppArmor/demo1/report.txt @@ -0,0 +1,46 @@ + +===== Pre‑cleanup: remove leftover profile ===== +pre‑cleanup done + +===== Step1 compile C elf ===== +compile done: /home/fnzhang/Project/examples/AppArmor/demo1/demo_app + +===== Step2 validate profile syntax ===== +profile.elf syntax OK +profile.script syntax OK + +===== Step3 install profile ===== +profile installed & enforce enabled + +===== Step4 run elf demo_app ===== +[probe2] open /etc/shadow: Permission denied +[probe3] open /tmp/demo_forbidden.log: Permission denied +[probe1] write /tmp/demo_elf.log: ALLOWED +-- verify /tmp/demo_elf.log exists (allowed write): +-rw-r--r-- 1 root root 13 Sep 3 09:48 /tmp/demo_elf.log + +===== Step5 run shell script ===== +[probe1] shell try write /tmp/demo_sh.log +[probe1] write /tmp/demo_sh.log: ALLOWED +[probe2] shell try read /etc/shadow +/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh: line 8: /usr/bin/cat: Permission denied +[probe3] shell try write /tmp/demo_forbidden.log +/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh: line 12: /tmp/demo_forbidden.log: Permission denied +-- verify /tmp/demo_sh.log exists (allowed write): +-rw-r--r-- 1 root root 15 Sep 3 09:48 /tmp/demo_sh.log + +===== Step6 aa-status snapshot ===== + /home/fnzhang/Project/examples/AppArmor/demo1/demo_app: enforce + /home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh: enforce + +===== Step7 AppArmor DENIED logs ===== +[397463.022144] audit: type=1400 audit(1788400106.547:1352): apparmor="DENIED" operation="mknod" class="file" profile="/home/fnzhang/Project/examples/AppArmor/demo1/demo_app" name="/tmp/demo_forbidden.log" pid=382024 comm="demo_app" requested_mask="c" denied_mask="c" fsuid=0 ouid=0 +[397463.026257] audit: type=1400 audit(1788400106.552:1353): apparmor="DENIED" operation="exec" class="file" profile="/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh" name="/usr/bin/cat" pid=382031 comm="demo_shell.sh" requested_mask="x" denied_mask="x" fsuid=0 ouid=0 +[397463.026325] audit: type=1400 audit(1788400106.552:1354): apparmor="DENIED" operation="open" class="file" profile="/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh" name="/etc/locale.alias" pid=382031 comm="demo_shell.sh" requested_mask="r" denied_mask="r" fsuid=0 ouid=0 +[397463.026690] audit: type=1400 audit(1788400106.553:1355): apparmor="DENIED" operation="mknod" class="file" profile="/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh" name="/tmp/demo_forbidden.log" pid=382029 comm="demo_shell.sh" requested_mask="c" denied_mask="c" fsuid=0 ouid=0 +[397463.026706] audit: type=1400 audit(1788400106.553:1356): apparmor="DENIED" operation="open" class="file" profile="/home/fnzhang/Project/examples/AppArmor/demo1/demo_shell.sh" name="/etc/locale.alias" pid=382029 comm="demo_shell.sh" requested_mask="r" denied_mask="r" fsuid=0 ouid=0 + +===== Step8 clean resources ===== + +======================================== +Demo finished, report file: /home/fnzhang/Project/examples/AppArmor/demo1/report.txt diff --git a/AppArmor/demo1/run.sh b/AppArmor/demo1/run.sh new file mode 100755 index 0000000..94a5b98 --- /dev/null +++ b/AppArmor/demo1/run.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# 运行方法: +# step1: chmod +x run.sh demo_shell.sh +# step2: sudo sh run.sh +set -e + +WORK_DIR=$(cd "$(dirname "$0")" && pwd) +cd "${WORK_DIR}" + +if [ "$(id -u)" -ne 0 ];then + echo "please run with sudo: sudo sh run.sh" + exit 1 +fi + +ELF_BIN="${WORK_DIR}/demo_app" +SCRIPT_FILE="${WORK_DIR}/demo_shell.sh" + +PROFILE_ELF_NAME="home.fnzhang.aa_demo.demo_app" +PROFILE_SCRIPT_NAME="home.fnzhang.aa_demo.demo_shell" +PROFILE_ELF="/etc/apparmor.d/${PROFILE_ELF_NAME}" +PROFILE_SCRIPT="/etc/apparmor.d/${PROFILE_SCRIPT_NAME}" + +REPORT="${WORK_DIR}/report.txt" +> "${REPORT}" + +log(){ + echo "$1" + echo "$1" >> "${REPORT}" +} + +echo "========================================" +echo "AppArmor Demo start, workdir: ${WORK_DIR}" +echo "========================================" + +# ========== 【前置清理:解决上次残留冲突】 ========== +log "" +log "===== Pre‑cleanup: remove leftover profile =====" +# 卸载内核中可能残留的旧 profile(-R 按 profile 名卸载) +apparmor_parser -R "${PROFILE_ELF}" 2>/dev/null || true +apparmor_parser -R "${PROFILE_SCRIPT}" 2>/dev/null || true +# 清掉 /etc/apparmor.d 下旧文件(含上次失败残留的错误 profile) +rm -f "${PROFILE_ELF}" "${PROFILE_SCRIPT}" +# 注意:不使用 systemctl reload apparmor —— 它会重载全部 200+ 个 profile, +# 触发内核 printk ratelimit(kauditd callbacks suppressed), +# 把随后 demo 真正的 DENIED 审计事件一并丢弃。 +log "pre‑cleanup done" + +# ========== Step1 compile C elf ========== +log "" +log "===== Step1 compile C elf =====" +gcc "${WORK_DIR}/demo_app.c" -o "${ELF_BIN}" +chmod +x "${ELF_BIN}" +log "compile done: ${ELF_BIN}" + +# ========== Step2 validate profile syntax ========== +log "" +log "===== Step2 validate profile syntax =====" +apparmor_parser -Q "${WORK_DIR}/profile.elf" && log "profile.elf syntax OK" +apparmor_parser -Q "${WORK_DIR}/profile.script" && log "profile.script syntax OK" + +# ========== Step3 install profile ========== +log "" +log "===== Step3 install profile =====" +cp "${WORK_DIR}/profile.elf" "${PROFILE_ELF}" +cp "${WORK_DIR}/profile.script" "${PROFILE_SCRIPT}" +apparmor_parser -r -W "${PROFILE_ELF}" || { rm -f "${PROFILE_ELF}"; log "ERROR: elf profile load failed"; exit 1; } +apparmor_parser -r -W "${PROFILE_SCRIPT}" || { rm -f "${PROFILE_SCRIPT}"; log "ERROR: script profile load failed"; exit 1; } + +log "profile installed & enforce enabled" + +# 等待内核 audit ratelimit 窗口恢复,避免 DENIED 事件被 printk ratelimit 吞掉 +sleep 6 + +# ========== Step4 run elf demo_app ========== +log "" +log "===== Step4 run elf demo_app =====" +rm -f /tmp/demo_elf.log /tmp/demo_forbidden.log +"${ELF_BIN}" 2>&1 | tee -a "${REPORT}" || log "(demo_app returned non-zero / was killed - expected)" +log "-- verify /tmp/demo_elf.log exists (allowed write):" +ls -l /tmp/demo_elf.log 2>&1 | tee -a "${REPORT}" + +# ========== Step5 run shell script ========== +log "" +log "===== Step5 run shell script =====" +rm -f /tmp/demo_sh.log /tmp/demo_forbidden.log +"${SCRIPT_FILE}" 2>&1 | tee -a "${REPORT}" || log "(demo_shell.sh returned non-zero / was killed - expected)" +log "-- verify /tmp/demo_sh.log exists (allowed write):" +ls -l /tmp/demo_sh.log 2>&1 | tee -a "${REPORT}" + +# ========== Step6 aa-status snapshot ========== +log "" +log "===== Step6 aa-status snapshot =====" +aa-status --json 2>/dev/null | python3 -c " +import json,sys +d = json.load(sys.stdin) +for name, mode in sorted(d.get('profiles', {}).items()): + if 'demo_app' in name or 'demo_shell' in name: + print(f' {name}: {mode}') +" | tee -a "${REPORT}" + +# ========== Step7 check deny evidences ========== +log "" +log "===== Step7 AppArmor DENIED logs =====" +# 等 ratelimit 窗口过去再收集(sleep 6 已在 Step3 后做过,这里直接查) +dmesg 2>/dev/null | grep -iE 'apparmor=\"DENIED\"' | grep -iE 'demo_app|demo_shell' | tail -10 | tee -a "${REPORT}" \ + || journalctl -k -b --no-pager 2>/dev/null | grep -iE 'apparmor=\"DENIED\"' | grep -iE 'demo_app|demo_shell' | tail -10 | tee -a "${REPORT}" +if [ -z "$(dmesg 2>/dev/null | grep 'apparmor=\"DENIED\"' | grep -iE 'demo_app|demo_shell')" ]; then + log "(提示: 若此处为空, 可能被内核 printk ratelimit 抑制; 显式 deny 规则本身不产生日志)" +fi + +# ========== Step8 clean resources ========== +log "" +log "===== Step8 clean resources =====" +apparmor_parser -R "${PROFILE_ELF}" 2>/dev/null || true +apparmor_parser -R "${PROFILE_SCRIPT}" 2>/dev/null || true +rm -f "${PROFILE_ELF}" "${PROFILE_SCRIPT}" + +rm -f "${ELF_BIN}" +rm -f /tmp/demo_elf.log /tmp/demo_sh.log /tmp/demo_forbidden.log + +log "" +log "========================================" +log "Demo finished, report file: ${REPORT}" diff --git a/cmake/visibility/CMakeLists.txt b/cmake/visibility/CMakeLists.txt new file mode 100644 index 0000000..98b415e --- /dev/null +++ b/cmake/visibility/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.15) +project(visibility_demo CXX) +set(CMAKE_CXX_STANDARD 17) + +# ============ color ============ +add_library(color + color/src/color.cpp + color/src/rgb_convert.cpp +) +target_include_directories(color + PUBLIC color/public # 对外头目录:用户由此 #include + PRIVATE color/include # 内部头目录:rgb_convert.h 只有 color 自己可见 +) +# 宏出现在公开头文件的 #ifdef 里,决定 API 形状。 +# 若只标 PRIVATE:color.cpp 编译出了 brightColorCode, +# 但用户看到的头文件里该声明被预处理掉 → 库和用户对 API 的理解不一致。 +target_compile_definitions(color PUBLIC COLOR_ENABLE_BRIGHT) + +# ============ timeutil ============ +add_library(timeutil timeutil/timeutil.cpp) +target_include_directories(timeutil PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/timeutil) + +# ============ printer ============ +add_library(printer printer/src/printer.cpp) +target_include_directories(printer PUBLIC printer/public) +target_link_libraries(printer + PUBLIC color # printer.h 引用了 color/color.h → 契约的一部分 + PRIVATE timeutil # 只有 printer.cpp 用 → 实现细节 +) +# 时间格式只影响 printer.cpp 的编译 → PRIVATE,不泄漏、改格式不触发下游重编 +target_compile_definitions(printer PRIVATE "PRINTER_TIME_FORMAT=\"%H:%M:%S\"") + +# ============ app ============ +add_executable(app app/main.cpp) +target_link_libraries(app PRIVATE printer) diff --git a/cmake/visibility/README.md b/cmake/visibility/README.md new file mode 100644 index 0000000..ed82dd4 --- /dev/null +++ b/cmake/visibility/README.md @@ -0,0 +1,64 @@ +# CMake target 可见性 Demo(visibility) + +> 来自博客 `docs/编程/20-接口可见性.md` §5.2「完整 Demo」。 +> 演示 `target_include_directories` / `target_link_libraries` / +> `target_compile_definitions` 上 PUBLIC / PRIVATE / INTERFACE 的传播语义。 + +## 运行 + +```bash +cd cmake/visibility +cmake -B build && cmake --build build +./build/app +``` + +预期输出(终端里是彩色的): + +``` +10:37:06 hello (绿色) +bright red (高亮红 —— PUBLIC 宏 COLOR_ENABLE_BRIGHT 传播到 app 的证据) +(没有 "leaked!" —— printer 的 PRIVATE 宏 PRINTER_TIME_FORMAT 被隔离的证据) +``` + +## 目录结构 + +``` +visibility/ +├── CMakeLists.txt +├── app/ +│ └── main.cpp # 最终用户,只认识 printer +├── color/ +│ ├── public/color/color.h # 对外 API 头(含 PUBLIC 宏开关 COLOR_ENABLE_BRIGHT) +│ ├── include/rgb_convert.h # 内部中间件头,仅库内 .cpp 共享(PRIVATE 目录) +│ └── src/{color,rgb_convert}.cpp +├── timeutil/ +│ └── {timeutil.h,timeutil.cpp} # printer 的私有依赖 +└── printer/ + ├── public/printer/printer.h # 对外 API 头(引用了 color → color 是 PUBLIC 依赖) + └── src/printer.cpp +``` + +依赖与可见性一图流: + +``` +app ──PRIVATE──> printer ──PUBLIC──> color ✓ app 能 include color.h、感知 COLOR_ENABLE_BRIGHT + │ └─(内部) rgb_convert.h ✗ 外部看不见(include/ 是 PRIVATE 目录) + └────PRIVATE──> timeutil ✗ app 看不见 timeutil.h + └────PRIVATE 宏 PRINTER_TIME_FORMAT ✗ 不泄漏给 app +``` + +## 验证实验(§5.3) + +| # | 操作 | 实测结果 | +|---|------|---------| +| 1 | 直接编译运行 | ✅ 输出 bright red,不输出 leaked! | +| 2 | main.cpp 加 `#include "rgb_convert.h"` | ✅ `fatal error: rgb_convert.h: No such file or directory` | +| 3 | main.cpp 加 `#include "timeutil.h"` | ✅ `fatal error: timeutil.h: No such file or directory` | +| 4 | printer 的 `PUBLIC color` 改 `PRIVATE color` | (未跑)printer 自身编译过,app 找不到 color/color.h 报错 | + +## 判断口诀 + +打开自己的公开头文件看一眼: +- 头文件里 `#include` 了谁 → 那个库 **PUBLIC** +- 头文件里 `#ifdef` 了哪个宏(宏影响 API 形状)→ 那个宏 **PUBLIC** +- 只在 .cpp 里出现的库/宏/路径 → **PRIVATE** diff --git a/cmake/visibility/app/main.cpp b/cmake/visibility/app/main.cpp new file mode 100644 index 0000000..3d03eec --- /dev/null +++ b/cmake/visibility/app/main.cpp @@ -0,0 +1,19 @@ +#include +#include + +int main() { + Printer p; + p.print("hello", Color::Green); + + // COLOR_ENABLE_BRIGHT 是 color 的 PUBLIC 宏, + // 顺着 app → printer → color 传播到这里,本分支会被编译进去: +#ifdef COLOR_ENABLE_BRIGHT + std::cout << brightColorCode(Color::Red) << "bright red" << "\033[0m\n"; +#endif + + // PRINTER_TIME_FORMAT 是 printer 的 PRIVATE 宏,这里看不见: +#ifdef PRINTER_TIME_FORMAT + std::cout << "leaked!\n"; // 永远不会被编译进去 +#endif + return 0; +} diff --git a/cmake/visibility/color/include/rgb_convert.h b/cmake/visibility/color/include/rgb_convert.h new file mode 100644 index 0000000..4f6d565 --- /dev/null +++ b/cmake/visibility/color/include/rgb_convert.h @@ -0,0 +1,4 @@ +#pragma once +// 内部工具:把 RGB 分量拼成 ANSI 真彩色转义码。 +// 供 color 库内部多个 .cpp 共享,不属于对外 API。 +const char* rgbToAnsi(int r, int g, int b); \ No newline at end of file diff --git a/cmake/visibility/color/public/color/color.h b/cmake/visibility/color/public/color/color.h new file mode 100644 index 0000000..03175cf --- /dev/null +++ b/cmake/visibility/color/public/color/color.h @@ -0,0 +1,11 @@ +#pragma once + +enum class Color { Red, Green, Blue }; + +const char* colorCode(Color c); + +// 这个宏出现在公开头文件里,直接决定 API 的形状(多不多一个函数)。 +// 因此该宏必须对"用 color 的人"同样可见 → CMake 里标 PUBLIC。 +#ifdef COLOR_ENABLE_BRIGHT +const char* brightColorCode(Color c); // 高亮色版本 +#endif \ No newline at end of file diff --git a/cmake/visibility/color/src/color.cpp b/cmake/visibility/color/src/color.cpp new file mode 100644 index 0000000..f24d051 --- /dev/null +++ b/cmake/visibility/color/src/color.cpp @@ -0,0 +1,22 @@ +#include "color/color.h" +#include "rgb_convert.h" // 内部头:能找到它,靠的是 PRIVATE 的 include/ 搜索路径 + +const char* colorCode(Color c) { + switch (c) { + case Color::Red: return rgbToAnsi(205, 49, 49); + case Color::Green: return rgbToAnsi(13, 188, 121); + case Color::Blue: return rgbToAnsi(36, 114, 200); + } + return ""; +} + +#ifdef COLOR_ENABLE_BRIGHT +const char* brightColorCode(Color c) { + switch (c) { + case Color::Red: return rgbToAnsi(241, 76, 76); + case Color::Green: return rgbToAnsi(35, 209, 139); + case Color::Blue: return rgbToAnsi(59, 142, 234); + } + return ""; +} +#endif diff --git a/cmake/visibility/color/src/rgb_convert.cpp b/cmake/visibility/color/src/rgb_convert.cpp new file mode 100644 index 0000000..0a9abde --- /dev/null +++ b/cmake/visibility/color/src/rgb_convert.cpp @@ -0,0 +1,8 @@ +#include "rgb_convert.h" +#include + +const char* rgbToAnsi(int r, int g, int b) { + static char buf[32]; // demo 从简,非线程安全 + std::snprintf(buf, sizeof(buf), "\033[38;2;%d;%d;%dm", r, g, b); + return buf; +} diff --git a/cmake/visibility/printer/public/printer/printer.h b/cmake/visibility/printer/public/printer/printer.h new file mode 100644 index 0000000..48c8d36 --- /dev/null +++ b/cmake/visibility/printer/public/printer/printer.h @@ -0,0 +1,9 @@ +#pragma once +#include +#include // 公开头引用了 color → color 必须是 PUBLIC 依赖 + // 注意:这里没有 timeutil.h,也没有 PRINTER_TIME_FORMAT + +class Printer { +public: + void print(const std::string& msg, Color c); +}; diff --git a/cmake/visibility/printer/src/printer.cpp b/cmake/visibility/printer/src/printer.cpp new file mode 100644 index 0000000..c00b467 --- /dev/null +++ b/cmake/visibility/printer/src/printer.cpp @@ -0,0 +1,12 @@ +#include "printer/printer.h" +#include "timeutil.h" // 只在实现里用 → timeutil 是 PRIVATE 依赖 +#include + +#ifndef PRINTER_TIME_FORMAT // 只在本 .cpp 里用的宏 → PRIVATE +#define PRINTER_TIME_FORMAT "%H:%M:%S" // 没从 CMake 传进来时的兜底 +#endif + +void Printer::print(const std::string& msg, Color c) { + std::cout << timestamp(PRINTER_TIME_FORMAT) << " " + << colorCode(c) << msg << "\033[0m" << std::endl; +} diff --git a/cmake/visibility/timeutil/timeutil.cpp b/cmake/visibility/timeutil/timeutil.cpp new file mode 100644 index 0000000..ced8525 --- /dev/null +++ b/cmake/visibility/timeutil/timeutil.cpp @@ -0,0 +1,9 @@ +#include "timeutil.h" +#include + +std::string timestamp(const char* fmt) { + char buf[32]; + std::time_t t = std::time(nullptr); + std::strftime(buf, sizeof(buf), fmt, std::localtime(&t)); + return buf; +} diff --git a/cmake/visibility/timeutil/timeutil.h b/cmake/visibility/timeutil/timeutil.h new file mode 100644 index 0000000..10a84d1 --- /dev/null +++ b/cmake/visibility/timeutil/timeutil.h @@ -0,0 +1,3 @@ +#pragma once +#include +std::string timestamp(const char* fmt); // fmt 形如 "%H:%M:%S"