三份旧笔记

素材是三份放在一起的旧东西:

  • 一份 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
  1. MD 建目录——建在 E:\RECYCLED\ 下面,用「回收站」这个目录名做掩护,再挂一个 UDrives. 前缀装得像系统组件;
  2. .{} 后缀是上表里没列的第 13 个 CLSID:{25336920-03F9-11CF-8FD0-00AA00686F13},微软自家的 MSHTML / “Browse in place”(IE 的就地浏览文档宿主)。我原本以为它是什么「驱动器枚举」对象,查了一圈才发现是 HTML 文档宿主——这意味着 Explorer 打开这个目录时,是把它当一篇文档去渲染,而不是当文件夹去列内容。
  3. IF EXIST O:\NUL 判断 O 盘是否已经挂过,挂过就先 SUBST /D 卸掉,防止重复;
  4. 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 里的位):

权限项缩写常量真正管的事
遍历文件夹/执行文件XFILE_TRAVERSE能不能穿过这个目录;可执行文件能不能跑
列出文件夹/读取数据RDFILE_LIST_DIRECTORY能不能列目录内容 / 读文件内容
读取属性RAFILE_READ_ATTRIBUTES能不能读「只读/隐藏/时间戳」这些元数据
读取扩展属性REAFILE_READ_EA能不能读 EA(NTFS 的扩展属性流)
创建文件/写入数据WDFILE_WRITE_DATA能不能写文件内容 / 在目录里建文件
创建文件夹/追加数据ADFILE_APPEND_DATA能不能追加 / 建子目录
写入属性WAFILE_WRITE_ATTRIBUTES能不能改属性位和时间戳
写入扩展属性WEAFILE_WRITE_EA能不能写 EA
删除子文件夹及文件DCFILE_DELETE_CHILD能不能删掉里面的东西
删除DDELETE能不能删掉这个对象
读取权限RCREAD_CONTROL能不能读它的 ACL
更改权限WDACWRITE_DAC能不能改它的 ACL
取得所有权WOWRITE_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 重写 DACLunauthorized

最后两行才是真正麻烦的地方:ACE 撤不回来。 想清掉只能提权(takeown + 管理员上下文)。我在两个临时目录里各留了一个这种文件当纪念品。

教训有两条:一是精确拒某个权限要用 .NET 的 FileSystemRights 逐项下(Delete 在 SDDL 里是 SD,就 0x10000,干净);二是这类实验必须放在能随手删的目录里做,并且先测「能不能撤回来」,再测「锁上之后效果如何」。


四、两套机制的边界在哪里

把两件事摆在一起,结论其实很朴素:

隐藏权限
所在层显示层(属性位 / shell 命名空间 / 盘符别名)访问检查层(ACL)
骗过谁资源管理器、看图的人老实调 API 的程序
拦不住谁cmd、dir /a、Everything、杀软、备份管理员、取得所有权、备份特权
主要用途降低可见度、防误删防误删/防误改、多人共用机器

三条硬边界,记住了能省很多误会:

  1. 隐藏不是加密。 文件内容一个字节都没变,任何能按路径访问的东西都能读。
  2. 权限不是加密。 拿到 WRITE_DAC 的人可以改 ACL;能 takeown 的人可以改所有者;备份程序用 SeBackupPrivilege 可以直接绕过 ACL 读文件。ACL 挡的是「正常路径」,不是「所有路径」。
  3. FAT32 / exFAT 没有 ACL。 这两套东西一放到 U 盘上就只剩「隐藏」那一半还能用——这也是为什么 SUBST + RECYCLED 那招在年代久远的资料里特别常见:那会儿大家还在用 FAT。

真正要保密,只有加密(BitLocker、EFS、7z 加密码)。隐藏和权限都只是「提高门槛」,门槛的价值在于防误操作和防随手一翻,不在于防有心人。


五、如果重做一次这个实验

我现在会这么设计,比当年那份笔记省事得多:

  1. 专用目录 + 一个肯定删得掉的父目录,别在有用的文件夹里试;
  2. 每次改动前先 icacls file > before.txt,改完再导出一次,SDDL 一对照就知道到底写了什么位——这次要不是看了 SDDL,我不会发现 (D) 还带了个 SYNCHRONIZE;
  3. 资源管理器和命令行各测一遍,结论里写明是哪条路径,别混着说;
  4. 写入测试别只用 WriteAllText——它要动属性,会把你对 WD 和 WA 的结论搅在一起;要单独测「写数据」就用 FileStream 追加;
  5. 先测「能不能撤回来」,再测「锁上之后效果如何」。顺序反了,就会像我一样留个删不掉的文件。

收尾

  1. 隐藏改的是「谁去看」,权限改的是「谁能动」。 前者全在显示层,一条 dir /a 或一个 subst 就穿透;后者才在文件系统里做访问检查。
  2. 「禁止读取权限就不能删」只对资源管理器成立。 而且只拒文件上的 DELETE 也没用——父目录的 FILE_DELETE_CHILD 是第二条路,两条一起堵才叫锁死。
  3. icacls 的 (D) 是 0x110000,带 SYNCHRONIZE。 它会把文件变成读不了、写不了、删不了、ACE 也撤不回来的砖。要精确拒删除,用 .NET 的 FileSystemRights.Delete。

三份素材和这次的复测脚本都扔进小工具仓了:

https://cnb.cool/t1mmoc/tspace/pwngore/toolbox → hide-and-permission/

文件是什么
clsid-table.md12 个 CLSID 对照表 + 「这只是显示层」的验证
hide-o-drive.bat那个 SUBST 藏盘脚本,原样
acl_probe.ps1一键复现上面那张表(ACE 用 .NET 下,跑完自己收拾)
ntfs-permission-notes.md原记录 + 四条修正

隐藏文件那三招,说到底只是「让人一眼看不见」;权限那 13 项,说到底只是「让程序老实报告没权限」。两样都不是安全边界,但两样都值得会——因为绝大多数数据丢失,不是被人拿走的,是被自己随手删掉的。