三份旧笔记
素材是三份放在一起的旧东西:
- 一份 NTFS 权限实验总结,全是「禁止某个权限项之后会发生什么」的经验条目;
- 一张 12 个 CLSID 的对照表,文件名写着「摸来的文件后缀」;
- 一个 .bat,用
SUBST加RECYCLED目录,把一堆文件变成一台「O 盘」。
它们看起来是两件事,其实是同一个问题的两面:让某个文件「不被人随便碰到」。
而这两面经常被混着讲,一混就错。隐藏改的是「谁去看」,权限改的是「谁能动」。 前者全在显示层,后者在访问检查层——这个区别,我在复测的时候用一条命令就验证了。
一、隐藏:三种「看不见」,深度完全不同
1. 属性隐藏:只是一个视图开关
最朴素的做法,attrib +h +s 把文件打成「隐藏 + 系统」。
实测(PowerShell):
(Get-Item h.txt).Attributes = 'Hidden,System'
# 默认列举
Get-ChildItem → 只剩目录
# 带 -Force
Get-ChildItem -Force → 目录, h.txt
dir 也一样,dir /a 一敲就出来。资源管理器里要勾「显示隐藏文件」,系统属性还得关掉「隐藏受保护的操作系统文件」。
这一层的本质:文件还在原地,只有一个属性位被置上了,读数据和隐藏没有任何关系。 顺手记一个副作用:带 +s 的文件会被系统当成「受保护的操作系统文件」,某些清理工具会直接跳过它——这才是这条属性真正的实用价值(防误删),而不是「藏」。
2. CLSID 目录名:骗的是资源管理器的解释器
第二份笔记就是干这个的,表长这样:
我的电脑 {20D04FE0-3AEA-1069-A2D8-08002B30309D}
我的文档 {450D8FBA-AD25-11D0-98A8-0800361B1103}
拨号网络 {992CFFA0-F557-101A-88EC-00DD010CCC48}
控制面板 {21EC2020-3AEA-1069-A2DD-08002B30309D}
计划任务 {D6277990-4C6A-11CF-8D87-00AA0060F5BF}
打印机 {2227A280-3AEA-1069-A2DE-08002B30309D}
记事本 {1FBA04EE-3024-11D2-8F1F-0000F87ABD16}
网络邻居 {208D2C60-3AEA-1069-A2D7-08002B30309D}
回收站 {645FF040-5081-101B-9F08-00AA002F954E}
公文包 {85BBD920-42A0-1069-A2E4-08002B30309D}
字体 {BD84B380-8CA2-1069-AB1D-08000948F534}
Web 文件夹 {BDEADF00-C265-11d0-BCED-00A0C90AB50F}
用法:新建一个文件夹,把名字改成 随便什么.{CLSID}。
Explorer 看到 . 后面跟着一个 CLSID,就不把这东西当文件夹解释了,而是当成那个 Shell 命名空间对象去打开。 所以 回收站.{645FF040-...} 点进去会直接看到回收站,你在里面放的文件自然不会在「文件夹视图」里列出来。
这份表的年代感很强——拨号网络、公文包、Web 文件夹,全是 2000 年代的东西,现在能用的只剩一半左右。名字我照抄没改,它传了好几手,别把名字当文档,把 CLSID 当文档就行。但它揭示的机制没过时:目录名后缀被当成对象标识,是一种存在于 shell 层的多态。
关键验证:文件系统完全不认这个后缀
我拿第 3 节的 SUBST 把它挂成盘符,然后用文件系统 API 去列:
subst X: "D:\...\UDrives.{25336920-03F9-11CF-8FD0-00AA00686F13}"
Get-ChildItem X:\ -Force
结果:
Name Length
---- ------
secret.txt 16
十六个字节,一字不差地列出来了。 CLSID 后缀对 FindFirstFile 这一类调用毫无意义——它只影响 Explorer 决定「用什么图标、拿什么去渲染」。任何按路径直接访问的东西(cmd、脚本、Everything、WizTree、杀软、备份程序)都当它是个普通目录。
这就是第一层和第二层的本质区别:属性是「视图不显示」,CLSID 是「视图显示成别的东西」。两者都没有让文件变得不可访问。
3. SUBST 虚拟盘:把东西藏到一个「不存在的盘」里
那个 .bat 只有十行,逐行读完就明白它在干什么:
@ECHO OFF
MD E:\RECYCLED\UDrives.{25336920-03F9-11CF-8FD0-00AA00686F13}>NUL
IF EXIST O:\NUL GOTO DELETE
SUBST O: E:\RECYCLED\UDrives.{25336920-03F9-11CF-8FD0-00AA00686F13}
START O:\
GOTO END
:DELETE
SUBST /D O:
:END
MD建目录——建在E:\RECYCLED\下面,用「回收站」这个目录名做掩护,再挂一个UDrives.前缀装得像系统组件;.{}后缀是上表里没列的第 13 个 CLSID:{25336920-03F9-11CF-8FD0-00AA00686F13},微软自家的 MSHTML / “Browse in place”(IE 的就地浏览文档宿主)。我原本以为它是什么「驱动器枚举」对象,查了一圈才发现是 HTML 文档宿主——这意味着 Explorer 打开这个目录时,是把它当一篇文档去渲染,而不是当文件夹去列内容。IF EXIST O:\NUL判断 O 盘是否已经挂过,挂过就先SUBST /D卸掉,防止重复;SUBST O:把目录映射成盘符,START O:\打开。
SUBST 是个 DOS 时代留下来的老命令:它不是挂载,是给设备命名空间加一条别名。 文件物理上还在 E 盘,只是有了第二个入口叫 O:。
这招的三个代价
- 重启就掉。
SUBST映射只活在当前登录会话里,不写注册表(写注册表要额外做,这脚本没做)。第二天开机,O 盘没了,文件还在 E 盘那个目录里。 RECYCLED不是真回收站。 现代 Windows 的回收站在$Recycle.Bin,RECYCLED只是 FAT 时代留下的名字——在 NTFS 上,它就是个普通文件夹,起个唬人的名字而已。磁盘清理、重置系统、甚至某些「清理垃圾」的国产工具,都可能直接把它当垃圾处理掉。- 一条
dir /a就穿帮。 隐藏的是「盘符视图」,不是文件。dir /a E:\RECYCLED或者 Everything 全盘索引,一样一眼看穿。
二、权限:13 个权限项,各自真正挡的是什么
第一份笔记是最有价值的一份,因为它全是**「禁止某项之后,实际发生了什么」**,而不是抄文档。先把 13 项摆齐(缩写是 icacls 用的那套,括号里是 SDDL 里的位):
| 权限项 | 缩写 | 常量 | 真正管的事 |
|---|---|---|---|
| 遍历文件夹/执行文件 | X | FILE_TRAVERSE | 能不能穿过这个目录;可执行文件能不能跑 |
| 列出文件夹/读取数据 | RD | FILE_LIST_DIRECTORY | 能不能列目录内容 / 读文件内容 |
| 读取属性 | RA | FILE_READ_ATTRIBUTES | 能不能读「只读/隐藏/时间戳」这些元数据 |
| 读取扩展属性 | REA | FILE_READ_EA | 能不能读 EA(NTFS 的扩展属性流) |
| 创建文件/写入数据 | WD | FILE_WRITE_DATA | 能不能写文件内容 / 在目录里建文件 |
| 创建文件夹/追加数据 | AD | FILE_APPEND_DATA | 能不能追加 / 建子目录 |
| 写入属性 | WA | FILE_WRITE_ATTRIBUTES | 能不能改属性位和时间戳 |
| 写入扩展属性 | WEA | FILE_WRITE_EA | 能不能写 EA |
| 删除子文件夹及文件 | DC | FILE_DELETE_CHILD | 能不能删掉里面的东西 |
| 删除 | D | DELETE | 能不能删掉这个对象 |
| 读取权限 | RC | READ_CONTROL | 能不能读它的 ACL |
| 更改权限 | WDAC | WRITE_DAC | 能不能改它的 ACL |
| 取得所有权 | WO | WRITE_OWNER | 能不能改所有者 |
笔记里那几条结论,逐条对下来看:
允许读取数据、读取扩展属性、读取权限 这三个,在 admin 条件下,所有者不是自己,可以删除与查看。
禁止「读取权限」可令其无法删除(只允许「读取权限」在 admin 条件下可删除文件)。
禁止「读取权限」在所有者不是自己的条件下无法查看与删除。因此,【读取权限】非常重要。
注意这句括号里的前提:「所有者不是自己」。这不是废话,是整段实验里最关键的一个条件——因为所有者永远隐式拥有 READ_CONTROL 和 WRITE_DAC,你没法用 ACL 把自己的权限读/改权限封死。所以「禁止读取权限」这个实验,只有在拿别人的文件做的时候才成立。
三、复测:四条结论,三条要改
我在 Win Server 2016 上把这几种组合过了一遍,条件是非提权、所有者是自己、文件在能随手删的临时目录里。ACE 用 .NET 的 ACL 接口下,不用 icacls——原因在结论四,这一步选错整份结论就会歪。
| 拒绝的权限 | 读内容 | 读 ACL | 写内容 | 删除(.NET API) |
|---|---|---|---|---|
RC 读取权限 | ✅ | ✅(所有者隐式) | ✅ | ✅ 能删 |
D 删除(精确 0x10000) | ✅ | ✅ | ✅ | ✅ 仍能删(见结论二) |
D + 父目录 DC | ✅ | ✅ | ✅ | ❌ Access denied |
WD 写入数据 | ✅ | ✅ | ❌ | ✅ |
WA 写入属性 | ✅ | ✅ | ❌ | ✅ |
结论一:「禁止读取权限 → 无法删除」是资源管理器的行为
笔记里那条在 Explorer 里确实成立,但在文件系统 API 层不成立:我给文件加了一条 DENY (RC),[System.IO.File]::Delete() 照样删掉了。
原因不难猜:Explorer 删除前要先读 ACL,读不到就没法判断「这人到底能不能删」,于是直接拒绝并弹一个你没权限的提示。而 DeleteFile 根本不做这个预检——它只看删除相关的位。
同一件事,两条路径,两套判定。 这条值得单独记:凡是「权限拒绝」类的实验,说清是在资源管理器里做的还是在命令行/API 里做的,否则结论没法复现。
结论二:只拒文件上的 DELETE,删不掉是假象
我第一次测的时候,给文件加 DENY (D),删除确实失败了,于是写下「这才是锁死的正确姿势」。这个结论是错的,只是失败原因刚好长得像。
用 .NET 精确拒掉 Delete(SDDL 里是 SD = 0x10000)之后,[IO.File]::Delete() 照样成功。因为删除有两条路:
- 文件自己的
DELETE; - 或者父目录上的
FILE_DELETE_CHILD(DC)——父目录是你的,这条路默认就通着。
实测对比:
只拒文件 DELETE -> 删除成功(父目录给了 FILE_DELETE_CHILD,这条路还在)
文件 DELETE + 父目录 DC -> 删除失败(两条路都堵上才真拦得住)
笔记最后那句——
配合【禁止删除】达到管理员才可删除的效果(二次确认)。如果加上文件夹【禁止删除子文件】则就连管理员也必须进入到权限管理修改权限后才可删除。
——讲的正是这件事:「父目录禁止删除子文件」不是锦上添花的补充,是必要条件。 两条路都堵上之后,想删只剩一条道:拿到 WRITE_DAC 去改 ACL。而改 ACL 必然发生在管理员上下文里、必然要你确认一次——所谓「二次确认」就是这么来的,不是设计,是绕不过去。
结论三:「禁止写入属性」连正常保存都会挂
这条笔记写的是「禁止【写入属性】或【写入扩展属性】任一均可导致写入后不可修改,但是可以删除」,我实测得到的现象比预想的更彻底:
DENY (WA) 之后,[System.IO.File]::WriteAllText() 直接抛 Access is denied——不是「能写但改不了」,是根本写不进去。
原因是 .NET 写文件时顺手要做几件事:更新「最后修改时间」、清/置存档位。这些都走 FILE_WRITE_ATTRIBUTES,WA 一拒,整个写入流程就断在第一步。而删除走的是另一套判定(DELETE 或父目录的 DC),和 WA 无关,所以「写不了、但删得掉」——和笔记一致。
实用推论:想让人「能打开看、但改不了」,最简组合是 WA 就够了,不用把整个「写入」权限取下;要连删除一起挡,还得按结论二堵两条路。
结论四:icacls 的 (D) 不是「只拒删除」,它会把文件变成砖
这条解释了我第一次为什么会得出错的结论。
我第一次是用 icacls file /deny "Timmoc:(D)" 下的 ACE,然后删除失败,就以为「DELETE 拒掉 = 删不掉」。后来想看这条 ACE 长什么样,才发现它落盘的 SDDL 是:
(D;;0x110000;;;S-1-5-21-...-1001)
0x110000 = DELETE (0x10000) | SYNCHRONIZE (0x100000)。
你在命令行只写了一个 (D),icacls 把「同步」这一位一起拒了。 而 GENERIC_READ / GENERIC_WRITE 都包含 SYNCHRONIZE——于是这个文件不是「删不掉」,而是读不了、写不了、删不了,连句柄都开不出来:
| 操作 | 结果 |
|---|---|
Get-Content | 拒绝访问 |
WriteAllText | 拒绝访问 |
[IO.File]::Delete | 拒绝访问 |
icacls /remove:d 撤回 ACE | 拒绝访问 |
Set-Acl 重写 DACL | unauthorized |
最后两行才是真正麻烦的地方:ACE 撤不回来。 想清掉只能提权(takeown + 管理员上下文)。我在两个临时目录里各留了一个这种文件当纪念品。
教训有两条:一是精确拒某个权限要用 .NET 的 FileSystemRights 逐项下(Delete 在 SDDL 里是 SD,就 0x10000,干净);二是这类实验必须放在能随手删的目录里做,并且先测「能不能撤回来」,再测「锁上之后效果如何」。
四、两套机制的边界在哪里
把两件事摆在一起,结论其实很朴素:
| 隐藏 | 权限 | |
|---|---|---|
| 所在层 | 显示层(属性位 / shell 命名空间 / 盘符别名) | 访问检查层(ACL) |
| 骗过谁 | 资源管理器、看图的人 | 老实调 API 的程序 |
| 拦不住谁 | cmd、dir /a、Everything、杀软、备份 | 管理员、取得所有权、备份特权 |
| 主要用途 | 降低可见度、防误删 | 防误删/防误改、多人共用机器 |
三条硬边界,记住了能省很多误会:
- 隐藏不是加密。 文件内容一个字节都没变,任何能按路径访问的东西都能读。
- 权限不是加密。 拿到
WRITE_DAC的人可以改 ACL;能takeown的人可以改所有者;备份程序用SeBackupPrivilege可以直接绕过 ACL 读文件。ACL 挡的是「正常路径」,不是「所有路径」。 - FAT32 / exFAT 没有 ACL。 这两套东西一放到 U 盘上就只剩「隐藏」那一半还能用——这也是为什么 SUBST + RECYCLED 那招在年代久远的资料里特别常见:那会儿大家还在用 FAT。
真正要保密,只有加密(BitLocker、EFS、7z 加密码)。隐藏和权限都只是「提高门槛」,门槛的价值在于防误操作和防随手一翻,不在于防有心人。
五、如果重做一次这个实验
我现在会这么设计,比当年那份笔记省事得多:
- 专用目录 + 一个肯定删得掉的父目录,别在有用的文件夹里试;
- 每次改动前先
icacls file > before.txt,改完再导出一次,SDDL 一对照就知道到底写了什么位——这次要不是看了 SDDL,我不会发现(D)还带了个SYNCHRONIZE; - 资源管理器和命令行各测一遍,结论里写明是哪条路径,别混着说;
- 写入测试别只用
WriteAllText——它要动属性,会把你对WD和WA的结论搅在一起;要单独测「写数据」就用FileStream追加; - 先测「能不能撤回来」,再测「锁上之后效果如何」。顺序反了,就会像我一样留个删不掉的文件。
收尾
- 隐藏改的是「谁去看」,权限改的是「谁能动」。 前者全在显示层,一条
dir /a或一个subst就穿透;后者才在文件系统里做访问检查。 - 「禁止读取权限就不能删」只对资源管理器成立。 而且只拒文件上的
DELETE也没用——父目录的FILE_DELETE_CHILD是第二条路,两条一起堵才叫锁死。 icacls的(D)是0x110000,带SYNCHRONIZE。 它会把文件变成读不了、写不了、删不了、ACE 也撤不回来的砖。要精确拒删除,用 .NET 的FileSystemRights.Delete。
三份素材和这次的复测脚本都扔进小工具仓了:
https://cnb.cool/t1mmoc/tspace/pwngore/toolbox → hide-and-permission/
| 文件 | 是什么 |
|---|---|
clsid-table.md | 12 个 CLSID 对照表 + 「这只是显示层」的验证 |
hide-o-drive.bat | 那个 SUBST 藏盘脚本,原样 |
acl_probe.ps1 | 一键复现上面那张表(ACE 用 .NET 下,跑完自己收拾) |
ntfs-permission-notes.md | 原记录 + 四条修正 |
隐藏文件那三招,说到底只是「让人一眼看不见」;权限那 13 项,说到底只是「让程序老实报告没权限」。两样都不是安全边界,但两样都值得会——因为绝大多数数据丢失,不是被人拿走的,是被自己随手删掉的。