在.NET项目集成混淆加密:自动化保护DLL和EXE

为了保护C#程序, 在.NET项目交付DLL或EXE之前,通常需要先编译,再手动打开混淆工具、选择程序集、加载配置并导出结果。对于经常发布的项目,这套操作不仅重复,还容易漏选文件或使用错误配置。

更稳定的方式是把恒盾C#混淆加密大师的CLI接入MSBuild:Release构建完成后自动获取当前项目的真实输出路径,调用CLI生成保护版本。由于Visual Studio、Rider、dotnet build和多数CI平台最终都会调用MSBuild,因此只需在项目文件中维护一套规则。

C#混淆加密大师1.5.0截图

集成后的构建流程

本文实现的流程如下:

  1. 正常编译.NET项目,不修改原始构建产物。
  2. 仅在Release配置且已经配置CLI路径时执行混淆。
  3. .deps.json.runtimeconfig.json和依赖DLL复制到protected目录。
  4. 使用--input--output覆盖配置文件中的路径,由CLI生成受保护的主程序集。
  5. CLI返回非零退出码或未生成目标文件时让构建失败,避免误发布未保护的程序。

最终目录类似下面这样:

1
2
3
4
5
6
7
8
bin/Release/net8.0/
├─ DotNetObfuscationSample.dll # 原始构建产物
├─ DotNetObfuscationSample.deps.json
├─ DotNetObfuscationSample.runtimeconfig.json
└─ protected/
├─ DotNetObfuscationSample.dll # 已混淆的主程序集
├─ DotNetObfuscationSample.deps.json
└─ DotNetObfuscationSample.runtimeconfig.json

原始目录可继续用于调试和问题定位,发布时只取protected目录即可。

第一步:准备CLI和混淆配置

安装恒盾C#混淆加密大师后,在安装目录中找到CLI.exe。然后在图形界面中配置需要启用的保护功能,通过“文件 > 导出配置”得到obfuscation.csop,并把它放在.csproj同一目录。配置文件中的ModulePathOutputPath可以保留示例值,因为构建时会通过CLI参数覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"ModulePath": "bin/Release/net8.0/MyApp.dll",
"OutputPath": "bin/Release/net8.0/protected/MyApp.dll",
"AntiILDasm": true,
"StringConfusor": true,
"IntConfusor": true,
"FieldRename": true,
"MethodRename": true,
"ParamRename": true,
"TypeRename": true,
"RenameMode": 1,
"Seed": "my-project-release",
}

实际使用时建议保留软件导出的完整配置,不要只复制上面的精简片段。可先启用少量保护选项,验证程序正常启动和主要功能后,再逐步增加保护强度。

第二步:在项目文件中加入MSBuild目标

打开需要保护的.csproj,在</Project>之前加入下面的内容:

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
38
<PropertyGroup>
<ObfuscatorCliPath Condition="'$(ObfuscatorCliPath)' == ''">$(OBFUSCATOR_CLI_PATH)</ObfuscatorCliPath>
<RunObfuscator Condition="'$(RunObfuscator)' == '' and '$(Configuration)' == 'Release' and '$(ObfuscatorCliPath)' != ''">true</RunObfuscator>
<RunObfuscator Condition="'$(RunObfuscator)' == ''">false</RunObfuscator>
<ObfuscatorConfigPath>$(MSBuildProjectDirectory)\obfuscation.csop</ObfuscatorConfigPath>
</PropertyGroup>

<Target Name="ObfuscateReleaseOutput"
AfterTargets="Build"
Condition="'$(Configuration)' == 'Release' and '$(RunObfuscator)' == 'true'">
<PropertyGroup>
<ObfuscatedOutputDir Condition="'$(ObfuscatedOutputDir)' == ''">$(TargetDir)protected\</ObfuscatedOutputDir>
<ObfuscatedOutputPath Condition="'$(ObfuscatedOutputPath)' == ''">$(ObfuscatedOutputDir)$(TargetFileName)</ObfuscatedOutputPath>
</PropertyGroup>

<Error Condition="'$(ObfuscatorCliPath)' == ''"
Text="RunObfuscator=true, but ObfuscatorCliPath or OBFUSCATOR_CLI_PATH is not configured." />
<Error Condition="!Exists('$(ObfuscatorCliPath)')"
Text="Obfuscator CLI was not found: $(ObfuscatorCliPath)" />
<Error Condition="!Exists('$(ObfuscatorConfigPath)')"
Text="Obfuscator config was not found: $(ObfuscatorConfigPath)" />

<RemoveDir Directories="$(ObfuscatedOutputDir)" />
<MakeDir Directories="$(ObfuscatedOutputDir)" />
<ItemGroup>
<ObfuscatorCompanionFile Include="$(TargetDir)**\*"
Exclude="$(ObfuscatedOutputDir)**\*;$(TargetPath)" />
</ItemGroup>
<Copy SourceFiles="@(ObfuscatorCompanionFile)"
DestinationFiles="@(ObfuscatorCompanionFile->'$(ObfuscatedOutputDir)%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />

