0xlane/pagecache-guard
GitHub: 0xlane/pagecache-guard
一个基于 fanotify 和 O_DIRECT 的 Linux 运行时完整性防护工具,用于检测并拦截页缓存篡改攻击(如 Dirty Pipe、Copy Fail),弥补传统文件完整性监控无法发现纯内存篡改的缺陷。
Stars: 2 | Forks: 2
# pagecache-guard
**[中文文档](README.zh-CN.md)**
一个运行时完整性防护工具,用于检测并拦截 Linux 页缓存篡改攻击。
它使用 `fanotify` + `O_DIRECT` 将页缓存内容与磁盘上的内容进行比较,覆盖多种攻击面:
- **SUID/SGID 二进制文件** — 拦截 `execve()`,阻止被篡改的二进制文件运行
- **守护进程执行的文件** — 进程树分析检测由 `crond`、`systemd`、`atd` 运行的文件
- **关键配置文件** — 对 `/etc/passwd`、`/etc/profile`、PAM 模块、`ld.so.preload` 进行 inode 级别的监控
- **共享库** — 定期扫描已映射的库
## 为什么需要这个工具
页缓存损坏漏洞允许攻击者修改**只读**文件在内存中的内容:
| CVE | 名称 | 年份 | O_DIRECT 可检测 |
|-----|------|------|:-------------------:|
| CVE-2026-43284 / CVE-2026-43500 | Dirty Frag | 2026 | ✅ |
| CVE-2026-31431 | Copy Fail | 2026 | ✅ |
| CVE-2022-0847 | Dirty Pipe | 2022 | ✅ |
| CVE-2016-5195 | Dirty COW | 2016 | ❌ |
传统的安全工具(文件完整性监控器、镜像扫描器、fs-verity)通过页缓存读取数据,因此**无法检测**这些攻击——它们会将篡改后的数据视为“正常”。`O_DIRECT` 完全绕过页缓存,直接从磁盘读取,使其成为检测纯页缓存篡改(Copy Fail、Dirty Pipe、Dirty Frag)的唯一可靠方法。Dirty COW 是个例外——它会通过页回写将损坏的数据写回磁盘,因此 `O_DIRECT` 读取到的也是相同的篡改内容。Dirty COW 需要通过传统的文件完整性检查(AIDE / `rpm -V` / Tripwire)来检测。
## 工作原理
```
┌──────────────────────────────────────────────────────────────┐
│ pagecache_guard (v0.2) │
│ │
│ FAN_OPEN_EXEC_PERM (mount mark) ────────────────────────── │
│ On execve(): │
│ 1. SUID/SGID binary? → O_DIRECT check (block/allow) │
│ 2. Parent is crond/systemd/atd? → O_DIRECT check │
│ 3. Neither? → FAN_ALLOW (zero overhead) │
│ │
│ FAN_OPEN_PERM (inode marks) ────────────────────────────── │
│ On open() of marked files: │
│ /etc/passwd, /etc/profile, PAM modules, ld.so.preload │
│ → O_DIRECT check (block/allow) │
│ │
│ Periodic O_DIRECT scan (background thread) ─────────────── │
│ Already-mapped shared libraries (libnss, libpam, etc.) │
│ → Alert only (cannot block, already mmap'd) │
└──────────────────────────────────────────────────────────────┘
```
```
flowchart TD
A[Start Guard] --> B[Scan SUID/SGID files]
B --> C[Set up fanotify\nmount marks + inode marks]
C --> D{Event received}
D --> E{SUID/SGID\nbinary?}
E -- Yes --> G{UID = 0?}
E -- No --> F{Parent is\nwatched daemon?}
F -- Yes --> I
F -- No --> P{Inode-marked\nfile?}
P -- Yes --> I
P -- No --> Q[FAN_ALLOW\nSkip]
G -- Yes --> H[Skip check]
G -- No --> I[O_DIRECT vs\nPage Cache]
I --> J{Match?}
J -- Yes --> K[FAN_ALLOW]
J -- No --> L[FAN_DENY\nBlock + Alert]
Q --> D
H --> D
K --> D
L --> D
```
## 快速开始
```
# 仅 SUID 模式 (向后兼容,与 v0.1 相同)
sudo python3 pagecache_guard.py /usr /bin /sbin
# 完全保护 — SUID + daemon-exec + 关键文件 + 定期扫描
sudo python3 -m pagecache_guard \
--watch-daemon crond,anacron,atd,systemd \
--watch-file /etc/passwd /etc/profile /etc/ld.so.preload \
--watch-pam /lib64/security \
--watch-lib /lib64/libnss_files.so.2 /lib64/libpam.so.0 \
/usr /bin /sbin
# 仅添加 daemon-exec 检测 (最简单的扩展)
sudo python3 -m pagecache_guard --watch-daemon crond,systemd /usr
# Dry-run 模式 (仅告警,不阻止)
sudo python3 -m pagecache_guard --dry-run /usr
# 定期重新扫描新的 SUID 文件 (每 5 分钟)
sudo python3 -m pagecache_guard --rescan-interval 300 /usr
# 记录到 syslog + 文件
sudo python3 -m pagecache_guard --syslog --log-file /var/log/pagecache_guard.log /usr
# 同时检查 root 执行
sudo python3 -m pagecache_guard --check-root /usr
```
## 示例输出
```
2026-05-09 14:20:01 INFO Scanning for SUID/SGID files in: /usr
2026-05-09 14:20:03 INFO Found 21 SUID/SGID files
2026-05-09 14:20:03 INFO Watching daemon parents: anacron, atd, crond, systemd
2026-05-09 14:20:03 INFO Auto-discovered 12 PAM modules in /lib64/security
2026-05-09 14:20:03 INFO Inode marks set for 15 files
2026-05-09 14:20:03 INFO Periodic scanner started: 2 libs, interval=5s
2026-05-09 14:20:03 INFO Guard active [ENFORCE] features=[SUID, daemon-exec, inode-watch(15), periodic-scan(2)] check_root=False
# 已阻止被篡改的 SUID 二进制文件:
2026-05-09 14:20:38 WARNING [ALERT] BLOCKED pid=2677362 uid=1000 /usr/bin/su reason=suid
(page cache tampered at offset 0)
# 已阻止被篡改的 cron 脚本:
2026-05-09 14:21:00 WARNING [ALERT] BLOCKED pid=2677500 uid=0 /usr/local/bin/backup.sh reason=daemon:crond
(page cache tampered at offset 128)
# 已阻止被篡改的 PAM 模块:
2026-05-09 14:21:05 WARNING [ALERT] BLOCKED pid=2677510 uid=0 /lib64/security/pam_unix.so reason=inode_watch
(page cache tampered at offset 4096)
```
在用户侧:
```
$ /usr/bin/su
bash: /usr/bin/su: Operation not permitted (exit 126)
```
## 代码结构
```
pagecache-guard/
├── pagecache_guard/ # Python package (v0.2)
│ ├── __init__.py
│ ├── __main__.py # CLI entry point (argparse + orchestration)
│ ├── config.py # Constants and libc handle
│ ├── core.py # O_DIRECT read, integrity check, checksum cache
│ ├── fanotify_handler.py # fanotify setup, mount marks, event loop
│ ├── process_tree.py # /proc traversal for daemon-child detection
│ ├── inode_watcher.py # FAN_OPEN_PERM inode marks for critical files
│ └── periodic_scanner.py # Background thread for mapped-lib scanning
├── pagecache_guard.py # Backward-compatible single-file entry point
├── poc/ # Exploitation PoCs
│ ├── host-attacks/ # 7 host-side attack path PoCs
│ ├── poc_marker.py
│ ├── verify_marker.py
│ └── shocker_copyfail.py
├── README.md
└── README.zh-CN.md
```
## 系统要求
| 组件 | 推荐 | 最低 | 备注 |
|-----------|-------------|---------|-------|
| **内核** | >= 5.0 | >= 2.6.37 | 5.0+ 支持 `FAN_OPEN_EXEC_PERM`;在较旧的内核上自动降级为 `FAN_OPEN_PERM` |
| **RHEL 8** | 4.18.0 | — | 已回移 `FAN_OPEN_EXEC_PERM`(已验证) |
| **文件系统** | ext4 / XFS / Btrfs | — | 必须支持 `O_DIRECT` |
| **权限** | root | `CAP_SYS_ADMIN` | fanotify 权限事件所需 |
| **Python** | 3.6+ | 3.6 | 使用了 f-strings 和 `os.splice` |
## 检测范围
v0.2 将覆盖范围从仅限 SUID 扩展到了 **7/7** 种主机端攻击路径(PoC 见 `poc/host-attacks/`):
| # | 攻击路径 | 机制 | 时机 | 能否拦截? |
|---|-------------|-----------|--------|:----------:|
| 1 | SUID/SGID 二进制文件覆盖 | `FAN_OPEN_EXEC_PERM` + SUID 检查 | 在 execve 时 | ✅ |
| 2 | `/etc/passwd` UID 篡改 | `FAN_OPEN_PERM` inode 标记 | 在被 NSS 打开时 | ✅ |
| 3 | PAM 模块绕过 | `FAN_OPEN_PERM` inode 标记 | 在认证期间被 dlopen 时 | ✅ |
| 4 | 共享库(新加载) | `FAN_OPEN_PERM` inode 标记 | 在被 dlopen 打开时 | ✅ |
| 4' | 共享库(已映射) | 周期性 O_DIRECT 扫描 | 轮询(默认 5 秒) | ❌ 仅告警 |
| 5 | `/etc/profile` 命令注入 | `FAN_OPEN_PERM` inode 标记 | 在被 shell source 时 | ✅ |
| 6 | Cron/systemd 执行的文件 | `FAN_OPEN_EXEC_PERM` + 父进程为守护进程 | 在 execve 时 | ✅ |
| 7 | `ld.so.preload` 劫持 | `FAN_OPEN_PERM` inode 标记 | 在被 ld.so 打开时 | ✅ |
| — | 容器逃逸(层共享) | 周期性 O_DIRECT 扫描 | 轮询 | ❌ 仅告警 |
**7 种路径中的 6 种**提供实时拦截;只有已映射的共享库是仅告警模式(轮询)。
## PoC 脚本
| 脚本 | 用途 |
|--------|---------|
| `poc/poc_marker.py` | 触发 Copy Fail 以将 `0xDEADBEEF` 写入文件的页缓存 |
| `poc/verify_marker.py` | 验证标记是否可见(测试跨容器页缓存共享) |
| `poc/shocker_copyfail.py` | Shocker + Copy Fail 组合 — 通过 `CAP_DAC_READ_SEARCH` 逃逸容器 |
| `poc/host-attacks/` | **7 种主机端利用 PoC**:passwd UID、PAM 绕过、共享库、profile 注入、cron 脚本、ld.so.preload、SUID ELF(参见 [README](poc/host-attacks/README.md)) |
**警告**:PoC 脚本需要存在漏洞的内核,且仅供授权研究使用。
## 技术细节
### 为什么使用 O_DIRECT?
页缓存损坏攻击修改了内核内存中的文件缓存,而不经过 VFS 写入路径。这意味着:
- **无脏页标志** — `sync` 不会将损坏的数据刷新到磁盘
- **文件完整性监控失效** — AIDE/OSSEC 等工具通过页缓存读取,会将篡改后的数据视为正常
- **镜像扫描器失效** — Trivy/Grype 扫描的是压缩后的层 blob,而不是页缓存
- **`docker diff` 失效** — 仅检查 overlayfs 上层的更改
- **fs-verity 失效** — 仅在磁盘到缓存读取时验证,无法检测缓存内的修改
`O_DIRECT` 是唯一能够绕过页缓存直接从块设备读取的标准 POSIX 机制,这使其在检测此类攻击方面具有独特优势。
### 为什么跳过 root?
Root 已经拥有全部权限 — 对于 root 用户而言,SUID 提权毫无意义。跳过 root 可以减少开销,并避免系统服务产生的干扰噪音。
在容器逃逸场景中,攻击者(作为容器 root)损坏了页缓存,但执行被篡改 SUID 二进制文件的**受害者**是主机上的非 root 用户 — 防护工具能正确拦截此类行为。
### 正常更新期间的误报
如果某个 SUID 二进制文件正在更新(例如通过 `yum update`),页缓存和磁盘内容可能会暂时不一致。然而,Linux 内核会阻止执行具有活动写入文件描述符的文件(`ETXTBSY`),因此正常的更新过程不会触发误报拦截。
## 相关研究
**作者的研究文章:**
- [深入理解 Copy Fail:Linux 页缓存漏洞的根本原因、利用与检测](https://reinject.top/en/posts/linux-security/copy-fail-cve-2026-31431/)
**参考链接:**
- [Copy Fail — xint.io](https://xint.io/posts/copy-fail-cve-2026-31431/) — 原始漏洞披露和技术分析
- [NVD 上的 CVE-2026-31431](https://nvd.nist.gov/vuln/detail/CVE-2026-31431)
- [内核修复提交](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a664bf3d603d)
## 许可证
MIT
标签:0day挖掘, API接口, Claude, Copy Fail, CVE检测, Dirty COW, Dirty Frag, Dirty Pipe, DNS 解析, eBPF替代方案, fanotify, Hpfeeds, O_DIRECT, PoC, SGID, SUID, Web报告查看器, 共享库保护, 内存安全, 内存页缓存篡改, 内核安全, 安全防护, 提权漏洞, 暴力破解, 本地提权, 系统安全加固, 系统调用, 网络安全, 运行时完整性, 配置文件监控, 隐私保护, 页缓存