ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

透明加密白名单机制解析:进程映像名与扩展名判定路径

透明加密白名单机制解析:进程映像名与扩展名判定路径

简介:面向需要处理易赛通数据泄露防护客户端加密文件的运维与安全技术人员,这份docx文档围绕加密文件难以还原、跨机打开异常等问题,梳理了一套解密分析思路。内容从客户端主进程、辅助进程与Filelock.sys驱动的职责入手,说明驱动层Hook关键API后按进程名和扩展名列表提供透明加解密服务的机制,并归纳进程不在列表、扩展名不受控等场景下的判定差异,为编写解密工具提供切入点。资源包共1个docx文件,大小约29KB,篇幅精简,适合作为排障时的原理参考和方案草图。已有2082人学习,读者可从中理解易赛通的加解密触发条件、白名单关系与工具设计前的分析框架,便于结合自身环境进一步验证。

1. 透明加密的假象:为什么应用层永远读不到“密文”

装了数据泄露防护客户端之后,最反直觉的一件事是:你用任何工具去读文件,读到的都是明文。记事本能打开,编辑器能搜索,压缩软件能压进去——加密对你完全不可见。很多人第一反应是写个“解密工具”去还原算法,方向从一开始就错了:你不是要破解什么密码,你是要让那层透明加密的机制自愿替你解一次密。

真要把“加密、解密”这件事讲透,得先承认一个事实:透明加密的边界不在应用层,而在文件系统驱动的 IRP 分发点。驱动在 IRP_MJ_CREATE 那一刻就已经决定好了这个文件句柄往后是“给明文”还是“给原始字节”,应用层后面做什么都改变不了这个判定。所以真正值得研究的问题是:这个判定依赖哪些输入,哪些输入是应用层能够影响的。答案就藏在两个名字里——进程映像名和文件扩展名。

2. 策略判定面:驱动在 IRP_MJ_CREATE 里怎么决定给不给你解密

2.1 用户态三件套,和内核里那个真正干活的组件

这类数据泄露防护客户端在用户态通常摆着两到三个进程。一个是主控进程,负责登录、策略拉取、审计上报和托盘界面;一个是守护进程,盯着主控进程,被杀就拉起来,顺带看着自己,形成互保。两者的可执行文件都放在同一个安装目录下,杀掉主控再新建一个受控扩展名的文件,落盘依然是密文——这个实验很能说明问题:策略和密钥根本不在用户态。

真正持有加解密能力的是%SystemRoot%\System32\drivers下的那个文件系统过滤驱动。它注册成 minifilter,在注册表HKLM\SYSTEM\CurrentControlSet\Services\<驱动名>\Instances里能看到它挂载的 Altitude,这个数值决定它在文件系统栈里的位置。反过来验证也成立:把驱动停掉重启,受控文件立刻变回一堆打不开的字节——因为此时你读到的就是磁盘上的原始密文。所谓“透明”,本质是驱动在 IRP 层替所有应用做了一次视图切换。

2.2 判定输入只有两项:EPROCESS 里的 15 字节映像名与文件后缀

把这类产品的行为按输入穷举一遍,会发现判定表其实很小:

当前进程映像名目标文件扩展名读取行为写入行为
在受控进程名单内在该进程对应的扩展名表内返回明文落盘前加密
在受控进程名单内不在扩展名表内返回原始字节原样落盘
不在受控进程名单内任意返回原始字节,或直接拒绝原样落盘,或直接拒绝

这张表解释了两件容易被忽略的事。第一,策略是“进程 × 扩展名”的笛卡尔积,不是全局开关,所以同一个进程读.docx会被解密、读.abc就不会。第二,判断进程身份用的不是完整路径,而是EPROCESS里的映像名——内核里那段ImageFileName[15],ANSI 编码,最多 15 个字节,比较时通常还不区分大小写。15 字节这个上限是硬约束,名字超长的进程会被截断,判定自然就对不上了。

扩展名那一侧同样有坑。驱动拿文件名一般走FltGetFileNameInformation或直接读FILE_OBJECT->FileName,拿到的是规范化路径。要注意备用数据流(report.docx:hidden这种)、末尾带点的怪名字、以及 8.3 短名(REPORT~1.DOC),不同实现在这几处的处理并不一致,这也是后面排查问题的常见入口。

2.3 判定发生在 CREATE:一次决策,整个 FILE_OBJECT 生命周期生效

