diff --git a/README.md b/README.md index a589f07..741219f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,30 @@ io-compatible file reads through an approved Python helper on Windows DLP hosts. -完整文档见 [docs/DLP_IO_LIBRARY.md](docs/DLP_IO_LIBRARY.md)。 +完整文档见 [docs/DLP_IO_LIBRARY.md](docs/DLP_IO_LIBRARY.md);DLP 环境实测特征与判定/写入逻辑推导见 [docs/DLP_ENVIRONMENT_NOTES.md](docs/DLP_ENVIRONMENT_NOTES.md)。 + +## DLP 程序化特征与实现逻辑 + +在透明加密 DLP 环境下实测到的关键事实(详见上面的特征文档): + +- **文件属性不可区分**:加密/未加密文件的 attrib、ADS、大小完全一致,只有内容可判。 +- **同尺寸流式加密**:密文无附加头尾、无静态 magic,且进程按身份分白名单—— + `python.exe`/`cmd.exe`/`powershell.exe` 读加密文件得到明文(透明解密), + `certutil.exe` 等非白名单工具得到磁盘密文。 +- **白名单读透明 ≠ 写加密**:写入是否被加密因机器策略而异,必须运行时探测。 + +对应的实现逻辑: + +- **读取始终明文**:打包 EXE(非白名单)经 stdin 注入 helper 源码拉起白名单 + `python.exe`,由它读明文、经认证 Named Pipe 回传(python 按身份白名单, + 任何父进程拉起都有效)。 +- **`is_encrypted(path)` 加密判定**:先比 certutil 原始哈希与当前进程视图哈希, + 不等即加密;相等时存在歧义(未加密,或 EXE 读加密文件),再比 helper 视图与 + 本地视图的前 64 KiB 定案——Python/pyd/EXE 三种环境都正确。 +- **写入始终未加密**:按文件后缀探测直写是否落盘明文(写探针文件后用 certutil + 原始哈希校验);若直写会被加密,自动切换到 PowerShell stdin 管道中转写盘, + 字节不落地任何临时文件;两通道均不可用则抛 `DlpWriteError`,绝不静默写密文。 + 可用 `DLP_IO_WRITE_CHANNEL=direct|powershell` 强制指定通道。 ## 安装 diff --git a/docs/DLP_ENVIRONMENT_NOTES.md b/docs/DLP_ENVIRONMENT_NOTES.md new file mode 100644 index 0000000..ca48402 --- /dev/null +++ b/docs/DLP_ENVIRONMENT_NOTES.md @@ -0,0 +1,150 @@ +# DLP 环境特征调研与检测/写入实现逻辑 + +本文记录在本机(启用透明加密 DLP 的 Windows)上对 `tests/DLP加密文件.pptx` 与 +`tests/DLP解密文件.pptx` 两个样本的实测结论、探索过程,以及 `dlp_io` 据此实现的 +`is_encrypted()` 判定逻辑和未加密写入通道选择逻辑。所有结论都来自实测,不同 DLP +产品/策略可能不同,库的应对方式是运行时探测而非硬编码假设。 + +## 一、样本事实 + +两个样本文件内容相同(解密后 SHA-256 均为 +`968b5db1…d2947100`),大小完全相同(2,466,572 字节)。加密文件在磁盘上存密文, +前 16 字节对比: + +``` +明文视图: 504b0304 0a000000 0000874e e2400000 (标准 ZIP/PPTX 头 "PK\x03\x04") +密文视图: 181b031a 1510197c 0a19 0e7c 6f726c5f (流式加密, 相同明文 0x00 在不同偏移密文不同) +``` + +特征:同尺寸、全文流式加密、无附加文件头/尾、无静态 magic 可识别。 + +## 二、文件属性层面无法区分(重要结论) + +对两个样本逐项对比,全部一致: + +- `attrib`:都只有 `A` 属性(无 EFS 的 E 属性等差异) +- `dir /r`:均无附加数据流(ADS) +- 文件大小、时间戳:一致 + +**结论:无法通过任何文件元数据判断加密状态,唯一可靠的判别特征是内容。** +这正是 `is_encrypted()` 采用内容对比的原因。 + +## 三、进程白名单模型(实测矩阵) + +DLP 透明加密按「进程身份」决定是否介入读写。用「读取加密样本看到什么」实测: + +| 进程 | 父进程 | 读加密样本视图 | 结论 | +|------|--------|----------------|------| +| `python.exe` | 任意(含非白名单 bash) | 明文 (PK 头) | **身份白名单**,不继承、不降级 | +| `cmd.exe`(`type`) | 任意 | 明文 | 白名单 | +| `powershell.exe` | 任意 | 明文 | 白名单(读透明) | +| `certutil.exe` | bash / cmd | 密文 | **非白名单, raw 视图** | +| Git Bash msys 工具(`head`/`sha256sum`/`cat`) | bash | 密文 | 非白名单 | +| `git.exe`(`hash-object`) | bash | 明文 | 白名单(两样本 blob hash 相同) | + +关键推论: + +1. **helper 桥接设计成立**:打包 EXE(非白名单)拉起 `python.exe` 子进程,python 按身份 + 仍是白名单,读到的就是明文——这是库读取链路的根基。 +2. **`cmd /c type` 不能当密文通道**:cmd 是白名单,读到的同样是明文。社区直觉「用 cmd + 读原始内容」在本 DLP 上不成立。 +3. **certutil 是本机可用的 raw 视图工具**:但 DLP 拦截白名单进程直接拉起它 + (python → certutil 报 `WinError 786`),必须经 `cmd /c certutil …` 中转。 +4. **`cmd /c` 中文路径坑**:把整条命令作为单个字符串传参会触发 cmd 的引号剥离规则, + 中文文件名丢失(`ERROR_FILE_NOT_FOUND`);用列表参数(subprocess 自动逐个加引号) + 则中文路径正常。 + +写入侧实测: + +| 写入方 | 落盘结果(raw 视图校验) | +|--------|--------------------------| +| `python.exe` 直写(.bin / .pptx 后缀) | 明文(本机策略不加密 python 写入) | +| `powershell.exe`(stdin 管道 → `[IO.File]`) | 明文 | +| `cmd /c more > f` | **破坏二进制**(0x00→CRLF、TAB→空格),不可用 | +| `cmd /c copy /b con f` | 挂起等待控制台输入,不可用 | +| `certutil -decode - f` | 不支持 stdin 输入,不可用 | +| `findstr ^ f` | raw 密文视图,但按行重新包装(0x0A→CRLF 注入),只能看头部不能还原内容 | + +注意:白名单读透明 ≠ 写加密。本机 python/powershell 读透明但写入落盘明文; +「白名单进程写入会被加密」的策略在其他机器上仍可能存在——所以写入通道必须 +运行时探测,不能假设。 + +## 四、探索过程(方法记录) + +1. **多通道哈希对比**:`certutil -hashfile`(非白名单视图)对两样本哈希不同 + (33d3… vs 968b…),python 读取两样本哈希相同(968b…)——证明 DLP 生效、 + python 白名单、certutil 非白名单,一条命令同时确认三件事。 +2. **头部字节对比**:`head -c 64 | xxd`(msys 工具,raw 视图)看到密文头,确认 + 同尺寸流加密、无附加头。 +3. **文件属性对比**:`attrib` / `dir /r` 无差异——排除元数据检测路线。 +4. **写入通道候选实验**:逐一验证 more/copy con/certutil -decode/findstr 全部 + 有二进制安全性或可用性问题,最终 `powershell [Console]::OpenStandardInput()` + 管道方案通过全字节(含 0x00/0x1A/0xFF)哈希校验。 +5. **误报纠正**:`python → cmd /c type` 一度显示明文,先怀疑「白名单继承」, + 后用 `bash → cmd /c type`(仍是明文)和 `bash → python`(明文)交叉验证, + 确认 python/cmd 均为身份白名单而非继承。 + +## 五、is_encrypted() 判定逻辑 + +判据:文件已加密 ⟺ 白名单视图 ≠ 非白名单(raw)视图。 + +难点:当前进程自身是否白名单是未知的(Python/pyd 是白名单,打包 EXE 不是), +单次对比存在歧义。`dlp_io.is_encrypted(path)` 用两阶段消歧: + +``` +h_raw = certutil -hashfile(path) # 非白名单 raw 视图(经 cmd /c 中转) +h_native = hashlib(当前进程读 path) + +if h_raw != h_native: + return True # 视图不一致: 文件已加密, 且当前进程是白名单 + +# 歧义分支: 文件未加密, 或文件已加密但当前进程是非白名单 EXE +head_helper = helper(白名单 python)读前 64 KiB +head_native = 当前进程读前 64 KiB +return head_helper != head_native +``` + +三种运行环境全部正确: + +| 环境 | 加密文件 | 未加密文件 | +|------|----------|------------| +| Python / pyd(白名单) | 阶段一即判定 True | 阶段二两视图一致 → False | +| 打包 EXE(非白名单) | 阶段一相等(都看到密文)→ 阶段二 helper 明文头 ≠ 本地密文头 → True | 阶段二一致 → False | + +代价:未加密文件需一次完整 certutil 哈希 + 一次本地完整读取 + helper 前 64 KiB +读取;大文件应缓存结果。库故意不引入「期望 magic 表」——内容对比与文件类型无关。 + +## 六、未加密写入通道选择逻辑 + +目标:任何环境下 `dlp_io.open(path, "w"/"a"/"x")` 落盘都是未加密文件。 + +``` +首次写某后缀时: + 1. 环境变量 DLP_IO_WRITE_CHANNEL=direct|powershell → 强制, 跳过探测 + 2. 直写探测: temp 目录写探针文件(同后缀, 含 0x00/0x1A/0xFF 等边界字节) + → certutil raw 哈希 == 内容哈希 ? + - 一致 → DIRECT(EXE 环境恒真; 本机 python 也真) + - certutil 不可用 → 视为无 DLP → DIRECT + 3. 否则 PowerShell 中转探测: 同 payload 经 stdin 管道由 powershell 子进程 + ([Console]::OpenStandardInput → [IO.File]::Open) 写盘 → 再校验 raw 哈希 + - 一致 → POWERSHELL + 4. 都失败 → raise DlpWriteError(绝不静默写出密文) +结果按后缀缓存(DLP 写策略常按文档类型区分)。 +``` + +中转通道要点:字节只走 stdin 内存管道,不落地任何临时文件(白名单进程写的临时 +文件本身可能被加密,会造成「密文套娃」);`FileMode` 映射 `w→Create`、 +`a→Append`、`x→CreateNew`;`x` 模式在通道选择前先做本地存在性检查,保证 +`FileExistsError` 语义与原生一致。 + +已知边界:探测文件写在 temp 目录,若 DLP 策略按目录区分,探针结论可能与目标 +目录不符——此时用 `DLP_IO_WRITE_CHANNEL` 显式指定。 + +## 七、相关已知问题 + +- 保护区内 `git push` 必现 `fatal: not a git repository`(DLP 拦截 push 进程 + 对 `.git` 的访问模式),解法见 `.kimi-code/skills/git-push-dlp/`(robocopy + 镜像 `.git` 到 TEMP 后从 stage 目录推送)。 +- 提交样本 fixture 时注意:本机 `git.exe` 是白名单视图,commit 会把加密样本按 + 明文存入仓库。测试用例已按环境自适应编写(依据本机 raw 视图与本地视图是否 + 一致判断期望),CI(无 DLP)与 DLP 开发机都能通过。 diff --git a/docs/DLP_IO_LIBRARY.md b/docs/DLP_IO_LIBRARY.md index adb506c..a666bec 100644 --- a/docs/DLP_IO_LIBRARY.md +++ b/docs/DLP_IO_LIBRARY.md @@ -4,6 +4,8 @@ 当前版本为 `dlp-io==0.2.0`,distribution 名是 `dlp-io`,import 名是 `dlp_io`。 +DLP 环境的实测特征(白名单矩阵、密文样本字节对比、各写入候选通道的可用性实验)与判定/写入逻辑的完整推导记录见 [DLP_ENVIRONMENT_NOTES.md](DLP_ENVIRONMENT_NOTES.md)。 + ## 安装 ### 方式一:Gitea PyPI registry(推荐)