<Message Importance="high" Text="Obfuscating $(TargetPath)" />
<Exec Command="&quot;$(ObfuscatorCliPath)&quot; obf --config &quot;$(ObfuscatorConfigPath)&quot; --input &quot;$(TargetPath)&quot; --output &quot;$(ObfuscatedOutputPath)&quot;" />
<Error Condition="!Exists('$(ObfuscatedOutputPath)')"
Text="Obfuscator did not create the expected output: $(ObfuscatedOutputPath)" />
<Message Importance="high" Text="Protected output: $(ObfuscatedOutputDir)" />
</Target>

这里使用的都是MSBuild内置属性,因此不需要硬编码项目名称或目标框架:

  • $(MSBuildProjectDirectory)是当前项目目录。
  • $(TargetPath)是本次构建产生的主程序集完整路径。
  • $(TargetFileName)会自动适配不同项目名称。
  • AfterTargets="Build"确保只有编译成功后才执行CLI。
  • Exec检查CLI退出码,随后的Error还会确认目标文件确实已经生成。

复制主程序集以外的完整输出内容,再由CLI生成受保护的主程序集,是为了保留程序运行需要的配置和依赖文件。不要只把混淆后的DLL单独交付。排除原始主程序集也能确保后续文件检查不会把未经保护的副本误判为成功结果。

第三步:配置CLI路径

不建议把个人电脑上的绝对安装路径写入.csproj。可以使用环境变量,让不同开发机和CI环境使用各自的CLI位置。

在PowerShell中为当前终端设置:

1
$env:OBFUSCATOR_CLI_PATH = 'C:\Tools\CSharpObfuscator\CLI.exe'

如需永久写入当前Windows用户的环境变量:

1
2
3
4
5
[Environment]::SetEnvironmentVariable(
'OBFUSCATOR_CLI_PATH',
'C:\Tools\CSharpObfuscator\CLI.exe',
'User'
)

永久设置后需要重新打开Visual Studio、Rider或终端。也可以在单次构建中直接传入路径:

1
dotnet build -c Release -p:ObfuscatorCliPath='C:\Tools\CSharpObfuscator\CLI.exe'

第四步:执行Release构建

在命令行中执行:

1
dotnet build -c Release

在Visual Studio中将解决方案配置切换为Release后执行“生成解决方案”,或者在Rider中构建Release配置,都会触发同一个MSBuild目标。Debug构建默认不会执行混淆。

如需临时跳过混淆,可以显式关闭开关:

1
dotnet build -c Release -p:RunObfuscator=false

构建日志中出现下面两行,表示集成目标已执行:

1
2
Obfuscating ...\bin\Release\net8.0\MyApp.dll
Protected output: ...\bin\Release\net8.0\protected\

在CI中使用

CI使用的仍然是相同命令,只需提前安装并注册CLI,再通过环境变量或MSBuild属性传入路径:

1
2
3
$env:OBFUSCATOR_CLI_PATH = "$env:OBFUSCATOR_HOME\CLI.exe"
dotnet restore
dotnet build -c Release --no-restore

建议把protected目录作为发布制品,并在上传前增加一个文件存在性检查:

1
2
3
4
$protectedFile = '.\bin\Release\net8.0\protected\MyApp.dll'
if (-not (Test-Path $protectedFile)) {
throw "未生成混淆后的程序集: $protectedFile"
}

WPF、WinForms、类库和ASP.NET Core项目

这套写法不依赖控制台项目:

  • WPF和WinForms项目的$(TargetPath)会指向其主程序集。
  • 类库项目可以直接保护生成的DLL。
  • ASP.NET Core项目应在完整发布目录中保留依赖、配置文件和静态资源,仅替换需要保护的业务程序集。
  • 一个解决方案包含多个项目时,只在真正需要保护的项目中加入目标,或者把公共目标提取到单独的.targets文件后按条件导入。

使用反射、依赖注入扫描、JSON序列化、XAML绑定、插件加载或P/Invoke时,重命名可能影响运行。应通过忽略列表保留外部依赖的名称,并对启动、登录、核心业务、配置加载和自动更新等流程进行回归测试。

dotnet publish和单文件发布注意事项

本文示例挂接在Build目标之后,适合普通DLL、EXE构建。执行dotnet publish时,发布过程还会在publish目录重新整理文件,因此不要直接把未经检查的publish目录当成混淆结果。

常规发布可先执行dotnet publish -c Release,再用CLI的--input--output处理发布目录中的主程序集。启用PublishSingleFile时,应将发布得到的单文件EXE作为CLI输入,而不是构建阶段的DLL。建议为单文件项目单独建立发布后目标,并验证解包、混淆、重构后的程序能够在干净环境启动。

完整示例代码

可以访问GitHub获取完整项目代码: https://github.com/leapever/csharp-obfuscator-cli-integration