常见的实现思路是在 pre-create 回调里把结论算出来,然后挂到 stream handle context 或 FILE_OBJECT 上下文上,后续 read/write/close 直接复用,不再重复判定。写成伪代码大致是这样:

// 驱动侧 pre-create 判定(示意逻辑,非任何厂商源码) FLT_PREOP_CALLBACK_STATUS PreCreate(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID *CompletionContext) { PFLT_FILE_NAME_INFORMATION nameInfo = NULL; FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo); FltParseFileNameInformation(nameInfo); // 1) 拿当前发起 IO 的进程映像名,只有 15 字节 ANSI,且大小写不敏感 PCHAR image = PsGetProcessImageFileName(IoGetCurrentProcess()); // 2) 查进程白名单;命中后再看扩展名是否在该进程的策略表内 POLICY_ENTRY *p = LookupProcess(image); BOOL needCrypto = p && ExtensionMatch(p, nameInfo->Extension); // 3) 结论挂到上下文,后续读写不再重算,保证同一句柄行为一致 SetStreamContext(Data, needCrypto); FltReleaseFileNameInformation(nameInfo); return FLT_PREOP_SUCCESS_WITH_CALLBACK; }

代码里PsGetProcessImageFileName返回的是PCHAR,不是 UNICODE 字符串,长度天然被ImageFileName[15]卡死;ExtensionMatch做的是查表而不是字符串后缀比较,所以中间加点、加空格这类花招不一定绕得过去。这两点决定了后面“借刀”能不能成功——能被影响的只有输入,不是逻辑。

2.4 为什么“读的时候再解密”这条路走不通

有人会想:既然驱动要给我明文,那我在 read 路径上做手脚不就行了。这条路在 Windows 上会被缓存管理器挡住。文件数据一旦进入 Cache Manager 的共享缓存页,后续所有ReadFile、内存映射、CreateFileMapping看到的都是同一份物理页,驱动如果只在某次 read 上返回不同内容,缓存里就会同时存在明文页和密文页,谁先谁后完全看调度。这正是策略判定必须放在 CREATE 阶段、且一次生效的根本原因:它把“这个句柄看到的世界”在打开瞬间就定死了。

理解了这一点,解密思路的方向就明确了:不要去改数据通路,去改 CREATE 时驱动看到的那两个输入。

3. 借刀杀人:把进程映像名做成通行证的构造过程

3.1 换思路:不碰算法,只碰身份

既然判定依赖“进程映像名”,而映像名来自加载时写入EPROCESS的那段字符串,那就不需要逆向任何加密算法,只要让发起 IO 的进程叫一个受信任的名字。这不是伪造令牌、也不涉及提权,本质是复用驱动自己开放的那条合法通道——驱动认为“这个名字的进程是可信的”,于是主动把明文交出来。整个过程里,我们没有解密任何东西,是驱动替我们解的。

思路落地要解决三个具体问题:怎么让映像名变成目标名字;拿到的明文往哪里写才不会被重新加密;最后怎么把扩展名改回去而不触发写路径。

3.2 让映像名命中白名单:复制自身与 CreateProcessW

映像名来自 PE 被加载时的文件名,所以你不需要改 PE 内部任何资源,把可执行文件复制一份改名就行。注意CreateProcessW的lpApplicationName要显式给出绝对路径,否则会走搜索顺序,可能启动到同名但不同目录的程序:

// launcher.c —— 把自己复制成白名单里的映像名,再以该身份启动真正的干活程序 #include <windows.h> static BOOL SpawnAs(const wchar_t *fakeName, const wchar_t *workDir) { wchar_t self[MAX_PATH], target[MAX_PATH]; GetModuleFileNameW(NULL, self, MAX_PATH); // 目标路径:放在工作目录下,注意目录本身不要是被监控的受控目录 swprintf_s(target, MAX_PATH, L"%ls\\%ls", workDir, fakeName); // 覆盖复制即可,映像名只取文件名部分,与内容无关 if (!CopyFileW(self, target, FALSE)) return FALSE; STARTUPINFOW si = { sizeof(si) }; PROCESS_INFORMATION pi = { 0 }; // lpApplicationName 显式传路径,避免搜索路径歧义 BOOL ok = CreateProcessW(target, NULL, NULL, NULL, FALSE, 0, NULL, workDir, &si, &pi); if (ok) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } return ok; }

fakeName要满足两个条件:长度不超过 15 字节(否则被截断,判定必失败),且完全命中驱动白名单的写法。workDir的选择也有讲究——如果工作目录本身在受控范围内,复制出来的可执行文件虽然扩展名是.exe通常不受影响,但目录上的审计和告警可能会亮。复制完成后父进程可以直接退出,子进程的映像名已经是目标值。

