远程调试C#程序环境的配置

远程调试适用于需要在本机调试在其他 Windows 设备上运行的程序,典型的比如调试 Surface 上的程序。

目标 Windows 主机不具备安装 Visual Studio 开发环境条件的,也可以用远程调试来进行调试。

微软MSDN官方有文章讲解如何进行远程调试,链接:Remote Debugging。

下载安装远程调试工具

根据所使用的 Visual Studio 版本下载对应的远程调试工具,VS2015 版本的下载链接在此:Remote Tools for Visual Studio 2015。

远程调试工具根据架构分别有 x86,x64 和 ARM 版本,根据开发主机和调试目标设备的架构进行下载,注意下载版本需要与机器架构一致,即 x64 的机器必须安装 x64 版本的远程调试工具,不能使用 x86 版本的(在安装过程中会提示不支持)。

在开发主机上和调试目标设备上分别安装远程调试工具。

远程调试工具配置

远程调试需要开发主机和目标设备处于同一局域网、工作组或者家庭组。远程调试不能通过 Internet 连接。

1. 目标设备配置

首先,需要设置目标设备上的远程调试器。

运行目标设备上安装好的 Remote Debugger,首次运行会提示进行配置,按照默认配置就好,主要是添加防火墙例外,另外一点是是否将远程调试配置成服务,根据需要或者参照官方文章进行配置。

另外,我们需要在目标设备上添加一个账户,用于开发主机连接到目标主机上的 Remote Debugger。打开控制面板的用户帐户,新建一个普通账户,用户名为 debug,同时设置一个密码。

点击 Remote Debugger 的菜单项 Tools/Options,默认选中了 “Windows Authentication”,点击右侧的 “Permissions…” 按钮,进入权限设置对话框。点击 “Add” 手动输入 debug 账户名,添加 debug 账户的调试权限。同时需要设置一下 “Maximum idle time” ,这个是指 Remote Debugger 运行时的最大空闲时间,如果超过这个时间,Debugger 就会自动关闭,建议设置较长时间,比如 3600 (1小时),否则可能需要频繁手动启动 Remote Debugger。

这样目标设备上的配置完成。

PS:

  • 调试 .NET 程序只能使用 “Windows Authentication”,不支持 “No Authentication” 选项。
  • 可能每次启动 Remote Debugger 都需要重新添加 “Permissions…” 账户。

2. 开发主机配置

然后,需要配置开发主机上的调试环境。

打开需要进行远程调试的工程,打开工程的 Properties 页面,选择 Debug 标签,勾选其中的 Use remote machine 选项,然后填写入目标设备的名称,这个名称可以打开目标设备的系统信息看到,VS 会自动在其后添加端口号。这里同样可以填写目标设备IP。此外确保 Working directiory 为空,下方 Enable native code debugging 未被选中。

这时候基本的远程调试就可以用了,但是我们需要在项目编译后将文件手动复制到远程主机上才能调试,这样很不方便(每次重新编译都需要重新复制)。官方文章中也没有提到如何实现自动部署。

我们可以使用 Windows 文件共享来解决这个问题。

  1. 启用开发主机上的 Guest 账户。进入控制面板/用户帐户,启用 Guest 账户,同时,进入 控制面板/系统和安全/管理工具/本地安全策略,从左侧打开 本地策略/用户权限分配,右侧找到拒绝从网络访问这台计算机,打开,然后删除掉 Guest 账户并保存。这样目标设备就可以通过局域网共享访问这台计算机的文件了。

  2. 在开发主机上新建一个 Shared 目录,右键设置共享,添加 Guest 账户为读取权限。

  3. 修改工程 Properties 页面,选择 Build 标签,修改下面 Output 的 Output path 路径为共享目录路径,注意这里的应该为 \\XXX\Shared\project_name\bin\Debug\, XXX 部分是开发主机的名称。这里不能直接使用本机的目录路径,否则目标设备无法访问。

启动项目调试,VS 会自动搜索目标设备名,如果找到则会提示输入认证用的用户名和密码,这里输入 debug 和 用户密码即可。

这样就可以开始程序调试了。

中软统一终端加密文件解密

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

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

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

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

加密解密过程

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

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

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

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

策略的漏洞

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

互联网安全相关通常都是这个问题: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 等等等,而这其中,这个系统部署和维护不够专业及时也加重了消耗。

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

添加可执行程序到 Path 中

将一个程序加入到环境 Path 中,通常做法是将程序文件所在目录附加到系统或者用户的 Path 变量中,这样我们在任何路径下的命令行中,都可以调用该程序工作。大部分应用程序安装过程中的做法也是如此。

但是这样会将目录中的所有文件暴露到 Path 中,其中大部分是我们不需要用到的。这样做也增加了潜在 DLL Hell 隐患,比如同名不同版本 DLL 暴露到 Path 中低版本覆盖了高版本导致程序运行时异常。

最好的做法是,只将我们需要用到的 exe 文件加入到 Path 中,Windows 上也的确有这样的解决方法。

以 vim 为例。我们新建一个 vim.bat 文件,编辑文件加入以下代码:

1
@"C:\Program Files (x86)\Vim\vim74\gvim.exe" %*

将该文件保存到一个目录中,然后再将该目录加入到用户或系统 Path 变量中。这样,这个 vim.bat 就暴露到 Path 中,可以在命令行中随时调用了,调用名为 vim,即 .bat 文件的文件名。

@ 表示禁止调用的回显,"" 引号包含目标程序的完整路径(这样路径中允许有空白符),%* 表示将传递给 .bat 文件的参数原封不动的传递给目标程序。

我们可以在此目录下新建其他 bat 文件,来向 Path 中加入各种所需的程序(同时也可以自定义名称)。

PS: 其实如果打开 Ruby 的安装目录,我们就会发现,irb 和 gem 都使用了这种方法。