简介:《针对被易赛通数据泄露防护客户端加密的文件解密思路》面向需要处理易赛通加密文件的安全研究人员、运维与开发人员,聚焦文件被透明加密后难以在未安装客户端环境正常打开的困扰。资源为1个docx文档,压缩包约29KB,以文字分析为主,篇幅精炼,便于快速通读和归档查阅。已有2086人学习下载,具备一定参考热度。文档围绕易赛通客户端的主进程、辅助进程与驱动模块进行结构梳理,归纳其在进程名列表与扩展名列表匹配时提供加解密服务的规律,并据此给出借助可信进程完成文件还原的解密思路,帮助读者理解驱动层Hook透明加解密的判断逻辑,为同类数据泄露防护产品的行为分析与文件恢复研究提供参考。
1. 一份打不开的 xlsx,把整条文件读写链路暴露了出来
同事把一个表格从装了管控客户端的工位拷到另一台机器,双击弹「文件已损坏」,十六进制一看,开头是几十个字节的结构化头部,后面才是看不出规律的数据块。这类数据泄露防护客户端(下文简称 DLP 客户端)做的事情其实很朴素:在文件系统过滤驱动那一层接管读写,应用层看到明文,落盘的却是密文。所以「被加密的文件怎么解回明文」这个问题,答案不在文件本身,而在「谁在读这个文件」。这篇按我拆这套客户端的顺序写:三层进程模型、策略矩阵、加密态识别脚本,最后落到怎么把这些脚本改成一个能交给 IT 做批量核查的小工具。手里压着一批打不开的历史文件、或者自家产品要做类似文件管控的,都能对着看。
2. 三层结构拆解:主程序、守护进程和文件过滤驱动各守一段
2.1 主程序:界面、登录和策略下发的入口
拆一个陌生客户端,我习惯先从安装目录的 exe 开始。主程序一般叫DLPMain.exe(32 位环境常有个不带后缀区分的同名版本),它是唯一有界面的那个:托盘图标、登录框、策略状态、告警弹窗都归它管。用户在办公机上看到的「文件已加密」提示,基本都是它弹出来的。
它的启动顺序值得记一下。开机时先起的是 Windows 服务(服务名通常带厂商前缀),服务再拉起守护进程,守护进程最后拉起主程序。这个链条意味着:你在任务管理器里结束主程序,只是关掉了一个 GUI 壳子,服务还在、驱动还在,加密照样生效。我见过太多人卡在这一步,以为「杀掉界面就解除了」,结果文件写出去还是密文,白白排查半天。
主程序另一件重要的事是策略同步。用户登录后,它把服务端的策略拉到本地做一份缓存,常见做法是落在C:\ProgramData\<厂商目录>\policy\下面,格式要么是加密的二进制,要么是带签名校验的 XML/JSON。这份缓存决定了本机此刻「哪些进程、哪些扩展名」会被文件加密流程接住。把这份缓存和驱动内存里的策略做对比,经常能发现两者不同步——策略改了但驱动没重新加载,这时候行为会很怪,属于后面避坑章要说的内容。
2.2 守护进程:互拉重启与进程保护
守护进程(DLPGuard.exe)没有界面,但它比主程序更难对付。它的职责只有两条:盯着主程序,死了就拉起来;盯着策略服务,断了就重连。实现上常见的是「双进程互拉 + Job Object 绑定」,A 进程把 B 放进自己的 Job,B 反过来也放一份,谁先退出另一方立刻重启它。
实际观察到的现象是:在任务管理器里结束主程序进程,一两秒内它会自己回来,PID 变了但窗口位置一样。结束守护进程,主程序会把守护进程拉回来。两个一起杀,Windows 服务管理器检测到服务异常退出,又会把整条链重建。所以从进程层面硬碰硬几乎没有赢面,这不是技术难度问题,是设计上就把「防卸载、防终止」当成需求写进去了。
对做二次开发的人来说,这里真正有用的信息不是「怎么杀」,而是「守护进程在保护什么」。它保护的其实是策略连续性——主程序挂了,策略更新就断了,驱动还在按旧策略跑,数据管控会出现空窗。站在产品设计角度这条逻辑是对的;站在排查角度,它意味着你看到的任何异常行为都要先确认「当前生效的策略是哪一份」。
2.3 文件过滤驱动:Hook 落在文件系统之上、缓存管理器之下
真正干活的是一层文件系统微过滤驱动(dlpflt.sys,驱动文件在System32\drivers下)。它注册时带一个 altitude 值,位置大概在文件系统和缓存管理器之间。这个位置决定了它的透明度:应用的ReadFile调用先经过缓存管理器,缓存未命中才下沉到文件系统,驱动就插在中间。
它挂的回调大致这几个:IRP_MJ_CREATE(打开文件)、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_SET_INFORMATION(改名、改大小、改时间戳)、IRP_MJ_CLEANUP(关闭句柄)。其中最关键的是CREATE的后置回调——驱动在这里拿到当前进程的映像信息,查一遍策略,然后把决策结果挂在FileObject的 stream context 上。后续这个句柄的所有读写都按 context 里记的标记走,不再重新判断。
这就是「应用层手段无能为力」的根本原因。你在用户态用任何 API、任何语言读文件,拿到的都是驱动处理完的缓冲区,中间没有可以插手的地方。文件内容在内存里短暂是明文,但你抓不到那个时机,也定位不到那块内存属于谁。想从这条路正面突破,基本是死路。
2.4 策略表:进程白名单和扩展名映射表
驱动本身不存策略,策略是主程序下发、驱动在内存里维护的两个集合:一个是进程映像集合(哪些程序被信任),一个是映射表(每个程序对应哪些扩展名需要加解密)。判定逻辑可以写成一行伪代码:if image in process_set and ext in ext_map[image]: 走加解密 else: 原样透传。
这里有两个容易忽略的细节。第一,进程身份的比对维度通常不止文件名,优先级一般是「完整映像路径 > 映像文件名 > 数字签名」,具体用哪个级别取决于厂商的取舍,路径级别最稳但兼容性差(程序一升级路径就变),文件名级别最宽松但可伪造。第二,扩展名的匹配一般不做大小写敏感,且会遇到多后缀文件(.tar.gz)取最后一段还是全段的问题,不同实现不一样,测绘时必须实测而不是猜。
3. 策略矩阵:进程身份乘扩展名,一次判定贯穿整个句柄生命周期
3.1 判定发生在 open 那一刻
很多人以为加密判断是在写数据的时候做的,其实不是。CREATE回调里就已经定好了这个句柄是「明文通道」还是「密文通道」,后面的读写只是执行。这带来一个很实际的推论:一个文件被打开之后,再换一个进程去读,读到的东西和第一个进程完全没关系,两个进程各自走各自的判定。
我在测绘的时候吃过一次亏:先用一个读取器打开文件读了一半,再用另一个读取器去读同一个文件的剩余部分,结果拼出来的数据半明半密,看着像文件损坏,其实是两段不同通道的数据被接在了一起。后来统一改成「一次打开、一次读完、一次落盘」,问题消失。
3.2 四种组合,一张表说清楚
把进程是否在白名单、扩展名是否在映射表两个维度交叉一下,结果只有四种。这张表是我后来所有排查的基准,遇到任何「为什么这个文件是明文/密文」的问题,先往表里对号:
| 进程在名单内 | 扩展名在映射表内 | 读到的内容 | 写出的内容 |
|---|---|---|---|
| 是 | 是 | 已解密 | 已加密 |
| 是 | 否 | 原样 | 原样(不加密) |
| 否 | 是 | 密文原样 | 原样(不加密) |
| 否 | 否 | 原样 | 原样 |
第三行是最容易误判的:扩展名明明在管控范围内,但因为打开它的进程不在名单里,驱动直接放行,读到的是磁盘上的原始字节,也就是密文。这种情况下报「文件损坏」是对的,因为确实是密文,只是没人给它解密。
第四行对应的是「换个扩展名就绕开」的现象,这不是漏洞,是设计使然——管控范围是按扩展名枚举的,枚举之外的东西本来就不在保护目标里。任何基于黑名单/白名单枚举的管控方案都有这个边界,区别只在于边界画得够不够细。
3.3 进程身份究竟认到哪一级
这一节是我认为整个架构里最值得讨论的地方。前面说过,进程身份比对有三个常见级别,选哪个直接决定了这套方案的强度:
- 只比映像文件名:实现最简单,兼容性最好,但同名的任意程序都会被当成可信进程。这是最弱的一档。
- 比完整映像路径:挡掉了同名替换,但程序升级换了安装目录就失效,运维成本高,实际部署里经常要靠通配符放宽,一放宽又回到第一档。
- 完整路径 + 数字签名校验:强度最高,代价是每次 open 都要做一次签名验证,性能开销不能忽略,通常要做缓存。
成熟产品一般会组合使用:白名单里存路径,命中后再校验签名,签名不匹配就降级成不信任。但很多早期版本、或者为了兼容老旧办公软件而放宽的版本,实际生效的只有文件名比对。判断自己面对的是哪一档,方法是准备两个同名不同路径的可执行文件做对照实验,看行为是否一致——这个方法在后面测绘章会展开。
3.4 这套设计的三个脆弱点,给做同类产品的人参考
第一,信任边界画在了进程身份上,而进程身份在用户态是可控量。这不是说去伪造它,而是说设计者必须意识到「进程名不是一个强身份」,把它当作唯一凭据,等于把安全强度押在了对手的自觉上。更稳的做法是把凭据下沉到驱动能独立验证的维度,比如进程的创建链、加载的模块列表、可执行文件的哈希。
第二,策略判定和文件句柄绑定的粒度比较粗。一个进程只要被信任,它读写所有映射表内扩展名的文件都畅通无阻,没有「这个进程只能读这一类文档」的细分。对于权限分级要求高的场景,应该在策略里引入数据密级维度,让判定变成三元组:进程 × 扩展名 × 密级。
第三,明文在句柄生命周期内对可信进程完全敞开。这是所有透明加密方案的共同软肋——要保证办公软件正常使用,就必须把明文交出去,交出去之后流向哪里就管不住了。想补这块,只能靠外发通道管控、剪贴板管控、截屏水印这些外围手段,指望加密层自己解决是不现实的。
4. 加密态识别与策略测绘:两个能直接跑的脚本
4.1 先判断文件到底是不是密文
拿到一批打不开的文件,第一步不是想办法解,而是确认它们属于哪种状态:真密文、传输损坏、还是本来就是个坏文件。加密后的文件有个共性——原始格式的魔数被破坏或整体后移,头部出现固定长度的结构化数据。下面这个脚本按魔数做第一轮筛查,把「格式正常」和「疑似密文」分开:
import sys from pathlib import Path # 常见办公/文档格式的魔数,命中即认为是明文 MAGIC = { b"PK\x03\x04": "zip 容器(xlsx/docx/pptx)", b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1": "OLE 复合文档(xls/doc/ppt)", b"%PDF": "PDF", b"\x89PNG\r\n\x1a\n": "PNG", b"\xff\xd8\xff": "JPEG", } def sniff(path: Path, head_len: int = 64): """读取文件头部,返回 (状态, 格式名, 头部字节)""" with open(path, "rb") as f: head = f.read(head_len) for magic, name in MAGIC.items(): if head.startswith(magic): return "plain", name, head # 魔数不匹配,可能是密文,也可能是无魔数的纯文本 return "suspect", None, head def scan(root: str): rows = [] for p in Path(root).rglob("*"): if not p.is_file(): continue try: state, kind, head = sniff(p) except OSError as e: rows.append((str(p), "read_error", str(e))) continue # 疑似密文时把前 16 字节打出来,方便后续比对固定头 detail = kind if kind else head[:16].hex() rows.append((str(p), state, detail)) return rows if __name__ == "__main__": for row in scan(sys.argv[1]): print("\t".join(row))sniff里的head_len取 64 是经验值,多数加密容器的头部元数据都在这个长度以内。MAGIC只列了办公场景最常遇到的五种,实际用的时候按你的文件类型往里加就行。scan用rglob递归,遇到读不了的文件(权限、占用)单独记一条而不是中断,这一点在处理几十万个文件时很重要。
跑完之后你会得到三类结果:plain是格式正常的,suspect里魔数不匹配的,以及read_error。对suspect的那批,把 16 字节的十六进制头横向比一下,如果几十个文件的前缀完全一致,基本可以确认是同一套容器的固定头部,这也顺带给了你解析头部长度字段的入口。
4.2 对照实验:同一个文件交给不同进程读
确认是密文之后,下一步是搞清楚「谁能读到明文」。做法是准备两个读取器,一个是被策略覆盖的办公程序或者它的无界面导出模式,另一个是自建脚本,用同样的方式读同一个文件,比对输出的哈希和长度。原始文件必须先用只读副本,不要动原件:
import hashlib import subprocess def sha256_of_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def read_via(cmd: list, path: str, timeout: int = 60): """cmd 是一个把文件内容写到 stdout 的读取器命令,末尾自动追加文件路径""" try: r = subprocess.run(cmd + [path], capture_output=True, timeout=timeout) except subprocess.TimeoutExpired: return None, 0, "timeout" if r.returncode != 0: return None, 0, r.stderr.decode(errors="ignore")[:200] return sha256_of_bytes(r.stdout), len(r.stdout), "" def compare(path: str, readers: dict): """readers 形如 {"script": ["python", "reader.py"], "office": ["soffice", "--cat"]}""" result = {} for name, cmd in readers.items(): digest, size, err = read_via(cmd, path) result[name] = {"sha256": digest, "size": size, "err": err} # 两个读取器结果不一致,说明其中一个走了加解密通道 keys = list(result) if len(keys) == 2: a, b = result[keys[0]], result[keys[1]] result["diverged"] = (a["sha256"] != b["sha256"]) return result这里read_via用子进程而不是直接在本脚本里open,原因是要让「谁打开文件」这件事可控——本脚本自己的进程身份和子进程不一样,混在一起测出来的结论是错的。compare里的diverged字段是核心判断:两个读取器输出不一致,说明至少有一方触发了驱动的转换逻辑;如果两个都读到同样的乱码,说明二者都在白名单外,得换读取器。
实际跑的时候建议准备三组对照:可信进程读密文、自建脚本读密文、可信进程读明文原件。三组数据的哈希关系一摆出来,策略到底怎么生效的就非常清楚了。
4.3 扩展名覆盖范围测绘
最后一件事是把映射表的边界摸出来。方法是同一份内容做多个副本,只改扩展名,用同一个读取器去读,看哪些被接管、哪些没有:
import shutil from pathlib import Path EXT_CANDIDATES = [".txt", ".docx", ".xlsx", ".pdf", ".csv", ".log", ".temp", ".bin", ".bak"] def build_matrix(sample: str, workdir: str, reader_cmd: list): workdir = Path(workdir) workdir.mkdir(parents=True, exist_ok=True) matrix = {} for ext in EXT_CANDIDATES: probe = workdir / f"probe{ext}" shutil.copyfile(sample, probe) # 用副本做探针,不碰原件 digest, size, err = read_via(reader_cmd, str(probe)) matrix[ext] = {"sha256": digest, "size": size, "err": err} probe.unlink(missing_ok=True) # 用完即删,避免残留 return matrixEXT_CANDIDATES里特意混进了.temp、.bin、.bak这类非办公扩展名作为对照组——如果这些也被处理,说明管控范围比预想的宽,映射表不是按办公格式枚举的。probe.unlink必须留着,探针文件本身也会被加密,留在盘上就是一堆无主密文,日后清理很麻烦。
把build_matrix的输出和 4.2 的对照结果拼起来,你会得到一张「扩展名 × 通道」的表。这张表就是后续所有工作的基准文档,建议存成 CSV 归档,换一台机器再测一遍做对比,能发现策略差异。
4.4 三种典型输出怎么读
第一种,所有扩展名都读到密文。说明你的读取器完全在名单外,得换一个可信读取器再测,否则这轮数据没有意义。
第二种,只有办公扩展名读到明文,探针扩展名全密文。这是最标准的情况,说明映射表就是办公格式枚举,边界清晰。
第三种,混合,部分扩展名明文部分密文,且规律不明显。这种通常意味着策略按用户组或者部门做了差异化下发,或者本机缓存和服务端不一致。遇到这种先把策略缓存文件的修改时间看一眼,再决定要不要等一轮策略同步重测。
5. 避坑与排查:五条在真机上踩过的记录
5.1 改了程序名,行为完全没变
现象:把自建读取器的可执行文件改成和可信进程一模一样的名字,放到同一个目录下,读文件依然拿到密文,和改之前没有任何区别。
原因:驱动比对的不只是文件名。要么用的是完整映像路径,要么加了签名校验,改个名字骗不过去。这种情况说明这套客户端在身份判定上选了较严的一档,属于设计到位,不是 bug。
解决:不要在身份伪装上继续投入时间,这条路在这套实现上走不通。转而确认自己的真实需求——如果是离职交接、历史资料归档这类场景,正确路径是走 IT 的正式导出流程,让管理员在服务端给指定文件做解密授权。技术手段能走多远,取决于策略配置有多宽,而不是取决于工具写得多巧。
5.2 明文存下去又变成密文了
现象:好不容易通过可信进程拿到了明文缓冲区,写到磁盘上,回头再打开又是乱码。用 4.1 的脚本一扫,新文件的头部又是那套固定结构。
原因:写出的那一刻,落盘的进程如果还在白名单内、扩展名还在映射表内,驱动会照样给它加密。读写是两个方向的判定,很多人只想到了读这一侧。
解决:落盘时的扩展名要选在映射表之外,这一条是原理决定的,不是技巧。但更重要的是先确认这么做是否被允许——把明文写到管控范围外,本质上就是在绕开数据保护,除非这是 IT 明确授权的操作,否则不要做。我在实际项目里遇到这个场景,一律是先跟安全团队确认,再决定落盘位置和后续的清理责任。
5.3 文件大小对不上,尾部多出一截
现象:解密后的数据和原件比,长度差了 16 字节、32 字节,或者末尾多了一段填充数据。Office 文件打开报格式错误。
原因:加密容器有自己的结构,通常是「固定头部 + 密文 + 填充/校验尾」。你拿到的缓冲区可能包含了头部和尾部,直接原样保存,对 Office 这类对 ZIP 结构严格的文件来说就是损坏。
解决:先解析头部里的长度字段,按偏移裁切。ZIP 系的文件可以用zipfile做一次结构校验,能打开说明裁切正确:
import io, zipfile def try_open_zip(data: bytes): """用 zipfile 校验缓冲区是不是完整的 zip 结构,返回成员数量""" try: with zipfile.ZipFile(io.BytesIO(data)) as z: return len(z.namelist()) except zipfile.BadZipFile: return -1返回-1就说明还有一层没处理干净,别急着往下一批文件上套,先在这一个样本上把偏移量试对。
5.4 工具跑着跑着被干掉了
现象:批量处理到几百个文件的时候,脚本突然退出,日志停在某个文件上,没有异常栈。重跑还是停在同一个位置附近。
原因:守护进程会对「异常行为」做检测,比如短时间内大量打开同类型文件、进程内存里有大块明文缓冲区。检测到之后常见处理是终止进程,有些实现还会把文件回滚到处理前的状态。
解决:这类场景本来就不该用批量脚本硬跑。合规做法是小样本、人工确认、逐批记录,每批处理后核对哈希清单。真要做量大的归档,走服务端的管理接口,让 IT 在策略层放开,比自己写脚本稳得多。
5.5 换了台机器结果全变了
现象:同一批文件、同一套脚本,在 A 机器上测出来的扩展名矩阵和 B 机器上完全不一样,两个人都说自己测的是对的。
原因:策略是按用户组或者部门下发的,两台机器登录的账号不同,拉到的策略本来就不同。还有一种情况是策略缓存没同步,本机还在跑几天前的旧策略。
解决:测绘前先记录三样东西——登录账号、策略缓存文件的修改时间、客户端的版本号。把这三个写在报告开头,后面所有结论都带着这个上下文。我现在的习惯是每个测绘结果文件第一行就是这三个字段的注释,不然一周后回来看根本不知道自己当时测的是哪套配置。
6. 二次开发:把测绘脚本做成合规的批量核查工具
前面几节的脚本都是零散的,真要交付给别人用,得包一层。我的做法是拆成三个模块:扫描器、判定器、报告器。扫描器只负责遍历目录、算哈希、调 4.1 的sniff;判定器把 4.2 和 4.3 的对照结果读进来,形成一张配置化的规则表;报告器输出 CSV 和一份人读的摘要。
改造时最值得做的一件事是把「读取器」抽象成配置项,而不是写死在代码里。用 JSON 描述每个读取器的命令模板、超时、可信状态:
{ "readers": [ {"name": "script", "cmd": ["python", "reader.py"], "trusted": false}, {"name": "office", "cmd": ["soffice", "--cat"], "trusted": true, "timeout": 120} ], "ext_candidates": [".txt", ".docx", ".xlsx", ".pdf", ".temp", ".bin"], "report": {"out_dir": "./out", "keep_probe": false} }这么改的好处是换环境不用动代码。你在 A 机器上测出的配置,拿到 B 机器上只改cmd路径就能复跑,两次报告一 diff 就知道策略差在哪。keep_probe这个开关我建议默认 false,探针文件用完必须删干净,留在盘上就是一堆没人认领的密文,日后审计对不上账。
验证方法上,我一般分三步走。第一步用三个已知状态的文件(一个明文原件、一个确认密文、一个损坏文件)跑一遍,确认三类结果都能正确分类,这是单元测试。第二步在一台机器上跑全量扫描,人工抽查十个文件的结论,看和肉眼判断是否一致。第三步换一台策略不同的机器跑同样的目录,比两份报告的差异,如果差异只出现在预期内的字段上,工具就算可信。
有个坑要提前说:别把「能不能读到明文」当成工具的核心指标。工具的价值在于把「哪些文件处于加密态、在什么条件下会被接管」这件事说清楚,输出应该是可核查的事实清单,而不是一个自动化的转换流程。前者能交给安全团队做决策依据,后者一旦被误用就是事故。这个边界我在写工具的时候就画死了:所有输出都是只读的报告,不做任何落盘转换。
从那以后我每次接手一堆打不开的历史文件,第一件事都是先做只读副本和一份哈希清单,再谈后面怎么处理。这个习惯救过我几次——有一次中途发现某批文件的状态和预期完全不符,靠哈希清单回溯才确认是副本复制环节出的问题,原件一个字节都没动。希望帮到你。
本文还有配套的精品资源,点击获取