3.3 读明文、写“壳外”扩展名:为什么必须是 .tmp 而不是 .docx

第二步最轻松:以白名单身份CreateFileW打开密文文件,ReadFile拿到的就是明文,因为驱动在 CREATE 阶段已经决定给这个句柄解密视图。真正的陷阱在第三步——写出去的时候。此刻进程仍在白名单内,如果你老老实实写回.docx,驱动在 write 路径上会照章办事,把明文重新加密一遍,等于白干。

所以输出路径必须落在一个“不在该进程扩展名表内”的后缀上,比如.temp、.part、.bin这类没人会配进策略的扩展名。这里有个容易被忽视的细节:不能先写成.docx再改名,因为写的那一刻就已经加密了;顺序必须是“先落到壳外后缀,再由别的进程改名”。

// reader.c —— 以白名单身份读明文,落到不被加密的后缀上 HANDLE hIn = CreateFileW(srcPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hIn == INVALID_HANDLE_VALUE) return; HANDLE hOut = CreateFileW(tmpPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hOut == INVALID_HANDLE_VALUE) { CloseHandle(hIn); return; } BYTE buf[64 * 1024]; DWORD got = 0, wrote = 0; while (ReadFile(hIn, buf, sizeof(buf), &got, NULL) && got > 0) { // 流式搬运,逐块写出,避免一次性把大文件读进内存 WriteFile(hOut, buf, got, &wrote, NULL); } CloseHandle(hOut); CloseHandle(hIn);

共享模式开了FILE_SHARE_DELETE,是为了后续改名时源句柄不挡路。缓冲区用 64KB 而不是整文件读入,除了省内存,还避开了一个坑:超大文件一次性ReadFile会更容易触发驱动的分块解密路径,某些实现在这个路径上的尾部处理不干净,表现为末尾多出几百字节填充。

3.4 最后一步改名必须由名单外的进程做

明文已经落在.temp上了,剩下把名字改成.docx。这一步必须换人做,理由在于改名走的是IRP_MJ_SET_INFORMATION(FileRenameInformation),而不是数据写入路径,但这个 IRP 恰恰是很多过滤驱动会挂 pre-op 的地方——用来更新文件头里的元数据、刷新审计记录,甚至按新扩展名补一次加密。如果改名者本身在白名单内、目标扩展名又在它的表里,某些实现会在 rename 或紧随其后的 close 上把内容重新加密。

用不在名单内的进程改名就绕开了这个分支:驱动判定“这个进程不受管”,rename 只当作普通元数据操作处理,内容一个字节都不碰。命令行解释器、资源管理器、或者你自己写的一个不叫白名单名字的小工具都行,关键是它的映像名不在列表里。

// renamer.c —— 由不受管进程执行,纯元数据操作,不触发内容写路径 #include <windows.h> BOOL RenameToFinal(const wchar_t *tmpPath, const wchar_t *finalPath) { // MOVEFILE_REPLACE_EXISTING 允许覆盖同名目标;同卷内是原子改名 return MoveFileExW(tmpPath, finalPath, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED); }

MOVEFILE_COPY_ALLOWED是为跨卷场景准备的备选:跨卷时MoveFileEx会退化成“复制 + 删除”,而复制就是实打实的数据写入,会重新走 write 路径。所以批量处理时务必保证临时目录与目标目录在同一卷上,否则这一步会把前面的努力全部作废,落盘又变成密文。

3.5 完整时序:两个不可省的中间态

把四步串起来看,中间存在两个必须存在的状态:一是“以白名单身份持有已解密的字节流”,二是“明文已经落盘但文件名还在壳外”。少任何一个都不行——少第一个拿不到明文,少第二个改名即触发再加密。这套流程之所以成立,完全建立在目标机制的两个设计选择上:用映像名做身份,用扩展名做内容分类。这两个选择带来的效率收益很大,但它们同时也是可以被外部影响的输入。

4. 落地实现:文件头、原始长度与“改名不触发写路径”的细节

4.1 长度对不上:逻辑长度与磁盘占用长度

真正动手写工具时,第一个撞上的问题往往是长度。GetFileSize报出来的值和磁盘实际占用对不上——这不是 bug,而是加密实现的两种流派造成的。一种是把原始明文长度写在文件头或文件尾的明文元数据区里,驱动读出来之后改写FILE_STANDARD_INFORMATION里的EndOfFile,让上层看到逻辑长度;另一种是长度根本不进文件,由驱动侧维护的映射表或者文件系统扩展属性记录,上层查询时直接替换返回值。

判断自己面对的是哪一种,有个很实用的办法:对比文件大小与磁盘占用。如果逻辑大小明显小于实际簇占用,说明密文比明文长(有头部、有块对齐填充),属于第一种或第二种都可能;如果两者基本相等,那大概率是等长变换(流密码或 CTR 模式),此时“截断”这件事根本不需要你做,驱动已经处理好了。搞清楚这一点,能省掉大量在尾部填充上瞎猜的时间。

4.2 内存映射的陷阱:缓存页里到底是谁的视图

用CreateFileMapping+MapViewOfFile读文件看起来更高效,但在这类场景里它经常翻车。原因还是缓存一致性:内存映射建立的是 Section 对象,页错误发生时由内存管理器直接向文件系统发分页读,这条路径上驱动拿到的是“以当前进程身份发起的读”,判定结果取决于发起缺页的那个线程属于哪个进程,而不是你最初CreateFile的那个进程。

一旦解密工具是多线程的、或者把映射视图交给了别的进程(句柄继承、跨进程共享),就可能出现“前半段明文、后半段密文”的诡异现象。血泪经验是:解密工具里老老实实用ReadFile顺序读,别碰映射视图,也别指望FILE_FLAG_NO_BUFFERING能帮你绕过什么——对齐要求会把你折磨到怀疑人生。

4.3 重命名的三条实现路径与驱动回调的差异

改名这件事在 Windows 上有三条路,行为并不完全一样。MoveFileExW是最省事的封装,同卷内最终落到FileRenameInformation;SetFileInformationByHandle配FILE_RENAME_INFO更底层一点,可以指定RootDirectory句柄做相对路径,长路径和特殊字符场景更稳;第三条是直接发NtSetInformationFile,除了排查问题基本没必要。

真正要关心的是驱动在这条 IRP 上挂没挂回调。挂了回调的实现在 rename 时可能会做三件事:更新文件头里记录的原始文件名(有些产品会把原文件名写进密文头部做溯源)、按新扩展名重新判定策略、写一条审计日志。前两件事都可能把明文重新变成密文,所以“由谁改名”比“用什么 API 改名”重要得多。写完工具后建议做个对照实验:同一个.temp文件,分别用白名单进程和非白名单进程改名,然后看落盘字节有没有变化,一眼就能确认驱动到底挂没挂这个回调。

4.4 批量处理的句柄共享与长路径

批量跑的时候,最容易出问题的是句柄冲突。加密文件常常被办公软件以独占方式打开,这时候CreateFileW会返回ERROR_SHARING_VIOLATION。解决办法不是死循环重试,而是先探测:以FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE全共享模式试开,失败就说明有别的进程持了独占锁,把它记录下来跳过,最后统一输出失败清单——批量任务里最忌讳的就是卡在一个文件上不动。

长路径同样要提前处理。超过 260 字符的路径在没开长路径支持的进程里会被截断,而这类企业环境里目录嵌套往往很深。稳妥做法是在路径前加\\?\前缀,同时全程使用宽字符 API,避免 ANSI 转换在中文字符上出岔子。这些跟加密机制本身无关,但它们决定了这套思路能不能从 demo 走到能跑完一整个目录。

5. 排查清单:五类翻车现场的现象、成因与定位手法

5.1 现象:读出来还是乱码,一个字节都没变

原因通常不在流程,而在身份没伪造干净。最常见的三种情况:复制出来的可执行文件名超过 15 字节被内核截断;CreateProcessW没传lpApplicationName,结果启动到了同名但别的目录的程序;或者驱动除了看映像名还校验了映像路径甚至数字签名。定位方法是拿NtQueryInformationProcess或任务管理器确认Image字段到底显示什么,再拿一个确定在白名单里的正版程序读同一个文件做对照。解决方式是缩短名字、显式传绝对路径,若确认存在签名校验,那这条路径就走不通了,得换思路。

5.2 现象:.xlsx、.docx还原后打不开,提示文件损坏

这类格式本质是 ZIP 容器,中央目录在文件尾部,任何长度错位都会导致整包解析失败。成因一般是尾部填充没处理——驱动给的长度是逻辑长度,但有些实现读出来的字节数比逻辑长度多,末尾带着对齐填充。解决办法是先按逻辑长度截断再落盘:用GetFileSizeEx拿驱动返回的长度,写完后SetEndOfFile精确截断一次。另一个成因是读取时用了FILE_FLAG_NO_BUFFERING导致非对齐读取,块边界上丢字节,改成普通缓冲读即可。验证手段很直接:拿还原后的文件跑一遍 ZIP 结构解析,看中央目录能不能定位到。

5.3 现象:改名那一瞬间,文件又变回密文

典型的原因是改名者本身在白名单里,且目标扩展名命中它的策略表,驱动在SET_INFORMATION的 pre-op 或随后的 cleanup 上补了一次加密。还有一种更隐蔽的情况:临时目录和目标是跨卷的,MoveFileExW悄悄退化成复制加删除,复制就是写数据,必然触发加密。定位方法是改名前后各算一次文件哈希,哈希变了就说明内容被动过。解决办法是换一个不在名单内的进程执行改名,并确保临时目录与目标目录同卷。

5.4 现象:伪装进程刚起来就被结束,或者行为怪异地被限制

守护进程往往不只看主控进程,也会顺手扫一遍系统里跑着的“眼熟名字”。一个映像名叫对了但路径、签名、父进程都不对的进程,很容易被识别成异常并终止。另外某些产品对白名单进程会额外注入模块或调整权限,伪装进程被注入之后行为可能和预期完全不同。定位方法是看进程存活时长和模块列表,如果加载了一堆不认识的东西,基本就是这个原因。解决办法是别追求长驻,读完就退,缩短暴露窗口;或者干脆用系统自带的、签名合法的进程去做读取动作。

5.5 现象:大文件还原后尾部多出几百字节,校验对不上

成因和 5.2 类似但场景不同:大文件在驱动里通常走分块解密,最后一块如果不满一个加密块,填充规则各家实现不一,有的按块对齐补零,有的在头部记录真实长度。小文件往往碰不到这个分支,所以表现为“小文件都行,大文件全废”。定位方法是拿一个恰好跨块边界的测试文件,对比GetFileSizeEx返回值和实际写出字节数,差多少就一目了然。解决办法是以驱动返回的逻辑长度为准,写完后强制截断,不要相信“读到 EOF 就是结束”。

6. 设计取舍:白名单式透明加密的结构性弱点

回过头看,这类产品在机制层面并没有做错什么。把判定放在文件系统过滤驱动里,是唯一能同时满足“应用无感”“性能可接受”“部署成本低”三个条件的方案:不用改任何业务软件,加解密开销被限制在 IO 路径上,管理员只需要维护策略表。真正的问题出在判定的输入选得太“便宜”——进程映像名是内核里一段 15 字节的 ANSI 字符串,扩展名是路径的后缀,这两样东西都完全由被管端自己决定。

设计选择带来的收益结构性代价
以进程映像名做身份判定在 O(1) 完成,无需每次校验签名或路径名字可以复制,身份可被冒用
以扩展名做内容分类策略表直观,管理员易维护换个后缀即可绕过,落地环节必须再改名
判定放在 CREATE 一次生效与缓存管理器兼容,行为稳定一旦判定错误,整个句柄生命周期都错
头部/元数据记录原始长度上层看到的是明文长度,兼容性好分块填充边界成为实现差异的来源

这套取舍在企业环境里其实算划算:它防的是“员工随手把文件拷到 U 盘”这类无意识泄露,而不是防一个有逆向能力、愿意花时间研究驱动行为的对手。真要提级防御,就得引入签名校验、路径白名单、内核态可信度量,代价是兼容性直线下降,随便一个自研小工具都跑不起来。安全强度和可用性在这里是硬对立的,厂商选了可用性,这个选择本身没有对错。

想验证自己环境里到底是哪种实现,我一般会走三步:先停掉用户态进程看加密是否仍然生效,确认边界在内核;再拿两个不同映像名的进程读同一个文件对比字节,确认判定输入是什么;最后用同卷和跨卷各改一次名,看内容哈希有没有变,确认 rename 路径上挂没挂回调。三步做完,机制轮廓基本就清楚了,后面写工具、排查问题都不会瞎猜。

那次在批量还原一批表格时,我图省事把临时目录设在了另一块盘上,结果几千个文件全部“还原成功”却又全部打不开——MoveFileExW走了复制路径,明文在复制过程中被重新加密了一遍。从那以后我每次跑批之前都强制走一遍“源目录、临时目录、目标目录是否同卷”的检查,宁可多写十行判断,也不愿再对着一堆哈希不匹配的文件发呆。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表